Skip to main content

oxicuda_sparse/format/
csc.rs

1//! Compressed Sparse Column (CSC) format.
2//!
3//! CSC is the column-compressed counterpart of CSR. It stores:
4//! - `col_ptr[cols+1]`: indices into `row_idx`/`values` for each column
5//! - `row_idx[nnz]`: row index for each non-zero
6//! - `values[nnz]`: the non-zero values
7//!
8//! CSC is efficient for column-oriented operations and is the natural
9//! format for the transpose of a CSR matrix.
10
11use oxicuda_blas::GpuFloat;
12use oxicuda_memory::DeviceBuffer;
13
14use crate::error::{SparseError, SparseResult};
15
16/// A sparse matrix in Compressed Sparse Column (CSC) format, stored on GPU.
17///
18/// The matrix has shape `(rows, cols)` with `nnz` non-zero elements.
19pub struct CscMatrix<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    /// Column pointer array of length `cols + 1`.
27    col_ptr: DeviceBuffer<i32>,
28    /// Row indices of length `nnz`.
29    row_idx: DeviceBuffer<i32>,
30    /// Non-zero values of length `nnz`.
31    values: DeviceBuffer<T>,
32}
33
34impl<T: GpuFloat> CscMatrix<T> {
35    /// Creates a CSC matrix from host-side arrays, uploading to GPU.
36    ///
37    /// # Arguments
38    ///
39    /// * `rows` -- Number of rows.
40    /// * `cols` -- Number of columns.
41    /// * `col_ptr` -- Column pointer array of length `cols + 1`.
42    /// * `row_idx` -- Row indices of length `nnz`.
43    /// * `values` -- Non-zero values of length `nnz`.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`SparseError::InvalidFormat`] if array lengths are inconsistent.
48    pub fn from_host(
49        rows: u32,
50        cols: u32,
51        col_ptr: &[i32],
52        row_idx: &[i32],
53        values: &[T],
54    ) -> SparseResult<Self> {
55        if rows == 0 || cols == 0 {
56            return Err(SparseError::InvalidFormat(
57                "rows and cols must be non-zero".to_string(),
58            ));
59        }
60
61        let expected_col_ptr_len = cols as usize + 1;
62        if col_ptr.len() != expected_col_ptr_len {
63            return Err(SparseError::InvalidFormat(format!(
64                "col_ptr length ({}) must be cols + 1 ({})",
65                col_ptr.len(),
66                expected_col_ptr_len
67            )));
68        }
69
70        let nnz = values.len();
71        if nnz == 0 {
72            return Err(SparseError::ZeroNnz);
73        }
74        if row_idx.len() != nnz {
75            return Err(SparseError::InvalidFormat(format!(
76                "row_idx length ({}) must equal values length ({})",
77                row_idx.len(),
78                nnz
79            )));
80        }
81
82        if col_ptr[0] != 0 {
83            return Err(SparseError::InvalidFormat(
84                "col_ptr[0] must be 0".to_string(),
85            ));
86        }
87        if col_ptr[cols as usize] != nnz as i32 {
88            return Err(SparseError::InvalidFormat(format!(
89                "col_ptr[cols] ({}) must equal nnz ({})",
90                col_ptr[cols as usize], nnz
91            )));
92        }
93        for i in 0..cols as usize {
94            if col_ptr[i] > col_ptr[i + 1] {
95                return Err(SparseError::InvalidFormat(format!(
96                    "col_ptr must be non-decreasing: col_ptr[{}]={} > col_ptr[{}]={}",
97                    i,
98                    col_ptr[i],
99                    i + 1,
100                    col_ptr[i + 1]
101                )));
102            }
103        }
104
105        // Validate row indices are within [0, rows); an out-of-range
106        // row_idx would otherwise cause SpMV/SpMM kernels to read device
107        // memory out of bounds.
108        for (k, &r) in row_idx.iter().enumerate() {
109            if r < 0 || r as u32 >= rows {
110                return Err(SparseError::InvalidFormat(format!(
111                    "row_idx[{k}] = {r} out of range [0, {rows})"
112                )));
113            }
114        }
115
116        let d_col_ptr = DeviceBuffer::from_host(col_ptr)?;
117        let d_row_idx = DeviceBuffer::from_host(row_idx)?;
118        let d_values = DeviceBuffer::from_host(values)?;
119
120        Ok(Self {
121            rows,
122            cols,
123            nnz: nnz as u32,
124            col_ptr: d_col_ptr,
125            row_idx: d_row_idx,
126            values: d_values,
127        })
128    }
129
130    /// Creates a CSC matrix from pre-allocated device buffers.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`SparseError::InvalidFormat`] if buffer lengths are inconsistent.
135    pub fn from_device(
136        rows: u32,
137        cols: u32,
138        nnz: u32,
139        col_ptr: DeviceBuffer<i32>,
140        row_idx: DeviceBuffer<i32>,
141        values: DeviceBuffer<T>,
142    ) -> SparseResult<Self> {
143        if col_ptr.len() != (cols as usize + 1) {
144            return Err(SparseError::InvalidFormat(format!(
145                "col_ptr length ({}) must be cols + 1 ({})",
146                col_ptr.len(),
147                cols as usize + 1
148            )));
149        }
150        if row_idx.len() != nnz as usize || values.len() != nnz as usize {
151            return Err(SparseError::InvalidFormat(
152                "row_idx and values lengths must equal nnz".to_string(),
153            ));
154        }
155        Ok(Self {
156            rows,
157            cols,
158            nnz,
159            col_ptr,
160            row_idx,
161            values,
162        })
163    }
164
165    /// Downloads the CSC arrays from GPU to host memory.
166    ///
167    /// # Errors
168    ///
169    /// Returns [`SparseError::Cuda`] on transfer failure.
170    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<i32>, Vec<T>)> {
171        let mut h_col_ptr = vec![0i32; self.col_ptr.len()];
172        let mut h_row_idx = vec![0i32; self.row_idx.len()];
173        let mut h_values = vec![T::gpu_zero(); self.values.len()];
174
175        self.col_ptr.copy_to_host(&mut h_col_ptr)?;
176        self.row_idx.copy_to_host(&mut h_row_idx)?;
177        self.values.copy_to_host(&mut h_values)?;
178
179        Ok((h_col_ptr, h_row_idx, h_values))
180    }
181
182    /// Converts this CSC matrix to CSR format on the host.
183    ///
184    /// Downloads data, transposes the structure (CSC -> CSR is analogous
185    /// to transposing the matrix), then uploads.
186    ///
187    /// # Errors
188    ///
189    /// Returns [`SparseError::Cuda`] on transfer failure.
190    pub fn to_csr(&self) -> SparseResult<super::CsrMatrix<T>> {
191        let (h_col_ptr, h_row_idx, h_values) = self.to_host()?;
192
193        // CSC -> CSR is the same as transposing the CSR->CSC algorithm
194        let mut row_counts = vec![0i32; self.rows as usize];
195        for &r in &h_row_idx {
196            row_counts[r as usize] += 1;
197        }
198
199        let mut h_row_ptr = vec![0i32; self.rows as usize + 1];
200        for i in 0..self.rows as usize {
201            h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i];
202        }
203
204        let mut h_csr_col_idx = vec![0i32; self.nnz as usize];
205        let mut h_csr_values = vec![T::gpu_zero(); self.nnz as usize];
206        let mut write_pos = h_row_ptr.clone();
207
208        for col in 0..self.cols as usize {
209            let start = h_col_ptr[col] as usize;
210            let end = h_col_ptr[col + 1] as usize;
211            for j in start..end {
212                let row = h_row_idx[j] as usize;
213                let dest = write_pos[row] as usize;
214                h_csr_col_idx[dest] = col as i32;
215                h_csr_values[dest] = h_values[j];
216                write_pos[row] += 1;
217            }
218        }
219
220        super::CsrMatrix::from_host(
221            self.rows,
222            self.cols,
223            &h_row_ptr,
224            &h_csr_col_idx,
225            &h_csr_values,
226        )
227    }
228
229    /// Returns the number of rows.
230    #[inline]
231    pub fn rows(&self) -> u32 {
232        self.rows
233    }
234
235    /// Returns the number of columns.
236    #[inline]
237    pub fn cols(&self) -> u32 {
238        self.cols
239    }
240
241    /// Returns the number of non-zero elements.
242    #[inline]
243    pub fn nnz(&self) -> u32 {
244        self.nnz
245    }
246
247    /// Returns a reference to the column pointer device buffer.
248    #[inline]
249    pub fn col_ptr(&self) -> &DeviceBuffer<i32> {
250        &self.col_ptr
251    }
252
253    /// Returns a reference to the row index device buffer.
254    #[inline]
255    pub fn row_idx(&self) -> &DeviceBuffer<i32> {
256        &self.row_idx
257    }
258
259    /// Returns a reference to the values device buffer.
260    #[inline]
261    pub fn values(&self) -> &DeviceBuffer<T> {
262        &self.values
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn csc_validation_col_ptr_length() {
272        let result = CscMatrix::<f32>::from_host(3, 3, &[0, 2, 4], &[0, 1, 0, 2], &[1.0; 4]);
273        assert!(result.is_err());
274    }
275
276    #[test]
277    fn csc_validation_zero_nnz() {
278        let result = CscMatrix::<f32>::from_host(2, 2, &[0, 0, 0], &[], &[]);
279        assert!(matches!(result, Err(SparseError::ZeroNnz)));
280    }
281
282    #[test]
283    fn csc_validation_row_idx_out_of_range() {
284        // rows = 2, so row index 2 is out of range
285        let result = CscMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, 2], &[1.0, 2.0]);
286        assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
287    }
288
289    #[test]
290    fn csc_validation_negative_row_idx() {
291        let result = CscMatrix::<f32>::from_host(2, 2, &[0, 1, 2], &[0, -1], &[1.0, 2.0]);
292        assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
293    }
294}