#![cfg(feature = "cuda")]
use crate::error::{DatasetsError, Result};
use oxicuda_blas::types::{Layout, MatrixDesc, MatrixDescMut, Transpose};
use scirs2_core::ndarray::{Array1, ArrayView1, ArrayView2};
pub fn cuda_is_available() -> bool {
oxicuda_driver::init().is_ok()
&& oxicuda_driver::device::Device::count()
.map(|c| c > 0)
.unwrap_or(false)
}
fn blas_err(e: oxicuda_blas::error::BlasError) -> DatasetsError {
DatasetsError::ComputationError(format!("oxicuda-blas: {e}"))
}
fn cuda_err(e: oxicuda_driver::CudaError) -> DatasetsError {
DatasetsError::ComputationError(format!("oxicuda CUDA driver: {e}"))
}
fn build_context() -> Result<std::sync::Arc<oxicuda_driver::Context>> {
oxicuda_driver::init()
.map_err(|e| DatasetsError::ComputationError(format!("CUDA unavailable: {e}")))?;
let count = oxicuda_driver::device::Device::count()
.map_err(|e| DatasetsError::ComputationError(format!("device count: {e}")))?;
if count <= 0 {
return Err(DatasetsError::ComputationError(
"no NVIDIA CUDA device available".into(),
));
}
let dev = oxicuda_driver::device::Device::get(0).map_err(cuda_err)?;
Ok(std::sync::Arc::new(
oxicuda_driver::Context::new(&dev).map_err(cuda_err)?,
))
}
pub fn cuda_regression_target(x: &ArrayView2<f64>, coef: &ArrayView1<f64>) -> Result<Array1<f64>> {
let n_samples = x.nrows();
let n_features = x.ncols();
if coef.len() != n_features {
return Err(DatasetsError::ValidationError(format!(
"cuda_regression_target: coef length {} does not match X column count {n_features}",
coef.len()
)));
}
if n_samples == 0 {
return Ok(Array1::zeros(0));
}
if n_features == 0 {
return Ok(Array1::zeros(n_samples));
}
let x_std = x.as_standard_layout();
let x_slice = x_std.as_slice().ok_or_else(|| {
DatasetsError::ComputationError("cuda_regression_target: X not contiguous".into())
})?;
let coef_vec: Vec<f64> = coef.iter().copied().collect();
let ctx = build_context()?;
let handle = oxicuda_blas::BlasHandle::new(&ctx).map_err(blas_err)?;
let d_x = oxicuda_memory::DeviceBuffer::from_host(x_slice).map_err(cuda_err)?;
let d_coef = oxicuda_memory::DeviceBuffer::from_host(&coef_vec).map_err(cuda_err)?;
let mut d_y = oxicuda_memory::DeviceBuffer::<f64>::alloc(n_samples).map_err(cuda_err)?;
let a_desc =
MatrixDesc::from_buffer(&d_x, n_samples as u32, n_features as u32, Layout::RowMajor)
.map_err(blas_err)?;
let b_desc = MatrixDesc::from_buffer(&d_coef, n_features as u32, 1, Layout::ColMajor)
.map_err(blas_err)?;
let mut c_desc = MatrixDescMut::from_buffer(&mut d_y, n_samples as u32, 1, Layout::ColMajor)
.map_err(blas_err)?;
oxicuda_blas::level3::gemm_api::gemm::<f64>(
&handle,
Transpose::NoTrans,
Transpose::NoTrans,
1.0,
&a_desc,
&b_desc,
0.0,
&mut c_desc,
)
.map_err(blas_err)?;
let mut y = vec![0.0f64; n_samples];
d_y.copy_to_host(&mut y).map_err(cuda_err)?;
Ok(Array1::from_vec(y))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn mat(rows: usize, cols: usize, data: Vec<f64>) -> Array2<f64> {
Array2::from_shape_vec((rows, cols), data).expect("valid test matrix shape")
}
#[test]
fn cuda_regression_target_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let x = mat(3, 2, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let coef = Array1::from_vec(vec![1.0, 1.0]);
let y =
cuda_regression_target(&x.view(), &coef.view()).expect("cuda_regression_target failed");
let expected = x.dot(&coef);
let max_diff = y
.iter()
.zip(expected.iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0f64, f64::max);
assert!(max_diff < 1e-9, "max abs diff {max_diff} exceeds 1e-9");
}
#[test]
fn cuda_regression_target_shape_mismatch_errors() {
let x = mat(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let coef_bad = Array1::from_vec(vec![1.0, 2.0, 3.0]);
assert!(cuda_regression_target(&x.view(), &coef_bad.view()).is_err());
}
}