Skip to main content

oxicuda_sparse/ops/
batched.rs

1//! Batched sparse operations.
2//!
3//! This module provides routines for executing many small sparse operations
4//! in a single logical batch, amortising kernel launch overhead and enabling
5//! fusion when beneficial.
6//!
7//! ## Provided types
8//!
9//! - [`BatchedSpMV`] -- batched sparse matrix-vector multiply (host-side loop).
10//! - [`BatchedSpMVPlan`] -- precomputed plan with concatenated CSR arrays.
11//! - [`UniformBatchedSpMV`] -- optimised path when all matrices share structure.
12//! - [`BatchedSpGEMM`] -- batched sparse-sparse matrix multiply.
13//! - [`BatchedTriSolve`] -- batched sparse triangular solve.
14//! - [`BatchScheduler`] -- heuristic-based execution strategy selector.
15
16use std::ops::{Add, AddAssign, Mul};
17
18use oxicuda_blas::GpuFloat;
19
20use crate::error::{SparseError, SparseResult};
21use crate::format::CsrMatrix;
22
23/// Result type for a single SpGEMM: (row_ptr, col_idx, values, rows, cols).
24type SpGEMMResultU32<T> = (Vec<i32>, Vec<i32>, Vec<T>, u32, u32);
25
26/// Result type for a single SpGEMM from host arrays: (row_ptr, col_idx, values, rows, cols).
27type SpGEMMResultUsize<T> = (Vec<i32>, Vec<i32>, Vec<T>, usize, usize);
28
29// ---------------------------------------------------------------------------
30// BatchScheduler -- strategy selection
31// ---------------------------------------------------------------------------
32
33/// Execution strategy chosen by [`BatchScheduler`].
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub enum Strategy {
36    /// Process each operation sequentially on a single stream.
37    Sequential,
38    /// Issue operations on `num_streams` concurrent CUDA streams.
39    Concurrent(usize),
40    /// Fuse all operations into a single kernel launch.
41    Fused,
42}
43
44/// Heuristic-based scheduler that decides how to execute a batch.
45///
46/// The strategy depends on batch size and average matrix complexity:
47///
48/// | Condition | Strategy |
49/// |-----------|----------|
50/// | batch <= 4 *and* avg\_nnz >= 10 000 | [`Sequential`](Strategy::Sequential) |
51/// | batch >= 64 *and* avg\_nnz < 256 | [`Fused`](Strategy::Fused) |
52/// | otherwise | [`Concurrent`](Strategy::Concurrent) with up to 8 streams |
53#[derive(Debug, Clone)]
54pub struct BatchScheduler {
55    _private: (),
56}
57
58impl BatchScheduler {
59    /// Create a new scheduler.
60    #[inline]
61    pub fn new() -> Self {
62        Self { _private: () }
63    }
64
65    /// Select an execution strategy for the given batch characteristics.
66    ///
67    /// # Arguments
68    ///
69    /// * `batch_size` -- Number of independent operations in the batch.
70    /// * `avg_nnz` -- Average number of non-zeros per matrix in the batch.
71    pub fn select_strategy(&self, batch_size: usize, avg_nnz: usize) -> Strategy {
72        Self::select_strategy_static(batch_size, avg_nnz)
73    }
74
75    /// Static version of [`select_strategy`](Self::select_strategy) for use
76    /// without constructing a scheduler instance.
77    pub fn select_strategy_static(batch_size: usize, avg_nnz: usize) -> Strategy {
78        // Small batches of large matrices -> sequential is fine
79        if batch_size <= 4 && avg_nnz >= 10_000 {
80            return Strategy::Sequential;
81        }
82        // Many tiny matrices -> fuse into single kernel
83        if batch_size >= 64 && avg_nnz < 256 {
84            return Strategy::Fused;
85        }
86        // Default: concurrent streams (capped at 8)
87        let streams = batch_size.clamp(1, 8);
88        Strategy::Concurrent(streams)
89    }
90}
91
92impl Default for BatchScheduler {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98// ---------------------------------------------------------------------------
99// BatchedSpMVPlan -- precomputed concatenated arrays
100// ---------------------------------------------------------------------------
101
102/// Precomputed plan for batched SpMV execution.
103///
104/// Concatenates all CSR arrays (row\_ptr, col\_indices, values) from multiple
105/// matrices into contiguous host-side arrays with offset tables, enabling
106/// efficient data transfer and potential kernel fusion.
107///
108/// # Type parameters
109///
110/// * `T` -- Element type (must satisfy [`GpuFloat`]).
111#[derive(Debug, Clone)]
112pub struct BatchedSpMVPlan<T> {
113    /// Concatenated row pointer array across all matrices.
114    pub concat_row_ptr: Vec<i32>,
115    /// Concatenated column index array across all matrices.
116    pub concat_col_idx: Vec<i32>,
117    /// Concatenated values array across all matrices.
118    pub concat_values: Vec<T>,
119    /// Start offset of each matrix's row\_ptr in `concat_row_ptr`.
120    pub batch_offsets_row_ptr: Vec<usize>,
121    /// Start offset of each matrix's col\_idx / values in the concatenated arrays.
122    pub batch_offsets_nnz: Vec<usize>,
123    /// Number of rows in each matrix.
124    pub row_counts: Vec<usize>,
125    /// Number of columns in each matrix.
126    pub col_counts: Vec<usize>,
127    /// Number of non-zeros in each matrix.
128    pub nnz_counts: Vec<usize>,
129    /// Total number of matrices in the batch.
130    pub batch_size: usize,
131    /// Recommended execution strategy.
132    pub strategy: Strategy,
133}
134
135impl<T: GpuFloat> BatchedSpMVPlan<T> {
136    /// Build a plan from a slice of CSR matrices.
137    ///
138    /// Downloads each matrix to host memory, concatenates the arrays, and
139    /// records offset metadata.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`SparseError::InvalidArgument`] if `matrices` is empty.
144    /// Returns [`SparseError::Cuda`] on download failure.
145    pub fn from_matrices(matrices: &[CsrMatrix<T>]) -> SparseResult<Self> {
146        if matrices.is_empty() {
147            return Err(SparseError::InvalidArgument(
148                "batch must contain at least one matrix".to_string(),
149            ));
150        }
151
152        let batch_size = matrices.len();
153        let mut concat_row_ptr = Vec::new();
154        let mut concat_col_idx = Vec::new();
155        let mut concat_values: Vec<T> = Vec::new();
156        let mut batch_offsets_row_ptr = Vec::with_capacity(batch_size);
157        let mut batch_offsets_nnz = Vec::with_capacity(batch_size);
158        let mut row_counts = Vec::with_capacity(batch_size);
159        let mut col_counts = Vec::with_capacity(batch_size);
160        let mut nnz_counts = Vec::with_capacity(batch_size);
161
162        for mat in matrices {
163            let (h_rp, h_ci, h_vals) = mat.to_host()?;
164
165            batch_offsets_row_ptr.push(concat_row_ptr.len());
166            batch_offsets_nnz.push(concat_col_idx.len());
167            row_counts.push(mat.rows() as usize);
168            col_counts.push(mat.cols() as usize);
169            nnz_counts.push(mat.nnz() as usize);
170
171            concat_row_ptr.extend_from_slice(&h_rp);
172            concat_col_idx.extend_from_slice(&h_ci);
173            concat_values.extend_from_slice(&h_vals);
174        }
175
176        let total_nnz = nnz_counts.iter().copied().sum::<usize>();
177        let avg_nnz = total_nnz.checked_div(batch_size).unwrap_or(0);
178        let strategy = BatchScheduler::select_strategy_static(batch_size, avg_nnz);
179
180        Ok(Self {
181            concat_row_ptr,
182            concat_col_idx,
183            concat_values,
184            batch_offsets_row_ptr,
185            batch_offsets_nnz,
186            row_counts,
187            col_counts,
188            nnz_counts,
189            batch_size,
190            strategy,
191        })
192    }
193
194    /// Build a plan from host-side arrays directly (no GPU download required).
195    ///
196    /// Each entry in the parallel arrays describes one matrix in the batch.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`SparseError::InvalidArgument`] if any of the slices are empty
201    /// or if their lengths do not match.
202    pub fn from_host_arrays(
203        row_ptrs: &[Vec<i32>],
204        col_indices: &[Vec<i32>],
205        values: &[Vec<T>],
206        rows: &[usize],
207        cols: &[usize],
208    ) -> SparseResult<Self> {
209        let batch_size = row_ptrs.len();
210        if batch_size == 0 {
211            return Err(SparseError::InvalidArgument(
212                "batch must contain at least one matrix".to_string(),
213            ));
214        }
215        if col_indices.len() != batch_size
216            || values.len() != batch_size
217            || rows.len() != batch_size
218            || cols.len() != batch_size
219        {
220            return Err(SparseError::InvalidArgument(
221                "all input slices must have the same length".to_string(),
222            ));
223        }
224
225        let mut concat_row_ptr = Vec::new();
226        let mut concat_col_idx = Vec::new();
227        let mut concat_values: Vec<T> = Vec::new();
228        let mut batch_offsets_row_ptr = Vec::with_capacity(batch_size);
229        let mut batch_offsets_nnz = Vec::with_capacity(batch_size);
230        let mut row_counts = Vec::with_capacity(batch_size);
231        let mut col_counts = Vec::with_capacity(batch_size);
232        let mut nnz_counts = Vec::with_capacity(batch_size);
233
234        for i in 0..batch_size {
235            batch_offsets_row_ptr.push(concat_row_ptr.len());
236            batch_offsets_nnz.push(concat_col_idx.len());
237            row_counts.push(rows[i]);
238            col_counts.push(cols[i]);
239            nnz_counts.push(values[i].len());
240
241            concat_row_ptr.extend_from_slice(&row_ptrs[i]);
242            concat_col_idx.extend_from_slice(&col_indices[i]);
243            concat_values.extend_from_slice(&values[i]);
244        }
245
246        let total_nnz = nnz_counts.iter().copied().sum::<usize>();
247        let avg_nnz = total_nnz.checked_div(batch_size).unwrap_or(0);
248        let strategy = BatchScheduler::select_strategy_static(batch_size, avg_nnz);
249
250        Ok(Self {
251            concat_row_ptr,
252            concat_col_idx,
253            concat_values,
254            batch_offsets_row_ptr,
255            batch_offsets_nnz,
256            row_counts,
257            col_counts,
258            nnz_counts,
259            batch_size,
260            strategy,
261        })
262    }
263
264    /// Returns the total number of non-zeros across the entire batch.
265    #[inline]
266    pub fn total_nnz(&self) -> usize {
267        self.nnz_counts.iter().copied().sum()
268    }
269
270    /// Returns the total number of rows across the entire batch.
271    #[inline]
272    pub fn total_rows(&self) -> usize {
273        self.row_counts.iter().copied().sum()
274    }
275
276    /// Returns the average non-zeros per matrix.
277    #[inline]
278    pub fn avg_nnz(&self) -> usize {
279        if self.batch_size == 0 {
280            return 0;
281        }
282        self.total_nnz() / self.batch_size
283    }
284}
285
286// ---------------------------------------------------------------------------
287// BatchedSpMV -- batched sparse matrix-vector multiply
288// ---------------------------------------------------------------------------
289
290/// Batched sparse matrix-vector multiplication.
291///
292/// Computes `y_i = alpha * A_i * x_i + beta * y_i` for a batch of sparse
293/// matrices `A_i`, dense vectors `x_i`, and output vectors `y_i`.
294///
295/// The baseline implementation iterates sequentially on the host. For GPU
296/// execution, use [`generate_batched_spmv_ptx`] to create a single fused
297/// kernel that processes all matrices via offset arrays.
298#[derive(Debug)]
299pub struct BatchedSpMV<T: GpuFloat> {
300    /// The batch of sparse matrices (stored as host-side CSR data).
301    matrices: Vec<HostCsr<T>>,
302}
303
304/// Host-side CSR representation for batched operations.
305#[derive(Debug, Clone)]
306struct HostCsr<T> {
307    rows: usize,
308    cols: usize,
309    row_ptr: Vec<i32>,
310    col_idx: Vec<i32>,
311    values: Vec<T>,
312}
313
314impl<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign> BatchedSpMV<T> {
315    /// Create a `BatchedSpMV` by downloading matrix data from device memory.
316    ///
317    /// # Errors
318    ///
319    /// Returns [`SparseError::InvalidArgument`] if `matrices` is empty.
320    /// Returns [`SparseError::Cuda`] on download failure.
321    pub fn from_device(matrices: &[CsrMatrix<T>]) -> SparseResult<Self> {
322        if matrices.is_empty() {
323            return Err(SparseError::InvalidArgument(
324                "batch must contain at least one matrix".to_string(),
325            ));
326        }
327
328        let mut host_mats = Vec::with_capacity(matrices.len());
329        for mat in matrices {
330            let (rp, ci, vals) = mat.to_host()?;
331            host_mats.push(HostCsr {
332                rows: mat.rows() as usize,
333                cols: mat.cols() as usize,
334                row_ptr: rp,
335                col_idx: ci,
336                values: vals,
337            });
338        }
339
340        Ok(Self {
341            matrices: host_mats,
342        })
343    }
344
345    /// Create a `BatchedSpMV` from host-side CSR arrays.
346    ///
347    /// # Errors
348    ///
349    /// Returns [`SparseError::InvalidArgument`] on dimension mismatch.
350    pub fn from_host(
351        row_ptrs: Vec<Vec<i32>>,
352        col_indices: Vec<Vec<i32>>,
353        values: Vec<Vec<T>>,
354        rows: Vec<usize>,
355        cols: Vec<usize>,
356    ) -> SparseResult<Self> {
357        let n = row_ptrs.len();
358        if n == 0 {
359            return Err(SparseError::InvalidArgument(
360                "batch must contain at least one matrix".to_string(),
361            ));
362        }
363        if col_indices.len() != n || values.len() != n || rows.len() != n || cols.len() != n {
364            return Err(SparseError::InvalidArgument(
365                "all input vectors must have the same length".to_string(),
366            ));
367        }
368
369        let mut host_mats = Vec::with_capacity(n);
370        for i in 0..n {
371            host_mats.push(HostCsr {
372                rows: rows[i],
373                cols: cols[i],
374                row_ptr: row_ptrs[i].clone(),
375                col_idx: col_indices[i].clone(),
376                values: values[i].clone(),
377            });
378        }
379
380        Ok(Self {
381            matrices: host_mats,
382        })
383    }
384
385    /// Returns the number of matrices in the batch.
386    #[inline]
387    pub fn batch_size(&self) -> usize {
388        self.matrices.len()
389    }
390
391    /// Execute the batched SpMV on the host (sequential baseline).
392    ///
393    /// For each matrix `A_i`:
394    ///   `y_i[r] = alpha * sum(A_i[r,c] * x_i[c]) + beta * y_i[r]`
395    ///
396    /// # Arguments
397    ///
398    /// * `xs` -- Slice of input vectors, one per matrix.
399    /// * `ys` -- Slice of output vectors, one per matrix (modified in place).
400    /// * `alpha` -- Scalar multiplier for `A * x`.
401    /// * `beta` -- Scalar multiplier for existing `y`.
402    ///
403    /// # Errors
404    ///
405    /// Returns [`SparseError::DimensionMismatch`] if slice lengths do not match
406    /// the batch size, or if individual vector dimensions are wrong.
407    pub fn execute(&self, xs: &[Vec<T>], ys: &mut [Vec<T>], alpha: T, beta: T) -> SparseResult<()> {
408        let n = self.matrices.len();
409        if xs.len() != n || ys.len() != n {
410            return Err(SparseError::DimensionMismatch(format!(
411                "expected {} vectors, got xs={}, ys={}",
412                n,
413                xs.len(),
414                ys.len()
415            )));
416        }
417
418        for (i, mat) in self.matrices.iter().enumerate() {
419            if xs[i].len() < mat.cols {
420                return Err(SparseError::DimensionMismatch(format!(
421                    "matrix {} has {} cols but x has {} elements",
422                    i,
423                    mat.cols,
424                    xs[i].len()
425                )));
426            }
427            if ys[i].len() < mat.rows {
428                return Err(SparseError::DimensionMismatch(format!(
429                    "matrix {} has {} rows but y has {} elements",
430                    i,
431                    mat.rows,
432                    ys[i].len()
433                )));
434            }
435
436            host_csr_spmv(mat, &xs[i], &mut ys[i], alpha, beta);
437        }
438
439        Ok(())
440    }
441}
442
443/// Perform a single CSR SpMV on the host: y = alpha*A*x + beta*y.
444fn host_csr_spmv<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
445    mat: &HostCsr<T>,
446    x: &[T],
447    y: &mut [T],
448    alpha: T,
449    beta: T,
450) {
451    for (row, y_row) in y.iter_mut().enumerate().take(mat.rows) {
452        let start = mat.row_ptr[row] as usize;
453        let end = mat.row_ptr[row + 1] as usize;
454
455        let mut acc = T::gpu_zero();
456        for j in start..end {
457            let col = mat.col_idx[j] as usize;
458            acc += mat.values[j] * x[col];
459        }
460
461        *y_row = alpha * acc + beta * *y_row;
462    }
463}
464
465// ---------------------------------------------------------------------------
466// UniformBatchedSpMV -- shared structure, only values differ
467// ---------------------------------------------------------------------------
468
469/// Optimised batched SpMV when all matrices share the same sparsity pattern.
470///
471/// Only the non-zero values differ between matrices; the row pointers and
472/// column indices are shared. This enables more efficient memory layout and
473/// potential vectorisation across the batch dimension.
474#[derive(Debug, Clone)]
475pub struct UniformBatchedSpMV<T> {
476    /// Number of rows in every matrix.
477    rows: usize,
478    /// Number of columns in every matrix.
479    cols: usize,
480    /// Shared row pointer array (length `rows + 1`).
481    row_ptr: Vec<i32>,
482    /// Shared column index array (length `nnz`).
483    col_idx: Vec<i32>,
484    /// Per-matrix values. `batch_values[i]` has length `nnz`.
485    batch_values: Vec<Vec<T>>,
486}
487
488impl<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign> UniformBatchedSpMV<T> {
489    /// Create a uniform batch from a pattern matrix and per-batch values.
490    ///
491    /// The pattern matrix supplies the shared structure (row\_ptr, col\_idx);
492    /// `batch_values[i]` supplies the non-zero values for the i-th matrix.
493    ///
494    /// # Errors
495    ///
496    /// Returns [`SparseError::InvalidArgument`] if `batch_values` is empty or
497    /// if any entry has the wrong length.
498    /// Returns [`SparseError::Cuda`] on download failure.
499    pub fn from_pattern(pattern: &CsrMatrix<T>, batch_values: Vec<Vec<T>>) -> SparseResult<Self> {
500        if batch_values.is_empty() {
501            return Err(SparseError::InvalidArgument(
502                "batch_values must not be empty".to_string(),
503            ));
504        }
505
506        let nnz = pattern.nnz() as usize;
507        for (i, vals) in batch_values.iter().enumerate() {
508            if vals.len() != nnz {
509                return Err(SparseError::InvalidArgument(format!(
510                    "batch_values[{}] has length {} but pattern nnz is {}",
511                    i,
512                    vals.len(),
513                    nnz
514                )));
515            }
516        }
517
518        let (rp, ci, _) = pattern.to_host()?;
519
520        Ok(Self {
521            rows: pattern.rows() as usize,
522            cols: pattern.cols() as usize,
523            row_ptr: rp,
524            col_idx: ci,
525            batch_values,
526        })
527    }
528
529    /// Create a uniform batch from host-side arrays (no GPU required).
530    ///
531    /// # Errors
532    ///
533    /// Returns [`SparseError::InvalidArgument`] on dimension / length mismatch.
534    pub fn from_host_arrays(
535        rows: usize,
536        cols: usize,
537        row_ptr: Vec<i32>,
538        col_idx: Vec<i32>,
539        batch_values: Vec<Vec<T>>,
540    ) -> SparseResult<Self> {
541        if batch_values.is_empty() {
542            return Err(SparseError::InvalidArgument(
543                "batch_values must not be empty".to_string(),
544            ));
545        }
546        if row_ptr.len() != rows + 1 {
547            return Err(SparseError::InvalidArgument(format!(
548                "row_ptr length {} != rows + 1 ({})",
549                row_ptr.len(),
550                rows + 1
551            )));
552        }
553        let nnz = col_idx.len();
554        for (i, vals) in batch_values.iter().enumerate() {
555            if vals.len() != nnz {
556                return Err(SparseError::InvalidArgument(format!(
557                    "batch_values[{}] length {} != nnz {}",
558                    i,
559                    vals.len(),
560                    nnz
561                )));
562            }
563        }
564
565        Ok(Self {
566            rows,
567            cols,
568            row_ptr,
569            col_idx,
570            batch_values,
571        })
572    }
573
574    /// Number of matrices in the batch.
575    #[inline]
576    pub fn batch_size(&self) -> usize {
577        self.batch_values.len()
578    }
579
580    /// Execute batched SpMV on the host.
581    ///
582    /// # Errors
583    ///
584    /// Returns [`SparseError::DimensionMismatch`] on vector size mismatch.
585    pub fn execute(&self, xs: &[Vec<T>], ys: &mut [Vec<T>], alpha: T, beta: T) -> SparseResult<()> {
586        let n = self.batch_values.len();
587        if xs.len() != n || ys.len() != n {
588            return Err(SparseError::DimensionMismatch(format!(
589                "expected {} vectors, got xs={}, ys={}",
590                n,
591                xs.len(),
592                ys.len()
593            )));
594        }
595
596        for i in 0..n {
597            if xs[i].len() < self.cols {
598                return Err(SparseError::DimensionMismatch(format!(
599                    "x[{}] length {} < cols {}",
600                    i,
601                    xs[i].len(),
602                    self.cols
603                )));
604            }
605            if ys[i].len() < self.rows {
606                return Err(SparseError::DimensionMismatch(format!(
607                    "y[{}] length {} < rows {}",
608                    i,
609                    ys[i].len(),
610                    self.rows
611                )));
612            }
613
614            let mat = HostCsr {
615                rows: self.rows,
616                cols: self.cols,
617                row_ptr: self.row_ptr.clone(),
618                col_idx: self.col_idx.clone(),
619                values: self.batch_values[i].clone(),
620            };
621            host_csr_spmv(&mat, &xs[i], &mut ys[i], alpha, beta);
622        }
623
624        Ok(())
625    }
626}
627
628// ---------------------------------------------------------------------------
629// BatchedSpGEMM -- batched sparse-sparse matrix multiply
630// ---------------------------------------------------------------------------
631
632/// Batched sparse-sparse matrix multiplication.
633///
634/// Computes `C_i = A_i * B_i` for a batch of sparse matrix pairs.
635/// The baseline runs each SpGEMM independently on the host using a
636/// hash-table accumulation approach.
637#[derive(Debug)]
638pub struct BatchedSpGEMM {
639    _private: (),
640}
641
642impl BatchedSpGEMM {
643    /// Create a new batched SpGEMM executor.
644    #[inline]
645    pub fn new() -> Self {
646        Self { _private: () }
647    }
648
649    /// Execute batched SpGEMM on the host.
650    ///
651    /// For each pair `(A_i, B_i)`, computes `C_i = A_i * B_i` using a
652    /// row-by-row hash-table accumulation.
653    ///
654    /// Returns host-side CSR triples `(row_ptr, col_idx, values)` for each
655    /// result matrix.
656    ///
657    /// # Errors
658    ///
659    /// Returns [`SparseError::InvalidArgument`] if batch sizes differ or are empty.
660    /// Returns [`SparseError::DimensionMismatch`] if dimensions are incompatible.
661    /// Returns [`SparseError::Cuda`] on download failure.
662    pub fn execute<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
663        a_batch: &[CsrMatrix<T>],
664        b_batch: &[CsrMatrix<T>],
665    ) -> SparseResult<Vec<SpGEMMResultU32<T>>> {
666        if a_batch.is_empty() {
667            return Err(SparseError::InvalidArgument(
668                "batch must not be empty".to_string(),
669            ));
670        }
671        if a_batch.len() != b_batch.len() {
672            return Err(SparseError::InvalidArgument(format!(
673                "a_batch length {} != b_batch length {}",
674                a_batch.len(),
675                b_batch.len()
676            )));
677        }
678
679        let mut results = Vec::with_capacity(a_batch.len());
680
681        for (i, (a, b)) in a_batch.iter().zip(b_batch.iter()).enumerate() {
682            if a.cols() != b.rows() {
683                return Err(SparseError::DimensionMismatch(format!(
684                    "pair {}: A.cols ({}) != B.rows ({})",
685                    i,
686                    a.cols(),
687                    b.rows()
688                )));
689            }
690
691            let (a_rp, a_ci, a_vals) = a.to_host()?;
692            let (b_rp, b_ci, b_vals) = b.to_host()?;
693
694            let (c_rp, c_ci, c_vals) = host_spgemm(
695                &a_rp,
696                &a_ci,
697                &a_vals,
698                a.rows() as usize,
699                &b_rp,
700                &b_ci,
701                &b_vals,
702                b.cols() as usize,
703            );
704
705            results.push((c_rp, c_ci, c_vals, a.rows(), b.cols()));
706        }
707
708        Ok(results)
709    }
710
711    /// Execute batched SpGEMM from host-side CSR arrays (no GPU required).
712    ///
713    /// # Errors
714    ///
715    /// Returns [`SparseError::InvalidArgument`] on length mismatch.
716    /// Returns [`SparseError::DimensionMismatch`] on dimension mismatch.
717    #[allow(clippy::too_many_arguments)]
718    pub fn execute_host<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
719        a_row_ptrs: &[Vec<i32>],
720        a_col_indices: &[Vec<i32>],
721        a_values: &[Vec<T>],
722        a_rows: &[usize],
723        a_cols: &[usize],
724        b_row_ptrs: &[Vec<i32>],
725        b_col_indices: &[Vec<i32>],
726        b_values: &[Vec<T>],
727        b_cols: &[usize],
728    ) -> SparseResult<Vec<SpGEMMResultUsize<T>>> {
729        let n = a_row_ptrs.len();
730        if n == 0 {
731            return Err(SparseError::InvalidArgument(
732                "batch must not be empty".to_string(),
733            ));
734        }
735        if b_row_ptrs.len() != n
736            || a_col_indices.len() != n
737            || a_values.len() != n
738            || a_rows.len() != n
739            || a_cols.len() != n
740            || b_col_indices.len() != n
741            || b_values.len() != n
742            || b_cols.len() != n
743        {
744            return Err(SparseError::InvalidArgument(
745                "all input slices must have the same length".to_string(),
746            ));
747        }
748
749        let mut results = Vec::with_capacity(n);
750        for i in 0..n {
751            let b_rows_i = a_cols[i]; // A.cols == B.rows
752            let (c_rp, c_ci, c_vals) = host_spgemm(
753                &a_row_ptrs[i],
754                &a_col_indices[i],
755                &a_values[i],
756                a_rows[i],
757                &b_row_ptrs[i],
758                &b_col_indices[i],
759                &b_values[i],
760                b_cols[i],
761            );
762            let _ = b_rows_i; // validated by caller
763            results.push((c_rp, c_ci, c_vals, a_rows[i], b_cols[i]));
764        }
765
766        Ok(results)
767    }
768}
769
770impl Default for BatchedSpGEMM {
771    fn default() -> Self {
772        Self::new()
773    }
774}
775
776/// Host-side SpGEMM for a single pair: C = A * B using hash accumulation.
777#[allow(clippy::too_many_arguments)]
778fn host_spgemm<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
779    a_rp: &[i32],
780    a_ci: &[i32],
781    a_vals: &[T],
782    a_rows: usize,
783    b_rp: &[i32],
784    b_ci: &[i32],
785    b_vals: &[T],
786    b_cols: usize,
787) -> (Vec<i32>, Vec<i32>, Vec<T>) {
788    use std::collections::BTreeMap;
789
790    let mut c_row_ptr = vec![0i32; a_rows + 1];
791    let mut c_col_idx = Vec::new();
792    let mut c_values: Vec<T> = Vec::new();
793
794    for row in 0..a_rows {
795        let a_start = a_rp[row] as usize;
796        let a_end = a_rp[row + 1] as usize;
797
798        // BTreeMap gives sorted column indices automatically
799        let mut acc: BTreeMap<usize, T> = BTreeMap::new();
800
801        for ja in a_start..a_end {
802            let a_col = a_ci[ja] as usize;
803            let a_val = a_vals[ja];
804
805            let b_start = b_rp[a_col] as usize;
806            let b_end = b_rp[a_col + 1] as usize;
807
808            for jb in b_start..b_end {
809                let b_col = b_ci[jb] as usize;
810                if b_col < b_cols {
811                    let product = a_val * b_vals[jb];
812                    acc.entry(b_col)
813                        .and_modify(|v| *v += product)
814                        .or_insert(product);
815                }
816            }
817        }
818
819        for (&col, &val) in &acc {
820            c_col_idx.push(col as i32);
821            c_values.push(val);
822        }
823        c_row_ptr[row + 1] = c_col_idx.len() as i32;
824    }
825
826    (c_row_ptr, c_col_idx, c_values)
827}
828
829// ---------------------------------------------------------------------------
830// BatchedTriSolve -- batched sparse triangular solve
831// ---------------------------------------------------------------------------
832
833/// Batched sparse triangular solve.
834///
835/// Solves `L_i * x_i = b_i` for each lower-triangular CSR matrix `L_i`
836/// and right-hand side `b_i` in the batch.
837#[derive(Debug)]
838pub struct BatchedTriSolve {
839    _private: (),
840}
841
842impl BatchedTriSolve {
843    /// Create a new batched triangular solve executor.
844    #[inline]
845    pub fn new() -> Self {
846        Self { _private: () }
847    }
848
849    /// Execute batched forward-substitution on the host.
850    ///
851    /// Each `L_i` must be square and lower-triangular with non-zero diagonal.
852    ///
853    /// # Errors
854    ///
855    /// Returns [`SparseError::InvalidArgument`] on batch size mismatch.
856    /// Returns [`SparseError::DimensionMismatch`] if matrices are not square.
857    /// Returns [`SparseError::SingularMatrix`] if a zero diagonal is encountered.
858    /// Returns [`SparseError::Cuda`] on download failure.
859    pub fn execute<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
860        l_batch: &[CsrMatrix<T>],
861        b_batch: &[Vec<T>],
862    ) -> SparseResult<Vec<Vec<T>>> {
863        if l_batch.is_empty() {
864            return Err(SparseError::InvalidArgument(
865                "batch must not be empty".to_string(),
866            ));
867        }
868        if l_batch.len() != b_batch.len() {
869            return Err(SparseError::InvalidArgument(format!(
870                "l_batch length {} != b_batch length {}",
871                l_batch.len(),
872                b_batch.len()
873            )));
874        }
875
876        let mut results = Vec::with_capacity(l_batch.len());
877
878        for (i, (l, b)) in l_batch.iter().zip(b_batch.iter()).enumerate() {
879            if l.rows() != l.cols() {
880                return Err(SparseError::DimensionMismatch(format!(
881                    "matrix {} is not square: {}x{}",
882                    i,
883                    l.rows(),
884                    l.cols()
885                )));
886            }
887            if b.len() < l.rows() as usize {
888                return Err(SparseError::DimensionMismatch(format!(
889                    "matrix {} has {} rows but rhs has {} elements",
890                    i,
891                    l.rows(),
892                    b.len()
893                )));
894            }
895
896            let (rp, ci, vals) = l.to_host()?;
897            let x = host_forward_solve(&rp, &ci, &vals, l.rows() as usize, b)?;
898            results.push(x);
899        }
900
901        Ok(results)
902    }
903
904    /// Execute batched forward-substitution from host-side arrays.
905    ///
906    /// # Errors
907    ///
908    /// Returns [`SparseError::InvalidArgument`] on length mismatch.
909    /// Returns [`SparseError::SingularMatrix`] on zero diagonal.
910    pub fn execute_host<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
911        row_ptrs: &[Vec<i32>],
912        col_indices: &[Vec<i32>],
913        values: &[Vec<T>],
914        sizes: &[usize],
915        rhs: &[Vec<T>],
916    ) -> SparseResult<Vec<Vec<T>>> {
917        let n = row_ptrs.len();
918        if n == 0 {
919            return Err(SparseError::InvalidArgument(
920                "batch must not be empty".to_string(),
921            ));
922        }
923        if col_indices.len() != n || values.len() != n || sizes.len() != n || rhs.len() != n {
924            return Err(SparseError::InvalidArgument(
925                "all input slices must have the same length".to_string(),
926            ));
927        }
928
929        let mut results = Vec::with_capacity(n);
930        for i in 0..n {
931            let x =
932                host_forward_solve(&row_ptrs[i], &col_indices[i], &values[i], sizes[i], &rhs[i])?;
933            results.push(x);
934        }
935
936        Ok(results)
937    }
938}
939
940impl Default for BatchedTriSolve {
941    fn default() -> Self {
942        Self::new()
943    }
944}
945
946/// Host-side lower-triangular forward substitution: L * x = b.
947fn host_forward_solve<T: GpuFloat + Add<Output = T> + Mul<Output = T> + AddAssign>(
948    row_ptr: &[i32],
949    col_idx: &[i32],
950    values: &[T],
951    n: usize,
952    b: &[T],
953) -> SparseResult<Vec<T>> {
954    let mut x = vec![T::gpu_zero(); n];
955
956    for row in 0..n {
957        let start = row_ptr[row] as usize;
958        let end = row_ptr[row + 1] as usize;
959
960        // Accumulate off-diagonal contributions and find diagonal entry.
961        // x[row] = (b[row] - sum(L[row,j]*x[j] for j<row)) / L[row,row]
962        let mut off_diag_sum = T::gpu_zero();
963        let mut diag = T::gpu_zero();
964
965        for j in start..end {
966            let col = col_idx[j] as usize;
967            if col < row {
968                off_diag_sum += values[j] * x[col];
969            } else if col == row {
970                diag = values[j];
971            }
972        }
973
974        // Check for zero diagonal
975        if diag == T::gpu_zero() {
976            return Err(SparseError::SingularMatrix);
977        }
978
979        // x[row] = (b[row] - off_diag_sum) / diag
980        // We cannot subtract directly without Sub trait. Use bit manipulation
981        // to negate for f32/f64.
982        let neg_sum = negate_float(off_diag_sum);
983        let numerator = b[row] + neg_sum;
984        x[row] = divide_float(numerator, diag);
985    }
986
987    Ok(x)
988}
989
990/// Negate a GpuFloat value by flipping the sign bit.
991#[inline]
992fn negate_float<T: GpuFloat>(val: T) -> T {
993    let bits = val.to_bits_u64();
994    if T::SIZE == 4 {
995        // f32: flip bit 31
996        T::from_bits_u64(bits ^ (1u64 << 31))
997    } else {
998        // f64: flip bit 63
999        T::from_bits_u64(bits ^ (1u64 << 63))
1000    }
1001}
1002
1003/// Divide two GpuFloat values by converting through f64.
1004#[inline]
1005fn divide_float<T: GpuFloat>(num: T, den: T) -> T {
1006    // Convert to f64, divide, convert back. Works for f32 and f64.
1007    let n_bits = num.to_bits_u64();
1008    let d_bits = den.to_bits_u64();
1009    if T::SIZE == 4 {
1010        let n = f32::from_bits(n_bits as u32) as f64;
1011        let d = f32::from_bits(d_bits as u32) as f64;
1012        let result = (n / d) as f32;
1013        T::from_bits_u64(u64::from(result.to_bits()))
1014    } else {
1015        let n = f64::from_bits(n_bits);
1016        let d = f64::from_bits(d_bits);
1017        let result = n / d;
1018        T::from_bits_u64(result.to_bits())
1019    }
1020}
1021
1022// ---------------------------------------------------------------------------
1023// PTX generation stub for batched SpMV
1024// ---------------------------------------------------------------------------
1025
1026/// Generate a batched SpMV PTX kernel that processes all matrices via offset
1027/// arrays in a single launch.
1028///
1029/// The generated kernel expects the following arguments:
1030/// - Concatenated row\_ptr, col\_idx, values arrays
1031/// - batch\_offsets\_row\_ptr, batch\_offsets\_nnz arrays
1032/// - row\_counts array
1033/// - x pointers array, y pointers array
1034/// - alpha, beta scalars
1035/// - batch\_size
1036///
1037/// Each thread block processes one matrix in the batch; within a block,
1038/// threads cooperate on rows using the scalar SpMV strategy.
1039///
1040/// # Note
1041///
1042/// This generates valid PTX source but actual GPU execution requires a
1043/// CUDA-capable device.
1044pub fn generate_batched_spmv_ptx<T: GpuFloat>() -> String {
1045    let type_name = T::NAME;
1046    let ptx_type = match T::SIZE {
1047        4 => ".f32",
1048        8 => ".f64",
1049        _ => ".f32",
1050    };
1051    let elem_size = match T::SIZE {
1052        8 => 8,
1053        _ => 4,
1054    };
1055    let zero_literal = match T::SIZE {
1056        8 => "0d0000000000000000",
1057        _ => "0f00000000",
1058    };
1059
1060    format!(
1061        r#"//
1062// Batched SpMV kernel for {type_name}
1063// Generated by oxicuda-sparse batched module
1064//
1065.version 7.0
1066.target sm_70
1067.address_size 64
1068
1069.visible .entry batched_spmv_{type_name}(
1070    .param .u64 concat_row_ptr,
1071    .param .u64 concat_col_idx,
1072    .param .u64 concat_values,
1073    .param .u64 batch_offsets_rp,
1074    .param .u64 batch_offsets_nnz,
1075    .param .u64 row_counts,
1076    .param .u64 x_ptrs,
1077    .param .u64 y_ptrs,
1078    .param {ptx_type} alpha,
1079    .param {ptx_type} beta,
1080    .param .u32 batch_size
1081)
1082{{
1083    .reg .u32 %r<16>;
1084    .reg .u64 %rd<32>;
1085    .reg {ptx_type} %f<8>;
1086    .reg .pred %p<4>;
1087
1088    // blockIdx.x = matrix index in batch
1089    mov.u32 %r0, %ctaid.x;
1090    // Early exit if blockIdx >= batch_size
1091    ld.param.u32 %r1, [batch_size];
1092    setp.ge.u32 %p0, %r0, %r1;
1093    @%p0 ret;
1094
1095    // threadIdx.x = local row within this matrix
1096    mov.u32 %r2, %tid.x;
1097
1098    // Load row_count for this matrix
1099    ld.param.u64 %rd0, [row_counts];
1100    cvt.u64.u32 %rd1, %r0;
1101    mad.wide.u32 %rd2, %r0, 4, %rd0;
1102    ld.global.u32 %r3, [%rd2];
1103
1104    // Early exit if tid >= row_count
1105    setp.ge.u32 %p1, %r2, %r3;
1106    @%p1 ret;
1107
1108    // Load row_ptr offset for this matrix
1109    ld.param.u64 %rd3, [batch_offsets_rp];
1110    mad.wide.u32 %rd4, %r0, 4, %rd3;
1111    ld.global.u32 %r4, [%rd4];
1112
1113    // row_start = concat_row_ptr[rp_offset + tid], row_end = next entry
1114    ld.param.u64 %rd5, [concat_row_ptr];
1115    add.u32 %r11, %r4, %r2;
1116    mul.wide.u32 %rd6, %r11, 4;
1117    add.u64 %rd5, %rd5, %rd6;
1118    ld.global.u32 %r5, [%rd5];
1119    add.u64 %rd6, %rd5, 4;
1120    ld.global.u32 %r6, [%rd6];
1121
1122    // Load nnz offset for this matrix
1123    ld.param.u64 %rd7, [batch_offsets_nnz];
1124    mad.wide.u32 %rd8, %r0, 4, %rd7;
1125    ld.global.u32 %r7, [%rd8];
1126
1127    // Load x and y pointers for this matrix
1128    ld.param.u64 %rd9, [x_ptrs];
1129    mad.wide.u32 %rd10, %r0, 8, %rd9;
1130    ld.global.u64 %rd10, [%rd10];
1131    ld.param.u64 %rd11, [y_ptrs];
1132    mad.wide.u32 %rd12, %r0, 8, %rd11;
1133    ld.global.u64 %rd11, [%rd12];
1134
1135    // Load concatenated col_idx / values bases and alpha / beta scalars
1136    ld.param.u64 %rd12, [concat_col_idx];
1137    ld.param.u64 %rd13, [concat_values];
1138    ld.param{ptx_type} %f6, [alpha];
1139    ld.param{ptx_type} %f7, [beta];
1140
1141    // acc = 0; iterate row_start .. row_end
1142    mov{ptx_type} %f0, {zero_literal};
1143    mov.u32 %r8, %r5;
1144
1145$ROW_LOOP:
1146    setp.lt.u32 %p2, %r8, %r6;
1147    @!%p2 bra $ROW_DONE;
1148
1149    // k = nnz_offset + absolute row nnz index
1150    add.u32 %r10, %r7, %r8;
1151
1152    // col = concat_col_idx[k]
1153    mul.wide.u32 %rd14, %r10, 4;
1154    add.u64 %rd15, %rd12, %rd14;
1155    ld.global.u32 %r9, [%rd15];
1156
1157    // val = concat_values[k]
1158    mul.wide.u32 %rd16, %r10, {elem_size};
1159    add.u64 %rd17, %rd13, %rd16;
1160    ld.global{ptx_type} %f1, [%rd17];
1161
1162    // x_val = x[col]
1163    mul.wide.u32 %rd18, %r9, {elem_size};
1164    add.u64 %rd19, %rd10, %rd18;
1165    ld.global{ptx_type} %f2, [%rd19];
1166
1167    // acc += val * x_val
1168    fma.rn{ptx_type} %f0, %f1, %f2, %f0;
1169
1170    add.u32 %r8, %r8, 1;
1171    bra $ROW_LOOP;
1172
1173$ROW_DONE:
1174    // y[tid] = alpha * acc + beta * y[tid]
1175    mul.wide.u32 %rd20, %r2, {elem_size};
1176    add.u64 %rd21, %rd11, %rd20;
1177    ld.global{ptx_type} %f3, [%rd21];
1178    mul.rn{ptx_type} %f4, %f6, %f0;
1179    mul.rn{ptx_type} %f5, %f7, %f3;
1180    add.rn{ptx_type} %f4, %f4, %f5;
1181    st.global{ptx_type} [%rd21], %f4;
1182
1183    ret;
1184}}
1185"#
1186    )
1187}
1188
1189// ---------------------------------------------------------------------------
1190// CPU reference: batched SpMV for multiple right-hand sides
1191// ---------------------------------------------------------------------------
1192
1193/// Batched SpMV (CPU reference): compute `y_i = A * x_i` for all `b` RHS vectors.
1194///
1195/// The shared sparse matrix `A` is given in CSR format. `x_batch` and `y_batch`
1196/// are stored in **column-major** order: element `(row_or_col, b)` is at index
1197/// `row_or_col * batch_size + b`.
1198///
1199/// # Arguments
1200///
1201/// * `n_rows` -- Number of rows in `A`.
1202/// * `n_cols` -- Number of columns in `A` (used for bounds; not strictly needed here).
1203/// * `row_ptr` -- CSR row pointer of length `n_rows + 1`.
1204/// * `col_idx` -- CSR column indices of length `nnz`.
1205/// * `values` -- CSR values of length `nnz`.
1206/// * `x_batch` -- Column-major matrix of shape `[n_cols × batch_size]`.
1207/// * `batch_size` -- Number of simultaneous right-hand sides.
1208///
1209/// # Returns
1210///
1211/// Output matrix `y` of shape `[n_rows × batch_size]` in column-major order.
1212pub fn batched_spmv_cpu(
1213    n_rows: usize,
1214    _n_cols: usize,
1215    row_ptr: &[u32],
1216    col_idx: &[u32],
1217    values: &[f32],
1218    x_batch: &[f32],
1219    batch_size: usize,
1220) -> Vec<f32> {
1221    let mut y = vec![0.0f32; n_rows * batch_size];
1222    for row in 0..n_rows {
1223        let start = row_ptr[row] as usize;
1224        let end = row_ptr[row + 1] as usize;
1225        for idx in start..end {
1226            let col = col_idx[idx] as usize;
1227            let val = values[idx];
1228            for b in 0..batch_size {
1229                y[row * batch_size + b] += val * x_batch[col * batch_size + b];
1230            }
1231        }
1232    }
1233    y
1234}
1235
1236// ---------------------------------------------------------------------------
1237// CPU reference: mixed-precision SpMV (FP16 storage, FP64 accumulation -> FP32)
1238// ---------------------------------------------------------------------------
1239
1240/// Mixed-precision SpMV (CPU reference): matrix values stored in FP16 range,
1241/// accumulation performed in FP64, result cast to FP32.
1242///
1243/// On actual GPU hardware, values would be loaded as FP16 (2 bytes each) and
1244/// accumulated in FP32 registers. This CPU reference simulates that by treating
1245/// `values_fp16` as FP32 scalars already quantised to FP16 precision, and
1246/// accumulating in FP64 to avoid catastrophic cancellation in the reference.
1247///
1248/// # Arguments
1249///
1250/// * `n_rows` -- Number of rows.
1251/// * `row_ptr` -- CSR row pointer of length `n_rows + 1`.
1252/// * `col_idx` -- CSR column indices of length `nnz`.
1253/// * `values_fp16` -- Per-entry matrix values (FP16-range, stored as f32).
1254/// * `x` -- Dense input vector of length `n_cols`.
1255///
1256/// # Returns
1257///
1258/// Output vector `y` of length `n_rows` as FP32.
1259pub fn mixed_precision_spmv_cpu(
1260    n_rows: usize,
1261    row_ptr: &[u32],
1262    col_idx: &[u32],
1263    values_fp16: &[f32],
1264    x: &[f32],
1265) -> Vec<f32> {
1266    let mut y = vec![0.0f32; n_rows];
1267    for row in 0..n_rows {
1268        let start = row_ptr[row] as usize;
1269        let end = row_ptr[row + 1] as usize;
1270        // Accumulate in f64 for numerical stability of the reference
1271        let mut acc = 0.0f64;
1272        for idx in start..end {
1273            let col = col_idx[idx] as usize;
1274            let val = values_fp16[idx] as f64;
1275            acc += val * (x[col] as f64);
1276        }
1277        y[row] = acc as f32;
1278    }
1279    y
1280}
1281
1282// ---------------------------------------------------------------------------
1283// Tests
1284// ---------------------------------------------------------------------------
1285
1286#[cfg(test)]
1287mod tests {
1288    use super::*;
1289
1290    // -- BatchScheduler tests -----------------------------------------------
1291
1292    #[test]
1293    fn scheduler_sequential_for_small_batch_large_matrices() {
1294        let s = BatchScheduler::new();
1295        assert_eq!(s.select_strategy(2, 50_000), Strategy::Sequential);
1296        assert_eq!(s.select_strategy(4, 10_000), Strategy::Sequential);
1297    }
1298
1299    #[test]
1300    fn scheduler_fused_for_large_batch_small_matrices() {
1301        let s = BatchScheduler::new();
1302        assert_eq!(s.select_strategy(100, 100), Strategy::Fused);
1303        assert_eq!(s.select_strategy(64, 255), Strategy::Fused);
1304    }
1305
1306    #[test]
1307    fn scheduler_concurrent_for_medium_cases() {
1308        let s = BatchScheduler::new();
1309        let strat = s.select_strategy(16, 1000);
1310        match strat {
1311            Strategy::Concurrent(n) => assert!((1..=8).contains(&n)),
1312            other => panic!("expected Concurrent, got {:?}", other),
1313        }
1314    }
1315
1316    #[test]
1317    fn scheduler_concurrent_caps_at_8_streams() {
1318        let strat = BatchScheduler::select_strategy_static(32, 1000);
1319        assert_eq!(strat, Strategy::Concurrent(8));
1320    }
1321
1322    #[test]
1323    fn scheduler_static_matches_instance() {
1324        let s = BatchScheduler::new();
1325        for (bs, nnz) in [(1, 100), (10, 500), (64, 100), (3, 20_000)] {
1326            assert_eq!(
1327                s.select_strategy(bs, nnz),
1328                BatchScheduler::select_strategy_static(bs, nnz)
1329            );
1330        }
1331    }
1332
1333    #[test]
1334    fn scheduler_default_trait() {
1335        let s = BatchScheduler::default();
1336        // Should not panic
1337        let _ = s.select_strategy(1, 1);
1338    }
1339
1340    // -- BatchedSpMVPlan tests (host arrays) --------------------------------
1341
1342    #[test]
1343    fn plan_from_host_arrays_basic() {
1344        // Two 2x2 identity matrices
1345        let rp = vec![vec![0, 1, 2], vec![0, 1, 2]];
1346        let ci = vec![vec![0, 1], vec![0, 1]];
1347        let vals: Vec<Vec<f32>> = vec![vec![1.0, 1.0], vec![2.0, 2.0]];
1348        let rows = vec![2, 2];
1349        let cols = vec![2, 2];
1350
1351        let plan = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &rows, &cols)
1352            .expect("plan creation should succeed");
1353
1354        assert_eq!(plan.batch_size, 2);
1355        assert_eq!(plan.row_counts, vec![2, 2]);
1356        assert_eq!(plan.nnz_counts, vec![2, 2]);
1357        assert_eq!(plan.total_nnz(), 4);
1358        assert_eq!(plan.total_rows(), 4);
1359        assert_eq!(plan.avg_nnz(), 2);
1360        assert_eq!(plan.batch_offsets_row_ptr, vec![0, 3]);
1361        assert_eq!(plan.batch_offsets_nnz, vec![0, 2]);
1362        assert_eq!(plan.concat_row_ptr, vec![0, 1, 2, 0, 1, 2]);
1363        assert_eq!(plan.concat_col_idx, vec![0, 1, 0, 1]);
1364    }
1365
1366    #[test]
1367    fn plan_from_host_arrays_empty_batch() {
1368        let result = BatchedSpMVPlan::<f32>::from_host_arrays(&[], &[], &[], &[], &[]);
1369        assert!(result.is_err());
1370    }
1371
1372    #[test]
1373    fn plan_from_host_arrays_length_mismatch() {
1374        let rp = vec![vec![0, 1]];
1375        let ci = vec![vec![0], vec![1]]; // wrong length
1376        let vals: Vec<Vec<f64>> = vec![vec![1.0]];
1377        let result = BatchedSpMVPlan::from_host_arrays(&rp, &ci, &vals, &[1], &[1]);
1378        assert!(result.is_err());
1379    }
1380
1381    // -- BatchedSpMV host execution tests -----------------------------------
1382
1383    #[test]
1384    fn spmv_host_identity_batch() {
1385        // Two 3x3 identity matrices
1386        let rp = vec![vec![0, 1, 2, 3], vec![0, 1, 2, 3]];
1387        let ci = vec![vec![0, 1, 2], vec![0, 1, 2]];
1388        let vals = vec![vec![1.0_f64, 1.0, 1.0], vec![1.0, 1.0, 1.0]];
1389        let rows = vec![3, 3];
1390        let cols = vec![3, 3];
1391
1392        let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols)
1393            .expect("batch creation should succeed");
1394        assert_eq!(batch.batch_size(), 2);
1395
1396        let xs = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
1397        let mut ys = vec![vec![0.0; 3], vec![0.0; 3]];
1398
1399        batch
1400            .execute(&xs, &mut ys, 1.0, 0.0)
1401            .expect("execute should succeed");
1402
1403        assert_eq!(ys[0], vec![1.0, 2.0, 3.0]);
1404        assert_eq!(ys[1], vec![4.0, 5.0, 6.0]);
1405    }
1406
1407    #[test]
1408    fn spmv_host_alpha_beta() {
1409        // Single 2x2 matrix: [[2, 0], [0, 3]]
1410        let rp = vec![vec![0, 1, 2]];
1411        let ci = vec![vec![0, 1]];
1412        let vals = vec![vec![2.0_f32, 3.0]];
1413        let rows = vec![2];
1414        let cols = vec![2];
1415
1416        let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1417
1418        let xs = vec![vec![1.0, 1.0]];
1419        let mut ys = vec![vec![10.0, 20.0]];
1420
1421        // y = 2.0 * A*x + 0.5 * y
1422        // A*x = [2, 3]
1423        // y = [2*2 + 0.5*10, 2*3 + 0.5*20] = [9, 16]
1424        batch
1425            .execute(&xs, &mut ys, 2.0, 0.5)
1426            .expect("execute should succeed");
1427
1428        assert!((ys[0][0] - 9.0).abs() < 1e-6);
1429        assert!((ys[0][1] - 16.0).abs() < 1e-6);
1430    }
1431
1432    #[test]
1433    fn spmv_host_dimension_mismatch() {
1434        let rp = vec![vec![0, 1]];
1435        let ci = vec![vec![0]];
1436        let vals = vec![vec![1.0_f32]];
1437        let rows = vec![1];
1438        let cols = vec![2];
1439
1440        let batch = BatchedSpMV::from_host(rp, ci, vals, rows, cols).expect("batch creation");
1441
1442        // Wrong number of vectors
1443        let xs = vec![vec![1.0; 2], vec![1.0; 2]];
1444        let mut ys = vec![vec![0.0], vec![0.0]];
1445        assert!(batch.execute(&xs, &mut ys, 1.0, 0.0).is_err());
1446    }
1447
1448    #[test]
1449    fn spmv_host_empty_batch_error() {
1450        let result = BatchedSpMV::<f64>::from_host(vec![], vec![], vec![], vec![], vec![]);
1451        assert!(result.is_err());
1452    }
1453
1454    // -- UniformBatchedSpMV tests -------------------------------------------
1455
1456    #[test]
1457    fn uniform_spmv_host_basic() {
1458        // Shared pattern: 2x2 diagonal
1459        let row_ptr = vec![0, 1, 2];
1460        let col_idx = vec![0, 1];
1461        let batch_values = vec![
1462            vec![1.0_f64, 1.0], // identity
1463            vec![2.0, 3.0],     // diag(2, 3)
1464        ];
1465
1466        let uniform = UniformBatchedSpMV::from_host_arrays(2, 2, row_ptr, col_idx, batch_values)
1467            .expect("creation should succeed");
1468        assert_eq!(uniform.batch_size(), 2);
1469
1470        let xs = vec![vec![1.0, 2.0], vec![1.0, 2.0]];
1471        let mut ys = vec![vec![0.0; 2], vec![0.0; 2]];
1472
1473        uniform
1474            .execute(&xs, &mut ys, 1.0, 0.0)
1475            .expect("execute should succeed");
1476
1477        assert_eq!(ys[0], vec![1.0, 2.0]); // I * [1,2]
1478        assert!((ys[1][0] - 2.0).abs() < 1e-10); // 2*1
1479        assert!((ys[1][1] - 6.0).abs() < 1e-10); // 3*2
1480    }
1481
1482    #[test]
1483    fn uniform_spmv_validation_errors() {
1484        // Empty batch_values
1485        let result =
1486            UniformBatchedSpMV::<f32>::from_host_arrays(2, 2, vec![0, 1, 2], vec![0, 1], vec![]);
1487        assert!(result.is_err());
1488
1489        // Wrong values length
1490        let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1491            2,
1492            2,
1493            vec![0, 1, 2],
1494            vec![0, 1],
1495            vec![vec![1.0]], // too short
1496        );
1497        assert!(result.is_err());
1498
1499        // Wrong row_ptr length
1500        let result = UniformBatchedSpMV::<f32>::from_host_arrays(
1501            2,
1502            2,
1503            vec![0, 1], // too short
1504            vec![0, 1],
1505            vec![vec![1.0, 2.0]],
1506        );
1507        assert!(result.is_err());
1508    }
1509
1510    // -- BatchedTriSolve tests (host) ---------------------------------------
1511
1512    #[test]
1513    fn tri_solve_host_basic() {
1514        // L = [[2, 0], [1, 3]], b = [4, 7]
1515        // x[0] = 4/2 = 2
1516        // x[1] = (7 - 1*2)/3 = 5/3
1517        let rp = vec![vec![0, 1, 3]];
1518        let ci = vec![vec![0, 0, 1]];
1519        let vals = vec![vec![2.0_f64, 1.0, 3.0]];
1520        let sizes = vec![2];
1521        let rhs = vec![vec![4.0, 7.0]];
1522
1523        let results = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs)
1524            .expect("solve should succeed");
1525
1526        assert_eq!(results.len(), 1);
1527        assert!((results[0][0] - 2.0).abs() < 1e-10);
1528        assert!((results[0][1] - 5.0 / 3.0).abs() < 1e-10);
1529    }
1530
1531    #[test]
1532    fn tri_solve_host_singular() {
1533        // L has zero on diagonal -> singular
1534        let rp = vec![vec![0, 1, 2]];
1535        let ci = vec![vec![0, 0]]; // row 1 has col 0 but no col 1 entry -> diag = 0
1536        let vals = vec![vec![1.0_f64, 2.0]];
1537        let sizes = vec![2];
1538        let rhs = vec![vec![1.0, 1.0]];
1539
1540        let result = BatchedTriSolve::execute_host(&rp, &ci, &vals, &sizes, &rhs);
1541        assert!(result.is_err());
1542    }
1543
1544    // -----------------------------------------------------------------------
1545    // Task 5c: SpMV / SpMM numerical accuracy tests (host-side, CPU-only)
1546    // -----------------------------------------------------------------------
1547
1548    // Helper: reference dense matrix-vector product y = A * x for a row-major
1549    // matrix A stored as a flat slice with `cols` columns.
1550    #[allow(dead_code)]
1551    fn dense_matvec(a: &[f64], rows: usize, cols: usize, x: &[f64]) -> Vec<f64> {
1552        let mut y = vec![0.0_f64; rows];
1553        for r in 0..rows {
1554            for c in 0..cols {
1555                y[r] += a[r * cols + c] * x[c];
1556            }
1557        }
1558        y
1559    }
1560
1561    // Helper: reference dense matrix-matrix product C = A * B, A is (m×k),
1562    // B is (k×n), result C is (m×n), all stored row-major.
1563    fn dense_matmul(a: &[f64], m: usize, k: usize, b: &[f64], n: usize) -> Vec<f64> {
1564        let mut c = vec![0.0_f64; m * n];
1565        for i in 0..m {
1566            for j in 0..n {
1567                let mut acc = 0.0_f64;
1568                for p in 0..k {
1569                    acc += a[i * k + p] * b[p * n + j];
1570                }
1571                c[i * n + j] = acc;
1572            }
1573        }
1574        c
1575    }
1576
1577    /// SpMV for a small 5×5 sparse matrix with known values.
1578    ///
1579    /// Matrix (dense):
1580    /// ```text
1581    /// [[1, 0, 2, 0, 0],
1582    ///  [0, 3, 0, 0, 0],
1583    ///  [0, 0, 4, 0, 5],
1584    ///  [0, 0, 0, 6, 0],
1585    ///  [7, 0, 0, 0, 8]]
1586    /// ```
1587    /// x = [1, 2, 3, 4, 5]
1588    /// Expected: [1+6, 6, 12+25, 24, 7+40] = [7, 6, 37, 24, 47]
1589    #[test]
1590    fn test_spmv_numerical_accuracy_small() {
1591        // CSR for the matrix above
1592        let row_ptr = vec![vec![0i32, 2, 3, 5, 6, 8]];
1593        let col_idx = vec![vec![0i32, 2, 1, 2, 4, 3, 0, 4]];
1594        let values = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0]];
1595        let rows = vec![5usize];
1596        let cols = vec![5usize];
1597
1598        let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols)
1599            .expect("batch creation should succeed");
1600
1601        let x = vec![vec![1.0_f64, 2.0, 3.0, 4.0, 5.0]];
1602        let mut y = vec![vec![0.0_f64; 5]];
1603
1604        batch
1605            .execute(&x, &mut y, 1.0, 0.0)
1606            .expect("execute should succeed");
1607
1608        let expected = [7.0_f64, 6.0, 37.0, 24.0, 47.0];
1609        for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1610            assert!(
1611                (got - exp).abs() < 1e-14,
1612                "y[{i}]: expected {exp}, got {got}"
1613            );
1614        }
1615    }
1616
1617    /// SpMV accuracy with large value spread (1e-6 to 1e6) — relative error < 1e-10.
1618    #[test]
1619    fn test_spmv_numerical_accuracy_value_spread() {
1620        // 4×4 diagonal with wildly varying values
1621        let big = 1e6_f64;
1622        let small = 1e-6_f64;
1623        // diag(1e-6, 1e6, 1e-6, 1e6)
1624        let row_ptr = vec![vec![0i32, 1, 2, 3, 4]];
1625        let col_idx = vec![vec![0i32, 1, 2, 3]];
1626        let values = vec![vec![small, big, small, big]];
1627        let rows = vec![4usize];
1628        let cols = vec![4usize];
1629
1630        let batch =
1631            BatchedSpMV::from_host(row_ptr, col_idx, values, rows, cols).expect("batch creation");
1632
1633        let x = vec![vec![1.0_f64, 1.0, 1.0, 1.0]];
1634        let mut y = vec![vec![0.0_f64; 4]];
1635        batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1636
1637        let expected = [small, big, small, big];
1638        for (i, (&got, &exp)) in y[0].iter().zip(expected.iter()).enumerate() {
1639            let rel_err = (got - exp).abs() / exp.abs().max(1e-300);
1640            assert!(
1641                rel_err < 1e-10,
1642                "y[{i}]: relative error {rel_err:.3e} exceeds threshold"
1643            );
1644        }
1645    }
1646
1647    /// SpMV with alpha/beta scaling: y = alpha * A * x + beta * y.
1648    #[test]
1649    fn test_spmv_alpha_beta_scaling() {
1650        // A = [[2, 0], [0, 3]], x = [1, 1], y_init = [10, 20]
1651        // A*x = [2, 3]
1652        // y = 5 * [2, 3] + 0.25 * [10, 20] = [10+2.5, 15+5] = [12.5, 20]
1653        let row_ptr = vec![vec![0i32, 1, 2]];
1654        let col_idx = vec![vec![0i32, 1]];
1655        let values = vec![vec![2.0_f64, 3.0]];
1656
1657        let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![2], vec![2])
1658            .expect("batch creation");
1659
1660        let x = vec![vec![1.0_f64, 1.0]];
1661        let mut y = vec![vec![10.0_f64, 20.0]];
1662        batch.execute(&x, &mut y, 5.0, 0.25).expect("execute");
1663
1664        assert!((y[0][0] - 12.5).abs() < 1e-13, "y[0] = {}", y[0][0]);
1665        assert!((y[0][1] - 20.0).abs() < 1e-13, "y[1] = {}", y[0][1]);
1666    }
1667
1668    /// SpMV for identity matrix: I * x = x.
1669    #[test]
1670    fn test_spmv_identity_matrix() {
1671        let n = 6usize;
1672        let row_ptr = vec![(0..=(n as i32)).collect::<Vec<i32>>()];
1673        let col_idx = vec![(0..n as i32).collect::<Vec<i32>>()];
1674        let values = vec![vec![1.0_f64; n]];
1675
1676        let batch = BatchedSpMV::from_host(row_ptr, col_idx, values, vec![n], vec![n])
1677            .expect("batch creation");
1678
1679        let x_data: Vec<f64> = (1..=(n as i64)).map(|v| v as f64).collect();
1680        let x = vec![x_data.clone()];
1681        let mut y = vec![vec![0.0_f64; n]];
1682        batch.execute(&x, &mut y, 1.0, 0.0).expect("execute");
1683
1684        for (i, (&got, &exp)) in y[0].iter().zip(x_data.iter()).enumerate() {
1685            assert!(
1686                (got - exp).abs() < 1e-14,
1687                "identity: y[{i}] = {got}, expected {exp}"
1688            );
1689        }
1690    }
1691
1692    /// SpMM: A * B = C where B is a dense matrix (multiple right-hand sides).
1693    ///
1694    /// Uses `BatchedSpMV::execute` once per column of B as a surrogate for SpMM.
1695    /// A is 4×4, B is 4×3, C = A * B verified against dense reference.
1696    #[test]
1697    fn test_spmm_numerical_accuracy() {
1698        // A (4×4 sparse):
1699        // [[1, 0, 2, 0],
1700        //  [0, 3, 0, 4],
1701        //  [5, 0, 6, 0],
1702        //  [0, 7, 0, 8]]
1703        let a_dense = [
1704            1.0_f64, 0.0, 2.0, 0.0, 0.0, 3.0, 0.0, 4.0, 5.0, 0.0, 6.0, 0.0, 0.0, 7.0, 0.0, 8.0,
1705        ];
1706
1707        let row_ptr = vec![0i32, 2, 4, 6, 8];
1708        let col_idx = vec![0i32, 2, 1, 3, 0, 2, 1, 3];
1709        let values = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1710
1711        // B (4×3):
1712        let b_dense = [
1713            1.0_f64, 0.0, 1.0, 0.0, 1.0, 2.0, 1.0, 0.0, 3.0, 0.0, 1.0, 4.0,
1714        ];
1715
1716        // C_ref = dense_matmul(A, B) (4×3)
1717        let c_ref = dense_matmul(&a_dense, 4, 4, &b_dense, 3);
1718
1719        // Compute each column of C using SpMV
1720        let n_cols = 3usize;
1721        let mut c_got = vec![0.0_f64; 4 * n_cols];
1722
1723        for col in 0..n_cols {
1724            // Extract column `col` of B as a vector
1725            let x_col: Vec<f64> = (0..4).map(|r| b_dense[r * n_cols + col]).collect();
1726
1727            let batch = BatchedSpMV::from_host(
1728                vec![row_ptr.clone()],
1729                vec![col_idx.clone()],
1730                vec![values.clone()],
1731                vec![4],
1732                vec![4],
1733            )
1734            .expect("batch creation");
1735
1736            let mut y = vec![vec![0.0_f64; 4]];
1737            batch.execute(&[x_col], &mut y, 1.0, 0.0).expect("execute");
1738
1739            for row in 0..4 {
1740                c_got[row * n_cols + col] = y[0][row];
1741            }
1742        }
1743
1744        for i in 0..4 * n_cols {
1745            assert!(
1746                (c_got[i] - c_ref[i]).abs() < 1e-13,
1747                "C[{i}]: got {}, expected {}",
1748                c_got[i],
1749                c_ref[i]
1750            );
1751        }
1752    }
1753
1754    // -- PTX generation test ------------------------------------------------
1755
1756    #[test]
1757    fn ptx_generation_f32() {
1758        let ptx = generate_batched_spmv_ptx::<f32>();
1759        assert!(ptx.contains("batched_spmv_f32"));
1760        assert!(ptx.contains(".f32"));
1761        assert!(ptx.contains(".version"));
1762    }
1763
1764    #[test]
1765    fn ptx_generation_f64() {
1766        let ptx = generate_batched_spmv_ptx::<f64>();
1767        assert!(ptx.contains("batched_spmv_f64"));
1768        assert!(ptx.contains(".f64"));
1769    }
1770
1771    #[test]
1772    fn batched_spmv_ptx_contains_loop() {
1773        let ptx = generate_batched_spmv_ptx::<f32>();
1774        assert!(ptx.contains("ROW_LOOP"));
1775        assert!(ptx.contains("fma.rn"));
1776        assert!(ptx.contains("ld.global"));
1777    }
1778
1779    // -- BatchedSpGEMM host test --------------------------------------------
1780
1781    #[test]
1782    fn spgemm_host_identity_times_matrix() {
1783        // I * A = A for 2x2
1784        let i_rp = vec![vec![0, 1, 2]];
1785        let i_ci = vec![vec![0, 1]];
1786        let i_vals: Vec<Vec<f64>> = vec![vec![1.0, 1.0]];
1787
1788        let a_rp = vec![vec![0, 2, 3]]; // row 0: cols 0,1; row 1: col 1
1789        let a_ci = vec![vec![0, 1, 1]];
1790        let a_vals: Vec<Vec<f64>> = vec![vec![2.0, 3.0, 4.0]];
1791
1792        let results = BatchedSpGEMM::execute_host(
1793            &i_rp,
1794            &i_ci,
1795            &i_vals,
1796            &[2],
1797            &[2],
1798            &a_rp,
1799            &a_ci,
1800            &a_vals,
1801            &[2],
1802        )
1803        .expect("spgemm should succeed");
1804
1805        assert_eq!(results.len(), 1);
1806        let (c_rp, c_ci, c_vals, m, n) = &results[0];
1807        assert_eq!(*m, 2);
1808        assert_eq!(*n, 2);
1809        // Row 0 of C should have entries at cols 0 and 1
1810        let r0_start = c_rp[0] as usize;
1811        let r0_end = c_rp[1] as usize;
1812        assert_eq!(r0_end - r0_start, 2);
1813        assert_eq!(c_ci[r0_start], 0);
1814        assert_eq!(c_ci[r0_start + 1], 1);
1815        assert!((c_vals[r0_start] - 2.0).abs() < 1e-10);
1816        assert!((c_vals[r0_start + 1] - 3.0).abs() < 1e-10);
1817    }
1818
1819    // -- batched_spmv_cpu tests ------------------------------------------------
1820
1821    #[test]
1822    fn batched_spmv_identity_2rhs() {
1823        // 4x4 identity matrix
1824        let n_rows = 4usize;
1825        let n_cols = 4usize;
1826        let row_ptr = vec![0u32, 1, 2, 3, 4];
1827        let col_idx = vec![0u32, 1, 2, 3];
1828        let values = vec![1.0f32; 4];
1829
1830        // X = [[1,5], [2,6], [3,7], [4,8]] column-major:
1831        // x_batch[col * batch_size + b]:
1832        //   col=0: b=0 -> 1, b=1 -> 5
1833        //   col=1: b=0 -> 2, b=1 -> 6
1834        //   col=2: b=0 -> 3, b=1 -> 7
1835        //   col=3: b=0 -> 4, b=1 -> 8
1836        let batch_size = 2usize;
1837        let x_batch = vec![1.0f32, 5.0, 2.0, 6.0, 3.0, 7.0, 4.0, 8.0];
1838
1839        let y = batched_spmv_cpu(
1840            n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1841        );
1842
1843        // y[row * batch_size + b] = x[row, b] since A is identity
1844        // row=0, b=0 -> 1; row=0, b=1 -> 5
1845        // row=1, b=0 -> 2; row=1, b=1 -> 6
1846        // row=2, b=0 -> 3; row=2, b=1 -> 7
1847        // row=3, b=0 -> 4; row=3, b=1 -> 8
1848        assert_eq!(y.len(), n_rows * batch_size);
1849        assert!((y[0] - 1.0).abs() < 1e-6, "row=0, b=0 should be 1.0");
1850        assert!((y[1] - 5.0).abs() < 1e-6, "row=0, b=1 should be 5.0");
1851        assert!((y[2] - 2.0).abs() < 1e-6, "row=1, b=0 should be 2.0");
1852        assert!((y[3] - 6.0).abs() < 1e-6, "row=1, b=1 should be 6.0");
1853        assert!((y[4] - 3.0).abs() < 1e-6, "row=2, b=0 should be 3.0");
1854        assert!((y[5] - 7.0).abs() < 1e-6, "row=2, b=1 should be 7.0");
1855        assert!((y[6] - 4.0).abs() < 1e-6, "row=3, b=0 should be 4.0");
1856        assert!((y[7] - 8.0).abs() < 1e-6, "row=3, b=1 should be 8.0");
1857    }
1858
1859    #[test]
1860    fn batched_spmv_correctness_3rhs() {
1861        // 3x3 matrix A:
1862        //  [2 1 0]
1863        //  [0 3 1]
1864        //  [1 0 4]
1865        // row_ptr = [0, 2, 4, 6]
1866        // col_idx = [0, 1, 1, 2, 0, 2]
1867        // values  = [2, 1, 3, 1, 1, 4]
1868        let n_rows = 3usize;
1869        let n_cols = 3usize;
1870        let row_ptr = vec![0u32, 2, 4, 6];
1871        let col_idx = vec![0u32, 1, 1, 2, 0, 2];
1872        let values = vec![2.0f32, 1.0, 3.0, 1.0, 1.0, 4.0];
1873        let batch_size = 3usize;
1874
1875        // 3 RHS column-major: x[:,0]=[1,0,0], x[:,1]=[0,1,0], x[:,2]=[0,0,1]
1876        // i.e., x_batch[col*3+b]: col=0: [1,0,0], col=1: [0,1,0], col=2: [0,0,1]
1877        let x_batch = vec![
1878            1.0f32, 0.0, 0.0, // col 0
1879            0.0, 1.0, 0.0, // col 1
1880            0.0, 0.0, 1.0, // col 2
1881        ];
1882
1883        let y = batched_spmv_cpu(
1884            n_rows, n_cols, &row_ptr, &col_idx, &values, &x_batch, batch_size,
1885        );
1886
1887        // A * [1,0,0] = [2,0,1]  => y[row*3+0]
1888        // A * [0,1,0] = [1,3,0]  => y[row*3+1]
1889        // A * [0,0,1] = [0,1,4]  => y[row*3+2]
1890        assert!((y[0] - 2.0).abs() < 1e-6, "A*e0 row0 = 2");
1891        assert!((y[1] - 1.0).abs() < 1e-6, "A*e1 row0 = 1");
1892        assert!((y[2] - 0.0).abs() < 1e-6, "A*e2 row0 = 0");
1893        assert!((y[3] - 0.0).abs() < 1e-6, "A*e0 row1 = 0");
1894        assert!((y[4] - 3.0).abs() < 1e-6, "A*e1 row1 = 3");
1895        assert!((y[5] - 1.0).abs() < 1e-6, "A*e2 row1 = 1");
1896        assert!((y[6] - 1.0).abs() < 1e-6, "A*e0 row2 = 1");
1897        assert!((y[7] - 0.0).abs() < 1e-6, "A*e1 row2 = 0");
1898        assert!((y[8] - 4.0).abs() < 1e-6, "A*e2 row2 = 4");
1899    }
1900
1901    // -- mixed_precision_spmv_cpu tests ----------------------------------------
1902
1903    #[test]
1904    fn mixed_precision_spmv_correctness() {
1905        // 4x4 identity matrix, x=[1,2,3,4] => y=[1,2,3,4]
1906        let n_rows = 4usize;
1907        let row_ptr = vec![0u32, 1, 2, 3, 4];
1908        let col_idx = vec![0u32, 1, 2, 3];
1909        let values_fp16 = vec![1.0f32; 4];
1910        let x = vec![1.0f32, 2.0, 3.0, 4.0];
1911
1912        let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1913
1914        assert_eq!(y.len(), n_rows);
1915        for (i, &yi) in y.iter().enumerate() {
1916            assert!(
1917                (yi - (i + 1) as f32).abs() < 1e-4,
1918                "y[{}] should be {} but got {}",
1919                i,
1920                i + 1,
1921                yi
1922            );
1923        }
1924    }
1925
1926    #[test]
1927    fn mixed_precision_accumulation_fp32() {
1928        // Large sum: 1000-row diagonal, all values=1.0, x=[1.0; 1000]
1929        // Each y[i] = 1.0, which is finite and representable in f32
1930        let n_rows = 1000usize;
1931        let row_ptr: Vec<u32> = (0..=1000).map(|i| i as u32).collect();
1932        let col_idx: Vec<u32> = (0..1000).map(|i| i as u32).collect();
1933        let values_fp16 = vec![1.0f32; 1000];
1934        let x = vec![1.0f32; 1000];
1935
1936        let y = mixed_precision_spmv_cpu(n_rows, &row_ptr, &col_idx, &values_fp16, &x);
1937
1938        assert_eq!(y.len(), n_rows);
1939        // All results should be finite (the accumulation via f64 avoids overflow)
1940        for (i, &yi) in y.iter().enumerate() {
1941            assert!(yi.is_finite(), "y[{}] = {} is not finite", i, yi);
1942            assert!(
1943                (yi - 1.0).abs() < 1e-4,
1944                "y[{}] should be 1.0, got {}",
1945                i,
1946                yi
1947            );
1948        }
1949    }
1950}