Browse Source

docker: --entrypoint takes tail[0], rest is argv (no repeat); port convention 30000 in recipes + docs

Alexander Korolev 3 weeks ago
parent
commit
ad7069edcd

+ 2 - 1
.agents/skills/sm121-launcher/SKILL.md

@@ -64,7 +64,8 @@ build_profile = "profile-name"  # optional: docker-images/<dir>/profiles/<name>.
 #   MUST be a top-level key: after [[extra_mounts]] it silently lands in the
 #   last mount table (TOML context) and is ignored.
 model = "org/model"
-port = 30000
+port = 30000               # the only port convention — when adapting a foreign
+                             # recipe, drop/rewrite its port (8000/8888/…) to this default
 required_nodes = 2
 tensor_parallel_size = 2
 pipeline_parallel_size = 1

+ 7 - 1
AGENTS.md

@@ -154,6 +154,12 @@ per node instead of running it.
 
 ## Recipe format (`recipes/<name>/recipe.toml`)
 
+**Port convention: every recipe serves on the cluster default port 30000 —
+leave the `port` field unset (or `30000`). When adapting a foreign recipe
+(forum/upstream), always drop or rewrite its `port` to the default — the
+foreign port (8000/8888/…) is not used on this cluster and breaks the
+bench/smoke defaults that read the recipe port.**
+
 Recipes and their image build contexts (`docker-images/`) both live here
 and must move together — the served image must match the recipe revision.
 <https://git.kilork.org/sm121/recipes> holds only `sm121 publish` output
@@ -165,7 +171,7 @@ docker_image = "img:tag"
 model = "org/model"
 parent_model = "org/base-model"     # optional; report category (medals/tree) ranks under the base model
 archived = false                    # optional; true hides the recipe from `sm121 report` (set via `--archive`)
-port = 30000                        # default
+port = 30000                        # default — the only port convention on this cluster
 required_nodes = 2                  # optional; also caps the launch set (N nodes, not all)
 solo = false                       # optional; single-node on first free node, no dist flags
 deploy = "docker"                   # optional; "docker" (default) or "tmux"

+ 0 - 1
recipes/dsv41-flash-exl3-2x/recipe.toml

@@ -24,7 +24,6 @@ engine = "vllm"
 docker_image = "ghcr.io/coolbho3k/deepseek-v4.1-flash-exl3-3bpw-2x-dgx-spark:20260917-rc2"
 model = ""
 name = "DeepSeek-V4.1-Flash EXL3 3bpw (2x Spark, TP2/DCP2, DSpark K=3, 1M)"
-port = 8888
 required_nodes = 2
 cluster = "8nodes-switch"
 tensor_parallel_size = 2

+ 0 - 1
recipes/glm53-flash-sparkring-2x/recipe.toml

@@ -14,7 +14,6 @@ name = "GLM-5.3-Flash NVFP4-Spark (SparkRing shared img, TP2 RoCEnante profile)"
 docker_image = "ghcr.io/fujitsupolycom/sparkring:glm53-shared-8b32ef2"  # prebuilt (no docker_image_dir); the 4774a7c5 memory-guard build (2026-09-11 re-run)
 model = "local-inference-lab/GLM-5.3-Flash-NVFP4-Spark"
 parent_model = "zai-org/GLM-5.3-Flash"
-port = 8000
 required_nodes = 2
 tensor_parallel_size = 2
 pipeline_parallel_size = 1

+ 0 - 1
recipes/glm53-flash-sparkring-4x/recipe.toml

@@ -14,7 +14,6 @@ docker_image = "sparkring-shared:local"
 docker_image_dir = "sparkring-shared"
 model = "local-inference-lab/GLM-5.3-Flash-NVFP4-Spark"
 parent_model = "zai-org/GLM-5.3-Flash"
-port = 8000
 required_nodes = 4
 tensor_parallel_size = 4
 pipeline_parallel_size = 1

+ 0 - 1
recipes/qwen38-flash-next-int4/recipe.toml

@@ -31,7 +31,6 @@ docker_image = "eugr/spark-vllm-b12x:latest"
 # the PLE-table index + the b12x arch rename that the raw snapshot lacks).
 model = ""
 parent_model = "Qwen/Qwen3.8-Flash-Next"
-port = 8000
 required_nodes = 1
 
 entrypoint = ["vllm", "serve"]

+ 7 - 5
src/docker/command.rs

@@ -379,13 +379,15 @@ pub(crate) fn build_docker_command(
         // this is exactly the previous behavior (the tail runs as-is); for
         // images with their own ENTRYPOINT (e.g. the ds41 runtime's default
         // OpenAI api_server) it overrides it with the recipe's command.
-        if let Some(first) = tail.first() {
+        // The remaining tail is the entrypoint's argv (no repetition of the
+        // first token — docker appends the command after the entrypoint).
+        if let Some((first, rest)) = tail.split_first() {
             cmd.push(format!("--entrypoint {}", shq(first)));
+            cmd.push(img);
+            cmd.extend(rest.iter().map(|t| shq(t)));
+        } else {
+            cmd.push(img);
         }
-        cmd.push(img);
-        // Shell-quote every tail word (entrypoint, model, args, flags) so a
-        // crafted recipe arg/env cannot break out of the remote `docker run`.
-        cmd.extend(tail.iter().map(|t| shq(t)));
     } else {
         let script = mods::build_mod_script(&ctx.recipe.mods.commands, &tail);
         // `--entrypoint sh` and `-c` stay literal; `img` and `script` are

+ 4 - 3
src/docker/sglang_tests.rs

@@ -98,13 +98,14 @@ fn sglang_worker() {
 #[test]
 fn sglang_entrypoint_model_path_order() {
     let cmd = srun(&sglang_recipe(), &tcls().nodes[0], true);
-    let ep = cmd.find("sglang serve").unwrap();
+    let ep = cmd.find("--entrypoint sglang").unwrap();
+    let ip = cmd.find("img").unwrap();
     let mp = cmd.find("--model-path").unwrap();
     let mdl = cmd.find("org/sgl-model").unwrap();
     let tp = cmd.find("--tp 4").unwrap();
     assert!(
-        ep < mp && mp < mdl,
-        "entrypoint before model-path before model"
+        ep < ip && ip < mp && mp < mdl,
+        "entrypoint flag, then image, then model-path, then model"
     );
     assert!(mdl < tp, "model before --tp");
     let port = cmd.find("--port 8000").unwrap();

+ 3 - 3
src/docker/tests.rs

@@ -268,12 +268,12 @@ fn entrypoint() {
     let mut r = trec();
     r.entrypoint = vec!["py".into(), "-m".into(), "mod".into()];
     let cmd = run(&r, &tcls().nodes[0], true);
+    let ep = cmd.find("--entrypoint py").unwrap();
     let ip = cmd.find("img").unwrap();
     let mp = cmd.find("mdl").unwrap();
-    let ep = cmd.find("py -m mod").unwrap();
     assert!(
-        ip < ep && ep < mp,
-        "entrypoint should be between image and model"
+        ep < ip && ip < mp,
+        "entrypoint flag before image, model after image"
     );
 }