Skip to main content

candle_graph/
comparison.rs

1//! Fail-closed replicated performance comparisons.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::Path;
5
6use anyhow::{ensure, Context, Result};
7use serde::{Deserialize, Serialize};
8
9use crate::artifact::{verify_bundle, verify_consumed_bundle_files};
10use crate::capability::MeasurementScope;
11use crate::trace::{analyze_health, parse_trace, ComparisonIdentity, TraceDocument};
12
13pub const SCHEMA: &str = "candle-graph/comparison/5";
14pub const MINIMUM_RUNS: usize = 5;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ComparisonVerdict {
19    Ineligible,
20    Inconclusive,
21    CandidateFaster,
22    CandidateSlower,
23}
24
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct SampleStatistics {
27    pub samples_ns: Vec<u64>,
28    pub median_ns: f64,
29    pub p95_ns: f64,
30    pub mad_ns: f64,
31}
32
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34pub struct ConfidenceInterval {
35    pub level: f64,
36    pub lower_delta_ns: f64,
37    pub upper_delta_ns: f64,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct TensorStatsComparisonRow {
42    pub label: String,
43    pub rms_a: f64,
44    pub rms_b: f64,
45    pub rms_ratio: Option<f64>,
46    pub abs_max_ratio: Option<f64>,
47    pub non_finite_a: u64,
48    pub non_finite_b: u64,
49    /// Events averaged into `rms_a`/`abs_max` for this label (duplicates included).
50    pub samples_a: usize,
51    pub samples_b: usize,
52    /// Cohort runs that contained this label; compare against the cohort run totals to see
53    /// whether an average covers the whole cohort or only part of it.
54    pub runs_a: usize,
55    pub runs_b: usize,
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
59pub struct TensorStatsComparison {
60    pub baseline_runs: usize,
61    pub candidate_runs: usize,
62    pub matched: Vec<TensorStatsComparisonRow>,
63    pub unmatched_a: Vec<String>,
64    pub unmatched_b: Vec<String>,
65}
66
67/// Trust state of the artifacts supplied to a comparison.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum ComparisonInputVerification {
71    VerifiedBundles,
72    UnverifiedTraces,
73}
74
75/// One content-addressed bundle input verified immediately before comparison.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct VerifiedBundleInput {
78    pub run_id: String,
79    pub manifest_sha256: String,
80}
81
82/// Cohort provenance that determines whether a comparison may be eligible.
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub struct ComparisonInputs {
85    pub verification: ComparisonInputVerification,
86    pub baseline: Vec<VerifiedBundleInput>,
87    pub candidate: Vec<VerifiedBundleInput>,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct ReplicatedComparison {
92    pub schema: String,
93    pub metric: String,
94    pub inputs: ComparisonInputs,
95    pub comparable: bool,
96    pub paired: bool,
97    pub verdict: ComparisonVerdict,
98    pub reasons: Vec<String>,
99    pub baseline_implementation_id: Option<String>,
100    pub candidate_implementation_id: Option<String>,
101    pub identity: Option<ComparisonIdentity>,
102    pub baseline: SampleStatistics,
103    pub candidate: SampleStatistics,
104    pub median_delta_ns: f64,
105    pub median_delta_percent: Option<f64>,
106    pub confidence_interval: Option<ConfidenceInterval>,
107    /// Numerical mechanism comparison, independent of timing eligibility.
108    #[serde(default)]
109    pub tensor_stats: TensorStatsComparison,
110}
111
112/// Verify finalized evidence bundles and compare their bound trace documents.
113pub fn compare_verified_bundles<B: AsRef<Path>, C: AsRef<Path>>(
114    baseline: &[B],
115    candidate: &[C],
116) -> Result<ReplicatedComparison> {
117    let (baseline_documents, baseline_inputs) = load_verified_cohort(baseline, "baseline")?;
118    let (candidate_documents, candidate_inputs) = load_verified_cohort(candidate, "candidate")?;
119    Ok(compare_documents(
120        &baseline_documents,
121        &candidate_documents,
122        ComparisonInputs {
123            verification: ComparisonInputVerification::VerifiedBundles,
124            baseline: baseline_inputs,
125            candidate: candidate_inputs,
126        },
127    ))
128}
129
130/// Compare raw trace documents for diagnostics only. This path is always ineligible.
131pub fn compare_unverified_traces(
132    baseline: &[TraceDocument],
133    candidate: &[TraceDocument],
134) -> ReplicatedComparison {
135    compare_documents(
136        baseline,
137        candidate,
138        ComparisonInputs {
139            verification: ComparisonInputVerification::UnverifiedTraces,
140            baseline: Vec::new(),
141            candidate: Vec::new(),
142        },
143    )
144}
145
146fn load_verified_cohort<P: AsRef<Path>>(
147    roots: &[P],
148    cohort: &str,
149) -> Result<(Vec<TraceDocument>, Vec<VerifiedBundleInput>)> {
150    let mut documents = Vec::with_capacity(roots.len());
151    let mut inputs = Vec::with_capacity(roots.len());
152    for (index, root) in roots.iter().enumerate() {
153        let root = root.as_ref();
154        let receipt = verify_bundle(root).with_context(|| {
155            format!("verify {cohort} bundle {} at {}", index + 1, root.display())
156        })?;
157        let document = parse_trace(root.join("trace.jsonl")).with_context(|| {
158            format!(
159                "parse verified {cohort} bundle {} trace at {}",
160                index + 1,
161                root.display()
162            )
163        })?;
164        ensure!(
165            document.run.run_id == receipt.run_id,
166            "verified {cohort} bundle {} manifest run ID {:?} does not match trace run ID {:?}",
167            index + 1,
168            receipt.run_id,
169            document.run.run_id
170        );
171        verify_consumed_bundle_files(root, &receipt, &["trace.jsonl"]).with_context(|| {
172            format!(
173                "post-read verify {cohort} bundle {} trace at {}",
174                index + 1,
175                root.display()
176            )
177        })?;
178        inputs.push(VerifiedBundleInput {
179            run_id: receipt.run_id,
180            manifest_sha256: receipt.manifest_sha256,
181        });
182        documents.push(document);
183    }
184    Ok((documents, inputs))
185}
186
187fn compare_documents(
188    baseline: &[TraceDocument],
189    candidate: &[TraceDocument],
190    inputs: ComparisonInputs,
191) -> ReplicatedComparison {
192    let mut reasons = Vec::new();
193    validate_input_provenance(&inputs, baseline, candidate, &mut reasons);
194    let baseline_samples = measured_samples(baseline, "baseline", &mut reasons);
195    let candidate_samples = measured_samples(candidate, "candidate", &mut reasons);
196    if baseline.len() < MINIMUM_RUNS || candidate.len() < MINIMUM_RUNS {
197        reasons.push(format!(
198            "at least {MINIMUM_RUNS} independent baseline and candidate runs are required"
199        ));
200    }
201    require_independent_run_ids(baseline, candidate, &mut reasons);
202    require_consistent_capture_semantics(baseline, candidate, &mut reasons);
203
204    let identity = common_identity(baseline, candidate, &mut reasons);
205    let baseline_implementation_id = cohort_implementation_id(baseline, "baseline", &mut reasons);
206    let candidate_implementation_id =
207        cohort_implementation_id(candidate, "candidate", &mut reasons);
208    let paired_samples = pair_samples(baseline, candidate, &mut reasons);
209    let paired = paired_samples.is_some();
210    let comparable = reasons.is_empty();
211    let baseline_stats = statistics(baseline_samples);
212    let candidate_stats = statistics(candidate_samples);
213    let median_delta_ns = paired_samples.as_ref().map_or_else(
214        || candidate_stats.median_ns - baseline_stats.median_ns,
215        |pairs| {
216            let mut deltas = pairs
217                .iter()
218                .map(|(baseline, candidate)| *candidate as i128 - *baseline as i128)
219                .collect::<Vec<_>>();
220            deltas.sort_unstable();
221            median_i128(&deltas)
222        },
223    );
224    let median_delta_percent = (baseline_stats.median_ns != 0.0)
225        .then_some(median_delta_ns / baseline_stats.median_ns * 100.0);
226    let confidence_interval = comparable.then(|| {
227        let (lower_delta_ns, upper_delta_ns) = bootstrap_delta_ci(
228            &baseline_stats.samples_ns,
229            &candidate_stats.samples_ns,
230            paired_samples.as_deref(),
231        );
232        ConfidenceInterval {
233            level: 0.95,
234            lower_delta_ns,
235            upper_delta_ns,
236        }
237    });
238    let verdict = match &confidence_interval {
239        None => ComparisonVerdict::Ineligible,
240        Some(ci) if ci.upper_delta_ns < 0.0 => ComparisonVerdict::CandidateFaster,
241        Some(ci) if ci.lower_delta_ns > 0.0 => ComparisonVerdict::CandidateSlower,
242        Some(_) => ComparisonVerdict::Inconclusive,
243    };
244    let tensor_stats = compare_tensor_stats(baseline, candidate);
245
246    ReplicatedComparison {
247        schema: SCHEMA.into(),
248        metric: "outer_wall_time_ns".into(),
249        inputs,
250        comparable,
251        paired,
252        verdict,
253        reasons,
254        baseline_implementation_id,
255        candidate_implementation_id,
256        identity,
257        baseline: baseline_stats,
258        candidate: candidate_stats,
259        median_delta_ns,
260        median_delta_percent,
261        confidence_interval,
262        tensor_stats,
263    }
264}
265
266fn compare_tensor_stats(
267    baseline: &[TraceDocument],
268    candidate: &[TraceDocument],
269) -> TensorStatsComparison {
270    #[derive(Default)]
271    struct Aggregate {
272        rms: f64,
273        abs_max: f64,
274        non_finite: u64,
275        samples: usize,
276        runs: usize,
277    }
278
279    fn aggregate(documents: &[TraceDocument]) -> BTreeMap<String, Aggregate> {
280        let mut by_label = BTreeMap::<String, Aggregate>::new();
281        for document in documents {
282            let mut seen = BTreeSet::new();
283            for event in &document.tensor_stats {
284                let entry = by_label.entry(event.label.clone()).or_default();
285                entry.rms += event.rms;
286                entry.abs_max += event.abs_max;
287                entry.non_finite = entry.non_finite.saturating_add(event.non_finite);
288                entry.samples += 1;
289                if seen.insert(event.label.as_str()) {
290                    entry.runs += 1;
291                }
292            }
293        }
294        by_label
295    }
296
297    fn mean(value: f64, samples: usize) -> f64 {
298        if samples == 0 {
299            0.0
300        } else {
301            value / samples as f64
302        }
303    }
304
305    fn ratio(a: f64, b: f64) -> Option<f64> {
306        if a == 0.0 {
307            (b == 0.0).then_some(1.0)
308        } else {
309            Some(b / a)
310        }
311    }
312
313    fn ratio_distance(ratio: Option<f64>) -> f64 {
314        match ratio {
315            Some(value) if value > 0.0 => value.ln().abs(),
316            _ => f64::INFINITY,
317        }
318    }
319
320    let baseline_runs = baseline.len();
321    let candidate_runs = candidate.len();
322    let baseline = aggregate(baseline);
323    let candidate = aggregate(candidate);
324    let mut matched = baseline
325        .iter()
326        .filter_map(|(label, a)| {
327            let b = candidate.get(label)?;
328            let rms_a = mean(a.rms, a.samples);
329            let rms_b = mean(b.rms, b.samples);
330            let abs_max_a = mean(a.abs_max, a.samples);
331            let abs_max_b = mean(b.abs_max, b.samples);
332            Some(TensorStatsComparisonRow {
333                label: label.clone(),
334                rms_a,
335                rms_b,
336                rms_ratio: ratio(rms_a, rms_b),
337                abs_max_ratio: ratio(abs_max_a, abs_max_b),
338                non_finite_a: a.non_finite,
339                non_finite_b: b.non_finite,
340                samples_a: a.samples,
341                samples_b: b.samples,
342                runs_a: a.runs,
343                runs_b: b.runs,
344            })
345        })
346        .collect::<Vec<_>>();
347    matched.sort_by(|a, b| {
348        ratio_distance(b.rms_ratio)
349            .total_cmp(&ratio_distance(a.rms_ratio))
350            .then_with(|| a.label.cmp(&b.label))
351    });
352    TensorStatsComparison {
353        baseline_runs,
354        candidate_runs,
355        matched,
356        unmatched_a: baseline
357            .keys()
358            .filter(|label| !candidate.contains_key(*label))
359            .cloned()
360            .collect(),
361        unmatched_b: candidate
362            .keys()
363            .filter(|label| !baseline.contains_key(*label))
364            .cloned()
365            .collect(),
366    }
367}
368
369fn validate_input_provenance(
370    inputs: &ComparisonInputs,
371    baseline: &[TraceDocument],
372    candidate: &[TraceDocument],
373    reasons: &mut Vec<String>,
374) {
375    match inputs.verification {
376        ComparisonInputVerification::UnverifiedTraces => reasons.push(
377            "unverified raw trace inputs are diagnostic only; finalized verified bundles are required for an eligible comparison"
378                .into(),
379        ),
380        ComparisonInputVerification::VerifiedBundles => {
381            validate_verified_cohort(&inputs.baseline, baseline, "baseline", reasons);
382            validate_verified_cohort(&inputs.candidate, candidate, "candidate", reasons);
383        }
384    }
385}
386
387fn validate_verified_cohort(
388    inputs: &[VerifiedBundleInput],
389    documents: &[TraceDocument],
390    cohort: &str,
391    reasons: &mut Vec<String>,
392) {
393    if inputs.len() != documents.len() {
394        reasons.push(format!(
395            "{cohort} bundle receipts must correspond one-to-one with trace documents"
396        ));
397        return;
398    }
399    for (index, (input, document)) in inputs.iter().zip(documents).enumerate() {
400        if input.run_id != document.run.run_id {
401            reasons.push(format!(
402                "{cohort} bundle {} receipt run ID does not match its trace",
403                index + 1
404            ));
405        }
406        if input.manifest_sha256.len() != 64
407            || !input
408                .manifest_sha256
409                .bytes()
410                .all(|byte| byte.is_ascii_hexdigit())
411        {
412            reasons.push(format!(
413                "{cohort} bundle {} receipt has an invalid manifest SHA-256",
414                index + 1
415            ));
416        }
417    }
418}
419
420fn require_independent_run_ids(
421    baseline: &[TraceDocument],
422    candidate: &[TraceDocument],
423    reasons: &mut Vec<String>,
424) {
425    let ids = baseline
426        .iter()
427        .chain(candidate)
428        .map(|document| document.run.run_id.as_str())
429        .collect::<Vec<_>>();
430    if ids.iter().copied().collect::<BTreeSet<_>>().len() != ids.len() {
431        reasons.push("run IDs must be unique across all replicates".into());
432    }
433}
434
435fn require_consistent_capture_semantics(
436    baseline: &[TraceDocument],
437    candidate: &[TraceDocument],
438    reasons: &mut Vec<String>,
439) {
440    let Some(first) = baseline.first().or_else(|| candidate.first()) else {
441        reasons.push("comparison contains no runs".into());
442        return;
443    };
444    if baseline.iter().chain(candidate).any(|document| {
445        document.run.entrypoint != first.run.entrypoint
446            || document.run.phase != first.run.phase
447            || document.run.device != first.run.device
448            || document.run.timing_mode != first.run.timing_mode
449            || document.run.warmup_steps != first.run.warmup_steps
450            || document.run.capture_step != first.run.capture_step
451            || document.run.capture_contract != first.run.capture_contract
452            || document.run.measured_region_device_synchronized
453                != first.run.measured_region_device_synchronized
454    }) {
455        reasons.push(
456            "entrypoint, phase, device, timing mode, synchronization, warmup, capture step, and capture contract must match"
457                .into(),
458        );
459    }
460}
461
462fn measured_samples(docs: &[TraceDocument], cohort: &str, reasons: &mut Vec<String>) -> Vec<u64> {
463    docs.iter()
464        .enumerate()
465        .map(|(index, doc)| {
466            let health = analyze_health(doc);
467            if !health.structurally_valid || !health.capture_complete {
468                reasons.push(format!(
469                    "{cohort} run {} is not a complete, structurally valid capture",
470                    index + 1
471                ));
472            }
473            if doc.run.capture_contract.measurement_scope != MeasurementScope::ProductionEquivalent
474            {
475                reasons.push(format!(
476                    "{cohort} run {} is not declared production-equivalent",
477                    index + 1
478                ));
479            }
480            if !doc.run.device.starts_with("cpu") && !doc.run.measured_region_device_synchronized {
481                reasons.push(format!(
482                    "{cohort} run {} does not synchronize its measured device region",
483                    index + 1
484                ));
485            }
486            let values = doc
487                .spans
488                .iter()
489                .filter(|span| span.measured && span.closed)
490                .map(|span| span.duration_ns)
491                .collect::<Vec<_>>();
492            if values.len() != 1 {
493                reasons.push(format!(
494                    "{cohort} run {} does not contain exactly one closed measured region",
495                    index + 1
496                ));
497            }
498            values.into_iter().next().unwrap_or(0)
499        })
500        .collect()
501}
502
503fn common_identity(
504    baseline: &[TraceDocument],
505    candidate: &[TraceDocument],
506    reasons: &mut Vec<String>,
507) -> Option<ComparisonIdentity> {
508    let identities = baseline
509        .iter()
510        .chain(candidate)
511        .map(|doc| doc.run.comparison_identity.as_ref())
512        .collect::<Vec<_>>();
513    let Some(first) = identities.first().copied().flatten() else {
514        reasons.push("comparison identity is missing".into());
515        return None;
516    };
517    if identities.iter().any(|identity| identity.is_none()) {
518        reasons.push("comparison identity is missing from one or more runs".into());
519        return None;
520    }
521    if let Err(error) = first.validate() {
522        reasons.push(format!("comparison identity is invalid: {error}"));
523        return None;
524    }
525    if identities
526        .iter()
527        .flatten()
528        .any(|identity| !same_conditions(first, identity))
529    {
530        reasons.push(
531            "workload, model, configuration, data, seed, batch, precision, or device state differs"
532                .into(),
533        );
534        return None;
535    }
536    let mut result = first.clone();
537    result.implementation_id = None;
538    result.pair_id = None;
539    Some(result)
540}
541
542fn cohort_implementation_id(
543    documents: &[TraceDocument],
544    cohort: &str,
545    reasons: &mut Vec<String>,
546) -> Option<String> {
547    let implementation_ids = documents
548        .iter()
549        .map(|document| {
550            document
551                .run
552                .comparison_identity
553                .as_ref()
554                .and_then(|identity| identity.implementation_id.as_deref())
555        })
556        .collect::<Vec<_>>();
557    let Some(first) = implementation_ids.first().copied().flatten() else {
558        reasons.push(format!("{cohort} implementation ID is missing"));
559        return None;
560    };
561    if implementation_ids.iter().any(|identity| identity.is_none()) {
562        reasons.push(format!(
563            "{cohort} implementation ID is missing from one or more runs"
564        ));
565        return None;
566    }
567    if implementation_ids
568        .iter()
569        .flatten()
570        .any(|identity| identity.trim().is_empty())
571    {
572        reasons.push(format!("{cohort} implementation ID must not be empty"));
573        return None;
574    }
575    if implementation_ids
576        .iter()
577        .flatten()
578        .any(|identity| *identity != first)
579    {
580        reasons.push(format!(
581            "{cohort} implementation ID differs within the cohort"
582        ));
583        return None;
584    }
585    Some(first.to_owned())
586}
587
588fn same_conditions(left: &ComparisonIdentity, right: &ComparisonIdentity) -> bool {
589    left.workload_id == right.workload_id
590        && left.model_id == right.model_id
591        && left.config_id == right.config_id
592        && left.data_id == right.data_id
593        && left.seed_policy == right.seed_policy
594        && left.physical_batch == right.physical_batch
595        && left.accumulation_steps == right.accumulation_steps
596        && left.precision == right.precision
597        && left.device_state == right.device_state
598}
599
600fn pair_samples(
601    baseline: &[TraceDocument],
602    candidate: &[TraceDocument],
603    reasons: &mut Vec<String>,
604) -> Option<Vec<(u64, u64)>> {
605    let any_pair_id = baseline.iter().chain(candidate).any(|doc| {
606        doc.run
607            .comparison_identity
608            .as_ref()
609            .and_then(|identity| identity.pair_id.as_ref())
610            .is_some()
611    });
612    if !any_pair_id {
613        return None;
614    }
615    let collect = |docs: &[TraceDocument]| -> Result<BTreeMap<String, u64>, &'static str> {
616        let mut values = BTreeMap::new();
617        for doc in docs {
618            let pair = doc
619                .run
620                .comparison_identity
621                .as_ref()
622                .and_then(|identity| identity.pair_id.clone())
623                .ok_or("pair IDs must be present on every run when pairing is requested")?;
624            let value = doc
625                .spans
626                .iter()
627                .find(|span| span.measured && span.closed)
628                .ok_or("paired runs require one closed measured region")?
629                .duration_ns;
630            if values.insert(pair, value).is_some() {
631                return Err("pair IDs must be unique within each cohort");
632            }
633        }
634        Ok(values)
635    };
636    let left = match collect(baseline) {
637        Ok(values) => values,
638        Err(reason) => {
639            reasons.push(reason.into());
640            return None;
641        }
642    };
643    let right = match collect(candidate) {
644        Ok(values) => values,
645        Err(reason) => {
646            reasons.push(reason.into());
647            return None;
648        }
649    };
650    if left.keys().collect::<BTreeSet<_>>() != right.keys().collect::<BTreeSet<_>>() {
651        reasons.push("baseline and candidate pair-ID sets must match exactly".into());
652        return None;
653    }
654    Some(
655        left.into_iter()
656            .map(|(key, value)| (value, right[&key]))
657            .collect(),
658    )
659}
660
661fn statistics(samples_ns: Vec<u64>) -> SampleStatistics {
662    let median_ns = percentile(&samples_ns, 0.5);
663    let p95_ns = percentile(&samples_ns, 0.95);
664    let deviations = samples_ns
665        .iter()
666        .map(|value| value.abs_diff(median_ns.round() as u64))
667        .collect::<Vec<_>>();
668    SampleStatistics {
669        samples_ns,
670        median_ns,
671        p95_ns,
672        mad_ns: percentile(&deviations, 0.5),
673    }
674}
675
676fn percentile(values: &[u64], quantile: f64) -> f64 {
677    if values.is_empty() {
678        return 0.0;
679    }
680    let mut values = values.to_vec();
681    values.sort_unstable();
682    let position = quantile * (values.len() - 1) as f64;
683    let lower = position.floor() as usize;
684    let upper = position.ceil() as usize;
685    let weight = position - lower as f64;
686    values[lower] as f64 * (1.0 - weight) + values[upper] as f64 * weight
687}
688
689fn bootstrap_delta_ci(
690    baseline: &[u64],
691    candidate: &[u64],
692    pairs: Option<&[(u64, u64)]>,
693) -> (f64, f64) {
694    const ITERATIONS: usize = 10_000;
695    let mut state = 0x4d595df4d0f33173u64;
696    let mut deltas = Vec::with_capacity(ITERATIONS);
697    for _ in 0..ITERATIONS {
698        if let Some(pairs) = pairs {
699            let mut sample = Vec::with_capacity(pairs.len());
700            for _ in 0..pairs.len() {
701                let index = random_index(&mut state, pairs.len());
702                sample.push(pairs[index].1 as i128 - pairs[index].0 as i128);
703            }
704            sample.sort_unstable();
705            deltas.push(median_i128(&sample));
706        } else {
707            let baseline_sample = resample(baseline, &mut state);
708            let candidate_sample = resample(candidate, &mut state);
709            deltas.push(percentile(&candidate_sample, 0.5) - percentile(&baseline_sample, 0.5));
710        }
711    }
712    deltas.sort_by(f64::total_cmp);
713    (deltas[249], deltas[9749])
714}
715
716fn resample(values: &[u64], state: &mut u64) -> Vec<u64> {
717    (0..values.len())
718        .map(|_| values[random_index(state, values.len())])
719        .collect()
720}
721
722fn random_index(state: &mut u64, length: usize) -> usize {
723    *state ^= *state << 13;
724    *state ^= *state >> 7;
725    *state ^= *state << 17;
726    (*state as usize) % length
727}
728
729fn median_i128(values: &[i128]) -> f64 {
730    if values.is_empty() {
731        0.0
732    } else if values.len().is_multiple_of(2) {
733        let upper = values.len() / 2;
734        (values[upper - 1] as f64 + values[upper] as f64) / 2.0
735    } else {
736        values[values.len() / 2] as f64
737    }
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use crate::capability::CaptureContract;
744    use crate::trace::{
745        RunOutcome, SpanKind, SpanRecord, TensorStatsEvent, TerminalEvent, TimingMode,
746        TraceRunMeta, SCHEMA as TRACE_SCHEMA,
747    };
748
749    fn run(cohort: &str, index: usize, duration_ns: u64, pair_id: Option<String>) -> TraceDocument {
750        TraceDocument {
751            schema: TRACE_SCHEMA.into(),
752            run: TraceRunMeta {
753                run_id: format!("{cohort}-{index}"),
754                correlation_id: format!("{cohort}-{index}"),
755                entrypoint: "demo::infer".into(),
756                phase: crate::ExecutionPhase::Infer,
757                timestamp: "2026-08-19T00:00:00Z".into(),
758                capture_step: 6,
759                warmup_steps: 5,
760                device: "cpu".into(),
761                measured_region_device_synchronized: false,
762                timing_mode: TimingMode::Host,
763                capture_contract: CaptureContract {
764                    measurement_scope: MeasurementScope::ProductionEquivalent,
765                    ..CaptureContract::default()
766                },
767                comparison_identity: Some(ComparisonIdentity {
768                    implementation_id: Some(cohort.into()),
769                    workload_id: "infer".into(),
770                    model_id: "m1".into(),
771                    config_id: "c1".into(),
772                    data_id: "d1".into(),
773                    seed_policy: "fixed".into(),
774                    physical_batch: 1,
775                    accumulation_steps: 1,
776                    precision: "f32".into(),
777                    device_state: "exclusive".into(),
778                    pair_id,
779                }),
780                tags: Default::default(),
781                candle_version: None,
782            },
783            spans: vec![SpanRecord {
784                id: "root".into(),
785                parent_id: None,
786                name: "infer".into(),
787                kind: SpanKind::Function,
788                measured: true,
789                start_ns: 0,
790                closed: true,
791                duration_ns,
792                step: None,
793            }],
794            ops: vec![],
795            tensors: vec![],
796            tensor_stats: vec![],
797            memory: vec![],
798            device_memory: vec![],
799            device_intervals: vec![],
800            gradients: vec![],
801            edges: vec![],
802            terminal: TerminalEvent {
803                outcome: RunOutcome::Complete,
804                timestamp_ns: duration_ns,
805                reason: None,
806            },
807        }
808    }
809
810    fn compare_test_replicates(
811        baseline: &[TraceDocument],
812        candidate: &[TraceDocument],
813    ) -> ReplicatedComparison {
814        let receipts = |documents: &[TraceDocument]| {
815            documents
816                .iter()
817                .map(|document| VerifiedBundleInput {
818                    run_id: document.run.run_id.clone(),
819                    manifest_sha256: "0".repeat(64),
820                })
821                .collect()
822        };
823        compare_documents(
824            baseline,
825            candidate,
826            ComparisonInputs {
827                verification: ComparisonInputVerification::VerifiedBundles,
828                baseline: receipts(baseline),
829                candidate: receipts(candidate),
830            },
831        )
832    }
833
834    #[test]
835    fn statistics_expose_raw_median_p95_and_mad() {
836        let stats = statistics(vec![10, 11, 12, 13, 100]);
837        assert_eq!(stats.samples_ns, vec![10, 11, 12, 13, 100]);
838        assert_eq!(stats.median_ns, 12.0);
839        assert_eq!(stats.p95_ns, 82.6);
840        assert_eq!(stats.mad_ns, 1.0);
841    }
842
843    #[test]
844    fn tensor_stats_average_every_event_and_expose_sample_and_run_coverage() {
845        let stats = |label: &str, rms: f64, abs_max: f64| TensorStatsEvent {
846            span_id: "s1".into(),
847            label: label.into(),
848            shape: vec![1],
849            dtype: "f32".into(),
850            elements: 1,
851            non_finite: if rms == 100.0 { 1 } else { 0 },
852            rms,
853            abs_max,
854            mean: rms,
855        };
856        let mut baseline = run("base", 0, 100, None);
857        baseline.tensor_stats = vec![
858            stats("stable", 2.0, 4.0),
859            stats("drift", 1.0, 2.0),
860            stats("drift", 100.0, 200.0),
861            stats("only_a", 3.0, 3.0),
862        ];
863        let mut candidate = run("next", 0, 90, None);
864        candidate.tensor_stats = vec![
865            stats("stable", 2.2, 4.4),
866            stats("drift", 4.0, 8.0),
867            stats("only_b", 5.0, 5.0),
868        ];
869
870        let comparison = compare_unverified_traces(&[baseline], &[candidate]);
871        let drift = &comparison.tensor_stats.matched[0];
872        assert_eq!(drift.label, "drift");
873        // Duplicate baseline events are averaged, not discarded after the first occurrence.
874        assert_eq!(drift.rms_a, 50.5);
875        assert_eq!(drift.rms_ratio, Some(4.0 / 50.5));
876        // A non-finite count in a duplicate event is retained.
877        assert_eq!(drift.non_finite_a, 1);
878        assert_eq!(drift.samples_a, 2);
879        assert_eq!(drift.samples_b, 1);
880        assert_eq!(drift.runs_a, 1);
881        assert_eq!(drift.runs_b, 1);
882        assert_eq!(comparison.tensor_stats.baseline_runs, 1);
883        assert_eq!(comparison.tensor_stats.candidate_runs, 1);
884        assert_eq!(comparison.tensor_stats.unmatched_a, vec!["only_a"]);
885        assert_eq!(comparison.tensor_stats.unmatched_b, vec!["only_b"]);
886    }
887
888    #[test]
889    fn out_of_domain_provenance_fails_closed() {
890        let baseline = (0..5)
891            .map(|i| run("base", i, 100 + i as u64, None))
892            .collect::<Vec<_>>();
893        let candidate = (0..5)
894            .map(|i| {
895                let mut document = run("next", i, 90 + i as u64, None);
896                document.run.capture_step = 0;
897                document.run.warmup_steps = 0;
898                document
899            })
900            .collect::<Vec<_>>();
901        let result = compare_test_replicates(&baseline, &candidate);
902        assert!(!result.comparable);
903        assert_eq!(result.verdict, ComparisonVerdict::Ineligible);
904        assert!(result
905            .reasons
906            .iter()
907            .any(|reason| reason.contains("not a complete, structurally valid capture")));
908
909        let mut zero_batch = baseline.clone();
910        for document in &mut zero_batch {
911            document
912                .run
913                .comparison_identity
914                .as_mut()
915                .unwrap()
916                .physical_batch = 0;
917        }
918        let result = compare_test_replicates(&zero_batch, &zero_batch.clone());
919        assert!(!result.comparable);
920        assert_eq!(result.verdict, ComparisonVerdict::Ineligible);
921    }
922
923    #[test]
924    fn confirms_only_when_replicated_interval_excludes_zero() {
925        let baseline = [100, 102, 99, 101, 103]
926            .into_iter()
927            .enumerate()
928            .map(|(i, value)| run("base", i, value, None))
929            .collect::<Vec<_>>();
930        let candidate = [75, 80, 78, 79, 77]
931            .into_iter()
932            .enumerate()
933            .map(|(i, value)| run("next", i, value, None))
934            .collect::<Vec<_>>();
935        let result = compare_test_replicates(&baseline, &candidate);
936        assert!(result.comparable);
937        assert_eq!(result.baseline_implementation_id.as_deref(), Some("base"));
938        assert_eq!(result.candidate_implementation_id.as_deref(), Some("next"));
939        assert_eq!(
940            result
941                .identity
942                .as_ref()
943                .and_then(|identity| identity.implementation_id.as_deref()),
944            None
945        );
946        assert_eq!(result.verdict, ComparisonVerdict::CandidateFaster);
947        assert!(result.confidence_interval.unwrap().upper_delta_ns < 0.0);
948    }
949
950    #[test]
951    fn fewer_than_five_or_duplicate_runs_fail_closed() {
952        let baseline = (0..4)
953            .map(|i| run("base", i, 100, None))
954            .collect::<Vec<_>>();
955        let mut candidate = (0..5).map(|i| run("next", i, 90, None)).collect::<Vec<_>>();
956        candidate[4].run.run_id = candidate[3].run.run_id.clone();
957        let result = compare_test_replicates(&baseline, &candidate);
958        assert!(!result.comparable);
959        assert_eq!(result.verdict, ComparisonVerdict::Ineligible);
960        assert!(result
961            .reasons
962            .iter()
963            .any(|reason| reason.contains("at least 5")));
964        assert!(result
965            .reasons
966            .iter()
967            .any(|reason| reason.contains("unique")));
968    }
969
970    #[test]
971    fn partial_pair_metadata_is_ineligible_instead_of_falling_back() {
972        let baseline = (0..5)
973            .map(|i| run("base", i, 100 + i as u64, Some(format!("pair-{i}"))))
974            .collect::<Vec<_>>();
975        let mut candidate = (0..5)
976            .map(|i| run("next", i, 90 + i as u64, Some(format!("pair-{i}"))))
977            .collect::<Vec<_>>();
978        candidate[4]
979            .run
980            .comparison_identity
981            .as_mut()
982            .unwrap()
983            .pair_id = None;
984        let result = compare_test_replicates(&baseline, &candidate);
985        assert!(!result.comparable);
986        assert!(!result.paired);
987        assert!(result
988            .reasons
989            .iter()
990            .any(|reason| reason.contains("every run")));
991    }
992
993    #[test]
994    fn paired_even_median_averages_the_middle_deltas() {
995        assert_eq!(median_i128(&[-5, -1, 3, 9]), 1.0);
996    }
997
998    #[test]
999    fn missing_empty_or_inconsistent_implementation_ids_fail_closed() {
1000        let baseline = (0..5)
1001            .map(|i| run("base", i, 100 + i as u64, None))
1002            .collect::<Vec<_>>();
1003        let candidate = (0..5)
1004            .map(|i| run("next", i, 90 + i as u64, None))
1005            .collect::<Vec<_>>();
1006
1007        for (implementation_id, expected_reason) in [
1008            (None, "missing"),
1009            (Some("   ".to_string()), "must not be empty"),
1010        ] {
1011            let mut invalid = baseline.clone();
1012            invalid[0]
1013                .run
1014                .comparison_identity
1015                .as_mut()
1016                .unwrap()
1017                .implementation_id = implementation_id;
1018            let result = compare_test_replicates(&invalid, &candidate);
1019            assert!(!result.comparable);
1020            assert_eq!(result.verdict, ComparisonVerdict::Ineligible);
1021            assert!(result
1022                .reasons
1023                .iter()
1024                .any(|reason| reason.contains(expected_reason)));
1025        }
1026
1027        let mut inconsistent = candidate.clone();
1028        inconsistent[4]
1029            .run
1030            .comparison_identity
1031            .as_mut()
1032            .unwrap()
1033            .implementation_id = Some("another-build".into());
1034        let result = compare_test_replicates(&baseline, &inconsistent);
1035        assert!(!result.comparable);
1036        assert!(result
1037            .reasons
1038            .iter()
1039            .any(|reason| reason.contains("differs within")));
1040    }
1041}