1use 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 pub samples_a: usize,
51 pub samples_b: usize,
52 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum ComparisonInputVerification {
71 VerifiedBundles,
72 UnverifiedTraces,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct VerifiedBundleInput {
78 pub run_id: String,
79 pub manifest_sha256: String,
80}
81
82#[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 #[serde(default)]
109 pub tensor_stats: TensorStatsComparison,
110}
111
112pub 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
130pub 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 assert_eq!(drift.rms_a, 50.5);
875 assert_eq!(drift.rms_ratio, Some(4.0 / 50.5));
876 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}