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