1use std::cell::Cell;
27
28use oxicuda_blas::{DiagType, FillMode};
29
30use crate::error::{SparseError, SparseResult};
31use crate::host_csr::HostCsr;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
40pub enum CusparsePointerMode {
41 #[default]
43 Host,
44 Device,
46}
47
48#[derive(Debug)]
56pub struct CusparseCompatHandle {
57 pointer_mode: CusparsePointerMode,
59 op_count: Cell<u64>,
61}
62
63impl CusparseCompatHandle {
64 pub fn create() -> SparseResult<Self> {
72 Ok(Self {
73 pointer_mode: CusparsePointerMode::default(),
74 op_count: Cell::new(0),
75 })
76 }
77
78 pub fn destroy(self) -> SparseResult<()> {
87 Ok(())
88 }
89
90 #[inline]
92 pub fn pointer_mode(&self) -> CusparsePointerMode {
93 self.pointer_mode
94 }
95
96 #[inline]
98 pub fn set_pointer_mode(&mut self, mode: CusparsePointerMode) {
99 self.pointer_mode = mode;
100 }
101
102 #[inline]
104 pub fn op_count(&self) -> u64 {
105 self.op_count.get()
106 }
107
108 #[inline]
110 fn record_op(&self) {
111 self.op_count.set(self.op_count.get() + 1);
112 }
113}
114
115pub fn cusparse_spmv(
126 handle: &CusparseCompatHandle,
127 alpha: f64,
128 a: &HostCsr,
129 x: &[f64],
130 beta: f64,
131 y: &mut [f64],
132) -> SparseResult<()> {
133 handle.record_op();
134 if x.len() != a.ncols {
135 return Err(SparseError::DimensionMismatch(format!(
136 "SpMV: x length ({}) must equal A.ncols ({})",
137 x.len(),
138 a.ncols
139 )));
140 }
141 if y.len() != a.nrows {
142 return Err(SparseError::DimensionMismatch(format!(
143 "SpMV: y length ({}) must equal A.nrows ({})",
144 y.len(),
145 a.nrows
146 )));
147 }
148
149 let ax = a.matvec(x);
153 for (yi, &ax_i) in y.iter_mut().zip(ax.iter()) {
154 let scaled = if beta == 0.0 { 0.0 } else { beta * *yi };
155 *yi = alpha * ax_i + scaled;
156 }
157 Ok(())
158}
159
160pub fn cusparse_spgemm(
169 handle: &CusparseCompatHandle,
170 a: &HostCsr,
171 b: &HostCsr,
172) -> SparseResult<HostCsr> {
173 handle.record_op();
174 a.matmul(b)
175}
176
177pub fn cusparse_spsv(
197 handle: &CusparseCompatHandle,
198 fill_mode: FillMode,
199 diag_type: DiagType,
200 alpha: f64,
201 a: &HostCsr,
202 b: &[f64],
203) -> SparseResult<Vec<f64>> {
204 handle.record_op();
205 if a.nrows != a.ncols {
206 return Err(SparseError::DimensionMismatch(format!(
207 "SpSV requires a square matrix, got {}x{}",
208 a.nrows, a.ncols
209 )));
210 }
211 if b.len() != a.nrows {
212 return Err(SparseError::DimensionMismatch(format!(
213 "SpSV: b length ({}) must equal A.nrows ({})",
214 b.len(),
215 a.nrows
216 )));
217 }
218 let n = a.nrows;
219 let mut x = vec![0.0f64; n];
220
221 match fill_mode {
222 FillMode::Lower => {
223 for i in 0..n {
226 let (acc, diag) = substitution_row(a, i, alpha * b[i], &x, c_below(i));
227 x[i] = finish_row(acc, diag, diag_type)?;
228 }
229 }
230 FillMode::Upper => {
231 for i in (0..n).rev() {
234 let (acc, diag) = substitution_row(a, i, alpha * b[i], &x, c_above(i));
235 x[i] = finish_row(acc, diag, diag_type)?;
236 }
237 }
238 FillMode::Full => {
239 return Err(SparseError::InvalidArgument(
240 "SpSV requires a triangular fill mode (Lower or Upper), got Full".to_string(),
241 ));
242 }
243 }
244
245 Ok(x)
246}
247
248#[inline]
250fn c_below(i: usize) -> impl Fn(usize) -> bool {
251 move |c| c < i
252}
253
254#[inline]
256fn c_above(i: usize) -> impl Fn(usize) -> bool {
257 move |c| c > i
258}
259
260#[inline]
264fn substitution_row(
265 a: &HostCsr,
266 i: usize,
267 rhs: f64,
268 x: &[f64],
269 is_solved: impl Fn(usize) -> bool,
270) -> (f64, Option<f64>) {
271 let mut acc = rhs;
272 let mut diag: Option<f64> = None;
273 let start = a.row_ptr[i];
274 let end = a.row_ptr[i + 1];
275 for k in start..end {
276 let c = a.col_indices[k];
277 if c == i {
278 diag = Some(a.values[k]);
279 } else if is_solved(c) {
280 acc -= a.values[k] * x[c];
281 }
282 }
283 (acc, diag)
284}
285
286#[inline]
289fn finish_row(acc: f64, diag: Option<f64>, diag_type: DiagType) -> SparseResult<f64> {
290 match diag_type {
291 DiagType::Unit => Ok(acc),
292 DiagType::NonUnit => match diag {
293 Some(d) if d.abs() > 1e-300 => Ok(acc / d),
294 _ => Err(SparseError::SingularMatrix),
295 },
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302
303 fn dense_matvec(dense: &[f64], nrows: usize, ncols: usize, x: &[f64]) -> Vec<f64> {
305 (0..nrows)
306 .map(|i| {
307 dense[i * ncols..(i + 1) * ncols]
308 .iter()
309 .zip(x.iter())
310 .map(|(&a, &xj)| a * xj)
311 .sum()
312 })
313 .collect()
314 }
315
316 fn sample_matrix() -> HostCsr {
317 HostCsr::new(
321 3,
322 3,
323 vec![0, 2, 3, 5],
324 vec![0, 2, 1, 0, 2],
325 vec![1.0, 2.0, 3.0, 4.0, 5.0],
326 )
327 .expect("sample")
328 }
329
330 #[test]
331 fn spmv_alpha1_beta0_matches_dense() {
332 let h = CusparseCompatHandle::create().expect("handle");
333 let a = sample_matrix();
334 let x = [1.0, 2.0, 3.0];
335 let mut y = vec![0.0; 3];
336 cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
337 let reference = dense_matvec(&a.to_dense(), 3, 3, &x);
338 for (got, want) in y.iter().zip(reference.iter()) {
339 assert!((got - want).abs() < 1e-12, "got {got} want {want}");
340 }
341 assert!(y.iter().all(|v| v.is_finite()));
342 }
343
344 #[test]
345 fn spmv_general_alpha_beta() {
346 let h = CusparseCompatHandle::create().expect("handle");
347 let a = sample_matrix();
348 let x = [2.0, -1.0, 0.5];
349 let y0 = [10.0, 20.0, 30.0];
350 let alpha = 1.5;
351 let beta = -2.0;
352 let mut y = y0.to_vec();
353 cusparse_spmv(&h, alpha, &a, &x, beta, &mut y).expect("spmv");
354 let ax = dense_matvec(&a.to_dense(), 3, 3, &x);
355 for i in 0..3 {
356 let want = alpha * ax[i] + beta * y0[i];
357 assert!((y[i] - want).abs() < 1e-12, "row {i}: {} vs {}", y[i], want);
358 }
359 }
360
361 #[test]
362 fn spmv_beta0_ignores_prior_nonfinite_y() {
363 let h = CusparseCompatHandle::create().expect("handle");
364 let a = sample_matrix();
365 let x = [1.0, 1.0, 1.0];
366 let mut y = vec![f64::NAN, f64::INFINITY, -f64::INFINITY];
367 cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
368 assert!(y.iter().all(|v| v.is_finite()), "beta=0 must overwrite y");
369 }
370
371 #[test]
372 fn spgemm_matches_dense() {
373 let h = CusparseCompatHandle::create().expect("handle");
374 let a = sample_matrix();
375 let b = HostCsr::new(
377 3,
378 2,
379 vec![0, 1, 2, 4],
380 vec![0, 1, 0, 1],
381 vec![1.0, 2.0, 3.0, 4.0],
382 )
383 .expect("b");
384 let c = cusparse_spgemm(&h, &a, &b).expect("spgemm");
385
386 let da = a.to_dense();
388 let db = b.to_dense();
389 let mut dc = [0.0f64; 3 * 2];
390 for i in 0..3 {
391 for j in 0..2 {
392 let mut acc = 0.0;
393 for k in 0..3 {
394 acc += da[i * 3 + k] * db[k * 2 + j];
395 }
396 dc[i * 2 + j] = acc;
397 }
398 }
399 for i in 0..3 {
400 for j in 0..2 {
401 let got = c.get(i, j).unwrap_or(0.0);
402 assert!((got - dc[i * 2 + j]).abs() < 1e-12, "C[{i},{j}]");
403 }
404 }
405 }
406
407 #[test]
408 fn spsv_lower_solves_system() {
409 let h = CusparseCompatHandle::create().expect("handle");
410 let l = HostCsr::new(
415 3,
416 3,
417 vec![0, 1, 3, 6],
418 vec![0, 0, 1, 0, 1, 2],
419 vec![2.0, 1.0, 3.0, 4.0, 5.0, 6.0],
420 )
421 .expect("l");
422 let x_true = [1.0, 2.0, 3.0];
423 let b = l.matvec(&x_true);
424 let x = cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &l, &b).expect("spsv");
425 for (got, want) in x.iter().zip(x_true.iter()) {
426 assert!((got - want).abs() < 1e-12, "got {got} want {want}");
427 }
428 let check = l.matvec(&x);
430 for (c, bi) in check.iter().zip(b.iter()) {
431 assert!((c - bi).abs() < 1e-12);
432 }
433 assert!(x.iter().all(|v| v.is_finite()));
434 }
435
436 #[test]
437 fn spsv_upper_solves_system() {
438 let h = CusparseCompatHandle::create().expect("handle");
439 let u = HostCsr::new(
444 3,
445 3,
446 vec![0, 3, 5, 6],
447 vec![0, 1, 2, 1, 2, 2],
448 vec![2.0, 1.0, 1.0, 3.0, 2.0, 4.0],
449 )
450 .expect("u");
451 let x_true = [-1.0, 2.0, 0.5];
452 let b = u.matvec(&x_true);
453 let x = cusparse_spsv(&h, FillMode::Upper, DiagType::NonUnit, 1.0, &u, &b).expect("spsv");
454 for (got, want) in x.iter().zip(x_true.iter()) {
455 assert!((got - want).abs() < 1e-12, "got {got} want {want}");
456 }
457 }
458
459 #[test]
460 fn spsv_unit_diagonal() {
461 let h = CusparseCompatHandle::create().expect("handle");
462 let l =
466 HostCsr::new(3, 3, vec![0, 0, 1, 3], vec![0, 0, 1], vec![2.0, 3.0, 4.0]).expect("l");
467 let x_true = [5.0, -1.0, 2.0];
468 let b = [
470 x_true[0],
471 2.0 * x_true[0] + x_true[1],
472 3.0 * x_true[0] + 4.0 * x_true[1] + x_true[2],
473 ];
474 let x = cusparse_spsv(&h, FillMode::Lower, DiagType::Unit, 1.0, &l, &b).expect("spsv");
475 for (got, want) in x.iter().zip(x_true.iter()) {
476 assert!((got - want).abs() < 1e-12, "got {got} want {want}");
477 }
478 }
479
480 #[test]
481 fn spsv_alpha_scales_rhs() {
482 let h = CusparseCompatHandle::create().expect("handle");
483 let l = HostCsr::new(2, 2, vec![0, 1, 3], vec![0, 0, 1], vec![2.0, 1.0, 4.0]).expect("l");
484 let b = [3.0, 7.0];
485 let alpha = 2.0;
486 let x = cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, alpha, &l, &b).expect("spsv");
487 let ax = l.matvec(&x);
489 for (axi, bi) in ax.iter().zip(b.iter()) {
490 assert!((axi - alpha * bi).abs() < 1e-12);
491 }
492 }
493
494 #[test]
495 fn handle_lifecycle_and_op_count() {
496 let mut h = CusparseCompatHandle::create().expect("create");
497 assert_eq!(h.pointer_mode(), CusparsePointerMode::Host);
498 assert_eq!(h.op_count(), 0);
499
500 let a = sample_matrix();
501 let x = [1.0, 1.0, 1.0];
502 let mut y = vec![0.0; 3];
503 cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut y).expect("spmv");
504 assert_eq!(h.op_count(), 1);
505
506 h.set_pointer_mode(CusparsePointerMode::Device);
507 assert_eq!(h.pointer_mode(), CusparsePointerMode::Device);
508
509 let _ = cusparse_spgemm(&h, &a, &a).expect("spgemm");
510 assert_eq!(h.op_count(), 2);
511
512 h.destroy().expect("destroy");
513 }
514
515 #[test]
516 fn spmv_dimension_mismatch_errors() {
517 let h = CusparseCompatHandle::create().expect("handle");
518 let a = sample_matrix();
519 let bad_x = [1.0, 2.0]; let mut y = vec![0.0; 3];
521 assert!(cusparse_spmv(&h, 1.0, &a, &bad_x, 0.0, &mut y).is_err());
522 let x = [1.0, 2.0, 3.0];
523 let mut bad_y = vec![0.0; 2]; assert!(cusparse_spmv(&h, 1.0, &a, &x, 0.0, &mut bad_y).is_err());
525 }
526
527 #[test]
528 fn spgemm_dimension_mismatch_errors() {
529 let h = CusparseCompatHandle::create().expect("handle");
530 let a = sample_matrix(); let b = HostCsr::new(2, 2, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("b"); assert!(cusparse_spgemm(&h, &a, &b).is_err());
533 }
534
535 #[test]
536 fn spsv_rejects_non_square_and_full() {
537 let h = CusparseCompatHandle::create().expect("handle");
538 let rect = HostCsr::new(2, 3, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("rect");
539 let b = [1.0, 2.0];
540 assert!(cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &rect, &b).is_err());
541
542 let sq = sample_matrix();
543 let b3 = [1.0, 2.0, 3.0];
544 assert!(
545 cusparse_spsv(&h, FillMode::Full, DiagType::NonUnit, 1.0, &sq, &b3).is_err(),
546 "Full fill mode must be rejected"
547 );
548 }
549
550 #[test]
551 fn spsv_singular_diagonal_errors() {
552 let h = CusparseCompatHandle::create().expect("handle");
553 let l = HostCsr::new(2, 2, vec![0, 1, 2], vec![0, 0], vec![2.0, 5.0]).expect("l");
556 let b = [2.0, 5.0];
557 assert!(cusparse_spsv(&h, FillMode::Lower, DiagType::NonUnit, 1.0, &l, &b).is_err());
558 }
559}