Skip to main content

oxicuda_sparse/format/
bsr.rs

1//! Block Sparse Row (BSR) format.
2//!
3//! BSR stores sparse matrices as a collection of dense blocks of size
4//! `block_dim x block_dim`. This is efficient for matrices with structured
5//! sparsity patterns, such as those arising from finite-element
6//! discretizations.
7//!
8//! The storage is organized as:
9//! - `row_ptr[block_rows+1]`: indices into `col_idx`/`values` for each block row
10//! - `col_idx[nnz_blocks]`: block column index for each non-zero block
11//! - `values[nnz_blocks * block_dim * block_dim]`: dense block data (row-major)
12
13use oxicuda_blas::GpuFloat;
14use oxicuda_memory::DeviceBuffer;
15
16use crate::error::{SparseError, SparseResult};
17
18/// A sparse matrix in Block Sparse Row (BSR) format, stored on GPU.
19///
20/// The matrix has shape `(rows, cols)` where `rows` and `cols` are multiples
21/// of `block_dim`. It contains `nnz_blocks` non-zero dense blocks.
22pub struct BsrMatrix<T: GpuFloat> {
23    /// Number of rows (must be a multiple of `block_dim`).
24    rows: u32,
25    /// Number of columns (must be a multiple of `block_dim`).
26    cols: u32,
27    /// Number of non-zero blocks.
28    nnz_blocks: u32,
29    /// Size of each dense block (block_dim x block_dim).
30    block_dim: u32,
31    /// Block row pointer array of length `block_rows + 1`.
32    row_ptr: DeviceBuffer<i32>,
33    /// Block column indices of length `nnz_blocks`.
34    col_idx: DeviceBuffer<i32>,
35    /// Dense block values of length `nnz_blocks * block_dim * block_dim`.
36    values: DeviceBuffer<T>,
37}
38
39impl<T: GpuFloat> BsrMatrix<T> {
40    /// Creates a BSR matrix from host-side arrays, uploading to GPU.
41    ///
42    /// # Arguments
43    ///
44    /// * `rows` -- Number of rows (must be a multiple of `block_dim`).
45    /// * `cols` -- Number of columns (must be a multiple of `block_dim`).
46    /// * `block_dim` -- Size of each dense block.
47    /// * `row_ptr` -- Block row pointer array of length `block_rows + 1`.
48    /// * `col_idx` -- Block column indices of length `nnz_blocks`.
49    /// * `values` -- Dense block values (row-major within each block).
50    ///
51    /// # Errors
52    ///
53    /// Returns [`SparseError::InvalidFormat`] if dimensions or array lengths
54    /// are inconsistent.
55    #[allow(clippy::too_many_arguments)]
56    pub fn from_host(
57        rows: u32,
58        cols: u32,
59        block_dim: u32,
60        row_ptr: &[i32],
61        col_idx: &[i32],
62        values: &[T],
63    ) -> SparseResult<Self> {
64        if rows == 0 || cols == 0 || block_dim == 0 {
65            return Err(SparseError::InvalidFormat(
66                "rows, cols, and block_dim must be non-zero".to_string(),
67            ));
68        }
69        if rows % block_dim != 0 {
70            return Err(SparseError::InvalidFormat(format!(
71                "rows ({rows}) must be a multiple of block_dim ({block_dim})"
72            )));
73        }
74        if cols % block_dim != 0 {
75            return Err(SparseError::InvalidFormat(format!(
76                "cols ({cols}) must be a multiple of block_dim ({block_dim})"
77            )));
78        }
79
80        let block_rows = rows / block_dim;
81        let expected_row_ptr_len = block_rows as usize + 1;
82        if row_ptr.len() != expected_row_ptr_len {
83            return Err(SparseError::InvalidFormat(format!(
84                "row_ptr length ({}) must be block_rows + 1 ({})",
85                row_ptr.len(),
86                expected_row_ptr_len
87            )));
88        }
89
90        let nnz_blocks = col_idx.len() as u32;
91        if nnz_blocks == 0 {
92            return Err(SparseError::ZeroNnz);
93        }
94
95        let block_elems = block_dim as usize * block_dim as usize;
96        let expected_values_len = nnz_blocks as usize * block_elems;
97        if values.len() != expected_values_len {
98            return Err(SparseError::InvalidFormat(format!(
99                "values length ({}) must be nnz_blocks * block_dim^2 ({})",
100                values.len(),
101                expected_values_len
102            )));
103        }
104
105        // Validate row_ptr
106        if row_ptr[0] != 0 {
107            return Err(SparseError::InvalidFormat(
108                "row_ptr[0] must be 0".to_string(),
109            ));
110        }
111        if row_ptr[block_rows as usize] != nnz_blocks as i32 {
112            return Err(SparseError::InvalidFormat(format!(
113                "row_ptr[block_rows] ({}) must equal nnz_blocks ({})",
114                row_ptr[block_rows as usize], nnz_blocks
115            )));
116        }
117
118        let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
119        let d_col_idx = DeviceBuffer::from_host(col_idx)?;
120        let d_values = DeviceBuffer::from_host(values)?;
121
122        Ok(Self {
123            rows,
124            cols,
125            nnz_blocks,
126            block_dim,
127            row_ptr: d_row_ptr,
128            col_idx: d_col_idx,
129            values: d_values,
130        })
131    }
132
133    /// Downloads the BSR arrays from GPU to host memory.
134    ///
135    /// # Errors
136    ///
137    /// Returns [`SparseError::Cuda`] on transfer failure.
138    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
139        let mut h_row_ptr = vec![0i32; self.row_ptr.len()];
140        let mut h_col_idx = vec![0i32; self.col_idx.len()];
141        let mut h_values = vec![T::gpu_zero(); self.values.len()];
142
143        self.row_ptr.copy_to_host(&mut h_row_ptr)?;
144        self.col_idx.copy_to_host(&mut h_col_idx)?;
145        self.values.copy_to_host(&mut h_values)?;
146
147        Ok((h_row_ptr, h_col_idx, h_values))
148    }
149
150    /// Returns the number of rows.
151    #[inline]
152    pub fn rows(&self) -> u32 {
153        self.rows
154    }
155
156    /// Returns the number of columns.
157    #[inline]
158    pub fn cols(&self) -> u32 {
159        self.cols
160    }
161
162    /// Returns the number of non-zero blocks.
163    #[inline]
164    pub fn nnz_blocks(&self) -> u32 {
165        self.nnz_blocks
166    }
167
168    /// Returns the block dimension.
169    #[inline]
170    pub fn block_dim(&self) -> u32 {
171        self.block_dim
172    }
173
174    /// Returns the number of block rows.
175    #[inline]
176    pub fn block_rows(&self) -> u32 {
177        self.rows / self.block_dim
178    }
179
180    /// Returns the number of block columns.
181    #[inline]
182    pub fn block_cols(&self) -> u32 {
183        self.cols / self.block_dim
184    }
185
186    /// Returns the total scalar non-zero count.
187    #[inline]
188    pub fn scalar_nnz(&self) -> u32 {
189        self.nnz_blocks * self.block_dim * self.block_dim
190    }
191
192    /// Returns a reference to the block row pointer device buffer.
193    #[inline]
194    pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
195        &self.row_ptr
196    }
197
198    /// Returns a reference to the block column index device buffer.
199    #[inline]
200    pub fn col_idx(&self) -> &DeviceBuffer<i32> {
201        &self.col_idx
202    }
203
204    /// Returns a reference to the values device buffer.
205    #[inline]
206    pub fn values(&self) -> &DeviceBuffer<T> {
207        &self.values
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    #[test]
216    fn bsr_validation_block_alignment() {
217        // rows not a multiple of block_dim
218        let result = BsrMatrix::<f32>::from_host(
219            5,
220            4,
221            2,
222            &[0, 1, 2, 3], // would be wrong length too
223            &[0, 1, 0],
224            &[1.0; 12],
225        );
226        assert!(result.is_err());
227    }
228
229    #[test]
230    fn bsr_block_counts() {
231        // 4x4 matrix with 2x2 blocks => 2 block rows, 2 block cols
232        let br = 4 / 2;
233        let bc = 4 / 2;
234        assert_eq!(br, 2);
235        assert_eq!(bc, 2);
236    }
237}