浏览代码

de-pin model revisions: kindling 2x/4x model -> floating org/model id (engine resolves via NAS refs/main -> latest snapshot, same as every other recipe; the pinned da920bb0 stays as an informational comment); 6x notes its tp6pad source rev. Bench manifest now records the model's resolved refs/main snapshot so a floating model stays reproducible (backward-compatible #[serde(default)]).

Alexander Korolev 3 周之前
父节点
当前提交
24d6737bc3

+ 5 - 1
recipes/glm53-kindling-2x/recipe.toml

@@ -29,7 +29,11 @@ docker_image = "spark-glm53:v9"
 # shared image build dir (all three TP variants build the same image)
 docker_image_dir = "glm53-kindling"
 entrypoint = ["vllm", "serve"]
-model = "/root/.cache/huggingface/hub/models--nvidia--GLM-5.3-Flash-NVFP4/snapshots/da920bb0b9f4a06727223a349e55468e38352348"
+# Floating model: the engine resolves the org/model id via the NAS HF cache
+# (refs/main → latest snapshot), so a model re-upload is picked up without
+# touching the recipe. The NAS copy stood at snapshot da920bb0 on 2026-10-02
+# (informational only; the path follows refs/main).
+model = "nvidia/GLM-5.3-Flash-NVFP4"
 parent_model = "nvidia/GLM-5.3-Flash-NVFP4"
 port = 30000
 required_nodes = 2

+ 5 - 1
recipes/glm53-kindling-4x/recipe.toml

@@ -26,7 +26,11 @@ docker_image = "spark-glm53:v9"
 # shared image build dir (all three TP variants build the same image)
 docker_image_dir = "glm53-kindling"
 entrypoint = ["vllm", "serve"]
-model = "/root/.cache/huggingface/hub/models--nvidia--GLM-5.3-Flash-NVFP4/snapshots/da920bb0b9f4a06727223a349e55468e38352348"
+# Floating model: the engine resolves the org/model id via the NAS HF cache
+# (refs/main → latest snapshot), so a model re-upload is picked up without
+# touching the recipe. The NAS copy stood at snapshot da920bb0 on 2026-10-02
+# (the 94/100 campaign) — informational only; the path follows refs/main.
+model = "nvidia/GLM-5.3-Flash-NVFP4"
 parent_model = "nvidia/GLM-5.3-Flash-NVFP4"
 port = 30000
 required_nodes = 4

+ 3 - 0
recipes/glm53-kindling-6x/recipe.toml

@@ -35,6 +35,9 @@ docker_image = "spark-glm53:v9"
 # shared image build dir (all three TP variants build the same image)
 docker_image_dir = "glm53-kindling"
 entrypoint = ["vllm", "serve"]
+# Local zero-padded checkpoint (a stable artifact, not a revision pin) — the
+# tp6pad tree is padded from snapshot da920bb0 (see bootstrap above); re-pad
+# from a new snapshot if the source model is re-uploaded.
 model = "/root/.cache/huggingface/glm-5.3-flash-nvfp4-tp6pad"
 parent_model = "nvidia/GLM-5.3-Flash-NVFP4"
 port = 30000

+ 75 - 3
src/bench_artifacts.rs

@@ -7,9 +7,11 @@
 //!   serve was launched from, so the run's recipe is pinned even after
 //!   the recipe (or its image pins) move on.
 //! - `<stem>.manifest.json` — provenance: recipe + config SHA-256, the
-//!   launcher's git commit, the bench tool's version, and the served
-//!   image's identity per node (docker id + `sm121.checksum` build
-//!   label, captured over SSH at bench time).
+//!   launcher's git commit, the bench tool's version, the served model's
+//!   resolved `refs/main` snapshot (the model reference floats, so the
+//!   snapshot pins the run), and the served image's identity per node
+//!   (docker id + `sm121.checksum` build label, captured over SSH at bench
+//!   time).
 //!
 //! `sm121 report` reads the manifests back (see [`crate::report`]) and
 //! shows each run's provenance — and flags a run whose recipe has since
@@ -110,6 +112,19 @@ pub struct ManifestBench {
     pub points: Option<String>,
 }
 
