Skip to main content

oxicuda_sparse/format/
csr.rs

1//! Compressed Sparse Row (CSR) format.
2//!
3//! CSR is the most widely used sparse matrix format. It stores:
4//! - `row_ptr[rows+1]`: indices into `col_idx`/`values` for each row
5//! - `col_idx[nnz]`: column index for each non-zero
6//! - `values[nnz]`: the non-zero values
7//!
8//! This is the primary format for SpMV, SpMM, and most sparse operations.
9
10use oxicuda_blas::GpuFloat;
11use oxicuda_memory::DeviceBuffer;
12
13use crate::error::{SparseError, SparseResult};
14
15/// A sparse matrix in Compressed Sparse Row (CSR) format, stored on GPU.
16///
17/// The matrix has shape `(rows, cols)` with `nnz` non-zero elements.
18/// All index and value arrays reside in device (GPU) memory.
19pub struct CsrMatrix<T: GpuFloat> {
20    /// Number of rows.
21    rows: u32,
22    /// Number of columns.
23    cols: u32,
24    /// Number of non-zero elements.
25    nnz: u32,
26    /// Row pointer array of length `rows + 1`. `row_ptr[i]` is the index
27    /// into `col_idx`/`values` where row `i` begins.
28    row_ptr: DeviceBuffer<i32>,
29    /// Column indices of length `nnz`.
30    col_idx: DeviceBuffer<i32>,
31    /// Non-zero values of length `nnz`.
32    values: DeviceBuffer<T>,
33}
34
35impl<T: GpuFloat> CsrMatrix<T> {
36    /// Creates a CSR matrix from host-side arrays, uploading to GPU.
37    ///
38    /// # Arguments
39    ///
40    /// * `rows` -- Number of rows.
41    /// * `cols` -- Number of columns.
42    /// * `row_ptr` -- Row pointer array of length `rows + 1`.
43    /// * `col_idx` -- Column indices of length `nnz`.
44    /// * `values` -- Non-zero values of length `nnz`.
45    ///
46    /// # Errors
47    ///
48    /// Returns [`SparseError::InvalidFormat`] if array lengths are inconsistent.
49    /// Returns [`SparseError::ZeroNnz`] if `values` is empty.
50    /// Returns [`SparseError::Cuda`] on GPU memory allocation failure.
51    pub fn from_host(
52        rows: u32,
53        cols: u32,
54        row_ptr: &[i32],
55        col_idx: &[i32],
56        values: &[T],
57    ) -> SparseResult<Self> {
58        // Validate dimensions
59        if rows == 0 || cols == 0 {
60            return Err(SparseError::InvalidFormat(
61                "rows and cols must be non-zero".to_string(),
62            ));
63        }
64
65        // Validate row_ptr length
66        let expected_row_ptr_len = rows as usize + 1;
67        if row_ptr.len() != expected_row_ptr_len {
68            return Err(SparseError::InvalidFormat(format!(
69                "row_ptr length ({}) must be rows + 1 ({})",
70                row_ptr.len(),
71                expected_row_ptr_len
72            )));
73        }
74
75        // Validate nnz consistency
76        let nnz = values.len();
77        if nnz == 0 {
78            return Err(SparseError::ZeroNnz);
79        }
80        if col_idx.len() != nnz {
81            return Err(SparseError::InvalidFormat(format!(
82                "col_idx length ({}) must equal values length ({})",
83                col_idx.len(),
84                nnz
85            )));
86        }
87
88        // Validate row_ptr is non-decreasing and within bounds
89        if row_ptr[0] != 0 {
90            return Err(SparseError::InvalidFormat(
91                "row_ptr[0] must be 0".to_string(),
92            ));
93        }
94        if row_ptr[rows as usize] != nnz as i32 {
95            return Err(SparseError::InvalidFormat(format!(
96                "row_ptr[rows] ({}) must equal nnz ({})",
97                row_ptr[rows as usize], nnz
98            )));
99        }
100        for i in 0..rows as usize {
101            if row_ptr[i] > row_ptr[i + 1] {
102                return Err(SparseError::InvalidFormat(format!(
103                    "row_ptr must be non-decreasing: row_ptr[{}]={} > row_ptr[{}]={}",
104                    i,
105                    row_ptr[i],
106                    i + 1,
107                    row_ptr[i + 1]
108                )));
109            }
110        }
111
112        // Upload to GPU
113        let d_row_ptr = DeviceBuffer::from_host(row_ptr)?;
114        let d_col_idx = DeviceBuffer::from_host(col_idx)?;
115        let d_values = DeviceBuffer::from_host(values)?;
116
117        Ok(Self {
118            rows,
119            cols,
120            nnz: nnz as u32,
121            row_ptr: d_row_ptr,
122            col_idx: d_col_idx,
123            values: d_values,
124        })
125    }
126
127    /// Creates a CSR matrix from pre-allocated device buffers.
128    ///
129    /// No validation of contents is performed; only lengths are checked.
130    ///
131    /// # Errors
132    ///
133    /// Returns [`SparseError::InvalidFormat`] if buffer lengths are inconsistent.
134    pub fn from_device(
135        rows: u32,
136        cols: u32,
137        nnz: u32,
138        row_ptr: DeviceBuffer<i32>,
139        col_idx: DeviceBuffer<i32>,
140        values: DeviceBuffer<T>,
141    ) -> SparseResult<Self> {
142        if row_ptr.len() != (rows as usize + 1) {
143            return Err(SparseError::InvalidFormat(format!(
144                "row_ptr length ({}) must be rows + 1 ({})",
145                row_ptr.len(),
146                rows as usize + 1
147            )));
148        }
149        if col_idx.len() != nnz as usize {
150            return Err(SparseError::InvalidFormat(format!(
151                "col_idx length ({}) must equal nnz ({})",
152                col_idx.len(),
153                nnz
154            )));
155        }
156        if values.len() != nnz as usize {
157            return Err(SparseError::InvalidFormat(format!(
158                "values length ({}) must equal nnz ({})",
159                values.len(),
160                nnz
161            )));
162        }
163        Ok(Self {
164            rows,
165            cols,
166            nnz,
167            row_ptr,
168            col_idx,
169            values,
170        })
171    }
172
173    /// Downloads the CSR arrays from GPU to host memory.
174    ///
175    /// # Errors
176    ///
177    /// Returns [`SparseError::Cuda`] on transfer failure.
178    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
179        let mut h_row_ptr = vec![0i32; self.row_ptr.len()];
180        let mut h_col_idx = vec![0i32; self.col_idx.len()];
181        let mut h_values = vec![T::gpu_zero(); self.values.len()];
182
183        self.row_ptr.copy_to_host(&mut h_row_ptr)?;
184        self.col_idx.copy_to_host(&mut h_col_idx)?;
185        self.values.copy_to_host(&mut h_values)?;
186
187        Ok((h_row_ptr, h_col_idx, h_values))
188    }
189
190    /// Returns the number of rows.
191    #[inline]
192    pub fn rows(&self) -> u32 {
193        self.rows
194    }
195
196    /// Returns the number of columns.
197    #[inline]
198    pub fn cols(&self) -> u32 {
199        self.cols
200    }
201
202    /// Returns the number of non-zero elements.
203    #[inline]
204    pub fn nnz(&self) -> u32 {
205        self.nnz
206    }
207
208    /// Returns the density of the matrix (nnz / (rows * cols)).
209    #[inline]
210    pub fn density(&self) -> f64 {
211        let total = self.rows as f64 * self.cols as f64;
212        if total == 0.0 {
213            return 0.0;
214        }
215        self.nnz as f64 / total
216    }
217
218    /// Checks whether the matrix metadata is structurally valid.
219    ///
220    /// This checks buffer lengths but does NOT download data to verify
221    /// content (e.g. sorted column indices). For full validation, use
222    /// [`to_host`](Self::to_host) and check on the CPU.
223    #[inline]
224    pub fn is_valid(&self) -> bool {
225        self.row_ptr.len() == (self.rows as usize + 1)
226            && self.col_idx.len() == self.nnz as usize
227            && self.values.len() == self.nnz as usize
228    }
229
230    /// Returns a reference to the row pointer device buffer.
231    #[inline]
232    pub fn row_ptr(&self) -> &DeviceBuffer<i32> {
233        &self.row_ptr
234    }
235
236    /// Returns a reference to the column index device buffer.
237    #[inline]
238    pub fn col_idx(&self) -> &DeviceBuffer<i32> {
239        &self.col_idx
240    }
241
242    /// Returns a reference to the values device buffer.
243    #[inline]
244    pub fn values(&self) -> &DeviceBuffer<T> {
245        &self.values
246    }
247
248    /// Returns a mutable reference to the values device buffer.
249    #[inline]
250    pub fn values_mut(&mut self) -> &mut DeviceBuffer<T> {
251        &mut self.values
252    }
253
254    /// Average number of non-zeros per row.
255    #[inline]
256    pub fn avg_nnz_per_row(&self) -> f64 {
257        if self.rows == 0 {
258            return 0.0;
259        }
260        self.nnz as f64 / self.rows as f64
261    }
262
263    /// Converts this CSR matrix to COO format on the host.
264    ///
265    /// Downloads data to host, expands row pointers to row indices,
266    /// then uploads the COO matrix back to GPU.
267    ///
268    /// # Errors
269    ///
270    /// Returns [`SparseError::Cuda`] on transfer failure.
271    pub fn to_coo(&self) -> SparseResult<super::CooMatrix<T>> {
272        let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
273
274        // Expand row_ptr to row_idx
275        let mut h_row_idx = Vec::with_capacity(self.nnz as usize);
276        for row in 0..self.rows {
277            let start = h_row_ptr[row as usize];
278            let end = h_row_ptr[row as usize + 1];
279            for _ in start..end {
280                h_row_idx.push(row as i32);
281            }
282        }
283
284        super::CooMatrix::from_host(self.rows, self.cols, &h_row_idx, &h_col_idx, &h_values)
285    }
286
287    /// Converts this CSR matrix to CSC format on the host.
288    ///
289    /// Downloads data, transposes the structure, then uploads.
290    ///
291    /// # Errors
292    ///
293    /// Returns [`SparseError::Cuda`] on transfer failure.
294    pub fn to_csc(&self) -> SparseResult<super::CscMatrix<T>> {
295        let (h_row_ptr, h_col_idx, h_values) = self.to_host()?;
296
297        // Build CSC by transposing CSR
298        let mut col_counts = vec![0i32; self.cols as usize];
299        for &c in &h_col_idx {
300            col_counts[c as usize] += 1;
301        }
302
303        let mut h_col_ptr = vec![0i32; self.cols as usize + 1];
304        for i in 0..self.cols as usize {
305            h_col_ptr[i + 1] = h_col_ptr[i] + col_counts[i];
306        }
307
308        let mut h_csc_row_idx = vec![0i32; self.nnz as usize];
309        let mut h_csc_values = vec![T::gpu_zero(); self.nnz as usize];
310        let mut write_pos = h_col_ptr.clone();
311
312        for row in 0..self.rows as usize {
313            let start = h_row_ptr[row] as usize;
314            let end = h_row_ptr[row + 1] as usize;
315            for j in start..end {
316                let col = h_col_idx[j] as usize;
317                let dest = write_pos[col] as usize;
318                h_csc_row_idx[dest] = row as i32;
319                h_csc_values[dest] = h_values[j];
320                write_pos[col] += 1;
321            }
322        }
323
324        super::CscMatrix::from_host(
325            self.rows,
326            self.cols,
327            &h_col_ptr,
328            &h_csc_row_idx,
329            &h_csc_values,
330        )
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn csr_validation_row_ptr_length() {
340        // row_ptr too short
341        let result = CsrMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
342        assert!(result.is_err());
343    }
344
345    #[test]
346    fn csr_validation_zero_nnz() {
347        let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
348        assert!(matches!(result, Err(SparseError::ZeroNnz)));
349    }
350
351    #[test]
352    fn csr_validation_mismatched_col_idx() {
353        let result = CsrMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0], &[1.0, 2.0]);
354        assert!(result.is_err());
355    }
356
357    #[test]
358    fn csr_density() {
359        // A 4x4 matrix with 4 nnz => density 0.25
360        // We cannot actually call from_host without a GPU, so test the formula
361        assert!((4.0_f64 / 16.0 - 0.25).abs() < 1e-10);
362    }
363}