mesh §3.6: the receipt sealed the model by name, so the name was not a binding #246
6 changed files with 252 additions and 6 deletions
12
mesh/SPEC.md
12
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) |
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ContentId>,
|
||||
precision: Option<String>,
|
||||
/// Seeds served before — a repeat counts as a cache hit.
|
||||
seen: std::collections::HashSet<u64>,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ContentId>,
|
||||
/// 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<String>,
|
||||
/// 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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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<Fidelity>,
|
||||
/// 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<ContentId>,
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
/// Per-request decision counters — the inputs to the µJ/decision metric
|
||||
|
|
|
|||
Loading…
Reference in a new issue