|
|
@@ -33,7 +33,7 @@ fn tnccl() -> NcclConfig {
|
|
|
NcclConfig {
|
|
|
ib_hca: "r".into(),
|
|
|
socket_ifname: "e".into(),
|
|
|
- ib_gid_index: 3,
|
|
|
+ ib_gid_index: Some(3),
|
|
|
ib_disable: 0,
|
|
|
gloo_socket_ifname: None,
|
|
|
ucx_net_devices: None,
|
|
|
@@ -108,6 +108,23 @@ fn run(recipe: &Recipe, node: &Node, head: bool) -> String {
|
|
|
squash(&build_docker_command(&make_dm_ctx(recipe, 2, 2, tp, false), node, head).join(" "))
|
|
|
}
|
|
|
|
|
|
+#[test]
|
|
|
+fn nccl_gid_unresolved_renders_auto() {
|
|
|
+ // A cluster [nccl] block without `ib_gid_index` (auto-detect) renders
|
|
|
+ // `NCCL_IB_GID_INDEX=auto` — the launcher resolves the real index per
|
|
|
+ // node before the docker run; `auto` only appears on the dry-run/unresolved
|
|
|
+ // path. Confirms the env is emitted and clearly marked, not dropped.
|
|
|
+ let mut nccl = tnccl();
|
|
|
+ nccl.ib_gid_index = None;
|
|
|
+ let mut ctx = make_dm_ctx(&trec(), 2, 2, 2, false);
|
|
|
+ ctx.nccl = Some(nccl);
|
|
|
+ let cmd = squash(&build_docker_command(&ctx, &tcls().nodes[0], true).join(" "));
|
|
|
+ assert!(
|
|
|
+ cmd.contains("-e NCCL_IB_GID_INDEX=auto"),
|
|
|
+ "unresolved GID should render 'auto' placeholder: {cmd}"
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
fn run_keep(recipe: &Recipe, node: &Node, head: bool) -> String {
|
|
|
let tp = if recipe.required_nodes == Some(recipe.tensor_parallel_size) {
|
|
|
2
|
|
|
@@ -241,109 +258,6 @@ fn entrypoint() {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-// ── SGLang engine tests ──────────────────────────────────────
|
|
|
-
|
|
|
-fn sglang_recipe() -> Recipe {
|
|
|
- Recipe {
|
|
|
- engine: "sglang".into(),
|
|
|
- name: "S".into(),
|
|
|
- docker_image: "sglang-img".into(),
|
|
|
- model: "org/sgl-model".into(),
|
|
|
- port: 8000,
|
|
|
- required_nodes: Some(4),
|
|
|
- pipeline_parallel_size: None,
|
|
|
- tensor_parallel_size: 4,
|
|
|
- args: RecipeArgs::List(vec!["--mem-fraction-static".into(), "0.8".into()]),
|
|
|
- env: HashMap::from([("TORCH_NCCL_ASYNC_ERROR_HANDLING".into(), "1".into())]),
|
|
|
- cluster: None,
|
|
|
- entrypoint: vec!["sglang".into(), "serve".into()],
|
|
|
- distributed_backend: Some(DistributedBackend::Mp),
|
|
|
- before_serve: vec![],
|
|
|
- after_stop: vec![],
|
|
|
- solo: None,
|
|
|
- deploy: DeployTarget::Docker,
|
|
|
- venv: None,
|
|
|
- venv_setup: VenvSetup::default(),
|
|
|
- venv_setup_script: None,
|
|
|
- mods: Default::default(),
|
|
|
- shm_size: String::new(),
|
|
|
- sync_image: true,
|
|
|
- docker_image_dir: None,
|
|
|
- extra_mounts: vec![],
|
|
|
- build: Default::default(),
|
|
|
- composed_model: None,
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-fn srun(recipe: &Recipe, node: &Node, head: bool) -> String {
|
|
|
- let tp = if recipe.required_nodes == Some(recipe.tensor_parallel_size) {
|
|
|
- 4
|
|
|
- } else {
|
|
|
- recipe.tensor_parallel_size
|
|
|
- };
|
|
|
- let mut ctx = make_dm_ctx(recipe, 4, 4, tp, false);
|
|
|
- ctx.recipe_name = "sg".to_string();
|
|
|
- squash(&build_docker_command(&ctx, node, head).join(" "))
|
|
|
-}
|
|
|
-
|
|
|
-#[test]
|
|
|
-fn sglang_host_ip() {
|
|
|
- let cmd = srun(&sglang_recipe(), &tcls().nodes[0], true);
|
|
|
- assert!(cmd.contains("-e SGLANG_WORKER_IP=1"));
|
|
|
- assert!(!cmd.contains("VLLM_HOST_IP"));
|
|
|
-}
|
|
|
-
|
|
|
-#[test]
|
|
|
-fn sglang_head() {
|
|
|
- let cmd = srun(&sglang_recipe(), &tcls().nodes[0], true);
|
|
|
- assert!(cmd.contains("--model-path"));
|
|
|
- assert!(cmd.contains("org/sgl-model"));
|
|
|
- assert!(cmd.contains("--tp 4"));
|
|
|
- assert!(cmd.contains("--nnodes 4"));
|
|
|
- assert!(cmd.contains("--node-rank 0"));
|
|
|
- assert!(cmd.contains("--dist-init-addr 1:25000"));
|
|
|
- assert!(!cmd.contains("--master-addr"));
|
|
|
- assert!(!cmd.contains("--tensor-parallel-size"));
|
|
|
- assert!(!cmd.contains("--pipeline-parallel-size"));
|
|
|
- assert!(!cmd.contains("--distributed-executor-backend"));
|
|
|
- assert!(cmd.contains("-e TORCH_NCCL_ASYNC_ERROR_HANDLING=1"));
|
|
|
- assert!(
|
|
|
- !cmd.contains("-p "),
|
|
|
- "host networking: no port mapping expected"
|
|
|
- );
|
|
|
- assert!(cmd.contains("--port 8000"));
|
|
|
- assert!(cmd.contains("-e HF_HOME=/root/.cache/huggingface"));
|
|
|
- assert!(cmd.contains("-v /home/user/.cache/huggingface:/root/.cache/huggingface"));
|
|
|
- assert!(
|
|
|
- cmd.contains("--rm"),
|
|
|
- "SGLang default --rm should be present"
|
|
|
- );
|
|
|
-}
|
|
|
-
|
|
|
-#[test]
|
|
|
-fn sglang_worker() {
|
|
|
- let cmd = srun(&sglang_recipe(), &tcls().nodes[1], false);
|
|
|
- assert!(!cmd.contains("-p 8000:8000"));
|
|
|
- assert!(cmd.contains("--node-rank 1"));
|
|
|
- assert!(cmd.contains("--port 8000"));
|
|
|
-}
|
|
|
-
|
|
|
-#[test]
|
|
|
-fn sglang_entrypoint_model_path_order() {
|
|
|
- let cmd = srun(&sglang_recipe(), &tcls().nodes[0], true);
|
|
|
- let ep = cmd.find("sglang serve").unwrap();
|
|
|
- let mp = cmd.find("--model-path").unwrap();
|
|
|
- let mdl = cmd.find("org/sgl-model").unwrap();
|
|
|
- let tp = cmd.find("--tp 4").unwrap();
|
|
|
- assert!(
|
|
|
- ep < mp && mp < mdl,
|
|
|
- "entrypoint before model-path before model"
|
|
|
- );
|
|
|
- assert!(mdl < tp, "model before --tp");
|
|
|
- let port = cmd.find("--port 8000").unwrap();
|
|
|
- assert!(tp < port, "--tp before --port");
|
|
|
-}
|
|
|
-
|
|
|
#[test]
|
|
|
fn keep_disables_rm() {
|
|
|
let cmd = run_keep(&trec(), &tcls().nodes[0], true);
|
|
|
@@ -498,3 +412,6 @@ fn entrypoint_script_non_sh_passes_through() {
|
|
|
|
|
|
#[path = "solo_tests.rs"]
|
|
|
mod solo_tests;
|
|
|
+
|
|
|
+#[path = "sglang_tests.rs"]
|
|
|
+mod sglang_tests;
|