jugar_probar/perf_gate/join.rs
1//! PP-LLAMA-001 v3.0 PP-3 / PP-22 / P-5 — the join key, and the only shape a
2//! ratio may take.
3//!
4//! # Why a ratio is a type and not an `f64`
5//!
6//! `scripts/lib/perf_receipt.py` used to emit `agg_ratio` and `decode_ratio` as
7//! bare scalars beside a band. A bare scalar cannot say which comparator run it
8//! divided by, whether that run was the same invocation, whether its band was
9//! the same `c`, its window the same length, or whether either lane timed out.
10//! Every one of those is a way the number is wrong, and none of them is visible
11//! in the number.
12//!
13//! So there is no `From<f64> for Ratio` and no public field assignment that
14//! makes one: [`super::drain::ComparatorStatus::Measured`] is constructible only
15//! through [`super::drain::BandInput::join`], which refuses
16//!
17//! - a comparator lane from a different `run_id` (PP-3 — "shares `run_id`"),
18//! - a [`JoinKey`] mismatch on any of the fourteen fields (PP-22),
19//! - `timeouts > 0` on either lane (PP-5),
20//! - a comparator configured with `n_batch = 1` (§5.3's recorded dissent: a
21//! `-b 1` comparator is a cripple, and once manufactured a 2.39×
22//! overstatement).
23//!
24//! # The two estimators (§4.3)
25//!
26//! | unit | metrics | estimator | [`RatioMethod`] |
27//! |---|---|---|---|
28//! | replicate (window statistics) | `agg`, `prefill` | mean of per-replicate `ln(subject/comparator)`, one-sided t lower bound, exponentiated | [`RatioMethod::ReplicateTLower`] |
29//! | request (per-request statistics) | `dec`, `ttft`, `itl_p95` | paired percentile bootstrap, 10 000 resamples, seed 2026 | [`RatioMethod::PairedPercentileBootstrap`] |
30//!
31//! `lcb95` is `None` — not `0.0`, and not the point estimate — when the design
32//! cannot support a bound (`n < 5` replicates, fewer than two requests). §4.3:
33//! "`n = 3` sizes an effect and bounds no variance."
34
35use serde::{Deserialize, Serialize};
36
37use super::drain::BandInput;
38use super::receipt::{ReceiptInput, TokenCountingMethod, Workload};
39
40/// Which estimator produced a [`Ratio`] (§4.3). On the wire as a snake_case
41/// token so a reader can tell a window statistic from a request statistic
42/// without knowing the metric's name.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum RatioMethod {
46 /// §4.3 request unit: paired percentile bootstrap, 5th percentile.
47 PairedPercentileBootstrap,
48 /// §4.3 replicate unit: one-sided t lower bound on the mean log-ratio.
49 ReplicateTLower,
50}
51
52impl RatioMethod {
53 /// The wire token.
54 #[must_use]
55 pub fn wire_token(self) -> &'static str {
56 match self {
57 Self::PairedPercentileBootstrap => "paired_percentile_bootstrap",
58 Self::ReplicateTLower => "replicate_t_lower",
59 }
60 }
61}
62
63/// P-5 — one metric's ratio, with the bound the verdict is taken on.
64///
65/// `point` is `x_subject / x_comparator`. `lcb95` is the one-sided 95% lower
66/// confidence bound; `None` means the design could not support one and the
67/// ratio is REPORTING only.
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct Ratio {
71 /// The ratio on the observed data.
72 pub point: f64,
73 /// One-sided 95% lower bound, or `None` when the design cannot support one.
74 pub lcb95: Option<f64>,
75 /// Which estimator produced it.
76 pub method: RatioMethod,
77 /// Observations the estimator used — replicates or requests, per `method`.
78 pub n: usize,
79}
80
81impl Ratio {
82 /// A ratio with no bound, for a design that cannot support one (`n < 5`
83 /// replicates). REPORTING only: P-5's verdict needs `lcb95`.
84 #[must_use]
85 pub fn reporting_only(point: f64, method: RatioMethod, n: usize) -> Self {
86 Self {
87 point,
88 lcb95: None,
89 method,
90 n,
91 }
92 }
93
94 /// P-5 — does this ratio PASS at non-inferiority margin `delta`?
95 ///
96 /// `false` when there is no bound: a ratio without a lower bound has not
97 /// been shown to be anything, and "no evidence" is not a pass.
98 #[must_use]
99 pub fn passes(&self, delta: f64) -> bool {
100 self.lcb95.is_some_and(|l| l >= 1.0 - delta)
101 }
102}
103
104/// The estimators' return type. The same struct as [`Ratio`] under the name the
105/// statistics modules use for it, so `bootstrap::paired_ratio_lcb` and
106/// `replicate::log_ratio_lcb` produce a value that goes on the wire unchanged
107/// rather than through a lossy conversion.
108pub type RatioBound = Ratio;
109
110/// P-3 — the three ratios a band may carry. `agg` is always present when the
111/// band joined at all; `dec` and `prefill` are `None` when the lane could not
112/// produce the metric (no streaming, no server timings).
113#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
114#[serde(deny_unknown_fields)]
115pub struct BandRatios {
116 /// Aggregate throughput ratio. Gated at c>1 (§7.2).
117 pub agg: Ratio,
118 /// Per-request decode ratio. Gated at c=1 (§7.2).
119 pub dec: Option<Ratio>,
120 /// Server-reported prefill ratio. Gated at c=1 (§7.2).
121 pub prefill: Option<Ratio>,
122}
123
124/// PP-22 — the fourteen fields two bands must agree on before their numbers may
125/// be divided.
126///
127/// Each field is a way two measurements can look comparable and not be. `c=4`
128/// against `c=16` compares different offered loads; a 30 s window against a 60 s
129/// one compares different amounts of thermal drift; `n_batch = 1` against a
130/// served comparator compares a working server against a crippled one.
131#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
132#[serde(deny_unknown_fields)]
133pub struct JoinKey {
134 /// Which host.
135 pub host: String,
136 /// Which workload.
137 pub workload: Workload,
138 /// The band's concurrency `c`.
139 pub band: u32,
140 /// Which model.
141 pub model: String,
142 /// Which quantization.
143 pub quant: String,
144 /// How tokens were counted (§4.4.6).
145 pub tokenization: TokenCountingMethod,
146 /// The measurement window, in milliseconds.
147 pub window_ms: u64,
148 /// Replicates run.
149 pub replicates: u32,
150 /// Whether those replicates alternated.
151 pub interleaved: bool,
152 /// Comparator `-c` per slot. `None` when the lane did not report one.
153 pub n_ctx_slot: Option<u32>,
154 /// KV cache type, e.g. `f16`.
155 pub kv_type: Option<String>,
156 /// Flash attention.
157 pub fa: Option<bool>,
158 /// Comparator `-b`. `Some(1)` is refused outright (§5.3).
159 pub n_batch: Option<u32>,
160 /// Generated tokens per request.
161 pub n_predict: u32,
162}
163
164impl JoinKey {
165 /// Build the key for one band of one receipt.
166 #[must_use]
167 pub fn of(receipt: &ReceiptInput, band: &BandInput) -> Self {
168 Self {
169 host: receipt.provenance.host.clone(),
170 workload: receipt.workload,
171 band: band.concurrency,
172 model: receipt.provenance.model.clone(),
173 quant: receipt.provenance.quantization.clone(),
174 tokenization: receipt.tokenization.method(),
175 // PP-22 keys on the DECLARED window (the protocol's), never the measured
176 // close instant: two lanes never close on the same millisecond.
177 window_ms: receipt.protocol.window_ms,
178 replicates: receipt.protocol.replicates,
179 interleaved: receipt.protocol.interleaved,
180 n_ctx_slot: band.lane.n_ctx_slot,
181 kv_type: band.lane.kv_type.clone(),
182 fa: band.lane.fa,
183 n_batch: band.lane.n_batch,
184 n_predict: receipt.protocol.n_predict,
185 }
186 }
187
188 /// §5.3 — a comparator serving one request at a time is not serving the
189 /// band. Checked on its own so a key can be rejected before it is compared
190 /// with anything.
191 ///
192 /// # Errors
193 /// When `n_batch == Some(1)`.
194 pub fn refuse_cripple(&self) -> Result<(), String> {
195 if self.n_batch == Some(1) {
196 return Err(format!(
197 "PP-22 join key at c={}: n_batch=1 — §5.3 refuses a `-b 1` comparator as a \
198 cripple; that configuration manufactured a 2.39x overstatement once \
199 (llama_pin.toml:129-165) and it is not a lane serving the band",
200 self.band
201 ));
202 }
203 Ok(())
204 }
205
206 /// PP-22 — refuse the join, naming **every** differing field.
207 ///
208 /// All of them, not the first: a caller told only "band differs" re-runs,
209 /// discovers the window differs too, and re-runs again. Both keys are also
210 /// checked for the `-b 1` cripple.
211 ///
212 /// # Errors
213 /// When any field differs, or when either key is a `-b 1` comparator.
214 pub fn refuse_mismatch(&self, other: &Self) -> Result<(), String> {
215 self.refuse_cripple()?;
216 other.refuse_cripple()?;
217 let mut differing = Vec::new();
218 let mut note = |name: &str, a: String, b: String| {
219 if a != b {
220 differing.push(format!("{name}: {a} != {b}"));
221 }
222 };
223 note("host", self.host.clone(), other.host.clone());
224 note(
225 "workload",
226 self.workload.wire_token().to_string(),
227 other.workload.wire_token().to_string(),
228 );
229 note("band", self.band.to_string(), other.band.to_string());
230 note("model", self.model.clone(), other.model.clone());
231 note("quant", self.quant.clone(), other.quant.clone());
232 note(
233 "tokenization",
234 self.tokenization.wire_token().to_string(),
235 other.tokenization.wire_token().to_string(),
236 );
237 note(
238 "window_ms",
239 self.window_ms.to_string(),
240 other.window_ms.to_string(),
241 );
242 note(
243 "replicates",
244 self.replicates.to_string(),
245 other.replicates.to_string(),
246 );
247 note(
248 "interleaved",
249 self.interleaved.to_string(),
250 other.interleaved.to_string(),
251 );
252 note(
253 "n_ctx_slot",
254 opt(self.n_ctx_slot.as_ref()),
255 opt(other.n_ctx_slot.as_ref()),
256 );
257 note(
258 "kv_type",
259 opt(self.kv_type.as_ref()),
260 opt(other.kv_type.as_ref()),
261 );
262 note("fa", opt(self.fa.as_ref()), opt(other.fa.as_ref()));
263 note(
264 "n_batch",
265 opt(self.n_batch.as_ref()),
266 opt(other.n_batch.as_ref()),
267 );
268 note(
269 "n_predict",
270 self.n_predict.to_string(),
271 other.n_predict.to_string(),
272 );
273 if differing.is_empty() {
274 return Ok(());
275 }
276 Err(format!(
277 "PP-22 join refused: {} — two bands that differ on any join field are not two \
278 measurements of the same thing, and their quotient is not a ratio",
279 differing.join("; ")
280 ))
281 }
282}
283
284fn opt<T: std::fmt::Display>(v: Option<&T>) -> String {
285 v.map_or_else(|| "null".to_string(), std::string::ToString::to_string)
286}
287
288#[cfg(test)]
289mod tests {
290 // The `<selftest-name>__<sentence>` spelling is load-bearing: PP-29's
291 // `scripts/spec_conformance.sh` joins the §6 invariant table to the test
292 // list on the prefix before the double underscore, so renaming these to
293 // single-underscore snake case would silently unjoin the rows they arm.
294 #![allow(non_snake_case)]
295 use super::*;
296
297 fn key(band: u32) -> JoinKey {
298 JoinKey {
299 host: "lambda".to_string(),
300 workload: Workload::W1,
301 band,
302 model: "qwen2.5-coder-7b-apache-q4k-v1".to_string(),
303 quant: "Q4_K_M".to_string(),
304 tokenization: TokenCountingMethod::ClientTokenizer,
305 window_ms: 60_000,
306 replicates: 5,
307 interleaved: true,
308 n_ctx_slot: Some(1024),
309 kv_type: Some("f16".to_string()),
310 fa: Some(true),
311 n_batch: Some(2048),
312 n_predict: 128,
313 }
314 }
315
316 /// PP-22 must-not-fire: identical keys join.
317 #[test]
318 fn join_ok__matching_keys_join() {
319 assert!(key(4).refuse_mismatch(&key(4)).is_ok());
320 }
321
322 /// PP-22 must-fire, first spelling: two different offered loads.
323 #[test]
324 fn join_mismatch__c4_against_c16_is_refused() {
325 let err = key(4).refuse_mismatch(&key(16)).expect_err("c differs");
326 assert!(err.contains("band: 4 != 16"), "{err}");
327 assert!(err.contains("PP-22"), "{err}");
328 }
329
330 /// PP-22 must-fire, second spelling: two different amounts of drift.
331 #[test]
332 fn joining_a_30s_window_with_a_60s_window_is_refused() {
333 let short = JoinKey {
334 window_ms: 30_000,
335 ..key(4)
336 };
337 let err = short.refuse_mismatch(&key(4)).expect_err("window differs");
338 assert!(err.contains("window_ms: 30000 != 60000"), "{err}");
339 }
340
341 /// PP-22 must-fire, third spelling: §5.3's recorded dissent.
342 #[test]
343 fn a_b1_comparator_is_refused_as_a_cripple() {
344 let crippled = JoinKey {
345 n_batch: Some(1),
346 ..key(4)
347 };
348 let err = key(4)
349 .refuse_mismatch(&crippled)
350 .expect_err("-b 1 comparator");
351 assert!(err.contains("cripple"), "{err}");
352 // And it is refused even when BOTH lanes were crippled identically —
353 // two crippled lanes agree on every field and are still not a parity
354 // measurement.
355 assert!(crippled.refuse_mismatch(&crippled).is_err());
356 }
357
358 /// Every differing field is named, not just the first one found.
359 #[test]
360 fn a_mismatch_names_every_differing_field() {
361 let other = JoinKey {
362 host: "gx10".to_string(),
363 window_ms: 30_000,
364 interleaved: false,
365 kv_type: Some("q8_0".to_string()),
366 fa: None,
367 ..key(16)
368 };
369 let err = key(4).refuse_mismatch(&other).expect_err("many differ");
370 for field in ["host", "band", "window_ms", "interleaved", "kv_type", "fa"] {
371 assert!(err.contains(field), "{field} missing from: {err}");
372 }
373 }
374
375 /// A `None` on one side and a value on the other is a difference, not a
376 /// wildcard: "the comparator did not report `n_ctx_slot`" is exactly the
377 /// case where the ratio must not be formed.
378 #[test]
379 fn an_absent_field_does_not_match_a_present_one() {
380 let unreported = JoinKey {
381 n_ctx_slot: None,
382 ..key(4)
383 };
384 let err = key(4)
385 .refuse_mismatch(&unreported)
386 .expect_err("null vs 1024");
387 assert!(err.contains("n_ctx_slot: 1024 != null"), "{err}");
388 }
389
390 /// P-5: a ratio with no bound has not been shown to be anything.
391 #[test]
392 fn a_ratio_without_a_bound_never_passes() {
393 let reporting = Ratio::reporting_only(1.42, RatioMethod::ReplicateTLower, 3);
394 assert!(!reporting.passes(0.0));
395 assert!(
396 !reporting.passes(0.5),
397 "no bound is not a pass at any delta"
398 );
399
400 let bounded = Ratio {
401 point: 1.02,
402 lcb95: Some(1.005),
403 method: RatioMethod::ReplicateTLower,
404 n: 5,
405 };
406 assert!(bounded.passes(0.0), "lcb95 >= 1 - 0 passes at parity");
407
408 let below = Ratio {
409 lcb95: Some(0.98),
410 ..bounded.clone()
411 };
412 assert!(!below.passes(0.0));
413 assert!(below.passes(0.05), "delta 0.05 admits an lcb95 of 0.98");
414 }
415
416 #[test]
417 fn ratio_method_wire_tokens_are_the_schema_spelling() {
418 assert_eq!(
419 RatioMethod::PairedPercentileBootstrap.wire_token(),
420 "paired_percentile_bootstrap"
421 );
422 assert_eq!(
423 RatioMethod::ReplicateTLower.wire_token(),
424 "replicate_t_lower"
425 );
426 let j = serde_json::to_string(&RatioMethod::ReplicateTLower).expect("serialises");
427 assert_eq!(j, "\"replicate_t_lower\"");
428 }
429}