#![cfg(feature = "cuda")]
use oxicuda_driver::Module;
use oxicuda_launch::{grid_size_for, Kernel, LaunchParams};
use oxicuda_memory::DeviceBuffer;
use oxicuda_ptx::arch::SmVersion;
use oxicuda_ptx::builder::KernelBuilder;
use oxicuda_ptx::ir::PtxType;
use std::sync::Arc;
use crate::error::{SpecialError, SpecialResult};
pub fn cuda_is_available() -> bool {
oxicuda_driver::init().is_ok()
&& oxicuda_driver::device::Device::count()
.map(|c| c > 0)
.unwrap_or(false)
}
pub fn generate_erf_ptx() -> SpecialResult<String> {
KernelBuilder::new("erf_batch")
.target(SmVersion::Sm80)
.param("out_ptr", PtxType::U64)
.param("n", PtxType::U32)
.param("in_ptr", PtxType::U64)
.body(move |b| {
let tid = b.global_thread_id_x();
let n_reg = b.load_param_u32("n");
b.if_lt_u32(tid.clone(), n_reg, |b| {
let in_ptr = b.load_param_u64("in_ptr");
let out_ptr = b.load_param_u64("out_ptr");
let eight = b.mov_imm_u32(8);
let off = b.mul_wide_u32_to_u64(tid.clone(), eight);
let in_addr = b.add_u64(in_ptr, off.clone());
let x = b.load_global_f64(in_addr);
let y = b.erf_f64(&x);
let out_addr = b.add_u64(out_ptr, off);
b.store_global_f64(out_addr, y);
});
b.ret();
})
.build()
.map_err(|e| {
SpecialError::ComputationError(format!("erf_batch PTX generation failed: {e}"))
})
}
fn build_handle() -> SpecialResult<(Arc<oxicuda_driver::Context>, oxicuda_driver::stream::Stream)> {
oxicuda_driver::init()
.map_err(|e| SpecialError::GpuNotAvailable(format!("CUDA unavailable: {e}")))?;
let count = oxicuda_driver::device::Device::count()
.map_err(|e| SpecialError::GpuNotAvailable(format!("device count: {e}")))?;
if count <= 0 {
return Err(SpecialError::GpuNotAvailable(
"no NVIDIA CUDA device available".into(),
));
}
let dev = oxicuda_driver::device::Device::get(0)
.map_err(|e| SpecialError::GpuNotAvailable(format!("device get: {e}")))?;
let ctx = Arc::new(
oxicuda_driver::Context::new(&dev)
.map_err(|e| SpecialError::GpuNotAvailable(format!("context: {e}")))?,
);
let stream = oxicuda_driver::stream::Stream::new(&ctx)
.map_err(|e| SpecialError::GpuNotAvailable(format!("stream: {e}")))?;
Ok((ctx, stream))
}
fn launch_err(e: oxicuda_driver::CudaError) -> SpecialError {
SpecialError::ComputationError(format!("oxicuda: {e}"))
}
pub fn cuda_erf_batch(input: &[f64]) -> SpecialResult<Vec<f64>> {
if input.is_empty() {
return Ok(Vec::new());
}
let n = input.len();
let ptx = generate_erf_ptx()?;
let (_ctx, stream) = build_handle()?;
let d_in = DeviceBuffer::from_host(input).map_err(launch_err)?;
let d_out = DeviceBuffer::<f64>::alloc(n).map_err(launch_err)?;
let module = Arc::new(
Module::from_ptx(&ptx)
.map_err(|e| SpecialError::ComputationError(format!("module from_ptx: {e}")))?,
);
let kernel = Kernel::from_module(module, "erf_batch")
.map_err(|e| SpecialError::ComputationError(format!("kernel from_module: {e}")))?;
let block = 256u32;
let grid = grid_size_for(n as u32, block);
let params = LaunchParams::new(grid, block);
let args = (d_out.as_device_ptr(), n as u32, d_in.as_device_ptr());
kernel
.launch(¶ms, &stream, &args)
.map_err(|e| SpecialError::ComputationError(format!("launch: {e}")))?;
stream
.synchronize()
.map_err(|e| SpecialError::ComputationError(format!("sync: {e}")))?;
let mut host_out = vec![0.0f64; n];
d_out.copy_to_host(&mut host_out).map_err(launch_err)?;
Ok(host_out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_erf_ptx_has_erf_markers() {
let ptx = generate_erf_ptx().expect("ptx");
assert!(ptx.contains(".entry erf_batch"), "missing entry: {ptx}");
assert!(ptx.contains("sm_80"), "missing target: {ptx}");
assert!(
ptx.contains("ld.global.f64"),
"missing ld.global.f64: {ptx}"
);
assert!(
ptx.contains("st.global.f64"),
"missing st.global.f64: {ptx}"
);
assert!(ptx.contains("fma.rn.f64"), "missing fma.rn.f64: {ptx}");
assert!(ptx.contains("abs.f64"), "missing abs.f64: {ptx}");
assert!(
ptx.contains("cvt.rzi.s32.f64"),
"missing cvt.rzi.s32.f64: {ptx}"
);
assert!(
!ptx.contains(".approx.f64"),
"honest f64 invariant violated: {ptx}"
);
}
#[test]
fn cuda_erf_batch_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let input: Vec<f64> = vec![-1.5, -0.5, 0.0, 0.25, 0.5, 1.0, 2.0];
let out = cuda_erf_batch(&input).expect("cuda erf");
assert_eq!(out.len(), input.len());
for (i, &x) in input.iter().enumerate() {
let want = crate::erf(x);
assert!(
(out[i] - want).abs() < 1e-6,
"erf({x}): got {}, want {want}",
out[i]
);
}
}
#[test]
fn cuda_erf_tail_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let input: Vec<f64> = vec![
-8.0, -6.0, -4.0, -2.0, -1.0, -0.25, 0.0, 0.25, 1.0, 2.0, 4.0, 6.0, 8.0,
];
let out = cuda_erf_batch(&input).expect("cuda erf tails");
assert_eq!(out.len(), input.len());
let tol = 1e-6_f64;
for (i, &x) in input.iter().enumerate() {
let want = crate::erf(x);
assert!(
(out[i] - want).abs() < tol,
"erf({x}): gpu={}, cpu={want}, |diff|={}",
out[i],
(out[i] - want).abs()
);
}
let neg_tail = input.iter().position(|&x| x == -8.0).expect("idx -8");
let pos_tail = input.iter().position(|&x| x == 8.0).expect("idx +8");
assert!(
(out[neg_tail] - (-1.0_f64)).abs() < tol,
"erf(-8) should saturate to -1, got {}",
out[neg_tail]
);
assert!(
(out[pos_tail] - 1.0_f64).abs() < tol,
"erf(8) should saturate to +1, got {}",
out[pos_tail]
);
}
}