engine_flags.rs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. //! Shared engine-specific argument builder — used by both the Docker
  2. //! (`docker/tail.rs`) and tmux (`tmux/commands.rs`) deployment paths to
  3. //! produce the model, recipe args, and distributed-rendezvous flags.
  4. //!
  5. //! This consolidates the duplicated flag logic that previously lived in
  6. //! `tail.rs` and `commands.rs` separately. Each deployment path still
  7. //! adds its own entrypoint and host/port flags on top.
  8. use crate::config::{DistributedBackend, Node, Recipe};
  9. /// Context for [`build_engine_args`] — the subset of `DockerCommandCtx`
  10. /// / `TmuxManager` state that the shared flag builder needs.
  11. pub struct EngineFlagsCtx<'a> {
  12. pub recipe: &'a Recipe,
  13. pub tp_size: u32,
  14. pub pp_size: u32,
  15. pub nnodes: u32,
  16. pub init_port: u16,
  17. pub master_ib_ip: &'a str,
  18. }
  19. /// Build the engine-specific arguments for a recipe: model path, recipe
  20. /// args, and distributed-rendezvous flags.
  21. ///
  22. /// Does **not** include the entrypoint, `--host`, or `--port` — those are
  23. /// deployment-specific and added by each path:
  24. /// - Docker (`tail.rs`): always adds `--host 0.0.0.0 --port <port>`.
  25. /// - tmux (`commands.rs`): adds `--port <port>` on the head node (or all
  26. /// nodes for SGLang).
  27. ///
  28. /// Single-node runs emit **no** distributed flags — the engine defaults
  29. /// to single-node. Multi-node runs get the full rendezvous flag set.
  30. pub fn build_engine_args(ctx: &EngineFlagsCtx<'_>, node: &Node) -> Vec<String> {
  31. let is_sglang = ctx.recipe.engine == "sglang";
  32. let is_distributed = matches!(
  33. ctx.recipe.distributed_backend,
  34. Some(DistributedBackend::Mp | DistributedBackend::Ray)
  35. );
  36. let mut args = Vec::new();
  37. // Model — SGLang uses --model-path, vLLM uses the bare model arg.
  38. if is_sglang {
  39. args.push("--model-path".to_string());
  40. }
  41. args.push(ctx.recipe.model.clone());
  42. // Recipe-specific args (e.g. --kv-cache-dtype, --max-model-len).
  43. args.extend(ctx.recipe.args.to_vec());
  44. // Distributed flags — only for multi-node runs.
  45. if ctx.nnodes > 1 {
  46. match ctx.recipe.distributed_backend {
  47. Some(DistributedBackend::Ray | DistributedBackend::Mp) if is_sglang => {
  48. args.extend(vec![
  49. "--tp".into(),
  50. ctx.tp_size.to_string(),
  51. "--nnodes".into(),
  52. ctx.nnodes.to_string(),
  53. "--node-rank".into(),
  54. node.rank.to_string(),
  55. "--dist-init-addr".into(),
  56. format!("{}:{}", ctx.master_ib_ip, ctx.init_port),
  57. ]);
  58. }
  59. Some(DistributedBackend::Ray | DistributedBackend::Mp) => {
  60. let mut fp = vec![
  61. "--tensor-parallel-size".into(),
  62. ctx.tp_size.to_string(),
  63. "--pipeline-parallel-size".into(),
  64. ctx.pp_size.to_string(),
  65. "--nnodes".into(),
  66. ctx.nnodes.to_string(),
  67. "--node-rank".into(),
  68. node.rank.to_string(),
  69. "--master-addr".into(),
  70. ctx.master_ib_ip.to_string(),
  71. "--master-port".into(),
  72. ctx.init_port.to_string(),
  73. ];
  74. if ctx.recipe.distributed_backend == Some(DistributedBackend::Ray) {
  75. fp.push("--distributed-executor-backend".into());
  76. fp.push("ray".into());
  77. }
  78. args.extend(fp);
  79. }
  80. _ => {}
  81. }
  82. }
  83. // vLLM workers in distributed mode run headless (no port binding).
  84. if ctx.nnodes > 1 && !is_sglang && node.rank > 0 && is_distributed {
  85. args.push("--headless".into());
  86. }
  87. args
  88. }
  89. #[cfg(test)]
  90. mod tests {
  91. use super::*;
  92. use crate::config::{DeployTarget, RecipeArgs, VenvSetup};
  93. use std::collections::HashMap;
  94. fn base_recipe() -> Recipe {
  95. Recipe {
  96. engine: "vllm".into(),
  97. name: "T".into(),
  98. docker_image: "img".into(),
  99. model: "mdl".into(),
  100. port: 8000,
  101. required_nodes: None,
  102. pipeline_parallel_size: Some(1),
  103. tensor_parallel_size: 1,
  104. args: RecipeArgs::List(vec!["--x".into(), "4096".into()]),
  105. env: HashMap::new(),
  106. cluster: None,
  107. entrypoint: vec!["vllm".into(), "serve".into()],
  108. distributed_backend: Some(DistributedBackend::Mp),
  109. before_serve: vec![],
  110. after_stop: vec![],
  111. solo: None,
  112. deploy: DeployTarget::Docker,
  113. venv: None,
  114. venv_setup: VenvSetup::default(),
  115. venv_setup_script: None,
  116. mods: Default::default(),
  117. shm_size: String::new(),
  118. sync_image: true,
  119. docker_image_dir: None,
  120. extra_mounts: vec![],
  121. build: Default::default(),
  122. composed_model: None,
  123. }
  124. }
  125. fn tnode(rank: u32) -> Node {
  126. Node {
  127. name: format!("n{}", rank),
  128. ssh_ip: "10.0.0.1".into(),
  129. ib_ip: Some("10.0.0.1".into()),
  130. rank,
  131. host_key: None,
  132. entity: None,
  133. }
  134. }
  135. #[test]
  136. fn single_node_no_distributed_flags() {
  137. let r = base_recipe();
  138. let ctx = EngineFlagsCtx {
  139. recipe: &r,
  140. tp_size: 1,
  141. pp_size: 1,
  142. nnodes: 1,
  143. init_port: 25000,
  144. master_ib_ip: "10.0.0.1",
  145. };
  146. let args = build_engine_args(&ctx, &tnode(0));
  147. // model + recipe args, no distributed flags
  148. assert_eq!(args, vec!["mdl", "--x", "4096"]);
  149. }
  150. #[test]
  151. fn multi_node_vllm_mp_flags() {
  152. let r = base_recipe();
  153. let ctx = EngineFlagsCtx {
  154. recipe: &r,
  155. tp_size: 2,
  156. pp_size: 1,
  157. nnodes: 2,
  158. init_port: 25000,
  159. master_ib_ip: "10.0.0.1",
  160. };
  161. let head_args = build_engine_args(&ctx, &tnode(0));
  162. assert!(head_args.contains(&"--tensor-parallel-size".into()));
  163. assert!(head_args.contains(&"2".into()));
  164. assert!(head_args.contains(&"--nnodes".into()));
  165. assert!(head_args.contains(&"2".into()));
  166. assert!(head_args.contains(&"--master-addr".into()));
  167. assert!(head_args.contains(&"10.0.0.1".into()));
  168. assert!(head_args.contains(&"--master-port".into()));
  169. assert!(head_args.contains(&"25000".into()));
  170. // head node: no --headless
  171. assert!(!head_args.contains(&"--headless".into()));
  172. let worker_args = build_engine_args(&ctx, &tnode(1));
  173. assert!(worker_args.contains(&"--headless".into()));
  174. }
  175. #[test]
  176. fn multi_node_sglang_flags() {
  177. let mut r = base_recipe();
  178. r.engine = "sglang".into();
  179. let ctx = EngineFlagsCtx {
  180. recipe: &r,
  181. tp_size: 2,
  182. pp_size: 1,
  183. nnodes: 2,
  184. init_port: 25000,
  185. master_ib_ip: "10.0.0.1",
  186. };
  187. let args = build_engine_args(&ctx, &tnode(0));
  188. // SGLang uses --model-path, --tp, --dist-init-addr
  189. assert!(args.contains(&"--model-path".into()));
  190. assert!(args.contains(&"--tp".into()));
  191. assert!(args.contains(&"--dist-init-addr".into()));
  192. assert!(args.contains(&"10.0.0.1:25000".into()));
  193. // SGLang never gets --headless
  194. assert!(!args.contains(&"--headless".into()));
  195. }
  196. #[test]
  197. fn ray_backend_adds_executor() {
  198. let mut r = base_recipe();
  199. r.distributed_backend = Some(DistributedBackend::Ray);
  200. let ctx = EngineFlagsCtx {
  201. recipe: &r,
  202. tp_size: 1,
  203. pp_size: 1,
  204. nnodes: 2,
  205. init_port: 25000,
  206. master_ib_ip: "10.0.0.1",
  207. };
  208. let args = build_engine_args(&ctx, &tnode(0));
  209. assert!(args.contains(&"--distributed-executor-backend".into()));
  210. assert!(args.contains(&"ray".into()));
  211. }
  212. }