use std::sync::Arc;
use oxicuda_driver::{Context, Device, Module, Stream};
use oxicuda_launch::{Kernel, LaunchParams};
use oxicuda_memory::DeviceBuffer;
use crate::handle::LcgRng;
struct GpuFixture {
ctx: Arc<Context>,
sm: u32,
}
fn gpu_fixture() -> Option<GpuFixture> {
oxicuda_driver::init().ok()?;
if Device::count().ok()? == 0 {
return None;
}
let Ok(dev) = Device::get(0) else {
return None;
};
let (major, minor) = dev.compute_capability().ok()?;
let sm = (major * 10 + minor) as u32;
let ctx = Context::new(&dev).ok()?;
Some(GpuFixture {
ctx: Arc::new(ctx),
sm,
})
}
fn close(a: f32, b: f32, rel: f32, abs: f32) -> bool {
(a - b).abs() <= rel * a.abs().max(b.abs()) + abs
}
fn worst_diff(gpu: &[f32], cpu: &[f32]) -> (f32, f32) {
let mut worst_abs = 0.0_f32;
let mut worst_rel = 0.0_f32;
for (&g, &c) in gpu.iter().zip(cpu.iter()) {
let a = (g - c).abs();
if a > worst_abs {
worst_abs = a;
}
let denom = g.abs().max(c.abs());
if denom > 0.0 {
let r = a / denom;
if r > worst_rel {
worst_rel = r;
}
}
}
(worst_rel, worst_abs)
}
fn load_kernel(ptx: &str, entry: &str) -> Kernel {
let module = Module::from_ptx(ptx)
.unwrap_or_else(|e| panic!("PTX JIT compile failed for `{entry}`: {e}"));
Kernel::from_module(Arc::new(module), entry)
.unwrap_or_else(|e| panic!("kernel `{entry}` not found in module: {e}"))
}
fn grid_1d(n: u32, block: u32) -> u32 {
n.div_ceil(block)
}
fn fill(rng: &mut LcgRng, n: usize, lo: f32, hi: f32) -> Vec<f32> {
(0..n)
.map(|_| (f64::from(lo) + f64::from(hi - lo) * rng.next_f64()) as f32)
.collect()
}
#[test]
fn correlate_matches_crate() {
use crate::linalg::mat_t_vec;
let Some(fx) = gpu_fixture() else {
return;
};
let m = 12_usize;
let n = 20_usize;
let mut rng = LcgRng::new(0x0C0_1A7E);
let phi = fill(&mut rng, m * n, -1.0, 1.0);
let r = fill(&mut rng, m, -1.0, 1.0);
let phi64: Vec<f64> = phi.iter().map(|&v| f64::from(v)).collect();
let r64: Vec<f64> = r.iter().map(|&v| f64::from(v)).collect();
let c_cpu_f64 = mat_t_vec(&phi64, m, n, &r64).expect("mat_t_vec");
let c_cpu: Vec<f32> = c_cpu_f64.iter().map(|&v| v as f32).collect();
let ptx = crate::ptx_kernels::correlate_ptx(fx.sm);
let kernel = load_kernel(&ptx, "correlate_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_c = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_c");
let d_phi = DeviceBuffer::<f32>::from_host(&phi).expect("d_phi");
let d_r = DeviceBuffer::<f32>::from_host(&r).expect("d_r");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(
d_c.as_device_ptr(),
d_phi.as_device_ptr(),
d_r.as_device_ptr(),
m as u32,
n as u32,
),
)
.expect("launch correlate_kernel");
stream.synchronize().expect("sync");
let mut c_gpu = vec![0.0_f32; n];
d_c.copy_to_host(&mut c_gpu).expect("copy c");
let (rel, abs) = worst_diff(&c_gpu, &c_cpu);
for j in 0..n {
assert!(
close(c_gpu[j], c_cpu[j], 1e-4, 1e-5),
"correlate c[{j}] mismatch: gpu={} cpu={} (worst rel={rel:e} abs={abs:e})",
c_gpu[j],
c_cpu[j]
);
}
}
#[test]
fn hard_threshold_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let n = 64_usize;
let threshold = 0.5_f32;
let mut rng = LcgRng::new(0x4A2D_7011);
let x = fill(&mut rng, n, -2.0, 2.0);
let expected: Vec<f32> = x
.iter()
.map(|&v| if v.abs() > threshold { v } else { 0.0 })
.collect();
let ptx = crate::ptx_kernels::hard_threshold_ptx(fx.sm);
let kernel = load_kernel(&ptx, "hard_threshold_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_out = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_out");
let d_x = DeviceBuffer::<f32>::from_host(&x).expect("d_x");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(
d_out.as_device_ptr(),
d_x.as_device_ptr(),
threshold,
n as u32,
),
)
.expect("launch hard_threshold_kernel");
stream.synchronize().expect("sync");
let mut out_gpu = vec![0.0_f32; n];
d_out.copy_to_host(&mut out_gpu).expect("copy out");
for (k, (&g, &e)) in out_gpu.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_bits(),
e.to_bits(),
"hard_threshold out[{k}] mismatch: gpu={g} host={e} (x={})",
x[k]
);
}
}
#[test]
fn soft_threshold_matches_crate() {
use crate::thresholding::soft_threshold;
let Some(fx) = gpu_fixture() else {
return;
};
let n = 64_usize;
let lambda = 0.3_f32;
let mut rng = LcgRng::new(0x50F7_0FED);
let x = fill(&mut rng, n, -2.0, 2.0);
let x64: Vec<f64> = x.iter().map(|&v| f64::from(v)).collect();
let y_cpu_f64 = soft_threshold(&x64, f64::from(lambda));
let y_cpu: Vec<f32> = y_cpu_f64.iter().map(|&v| v as f32).collect();
let ptx = crate::ptx_kernels::soft_threshold_ptx(fx.sm);
let kernel = load_kernel(&ptx, "soft_threshold_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_y = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_y");
let d_x = DeviceBuffer::<f32>::from_host(&x).expect("d_x");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(d_y.as_device_ptr(), d_x.as_device_ptr(), lambda, n as u32),
)
.expect("launch soft_threshold_kernel");
stream.synchronize().expect("sync");
let mut y_gpu = vec![0.0_f32; n];
d_y.copy_to_host(&mut y_gpu).expect("copy y");
let (rel, abs) = worst_diff(&y_gpu, &y_cpu);
for k in 0..n {
assert!(
close(y_gpu[k], y_cpu[k], 1e-5, 1e-6),
"soft_threshold y[{k}] mismatch: gpu={} cpu={} (worst rel={rel:e} abs={abs:e})",
y_gpu[k],
y_cpu[k]
);
}
}
#[test]
fn iht_step_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let n = 128_usize;
let mu = 0.25_f32;
let mut rng = LcgRng::new(0x1147_5742);
let x0 = fill(&mut rng, n, -1.0, 1.0);
let grad = fill(&mut rng, n, -1.0, 1.0);
let expected: Vec<f32> = x0
.iter()
.zip(grad.iter())
.map(|(&x, &g)| x + mu * g)
.collect();
let ptx = crate::ptx_kernels::iht_step_ptx(fx.sm);
let kernel = load_kernel(&ptx, "iht_step_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_x = DeviceBuffer::<f32>::from_host(&x0).expect("d_x");
let d_grad = DeviceBuffer::<f32>::from_host(&grad).expect("d_grad");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(d_x.as_device_ptr(), d_grad.as_device_ptr(), mu, n as u32),
)
.expect("launch iht_step_kernel");
stream.synchronize().expect("sync");
let mut x_gpu = vec![0.0_f32; n];
d_x.copy_to_host(&mut x_gpu).expect("copy x");
let (rel, abs) = worst_diff(&x_gpu, &expected);
for k in 0..n {
assert!(
close(x_gpu[k], expected[k], 1e-5, 1e-6),
"iht_step x[{k}] mismatch: gpu={} host={} (worst rel={rel:e} abs={abs:e})",
x_gpu[k],
expected[k]
);
}
}
#[test]
fn amp_onsager_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let m = 100_usize;
let b_over_m = 0.7_f32;
let mut rng = LcgRng::new(0xA3B0_05A6);
let residual = fill(&mut rng, m, -1.5, 1.5);
let z_prev = fill(&mut rng, m, -1.5, 1.5);
let expected: Vec<f32> = residual
.iter()
.zip(z_prev.iter())
.map(|(&r, &z)| r + b_over_m * z)
.collect();
let ptx = crate::ptx_kernels::amp_onsager_ptx(fx.sm);
let kernel = load_kernel(&ptx, "amp_onsager_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_new = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; m]).expect("d_new");
let d_res = DeviceBuffer::<f32>::from_host(&residual).expect("d_res");
let d_prev = DeviceBuffer::<f32>::from_host(&z_prev).expect("d_prev");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(m as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(
d_new.as_device_ptr(),
d_res.as_device_ptr(),
d_prev.as_device_ptr(),
b_over_m,
m as u32,
),
)
.expect("launch amp_onsager_kernel");
stream.synchronize().expect("sync");
let mut z_gpu = vec![0.0_f32; m];
d_new.copy_to_host(&mut z_gpu).expect("copy z_new");
let (rel, abs) = worst_diff(&z_gpu, &expected);
for k in 0..m {
assert!(
close(z_gpu[k], expected[k], 1e-5, 1e-6),
"amp_onsager z_new[{k}] mismatch: gpu={} host={} (worst rel={rel:e} abs={abs:e})",
z_gpu[k],
expected[k]
);
}
}
#[test]
fn svt_threshold_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let n = 64_usize;
let tau = 0.4_f32;
let mut rng = LcgRng::new(0x5174_7A11);
let sigma = fill(&mut rng, n, 0.0, 2.0);
let expected: Vec<f32> = sigma.iter().map(|&s| (s - tau).max(0.0)).collect();
let ptx = crate::ptx_kernels::svt_threshold_ptx(fx.sm);
let kernel = load_kernel(&ptx, "svt_threshold_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_out = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_out");
let d_in = DeviceBuffer::<f32>::from_host(&sigma).expect("d_in");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(d_out.as_device_ptr(), d_in.as_device_ptr(), tau, n as u32),
)
.expect("launch svt_threshold_kernel");
stream.synchronize().expect("sync");
let mut out_gpu = vec![0.0_f32; n];
d_out.copy_to_host(&mut out_gpu).expect("copy out");
for (k, (&g, &e)) in out_gpu.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_bits(),
e.to_bits(),
"svt_threshold out[{k}] mismatch: gpu={g} host={e} (sigma={})",
sigma[k]
);
}
}
#[test]
fn tv_grad_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let n = 64_usize;
let mut rng = LcgRng::new(0x7174_6D11);
let x = fill(&mut rng, n, -3.0, 3.0);
let mut expected = vec![0.0_f32; n];
for i in 0..n - 1 {
expected[i] = x[i + 1] - x[i];
}
let ptx = crate::ptx_kernels::tv_grad_ptx(fx.sm);
let kernel = load_kernel(&ptx, "tv_grad_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_grad = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_grad");
let d_x = DeviceBuffer::<f32>::from_host(&x).expect("d_x");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(d_grad.as_device_ptr(), d_x.as_device_ptr(), n as u32),
)
.expect("launch tv_grad_kernel");
stream.synchronize().expect("sync");
let mut grad_gpu = vec![0.0_f32; n];
d_grad.copy_to_host(&mut grad_gpu).expect("copy grad");
for (k, (&g, &e)) in grad_gpu.iter().zip(expected.iter()).enumerate() {
assert_eq!(
g.to_bits(),
e.to_bits(),
"tv_grad grad[{k}] mismatch: gpu={g} host={e}"
);
}
}
fn run_iht_cp_async(fx: &GpuFixture, n: usize, block: u32) {
let mu = 0.3125_f32; let mut rng = LcgRng::new(0xCA50_0000 ^ n as u64);
let x0 = fill(&mut rng, n, -1.0, 1.0);
let grad = fill(&mut rng, n, -1.0, 1.0);
let expected: Vec<f32> = x0
.iter()
.zip(grad.iter())
.map(|(&x, &g)| x + mu * g)
.collect();
let ptx = crate::ptx_advanced::iht_step_cp_async_ptx(fx.sm);
let kernel = load_kernel(&ptx, "iht_step_cp_async_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_x = DeviceBuffer::<f32>::from_host(&x0).expect("d_x");
let d_grad = DeviceBuffer::<f32>::from_host(&grad).expect("d_grad");
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(d_x.as_device_ptr(), d_grad.as_device_ptr(), mu, n as u32),
)
.expect("launch iht_step_cp_async_kernel");
stream.synchronize().expect("sync");
let mut x_gpu = vec![0.0_f32; n];
d_x.copy_to_host(&mut x_gpu).expect("copy x");
let (rel, abs) = worst_diff(&x_gpu, &expected);
for k in 0..n {
assert!(
close(x_gpu[k], expected[k], 1e-5, 1e-6),
"iht_cp_async x[{k}] mismatch (n={n}, block={block}): gpu={} host={} \
(worst rel={rel:e} abs={abs:e})",
x_gpu[k],
expected[k]
);
}
}
#[test]
fn iht_step_cp_async_aligned_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
run_iht_cp_async(&fx, 256, 256);
}
#[test]
fn iht_step_cp_async_ragged_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
run_iht_cp_async(&fx, 300, 256);
}
#[test]
fn svt_threshold_ws_matches_host() {
let Some(fx) = gpu_fixture() else {
return;
};
let n = 24_usize;
let tau = 0.7_f32;
let block = 32_u32;
let mut rng = LcgRng::new(0x5374_7757);
let sigma = fill(&mut rng, n, 0.0, 2.0);
let thresholded: Vec<f32> = sigma.iter().map(|&s| (s - tau).max(0.0)).collect();
let nucnorm_expected: f32 = thresholded.iter().copied().sum();
let ptx = crate::ptx_advanced::svt_threshold_warpshuffle_ptx(fx.sm);
let kernel = load_kernel(&ptx, "svt_threshold_ws_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_out = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_out");
let d_partial = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; 1]).expect("d_partial");
let d_in = DeviceBuffer::<f32>::from_host(&sigma).expect("d_in");
let params = LaunchParams::new(1_u32, block);
kernel
.launch(
¶ms,
&stream,
&(
d_out.as_device_ptr(),
d_partial.as_device_ptr(),
d_in.as_device_ptr(),
tau,
n as u32,
),
)
.expect("launch svt_threshold_ws_kernel");
stream.synchronize().expect("sync");
let mut out_gpu = vec![0.0_f32; n];
let mut partial_gpu = vec![0.0_f32; 1];
d_out.copy_to_host(&mut out_gpu).expect("copy out");
d_partial
.copy_to_host(&mut partial_gpu)
.expect("copy partial");
for (k, (&g, &e)) in out_gpu.iter().zip(thresholded.iter()).enumerate() {
assert_eq!(
g.to_bits(),
e.to_bits(),
"svt_ws out[{k}] mismatch: gpu={g} host={e} (sigma={})",
sigma[k]
);
}
assert!(
close(partial_gpu[0], nucnorm_expected, 1e-5, 1e-6),
"svt_ws nuclear-norm partial mismatch: gpu={} host={}",
partial_gpu[0],
nucnorm_expected
);
}
fn run_correlate_fallback(fx: &GpuFixture, ptx: &str) {
use crate::linalg::mat_t_vec;
let m = 10_usize;
let n = 16_usize;
let mut rng = LcgRng::new(0xFA11_BACC);
let phi = fill(&mut rng, m * n, -1.0, 1.0);
let r = fill(&mut rng, m, -1.0, 1.0);
let phi64: Vec<f64> = phi.iter().map(|&v| f64::from(v)).collect();
let r64: Vec<f64> = r.iter().map(|&v| f64::from(v)).collect();
let c_cpu_f64 = mat_t_vec(&phi64, m, n, &r64).expect("mat_t_vec");
let c_cpu: Vec<f32> = c_cpu_f64.iter().map(|&v| v as f32).collect();
let kernel = load_kernel(ptx, "correlate_kernel");
let stream = Stream::new(&fx.ctx).expect("stream");
let d_c = DeviceBuffer::<f32>::from_host(&vec![0.0_f32; n]).expect("d_c");
let d_phi = DeviceBuffer::<f32>::from_host(&phi).expect("d_phi");
let d_r = DeviceBuffer::<f32>::from_host(&r).expect("d_r");
let block = 256_u32;
let params = LaunchParams::new(grid_1d(n as u32, block), block);
kernel
.launch(
¶ms,
&stream,
&(
d_c.as_device_ptr(),
d_phi.as_device_ptr(),
d_r.as_device_ptr(),
m as u32,
n as u32,
),
)
.expect("launch correlate fallback");
stream.synchronize().expect("sync");
let mut c_gpu = vec![0.0_f32; n];
d_c.copy_to_host(&mut c_gpu).expect("copy c");
for j in 0..n {
assert!(
close(c_gpu[j], c_cpu[j], 1e-4, 1e-5),
"correlate fallback c[{j}] mismatch: gpu={} cpu={}",
c_gpu[j],
c_cpu[j]
);
}
}
#[test]
fn correlate_tma_fallback_matches_crate_on_sm86() {
let Some(fx) = gpu_fixture() else {
return;
};
let ptx = crate::ptx_advanced::correlate_tma_ptx(fx.sm);
if fx.sm >= 90 {
return; }
assert_eq!(
ptx,
crate::ptx_kernels::correlate_ptx(fx.sm),
"sm {} TMA fallback must equal portable correlate",
fx.sm
);
run_correlate_fallback(&fx, &ptx);
}
#[test]
fn correlate_fp8_fallback_matches_crate_on_sm86() {
let Some(fx) = gpu_fixture() else {
return;
};
let ptx = crate::ptx_advanced::correlate_fp8_ptx(fx.sm);
if fx.sm >= 89 {
return; }
assert_eq!(
ptx,
crate::ptx_kernels::correlate_ptx(fx.sm),
"sm {} FP8 fallback must equal portable correlate",
fx.sm
);
run_correlate_fallback(&fx, &ptx);
}