Skip to main content

cbtop/backend_regression/
mod.rs

1//! Cross-Backend Regression Detector (PMAT-031)
2//!
3//! Detect performance regressions when switching between compute backends.
4//!
5//! # Features
6//!
7//! - Compare efficiency across backends (Scalar, SSE2, AVX2, CUDA, Metal)
8//! - Detect size thresholds where performance cliffs occur
9//! - Recommend optimal backend for given workload size
10//! - Measure GPU transfer overhead vs compute benefit
11//!
12//! # Falsification Criteria (F1231-F1240)
13//!
14//! See `tests/backend_regression_f1231.rs` for falsification tests.
15
16mod analysis;
17mod detector;
18mod types;
19
20pub use analysis::{BackendSummary, TransferAnalysis};
21pub use types::{
22    Backend, BackendComparison, BackendMeasurement, BackendRecommendation, SizeCliff, WorkloadType,
23};
24
25use std::collections::HashSet;
26
27/// Backend regression detector
28#[derive(Debug, Clone)]
29pub struct BackendRegressionDetector {
30    /// Measurements collected
31    measurements: Vec<BackendMeasurement>,
32    /// Regression threshold (default 10%)
33    threshold_percent: f64,
34    /// Cliff detection threshold (default 10%)
35    cliff_threshold_percent: f64,
36    /// Available backends
37    available_backends: Vec<Backend>,
38}
39
40impl Default for BackendRegressionDetector {
41    fn default() -> Self {
42        Self {
43            measurements: Vec::new(),
44            threshold_percent: 10.0,
45            cliff_threshold_percent: 10.0,
46            available_backends: vec![Backend::Scalar, Backend::Sse2, Backend::Avx2],
47        }
48    }
49}
50
51impl BackendRegressionDetector {
52    /// Create new detector
53    pub fn new() -> Self {
54        Self::default()
55    }
56
57    /// Set regression threshold
58    pub fn with_threshold(mut self, percent: f64) -> Self {
59        self.threshold_percent = percent;
60        self
61    }
62
63    /// Set cliff detection threshold
64    pub fn with_cliff_threshold(mut self, percent: f64) -> Self {
65        self.cliff_threshold_percent = percent;
66        self
67    }
68
69    /// Set available backends
70    pub fn with_backends(mut self, backends: Vec<Backend>) -> Self {
71        self.available_backends = backends;
72        self
73    }
74
75    /// Add a measurement
76    pub fn add_measurement(&mut self, measurement: BackendMeasurement) {
77        self.measurements.push(measurement);
78    }
79
80    /// Add measurement from values
81    pub fn add(
82        &mut self,
83        backend: Backend,
84        workload: WorkloadType,
85        size: usize,
86        latency_us: f64,
87        throughput: f64,
88        efficiency: f64,
89    ) {
90        self.add_measurement(
91            BackendMeasurement::new(backend, workload, size, latency_us, throughput)
92                .with_efficiency(efficiency),
93        );
94    }
95
96    /// Get measurement count
97    pub fn measurement_count(&self) -> usize {
98        self.measurements.len()
99    }
100
101    /// Access measurements slice
102    pub(crate) fn measurements(&self) -> &[BackendMeasurement] {
103        &self.measurements
104    }
105
106    /// Access threshold percent
107    pub(crate) fn threshold_percent(&self) -> f64 {
108        self.threshold_percent
109    }
110
111    /// Access cliff threshold percent
112    pub(crate) fn cliff_threshold_percent(&self) -> f64 {
113        self.cliff_threshold_percent
114    }
115
116    /// Collect unique values from measurements via an extractor.
117    pub(crate) fn unique<T: Eq + std::hash::Hash + Copy>(
118        &self,
119        f: impl Fn(&BackendMeasurement) -> T,
120    ) -> Vec<T> {
121        self.measurements
122            .iter()
123            .map(f)
124            .collect::<HashSet<_>>()
125            .into_iter()
126            .collect()
127    }
128
129    /// Collect unique values from measurements matching a workload.
130    pub(crate) fn unique_for<T: Eq + std::hash::Hash + Copy>(
131        &self,
132        workload: WorkloadType,
133        f: impl Fn(&BackendMeasurement) -> T,
134    ) -> Vec<T> {
135        self.measurements
136            .iter()
137            .filter(|m| m.workload == workload)
138            .map(f)
139            .collect::<HashSet<_>>()
140            .into_iter()
141            .collect()
142    }
143
144    /// Find measurement
145    pub(crate) fn find_measurement(
146        &self,
147        backend: Backend,
148        workload: WorkloadType,
149        size: usize,
150    ) -> Option<&BackendMeasurement> {
151        self.measurements
152            .iter()
153            .find(|m| m.backend == backend && m.workload == workload && m.size == size)
154    }
155
156    /// Check if backend is available
157    pub fn is_backend_available(&self, backend: Backend) -> bool {
158        self.available_backends.contains(&backend)
159    }
160
161    /// Get available backends
162    pub fn available_backends(&self) -> &[Backend] {
163        &self.available_backends
164    }
165
166    /// Clear all measurements
167    pub fn clear(&mut self) {
168        self.measurements.clear();
169    }
170}
171
172#[cfg(test)]
173mod tests;