1#![cfg(feature = "cuda")]
2use crate::error::{OptimizeError, OptimizeResult};
34use oxicuda_blas::types::{Layout, MatrixDesc, MatrixDescMut, Transpose};
35use scirs2_core::ndarray::{Array1, ArrayView1, ArrayView2};
36
37pub fn cuda_is_available() -> bool {
44 oxicuda_driver::init().is_ok()
45 && oxicuda_driver::device::Device::count()
46 .map(|c| c > 0)
47 .unwrap_or(false)
48}
49
50fn blas_err(e: oxicuda_blas::error::BlasError) -> OptimizeError {
52 OptimizeError::ComputationError(format!("oxicuda-blas: {e}"))
53}
54
55fn cuda_err(e: oxicuda_driver::CudaError) -> OptimizeError {
57 OptimizeError::ComputationError(format!("oxicuda CUDA driver: {e}"))
58}
59
60fn build_context() -> OptimizeResult<std::sync::Arc<oxicuda_driver::Context>> {
66 oxicuda_driver::init()
67 .map_err(|e| OptimizeError::ComputationError(format!("CUDA unavailable: {e}")))?;
68 let count = oxicuda_driver::device::Device::count()
69 .map_err(|e| OptimizeError::ComputationError(format!("device count: {e}")))?;
70 if count <= 0 {
71 return Err(OptimizeError::ComputationError(
72 "no NVIDIA CUDA device available".into(),
73 ));
74 }
75 let dev = oxicuda_driver::device::Device::get(0).map_err(cuda_err)?;
76 Ok(std::sync::Arc::new(
77 oxicuda_driver::Context::new(&dev).map_err(cuda_err)?,
78 ))
79}
80
81pub fn cuda_hessian_vector_product(
97 h: &ArrayView2<f64>,
98 v: &ArrayView1<f64>,
99) -> OptimizeResult<Array1<f64>> {
100 let n = h.nrows();
101 if !h.is_square() {
102 return Err(OptimizeError::ValueError(format!(
103 "cuda_hessian_vector_product: Hessian must be square, got {n}x{}",
104 h.ncols()
105 )));
106 }
107 if v.len() != n {
108 return Err(OptimizeError::ValueError(format!(
109 "cuda_hessian_vector_product: vector length {} does not match Hessian dimension {n}",
110 v.len()
111 )));
112 }
113 if n == 0 {
114 return Ok(Array1::zeros(0));
115 }
116
117 let h_std = h.as_standard_layout();
120 let h_slice = h_std.as_slice().ok_or_else(|| {
121 OptimizeError::ComputationError("cuda_hessian_vector_product: H not contiguous".into())
122 })?;
123 let v_vec: Vec<f64> = v.iter().copied().collect();
124
125 let ctx = build_context()?;
126 let handle = oxicuda_blas::BlasHandle::new(&ctx).map_err(blas_err)?;
127
128 let d_h = oxicuda_memory::DeviceBuffer::from_host(h_slice).map_err(cuda_err)?;
129 let d_v = oxicuda_memory::DeviceBuffer::from_host(&v_vec).map_err(cuda_err)?;
130 let mut d_c = oxicuda_memory::DeviceBuffer::<f64>::alloc(n).map_err(cuda_err)?;
131
132 let a_desc =
135 MatrixDesc::from_buffer(&d_h, n as u32, n as u32, Layout::RowMajor).map_err(blas_err)?;
136 let b_desc = MatrixDesc::from_buffer(&d_v, n as u32, 1, Layout::ColMajor).map_err(blas_err)?;
137 let mut c_desc =
138 MatrixDescMut::from_buffer(&mut d_c, n as u32, 1, Layout::ColMajor).map_err(blas_err)?;
139
140 oxicuda_blas::level3::gemm_api::gemm::<f64>(
141 &handle,
142 Transpose::NoTrans,
143 Transpose::NoTrans,
144 1.0,
145 &a_desc,
146 &b_desc,
147 0.0,
148 &mut c_desc,
149 )
150 .map_err(blas_err)?;
151
152 let mut hv = vec![0.0f64; n];
153 d_c.copy_to_host(&mut hv).map_err(cuda_err)?;
154 Ok(Array1::from_vec(hv))
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use scirs2_core::ndarray::Array2;
161
162 fn mat(rows: usize, cols: usize, data: Vec<f64>) -> Array2<f64> {
163 Array2::from_shape_vec((rows, cols), data).expect("valid test matrix shape")
164 }
165
166 #[test]
167 fn cuda_hessian_vector_product_or_skip() {
168 if !cuda_is_available() {
169 eprintln!("skipping: no NVIDIA CUDA device");
170 assert!(!cuda_is_available());
171 return;
172 }
173 let h = mat(3, 3, vec![4.0, 1.0, 0.0, 1.0, 3.0, 1.0, 0.0, 1.0, 2.0]);
175 let v = Array1::from_vec(vec![1.0, 2.0, 3.0]);
176 let hv = cuda_hessian_vector_product(&h.view(), &v.view())
177 .expect("cuda_hessian_vector_product failed");
178 let expected = h.dot(&v);
179 let max_diff = hv
180 .iter()
181 .zip(expected.iter())
182 .map(|(g, e)| (g - e).abs())
183 .fold(0.0f64, f64::max);
184 assert!(max_diff < 1e-9, "max abs diff {max_diff} exceeds 1e-9");
185 }
186
187 #[test]
188 fn cuda_hessian_vector_product_nonsymmetric_or_skip() {
189 if !cuda_is_available() {
190 eprintln!("skipping: no NVIDIA CUDA device");
191 assert!(!cuda_is_available());
192 return;
193 }
194 let h = mat(
198 4,
199 4,
200 vec![
201 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,
202 16.0,
203 ],
204 );
205 let v = Array1::from_vec(vec![1.0, -1.0, 2.0, -2.0]);
206 let hv = cuda_hessian_vector_product(&h.view(), &v.view())
207 .expect("cuda_hessian_vector_product nonsymmetric failed");
208 let expected = h.dot(&v);
209 let max_diff = hv
210 .iter()
211 .zip(expected.iter())
212 .map(|(g, e)| (g - e).abs())
213 .fold(0.0f64, f64::max);
214 assert!(
215 max_diff < 1e-9,
216 "nonsymmetric 4x4 HVP max abs diff {max_diff} exceeds 1e-9"
217 );
218 }
219
220 #[test]
221 fn cuda_hessian_vector_product_shape_mismatch_errors() {
222 let nonsquare = mat(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
225 let v = Array1::from_vec(vec![1.0, 2.0, 3.0]);
226 assert!(cuda_hessian_vector_product(&nonsquare.view(), &v.view()).is_err());
227
228 let square = mat(2, 2, vec![1.0, 0.0, 0.0, 1.0]);
230 let v_bad = Array1::from_vec(vec![1.0, 2.0, 3.0]);
231 assert!(cuda_hessian_vector_product(&square.view(), &v_bad.view()).is_err());
232 }
233}