engine_flags.rs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. // An empty model is valid for entrypoint-driven images: the image's own
  39. // entrypoint selects the model (e.g. via its MODEL_PATH env), so no
  40. // positional model is emitted (the launcher's own tail args still are).
  41. if !ctx.recipe.model.is_empty() {
  42. if is_sglang {
  43. args.push("--model-path".to_string());
  44. }
  45. args.push(ctx.recipe.model.clone());
  46. }
  47. // Recipe-specific args (e.g. --kv-cache-dtype, --max-model-len).
  48. args.extend(ctx.recipe.args.to_vec());
  49. // Distributed flags — only for multi-node runs.
  50. if ctx.nnodes > 1 {
  51. match ctx.recipe.distributed_backend {
  52. Some(DistributedBackend::Ray | DistributedBackend::Mp) if is_sglang => {
  53. args.extend(vec![
  54. "--tp".into(),
  55. ctx.tp_size.to_string(),
  56. "--nnodes".into(),
  57. ctx.nnodes.to_string(),
  58. "--node-rank".into(),
  59. node.rank.to_string(),
  60. "--dist-init-addr".into(),
  61. format!("{}:{}", ctx.master_ib_ip, ctx.init_port),
  62. ]);
  63. }
  64. Some(DistributedBackend::Ray | DistributedBackend::Mp) => {
  65. let mut fp = vec![
  66. "--tensor-parallel-size".into(),
  67. ctx.tp_size.to_string(),
  68. "--pipeline-parallel-size".into(),
  69. ctx.pp_size.to_string(),
  70. "--nnodes".into(),
  71. ctx.nnodes.to_string(),
  72. "--node-rank".into(),
  73. node.rank.to_string(),
  74. "--master-addr".into(),
  75. ctx.master_ib_ip.to_string(),
  76. "--master-port".into(),
  77. ctx.init_port.to_string(),
  78. ];
  79. if ctx.recipe.distributed_backend == Some(DistributedBackend::Ray) {
  80. fp.push("--distributed-executor-backend".into());
  81. fp.push("ray".into());
  82. }
  83. args.extend(fp);
  84. }
  85. _ => {}
  86. }
  87. }
  88. // vLLM workers in distributed mode run headless (no port binding).
  89. if ctx.nnodes > 1 && !is_sglang && node.rank > 0 && is_distributed {
  90. args.push("--headless".into());
  91. }
  92. args
  93. }
  94. #[cfg(test)]
  95. mod tests {
  96. use super::*;
  97. use crate::config::DeployTarget;
  98. use crate::pre_serve::{RecipeArgs, VenvSetup};
  99. use std::collections::HashMap;
  100. fn base_recipe() -> Recipe {
  101. Recipe {
  102. build_profile: None,
  103. parent_model: None,
  104. archived: false,
  105. engine: "vllm".into(),
  106. name: "T".into(),
  107. docker_image: "img".into(),
  108. model: "mdl".into(),
  109. port: 8000,
  110. required_nodes: None,
  111. pipeline_parallel_size: Some(1),
  112. tensor_parallel_size: 1,
  113. args: RecipeArgs::List(vec!["--x".into(), "4096".into()]),
  114. env: HashMap::new(),
  115. cluster: None,
  116. entrypoint: vec!["vllm".into(), "serve".into()],
  117. distributed_backend: Some(DistributedBackend::Mp),
  118. before_serve: vec![],
  119. after_stop: vec![],
  120. solo: None,
  121. deploy: DeployTarget::Docker,
  122. venv: None,
  123. venv_setup: VenvSetup::default(),
  124. venv_setup_script: None,
  125. mods: Default::default(),
  126. shm_size: String::new(),
  127. devices: vec![],
  128. memory_limit: None,
  129. cpus: None,
  130. sync_image: true,
  131. docker_image_dir: None,
  132. extra_mounts: vec![],
  133. extra_mounts_manifest: None,
  134. build: Default::default(),
  135. composed_model: None,
  136. staging: vec![],
  137. }
  138. }
  139. fn tnode(rank: u32) -> Node {
  140. Node {
  141. name: format!("n{}", rank),
  142. ssh_ip: "10.0.0.1".into(),
  143. ib_ip: Some("10.0.0.1".into()),
  144. rank,
  145. host_key: None,
  146. entity: None,
  147. }
  148. }
  149. #[test]
  150. fn single_node_no_distributed_flags() {
  151. let r = base_recipe();
  152. let ctx = EngineFlagsCtx {
  153. recipe: &r,
  154. tp_size: 1,
  155. pp_size: 1,
  156. nnodes: 1,
  157. init_port: 25000,
  158. master_ib_ip: "10.0.0.1",
  159. };
  160. let args = build_engine_args(&ctx, &tnode(0));
  161. // model + recipe args, no distributed flags
  162. assert_eq!(args, vec!["mdl", "--x", "4096"]);
  163. }
  164. #[test]
  165. fn multi_node_vllm_mp_flags() {
  166. let r = base_recipe();
  167. let ctx = EngineFlagsCtx {
  168. recipe: &r,
  169. tp_size: 2,
  170. pp_size: 1,
  171. nnodes: 2,
  172. init_port: 25000,
  173. master_ib_ip: "10.0.0.1",
  174. };
  175. let head_args = build_engine_args(&ctx, &tnode(0));
  176. assert!(head_args.contains(&"--tensor-parallel-size".into()));
  177. assert!(head_args.contains(&"2".into()));
  178. assert!(head_args.contains(&"--nnodes".into()));
  179. assert!(head_args.contains(&"2".into()));
  180. assert!(head_args.contains(&"--master-addr".into()));
  181. assert!(head_args.contains(&"10.0.0.1".into()));
  182. assert!(head_args.contains(&"--master-port".into()));
  183. assert!(head_args.contains(&"25000".into()));
  184. // head node: no --headless
  185. assert!(!head_args.contains(&"--headless".into()));
  186. let worker_args = build_engine_args(&ctx, &tnode(1));
  187. assert!(worker_args.contains(&"--headless".into()));
  188. }
  189. #[test]
  190. fn multi_node_sglang_flags() {
  191. let mut r = base_recipe();
  192. r.engine = "sglang".into();
  193. let ctx = EngineFlagsCtx {
  194. recipe: &r,
  195. tp_size: 2,
  196. pp_size: 1,
  197. nnodes: 2,
  198. init_port: 25000,
  199. master_ib_ip: "10.0.0.1",
  200. };
  201. let args = build_engine_args(&ctx, &tnode(0));
  202. // SGLang uses --model-path, --tp, --dist-init-addr
  203. assert!(args.contains(&"--model-path".into()));
  204. assert!(args.contains(&"--tp".into()));
  205. assert!(args.contains(&"--dist-init-addr".into()));
  206. assert!(args.contains(&"10.0.0.1:25000".into()));
  207. // SGLang never gets --headless
  208. assert!(!args.contains(&"--headless".into()));
  209. }
  210. #[test]
  211. fn ray_backend_adds_executor() {
  212. let mut r = base_recipe();
  213. r.distributed_backend = Some(DistributedBackend::Ray);
  214. let ctx = EngineFlagsCtx {
  215. recipe: &r,
  216. tp_size: 1,
  217. pp_size: 1,
  218. nnodes: 2,
  219. init_port: 25000,
  220. master_ib_ip: "10.0.0.1",
  221. };
  222. let args = build_engine_args(&ctx, &tnode(0));
  223. assert!(args.contains(&"--distributed-executor-backend".into()));
  224. assert!(args.contains(&"ray".into()));
  225. }
  226. }