Skip to main content

cbtop/backend_regression/
detector.rs

1//! Backend comparison, cliff detection, and recommendation logic.
2
3use super::analysis::{BackendSummary, TransferAnalysis};
4use super::types::{Backend, BackendComparison, BackendRecommendation, SizeCliff, WorkloadType};
5use super::BackendRegressionDetector;
6
7impl BackendRegressionDetector {
8    /// Compare two backends for a specific workload/size
9    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    /// Detect size cliffs for a backend
46    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    /// Analyze GPU transfer overhead
84    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    /// Recommend best backend for given workload/size
141    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    /// Get all comparisons for a workload
192    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    /// Detect all regressions
215    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    /// Generate summary report
224    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}