solo_tests.rs 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. //! Solo / single-node flag-emission tests — split from `tests.rs` to keep
  2. //! that file under the 500-line limit. Uses the parent module's `trec`,
  3. //! `tcls`, `make_dm_ctx`, and `build_docker_command` helpers.
  4. use super::*;
  5. use crate::config::Recipe;
  6. #[test]
  7. fn single_node_run_omits_distributed_flags() {
  8. // A solo or required_nodes=1 run must NOT emit --nnodes/--tp/--master-addr
  9. // etc. — the engine defaults to single-node. (Regression for the bug
  10. // where a 1-node recipe on a multi-node cluster launched N containers each
  11. // told `--nnodes N --tp N`.)
  12. let mut r = trec();
  13. r.required_nodes = Some(1);
  14. r.tensor_parallel_size = 1;
  15. r.pipeline_parallel_size = Some(1);
  16. // nnodes=1 is what start_cluster computes after trimming to required_nodes.
  17. let ctx = make_dm_ctx(&r, 1, 1, 1, false);
  18. let cmd = build_docker_command(&ctx, &tcls().nodes[0], true).join(" ");
  19. assert!(
  20. !cmd.contains("--nnodes"),
  21. "single-node must not pass --nnodes: {cmd}"
  22. );
  23. assert!(
  24. !cmd.contains("--tensor-parallel-size"),
  25. "single-node must not pass --tp: {cmd}"
  26. );
  27. assert!(
  28. !cmd.contains("--pipeline-parallel-size"),
  29. "single-node must not pass --pp: {cmd}"
  30. );
  31. assert!(
  32. !cmd.contains("--master-addr"),
  33. "single-node must not pass --master-addr: {cmd}"
  34. );
  35. assert!(
  36. !cmd.contains("--master-port"),
  37. "single-node must not pass --master-port: {cmd}"
  38. );
  39. assert!(
  40. !cmd.contains("--node-rank"),
  41. "single-node must not pass --node-rank: {cmd}"
  42. );
  43. assert!(
  44. !cmd.contains("--headless"),
  45. "single-node must not pass --headless: {cmd}"
  46. );
  47. // But the engine still gets --host/--port.
  48. assert!(cmd.contains("--host"));
  49. assert!(cmd.contains("--port"));
  50. }
  51. #[test]
  52. fn multi_node_run_still_emits_distributed_flags() {
  53. // Regression guard: the single-node fix must not break the multi-node path.
  54. let r = trec(); // required_nodes=2, tp collapses to nnodes=2
  55. let ctx = make_dm_ctx(&r, 2, 2, 2, false);
  56. let cmd = squash(&build_docker_command(&ctx, &tcls().nodes[0], true).join(" "));
  57. assert!(cmd.contains("--nnodes 2"));
  58. assert!(cmd.contains("--tensor-parallel-size 2"));
  59. assert!(cmd.contains("--pipeline-parallel-size 2"));
  60. assert!(cmd.contains("--master-addr"));
  61. assert!(cmd.contains("--master-port"));
  62. assert!(cmd.contains("--node-rank 0"));
  63. }
  64. #[test]
  65. fn solo_worker_node_emits_headless_no_rendezvous() {
  66. // Multi-node: a worker (rank>0) gets --headless; single-node never does.
  67. let r = trec();
  68. let ctx = make_dm_ctx(&r, 2, 2, 2, false);
  69. let worker_cmd = build_docker_command(&ctx, &tcls().nodes[1], false).join(" ");
  70. assert!(
  71. worker_cmd.contains("--headless"),
  72. "multi-node worker needs --headless: {worker_cmd}"
  73. );
  74. // Single-node worker (n/a really, but guard): headless never appears.
  75. let mut r1 = r.clone();
  76. r1.required_nodes = Some(1);
  77. let ctx1 = make_dm_ctx(&r1, 1, 1, 1, false);
  78. let solo_cmd = build_docker_command(&ctx1, &tcls().nodes[1], false).join(" ");
  79. assert!(!solo_cmd.contains("--headless"));
  80. }
  81. #[test]
  82. fn recipe_solo_field_deserializes() {
  83. let toml_str = r#"
  84. name = "Solo"
  85. model = "org/m"
  86. solo = true
  87. required_nodes = 4
  88. "#;
  89. let r: Recipe = toml::from_str(toml_str).unwrap();
  90. assert_eq!(r.solo, Some(true));
  91. // unset → None (default multi-node)
  92. let toml2 = r#"
  93. name = "Multi"
  94. model = "org/m"
  95. required_nodes = 4
  96. "#;
  97. let r2: Recipe = toml::from_str(toml2).unwrap();
  98. assert_eq!(r2.solo, None);
  99. }