| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104 |
- //! 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 = squash(&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);
- }
|