Jelajahi Sumber

feat(compose): support subdirectory globs in composed_model sources

Globs like `grids/*.json` now work: the validator allows `/`, and the
symlink loop preserves the relative path (mkdir -p parent dirs, symlink
target uses $csnap/$rel) instead of flattening via `basename`.

Also fixes a latent shq-in-double-quotes bug: {host_q} was wrapped in
`"..."`, making shq's single-quote wrapping literal characters in the
path — every ln/dirname call failed silently, producing zero symlinks.
{host_q} is now bare with only "$rel" quoted.

Tested on live cluster with test-composed recipe (grids/*.json source):
grids/ subdir created with 75 L*.json symlinks resolving correctly
inside the container.
Alexander Korolev 2 bulan lalu
induk
melakukan
0208fc12f1
3 mengubah file dengan 20 tambahan dan 4 penghapusan
  1. 5 1
      AGENTS.md
  2. 4 0
      recipes/test-composed/recipe.toml
  3. 11 3
      src/docker/compose.rs

+ 5 - 1
AGENTS.md

@@ -207,13 +207,17 @@ repo = "aidendle94/GLM-5.2-MXFP4-Experts-GPTQ"      # base: config, weights, tok
 
 [[composed_model.sources]]
 repo = "aidendle94/GLM-5.2-NF4-Lloyd-Experts-GPTQ"  # overlay: Lloyd NF4 experts
-glob = "L*.safetensors"                             # only expert weight files
+glob = "L*.safetensors"                             # only expert weight files (glob, may include / for subdirs)
 
 [[composed_model.files]]            # generated real files (not symlinks)
 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, `* ? . _ - /`.
+
 The composed dir is cleaned up automatically on `sm121 stop`.
 
 ## Config fields

+ 4 - 0
recipes/test-composed/recipe.toml

@@ -35,6 +35,10 @@ repo = "aidendle94/GLM-5.2-MXFP4-Experts-GPTQ"
 repo = "aidendle94/GLM-5.2-NF4-Lloyd-Experts-GPTQ"
 glob = "L*.safetensors"
 
+[[composed_model.sources]]
+repo = "aidendle94/GLM-5.2-NF4-Lloyd-Experts-GPTQ"
+glob = "grids/*.json"
+
 [[composed_model.files]]
 name = "nf4_layers.json"
 content = '{"layers":[3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77]}'

+ 11 - 3
src/docker/compose.rs

@@ -104,11 +104,11 @@ async fn symlink_source(
     // injection (the glob is inserted raw into the shell `for` statement).
     if !glob
         .chars()
-        .all(|c| c.is_ascii_alphanumeric() || "*?._-".contains(c))
+        .all(|c| c.is_ascii_alphanumeric() || "*?._-/".contains(c))
     {
         anyhow::bail!(
             "composed_model source '{}': glob '{}' contains invalid characters \
-             (allowed: alphanumeric, * ? . _ -)",
+             (allowed: alphanumeric, * ? . _ - /)",
             source.repo,
             glob
         );
@@ -124,6 +124,11 @@ async fn symlink_source(
     // Paths are assembled via `printf` (not embedded in double-quoted
     // assignments) so shq's single-quote wrapping doesn't become literal
     // characters in the path — same pattern as resolve_path_ref_on_node.
+    //
+    // 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.
     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\")\" && \
@@ -131,7 +136,10 @@ async fn symlink_source(
          csnap=\"$(printf '/root/.cache/huggingface/hub/%s/snapshots/%s' {model_q} \"$ref\")\" && \
          for f in \"$snap\"/{glob}; do \
            [ -e \"$f\" ] || [ -L \"$f\" ] || continue; \
-           ln -snf \"$csnap/$(basename \"$f\")\" {host_q}/; \
+           rel=\"${{f#\"$snap\"/}}\"; \
+           parent=\"$(dirname {host_q}/\"$rel\")\"; \
+           mkdir -p \"$parent\" && \
+           ln -snf \"$csnap/$rel\" {host_q}/\"$rel\"; \
          done",
         refs_q = shq(&refs_path),
         cache_q = shq(cache_str),