Procházet zdrojové kódy

docker: --entrypoint override for entrypoint-driven images; trim cluster to launch set before hooks/sync; recipe.cluster on auto

Alexander Korolev před 3 týdny
rodič
revize
3bcc7760a0

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

@@ -28,6 +28,7 @@ port = 8888
 required_nodes = 2
 cluster = "8nodes-switch"
 tensor_parallel_size = 2
+pipeline_parallel_size = 1
 distributed_backend = "mp"
 shm_size = "2g"
 devices = ["/dev/dri/card0"]

+ 7 - 0
src/docker/command.rs

@@ -375,6 +375,13 @@ pub(crate) fn build_docker_command(
     let tail = tail::build_tail(&tctx, node);
     let img = shq(&ctx.recipe.docker_image);
     if ctx.recipe.mods.commands.is_empty() {
+        // `--entrypoint <first tail token>`: for images without an ENTRYPOINT
+        // 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() {
+            cmd.push(format!("--entrypoint {}", shq(first)));
+        }
         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`.

+ 14 - 0
src/docker/command_tests.rs

@@ -368,3 +368,17 @@ fn no_memory_limit_no_flags() {
     assert!(!cmd.contains("--memory"));
     assert!(!cmd.contains("--cpus"));
 }
+
+#[test]
+fn entrypoint_flag_overrides_image_entrypoint() {
+    let mut r = base_recipe();
+    r.entrypoint = vec![
+        "/opt/ds41-venv/bin/python".into(),
+        "/opt/ds41-serving/serve.py".into(),
+    ];
+    let cmd = run_with(&r);
+    assert!(
+        cmd.contains("--entrypoint '/opt/ds41-venv/bin/python'"),
+        "in: {cmd}"
+    );
+}

+ 12 - 6
src/docker/mod.rs

@@ -71,8 +71,6 @@ pub async fn serve(
     let mut recipe = load_recipe(recipe_name, &config.recipe_sources).await?;
     let rn = recipe.name.clone();
     let re = recipe.engine.clone();
-    // CLI `--cluster auto` (the default) falls back to the recipe's `cluster`
-    // field — same behavior as the tmux path.
     let cluster_name = if cluster_name == "auto" {
         recipe.cluster.clone().unwrap_or_else(|| "auto".to_string())
     } else {
@@ -185,6 +183,15 @@ pub async fn serve(
             cluster.name, nn, nn
         );
     }
+    // Trim to the launch set (the `required_nodes` cap) BEFORE the pre-serve
+    // hooks and image sync so every phase targets exactly the nodes the recipe
+    // launches on, not every free node in the cluster. `start_cluster`'s trim
+    // becomes a no-op afterwards.
+    if let Some(n) = recipe.required_nodes
+        && (n as usize) < cluster.nodes.len()
+    {
+        cluster.nodes.truncate(n as usize);
+    }
     crate::before_serve::run(
         config,
         &cluster,
@@ -194,10 +201,9 @@ pub async fn serve(
     )
     .await?;
 
-    // `built_on` records the node that (re)built the image this serve: after
-    // a fresh build that image is authoritative — the sync step below must
-    // pull FROM it, never let a peer's older image (by creation timestamp)
-    // overwrite the new build.
+    // `built_on` records the node that (re)built the image: the sync step
+    // below must pull FROM it, never let a peer's older image overwrite the
+    // new build.
     let mut built_on: Option<String> = None;
     if let Some(ref build_node_name) = opts.build_node {
         if !opts.dry_run && !cluster.nodes.iter().any(|n| n.name == *build_node_name) {