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`
|
> probabilities of a published decision model differ between `fp16` and `fp32`
|
||||||
> even where the selected answer agrees, so a receipt that omits precision does
|
> even where the selected answer agrees, so a receipt that omits precision does
|
||||||
> not reproduce.
|
> 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"` |
|
| `mesh_receipt` | `"mesh/1"` |
|
||||||
| `request_id` | hex content id of the request |
|
| `request_id` | hex content id of the request |
|
||||||
| `grant_id` | hex JCP grant id this inference debited (omitted if ungranted) |
|
| `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 |
|
| `route` | the router's one-line justification |
|
||||||
| `tokens_in/out/accepted`, `cache_hit_tokens`, `spec_proposed/accepted` | the outcome counters |
|
| `tokens_in/out/accepted`, `cache_hit_tokens`, `spec_proposed/accepted` | the outcome counters |
|
||||||
| `fidelity_floor` | the floor the request demanded (uint) |
|
| `fidelity_floor` | the floor the request demanded (uint) |
|
||||||
|
|
|
||||||
|
|
@ -213,22 +213,37 @@ impl<'m> Mesh<'m> {
|
||||||
let fidelity_met = basis >= req.fidelity_floor;
|
let fidelity_met = basis >= req.fidelity_floor;
|
||||||
|
|
||||||
// 4. Build, chain, and seal the receipt.
|
// 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 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 {
|
let receipt = MeshReceipt {
|
||||||
request_id: req.id(),
|
request_id: req.id(),
|
||||||
grant_id,
|
grant_id,
|
||||||
backend: backend.clone(),
|
backend: backend.clone(),
|
||||||
recipe,
|
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(),
|
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,
|
fidelity_floor: req.fidelity_floor,
|
||||||
// The pair travels together: their purpose is to make a divergence
|
// The pair travels together: their purpose is to make a divergence
|
||||||
// between promise and delivery checkable, and the advertisement
|
// between promise and delivery checkable, and the advertisement
|
||||||
// alone adds nothing a verifier does not already have from
|
// alone adds nothing a verifier does not already have from
|
||||||
// `backend`. Both absent is the historical shape, which seals to
|
// `backend`. Both absent is the historical shape, which seals to
|
||||||
// the bytes the published pack shipped with.
|
// the bytes the published pack shipped with.
|
||||||
fidelity_advertised: served.outcome.delivered_fidelity.map(|_| advertised),
|
fidelity_advertised: delivered.map(|_| advertised),
|
||||||
fidelity_delivered: served.outcome.delivered_fidelity,
|
fidelity_delivered: delivered,
|
||||||
fidelity_met,
|
fidelity_met,
|
||||||
energy,
|
energy,
|
||||||
sequence: 0, // stamped by the log
|
sequence: 0, // stamped by the log
|
||||||
|
|
@ -382,6 +397,8 @@ mod tests {
|
||||||
spec_accepted: 0,
|
spec_accepted: 0,
|
||||||
decision: None,
|
decision: None,
|
||||||
delivered_fidelity: self.serves,
|
delivered_fidelity: self.serves,
|
||||||
|
weights_digest: None,
|
||||||
|
precision: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -517,4 +534,53 @@ mod tests {
|
||||||
added.sort();
|
added.sort();
|
||||||
assert_eq!(added, vec!["fidelity_advertised", "fidelity_delivered"]);
|
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
|
// makes no claim: the gate falls back to the advertised
|
||||||
// rating, which is where these engines already stood.
|
// rating, which is where these engines already stood.
|
||||||
delivered_fidelity: None,
|
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 }
|
Served { tokens, outcome }
|
||||||
}
|
}
|
||||||
|
|
@ -261,6 +268,13 @@ mod falcon {
|
||||||
// makes no claim: the gate falls back to the advertised
|
// makes no claim: the gate falls back to the advertised
|
||||||
// rating, which is where these engines already stood.
|
// rating, which is where these engines already stood.
|
||||||
delivered_fidelity: None,
|
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 }
|
Served { tokens, outcome }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![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
|
/// What an engine returns from one `serve`: the generated token ids and the
|
||||||
/// serving counters.
|
/// serving counters.
|
||||||
|
|
@ -69,6 +69,14 @@ pub struct ReferenceEngine {
|
||||||
/// it unconditionally would change every published vector and break every
|
/// it unconditionally would change every published vector and break every
|
||||||
/// verifier that byte-compares them.
|
/// verifier that byte-compares them.
|
||||||
report_delivered: bool,
|
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.
|
/// Seeds served before — a repeat counts as a cache hit.
|
||||||
seen: std::collections::HashSet<u64>,
|
seen: std::collections::HashSet<u64>,
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +103,8 @@ impl ReferenceEngine {
|
||||||
work_per_token,
|
work_per_token,
|
||||||
spec_accept_permille: 0,
|
spec_accept_permille: 0,
|
||||||
report_delivered: false,
|
report_delivered: false,
|
||||||
|
weights_digest: None,
|
||||||
|
precision: None,
|
||||||
seen: std::collections::HashSet::new(),
|
seen: std::collections::HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -113,6 +123,21 @@ impl ReferenceEngine {
|
||||||
self
|
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 {
|
pub fn with_speculation(mut self, accept_permille: u32) -> Self {
|
||||||
self.spec_accept_permille = accept_permille.min(1000);
|
self.spec_accept_permille = accept_permille.min(1000);
|
||||||
self
|
self
|
||||||
|
|
@ -201,6 +226,12 @@ impl ServingEngine for ReferenceEngine {
|
||||||
spec_accepted,
|
spec_accepted,
|
||||||
decision: None,
|
decision: None,
|
||||||
delivered_fidelity: self.report_delivered.then_some(self.fidelity),
|
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 }
|
Served { tokens, outcome }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,26 @@ pub struct MeshReceipt {
|
||||||
/// The backend that served the request.
|
/// The backend that served the request.
|
||||||
pub backend: BackendId,
|
pub backend: BackendId,
|
||||||
/// The recipe (composed model) that served it.
|
/// 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,
|
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.
|
/// One-line, auditable reason the router chose this route.
|
||||||
pub route_justification: String,
|
pub route_justification: String,
|
||||||
/// Serving counters (the inputs to the J/accepted-token figure).
|
/// 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("backend".into(), json!(self.backend.0));
|
||||||
obj.insert("recipe".into(), json!(self.recipe));
|
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("route".into(), json!(self.route_justification));
|
||||||
obj.insert("tokens_in".into(), json!(self.outcome.tokens_in));
|
obj.insert("tokens_in".into(), json!(self.outcome.tokens_in));
|
||||||
obj.insert("tokens_out".into(), json!(self.outcome.tokens_out));
|
obj.insert("tokens_out".into(), json!(self.outcome.tokens_out));
|
||||||
|
|
@ -306,6 +336,8 @@ mod tests {
|
||||||
grant_id: None,
|
grant_id: None,
|
||||||
backend: BackendId::new("ref-f1"),
|
backend: BackendId::new("ref-f1"),
|
||||||
recipe: "tiny-greedy".into(),
|
recipe: "tiny-greedy".into(),
|
||||||
|
weights_digest: None,
|
||||||
|
precision: None,
|
||||||
route_justification: "cheapest-sufficient".into(),
|
route_justification: "cheapest-sufficient".into(),
|
||||||
outcome: outcome(accepted),
|
outcome: outcome(accepted),
|
||||||
fidelity_floor: Fidelity::F1,
|
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
|
/// A typed decision accepts no output tokens, so the token metric is
|
||||||
/// undefined and the decision metric is the one that exists.
|
/// 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.
|
/// 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 struct ServeOutcome {
|
||||||
pub tokens_in: u32,
|
pub tokens_in: u32,
|
||||||
pub tokens_out: u32,
|
pub tokens_out: u32,
|
||||||
|
|
@ -314,6 +317,42 @@ pub struct ServeOutcome {
|
||||||
/// basis.
|
/// basis.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub delivered_fidelity: Option<Fidelity>,
|
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
|
/// Per-request decision counters — the inputs to the µJ/decision metric
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue