Skip to main content

jugar_probar/gpu_pixels/
regression.rs

1//! GPU Kernel Regression Testing
2//!
3//! Tracks kernel PTX signatures and detects regressions across versions.
4
5// Allow unwrap on Option::last() when we've already checked non-empty
6#![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/// Configuration for regression testing
14#[derive(Debug, Clone)]
15pub struct RegressionConfig {
16    /// Directory to store baseline PTX
17    pub baseline_dir: String,
18    /// Auto-update baselines on pass
19    pub auto_update: bool,
20    /// Kernel pixel config
21    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/// Result of regression check
35#[derive(Debug, Clone)]
36pub struct RegressionResult {
37    /// Kernel name
38    pub kernel_name: String,
39    /// Is this a regression?
40    pub is_regression: bool,
41    /// Regression details
42    pub details: Option<String>,
43    /// Pixel test results
44    pub pixel_results: GpuPixelTestSuite,
45    /// Duration
46    pub duration: Duration,
47}
48
49impl RegressionResult {
50    /// Check if kernel passed all checks
51    #[must_use]
52    pub fn passed(&self) -> bool {
53        !self.is_regression && self.pixel_results.all_passed()
54    }
55}
56
57/// GPU kernel regression test suite
58#[derive(Debug)]
59pub struct GpuRegressionSuite {
60    /// Configuration
61    config: RegressionConfig,
62    /// Baseline PTX for each kernel
63    baselines: HashMap<String, String>,
64    /// Results
65    results: Vec<RegressionResult>,
66}
67
68impl GpuRegressionSuite {
69    /// Create new regression suite
70    #[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    /// Add baseline PTX for a kernel
80    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    /// Test kernel against baseline
86    pub fn test_kernel(&mut self, kernel_name: &str, ptx: &str) -> &RegressionResult {
87        let start = Instant::now();
88
89        // Run pixel tests
90        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        // Check against baseline
96        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    /// Compare PTX for regression
115    fn compare_ptx(&self, baseline: &str, current: &str) -> (bool, Option<String>) {
116        // Extract key patterns
117        let baseline_patterns = self.extract_patterns(baseline);
118        let current_patterns = self.extract_patterns(current);
119
120        // Check for regressions
121        let mut regressions = Vec::new();
122
123        // Check shared memory addressing
124        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        // Check barrier presence
129        if baseline_patterns.has_barrier && !current_patterns.has_barrier {
130            regressions.push("Regression: removed barrier synchronization");
131        }
132
133        // Check kernel name
134        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    /// Extract testable patterns from PTX
146    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    /// Get all results
160    #[must_use]
161    pub fn results(&self) -> &[RegressionResult] {
162        &self.results
163    }
164
165    /// Check if all tests passed
166    #[must_use]
167    pub fn all_passed(&self) -> bool {
168        self.results.iter().all(|r| r.passed())
169    }
170
171    /// Get summary
172    #[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/// Extracted PTX patterns for comparison
186#[derive(Debug, Clone)]
187#[allow(dead_code)] // Reserved for future PTX pattern analysis
188struct PtxPatterns {
189    kernel_names: Vec<String>,
190    uses_u32_shared: bool,
191    has_barrier: bool,
192    has_shared_mem: bool,
193}
194
195/// Run regression suite on multiple kernels
196#[must_use]
197pub fn run_regression_suite(
198    kernels: &[(&str, &str)], // (name, ptx)
199    baselines: &[(&str, &str)],
200    config: RegressionConfig,
201) -> GpuRegressionSuite {
202    let mut suite = GpuRegressionSuite::new(config);
203
204    // Add baselines
205    for (name, ptx) in baselines {
206        suite.add_baseline(name, ptx);
207    }
208
209    // Test kernels
210    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        // Baseline uses u32 shared memory
253        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        // Current uses u64 (regression!)
266        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        // Should detect regression (u64 addressing)
278        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}