diff --git a/mesh/SPEC.md b/mesh/SPEC.md index 338b7e1c4..399354aac 100644 --- a/mesh/SPEC.md +++ b/mesh/SPEC.md @@ -225,6 +225,14 @@ decision from a warm one. > probabilities of a published decision model differ between `fp16` and `fp32` > even where the selected answer agrees, so a receipt that omits precision does > not reproduce. +> +> The fields are `weights_digest` and `precision` (§6.1), both OPTIONAL. A +> backend that can address its weights **SHOULD** report both; one that cannot +> omits them, and the receipt then binds the model by `recipe` alone — a name, +> which an operator can re-point at other weights while every receipt sealed +> under it goes on verifying. That is the weaker basis, and the absence of the +> fields is how a verifier can tell it apart from the stronger one, rather than +> having to assume. --- @@ -309,7 +317,9 @@ and nulls; booleans are encoded as `0|1` uints and absent options dropped. | `mesh_receipt` | `"mesh/1"` | | `request_id` | hex content id of the request | | `grant_id` | hex JCP grant id this inference debited (omitted if ungranted) | -| `backend`, `recipe` | the backend and composed model that served it | +| `backend`, `recipe` | the backend and composed model that served it — `recipe` is a name, read it with the two fields below | +| `weights_digest` | hex content address of the weights that answered (OPTIONAL — §3.6; absent means the receipt binds the model by name alone) | +| `precision` | the numeric precision the forward pass ran at, as the backend names it — `fp32`, `fp16`, `bf16`, `int8` (OPTIONAL — §3.6; paired with `weights_digest`, since neither reproduces alone) | | `route` | the router's one-line justification | | `tokens_in/out/accepted`, `cache_hit_tokens`, `spec_proposed/accepted` | the outcome counters | | `fidelity_floor` | the floor the request demanded (uint) | diff --git a/mesh/mesh-rs/crates/mesh-core/src/lib.rs b/mesh/mesh-rs/crates/mesh-core/src/lib.rs index 97b225f0b..22517d79d 100644 --- a/mesh/mesh-rs/crates/mesh-core/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-core/src/lib.rs @@ -213,22 +213,37 @@ impl<'m> Mesh<'m> { let fidelity_met = basis >= req.fidelity_floor; // 4. Build, chain, and seal the receipt. + // + // Read off the outcome before it is moved into the receipt. `ServeOutcome` + // stopped being `Copy` when `precision` made it hold a `String`, so the + // fields the receipt lifts out of it are taken first. let nonce = nonce_from(&req.id()); + let weights_digest = served.outcome.weights_digest; + let precision = served.outcome.precision.clone(); + let delivered = served.outcome.delivered_fidelity; let receipt = MeshReceipt { request_id: req.id(), grant_id, backend: backend.clone(), recipe, + // Carried through from the party that ran the weights, absent where + // it did not say. No check is needed on the way past: `ContentId` is + // 32 bytes, so a digest that is not one cannot have been reported, + // and the field's presence therefore always carries its substance. + weights_digest, + precision, route_justification: route.justification.clone(), - outcome: served.outcome, + // Cloned, not moved: the response carries the same outcome, and + // `ServeOutcome` is six counters plus two options and a short name. + outcome: served.outcome.clone(), fidelity_floor: req.fidelity_floor, // The pair travels together: their purpose is to make a divergence // between promise and delivery checkable, and the advertisement // alone adds nothing a verifier does not already have from // `backend`. Both absent is the historical shape, which seals to // the bytes the published pack shipped with. - fidelity_advertised: served.outcome.delivered_fidelity.map(|_| advertised), - fidelity_delivered: served.outcome.delivered_fidelity, + fidelity_advertised: delivered.map(|_| advertised), + fidelity_delivered: delivered, fidelity_met, energy, sequence: 0, // stamped by the log @@ -382,6 +397,8 @@ mod tests { spec_accepted: 0, decision: None, delivered_fidelity: self.serves, + weights_digest: None, + precision: None, }, } } @@ -517,4 +534,53 @@ mod tests { added.sort(); assert_eq!(added, vec!["fidelity_advertised", "fidelity_delivered"]); } + + /// The model identity rides the whole path — engine, receipt, envelope — + /// and is signed rather than advisory (SPEC §3.6). + /// + /// Same shape as the fidelity test above, for the same reason: two meshes + /// differing only in whether the engine reports what ran. If the identity + /// were carried but not sealed the envelopes would match, and a verifier + /// comparing signatures could not tell a receipt bound to these weights + /// from one bound to any others. + #[test] + fn the_model_identity_rides_the_path_and_is_signed() { + let meter = SteppingMeter::new("test-rapl", Provenance::HwShunt, 1_000_000); + let (sk, steward) = steward_key(); + let digest = ContentId::of_bytes(b"ssm-1b weights, revision 3"); + + let mut by_name = Mesh::new(&meter, sk.clone(), steward.clone()); + by_name.register( + BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000), + Box::new(ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 256)), + ); + let mut bound = Mesh::new(&meter, sk, steward); + bound.register( + BackendSpec::new("tiny", "ssm-1b", Fidelity::F1, 50_000), + Box::new( + ReferenceEngine::new("tiny", "ssm-1b", Fidelity::F1, 50_000, 256) + .reporting_model_identity(digest, "fp16"), + ), + ); + + let req = request(8, Fidelity::F1, 10_000_000); + let a = by_name.serve(&req, &[], None).expect("serve"); + let b = bound.serve(&req, &[], None).expect("serve"); + + // Both name the same recipe — which is the point: the name alone does + // not distinguish them, and before these fields nothing else did. + assert_eq!(a.receipt.recipe, b.receipt.recipe); + assert!(a.receipt.weights_digest.is_none()); + assert_eq!(b.receipt.weights_digest, Some(digest)); + assert_eq!(b.receipt.precision.as_deref(), Some("fp16")); + + assert_ne!(a.envelope, b.envelope, "the identity must be signed, not advisory"); + + let (wa, wb) = (a.receipt.to_jcr1_wire("s"), b.receipt.to_jcr1_wire("s")); + assert_eq!(wb["weights_digest"], serde_json::json!(digest.to_hex())); + let (oa, ob) = (wa.as_object().unwrap(), wb.as_object().unwrap()); + let mut added: Vec<&String> = ob.keys().filter(|k| !oa.contains_key(*k)).collect(); + added.sort(); + assert_eq!(added, vec!["precision", "weights_digest"]); + } } diff --git a/mesh/mesh-rs/crates/mesh-engine-gguf/src/lib.rs b/mesh/mesh-rs/crates/mesh-engine-gguf/src/lib.rs index 48f5bfac8..63d499e66 100644 --- a/mesh/mesh-rs/crates/mesh-engine-gguf/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-engine-gguf/src/lib.rs @@ -123,6 +123,13 @@ mod engine { // makes no claim: the gate falls back to the advertised // rating, which is where these engines already stood. delivered_fidelity: None, + // The weights are a real file, so a digest is obtainable — + // but once, at load, never per request. Until this engine does + // that it reports absent rather than something cheaper that + // would look like a binding: the field's presence is a claim to + // re-executability, and a partial digest is worse than none. + weights_digest: None, + precision: None, }; Served { tokens, outcome } } @@ -261,6 +268,13 @@ mod falcon { // makes no claim: the gate falls back to the advertised // rating, which is where these engines already stood. delivered_fidelity: None, + // The weights are a real file, so a digest is obtainable — + // but once, at load, never per request. Until this engine does + // that it reports absent rather than something cheaper that + // would look like a binding: the field's presence is a claim to + // re-executability, and a partial digest is worse than none. + weights_digest: None, + precision: None, }; Served { tokens, outcome } } diff --git a/mesh/mesh-rs/crates/mesh-engine/src/lib.rs b/mesh/mesh-rs/crates/mesh-engine/src/lib.rs index 994445816..a22caf540 100644 --- a/mesh/mesh-rs/crates/mesh-engine/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-engine/src/lib.rs @@ -13,7 +13,7 @@ #![forbid(unsafe_code)] -use mesh_schema::{BackendId, Fidelity, ServeOutcome, ServeRequest}; +use mesh_schema::{BackendId, ContentId, Fidelity, ServeOutcome, ServeRequest}; /// What an engine returns from one `serve`: the generated token ids and the /// serving counters. @@ -69,6 +69,14 @@ pub struct ReferenceEngine { /// it unconditionally would change every published vector and break every /// verifier that byte-compares them. report_delivered: bool, + /// What to report as the weights' address and the arithmetic's precision. + /// Both `None` by default, for the same reason as `report_delivered`: the + /// published `mesh/1` vectors are generated from this engine, and a receipt + /// that carries either field seals to different bytes. This engine answers + /// from a table, so it has no weights of its own to address — the switch + /// exists to make the fields exercisable end to end. + weights_digest: Option, + precision: Option, /// Seeds served before — a repeat counts as a cache hit. seen: std::collections::HashSet, } @@ -95,6 +103,8 @@ impl ReferenceEngine { work_per_token, spec_accept_permille: 0, report_delivered: false, + weights_digest: None, + precision: None, seen: std::collections::HashSet::new(), } } @@ -113,6 +123,21 @@ impl ReferenceEngine { self } + /// Report a weights address and a precision, as a backend serving real + /// weights would (SPEC §3.6). + /// + /// Off by default for the same reason as `reporting_delivered_fidelity`: + /// the published vectors are built from this engine, and either field + /// changes the sealed bytes. With it, a vector exercises the binding; a + /// vector built without it keeps the bytes the pack shipped with. A real + /// backend needs no switch — it digests its weights once at load and + /// reports what it ran. + pub fn reporting_model_identity(mut self, weights_digest: ContentId, precision: &str) -> Self { + self.weights_digest = Some(weights_digest); + self.precision = Some(precision.to_string()); + self + } + pub fn with_speculation(mut self, accept_permille: u32) -> Self { self.spec_accept_permille = accept_permille.min(1000); self @@ -201,6 +226,12 @@ impl ServingEngine for ReferenceEngine { spec_accepted, decision: None, delivered_fidelity: self.report_delivered.then_some(self.fidelity), + // This engine answers from a table: there are no weights to + // address and no arithmetic whose precision could be reported. + // A backend serving real weights digests them once at load and + // reports it here; absent is the honest answer for this one. + weights_digest: self.weights_digest, + precision: self.precision.clone(), }; Served { tokens, outcome } } diff --git a/mesh/mesh-rs/crates/mesh-receipt/src/lib.rs b/mesh/mesh-rs/crates/mesh-receipt/src/lib.rs index be07de0b2..c5a567d19 100644 --- a/mesh/mesh-rs/crates/mesh-receipt/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-receipt/src/lib.rs @@ -55,7 +55,26 @@ pub struct MeshReceipt { /// The backend that served the request. pub backend: BackendId, /// The recipe (composed model) that served it. + /// + /// A name. Read it with `weights_digest` and `precision`: alone it says + /// which label the operator gave the model, not which weights ran. pub recipe: String, + /// The content address of the weights `recipe` named, when the backend + /// reported one (SPEC §3.6). + /// + /// This is what makes `recipe` binding rather than advisory. Sealed into the + /// signed core, so re-pointing a name at different weights no longer leaves + /// past receipts verifying against a model that did not answer. `None` is + /// the historical shape, skipped on the wire — the receipt then binds the + /// model by name alone, and says so by the field's absence. + pub weights_digest: Option, + /// The numeric precision the forward pass ran at (SPEC §3.6), when the + /// backend reported one. + /// + /// Travels with `weights_digest` because neither reproduces alone: the same + /// weights at `fp16` and `fp32` give different probabilities even where the + /// selected answer agrees. `None` is the historical shape, skipped. + pub precision: Option, /// One-line, auditable reason the router chose this route. pub route_justification: String, /// Serving counters (the inputs to the J/accepted-token figure). @@ -140,6 +159,17 @@ impl MeshReceipt { } obj.insert("backend".into(), json!(self.backend.0)); obj.insert("recipe".into(), json!(self.recipe)); + // Emitted only when the backend reported them, so a receipt that binds + // the model by name alone seals to the bytes it sealed to before these + // fields existed — the published pack's vectors included. `canonical_cbor` + // orders keys itself (by length, then lexicographically), so where these + // are inserted here never reaches the wire. + if let Some(d) = self.weights_digest { + obj.insert("weights_digest".into(), json!(d.to_hex())); + } + if let Some(p) = &self.precision { + obj.insert("precision".into(), json!(p)); + } obj.insert("route".into(), json!(self.route_justification)); obj.insert("tokens_in".into(), json!(self.outcome.tokens_in)); obj.insert("tokens_out".into(), json!(self.outcome.tokens_out)); @@ -306,6 +336,8 @@ mod tests { grant_id: None, backend: BackendId::new("ref-f1"), recipe: "tiny-greedy".into(), + weights_digest: None, + precision: None, route_justification: "cheapest-sufficient".into(), outcome: outcome(accepted), fidelity_floor: Fidelity::F1, @@ -319,6 +351,60 @@ mod tests { } } + /// `recipe` is a name; `weights_digest` and `precision` are what make it + /// binding (SPEC §3.6). + /// + /// Three properties, and the third is the one that matters. Without it the + /// other two would pass on fields that reached the struct and never reached + /// the signed core — present on the wire, absent from what the signature + /// covers, which is exactly the shape of a binding that is not one. + #[test] + fn the_model_identity_is_sealed_not_merely_carried() { + let steward = "did:example:mesh-endpoint"; + let sk = SigningKey::from_bytes(&[9u8; 32]); + let digest = ContentId::of_bytes(b"the weights that answered"); + + // 1. Absent is the historical shape: it seals to the bytes it sealed to + // before these fields existed, which is what keeps the published + // pack's vectors verifying. + let bare = receipt(Energy::measured(1_000), 4); + assert!(bare.weights_digest.is_none() && bare.precision.is_none()); + let bare_wire = bare.to_jcr1_wire(steward); + assert!( + bare_wire.get("weights_digest").is_none() && bare_wire.get("precision").is_none(), + "an unreported identity must be absent from the wire, not null or empty" + ); + let bare_core = bare.signed_core(steward).unwrap(); + + // 2. Present, it reaches the wire as hex and as the backend's own name, + // and the receipt still seals and verifies. + let mut bound = receipt(Energy::measured(1_000), 4); + bound.weights_digest = Some(digest); + bound.precision = Some("fp16".into()); + let bound_wire = bound.to_jcr1_wire(steward); + assert_eq!(bound_wire["weights_digest"], json!(digest.to_hex())); + assert_eq!(bound_wire["precision"], json!("fp16")); + let env = bound.seal(&sk, steward).unwrap(); + assert!(bound + .verify_envelope(&env, steward, &sk.verifying_key().to_bytes()) + .is_ok()); + + // 3. The binding is in the signed core. Same receipt but for the + // identity ⇒ different sealed bytes, so a signature over one cannot + // be read as a signature over the other. + let bound_core = bound.signed_core(steward).unwrap(); + assert_ne!( + bare_core, bound_core, + "the identity must change the bytes the signature covers, or it binds nothing" + ); + + // And the digest is what distinguishes them, not just its presence: + // a different digest is a different core again. + let mut other = bound.clone(); + other.weights_digest = Some(ContentId::of_bytes(b"different weights")); + assert_ne!(bound_core, other.signed_core(steward).unwrap()); + } + /// A typed decision accepts no output tokens, so the token metric is /// undefined and the decision metric is the one that exists. /// diff --git a/mesh/mesh-rs/crates/mesh-schema/src/lib.rs b/mesh/mesh-rs/crates/mesh-schema/src/lib.rs index c1d73c50f..5f78ea802 100644 --- a/mesh/mesh-rs/crates/mesh-schema/src/lib.rs +++ b/mesh/mesh-rs/crates/mesh-schema/src/lib.rs @@ -272,7 +272,10 @@ pub struct RouteDecision { } /// Per-request serving counters — the inputs to the J/accepted-token metric. -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +// `Copy` was dropped when `precision` arrived: an open-vocabulary name is a +// `String`, and nothing in the workspace relied on copying this — removing the +// derive produced no error anywhere outside the new fields themselves. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServeOutcome { pub tokens_in: u32, pub tokens_out: u32, @@ -314,6 +317,42 @@ pub struct ServeOutcome { /// basis. #[serde(default, skip_serializing_if = "Option::is_none")] pub delivered_fidelity: Option, + /// The content address of the weights that answered, as the party that ran + /// them reports it (SPEC §3.6). + /// + /// `recipe` is a name, and a name is not a binding: re-point it at + /// different weights, a different quantization or a different revision and + /// every receipt sealed under it still verifies while describing a decode + /// some other model produced. A decision is one forward pass with no + /// sampling, so given the weights and the precision it is reproducible — + /// which is what makes a decision receipt checkable by re-execution rather + /// than merely signed. + /// + /// A [`ContentId`], not a string, so a digest that is not one cannot be + /// reported at all. The field's presence is the claim that the model is + /// bound by content; a 32-byte address keeps that claim structural instead + /// of something a validator has to catch later. + /// + /// `None` is the historical shape: skipped on the wire, and the receipt then + /// binds the model by name alone, which is the older and weaker basis. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub weights_digest: Option, + /// The numeric precision the forward pass ran at, named as the backend names + /// it (`fp32`, `fp16`, `bf16`, `int8`). + /// + /// A `String`, not an enum, because the vocabulary is open and this standard + /// does not own it. An enum would either refuse a precision that really ran + /// or need a catch-all variant, and a catch-all that discards the name + /// records less than the string does. + /// + /// Load-bearing for reproduction rather than descriptive: a published + /// decision model's probabilities differ between `fp16` and `fp32` even + /// where the selected answer agrees, so a receipt that omits the precision + /// does not reproduce (SPEC §3.6). Stored-weight quantization is the + /// digest's business; this is what the arithmetic ran in. `None` is the + /// historical shape. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub precision: Option, } /// Per-request decision counters — the inputs to the µJ/decision metric