+/// Provenance of the served model. The recipe's `model` reference floats
+/// (the engine follows the HF cache's `refs/main` → latest snapshot), so the
+/// snapshot it actually resolved to at bench time is what makes a run
+/// reproducible. `snapshot` is null when `model` is a local path (no
+/// `org/name` id to resolve) or the repo isn't cached on the head node.
+#[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
+pub struct ManifestModel {
+    /// The recipe's `model` field (an `org/name` repo id or a local path).
+    pub id: String,
+    /// The `refs/main` snapshot hash the id resolved to on the head node.
+    pub snapshot: Option<String>,
+}
+
 /// One per bench run; the top-level shape of `<stem>.manifest.json`.
 #[derive(serde::Serialize, serde::Deserialize, Default, Debug, Clone)]
 pub struct Manifest {
@@ -128,6 +143,11 @@ pub struct Manifest {
     /// { "id", "sm121_checksum" } } }`. Nodes without the image are
     /// absent from `nodes`.
     pub images: serde_json::Map<String, serde_json::Value>,
+    /// The recipe `model` id and the `refs/main` snapshot it resolved to
+    /// at bench time (the model reference floats; the snapshot pins the run).
+    /// Absent in pre-model manifests (defaults to empty).
+    #[serde(default)]
+    pub models: Vec<ManifestModel>,
 }
 
 /// `docker image inspect` the served image on every cluster node and
@@ -159,6 +179,31 @@ pub async fn capture_images(config: &Config, cluster: &Cluster, image: &str) ->
     serde_json::json!({ "build_checksum": build_checksum, "nodes": nodes })
 }
 
+/// Resolve the recipe `model` id to its `refs/main` snapshot on the head
+/// node (the shared-NAS HF cache). Returns the snapshot hash, or `None`
+/// when the model is a local path (no `org/name` id to resolve) or the repo
+/// isn't cached there. Only `org/name` repo ids carry a `refs/main`.
+async fn capture_model_snapshot(
+    config: &Config,
+    node: &crate::config::Node,
+    model: &str,
+) -> Option<String> {
+    if model.starts_with('/') || model.starts_with('~') || !model.contains('/') {
+        return None;
+    }
+    let model_dir = crate::config::model_cache_dir_name(model);
+    let hf_cache = config.resolved_hf_cache_dir();
+    let refs_path = format!("{}/hub/{}/refs/main", hf_cache.display(), model_dir);
+    let cmd = format!("cat {} 2>/dev/null", crate::docker::shq(&refs_path));
+    let out = run_ssh(config, node, cmd, false).await.ok()?;
+    let snap = out.trim();
+    if snap.is_empty() {
+        None
+    } else {
+        Some(snap.to_string())
+    }
+}
+
 /// Inputs for [`write_run_package`] (bundled: the signature would
 /// otherwise trip `too_many_arguments`).
 pub struct RunPackage<'a> {
@@ -217,6 +262,18 @@ pub async fn write_run_package(p: &RunPackage<'_>) -> Result<()> {
         );
     }
 
+    // The model reference floats (engine follows refs/main); capture the
+    // snapshot it resolved to on the head node so the run is reproducible.
+    let mut models: Vec<ManifestModel> = Vec::new();
+    if !recipe.model.is_empty()
+        && let Some(head) = cluster.nodes.first()
+    {
+        models.push(ManifestModel {
+            id: recipe.model.clone(),
+            snapshot: capture_model_snapshot(config, head, &recipe.model).await,
+        });
+    }
+
     let m = Manifest {
         schema_version: 1,
         run: stem.to_string(),
@@ -248,6 +305,7 @@ pub async fn write_run_package(p: &RunPackage<'_>) -> Result<()> {
             points,
         },
         images,
+        models,
     };
     let manifest_path = reports_dir.join(format!("{stem}.manifest.json"));
     fs::write(
@@ -294,6 +352,20 @@ mod tests {
         assert!(s.ends_with('Z'));
     }
 
+    #[test]
+    fn old_manifest_without_models_deserializes() {
+        // Pre-model manifests lack the `models` field; `#[serde(default)]`
+        // keeps `sm121 report` able to read them.
+        let json = r#"{"schema_version":1,"run":"r","created_utc":"2026-01-01T00:00:00Z",
+            "recipe":{"name":"X","file":"f","sha256":"s"},
+            "cluster":{"name":"c","nodes":[]},
+            "launcher":{"bin":"b","git_sha":null},
+            "bench":{"tool":"tool-eval-bench","version":"1","args":[],"final_score":null,"points":null},
+            "images":{}}"#;
+        let m: Manifest = serde_json::from_str(json).unwrap();
+        assert!(m.models.is_empty());
+    }
+
     #[test]
     fn run_package_writes_copy_and_manifest() {
         let dir = std::env::temp_dir().join(format!("sm121-artifacts-{}-x", std::process::id()));