|
|
@@ -0,0 +1,104 @@
|
|
|
+//! Solo / single-node flag-emission tests — split from `tests.rs` to keep
|
|
|
+//! that file under the 500-line limit. Uses the parent module's `trec`,
|
|
|
+//! `tcls`, `make_dm_ctx`, and `build_docker_command` helpers.
|
|
|
+
|
|
|
+use super::*;
|
|
|
+use crate::config::Recipe;
|
|
|
+
|
|
|
+#[test]
|
|
|
+fn single_node_run_omits_distributed_flags() {
|
|
|
+ // A solo or required_nodes=1 run must NOT emit --nnodes/--tp/--master-addr
|
|
|
+ // etc. — the engine defaults to single-node. (Regression for the bug
|
|
|
+ // where a 1-node recipe on a multi-node cluster launched N containers each
|
|
|
+ // told `--nnodes N --tp N`.)
|
|
|
+ let mut r = trec();
|
|
|
+ r.required_nodes = Some(1);
|
|
|
+ r.tensor_parallel_size = 1;
|
|
|
+ r.pipeline_parallel_size = Some(1);
|
|
|
+ // nnodes=1 is what start_cluster computes after trimming to required_nodes.
|
|
|
+ let ctx = make_dm_ctx(&r, 1, 1, 1, false);
|
|
|
+ let cmd = build_docker_command(&ctx, &tcls().nodes[0], true).join(" ");
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--nnodes"),
|
|
|
+ "single-node must not pass --nnodes: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--tensor-parallel-size"),
|
|
|
+ "single-node must not pass --tp: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--pipeline-parallel-size"),
|
|
|
+ "single-node must not pass --pp: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--master-addr"),
|
|
|
+ "single-node must not pass --master-addr: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--master-port"),
|
|
|
+ "single-node must not pass --master-port: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--node-rank"),
|
|
|
+ "single-node must not pass --node-rank: {cmd}"
|
|
|
+ );
|
|
|
+ assert!(
|
|
|
+ !cmd.contains("--headless"),
|
|
|
+ "single-node must not pass --headless: {cmd}"
|
|
|
+ );
|
|
|
+ // But the engine still gets --host/--port.
|
|
|
+ assert!(cmd.contains("--host"));
|
|
|
+ assert!(cmd.contains("--port"));
|
|
|
+}
|
|
|
+
|
|
|
+#[test]
|
|
|
+fn multi_node_run_still_emits_distributed_flags() {
|
|
|
+ // Regression guard: the single-node fix must not break the multi-node path.
|
|
|
+ let r = trec(); // required_nodes=2, tp collapses to nnodes=2
|
|
|
+ let ctx = make_dm_ctx(&r, 2, 2, 2, false);
|
|
|
+ let cmd = build_docker_command(&ctx, &tcls().nodes[0], true).join(" ");
|
|
|
+ assert!(cmd.contains("--nnodes 2"));
|
|
|
+ assert!(cmd.contains("--tensor-parallel-size 2"));
|
|
|
+ assert!(cmd.contains("--pipeline-parallel-size 2"));
|
|
|
+ assert!(cmd.contains("--master-addr"));
|
|
|
+ assert!(cmd.contains("--master-port"));
|
|
|
+ assert!(cmd.contains("--node-rank 0"));
|
|
|
+}
|
|
|
+
|
|
|
+#[test]
|
|
|
+fn solo_worker_node_emits_headless_no_rendezvous() {
|
|
|
+ // Multi-node: a worker (rank>0) gets --headless; single-node never does.
|
|
|
+ let r = trec();
|
|
|
+ let ctx = make_dm_ctx(&r, 2, 2, 2, false);
|
|
|
+ let worker_cmd = build_docker_command(&ctx, &tcls().nodes[1], false).join(" ");
|
|
|
+ assert!(
|
|
|
+ worker_cmd.contains("--headless"),
|
|
|
+ "multi-node worker needs --headless: {worker_cmd}"
|
|
|
+ );
|
|
|
+ // Single-node worker (n/a really, but guard): headless never appears.
|
|
|
+ let mut r1 = r.clone();
|
|
|
+ r1.required_nodes = Some(1);
|
|
|
+ let ctx1 = make_dm_ctx(&r1, 1, 1, 1, false);
|
|
|
+ let solo_cmd = build_docker_command(&ctx1, &tcls().nodes[1], false).join(" ");
|
|
|
+ assert!(!solo_cmd.contains("--headless"));
|
|
|
+}
|
|
|
+
|
|
|
+#[test]
|
|
|
+fn recipe_solo_field_deserializes() {
|
|
|
+ let toml_str = r#"
|
|
|
+name = "Solo"
|
|
|
+model = "org/m"
|
|
|
+solo = true
|
|
|
+required_nodes = 4
|
|
|
+"#;
|
|
|
+ let r: Recipe = toml::from_str(toml_str).unwrap();
|
|
|
+ assert_eq!(r.solo, Some(true));
|
|
|
+ // unset → None (default multi-node)
|
|
|
+ let toml2 = r#"
|
|
|
+name = "Multi"
|
|
|
+model = "org/m"
|
|
|
+required_nodes = 4
|
|
|
+"#;
|
|
|
+ let r2: Recipe = toml::from_str(toml2).unwrap();
|
|
|
+ assert_eq!(r2.solo, None);
|
|
|
+}
|