jugar_probar/gpu_pixels/
regression.rs1#![allow(clippy::unwrap_used)]
7
8use super::kernel_pixels::{GpuPixelResult, GpuPixelTestSuite, KernelPixelConfig};
9use super::ptx_analysis::PtxAnalyzer;
10use std::collections::HashMap;
11use std::time::{Duration, Instant};
12
13#[derive(Debug, Clone)]
15pub struct RegressionConfig {
16 pub baseline_dir: String,
18 pub auto_update: bool,
20 pub pixel_config: KernelPixelConfig,
22}
23
24impl Default for RegressionConfig {
25 fn default() -> Self {
26 Self {
27 baseline_dir: ".gpu_baselines".to_string(),
28 auto_update: false,
29 pixel_config: KernelPixelConfig::default(),
30 }
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct RegressionResult {
37 pub kernel_name: String,
39 pub is_regression: bool,
41 pub details: Option<String>,
43 pub pixel_results: GpuPixelTestSuite,
45 pub duration: Duration,
47}
48
49impl RegressionResult {
50 #[must_use]
52 pub fn passed(&self) -> bool {
53 !self.is_regression && self.pixel_results.all_passed()
54 }
55}
56
57#[derive(Debug)]
59pub struct GpuRegressionSuite {
60 config: RegressionConfig,
62 baselines: HashMap<String, String>,
64 results: Vec<RegressionResult>,
66}
67
68impl GpuRegressionSuite {
69 #[must_use]
71 pub fn new(config: RegressionConfig) -> Self {
72 Self {
73 config,
74 baselines: HashMap::new(),
75 results: Vec::new(),
76 }
77 }
78
79 pub fn add_baseline(&mut self, kernel_name: &str, ptx: &str) {
81 self.baselines
82 .insert(kernel_name.to_string(), ptx.to_string());
83 }
84
85 pub fn test_kernel(&mut self, kernel_name: &str, ptx: &str) -> &RegressionResult {
87 let start = Instant::now();
88
89 let mut pixel_suite = GpuPixelTestSuite::new(kernel_name);
91 let ptx_result = PtxAnalyzer::default().analyze(ptx);
92 pixel_suite.add_result(GpuPixelResult::from_ptx_validation(&ptx_result));
93 pixel_suite.run_kernel_pixels(ptx, &self.config.pixel_config);
94
95 let (is_regression, details) = if let Some(baseline) = self.baselines.get(kernel_name) {
97 self.compare_ptx(baseline, ptx)
98 } else {
99 (false, None)
100 };
101
102 let result = RegressionResult {
103 kernel_name: kernel_name.to_string(),
104 is_regression,
105 details,
106 pixel_results: pixel_suite,
107 duration: start.elapsed(),
108 };
109
110 self.results.push(result);
111 self.results.last().unwrap()
112 }
113
114 fn compare_ptx(&self, baseline: &str, current: &str) -> (bool, Option<String>) {
116 let baseline_patterns = self.extract_patterns(baseline);
118 let current_patterns = self.extract_patterns(current);
119
120 let mut regressions = Vec::new();
122
123 if baseline_patterns.uses_u32_shared && !current_patterns.uses_u32_shared {
125 regressions.push("Regression: switched from u32 to u64 shared memory addressing");
126 }
127
128 if baseline_patterns.has_barrier && !current_patterns.has_barrier {
130 regressions.push("Regression: removed barrier synchronization");
131 }
132
133 if baseline_patterns.kernel_names != current_patterns.kernel_names {
135 regressions.push("Regression: kernel name changed");
136 }
137
138 if regressions.is_empty() {
139 (false, None)
140 } else {
141 (true, Some(regressions.join("; ")))
142 }
143 }
144
145 fn extract_patterns(&self, ptx: &str) -> PtxPatterns {
147 let analyzer = PtxAnalyzer::default();
148 let result = analyzer.analyze(ptx);
149
150 PtxPatterns {
151 kernel_names: result.kernel_names,
152 uses_u32_shared: !ptx.contains("[%rd")
153 || !ptx.contains("st.shared") && !ptx.contains("ld.shared"),
154 has_barrier: ptx.contains("bar.sync"),
155 has_shared_mem: ptx.contains(".shared"),
156 }
157 }
158
159 #[must_use]
161 pub fn results(&self) -> &[RegressionResult] {
162 &self.results
163 }
164
165 #[must_use]
167 pub fn all_passed(&self) -> bool {
168 self.results.iter().all(|r| r.passed())
169 }
170
171 #[must_use]
173 pub fn summary(&self) -> String {
174 let passed = self.results.iter().filter(|r| r.passed()).count();
175 let total = self.results.len();
176 let regressions = self.results.iter().filter(|r| r.is_regression).count();
177
178 format!(
179 "GPU Regression Suite: {}/{} passed, {} regressions",
180 passed, total, regressions
181 )
182 }
183}
184
185#[derive(Debug, Clone)]
187#[allow(dead_code)] struct PtxPatterns {
189 kernel_names: Vec<String>,
190 uses_u32_shared: bool,
191 has_barrier: bool,
192 has_shared_mem: bool,
193}
194
195#[must_use]
197pub fn run_regression_suite(
198 kernels: &[(&str, &str)], baselines: &[(&str, &str)],
200 config: RegressionConfig,
201) -> GpuRegressionSuite {
202 let mut suite = GpuRegressionSuite::new(config);
203
204 for (name, ptx) in baselines {
206 suite.add_baseline(name, ptx);
207 }
208
209 for (name, ptx) in kernels {
211 suite.test_kernel(name, ptx);
212 }
213
214 suite
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_regression_suite_creation() {
223 let suite = GpuRegressionSuite::new(RegressionConfig::default());
224 assert!(suite.results().is_empty());
225 assert!(suite.all_passed());
226 }
227
228 #[test]
229 fn test_add_baseline() {
230 let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
231 suite.add_baseline("test_kernel", ".version 8.0");
232 assert!(suite.baselines.contains_key("test_kernel"));
233 }
234
235 #[test]
236 fn test_no_regression_without_baseline() {
237 let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
238 let ptx = r#"
239.version 8.0
240.target sm_70
241.address_size 64
242.visible .entry test() { ret; }
243"#;
244 let result = suite.test_kernel("new_kernel", ptx);
245 assert!(!result.is_regression);
246 }
247
248 #[test]
249 fn test_regression_detected() {
250 let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
251
252 let baseline = r#"
254.version 8.0
255.target sm_70
256.visible .entry test() {
257 .shared .b8 smem[1024];
258 st.shared.f32 [%r0], %f0;
259 bar.sync 0;
260 ret;
261}
262"#;
263 suite.add_baseline("test", baseline);
264
265 let current = r#"
267.version 8.0
268.target sm_70
269.visible .entry test() {
270 .shared .b8 smem[1024];
271 st.shared.f32 [%rd0], %f0;
272 bar.sync 0;
273 ret;
274}
275"#;
276 let result = suite.test_kernel("test", current);
277 assert!(!result.pixel_results.all_passed() || result.is_regression);
279 }
280
281 #[test]
282 fn test_summary_format() {
283 let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
284 let ptx = ".version 8.0\n.visible .entry t() { ret; }";
285 suite.test_kernel("k1", ptx);
286 suite.test_kernel("k2", ptx);
287
288 let summary = suite.summary();
289 assert!(summary.contains("GPU Regression Suite"));
290 assert!(summary.contains("/2"));
291 }
292
293 #[test]
294 fn test_run_regression_suite() {
295 let kernels = vec![
296 ("k1", ".version 8.0\n.visible .entry k1() { ret; }"),
297 ("k2", ".version 8.0\n.visible .entry k2() { ret; }"),
298 ];
299 let baselines: Vec<(&str, &str)> = vec![];
300
301 let suite = run_regression_suite(&kernels, &baselines, RegressionConfig::default());
302 assert_eq!(suite.results().len(), 2);
303 }
304}