Browse Source

feat(compose): recursive whole-hierarchy symlink when no glob is set

With no `glob`, a composed_model source now symlinks the ENTIRE snapshot
recursively (every file in every subdir, as individual symlinks with real
parent dirs) instead of flattening via `*` (which made subdirs into
single symlink-to-dirs). This is more intuitive ("no glob = everything")
and fixes an overlay gap: a later source can now place individual files
at any path, including inside a subdir an earlier source provided.

With a `glob`, behavior is unchanged (shell glob, one level, `/` allowed
for a specific subdir).

Tested on live cluster (test-composed recipe): mtp-draft/, run-logs/,
vllm_overlay/ now real dirs with individual file symlinks (258 total vs
~166); overlay-into-subdir (mkdir inside composed subdir) succeeds;
recursively-symlinked files resolve inside the container.
Alexander Korolev 2 tháng trước cách đây
mục cha
commit
f9abbeb509
2 tập tin đã thay đổi với 34 bổ sung và 11 xóa
  1. 7 3
      AGENTS.md
  2. 27 8
      src/docker/compose.rs

+ 7 - 3
AGENTS.md

@@ -204,6 +204,7 @@ mount = "/root/model"               # container-side target; `model` is rewritte
 
 [[composed_model.sources]]          # symlinked into the composed dir (later wins)
 repo = "aidendle94/GLM-5.2-MXFP4-Experts-GPTQ"      # base: config, weights, tokenizer
+# (no glob → whole hierarchy symlinked recursively, incl. subdirs)
 
 [[composed_model.sources]]
 repo = "aidendle94/GLM-5.2-NF4-Lloyd-Experts-GPTQ"  # overlay: Lloyd NF4 experts
@@ -214,9 +215,12 @@ name = "nf4_layers.json"
 content = '{"layers":[3,4,...,77]}'
 ```
 
-Globs may include `/` to select files from subdirectories (e.g.
-`grids/*.json`) — the directory structure is preserved in the composed dir.
-Allowed characters: alphanumeric, `* ? . _ - /`.
+With **no glob**, the entire snapshot hierarchy is symlinked recursively
+(every file in every subdir, as individual symlinks with real parent dirs) —
+so a later source can overlay individual files at any path. With a `glob`,
+only matching entries are symlinked (one level; `/` allowed for a specific
+subdir, e.g. `grids/*.json`) and the directory structure is preserved.
+Allowed glob characters: alphanumeric, `* ? . _ - /`.
 
 The composed dir is cleaned up automatically on `sm121 stop`.
 

+ 27 - 8
src/docker/compose.rs

@@ -89,6 +89,11 @@ pub(crate) async fn build_composed_dir_on_node(
 /// Resolve a source repo's snapshot on the node and symlink its files into
 /// the composed dir. Symlink targets are container-side absolute paths
 /// (`/root/.cache/huggingface/...`) so they resolve inside the container.
+///
+/// With no `glob`, the ENTIRE snapshot hierarchy is symlinked recursively
+/// (every file in every subdir, real dirs) — so a later source can overlay
+/// individual files at any path. With a `glob`, only matching top-level (or
+/// explicit-subdir) entries are symlinked.
 async fn symlink_source(
     config: &crate::config::Config,
     node: &crate::config::Node,
@@ -98,22 +103,33 @@ async fn symlink_source(
 ) -> Result<()> {
     let model_dir = crate::config::model_cache_dir_name(&source.repo);
     let refs_path = format!("{}/hub/{}/refs/main", cache_str, model_dir);
-    let glob = source.glob.as_deref().unwrap_or("*");
+    let glob = source.glob.as_deref();
 
     // Validate the glob: only allow glob-safe characters to prevent shell
     // injection (the glob is inserted raw into the shell `for` statement).
-    if !glob
-        .chars()
-        .all(|c| c.is_ascii_alphanumeric() || "*?._-/".contains(c))
+    if let Some(g) = glob
+        && !g
+            .chars()
+            .all(|c| c.is_ascii_alphanumeric() || "*?._-/".contains(c))
     {
         anyhow::bail!(
             "composed_model source '{}': glob '{}' contains invalid characters \
              (allowed: alphanumeric, * ? . _ - /)",
             source.repo,
-            glob
+            g
         );
     }
 
+    // Enumerate: no glob → recursive `find` (whole hierarchy, every file in
+    // every subdir); glob → shell glob match (one level, `/` allowed for a
+    // specific subdir). Both feed the same per-file body below.
+    let enumerate: String = match glob {
+        None => String::from(
+            "find \"$snap\" -mindepth 1 \\( -type l -o -type f \\) | while IFS= read -r f; do",
+        ),
+        Some(g) => format!("for f in \"$snap\"/{}; do", g),
+    };
+
     // One SSH round-trip: resolve refs/main, then symlink each matching file.
     // The symlink TARGET is the CONTAINER-side path (/root/.cache/huggingface/...)
     // not the host-side path — the link is dangling on the host but resolves
@@ -127,14 +143,16 @@ async fn symlink_source(
     //
     // Relative paths are preserved: a glob like `grids/*.json` creates
     // `$host/grids/L10.json` (with the subdir), and the symlink target is
-    // `$csnap/grids/L10.json` (matching structure). `mkdir -p` creates
-    // parent dirs as needed.
+    // `$csnap/grids/L10.json` (matching structure). With no glob, `find`
+    // recurses so `mtp-draft/foo.bin` etc. are symlinked individually.
+    // `mkdir -p` creates parent dirs as needed (real dirs, not symlinks —
+    // so later sources can overlay files at any path).
     let cmd = format!(
         "ref=\"$(cat {refs_q} 2>/dev/null)\" && test -n \"$ref\" && \
          snap=\"$(printf '%s/hub/%s/snapshots/%s' {cache_q} {model_q} \"$ref\")\" && \
          test -d \"$snap\" && \
          csnap=\"$(printf '/root/.cache/huggingface/hub/%s/snapshots/%s' {model_q} \"$ref\")\" && \
-         for f in \"$snap\"/{glob}; do \
+         {enumerate} \
            [ -e \"$f\" ] || [ -L \"$f\" ] || continue; \
            rel=\"${{f#\"$snap\"/}}\"; \
            parent=\"$(dirname {host_q}/\"$rel\")\"; \
@@ -145,6 +163,7 @@ async fn symlink_source(
         cache_q = shq(cache_str),
         model_q = shq(&model_dir),
         host_q = shq(host_path),
+        enumerate = enumerate,
     );
 
     run_ssh(config, node, cmd, false).await.with_context(|| {