metrale_bench/gate/record_serve.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: The serve settings a gate record discloses about the server it
4//! measured (`GateRecord::serve_resolved`).
5//!
6//! `served_by` names the in-tree recipe (`gate::recipe_closure`), and
7//! `serve_overrides` only the keys changed for the run; neither states what
8//! the server resolved those settings to (2026-10-02). The keys are defined here, in the crate that owns the
9//! record, and the server CLI fills them from its rendered serve flags.
10//!
11//! Owner: bench gate (records).
12//! Invariants:
13//! - A [`disclosure`] always carries [`SPECULATIVE`], [`WEIGHT_QUANTIZATION`] and
14//! [`ACTIVATION_QUANTIZATION`]; every other key is present only when resolved or on.
15
16use std::collections::BTreeMap;
17
18use super::record::GateRecord;
19
20/// 2026-09-26: Key for the `--mtp-gate` regime in force: `force` or `auto`.
21pub const MTP_GATE: &str = "mtp_gate";
22/// 2026-09-26: Key for whether `--speculative` was on: `true` or `false`.
23pub const SPECULATIVE: &str = "speculative";
24/// 2026-09-26: Key for `--prefill-codispatch`, present (`true`) only when the
25/// rendered serve gave the flag.
26pub const PREFILL_CODISPATCH: &str = "prefill_codispatch";
27/// 2026-09-26: Key for `--w4a4-downcast`, present (`true`) only when on. The
28/// flag defaults to false and has no environment fallback, so an absent key
29/// means off.
30pub const W4A4_DOWNCAST: &str = "w4a4_downcast";
31/// 2026-09-28: Key for `--weight-quantization`, always present on a record written since the
32/// flag exists, with the tier's name (`declared`, `nvfp4`). A record without it predates the
33/// flag, and its server ran what `nvfp4` names.
34pub const WEIGHT_QUANTIZATION: &str = "weight_quantization";
35/// 2026-09-27: Key for `--expert-quantization`, present only for a tier other than the default
36/// `fp8`, with the tier's name as the value (`nvfp4-gate-up`, `nvfp4`). The flag has no
37/// environment fallback, so an absent key means `fp8`.
38pub const EXPERT_QUANTIZATION: &str = "expert_quantization";
39/// 2026-09-30: Key for `--activation-quantization`, always present on a record written since the
40/// flag exists, with the value's canonical form (`adaptive`, `declared`, a ladder). A record
41/// without it predates the flag, and its server ran what `adaptive` names.
42pub const ACTIVATION_QUANTIZATION: &str = "activation_quantization";
43
44/// 2026-09-26: The disclosure for a server whose rendered flags resolved to
45/// these values.
46///
47/// `mtp_gate_force` is the server's resolution of `--mtp-gate` and
48/// `--hermetic`: `Some(true)` is written `force`, `Some(false)` `auto`.
49/// `None` (no flag, so the server's `METRALE_MTP_GATE_FORCE` decides) writes
50/// no key rather than a guessed default.
51///
52/// `prefill_codispatch` writes `true` when the flag was given and no key
53/// otherwise; the server's `METRALE_PREFILL_CODISPATCH` then decides, and
54/// `serve_env` discloses it when the recipe declares it.
55///
56/// `expert_quantization` is the tier's name when it is not the default `fp8`, else `None`.
57/// `weight_quantization` is the `--weight-quantization` tier's name, always written, and so is
58/// `activation_quantization`, the `--activation-quantization` value's canonical form.
59pub fn disclosure(
60 mtp_gate_force: Option<bool>,
61 speculative: bool,
62 prefill_codispatch: bool,
63 w4a4_downcast: bool,
64 expert_quantization: Option<&str>,
65 weight_quantization: &str,
66 activation_quantization: &str,
67) -> BTreeMap<String, String> {
68 let mut m = BTreeMap::new();
69 m.insert(
70 ACTIVATION_QUANTIZATION.to_string(),
71 activation_quantization.to_string(),
72 );
73 m.insert(SPECULATIVE.to_string(), speculative.to_string());
74 m.insert(
75 WEIGHT_QUANTIZATION.to_string(),
76 weight_quantization.to_string(),
77 );
78 if let Some(force) = mtp_gate_force {
79 m.insert(
80 MTP_GATE.to_string(),
81 if force { "force" } else { "auto" }.to_string(),
82 );
83 }
84 if prefill_codispatch {
85 m.insert(PREFILL_CODISPATCH.to_string(), "true".to_string());
86 }
87 if w4a4_downcast {
88 m.insert(W4A4_DOWNCAST.to_string(), "true".to_string());
89 }
90 if let Some(tier) = expert_quantization {
91 m.insert(EXPERT_QUANTIZATION.to_string(), tier.to_string());
92 }
93 m
94}
95
96/// 2026-09-28: Key for `--forward`, present only for a forward other than `legacy`.
97pub const FORWARD: &str = "forward";
98/// 2026-09-28: Key for the live decode plan's digest (`metrale_circuit::digest::plan_digest`),
99/// present only when the server runs a circuit forward.
100pub const PLAN_DIGEST: &str = "plan_digest";
101
102/// 2026-09-28: What a server reports about its forward (`GET /forward`): the server fills it
103/// from its model, the harness reads it into the record.
104#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
105pub struct LiveForward {
106 /// 2026-09-28: `legacy`, `circuit` or `circuit-reference`.
107 pub forward: String,
108 /// 2026-09-28: The decode plan's digest; `None` under `legacy`.
109 #[serde(default)]
110 pub plan_digest: Option<String>,
111 /// 2026-10-01: The slot count `--max-batch-size auto` resolved to; `None` for an explicit
112 /// count (which the rendered serve already states).
113 #[serde(default)]
114 pub auto_max_batch_size: Option<usize>,
115 /// 2026-10-02: The MoE expert-table decision the serve's memory plan made before load
116 /// (`build` or `skip`); `None` when the loader reads none.
117 #[serde(default)]
118 pub moe_expert_tables: Option<String>,
119 /// 2026-10-03: SHA-256 of the kernel tree the serve's memory plan was computed from
120 /// (`metrale_kernel_tree::SHA256`); `None` when the serve made no plan.
121 #[serde(default)]
122 pub kernel_tree: Option<String>,
123}
124
125/// 2026-10-01: Key for the slot count `--max-batch-size auto` resolved to, written `auto:<n>`;
126/// present only for `auto`.
127pub const MAX_BATCH_SIZE: &str = "max_batch_size";
128/// 2026-10-02: Key for the MoE expert-table decision, present (`skip`) only when the serve's
129/// memory plan dropped the transposed MoE prefill tables. Absent means built, as every serve
130/// before the decision existed did.
131pub const MOE_EXPERT_TABLES: &str = "moe_expert_tables";
132/// 2026-10-03: Key for the SHA-256 of the kernel tree the serve's memory plan read, present
133/// whenever the serve planned (every serve whose loader reads the MoE expert-table decision).
134pub const KERNEL_TREE: &str = "kernel_tree";
135
136/// 2026-09-28: Add the live forward to `resolved`: [`FORWARD`] when it is not `legacy`, and
137/// [`PLAN_DIGEST`]. `requested` is the forward the rendered serve asked for; a server running
138/// another one, or a circuit without a digest, or legacy with one, is refused: the record would
139/// state a configuration the measurement did not run.
140pub fn merge_live_forward(
141 resolved: &mut BTreeMap<String, String>,
142 requested: &str,
143 live: &LiveForward,
144) -> Result<(), String> {
145 let skipped = match live.moe_expert_tables.as_deref() {
146 None | Some("build") => false,
147 Some("skip") => true,
148 Some(other) => return Err(format!("the server reports MoE expert tables `{other}`")),
149 };
150 if let Some(d) = &live.kernel_tree
151 && !(d.len() == 64 && d.bytes().all(|b| b.is_ascii_hexdigit()))
152 {
153 return Err(format!(
154 "the server reports kernel tree `{d}`, not a SHA-256"
155 ));
156 }
157 if live.forward != requested {
158 return Err(format!(
159 "the server runs forward `{}`, the rendered serve asked for `{requested}`",
160 live.forward
161 ));
162 }
163 match (live.forward.as_str(), &live.plan_digest) {
164 ("legacy", None) => {}
165 ("legacy", Some(d)) => return Err(format!("a legacy forward reports plan digest {d}")),
166 (other, Some(d)) => {
167 resolved.insert(FORWARD.to_string(), other.to_string());
168 resolved.insert(PLAN_DIGEST.to_string(), d.clone());
169 }
170 (other, None) => return Err(format!("forward `{other}` reports no plan digest")),
171 }
172 if let Some(n) = live.auto_max_batch_size {
173 resolved.insert(MAX_BATCH_SIZE.to_string(), format!("auto:{n}"));
174 }
175 if skipped {
176 resolved.insert(MOE_EXPERT_TABLES.to_string(), "skip".to_string());
177 }
178 if let Some(d) = &live.kernel_tree {
179 resolved.insert(KERNEL_TREE.to_string(), d.clone());
180 }
181 Ok(())
182}
183
184impl GateRecord {
185 /// 2026-09-26: Attach what the gate's serve resolved; see [`disclosure`].
186 #[must_use]
187 pub fn with_serve_resolved(mut self, resolved: BTreeMap<String, String>) -> Self {
188 self.serve_resolved = resolved;
189 self
190 }
191}
192
193#[cfg(test)]
194#[path = "record_serve_tests.rs"]
195mod record_serve_tests;