Skip to main content

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}