|
@@ -1,4 +1,4 @@
|
|
|
-pub use crate::pre_serve::{BeforeServeTable, OnFailure, PreServeCommand};
|
|
|
|
|
|
|
+pub use crate::pre_serve::{BeforeServeTable, ModsTable, OnFailure, PreServeCommand};
|
|
|
use anyhow::{Context, Result};
|
|
use anyhow::{Context, Result};
|
|
|
use serde::Deserialize;
|
|
use serde::Deserialize;
|
|
|
use std::collections::HashMap;
|
|
use std::collections::HashMap;
|
|
@@ -50,6 +50,11 @@ pub struct Config {
|
|
|
pub exec_aliases: HashMap<String, String>,
|
|
pub exec_aliases: HashMap<String, String>,
|
|
|
#[serde(default)]
|
|
#[serde(default)]
|
|
|
pub home_assistant: Option<HomeAssistantConfig>,
|
|
pub home_assistant: Option<HomeAssistantConfig>,
|
|
|
|
|
+ /// Global mods directory — script files merged with recipe mods/ into
|
|
|
|
|
+ /// the container's /mods/. Recipe-level [mods] commands explicitly
|
|
|
|
|
+ /// reference files from either source via /mods/<file>.
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub global_mods_dir: Option<std::path::PathBuf>,
|
|
|
}
|
|
}
|
|
|
/// Home Assistant config for `power on`/`power off`.
|
|
/// Home Assistant config for `power on`/`power off`.
|
|
|
#[derive(Clone, Deserialize)]
|
|
#[derive(Clone, Deserialize)]
|
|
@@ -115,6 +120,10 @@ pub struct Recipe {
|
|
|
/// Recipe-level pre-serve commands (appended after cluster-level).
|
|
/// Recipe-level pre-serve commands (appended after cluster-level).
|
|
|
#[serde(default)]
|
|
#[serde(default)]
|
|
|
pub before_serve: Vec<PreServeCommand>,
|
|
pub before_serve: Vec<PreServeCommand>,
|
|
|
|
|
+ /// Mods: commands that run *inside* the container via entrypoint wrapping
|
|
|
|
|
+ /// before the engine process starts. Requires `entrypoint` to be set.
|
|
|
|
|
+ #[serde(default)]
|
|
|
|
|
+ pub mods: ModsTable,
|
|
|
}
|
|
}
|
|
|
/// Resolves cluster by exact name, then unique prefix. Errors if none or ambiguous.
|
|
/// Resolves cluster by exact name, then unique prefix. Errors if none or ambiguous.
|
|
|
fn resolve_cluster_name<'a>(clusters: &'a [Cluster], name: &str) -> Result<&'a Cluster> {
|
|
fn resolve_cluster_name<'a>(clusters: &'a [Cluster], name: &str) -> Result<&'a Cluster> {
|
|
@@ -205,6 +214,19 @@ pub fn resolve_cluster<'c>(
|
|
|
};
|
|
};
|
|
|
select_cluster(config, &choice, recipe.required_nodes)
|
|
select_cluster(config, &choice, recipe.required_nodes)
|
|
|
}
|
|
}
|
|
|
|
|
+/// Resolve the path to a recipe's `mods/` directory, if it exists.
|
|
|
|
|
+/// Checks cwd first, then the default config dir.
|
|
|
|
|
+pub async fn resolve_mods_dir(name: &str) -> Option<std::path::PathBuf> {
|
|
|
|
|
+ let cwd = std::path::PathBuf::from(format!("recipes/{name}/mods"));
|
|
|
|
|
+ if cwd.is_dir() {
|
|
|
|
|
+ return Some(cwd);
|
|
|
|
|
+ }
|
|
|
|
|
+ let cfg = default_config_dir()?
|
|
|
|
|
+ .join("recipes")
|
|
|
|
|
+ .join(name)
|
|
|
|
|
+ .join("mods");
|
|
|
|
|
+ cfg.is_dir().then_some(cfg)
|
|
|
|
|
+}
|
|
|
/// Loads a recipe (cwd first, then `<default_config_dir>/recipes/<name>/recipe.toml`,
|
|
/// Loads a recipe (cwd first, then `<default_config_dir>/recipes/<name>/recipe.toml`,
|
|
|
/// falling back to cwd for the error).
|
|
/// falling back to cwd for the error).
|
|
|
pub async fn load_recipe(recipe_name: &str) -> Result<Recipe> {
|
|
pub async fn load_recipe(recipe_name: &str) -> Result<Recipe> {
|
|
@@ -266,6 +288,7 @@ mod tests {
|
|
|
before_serve: HashMap::new(),
|
|
before_serve: HashMap::new(),
|
|
|
exec_aliases: HashMap::new(),
|
|
exec_aliases: HashMap::new(),
|
|
|
home_assistant: None,
|
|
home_assistant: None,
|
|
|
|
|
+ global_mods_dir: None,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
fn test_nccl() -> NcclConfig {
|
|
fn test_nccl() -> NcclConfig {
|
|
@@ -276,13 +299,9 @@ mod tests {
|
|
|
ib_disable: 0,
|
|
ib_disable: 0,
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
- // ── Deserialization tests ──────────────────────────────────
|
|
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_recipe_deserialize_minimal() {
|
|
fn test_recipe_deserialize_minimal() {
|
|
|
- let toml_str = r#"
|
|
|
|
|
-name = "Minimal"
|
|
|
|
|
-model = "test/model"
|
|
|
|
|
-"#;
|
|
|
|
|
|
|
+ let toml_str = "\nname = \"Minimal\"\nmodel = \"test/model\"\n";
|
|
|
let recipe: Recipe = toml::from_str(toml_str).unwrap();
|
|
let recipe: Recipe = toml::from_str(toml_str).unwrap();
|
|
|
assert_eq!(recipe.name, "Minimal");
|
|
assert_eq!(recipe.name, "Minimal");
|
|
|
assert_eq!(recipe.model, "test/model");
|
|
assert_eq!(recipe.model, "test/model");
|
|
@@ -318,7 +337,6 @@ OTHER = "123"
|
|
|
assert_eq!(recipe.port, 30000);
|
|
assert_eq!(recipe.port, 30000);
|
|
|
assert_eq!(recipe.required_nodes, Some(4));
|
|
assert_eq!(recipe.required_nodes, Some(4));
|
|
|
assert_eq!(recipe.pipeline_parallel_size, Some(2));
|
|
assert_eq!(recipe.pipeline_parallel_size, Some(2));
|
|
|
- assert_eq!(recipe.tensor_parallel_size, 1);
|
|
|
|
|
assert_eq!(recipe.distributed_backend, "ray");
|
|
assert_eq!(recipe.distributed_backend, "ray");
|
|
|
assert_eq!(recipe.cluster, Some("my-cluster".to_string()));
|
|
assert_eq!(recipe.cluster, Some("my-cluster".to_string()));
|
|
|
assert_eq!(recipe.args, vec!["--arg1", "val1", "--arg2"]);
|
|
assert_eq!(recipe.args, vec!["--arg1", "val1", "--arg2"]);
|
|
@@ -358,25 +376,15 @@ ib_disable = 0
|
|
|
assert_eq!(config.clusters.len(), 1);
|
|
assert_eq!(config.clusters.len(), 1);
|
|
|
assert_eq!(config.clusters[0].nodes.len(), 2);
|
|
assert_eq!(config.clusters[0].nodes.len(), 2);
|
|
|
assert_eq!(config.clusters[0].nodes[0].host_key, None);
|
|
assert_eq!(config.clusters[0].nodes[0].host_key, None);
|
|
|
- assert!(config.nccl.contains_key("prod"));
|
|
|
|
|
- assert_eq!(config.nccl["prod"].ib_hca, "mlx5_0");
|
|
|
|
|
|
|
+ assert!(config.nccl.contains_key("prod") && config.nccl["prod"].ib_hca == "mlx5_0");
|
|
|
}
|
|
}
|
|
|
#[test]
|
|
#[test]
|
|
|
fn test_config_deserialize_with_host_key() {
|
|
fn test_config_deserialize_with_host_key() {
|
|
|
- let toml_str = r#"
|
|
|
|
|
-ssh_user = "admin"
|
|
|
|
|
-ssh_key_path = "/path/to/key"
|
|
|
|
|
-[[clusters]]
|
|
|
|
|
-name = "secure"
|
|
|
|
|
-master_ib_ip = "1.1.1.1"
|
|
|
|
|
-nodes = [
|
|
|
|
|
- { name = "n1", ssh_ip = "2.2.2.2", ib_ip = "1.1.1.1", rank = 0, host_key = "SHA256:abc123" },
|
|
|
|
|
-]
|
|
|
|
|
-"#;
|
|
|
|
|
|
|
+ let toml_str = "\nssh_user = \"admin\"\nssh_key_path = \"/path/to/key\"\n[[clusters]]\nname = \"secure\"\nmaster_ib_ip = \"1.1.1.1\"\nnodes = [\n { name = \"n1\", ssh_ip = \"2.2.2.2\", ib_ip = \"1.1.1.1\", rank = 0, host_key = \"SHA256:abc123\" },\n]\n";
|
|
|
let config: Config = toml::from_str(toml_str).unwrap();
|
|
let config: Config = toml::from_str(toml_str).unwrap();
|
|
|
assert_eq!(
|
|
assert_eq!(
|
|
|
config.clusters[0].nodes[0].host_key,
|
|
config.clusters[0].nodes[0].host_key,
|
|
|
- Some("SHA256:abc123".to_string())
|
|
|
|
|
|
|
+ Some("SHA256:abc123".into())
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
#[test]
|
|
#[test]
|
|
@@ -391,6 +399,7 @@ nodes = [
|
|
|
before_serve: HashMap::new(),
|
|
before_serve: HashMap::new(),
|
|
|
exec_aliases: HashMap::new(),
|
|
exec_aliases: HashMap::new(),
|
|
|
home_assistant: None,
|
|
home_assistant: None,
|
|
|
|
|
+ global_mods_dir: None,
|
|
|
};
|
|
};
|
|
|
config.nccl.insert("test-cluster".to_string(), test_nccl());
|
|
config.nccl.insert("test-cluster".to_string(), test_nccl());
|
|
|
let (cluster, nccl) = select_cluster(&config, "test-cluster", None).unwrap();
|
|
let (cluster, nccl) = select_cluster(&config, "test-cluster", None).unwrap();
|
|
@@ -409,6 +418,7 @@ nodes = [
|
|
|
before_serve: HashMap::new(),
|
|
before_serve: HashMap::new(),
|
|
|
exec_aliases: HashMap::new(),
|
|
exec_aliases: HashMap::new(),
|
|
|
home_assistant: None,
|
|
home_assistant: None,
|
|
|
|
|
+ global_mods_dir: None,
|
|
|
};
|
|
};
|
|
|
config.nccl.insert("test-cluster".to_string(), test_nccl());
|
|
config.nccl.insert("test-cluster".to_string(), test_nccl());
|
|
|
let (cluster, nccl) = select_cluster(&config, "auto", None).unwrap();
|
|
let (cluster, nccl) = select_cluster(&config, "auto", None).unwrap();
|
|
@@ -475,14 +485,11 @@ nodes = [
|
|
|
let recipe: Recipe = toml::from_str("name = \"x\"\nmodel = \"m\"").unwrap();
|
|
let recipe: Recipe = toml::from_str("name = \"x\"\nmodel = \"m\"").unwrap();
|
|
|
assert_eq!(recipe.distributed_backend, "mp");
|
|
assert_eq!(recipe.distributed_backend, "mp");
|
|
|
}
|
|
}
|
|
|
- // ── Default config dir ─────────────────────────────────────
|
|
|
|
|
#[test]
|
|
#[test]
|
|
|
fn default_config_dir_appends_sm121_launcher() {
|
|
fn default_config_dir_appends_sm121_launcher() {
|
|
|
let Some(dir) = default_config_dir() else {
|
|
let Some(dir) = default_config_dir() else {
|
|
|
- // No config dir on this system; nothing to assert.
|
|
|
|
|
return;
|
|
return;
|
|
|
};
|
|
};
|
|
|
- assert!(dir.ends_with("sm121-launcher"));
|
|
|
|
|
- assert!(dir.parent().is_some());
|
|
|
|
|
|
|
+ assert!(dir.ends_with("sm121-launcher") && dir.parent().is_some());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|