|
|
@@ -293,208 +293,5 @@ pub(crate) fn build_docker_command(
|
|
|
}
|
|
|
|
|
|
#[cfg(test)]
|
|
|
-mod tests {
|
|
|
- use super::*;
|
|
|
- use crate::config::{
|
|
|
- Cluster, DeployTarget, DistributedBackend, ExtraMount, NcclConfig, Node, Recipe, RecipeArgs,
|
|
|
- };
|
|
|
- use std::collections::HashMap;
|
|
|
-
|
|
|
- fn tcls() -> Cluster {
|
|
|
- Cluster {
|
|
|
- name: "t".into(),
|
|
|
- master_ib_ip: "1".into(),
|
|
|
- nodes: vec![Node {
|
|
|
- name: "n0".into(),
|
|
|
- ssh_ip: "10.0.0.1".into(),
|
|
|
- ib_ip: "1".into(),
|
|
|
- rank: 0,
|
|
|
- host_key: None,
|
|
|
- entity: None,
|
|
|
- }],
|
|
|
- extra_entities: vec![],
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- fn tnccl() -> NcclConfig {
|
|
|
- NcclConfig {
|
|
|
- ib_hca: "r".into(),
|
|
|
- socket_ifname: "e".into(),
|
|
|
- ib_gid_index: 3,
|
|
|
- ib_disable: 0,
|
|
|
- gloo_socket_ifname: None,
|
|
|
- ucx_net_devices: None,
|
|
|
- nccl_cross_nic: None,
|
|
|
- nccl_proto: None,
|
|
|
- nccl_p2p_level: None,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- fn minimal_config() -> Config {
|
|
|
- Config {
|
|
|
- ssh_user: "u".into(),
|
|
|
- ssh_key_path: "/tmp/key".into(),
|
|
|
- clusters: vec![],
|
|
|
- nccl: HashMap::new(),
|
|
|
- hf_cache_dir: None,
|
|
|
- init_port: 25000,
|
|
|
- before_serve: HashMap::new(),
|
|
|
- exec_aliases: HashMap::new(),
|
|
|
- home_assistant: None,
|
|
|
- global_mods_dir: None,
|
|
|
- drop_caches: true,
|
|
|
- mount_aliases: HashMap::from([("models".to_string(), "/mnt/nas/models".to_string())]),
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- fn run_with(recipe: &Recipe) -> String {
|
|
|
- let resolved = resolve_extra_mounts(&minimal_config(), recipe).unwrap();
|
|
|
- let ctx = DockerCommandCtx {
|
|
|
- cluster: tcls(),
|
|
|
- nccl: tnccl(),
|
|
|
- recipe: recipe.clone(),
|
|
|
- recipe_name: "r".to_string(),
|
|
|
- pp_size: 2,
|
|
|
- tp_size: 1,
|
|
|
- nnodes: 2,
|
|
|
- cache_dir: "/home/user/.cache/huggingface".to_string(),
|
|
|
- keep: false,
|
|
|
- init_port: 25000,
|
|
|
- mods_remote_path: String::new(),
|
|
|
- extra_mounts: resolved.static_mounts,
|
|
|
- };
|
|
|
- build_docker_command(&ctx, &tcls().nodes[0], true).join(" ")
|
|
|
- }
|
|
|
-
|
|
|
- fn base_recipe() -> Recipe {
|
|
|
- Recipe {
|
|
|
- engine: "vllm".into(),
|
|
|
- name: "T".into(),
|
|
|
- docker_image: "img".into(),
|
|
|
- model: "mdl".into(),
|
|
|
- port: 8000,
|
|
|
- required_nodes: Some(2),
|
|
|
- pipeline_parallel_size: Some(2),
|
|
|
- tensor_parallel_size: 1,
|
|
|
- args: RecipeArgs::List(vec!["--x".into(), "4096".into()]),
|
|
|
- env: HashMap::new(),
|
|
|
- cluster: None,
|
|
|
- entrypoint: vec![],
|
|
|
- distributed_backend: Some(DistributedBackend::Mp),
|
|
|
- before_serve: vec![],
|
|
|
- after_stop: vec![],
|
|
|
- solo: None,
|
|
|
- deploy: DeployTarget::Docker,
|
|
|
- venv: None,
|
|
|
- mods: Default::default(),
|
|
|
- shm_size: String::new(),
|
|
|
- sync_image: true,
|
|
|
- docker_image_dir: None,
|
|
|
- extra_mounts: vec![],
|
|
|
- build: Default::default(),
|
|
|
- composed_model: None,
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn extra_mounts_are_mounted() {
|
|
|
- let mut r = base_recipe();
|
|
|
- r.extra_mounts.push(ExtraMount {
|
|
|
- alias: Some("models".into()),
|
|
|
- host: Some("~/.cache/models".into()),
|
|
|
- repo: None,
|
|
|
- subfolder: None,
|
|
|
- container: "/models".into(),
|
|
|
- read_only: true,
|
|
|
- env: Some("MODEL_DIR".into()),
|
|
|
- });
|
|
|
- let cmd = run_with(&r);
|
|
|
- // alias present in minimal_config() → alias host wins
|
|
|
- assert!(cmd.contains("-v /mnt/nas/models:/models:ro"));
|
|
|
- assert!(cmd.contains("-v /home/user/.cache/huggingface:/root/.cache/huggingface"));
|
|
|
- // env injected pointing at the container path, before recipe env
|
|
|
- assert!(cmd.contains("-e MODEL_DIR=/models"));
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn extra_mounts_fallback_to_host_when_alias_absent() {
|
|
|
- let mut r = base_recipe();
|
|
|
- r.extra_mounts.push(ExtraMount {
|
|
|
- alias: Some("no_such_alias".into()),
|
|
|
- host: Some("~/.cache/jit".into()),
|
|
|
- repo: None,
|
|
|
- subfolder: None,
|
|
|
- container: "/root/.cache/jit".into(),
|
|
|
- read_only: false,
|
|
|
- env: None,
|
|
|
- });
|
|
|
- let cmd = run_with(&r);
|
|
|
- // alias not in config → host default used with ~ expansion (ssh_user=u)
|
|
|
- assert!(cmd.contains("-v /home/u/.cache/jit:/root/.cache/jit"));
|
|
|
- assert!(!cmd.contains(":ro"));
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn model_path_ref_is_partitioned_not_mounted() {
|
|
|
- // repo-based mounts are path references, NOT bind-mounts. They are
|
|
|
- // partitioned into path_refs and resolved per-node via substitution.
|
|
|
- let mut r = base_recipe();
|
|
|
- r.extra_mounts.push(ExtraMount {
|
|
|
- alias: None,
|
|
|
- host: None,
|
|
|
- repo: Some("aidendle94/GLM-5.2-Hybrid-FP8-MXFP4".into()),
|
|
|
- subfolder: Some("mtp-draft".into()),
|
|
|
- container: "mtp_draft".into(),
|
|
|
- read_only: true,
|
|
|
- env: None,
|
|
|
- });
|
|
|
- let resolved = resolve_extra_mounts(&minimal_config(), &r).unwrap();
|
|
|
- assert_eq!(resolved.path_refs.len(), 1);
|
|
|
- assert_eq!(resolved.static_mounts.len(), 0);
|
|
|
- let pr = &resolved.path_refs[0];
|
|
|
- assert_eq!(pr.repo, "aidendle94/GLM-5.2-Hybrid-FP8-MXFP4");
|
|
|
- assert_eq!(pr.subfolder.as_deref(), Some("mtp-draft"));
|
|
|
- assert_eq!(pr.name, "mtp_draft");
|
|
|
- // No -v flag for path refs in build_docker_command (substitution only)
|
|
|
- let cmd = run_with(&r);
|
|
|
- assert!(!cmd.contains("-v /models/mtp-draft"));
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn substitute_path_refs_replaces_placeholders() {
|
|
|
- let mut r = base_recipe();
|
|
|
- r.args = RecipeArgs::List(vec![
|
|
|
- "--speculative-config".into(),
|
|
|
- "{\"model\":\"{{mtp_draft}}\"}".into(),
|
|
|
- ]);
|
|
|
- let path_refs = vec![(
|
|
|
- "mtp_draft".to_string(),
|
|
|
- "/root/.cache/huggingface/hub/models--aidendle94--GLM-5.2-Hybrid-FP8-MXPF4/snapshots/bb0d0858/mtp-draft".to_string(),
|
|
|
- )];
|
|
|
- let substituted = substitute_path_refs(&r, &path_refs);
|
|
|
- if let RecipeArgs::List(args) = &substituted.args {
|
|
|
- assert_eq!(
|
|
|
- args[1],
|
|
|
- "{\"model\":\"/root/.cache/huggingface/hub/models--aidendle94--GLM-5.2-Hybrid-FP8-MXPF4/snapshots/bb0d0858/mtp-draft\"}"
|
|
|
- );
|
|
|
- } else {
|
|
|
- panic!("expected list args");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- #[test]
|
|
|
- fn substitute_path_refs_in_env() {
|
|
|
- let mut r = base_recipe();
|
|
|
- r.env
|
|
|
- .insert("MTP_PATH".to_string(), "{{mtp_draft}}".to_string());
|
|
|
- let path_refs = vec![(
|
|
|
- "mtp_draft".to_string(),
|
|
|
- "/root/.cache/huggingface/hub/models--aidendle94--GLM-5.2-Hybrid-FP8-MXPF4/snapshots/bb0d0858/mtp-draft".to_string(),
|
|
|
- )];
|
|
|
- let substituted = substitute_path_refs(&r, &path_refs);
|
|
|
- assert_eq!(
|
|
|
- substituted.env["MTP_PATH"],
|
|
|
- "/root/.cache/huggingface/hub/models--aidendle94--GLM-5.2-Hybrid-FP8-MXPF4/snapshots/bb0d0858/mtp-draft"
|
|
|
- );
|
|
|
- }
|
|
|
-}
|
|
|
+#[path = "command_tests.rs"]
|
|
|
+mod tests;
|