Skip to main content

oxicuda_sparse/format/
csr5.rs

1//! CSR5 sparse matrix format.
2//!
3//! CSR5 is a tile-based variant of CSR designed for load-balanced SpMV on GPUs.
4//! It divides the non-zero elements into fixed-size tiles (typically 32 elements
5//! wide, matching the warp size), with each tile assigned to one warp.
6//!
7//! The key insight is that tiles may straddle row boundaries. The `tile_desc`
8//! array encodes where rows start and end within each tile, enabling each warp
9//! to know exactly which rows its elements contribute to.
10//!
11//! ## Structure
12//!
13//! A `Csr5Matrix<T>` contains:
14//! - The original CSR arrays (`row_ptr`, `col_idx`, `values`)
15//! - `tile_ptr[num_tiles+1]`: maps tile index to starting element index
16//! - `tile_desc[num_tiles]`: bit-packed descriptor encoding row boundaries
17//!   within each tile (which rows start/end in this tile)
18//! - `calibrator[rows]`: workspace for cross-tile row contribution merging
19//!
20//! ## Reference
21//!
22//! W. Liu and B. Vinter, "CSR5: An Efficient Storage Format for
23//! Cross-Platform Sparse Matrix-Vector Multiplication", ICS 2015.
24
25use oxicuda_blas::GpuFloat;
26use oxicuda_memory::DeviceBuffer;
27
28use crate::error::{SparseError, SparseResult};
29
30/// Number of non-zero elements per tile (warp width).
31pub const CSR5_TILE_WIDTH: u32 = 32;
32
33/// Number of rows encoded per tile descriptor segment.
34/// Each tile can contain contributions to multiple rows;
35/// this is the maximum tracked per tile.
36pub const CSR5_SIGMA: u32 = 32;
37
38/// Tile descriptor: bit-packed row boundary information for one tile.
39///
40/// The descriptor encodes:
41/// - `y_offset`: the starting row offset (local within the tile's first row)
42/// - `seg_offset`: bitmask indicating which lanes in the warp start a new row
43/// - `empty_offset`: number of empty (padding) elements at the end of the tile
44///
45/// This allows each warp to determine which rows its non-zero elements belong
46/// to and how to reduce partial sums correctly.
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
48#[repr(C)]
49pub struct TileDescriptor {
50    /// Bitmask: bit `i` is set if lane `i` starts a new row segment.
51    pub seg_mask: u32,
52    /// Index of the first row that this tile contributes to.
53    pub first_row: u32,
54}
55
56// SAFETY: TileDescriptor is repr(C) with only u32 fields, trivially bitwise
57// copyable and has no interior pointers -- safe for GPU memcpy.
58unsafe impl Send for TileDescriptor {}
59unsafe impl Sync for TileDescriptor {}
60
61/// A sparse matrix in CSR5 format, stored on GPU.
62///
63/// CSR5 augments the standard CSR representation with tile-based metadata
64/// for load-balanced GPU SpMV. The non-zero elements are logically divided
65/// into tiles of `CSR5_TILE_WIDTH` elements each. Each tile maps to one
66/// warp during SpMV execution.
67pub struct Csr5Matrix<T: GpuFloat> {
68    /// Number of rows in the matrix.
69    rows: u32,
70    /// Number of columns in the matrix.
71    cols: u32,
72    /// Total number of non-zeros.
73    nnz: u32,
74    /// Number of tiles.
75    num_tiles: u32,
76
77    // -- Original CSR arrays --
78    /// CSR row pointer array of length `rows + 1`.
79    row_ptr: DeviceBuffer<i32>,
80    /// CSR column indices of length `nnz`.
81    col_idx: DeviceBuffer<i32>,
82    /// CSR values of length `nnz`.
83    values: DeviceBuffer<T>,
84
85    // -- CSR5 tile metadata --
86    /// Tile pointer: maps tile index to starting nnz index.
87    /// Length `num_tiles + 1`.
88    tile_ptr: DeviceBuffer<u32>,
89    /// Tile descriptors: row boundary information per tile.
90    /// Length `num_tiles`.
91    tile_desc: DeviceBuffer<TileDescriptor>,
92    /// Calibrator: workspace for cross-tile row contribution merging.
93    /// Length `rows`. Used during the "calibrate" phase of CSR5 SpMV.
94    calibrator: DeviceBuffer<T>,
95}
96
97impl<T: GpuFloat> Csr5Matrix<T> {
98    /// Constructs a CSR5 matrix from host-side CSR arrays.
99    ///
100    /// This performs the CSR-to-CSR5 conversion on the host, computing
101    /// tile pointers and descriptors, then uploads everything to GPU memory.
102    ///
103    /// # Arguments
104    ///
105    /// * `rows` -- Number of rows.
106    /// * `cols` -- Number of columns.
107    /// * `row_ptr` -- CSR row pointer (length `rows + 1`).
108    /// * `col_idx` -- CSR column indices (length `nnz`).
109    /// * `values` -- CSR values (length `nnz`).
110    ///
111    /// # Errors
112    ///
113    /// Returns [`SparseError::InvalidFormat`] if inputs are inconsistent.
114    /// Returns [`SparseError::Cuda`] on GPU allocation/transfer failure.
115    pub fn from_csr_host(
116        rows: u32,
117        cols: u32,
118        row_ptr: &[i32],
119        col_idx: &[i32],
120        values: &[T],
121    ) -> SparseResult<Self> {
122        // Validate inputs
123        if rows == 0 || cols == 0 {
124            return Err(SparseError::InvalidFormat(
125                "rows and cols must be non-zero".to_string(),
126            ));
127        }
128        if row_ptr.len() != rows as usize + 1 {
129            return Err(SparseError::InvalidFormat(format!(
130                "row_ptr length ({}) must be rows + 1 ({})",
131                row_ptr.len(),
132                rows as usize + 1
133            )));
134        }
135        if col_idx.len() != values.len() {
136            return Err(SparseError::InvalidFormat(format!(
137                "col_idx length ({}) must equal values length ({})",
138                col_idx.len(),
139                values.len()
140            )));
141        }
142
143        let nnz = col_idx.len() as u32;
144        if nnz == 0 {
145            return Err(SparseError::ZeroNnz);
146        }
147
148        // Compute tile metadata
149        let num_tiles = nnz.div_ceil(CSR5_TILE_WIDTH);
150
151        // Build tile_ptr: tile i spans elements [i*TILE_WIDTH .. (i+1)*TILE_WIDTH]
152        let mut h_tile_ptr = Vec::with_capacity(num_tiles as usize + 1);
153        for t in 0..=num_tiles {
154            h_tile_ptr.push((t * CSR5_TILE_WIDTH).min(nnz));
155        }
156
157        // Build tile descriptors
158        let mut h_tile_desc = Vec::with_capacity(num_tiles as usize);
159        for t in 0..num_tiles {
160            let tile_start = h_tile_ptr[t as usize];
161            let tile_end = h_tile_ptr[t as usize + 1];
162
163            // Find the first row that this tile contributes to
164            let first_row = find_row_for_element(row_ptr, tile_start);
165
166            // Build segment mask: bit i is set if element (tile_start + i) is
167            // the first element of a new row
168            let mut seg_mask: u32 = 0;
169            for lane in 0..CSR5_TILE_WIDTH {
170                let elem_idx = tile_start + lane;
171                if elem_idx >= tile_end {
172                    break;
173                }
174                // Check if this element starts a new row
175                let elem_row = find_row_for_element(row_ptr, elem_idx);
176                if lane == 0 {
177                    // First lane always starts the first row segment
178                    // (seg_mask bit 0 is set implicitly by first_row)
179                } else {
180                    let prev_row = find_row_for_element(row_ptr, elem_idx - 1);
181                    if elem_row != prev_row {
182                        seg_mask |= 1 << lane;
183                    }
184                }
185            }
186
187            h_tile_desc.push(TileDescriptor {
188                seg_mask,
189                first_row,
190            });
191        }
192
193        // Calibrator: zero-initialized workspace
194        let h_calibrator = vec![T::gpu_zero(); rows as usize];
195
196        // Upload to GPU
197        let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
198        let d_col_idx = DeviceBuffer::from_host(col_idx)?;
199        let d_values = DeviceBuffer::from_host(values)?;
200        let d_tile_ptr = DeviceBuffer::from_host(&h_tile_ptr)?;
201        let d_tile_desc = DeviceBuffer::from_host(&h_tile_desc)?;
202        let d_calibrator = DeviceBuffer::from_host(&h_calibrator)?;
203
204        Ok(Self {
205            rows,
206            cols,
207            nnz,
208            num_tiles,
209            row_ptr: d_row_ptr,
210            col_idx: d_col_idx,
211            values: d_values,
212            tile_ptr: d_tile_ptr,
213            tile_desc: d_tile_desc,
214            calibrator: d_calibrator,
215        })
216    }
217
218    /// Constructs a CSR5 matrix from an existing GPU-resident CSR matrix.
219    ///
220    /// Downloads CSR data to host, computes tile metadata, then re-uploads
221    /// everything.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`SparseError::Cuda`] on GPU transfer/allocation failure.
226    pub fn from_csr(csr: &super::CsrMatrix<T>) -> SparseResult<Self> {
227        let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
228        Self::from_csr_host(csr.rows(), csr.cols(), &h_row_ptr, &h_col_idx, &h_values)
229    }
230
231    // -- Accessors --
232
233    /// Returns the number of rows.
234    #[inline]
235    pub fn rows(&self) -> u32 {
236        self.rows
237    }
238
239    /// Returns the number of columns.
240    #[inline]
241    pub fn cols(&self) -> u32 {
242        self.cols
243    }
244
245    /// Returns the total non-zero count.
246    #[inline]
247    pub fn nnz(&self) -> u32 {
248        self.nnz
249    }
250
251    /// Returns the number of tiles.
252    #[inline]
253    pub fn num_tiles(&self) -> u32 {
254        self.num_tiles
255    }
256
257    /// Returns a reference to the CSR row pointer device buffer.
258    #[inline]
259    pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
260        &self.row_ptr
261    }
262
263    /// Returns a reference to the CSR column index device buffer.
264    #[inline]
265    pub fn col_idx(&self) -> &DeviceBuffer<i32> {
266        &self.col_idx
267    }
268
269    /// Returns a reference to the values device buffer.
270    #[inline]
271    pub fn values(&self) -> &DeviceBuffer<T> {
272        &self.values
273    }
274
275    /// Returns a reference to the tile pointer device buffer.
276    #[inline]
277    pub fn tile_ptr(&self) -> &DeviceBuffer<u32> {
278        &self.tile_ptr
279    }
280
281    /// Returns a reference to the tile descriptor device buffer.
282    #[inline]
283    pub fn tile_desc(&self) -> &DeviceBuffer<TileDescriptor> {
284        &self.tile_desc
285    }
286
287    /// Returns a reference to the calibrator device buffer.
288    #[inline]
289    pub fn calibrator(&self) -> &DeviceBuffer<T> {
290        &self.calibrator
291    }
292
293    /// Downloads tile metadata to host for inspection.
294    ///
295    /// # Errors
296    ///
297    /// Returns [`SparseError::Cuda`] on transfer failure.
298    pub fn tile_metadata_to_host(&self) -> SparseResult<(Vec<u32>, Vec<TileDescriptor>)> {
299        let mut h_tile_ptr = vec![0u32; self.tile_ptr.len()];
300        let mut h_tile_desc = vec![TileDescriptor::default(); self.tile_desc.len()];
301
302        self.tile_ptr.copy_to_host(&mut h_tile_ptr)?;
303        self.tile_desc.copy_to_host(&mut h_tile_desc)?;
304
305        Ok((h_tile_ptr, h_tile_desc))
306    }
307}
308
309/// Binary search to find which row a given non-zero element index belongs to.
310///
311/// Given `row_ptr[0..rows+1]` and an element index `elem_idx`, returns the
312/// row `r` such that `row_ptr[r] <= elem_idx < row_ptr[r+1]`.
313fn find_row_for_element(row_ptr: &[i32], elem_idx: u32) -> u32 {
314    let elem = elem_idx as i32;
315    let num_rows = row_ptr.len() - 1;
316
317    // Binary search: find the largest r such that row_ptr[r] <= elem
318    let mut lo: usize = 0;
319    let mut hi: usize = num_rows;
320
321    while lo < hi {
322        let mid = lo + (hi - lo).div_ceil(2);
323        if row_ptr[mid] <= elem {
324            lo = mid;
325        } else {
326            hi = mid - 1;
327        }
328    }
329
330    lo as u32
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336
337    /// Lightweight CPU-only CSR5 reference structure for testing tile metadata.
338    /// The main `Csr5Matrix<T>` requires GPU (DeviceBuffer). This struct
339    /// enables verifying tile_count, tile_width, and tile_ptr logic without GPU.
340    struct Csr5CpuRef {
341        #[allow(dead_code)]
342        pub n_rows: usize,
343        #[allow(dead_code)]
344        pub nnz: usize,
345        pub tile_width: usize,
346        pub tile_count: usize,
347        /// tile_ptr[i] = first row index covered by tile i.
348        /// tile_ptr[tile_count] = n_rows.
349        pub tile_ptr: Vec<u32>,
350    }
351
352    impl Csr5CpuRef {
353        /// Build CPU reference CSR5 metadata from host CSR arrays.
354        fn from_csr(n_rows: usize, row_ptr: &[i32], nnz: usize) -> Self {
355            let tile_width = CSR5_TILE_WIDTH as usize;
356            let tile_count = nnz.div_ceil(tile_width);
357
358            // tile_ptr[i] = the row that contains the first element of tile i
359            let mut tile_ptr = Vec::with_capacity(tile_count + 1);
360            for tile in 0..tile_count {
361                let first_elem = (tile * tile_width) as u32;
362                tile_ptr.push(find_row_for_element(row_ptr, first_elem));
363            }
364            tile_ptr.push(n_rows as u32);
365
366            Self {
367                n_rows,
368                nnz,
369                tile_width,
370                tile_count,
371                tile_ptr,
372            }
373        }
374    }
375
376    #[test]
377    fn csr5_tile_width() {
378        assert_eq!(CSR5_TILE_WIDTH, 32);
379    }
380
381    #[test]
382    fn find_row_for_element_basic() {
383        // 3x3 identity matrix: row_ptr = [0, 1, 2, 3]
384        let row_ptr = [0i32, 1, 2, 3];
385        assert_eq!(find_row_for_element(&row_ptr, 0), 0);
386        assert_eq!(find_row_for_element(&row_ptr, 1), 1);
387        assert_eq!(find_row_for_element(&row_ptr, 2), 2);
388    }
389
390    #[test]
391    fn find_row_for_element_varying_density() {
392        // Row 0: 3 nnz, Row 1: 1 nnz, Row 2: 2 nnz
393        // row_ptr = [0, 3, 4, 6]
394        let row_ptr = [0i32, 3, 4, 6];
395        assert_eq!(find_row_for_element(&row_ptr, 0), 0);
396        assert_eq!(find_row_for_element(&row_ptr, 1), 0);
397        assert_eq!(find_row_for_element(&row_ptr, 2), 0);
398        assert_eq!(find_row_for_element(&row_ptr, 3), 1);
399        assert_eq!(find_row_for_element(&row_ptr, 4), 2);
400        assert_eq!(find_row_for_element(&row_ptr, 5), 2);
401    }
402
403    #[test]
404    fn tile_descriptor_default() {
405        let td = TileDescriptor::default();
406        assert_eq!(td.seg_mask, 0);
407        assert_eq!(td.first_row, 0);
408    }
409
410    #[test]
411    fn csr5_tile_count_computation() {
412        // 10 nnz with tile width 32 => 1 tile
413        let num_tiles = 10_u32.div_ceil(CSR5_TILE_WIDTH);
414        assert_eq!(num_tiles, 1);
415
416        // 64 nnz => 2 tiles
417        let num_tiles = 64_u32.div_ceil(CSR5_TILE_WIDTH);
418        assert_eq!(num_tiles, 2);
419
420        // 33 nnz => 2 tiles
421        let num_tiles = 33_u32.div_ceil(CSR5_TILE_WIDTH);
422        assert_eq!(num_tiles, 2);
423    }
424
425    #[test]
426    fn csr5_sigma_value() {
427        assert_eq!(CSR5_SIGMA, 32);
428    }
429
430    // --- New tests using Csr5CpuRef ---
431
432    #[test]
433    fn csr5_tile_count_32_nnz() {
434        // Exactly 32 nnz => 1 tile (32 / 32 = 1)
435        // Use a 32-row diagonal matrix (each row has exactly 1 nnz)
436        let n_rows = 32usize;
437        let row_ptr: Vec<i32> = (0..=32_i32).collect();
438        let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 32);
439        assert_eq!(mat.tile_count, 1, "32 nnz should yield exactly 1 tile");
440    }
441
442    #[test]
443    fn csr5_tile_count_33_nnz() {
444        // 33 nnz => ceil(33/32) = 2 tiles
445        // Use a 33-row diagonal matrix (each row has exactly 1 nnz)
446        let n_rows = 33usize;
447        let row_ptr: Vec<i32> = (0..=33_i32).collect();
448        let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 33);
449        assert_eq!(mat.tile_count, 2, "33 nnz should yield 2 tiles");
450    }
451
452    #[test]
453    fn csr5_tile_count_1024_nnz() {
454        // 1024 nnz => 1024/32 = 32 tiles (exact)
455        let n_rows = 1024usize;
456        let row_ptr: Vec<i32> = (0..=1024_i32).collect();
457        let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 1024);
458        assert_eq!(mat.tile_count, 32, "1024 nnz should yield 32 tiles");
459    }
460
461    #[test]
462    fn csr5_tile_width_is_32() {
463        let n_rows = 4usize;
464        let row_ptr = vec![0i32, 1, 2, 3, 4];
465        let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 4);
466        assert_eq!(
467            mat.tile_width, 32,
468            "tile_width must always be 32 (warp size)"
469        );
470    }
471
472    #[test]
473    fn csr5_tile_ptr_maps_row_correctly() {
474        // 4x4 identity: row_ptr = [0,1,2,3,4], 4 nnz => 1 tile
475        // tile_ptr[0] should be 0 (tile 0 starts at nnz 0, which belongs to row 0)
476        let n_rows = 4usize;
477        let row_ptr = vec![0i32, 1, 2, 3, 4];
478        let mat = Csr5CpuRef::from_csr(n_rows, &row_ptr, 4);
479        assert_eq!(mat.tile_count, 1);
480        assert_eq!(
481            mat.tile_ptr[0], 0,
482            "tile 0 starts at row 0 for a 4x4 identity matrix"
483        );
484        assert_eq!(
485            mat.tile_ptr[1], n_rows as u32,
486            "tile_ptr[tile_count] must equal n_rows"
487        );
488    }
489}