// SGLang serve command tests. Moved out of `docker/tests.rs` so the parent // module stays under the ≤500-line gate. Helpers (`tcls`, `make_dm_ctx`, // `squash`, ...) are in `super`. use super::*; fn sglang_recipe() -> Recipe { Recipe { build_profile: None, parent_model: None, archived: false, 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(), devices: vec![], memory_limit: None, cpus: None, sync_image: true, docker_image_dir: None, extra_mounts: vec![], extra_mounts_manifest: None, build: Default::default(), composed_model: None, staging: vec![], } } 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).unwrap().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("--entrypoint sglang").unwrap(); let ip = cmd.find("img").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 < ip && ip < mp && mp < mdl, "entrypoint flag, then image, then model-path, then model" ); assert!(mdl < tp, "model before --tp"); let port = cmd.find("--port 8000").unwrap(); assert!(tp < port, "--tp before --port"); }