cbtop/backend_regression/
detector.rs1use super::analysis::{BackendSummary, TransferAnalysis};
4use super::types::{Backend, BackendComparison, BackendRecommendation, SizeCliff, WorkloadType};
5use super::BackendRegressionDetector;
6
7impl BackendRegressionDetector {
8 pub fn compare_backends(
10 &self,
11 baseline: Backend,
12 comparison: Backend,
13 workload: WorkloadType,
14 size: usize,
15 ) -> Option<BackendComparison> {
16 let baseline_m = self.find_measurement(baseline, workload, size)?;
17 let comparison_m = self.find_measurement(comparison, workload, size)?;
18
19 let efficiency_ratio = if baseline_m.efficiency_percent > 0.0 {
20 comparison_m.efficiency_percent / baseline_m.efficiency_percent
21 } else {
22 0.0
23 };
24
25 let speedup = if comparison_m.latency_us > 0.0 {
26 baseline_m.latency_us / comparison_m.latency_us
27 } else {
28 0.0
29 };
30
31 let is_regression = speedup < (1.0 - self.threshold_percent() / 100.0);
32
33 Some(BackendComparison {
34 baseline,
35 comparison,
36 workload,
37 size,
38 efficiency_ratio,
39 speedup,
40 is_regression,
41 threshold: self.threshold_percent(),
42 })
43 }
44
45 pub fn detect_size_cliffs(&self, backend: Backend, workload: WorkloadType) -> Vec<SizeCliff> {
47 let mut measurements: Vec<_> = self
48 .measurements()
49 .iter()
50 .filter(|m| m.backend == backend && m.workload == workload)
51 .collect();
52
53 measurements.sort_by_key(|m| m.size);
54
55 let mut cliffs = Vec::new();
56
57 for window in measurements.windows(2) {
58 let before = &window[0];
59 let after = &window[1];
60
61 if before.efficiency_percent > 0.0 {
62 let drop = (before.efficiency_percent - after.efficiency_percent)
63 / before.efficiency_percent
64 * 100.0;
65
66 if drop > self.cliff_threshold_percent() {
67 cliffs.push(SizeCliff {
68 backend,
69 workload,
70 size_before: before.size,
71 size_after: after.size,
72 efficiency_before: before.efficiency_percent,
73 efficiency_after: after.efficiency_percent,
74 drop_percent: drop,
75 });
76 }
77 }
78 }
79
80 cliffs
81 }
82
83 pub fn analyze_transfer_overhead(
85 &self,
86 backend: Backend,
87 workload: WorkloadType,
88 ) -> Option<TransferAnalysis> {
89 if !backend.is_gpu() {
90 return None;
91 }
92
93 let measurements: Vec<_> = self
94 .measurements()
95 .iter()
96 .filter(|m| {
97 m.backend == backend
98 && m.workload == workload
99 && m.transfer_time_us.is_some()
100 && m.compute_time_us.is_some()
101 })
102 .collect();
103
104 if measurements.is_empty() {
105 return None;
106 }
107
108 let mut total_transfer = 0.0;
109 let mut total_compute = 0.0;
110 let mut sizes_with_overhead = Vec::new();
111
112 for m in &measurements {
113 let transfer = m
114 .transfer_time_us
115 .expect("transfer_time_us MUST be set for GPU measurements");
116 let compute = m
117 .compute_time_us
118 .expect("compute_time_us MUST be set for GPU measurements");
119 total_transfer += transfer;
120 total_compute += compute;
121
122 let overhead = transfer / (transfer + compute);
123 if overhead > 0.5 {
124 sizes_with_overhead.push((m.size, overhead));
125 }
126 }
127
128 let avg_overhead = total_transfer / (total_transfer + total_compute);
129
130 Some(TransferAnalysis {
131 backend,
132 workload,
133 average_overhead: avg_overhead,
134 total_transfer_time_us: total_transfer,
135 total_compute_time_us: total_compute,
136 sizes_dominated_by_transfer: sizes_with_overhead,
137 })
138 }
139
140 pub fn recommend_backend(
142 &self,
143 workload: WorkloadType,
144 size: usize,
145 ) -> Option<BackendRecommendation> {
146 let candidates: Vec<_> = self
147 .measurements()
148 .iter()
149 .filter(|m| m.workload == workload && m.size == size)
150 .collect();
151
152 if candidates.is_empty() {
153 return None;
154 }
155
156 let best = candidates.iter().max_by(|a, b| {
157 a.throughput
158 .partial_cmp(&b.throughput)
159 .expect("throughput MUST be comparable (no NaN)")
160 })?;
161
162 let confidence = (best.efficiency_percent / 100.0).clamp(0.0, 1.0);
163
164 let reason = if best.backend.is_gpu() {
165 if let Some(overhead) = best.transfer_overhead() {
166 if overhead > 0.3 {
167 format!(
168 "GPU selected but transfer overhead is {:.1}%",
169 overhead * 100.0
170 )
171 } else {
172 "Best throughput with low transfer overhead".to_string()
173 }
174 } else {
175 "Best throughput among available backends".to_string()
176 }
177 } else {
178 "Best CPU backend for this size".to_string()
179 };
180
181 Some(BackendRecommendation {
182 backend: best.backend,
183 workload,
184 size,
185 expected_efficiency: best.efficiency_percent,
186 confidence,
187 reason,
188 })
189 }
190
191 pub fn compare_all_backends(&self, workload: WorkloadType) -> Vec<BackendComparison> {
193 let sizes = self.unique_for(workload, |m| m.size);
194 let backends = self.unique_for(workload, |m| m.backend);
195
196 let mut comparisons = Vec::new();
197
198 for size in &sizes {
199 if let Some(scalar) = backends.iter().find(|b| **b == Backend::Scalar) {
200 for backend in &backends {
201 if *backend != Backend::Scalar {
202 if let Some(cmp) = self.compare_backends(*scalar, *backend, workload, *size)
203 {
204 comparisons.push(cmp);
205 }
206 }
207 }
208 }
209 }
210
211 comparisons
212 }
213
214 pub fn detect_regressions(&self) -> Vec<BackendComparison> {
216 self.unique(|m| m.workload)
217 .into_iter()
218 .flat_map(|w| self.compare_all_backends(w))
219 .filter(|cmp| cmp.is_regression)
220 .collect()
221 }
222
223 pub fn summary(&self) -> BackendSummary {
225 let workloads = self.unique(|m| m.workload);
226 let backends = self.unique(|m| m.backend);
227 let regressions = self.detect_regressions();
228
229 let all_cliffs: Vec<_> = backends
230 .iter()
231 .flat_map(|b| {
232 workloads
233 .iter()
234 .flat_map(move |w| self.detect_size_cliffs(*b, *w))
235 })
236 .collect();
237
238 BackendSummary {
239 measurement_count: self.measurements().len(),
240 backend_count: backends.len(),
241 workload_count: workloads.len(),
242 regression_count: regressions.len(),
243 cliff_count: all_cliffs.len(),
244 regressions,
245 cliffs: all_cliffs,
246 }
247 }
248}