|
|
@@ -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()));
|