jugar_probar/perf_gate/protocol.rs
1//! PP-LLAMA-001 v3.0 §5.1 — protocol parameters, band configuration, and the
2//! conformance predicate that makes a shrunken run say so.
3//!
4//! # Where the numbers live (PP-33)
5//!
6//! Every protocol parameter is declared in `scripts/perf-matrix.yaml` under
7//! `protocol:` and read from there by [`ProtocolParams::from_matrix`]. The
8//! `pub const`s below are the **spec fallback**: they exist so this module's
9//! own tests have a value to compare against when the matrix has not yet been
10//! amended, and so [`ProtocolParams::from_matrix`] can be proven to *differ*
11//! from them (`conformance_violations_read_the_loaded_params_not_the_consts`).
12//! They are documented fallbacks, never a silent default: a matrix without a
13//! `protocol:` block makes [`ProtocolParams::from_matrix`] return `Err` naming
14//! the missing block.
15//!
16//! Nothing in this file is a threshold; they are all protocol parameters.
17
18use std::time::Duration;
19
20use serde::{Deserialize, Serialize};
21
22/// §4.4.1 — the client model. Closed-loop is the only model this module
23/// implements, and it is *recorded* rather than assumed so the choice is
24/// falsifiable from the receipt alone.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ClientModel {
28 /// `c` workers; each issues a request, waits for completion, immediately
29 /// issues the next.
30 ClosedLoop,
31}
32
33/// §4.4.2 — warmup requests are `WARMUP_MULTIPLIER × c`, discarded.
34pub const WARMUP_MULTIPLIER: usize = 2;
35/// §4.4.2 — quiesce between warmup completion and the first sampled request.
36pub const QUIESCE: Duration = Duration::from_secs(5);
37/// §4.4.2 — minimum sampled requests is `max(MIN_SAMPLES_FLOOR, MIN_SAMPLES_PER_WORKER × c)`.
38pub const MIN_SAMPLES_FLOOR: usize = 30;
39/// §4.4.2 — the per-worker term of the minimum-sample rule.
40pub const MIN_SAMPLES_PER_WORKER: usize = 8;
41/// §4.4.2 — minimum wall-clock per band.
42pub const MIN_WALL_CLOCK: Duration = Duration::from_secs(60);
43/// §4.4.3 — hard per-request timeout. A request exceeding this increments `timeouts`.
44pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(120);
45/// §5.1 — cooldown between the two lanes of one interleaved replicate.
46pub const COOLDOWN: Duration = Duration::from_secs(10);
47/// §4.3 — full band replicates per cell. **Five**, not three: `n = 3` sizes an
48/// effect and bounds no variance, so no σ-dependent status may change below 5.
49pub const REPLICATES: usize = 5;
50/// §4.3 — replicates are interleaved A,B,A,B,…; a non-interleaved receipt is
51/// `NONCONFORMANT-VALID` (PP-9's key carries `interleaved: true`).
52pub const INTERLEAVED: bool = true;
53/// §5.1 W1 — generated tokens per request, on the wire as OpenAI `max_tokens`.
54pub const N_PREDICT: u32 = 128;
55/// §5.1 — the pinned sampler temperature for both lanes (PP-28).
56pub const SAMPLER_TEMPERATURE: f64 = 0.0;
57/// §5.1 — the pinned sampler seed for both lanes (PP-28).
58pub const SAMPLER_SEED: u64 = 0;
59/// §5.1 — `ignore_eos` on both lanes, so every retained sample runs to
60/// `n_predict` (PP-28).
61pub const SAMPLER_IGNORE_EOS: bool = true;
62/// PP-27 — a live stream has `median(ttft / e2e)` well below 1; a replayed one
63/// approaches 1 because the whole answer arrives at once. Matrix key
64/// `stream.live_ttft_over_e2e_max`.
65pub const STREAM_LIVE_TTFT_OVER_E2E_MAX: f64 = 0.95;
66/// PP-26 — tokens that must agree between `m=1` and the batched run. Matrix key
67/// `witness.min_agree_tokens`.
68pub const WITNESS_MIN_AGREE_TOKENS: u32 = 64;
69/// §4.4.4 — bootstrap resamples.
70pub const BOOTSTRAP_RESAMPLES: usize = 10_000;
71/// §4.4.4 — bootstrap seed. Goes in the receipt; the interval is reproducible
72/// from the retained samples with this value and no other.
73pub const BOOTSTRAP_SEED: u64 = 2026;
74
75/// §4.4.2 — `max(30, 8 × c)`.
76#[must_use]
77pub fn min_sampled_requests(concurrency: usize) -> usize {
78 MIN_SAMPLES_FLOOR.max(MIN_SAMPLES_PER_WORKER * concurrency)
79}
80
81/// §4.4.2 — `2 × c`.
82#[must_use]
83pub fn warmup_requests(concurrency: usize) -> usize {
84 WARMUP_MULTIPLIER * concurrency
85}
86
87/// `scripts/perf-matrix.yaml`, compiled in.
88///
89/// PP-33 puts every number the gate compares against in that file. Reading it
90/// at runtime would make the producer depend on a path that does not exist in a
91/// published crate; `include_str!` binds the exact bytes of the checkout the
92/// binary was built from, which is also what the receipt's `commit` claims.
93///
94/// PMAT-958: the bytes arrive through `build.rs`, which copies the workspace
95/// file into `OUT_DIR` and refuses to build if the vendored copy shipped in the
96/// crate (`perf-matrix.vendored.yaml`) differs from it; a published crate, which
97/// has no `scripts/`, embeds the vendored copy.
98pub const PERF_MATRIX_SOURCE: &str = include_str!(concat!(env!("OUT_DIR"), "/perf-matrix.yaml"));
99
100/// §5.1 / PP-28 — the sampler pinned on both lanes, on the wire in every receipt.
101#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct Sampler {
104 /// Greedy decoding. `0.0` in every conformant run.
105 pub temperature: f64,
106 /// The seed both lanes were given.
107 pub seed: u64,
108 /// `true` in W1, so `completion_tokens == n_predict` on every sample.
109 pub ignore_eos: bool,
110}
111
112impl Sampler {
113 /// The §5.1 pin, as the spec fallback.
114 #[must_use]
115 pub const fn spec_fallback() -> Self {
116 Self {
117 temperature: SAMPLER_TEMPERATURE,
118 seed: SAMPLER_SEED,
119 ignore_eos: SAMPLER_IGNORE_EOS,
120 }
121 }
122}
123
124/// §5.1 — the protocol block, read from `perf-matrix.yaml` and emitted verbatim
125/// at the receipt's top level as `protocol`.
126///
127/// A reader that cannot see the window, the warmup, the cooldown, the sampler
128/// and the replicate count cannot tell a 60 s conformant band from a 5 s one,
129/// and two receipts written under different protocols are not comparable — which
130/// is why the whole block is also in the PP-22 join key.
131#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct ProtocolParams {
134 /// `T` — the measurement window, in milliseconds.
135 pub window_ms: u64,
136 /// Warmup requests per worker, discarded (§4.4.2's `2 × c` as a per-worker
137 /// count, so the band-level figure is `warmup_requests_per_worker × c`).
138 pub warmup_requests_per_worker: u32,
139 /// Quiesce between warmup completion and the first sampled request.
140 pub quiesce_ms: u64,
141 /// §5.1 — cooldown between the two lanes of an interleaved replicate.
142 pub cooldown_ms: u64,
143 /// Generated tokens per request (`max_tokens` on the wire).
144 pub n_predict: u32,
145 /// Replicates per cell. The matrix declares the FLOOR (`replicates_min`);
146 /// a receipt records the number actually run and is `NONCONFORMANT-VALID`
147 /// below it.
148 pub replicates: u32,
149 /// Whether the replicates alternated A,B,A,B,….
150 pub interleaved: bool,
151 /// The pinned sampler (PP-28).
152 pub sampler: Sampler,
153}
154
155impl ProtocolParams {
156 /// The spec literals, as a documented fallback for tests and for callers
157 /// that must render a receipt before the matrix carries a `protocol:` block.
158 ///
159 /// Never reached silently by [`Self::from_matrix`], which returns `Err`.
160 #[must_use]
161 pub const fn spec_fallback() -> Self {
162 Self {
163 window_ms: MIN_WALL_CLOCK.as_millis() as u64,
164 warmup_requests_per_worker: WARMUP_MULTIPLIER as u32,
165 quiesce_ms: QUIESCE.as_millis() as u64,
166 cooldown_ms: COOLDOWN.as_millis() as u64,
167 n_predict: N_PREDICT,
168 replicates: REPLICATES as u32,
169 interleaved: INTERLEAVED,
170 sampler: Sampler::spec_fallback(),
171 }
172 }
173
174 /// Read the `protocol:` block out of the compiled-in `perf-matrix.yaml`.
175 ///
176 /// # Errors
177 /// When the file does not parse as YAML, or when it carries no `protocol:`
178 /// block, or when the block is missing a key. The error NAMES the missing
179 /// thing: a protocol silently defaulted to the Rust consts is exactly the
180 /// drift PP-33 exists to prevent.
181 pub fn from_matrix() -> Result<Self, String> {
182 Self::from_matrix_source(PERF_MATRIX_SOURCE)
183 }
184
185 /// [`Self::from_matrix`] against an explicit document, so a test can feed a
186 /// matrix that differs from the shipped one.
187 ///
188 /// # Errors
189 /// As [`Self::from_matrix`].
190 pub fn from_matrix_source(source: &str) -> Result<Self, String> {
191 let block = matrix_block(source, "protocol")?;
192 let raw: MatrixProtocolBlock = serde_yaml_ng::from_value(block)
193 .map_err(|e| format!("perf-matrix.yaml `protocol:` block: {e}"))?;
194 Ok(Self {
195 window_ms: raw.window_ms,
196 warmup_requests_per_worker: raw.warmup_requests_per_worker,
197 quiesce_ms: raw.quiesce_ms,
198 cooldown_ms: raw.cooldown_ms,
199 n_predict: raw.n_predict,
200 replicates: raw.replicates_min,
201 interleaved: raw.interleaved,
202 sampler: raw.sampler,
203 })
204 }
205
206 /// The parameters a receipt written on this checkout must use, **with the
207 /// provenance of where they came from**.
208 ///
209 /// [`Self::from_matrix`] is the honest reader and returns `Err`; this is the
210 /// one place the fallback is taken, and the returned [`ProtocolSource`] is
211 /// what the producer prints once and — on the fallback — names in
212 /// `unproduced_fields`.
213 ///
214 /// The silent version of this ([`Self::effective`], which discarded the
215 /// error) put the Rust consts on the wire under a `protocol:` block the
216 /// receipt then claimed came from the matrix. PP-33's whole point is that
217 /// every gated number lives in `perf-matrix.yaml`; a receipt that quietly
218 /// substituted a compiled-in copy is the drift it exists to prevent, and it
219 /// was invisible because nothing ever called `source()`.
220 #[must_use]
221 pub fn effective_with_source() -> (Self, ProtocolSource) {
222 match Self::from_matrix() {
223 Ok(params) => (params, ProtocolSource::Matrix),
224 Err(reason) => (Self::spec_fallback(), ProtocolSource::SpecFallback(reason)),
225 }
226 }
227
228 /// [`Self::effective_with_source`] without the provenance, for callers that
229 /// record it separately (the CLI producer) or do not write a receipt at all
230 /// (tests, `BandConfig`).
231 #[must_use]
232 pub fn effective() -> Self {
233 Self::effective_with_source().0
234 }
235
236 /// `"perf-matrix.yaml"` when the matrix declares a `protocol:` block, and
237 /// the reason the fallback was taken otherwise.
238 ///
239 /// # Errors
240 /// When the matrix has no `protocol:` block; the error is the provenance
241 /// note a caller puts in `unproduced_fields`.
242 pub fn source() -> Result<&'static str, String> {
243 Self::from_matrix().map(|_| "perf-matrix.yaml `protocol:`")
244 }
245}
246
247/// PP-33 — where a [`ProtocolParams`] came from.
248///
249/// Not a boolean: the fallback carries the reason it was taken, because "the
250/// matrix has no `protocol:` block" and "the matrix does not parse as YAML" ask
251/// for different fixes and a receipt that says only "fallback" tells the reader
252/// neither.
253#[derive(Debug, Clone, PartialEq, Eq)]
254pub enum ProtocolSource {
255 /// Read from the compiled-in `scripts/perf-matrix.yaml`.
256 Matrix,
257 /// The matrix could not supply them; the reason is carried verbatim.
258 SpecFallback(String),
259}
260
261impl ProtocolSource {
262 /// The single line the producer prints before the first request.
263 #[must_use]
264 pub fn announcement(&self) -> String {
265 match self {
266 Self::Matrix => "protocol: matrix (scripts/perf-matrix.yaml `protocol:`)".to_string(),
267 Self::SpecFallback(reason) => {
268 format!("protocol: spec fallback because {reason}")
269 }
270 }
271 }
272
273 /// The `unproduced_fields` entry, or `None` when the matrix supplied the
274 /// parameters and there is nothing unproduced.
275 #[must_use]
276 pub fn unproduced_note(&self) -> Option<String> {
277 match self {
278 Self::Matrix => None,
279 Self::SpecFallback(reason) => Some(format!(
280 "PP-33 protocol — the `protocol:` block on this receipt is the compiled-in Rust \
281 spec fallback, NOT scripts/perf-matrix.yaml: {reason}. Every protocol parameter \
282 a gate compares against must live in the matrix; these came from consts and are \
283 unverifiable against it."
284 )),
285 }
286 }
287}
288
289/// PP-27 — `stream.live_ttft_over_e2e_max` from the matrix.
290///
291/// # Errors
292/// When the matrix has no `stream:` block or no `live_ttft_over_e2e_max` key.
293pub fn stream_live_ttft_over_e2e_max_from(source: &str) -> Result<f64, String> {
294 let block = matrix_block(source, "stream")?;
295 let raw: MatrixStreamBlock = serde_yaml_ng::from_value(block)
296 .map_err(|e| format!("perf-matrix.yaml `stream:` block: {e}"))?;
297 Ok(raw.live_ttft_over_e2e_max)
298}
299
300/// [`stream_live_ttft_over_e2e_max_from`] over the compiled-in matrix, falling
301/// back to [`STREAM_LIVE_TTFT_OVER_E2E_MAX`] when the block is absent.
302#[must_use]
303pub fn stream_live_ttft_over_e2e_max() -> f64 {
304 stream_live_ttft_over_e2e_max_from(PERF_MATRIX_SOURCE).unwrap_or(STREAM_LIVE_TTFT_OVER_E2E_MAX)
305}
306
307/// PP-26 — `witness.min_agree_tokens` from the matrix.
308///
309/// # Errors
310/// When the matrix has no `witness:` block or no `min_agree_tokens` key.
311pub fn witness_min_agree_tokens_from(source: &str) -> Result<u32, String> {
312 let block = matrix_block(source, "witness")?;
313 let raw: MatrixWitnessBlock = serde_yaml_ng::from_value(block)
314 .map_err(|e| format!("perf-matrix.yaml `witness:` block: {e}"))?;
315 Ok(raw.min_agree_tokens)
316}
317
318/// [`witness_min_agree_tokens_from`] over the compiled-in matrix, falling back
319/// to [`WITNESS_MIN_AGREE_TOKENS`] when the block is absent.
320#[must_use]
321pub fn witness_min_agree_tokens() -> u32 {
322 witness_min_agree_tokens_from(PERF_MATRIX_SOURCE).unwrap_or(WITNESS_MIN_AGREE_TOKENS)
323}
324
325/// Pull one top-level block out of the matrix, naming what is missing.
326fn matrix_block(source: &str, key: &str) -> Result<serde_yaml_ng::Value, String> {
327 let doc: serde_yaml_ng::Value = serde_yaml_ng::from_str(source)
328 .map_err(|e| format!("perf-matrix.yaml does not parse as YAML: {e}"))?;
329 doc.get(key).cloned().ok_or_else(|| {
330 format!(
331 "perf-matrix.yaml has no `{key}:` block — PP-33 requires every protocol parameter and \
332 threshold to live there; refusing to substitute the Rust spec fallback silently"
333 )
334 })
335}
336
337/// The matrix spelling of the protocol block. Governance keys
338/// (`threshold_class`, `author`, `prompt_tokens`) are ignored here rather than
339/// mirrored, so adding one does not have to touch this crate.
340///
341/// These three `Matrix*` readers are the deliberate exception to the
342/// `deny_unknown_fields` rule every other `Deserialize` type in `perf_gate/`
343/// carries: PP-33 requires the matrix to hold governance metadata beside each
344/// number, and refusing an unknown key here would make adding an `author:` to
345/// `perf-matrix.yaml` fail this crate's build. The receipt types are the ones
346/// that must refuse a key they do not understand — the receipt is the evidence,
347/// the matrix is the policy.
348#[derive(Debug, Deserialize)]
349struct MatrixProtocolBlock {
350 window_ms: u64,
351 warmup_requests_per_worker: u32,
352 quiesce_ms: u64,
353 cooldown_ms: u64,
354 n_predict: u32,
355 replicates_min: u32,
356 interleaved: bool,
357 sampler: Sampler,
358}
359
360#[derive(Debug, Deserialize)]
361struct MatrixStreamBlock {
362 live_ttft_over_e2e_max: f64,
363}
364
365#[derive(Debug, Deserialize)]
366struct MatrixWitnessBlock {
367 min_agree_tokens: u32,
368}
369
370/// One band's measurement parameters.
371///
372/// [`BandConfig::conformant`] is the only constructor that produces §4.4-legal
373/// values. [`BandConfig::relaxed`] exists so unit tests can exercise the driver
374/// in milliseconds instead of minutes — and every run carries
375/// [`BandConfig::conformance_violations`] into its receipt, so a relaxed run is
376/// self-identifying rather than indistinguishable from a real one. A knob that
377/// lets you shrink the window without saying so is how a gate stops being able
378/// to fail.
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380#[serde(deny_unknown_fields)]
381pub struct BandConfig {
382 /// Fixed concurrency `c`.
383 pub concurrency: usize,
384 /// Warmup requests, discarded and never written to the receipt.
385 pub warmup_requests: usize,
386 /// Quiesce between warmup completion and the first sampled request.
387 pub quiesce: Duration,
388 /// Minimum sampled requests to issue before the window may close.
389 pub min_samples: usize,
390 /// Minimum wall-clock the window must stay open.
391 pub min_wall_clock: Duration,
392 /// Hard per-request timeout.
393 pub request_timeout: Duration,
394 /// §5.1 — the pause between the two lanes of one interleaved replicate.
395 /// Without it the second lane inherits the first lane's thermal and VRAM
396 /// state, which is the drift interleaving exists to cancel.
397 pub cooldown: Duration,
398 /// Recorded, not assumed.
399 pub client_model: ClientModel,
400}
401
402impl BandConfig {
403 /// The §4.4-conformant configuration for concurrency `c`.
404 ///
405 /// # Panics
406 /// Never; `concurrency` of 0 is clamped to 1 so the worker pool always has
407 /// a worker. A zero-worker band is unrepresentable rather than empty.
408 #[must_use]
409 pub fn conformant(concurrency: usize) -> Self {
410 let concurrency = concurrency.max(1);
411 Self {
412 concurrency,
413 warmup_requests: warmup_requests(concurrency),
414 quiesce: QUIESCE,
415 min_samples: min_sampled_requests(concurrency),
416 min_wall_clock: MIN_WALL_CLOCK,
417 request_timeout: REQUEST_TIMEOUT,
418 cooldown: COOLDOWN,
419 client_model: ClientModel::ClosedLoop,
420 }
421 }
422
423 /// A shrunken configuration for tests and smoke runs. **Never conformant**:
424 /// [`Self::conformance_violations`] is non-empty by construction unless the
425 /// caller happens to pass the spec values back in.
426 #[must_use]
427 pub fn relaxed(
428 concurrency: usize,
429 min_samples: usize,
430 min_wall_clock: Duration,
431 quiesce: Duration,
432 ) -> Self {
433 let concurrency = concurrency.max(1);
434 Self {
435 concurrency,
436 warmup_requests: warmup_requests(concurrency),
437 quiesce,
438 min_samples,
439 min_wall_clock,
440 request_timeout: REQUEST_TIMEOUT,
441 cooldown: COOLDOWN,
442 client_model: ClientModel::ClosedLoop,
443 }
444 }
445
446 /// [`Self::relaxed`] with the §5.1 cooldown shrunk too, for tests that must
447 /// exercise the cooldown departure itself.
448 #[must_use]
449 pub fn relaxed_with_cooldown(
450 concurrency: usize,
451 min_samples: usize,
452 min_wall_clock: Duration,
453 quiesce: Duration,
454 cooldown: Duration,
455 ) -> Self {
456 Self {
457 cooldown,
458 ..Self::relaxed(concurrency, min_samples, min_wall_clock, quiesce)
459 }
460 }
461
462 /// Every way this configuration departs from the protocol, in prose, for
463 /// the receipt.
464 ///
465 /// Compares against [`ProtocolParams::effective`] — the `protocol:` block of
466 /// `perf-matrix.yaml` when it exists (PP-33), the spec fallback otherwise —
467 /// so amending the matrix moves this predicate rather than a Rust literal.
468 #[must_use]
469 pub fn conformance_violations(&self) -> Vec<String> {
470 self.conformance_violations_against(&ProtocolParams::effective())
471 }
472
473 /// [`Self::conformance_violations`] against explicit parameters, so a test
474 /// can prove the predicate reads the loaded block and not a constant.
475 #[must_use]
476 pub fn conformance_violations_against(&self, params: &ProtocolParams) -> Vec<String> {
477 let mut out = Vec::new();
478 let want_warmup = params.warmup_requests_per_worker as usize * self.concurrency;
479 if self.warmup_requests < want_warmup {
480 out.push(format!(
481 "§4.4.2 warmup_requests={} < {}*c={want_warmup}",
482 self.warmup_requests, params.warmup_requests_per_worker
483 ));
484 }
485 let want_quiesce = Duration::from_millis(params.quiesce_ms);
486 if self.quiesce < want_quiesce {
487 out.push(format!(
488 "§4.4.2 quiesce={:?} < {want_quiesce:?}",
489 self.quiesce
490 ));
491 }
492 let want_samples = min_sampled_requests(self.concurrency);
493 if self.min_samples < want_samples {
494 out.push(format!(
495 "§4.4.2 min_samples={} < max(30, 8*c)={want_samples}",
496 self.min_samples
497 ));
498 }
499 let want_window = Duration::from_millis(params.window_ms);
500 if self.min_wall_clock < want_window {
501 out.push(format!(
502 "§5.1 min_wall_clock={:?} < window_ms={want_window:?}",
503 self.min_wall_clock
504 ));
505 }
506 let want_cooldown = Duration::from_millis(params.cooldown_ms);
507 if self.cooldown < want_cooldown {
508 out.push(format!(
509 "§5.1 cooldown={:?} < cooldown_ms={want_cooldown:?}",
510 self.cooldown
511 ));
512 }
513 if self.request_timeout != REQUEST_TIMEOUT {
514 out.push(format!(
515 "§4.4.3 request_timeout={:?} != 120s",
516 self.request_timeout
517 ));
518 }
519 out
520 }
521
522 /// True when [`Self::conformance_violations`] is empty.
523 #[must_use]
524 pub fn is_conformant(&self) -> bool {
525 self.conformance_violations().is_empty()
526 }
527}
528
529/// §4.4.3 / §4.4.7 — how one request ended, and the §4.4.7 `SUSPECT` fraction.
530///
531/// Both are defined once, in [`super::drain`], and re-exported here so
532/// §4.4.1-§4.4.5 code keeps its `protocol::` path. The four outcomes are the
533/// four counters the receipt must carry, and they are mutually exclusive, so
534/// `requested = completed + timeouts + truncated + errors` holds by
535/// construction rather than by convention.
536///
537/// **[`Outcome::AbandonedAtDrain`] is the §4.4.7 sense of `truncated`, NOT
538/// `finish_reason == "length"`.** Under W1 every request stops at
539/// `max_tokens = 128` with EOS ignored, so reading `truncated` as the
540/// finish-reason sense would exclude the entire workload from `agg_tok_s`'s
541/// numerator and report zero throughput for a healthy server. The variant is
542/// spelled `AbandonedAtDrain` rather than `Truncated` precisely so the two
543/// senses cannot be conflated; conflating them is how two conformant harnesses
544/// produce incomparable receipts.
545pub use super::drain::{Outcome, DRAIN_SUSPECT_FRACTION};
546
547/// §4.4.6 — the `tokenization` block lives in [`super::receipt`].
548///
549/// This module used to carry its own `Tokenization` struct with an
550/// `Option<String>` digest and a `validate()`.
551/// [`super::receipt::TokenizationBlock`] is the same §4.4.6 block as an enum,
552/// in which "`client_tokenizer` with no digest" is unrepresentable rather than
553/// merely rejected, and it is the one wired into the emitter
554/// `scripts/perf_gate.sh` reads. Keeping both would be two spellings of one
555/// schema, free to drift.
556pub use super::receipt::{TokenCountingMethod, TokenizationBlock};
557
558#[cfg(test)]
559mod tests {
560 use super::*;
561
562 /// A `perf-matrix.yaml` fragment carrying exactly the keys PP-33 puts
563 /// there. Used so the loader is exercised whether or not the shipped matrix
564 /// has been amended yet.
565 const FIXTURE_MATRIX: &str = "\
566schema_version: 2
567protocol:
568 window_ms: 60000
569 warmup_requests_per_worker: 2
570 quiesce_ms: 5000
571 cooldown_ms: 10000
572 n_predict: 128
573 prompt_tokens: 512
574 replicates_min: 5
575 interleaved: true
576 sampler: {temperature: 0.0, seed: 0, ignore_eos: true}
577 threshold_class: policy
578 author: spec-owner
579stream:
580 live_ttft_over_e2e_max: 0.95
581 threshold_class: policy
582 author: spec-owner
583witness:
584 min_agree_tokens: 64
585 threshold_class: policy
586 author: spec-owner
587";
588
589 #[test]
590 fn min_sampled_requests_is_max_30_or_8c() {
591 assert_eq!(min_sampled_requests(1), 30);
592 assert_eq!(min_sampled_requests(3), 30);
593 assert_eq!(min_sampled_requests(4), 32);
594 assert_eq!(min_sampled_requests(8), 64);
595 assert_eq!(min_sampled_requests(16), 128);
596 }
597
598 #[test]
599 fn warmup_is_two_per_worker() {
600 for c in [1_usize, 4, 8, 16] {
601 assert_eq!(warmup_requests(c), 2 * c);
602 }
603 }
604
605 /// The shipped defaults ARE the spec values. If someone edits a constant,
606 /// this is the test that reds.
607 #[test]
608 fn conformant_config_matches_the_spec_literals() {
609 for c in [1_usize, 4, 8, 16] {
610 let cfg = BandConfig::conformant(c);
611 assert_eq!(cfg.concurrency, c);
612 assert_eq!(cfg.warmup_requests, 2 * c);
613 assert_eq!(cfg.quiesce, Duration::from_secs(5));
614 assert_eq!(cfg.min_samples, 30.max(8 * c));
615 assert_eq!(cfg.min_wall_clock, Duration::from_secs(60));
616 assert_eq!(cfg.request_timeout, Duration::from_secs(120));
617 assert_eq!(cfg.cooldown, Duration::from_secs(10));
618 assert_eq!(cfg.client_model, ClientModel::ClosedLoop);
619 assert!(
620 cfg.is_conformant(),
621 "violations: {:?}",
622 cfg.conformance_violations()
623 );
624 }
625 }
626
627 /// A matrix source with no `protocol:` block does not quietly become the
628 /// Rust constants: the reader says which block is missing (PP-33).
629 #[test]
630 fn a_matrix_without_a_protocol_block_is_an_error_not_a_default() {
631 let err = ProtocolParams::from_matrix_source("schema_version: 2\nbands: [1, 4]\n")
632 .expect_err("no protocol block");
633 assert!(err.contains("`protocol:`"), "{err}");
634 assert!(err.contains("PP-33"), "{err}");
635 }
636
637 /// And a block that is present is read field by field.
638 #[test]
639 fn the_protocol_block_is_read_from_the_matrix() {
640 let p = ProtocolParams::from_matrix_source(FIXTURE_MATRIX).expect("block parses");
641 assert_eq!(p.window_ms, 60_000);
642 assert_eq!(p.warmup_requests_per_worker, 2);
643 assert_eq!(p.quiesce_ms, 5_000);
644 assert_eq!(p.cooldown_ms, 10_000);
645 assert_eq!(p.n_predict, 128);
646 assert_eq!(
647 p.replicates, 5,
648 "matrix `replicates_min` is the receipt's n floor"
649 );
650 assert!(p.interleaved);
651 assert_eq!(p.sampler.temperature, 0.0);
652 assert_eq!(p.sampler.seed, 0);
653 assert!(p.sampler.ignore_eos);
654 }
655
656 /// THE POINT: the conformance predicate reads the LOADED parameters. Feed a
657 /// matrix declaring a 120 s window and a 60 s band stops being conformant —
658 /// which a predicate hard-coded to `MIN_WALL_CLOCK` could not do.
659 #[test]
660 fn conformance_violations_read_the_loaded_params_not_the_consts() {
661 let cfg = BandConfig::conformant(4);
662 let spec = ProtocolParams::spec_fallback();
663 assert!(cfg.conformance_violations_against(&spec).is_empty());
664
665 let wider = ProtocolParams {
666 window_ms: 120_000,
667 ..spec
668 };
669 let v = cfg.conformance_violations_against(&wider);
670 assert_eq!(v.len(), 1, "{v:?}");
671 assert!(v[0].contains("min_wall_clock"), "{v:?}");
672 }
673
674 /// The cooldown is a departure like any other, and it is checked.
675 #[test]
676 fn a_missing_cooldown_is_a_conformance_violation() {
677 let cfg = BandConfig::relaxed_with_cooldown(
678 4,
679 32,
680 Duration::from_secs(60),
681 Duration::from_secs(5),
682 Duration::ZERO,
683 );
684 let v = cfg.conformance_violations();
685 assert_eq!(v.len(), 1, "{v:?}");
686 assert!(v[0].contains("cooldown"), "{v:?}");
687 }
688
689 /// `effective()` takes the matrix when it has the block and the documented
690 /// fallback otherwise, and `source()` says which — so a caller can name the
691 /// substitution in `unproduced_fields` rather than have it be silent.
692 #[test]
693 fn the_effective_params_say_where_they_came_from() {
694 let effective = ProtocolParams::effective();
695 match ProtocolParams::source() {
696 Ok(where_from) => {
697 assert_eq!(where_from, "perf-matrix.yaml `protocol:`");
698 assert_eq!(effective, ProtocolParams::from_matrix().expect("block"));
699 }
700 Err(reason) => {
701 assert!(reason.contains("`protocol:`"), "{reason}");
702 assert_eq!(effective, ProtocolParams::spec_fallback());
703 }
704 }
705 }
706
707 /// PP-27's and PP-26's numbers come out of the matrix too.
708 #[test]
709 fn the_stream_and_witness_thresholds_are_read_from_the_matrix() {
710 assert_eq!(
711 stream_live_ttft_over_e2e_max_from(FIXTURE_MATRIX).expect("stream block"),
712 0.95
713 );
714 assert_eq!(
715 witness_min_agree_tokens_from(FIXTURE_MATRIX).expect("witness block"),
716 64
717 );
718 assert!(stream_live_ttft_over_e2e_max_from("bands: [1]\n").is_err());
719 assert!(witness_min_agree_tokens_from("bands: [1]\n").is_err());
720 }
721
722 /// When `scripts/perf-matrix.yaml` DOES declare the block, it must agree
723 /// with the fallback the tests compare against — otherwise the two drift
724 /// and a receipt is written under one protocol and validated under another.
725 #[test]
726 fn the_shipped_matrix_block_when_present_agrees_with_the_spec_fallback() {
727 match ProtocolParams::from_matrix() {
728 Ok(loaded) => assert_eq!(
729 loaded,
730 ProtocolParams::spec_fallback(),
731 "scripts/perf-matrix.yaml `protocol:` disagrees with protocol.rs's fallback"
732 ),
733 Err(reason) => assert!(
734 reason.contains("`protocol:`"),
735 "the only acceptable absence is a named one: {reason}"
736 ),
737 }
738 }
739
740 /// PP-33 — `effective_with_source` says WHICH of the two sources supplied
741 /// the block, and the fallback carries the reason.
742 ///
743 /// `effective()` swallowed the error and `source()` was never called by
744 /// anything, so a run whose matrix did not parse put the compiled-in Rust
745 /// constants on the wire under a `protocol:` block the receipt then
746 /// presented as the matrix's. PP-33's whole point is that every number a
747 /// gate compares against lives in `perf-matrix.yaml`; a silent compiled-in
748 /// substitute is the drift it exists to prevent.
749 #[test]
750 fn the_protocol_source_is_reported_and_the_fallback_says_why() {
751 let (params, source) = ProtocolParams::effective_with_source();
752 match ProtocolParams::from_matrix() {
753 Ok(from_matrix) => {
754 assert_eq!(source, ProtocolSource::Matrix);
755 assert_eq!(params, from_matrix);
756 assert!(source.announcement().contains("matrix"), "{source:?}");
757 assert!(
758 source.unproduced_note().is_none(),
759 "the matrix supplied them; nothing is unproduced"
760 );
761 }
762 Err(reason) => {
763 assert_eq!(source, ProtocolSource::SpecFallback(reason.clone()));
764 assert_eq!(params, ProtocolParams::spec_fallback());
765 }
766 }
767
768 // The fallback's own two obligations, whatever the shipped matrix does:
769 // it names the reason on stdout, and it names itself in the receipt.
770 let fallback =
771 ProtocolSource::SpecFallback("perf-matrix.yaml has no `protocol:` block".to_string());
772 assert_eq!(
773 fallback.announcement(),
774 "protocol: spec fallback because perf-matrix.yaml has no `protocol:` block"
775 );
776 let note = fallback
777 .unproduced_note()
778 .expect("a fallback is an unproduced field");
779 assert!(note.contains("PP-33"), "{note}");
780 assert!(note.contains("no `protocol:` block"), "{note}");
781 assert!(
782 note.contains("NOT scripts/perf-matrix.yaml"),
783 "the note must say the block on the wire is not the matrix's: {note}"
784 );
785 }
786
787 /// The escape hatch must be visible from the receipt. A relaxed run that
788 /// reported itself conformant is exactly the fabricated-baseline class.
789 #[test]
790 fn relaxed_config_reports_every_departure() {
791 let cfg = BandConfig::relaxed(4, 8, Duration::from_millis(50), Duration::ZERO);
792 assert!(!cfg.is_conformant());
793 let v = cfg.conformance_violations();
794 assert_eq!(
795 v.len(),
796 3,
797 "expected quiesce+min_samples+min_wall, got {v:?}"
798 );
799 assert!(v.iter().any(|s| s.contains("quiesce")));
800 assert!(v.iter().any(|s| s.contains("min_samples")));
801 assert!(v.iter().any(|s| s.contains("min_wall_clock")));
802 }
803
804 #[test]
805 fn client_model_serializes_as_closed_loop() {
806 let j =
807 serde_json::to_string(&ClientModel::ClosedLoop).expect("ClientModel must serialize");
808 assert_eq!(j, "\"closed_loop\"");
809 }
810
811 /// `protocol::REQUEST_TIMEOUT` (a `Duration`) and `drain::REQUEST_TIMEOUT_MS`
812 /// (an `f64`) are the same §4.4.3 limit in two types the compiler cannot
813 /// unify. Editing one without the other is the drift this pins.
814 #[test]
815 fn the_two_request_timeout_spellings_agree() {
816 assert_eq!(
817 REQUEST_TIMEOUT.as_millis(),
818 u128::from(super::super::drain::REQUEST_TIMEOUT_MS as u64)
819 );
820 }
821
822 /// The §4.4.6 block is `receipt::TokenizationBlock`; the poka-yoke that used
823 /// to live on `protocol::Tokenization` moved with it and is still enforced.
824 #[test]
825 fn declared_method_and_available_counter_must_agree() {
826 let ct = TokenizationBlock::ClientTokenizer {
827 tokenizer_sha256: "c".repeat(64),
828 counts_special_tokens: true,
829 counts_prompt_echo: false,
830 };
831 assert!(ct.validate().is_ok());
832 assert!(ct.require_counter(false).is_err());
833 assert!(ct.require_counter(true).is_ok());
834
835 let su = TokenizationBlock::ServerUsage {
836 counts_special_tokens: true,
837 counts_prompt_echo: false,
838 };
839 assert!(su.require_counter(true).is_err());
840 assert!(su.require_counter(false).is_ok());
841 }
842}