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}