recipe.rs 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. //! The `Recipe` type (parsed from `recipes/<name>/recipe.toml`) and
  2. //! [`EnvValue`] — the `[env]` value type: a literal string, or a reference
  3. //! into a recipe field (`{ from = "port" }`) resolved at serve time.
  4. use crate::config::types::{
  5. ComposedModel, DeployTarget, ExtraMount, RecipeBuild, default_engine, default_port,
  6. default_shm_size, default_sync_image, default_tensor_parallel_size,
  7. };
  8. use crate::pre_serve::{DistributedBackend, ModsTable, PreServeCommand, RecipeArgs, VenvSetup};
  9. use anyhow::{Result, bail};
  10. use serde::Deserialize;
  11. use std::collections::HashMap;
  12. /// An `[env]` entry value: either a literal string, or a reference into a
  13. /// recipe field resolved at serve time (`{ from = "port" }`). Untagged so
  14. /// both forms mix freely in one `[env]` table.
  15. #[derive(Clone, Debug, PartialEq, Deserialize)]
  16. #[serde(untagged)]
  17. pub enum EnvValue {
  18. Plain(String),
  19. Ref { from: String },
  20. }
  21. impl From<String> for EnvValue {
  22. fn from(s: String) -> Self {
  23. EnvValue::Plain(s)
  24. }
  25. }
  26. impl From<&str> for EnvValue {
  27. fn from(s: &str) -> Self {
  28. EnvValue::Plain(s.to_string())
  29. }
  30. }
  31. impl EnvValue {
  32. /// Resolve to a literal value. A `Ref` entry pulls its value from a
  33. /// recipe field (`from = "port"` → the recipe's `port`), so the value is
  34. /// declared once instead of being duplicated between `port = ...` and
  35. /// `[env]`. Unknown targets are an error listing the supported ones.
  36. pub fn resolve(&self, key: &str, recipe: &Recipe) -> Result<String> {
  37. match self {
  38. EnvValue::Plain(s) => Ok(s.clone()),
  39. EnvValue::Ref { from } => match from.as_str() {
  40. "port" => Ok(recipe.port.to_string()),
  41. _ => {
  42. bail!("recipe [env] '{key}': unknown 'from' target '{from}' (supported: port)")
  43. }
  44. },
  45. }
  46. }
  47. }
  48. #[derive(Clone, Deserialize)]
  49. pub struct Recipe {
  50. #[serde(default = "default_engine")]
  51. pub engine: String,
  52. pub name: String,
  53. #[serde(default)]
  54. pub docker_image: String,
  55. #[serde(default)]
  56. pub model: String,
  57. /// Parent model this recipe's category ranks under (medals, tree
  58. /// grouping). Optional — defaults to `model` itself. Set it when a
  59. /// quantized/forked repo (`local-inference-lab/GLM-5.3-Flash-NVFP4-Spark`)
  60. /// should compete inside its base model's category
  61. /// (`zai-org/GLM-5.3-Flash`).
  62. #[serde(default)]
  63. pub parent_model: Option<String>,
  64. #[serde(default = "default_port")]
  65. pub port: u16,
  66. #[serde(default)]
  67. pub required_nodes: Option<u32>,
  68. #[serde(default)]
  69. pub pipeline_parallel_size: Option<u32>,
  70. #[serde(default = "default_tensor_parallel_size")]
  71. pub tensor_parallel_size: u32,
  72. #[serde(default)]
  73. pub args: RecipeArgs,
  74. #[serde(default)]
  75. pub env: HashMap<String, EnvValue>,
  76. #[serde(default)]
  77. pub cluster: Option<String>,
  78. #[serde(default)]
  79. pub entrypoint: Vec<String>,
  80. pub distributed_backend: Option<DistributedBackend>,
  81. #[serde(default = "default_shm_size")]
  82. pub shm_size: String,
  83. #[serde(default)]
  84. pub before_serve: Vec<PreServeCommand>,
  85. /// Commands run per node **after** stopping containers (`sm121 stop`);
  86. /// same shape as [`before_serve`] (raw shell or `Script` aliases),
  87. /// parallel across nodes. E.g. `after_stop = [{ script =
  88. /// "restore_dgx_services" }]`.
  89. #[serde(default)]
  90. pub after_stop: Vec<PreServeCommand>,
  91. /// Whether this recipe should always be served single-node on the first
  92. /// free node found (solo mode). When set, `sm121 serve` ignores
  93. /// `required_nodes` / multi-node cluster selection and launches exactly
  94. /// one container with no distributed flags. The CLI `--solo`/`--no-solo`
  95. /// flag overrides this per-invocation.
  96. #[serde(default)]
  97. pub solo: Option<bool>,
  98. /// Deployment target — `docker` (default) or `tmux`. When `tmux`, the
  99. /// `serve` command launches via detached `tmux` sessions instead of
  100. /// `docker run`. Docker-only features (mods, composed_model, image
  101. /// sync) are rejected in tmux mode.
  102. #[serde(default)]
  103. pub deploy: DeployTarget,
  104. /// Python virtualenv path for tmux deployment. When set, the tmux
  105. /// launch command prepends `source <venv>/bin/activate &&` before
  106. /// the entrypoint, so the engine runs from the specified venv instead
  107. /// of the system Python. Docker-only (ignored in Docker mode — the
  108. /// container has its own Python).
  109. ///
  110. /// Example: `venv = "~/venvs/vllm025"`
  111. #[serde(default)]
  112. pub venv: Option<String>,
  113. /// Shell commands to create and initialize the venv at `venv` if it
  114. /// does not already exist on a node. Either an inline multi-line string
  115. /// (one command per line; blank lines and `#` comments are skipped) or
  116. /// a `Vec<String>` list. Only used in tmux deployment mode.
  117. ///
  118. /// The launcher checks `test -f <venv>/bin/activate` on each node
  119. /// before serve; if absent, it runs these commands in order, then
  120. /// re-checks. A failure aborts the serve.
  121. ///
  122. /// Example (inline): `uv venv ~/venvs/vllm025 -p 3.12 && uv pip install ...`
  123. /// Example (list): `["uv venv ~/venvs/vllm025 -p 3.12", "..."]`
  124. #[serde(default)]
  125. pub venv_setup: VenvSetup,
  126. /// Path to a shell script (relative to the recipe directory) that creates
  127. /// and initializes the venv. When set, the launcher reads the file from
  128. /// `recipes/<recipe_name>/<venv_setup_script>` and runs it on each node
  129. /// if the venv doesn't exist. Mutually exclusive with `venv_setup` — if
  130. /// both are set, `venv_setup_script` takes precedence.
  131. ///
  132. /// Example: `venv_setup_script = "venv_setup.sh"`
  133. #[serde(default)]
  134. pub venv_setup_script: Option<String>,
  135. #[serde(default)]
  136. pub mods: ModsTable,
  137. #[serde(default = "default_sync_image")]
  138. pub sync_image: bool,
  139. /// Build directory for docker-images/ (default: derived from recipe name).
  140. #[serde(default)]
  141. pub docker_image_dir: Option<String>,
  142. /// Recipe-driven `versions.env` overrides merged at build time (recipe
  143. /// wins, after the profile) and folded into the build checksum, so a pin
  144. /// change here triggers a rebuild without editing the image tree. Keys
  145. /// must match the `versions.env` names the `Dockerfile` declares as ARG.
  146. #[serde(default)]
  147. pub build: RecipeBuild,
  148. /// Named version profile: `docker-images/<dir>/profiles/<name>.env` (same
  149. /// `KEY=VAL` format as `versions.env`). Merged over the image's pins
  150. /// (profile wins) then recipe `[build.versions]` (recipe wins) before
  151. /// build, folded into the build checksum. Lets a recipe reference a full
  152. /// pin set (an engine generation: vLLM + b12x + LMCache cut together) by
  153. /// name instead of copy-pasting every pin. Docker-only.
  154. #[serde(default)]
  155. pub build_profile: Option<String>,
  156. /// When set, the launcher builds a /tmp composed-model dir per-node
  157. /// (symlinks into the HF cache + generated files), bind-mounts it at
  158. /// `mount`, and rewrites `model` to that path. Lets a recipe combine
  159. /// multiple HF repos (e.g. a base checkpoint + an expert overlay)
  160. /// without mutating the HF cache. See [`ComposedModel`].
  161. ///
  162. /// Docker-only: rejected when `deploy = "tmux"`.
  163. #[serde(default)]
  164. pub composed_model: Option<ComposedModel>,
  165. /// Extra bind-mounts for `docker run` (e.g. persistent JIT cache). Host
  166. /// path resolves from `Config.mount_aliases` when `alias` is set (alias
  167. /// wins); otherwise falls back to `host` (`~` expanded). One of the two
  168. /// is required.
  169. #[serde(default)]
  170. pub extra_mounts: Vec<ExtraMount>,
  171. }
  172. impl Recipe {
  173. /// All `[env]` entries resolved to literal values — `Ref` entries pull
  174. /// their value from the recipe's own fields (fail-fast on unknown
  175. /// targets). Docker `-e` and tmux `export` use this.
  176. pub fn resolved_env(&self) -> Result<HashMap<String, String>> {
  177. let mut out = HashMap::new();
  178. for (k, v) in &self.env {
  179. out.insert(k.clone(), v.resolve(k, self)?);
  180. }
  181. Ok(out)
  182. }
  183. }