metrale_bench/benchmarks/serve_matrix/
mod.rs1pub mod host;
15pub mod plan;
16pub mod probes;
17pub mod report;
18pub mod schema;
19pub mod score;
20
21use crate::hardware::Sensitivity;
22use std::future::Future;
23use std::sync::Arc;
24use std::time::{Duration, Instant};
25
26use anyhow::{Context, Result};
27
28use crate::benchmark::{Benchmark, BenchmarkDescriptor};
29use crate::benchmarks::{baseline, one_line, unique_prefix_tag};
30use crate::http;
31use crate::metadata::PluginMetadata;
32use crate::params::{ParamSpec, ParamValues};
33use crate::plugin::{Plugin, PluginHandle};
34use crate::result::{BenchmarkResult, LogLine, RunStatus};
35
36use host::{ServeHost, ServeOptions};
37use plan::{Plan, Round};
38use score::{Outcome, RoundResult, Signals};
39
40const LONG_CTX_HEADROOM: usize = 256;
44
45const SUMMARY: &str = "Boot every servable checkpoint, probe it, gate the matrix";
46pub const METADATA: PluginMetadata = PluginMetadata::metrale(SUMMARY);
47
48pub const DESCRIPTOR: BenchmarkDescriptor = BenchmarkDescriptor {
49 id: "serve-matrix",
50 name: "Serve Matrix",
51 summary: SUMMARY,
52 detail: "The release serve gate. For every checkpoint this box can actually serve — cached \
53 weights intersected with the kernels this build compiled — it loads the model, \
54 checks coherence, code generation, tool calling and long-context recall, measures \
55 decode throughput, and moves on. Coverage is enforced: a checkpoint that is \
56 planned and fails to boot is a FAIL, never a silent absence. Booting replaces \
57 whatever model is currently serving; the one that was serving when the run started \
58 is restored at the end.",
59 duration_hint: "~5–10 min per checkpoint",
60 expected_secs: 600,
61 updated: "2026-08-02",
62 needs_confirmation: true,
64 intended_for: None,
66 threshold_params: &[],
67 sensitivity: Sensitivity::Correctness,
72 ctor: || Box::new(ServeMatrix::default()),
73};
74
75#[derive(Default)]
76pub struct ServeMatrix {
77 handle: Option<PluginHandle>,
78 host: Option<Arc<dyn ServeHost>>,
79 started: Option<Instant>,
80 include: String,
81 opts: Option<ServeOptions>,
82 long_ctx_tokens: usize,
83 tps_tokens: usize,
84 probe_budget: usize,
85 timeout: Duration,
86 update_baselines: bool,
87 plan: Plan,
90 planned_built: bool,
91 cursor: usize,
92 results: Vec<RoundResult>,
93 baseline: Option<baseline::Baseline>,
94}
95
96impl ServeMatrix {
97 pub fn with_host(host: Arc<dyn ServeHost>) -> Self {
100 Self {
101 host: Some(host),
102 ..Self::default()
103 }
104 }
105
106 fn handle(&self) -> Result<&PluginHandle> {
107 self.handle.as_ref().context("benchmark was not loaded")
108 }
109
110 fn host(&self) -> Result<Arc<dyn ServeHost>> {
111 self.host.clone().context(host::NO_HOST)
112 }
113
114 fn options(&self) -> Result<ServeOptions> {
115 self.opts.context("benchmark was not configured")
116 }
117
118 fn elapsed(&self) -> Duration {
119 self.started.map(|s| s.elapsed()).unwrap_or_default()
120 }
121
122 fn baseline_for(&self, label: &str) -> Option<f64> {
123 self.baseline.as_ref()?.get(&format!("tps:{label}"))
124 }
125
126 async fn run_round(&self, round: &Round) -> Outcome {
129 let Ok(handle) = self.handle().cloned() else {
130 return Outcome::NotReached;
131 };
132 let (Ok(host), Ok(opts)) = (self.host(), self.options()) else {
133 return Outcome::NotReached;
134 };
135 handle.status(format!("{} · booting", round.label()));
136 let target = match host.serve(&round.model, opts).await {
137 Ok(t) => t,
138 Err(e) => return Outcome::BootFailed(one_line(format!("{e:#}"))),
139 };
140 if let Err(e) = http::probe(&target, Duration::from_secs(30)).await {
143 return Outcome::BootFailed(format!(
144 "came up but did not answer: {}",
145 one_line(format!("{e:#}"))
146 ));
147 }
148
149 let mut s = Signals::default();
150 handle.status(format!("{} · coherence", round.label()));
151 let (coherence, concern) = probes::coherence_probe(&target, self.timeout).await;
152 s.coherence_pass = coherence.passed;
153 s.coherence_total = coherence.total;
154 s.identity = coherence.identity;
155 if !concern.is_empty() {
156 handle.warn(format!("{}: {concern}", round.label()));
157 }
158
159 handle.status(format!("{} · codegen", round.label()));
160 s.codegen = probes::codegen_probe(&target, self.timeout, self.probe_budget).await;
161 handle.status(format!("{} · tool call", round.label()));
162 s.tool_call = probes::tool_call_probe(&target, self.timeout, self.probe_budget).await;
163
164 if self.long_ctx_tokens > 0 {
165 handle.status(format!("{} · long context", round.label()));
166 let tag = unique_prefix_tag(&format!("sm-lc-{}", self.cursor), handle.run_id());
167 s.long_ctx =
168 probes::long_context_probe(&target, self.timeout, self.long_ctx_tokens, &tag).await;
169 }
170
171 handle.status(format!("{} · throughput", round.label()));
172 let tag = unique_prefix_tag(&format!("sm-tps-{}", self.cursor), handle.run_id());
173 let (tps, err) = probes::tps_probe(&target, self.timeout, self.tps_tokens, &tag).await;
174 s.tps = tps;
175 if let Some(e) = err {
176 handle.warn(format!("{}: throughput probe failed: {e}", round.label()));
177 }
178 Outcome::Probed(Box::new(s))
179 }
180
181 fn build_plan(&mut self) -> Result<()> {
183 let roster = self.host()?.roster().context("reading the box's roster")?;
184 self.plan = Plan::build(&roster, &self.include);
185 self.planned_built = true;
186 Ok(())
187 }
188
189 fn write_baselines(&self) -> Result<usize> {
193 let handle = self.handle()?;
194 let mut metrics = std::collections::BTreeMap::new();
195 for r in self.results.iter().filter(|r| r.bars().is_empty()) {
196 if let Some(tps) = r.signals().and_then(|s| s.tps).filter(|v| *v > 0.0) {
197 metrics.insert(format!("tps:{}", r.label), (tps * 10.0).round() / 10.0);
198 }
199 }
200 let written = metrics.len();
201 baseline::save(
202 handle.artifacts(),
203 DESCRIPTOR.id,
204 &handle.target().base_url,
205 &handle.target().model,
206 metrics,
207 )?;
208 Ok(written)
209 }
210
211 fn finish(&self) -> Result<BenchmarkResult> {
212 let tally = score::tally(&self.plan, &self.results);
213 let total = self.plan.planned_count() as u64;
214 let mut frame = BenchmarkResult {
215 status: RunStatus::Completed,
216 ..BenchmarkResult::running("done", self.elapsed())
217 }
218 .with_progress(total, total)
219 .with_summary(report::summary(&tally))
220 .with_table(report::table(&self.plan, &self.results))
221 .with_verdict(report::verdict(&tally, &self.plan));
222 if self.update_baselines {
223 match self.write_baselines() {
224 Ok(n) => {
225 frame = frame.log_line(LogLine::info(format!(
226 "{n} throughput baseline(s) refreshed from the rounds that PASSED — later \
227 runs gate against these. Rounds below bar were not blessed."
228 )))
229 }
230 Err(e) => {
231 frame = frame.log_line(LogLine::warn(format!("baselines not written: {e:#}")))
232 }
233 }
234 } else if self.baseline.is_none() {
235 frame = frame.log_line(LogLine::warn(
236 "no throughput baseline on this box: the tok/s column is liveness only, not a \
237 regression check. Re-run with `update baselines` on to record one.",
238 ));
239 }
240 Ok(frame)
241 }
242}
243
244impl Plugin for ServeMatrix {
245 fn metadata(&self) -> &'static PluginMetadata {
246 &METADATA
247 }
248
249 fn load(&mut self, handle: PluginHandle) -> impl Future<Output = Result<()>> + Send {
250 if self.host.is_none() {
253 self.host = host::installed();
254 }
255 self.baseline = baseline::load(handle.artifacts(), DESCRIPTOR.id);
256 self.handle = Some(handle);
257 self.started = Some(Instant::now());
258 let ok = self.host.is_some();
259 async move {
260 if ok {
261 Ok(())
262 } else {
263 anyhow::bail!(host::NO_HOST)
264 }
265 }
266 }
267}
268
269impl Benchmark for ServeMatrix {
270 fn descriptor(&self) -> &'static BenchmarkDescriptor {
271 &DESCRIPTOR
272 }
273
274 fn parameters(&self) -> Vec<ParamSpec> {
275 schema::specs()
276 }
277
278 fn configure(&mut self, values: &ParamValues) -> Result<()> {
279 let specs = self.parameters();
280 values.validate_against(&specs)?;
281 let include = values.text("include")?.trim().to_string();
282 self.include = if include.eq_ignore_ascii_case("all") {
285 String::new()
286 } else {
287 include
288 };
289 let max_seq_len = values.usize("max_seq_len")?;
290 self.long_ctx_tokens = values.usize("long_ctx_tokens")?;
291 if self.long_ctx_tokens + LONG_CTX_HEADROOM > max_seq_len {
292 anyhow::bail!(
295 "Long-context probe: {} tokens plus {LONG_CTX_HEADROOM} tokens of needle, \
296 question and answer does not fit in a {max_seq_len}-token context",
297 self.long_ctx_tokens
298 );
299 }
300 self.opts = Some(ServeOptions {
301 max_seq_len,
302 speculative: values.bool("speculative")?,
303 });
304 self.tps_tokens = values.usize("tps_tokens")?;
305 self.probe_budget = values.usize("probe_budget")?;
306 self.timeout = Duration::from_secs(values.usize("request_timeout_s")? as u64);
307 self.update_baselines = values.bool("update_baselines")?;
308 self.plan = Plan::default();
309 self.planned_built = false;
310 self.cursor = 0;
311 self.results.clear();
312 Ok(())
313 }
314
315 async fn next(&mut self) -> Result<BenchmarkResult> {
316 let handle = self.handle()?.clone();
317 handle.check_cancelled()?;
318
319 if !self.planned_built {
320 self.build_plan()?;
321 let total = self.plan.planned_count() as u64;
322 let skipped: Vec<String> = self
323 .plan
324 .skipped()
325 .map(|(r, why)| format!("{} ({})", r.model, why.reason()))
326 .collect();
327 let mut frame = BenchmarkResult::running("plan", self.elapsed())
328 .with_progress(0, total.max(1))
329 .with_table(report::table(&self.plan, &self.results))
330 .log_line(LogLine::info(format!(
331 "{total} checkpoint(s) planned · {} not runnable on this box · {} outside the \
332 filter",
333 self.plan.skipped().count(),
334 self.plan.excluded_count()
335 )));
336 if !skipped.is_empty() {
337 frame = frame.log_line(LogLine::info(format!("skipping {}", skipped.join(", "))));
338 }
339 return Ok(frame);
340 }
341
342 let total = self.plan.planned_count();
343 if self.cursor >= total {
344 return self.finish();
345 }
346
347 let round = self
349 .plan
350 .planned()
351 .nth(self.cursor)
352 .cloned()
353 .context("the plan changed under the cursor")?;
354 let outcome = self.run_round(&round).await;
355 let result = RoundResult {
356 label: round.label(),
357 baseline_tps: self.baseline_for(&round.label()),
358 outcome,
359 };
360 let line = report::round_line(&result);
361 let failed = !result.bars().is_empty();
362 self.results.push(result);
363 self.cursor += 1;
364 let total = total as u64;
365 handle.progress(self.cursor as u64, total);
366 let tally = score::tally(&self.plan, &self.results);
367 Ok(BenchmarkResult::running(round.label(), self.elapsed())
368 .with_progress(self.cursor as u64, total)
369 .with_summary(report::summary(&tally))
370 .with_table(report::table(&self.plan, &self.results))
371 .log_line(if failed {
372 LogLine::error(line)
373 } else {
374 LogLine::info(line)
375 }))
376 }
377
378 async fn cleanup(&mut self) -> Result<()> {
381 let Some(host) = self.host.clone() else {
382 return Ok(());
383 };
384 if !self.planned_built {
385 return Ok(());
386 }
387 host.restore().await.context("restoring the previous model")
388 }
389}
390
391#[cfg(test)]
392#[path = "serve_matrix_tests.rs"]
393mod tests;