#![cfg(feature = "cuda")]
use crate::error::{OptimizeError, OptimizeResult};
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) -> OptimizeError {
OptimizeError::ComputationError(format!("oxicuda-blas: {e}"))
}
fn cuda_err(e: oxicuda_driver::CudaError) -> OptimizeError {
OptimizeError::ComputationError(format!("oxicuda CUDA driver: {e}"))
}
fn build_context() -> OptimizeResult<std::sync::Arc<oxicuda_driver::Context>> {
oxicuda_driver::init()
.map_err(|e| OptimizeError::ComputationError(format!("CUDA unavailable: {e}")))?;
let count = oxicuda_driver::device::Device::count()
.map_err(|e| OptimizeError::ComputationError(format!("device count: {e}")))?;
if count <= 0 {
return Err(OptimizeError::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_hessian_vector_product(
h: &ArrayView2<f64>,
v: &ArrayView1<f64>,
) -> OptimizeResult<Array1<f64>> {
let n = h.nrows();
if !h.is_square() {
return Err(OptimizeError::ValueError(format!(
"cuda_hessian_vector_product: Hessian must be square, got {n}x{}",
h.ncols()
)));
}
if v.len() != n {
return Err(OptimizeError::ValueError(format!(
"cuda_hessian_vector_product: vector length {} does not match Hessian dimension {n}",
v.len()
)));
}
if n == 0 {
return Ok(Array1::zeros(0));
}
let h_std = h.as_standard_layout();
let h_slice = h_std.as_slice().ok_or_else(|| {
OptimizeError::ComputationError("cuda_hessian_vector_product: H not contiguous".into())
})?;
let v_vec: Vec<f64> = v.iter().copied().collect();
let ctx = build_context()?;
let handle = oxicuda_blas::BlasHandle::new(&ctx).map_err(blas_err)?;
let d_h = oxicuda_memory::DeviceBuffer::from_host(h_slice).map_err(cuda_err)?;
let d_v = oxicuda_memory::DeviceBuffer::from_host(&v_vec).map_err(cuda_err)?;
let mut d_c = oxicuda_memory::DeviceBuffer::<f64>::alloc(n).map_err(cuda_err)?;
let a_desc =
MatrixDesc::from_buffer(&d_h, n as u32, n as u32, Layout::RowMajor).map_err(blas_err)?;
let b_desc = MatrixDesc::from_buffer(&d_v, n as u32, 1, Layout::ColMajor).map_err(blas_err)?;
let mut c_desc =
MatrixDescMut::from_buffer(&mut d_c, n 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 hv = vec![0.0f64; n];
d_c.copy_to_host(&mut hv).map_err(cuda_err)?;
Ok(Array1::from_vec(hv))
}
#[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_hessian_vector_product_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let h = mat(3, 3, vec![4.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0]);
let v = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let hv = cuda_hessian_vector_product(&h.view(), &v.view())
.expect("cuda_hessian_vector_product failed");
let expected = h.dot(&v);
let max_diff = hv
.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_hessian_vector_product_nonsymmetric_or_skip() {
if !cuda_is_available() {
eprintln!("skipping: no NVIDIA CUDA device");
assert!(!cuda_is_available());
return;
}
let h = mat(
4,
4,
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
16.0,
],
);
let v = Array1::from_vec(vec![1.0, -1.0, 2.0, -2.0]);
let hv = cuda_hessian_vector_product(&h.view(), &v.view())
.expect("cuda_hessian_vector_product nonsymmetric failed");
let expected = h.dot(&v);
let max_diff = hv
.iter()
.zip(expected.iter())
.map(|(g, e)| (g - e).abs())
.fold(0.0f64, f64::max);
assert!(
max_diff < 1e-9,
"nonsymmetric 4x4 HVP max abs diff {max_diff} exceeds 1e-9"
);
}
#[test]
fn cuda_hessian_vector_product_shape_mismatch_errors() {
let nonsquare = mat(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let v = Array1::from_vec(vec![1.0, 2.0, 3.0]);
assert!(cuda_hessian_vector_product(&nonsquare.view(), &v.view()).is_err());
let square = mat(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
let v_bad = Array1::from_vec(vec![1.0, 2.0, 3.0]);
assert!(cuda_hessian_vector_product(&square.view(), &v_bad.view()).is_err());
}
}