#![allow(clippy::unwrap_used)]
use super::kernel_pixels::{GpuPixelResult, GpuPixelTestSuite, KernelPixelConfig};
use super::ptx_analysis::PtxAnalyzer;
use std::collections::HashMap;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct RegressionConfig {
pub baseline_dir: String,
pub auto_update: bool,
pub pixel_config: KernelPixelConfig,
}
impl Default for RegressionConfig {
fn default() -> Self {
Self {
baseline_dir: ".gpu_baselines".to_string(),
auto_update: false,
pixel_config: KernelPixelConfig::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct RegressionResult {
pub kernel_name: String,
pub is_regression: bool,
pub details: Option<String>,
pub pixel_results: GpuPixelTestSuite,
pub duration: Duration,
}
impl RegressionResult {
#[must_use]
pub fn passed(&self) -> bool {
!self.is_regression && self.pixel_results.all_passed()
}
}
#[derive(Debug)]
pub struct GpuRegressionSuite {
config: RegressionConfig,
baselines: HashMap<String, String>,
results: Vec<RegressionResult>,
}
impl GpuRegressionSuite {
#[must_use]
pub fn new(config: RegressionConfig) -> Self {
Self {
config,
baselines: HashMap::new(),
results: Vec::new(),
}
}
pub fn add_baseline(&mut self, kernel_name: &str, ptx: &str) {
self.baselines
.insert(kernel_name.to_string(), ptx.to_string());
}
pub fn test_kernel(&mut self, kernel_name: &str, ptx: &str) -> &RegressionResult {
let start = Instant::now();
let mut pixel_suite = GpuPixelTestSuite::new(kernel_name);
let ptx_result = PtxAnalyzer::default().analyze(ptx);
pixel_suite.add_result(GpuPixelResult::from_ptx_validation(&ptx_result));
pixel_suite.run_kernel_pixels(ptx, &self.config.pixel_config);
let (is_regression, details) = if let Some(baseline) = self.baselines.get(kernel_name) {
self.compare_ptx(baseline, ptx)
} else {
(false, None)
};
let result = RegressionResult {
kernel_name: kernel_name.to_string(),
is_regression,
details,
pixel_results: pixel_suite,
duration: start.elapsed(),
};
self.results.push(result);
self.results.last().unwrap()
}
fn compare_ptx(&self, baseline: &str, current: &str) -> (bool, Option<String>) {
let baseline_patterns = self.extract_patterns(baseline);
let current_patterns = self.extract_patterns(current);
let mut regressions = Vec::new();
if baseline_patterns.uses_u32_shared && !current_patterns.uses_u32_shared {
regressions.push("Regression: switched from u32 to u64 shared memory addressing");
}
if baseline_patterns.has_barrier && !current_patterns.has_barrier {
regressions.push("Regression: removed barrier synchronization");
}
if baseline_patterns.kernel_names != current_patterns.kernel_names {
regressions.push("Regression: kernel name changed");
}
if regressions.is_empty() {
(false, None)
} else {
(true, Some(regressions.join("; ")))
}
}
fn extract_patterns(&self, ptx: &str) -> PtxPatterns {
let analyzer = PtxAnalyzer::default();
let result = analyzer.analyze(ptx);
PtxPatterns {
kernel_names: result.kernel_names,
uses_u32_shared: !ptx.contains("[%rd")
|| !ptx.contains("st.shared") && !ptx.contains("ld.shared"),
has_barrier: ptx.contains("bar.sync"),
has_shared_mem: ptx.contains(".shared"),
}
}
#[must_use]
pub fn results(&self) -> &[RegressionResult] {
&self.results
}
#[must_use]
pub fn all_passed(&self) -> bool {
self.results.iter().all(|r| r.passed())
}
#[must_use]
pub fn summary(&self) -> String {
let passed = self.results.iter().filter(|r| r.passed()).count();
let total = self.results.len();
let regressions = self.results.iter().filter(|r| r.is_regression).count();
format!(
"GPU Regression Suite: {}/{} passed, {} regressions",
passed, total, regressions
)
}
}
#[derive(Debug, Clone)]
#[allow(dead_code)] struct PtxPatterns {
kernel_names: Vec<String>,
uses_u32_shared: bool,
has_barrier: bool,
has_shared_mem: bool,
}
#[must_use]
pub fn run_regression_suite(
kernels: &[(&str, &str)], baselines: &[(&str, &str)],
config: RegressionConfig,
) -> GpuRegressionSuite {
let mut suite = GpuRegressionSuite::new(config);
for (name, ptx) in baselines {
suite.add_baseline(name, ptx);
}
for (name, ptx) in kernels {
suite.test_kernel(name, ptx);
}
suite
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regression_suite_creation() {
let suite = GpuRegressionSuite::new(RegressionConfig::default());
assert!(suite.results().is_empty());
assert!(suite.all_passed());
}
#[test]
fn test_add_baseline() {
let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
suite.add_baseline("test_kernel", ".version 8.0");
assert!(suite.baselines.contains_key("test_kernel"));
}
#[test]
fn test_no_regression_without_baseline() {
let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
let ptx = r#"
.version 8.0
.target sm_70
.address_size 64
.visible .entry test() { ret; }
"#;
let result = suite.test_kernel("new_kernel", ptx);
assert!(!result.is_regression);
}
#[test]
fn test_regression_detected() {
let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
let baseline = r#"
.version 8.0
.target sm_70
.visible .entry test() {
.shared .b8 smem[1024];
st.shared.f32 [%r0], %f0;
bar.sync 0;
ret;
}
"#;
suite.add_baseline("test", baseline);
let current = r#"
.version 8.0
.target sm_70
.visible .entry test() {
.shared .b8 smem[1024];
st.shared.f32 [%rd0], %f0;
bar.sync 0;
ret;
}
"#;
let result = suite.test_kernel("test", current);
assert!(!result.pixel_results.all_passed() || result.is_regression);
}
#[test]
fn test_summary_format() {
let mut suite = GpuRegressionSuite::new(RegressionConfig::default());
let ptx = ".version 8.0\n.visible .entry t() { ret; }";
suite.test_kernel("k1", ptx);
suite.test_kernel("k2", ptx);
let summary = suite.summary();
assert!(summary.contains("GPU Regression Suite"));
assert!(summary.contains("/2"));
}
#[test]
fn test_run_regression_suite() {
let kernels = vec![
("k1", ".version 8.0\n.visible .entry k1() { ret; }"),
("k2", ".version 8.0\n.visible .entry k2() { ret; }"),
];
let baselines: Vec<(&str, &str)> = vec![];
let suite = run_regression_suite(&kernels, &baselines, RegressionConfig::default());
assert_eq!(suite.results().len(), 2);
}
}