Skip to main content

oxicuda_sparse/format/
ell.rs

1//! ELLPACK (ELL) sparse matrix format.
2//!
3//! The ELLPACK format stores at most `max_nnz_per_row` entries per row,
4//! using a padded column index array and a padded values array, each of
5//! shape `(rows, max_nnz_per_row)` stored in column-major order.
6//!
7//! Unused entries are padded with a sentinel column index of -1 and a
8//! zero value. This format is highly efficient for matrices with regular
9//! sparsity patterns (similar nnz per row) since it avoids indirect
10//! indexing and enables coalesced memory access on GPUs.
11
12use oxicuda_blas::GpuFloat;
13use oxicuda_memory::DeviceBuffer;
14
15use crate::error::{SparseError, SparseResult};
16
17/// Sentinel value for unused ELLPACK entries.
18pub const ELL_SENTINEL: i32 = -1;
19
20/// A sparse matrix in ELLPACK (ELL) format, stored on GPU.
21///
22/// Both `col_idx` and `values` have length `rows * max_nnz_per_row`,
23/// stored in column-major order: element `(i, k)` is at index
24/// `k * rows + i`.
25pub struct EllMatrix<T: GpuFloat> {
26    /// Number of rows.
27    rows: u32,
28    /// Number of columns.
29    cols: u32,
30    /// Maximum non-zeros per row (determines the padding width).
31    max_nnz_per_row: u32,
32    /// Column indices: length `rows * max_nnz_per_row`. Unused entries are -1.
33    col_idx: DeviceBuffer<i32>,
34    /// Values: length `rows * max_nnz_per_row`. Unused entries are zero.
35    values: DeviceBuffer<T>,
36}
37
38impl<T: GpuFloat> EllMatrix<T> {
39    /// Creates an ELL matrix from host-side padded arrays, uploading to GPU.
40    ///
41    /// # Arguments
42    ///
43    /// * `rows` -- Number of rows.
44    /// * `cols` -- Number of columns.
45    /// * `max_nnz_per_row` -- Maximum entries stored per row.
46    /// * `col_idx` -- Padded column indices, length `rows * max_nnz_per_row`,
47    ///   column-major. Unused entries must be `ELL_SENTINEL` (-1).
48    /// * `values` -- Padded values, length `rows * max_nnz_per_row`,
49    ///   column-major. Unused entries should be zero.
50    ///
51    /// # Errors
52    ///
53    /// Returns [`SparseError::InvalidFormat`] if array lengths are incorrect.
54    pub fn from_host(
55        rows: u32,
56        cols: u32,
57        max_nnz_per_row: u32,
58        col_idx: &[i32],
59        values: &[T],
60    ) -> SparseResult<Self> {
61        if rows == 0 || cols == 0 {
62            return Err(SparseError::InvalidFormat(
63                "rows and cols must be non-zero".to_string(),
64            ));
65        }
66        if max_nnz_per_row == 0 {
67            return Err(SparseError::ZeroNnz);
68        }
69
70        let total = rows as usize * max_nnz_per_row as usize;
71        if col_idx.len() != total {
72            return Err(SparseError::InvalidFormat(format!(
73                "col_idx length ({}) must be rows * max_nnz_per_row ({})",
74                col_idx.len(),
75                total
76            )));
77        }
78        if values.len() != total {
79            return Err(SparseError::InvalidFormat(format!(
80                "values length ({}) must be rows * max_nnz_per_row ({})",
81                values.len(),
82                total
83            )));
84        }
85
86        let d_col_idx = DeviceBuffer::from_host(col_idx)?;
87        let d_values = DeviceBuffer::from_host(values)?;
88
89        Ok(Self {
90            rows,
91            cols,
92            max_nnz_per_row,
93            col_idx: d_col_idx,
94            values: d_values,
95        })
96    }
97
98    /// Creates an ELL matrix from a CSR matrix on the host.
99    ///
100    /// Determines `max_nnz_per_row` from the CSR structure, then pads
101    /// each row to that width.
102    ///
103    /// # Errors
104    ///
105    /// Returns [`SparseError::Cuda`] on transfer failure.
106    pub fn from_csr(csr: &super::CsrMatrix<T>) -> SparseResult<Self> {
107        let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
108        let rows = csr.rows();
109        let cols = csr.cols();
110
111        // Find max nnz per row
112        let mut max_nnz: u32 = 0;
113        for i in 0..rows as usize {
114            let row_nnz = (h_row_ptr[i + 1] - h_row_ptr[i]) as u32;
115            if row_nnz > max_nnz {
116                max_nnz = row_nnz;
117            }
118        }
119
120        if max_nnz == 0 {
121            return Err(SparseError::ZeroNnz);
122        }
123
124        // Build padded ELL arrays (column-major: element (i, k) at k * rows + i)
125        let total = rows as usize * max_nnz as usize;
126        let mut ell_col_idx = vec![ELL_SENTINEL; total];
127        let mut ell_values = vec![T::gpu_zero(); total];
128
129        for i in 0..rows as usize {
130            let start = h_row_ptr[i] as usize;
131            let end = h_row_ptr[i + 1] as usize;
132            for (k, j) in (start..end).enumerate() {
133                let idx = k * rows as usize + i;
134                ell_col_idx[idx] = h_col_idx[j];
135                ell_values[idx] = h_values[j];
136            }
137        }
138
139        Self::from_host(rows, cols, max_nnz, &ell_col_idx, &ell_values)
140    }
141
142    /// Downloads the ELL arrays from GPU to host memory.
143    ///
144    /// # Errors
145    ///
146    /// Returns [`SparseError::Cuda`] on transfer failure.
147    pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<T>)> {
148        let mut h_col_idx = vec![0i32; self.col_idx.len()];
149        let mut h_values = vec![T::gpu_zero(); self.values.len()];
150
151        self.col_idx.copy_to_host(&mut h_col_idx)?;
152        self.values.copy_to_host(&mut h_values)?;
153
154        Ok((h_col_idx, h_values))
155    }
156
157    /// Returns the number of rows.
158    #[inline]
159    pub fn rows(&self) -> u32 {
160        self.rows
161    }
162
163    /// Returns the number of columns.
164    #[inline]
165    pub fn cols(&self) -> u32 {
166        self.cols
167    }
168
169    /// Returns the maximum non-zeros per row.
170    #[inline]
171    pub fn max_nnz_per_row(&self) -> u32 {
172        self.max_nnz_per_row
173    }
174
175    /// Returns a reference to the column index device buffer.
176    #[inline]
177    pub fn col_idx(&self) -> &DeviceBuffer<i32> {
178        &self.col_idx
179    }
180
181    /// Returns a reference to the values device buffer.
182    #[inline]
183    pub fn values(&self) -> &DeviceBuffer<T> {
184        &self.values
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn ell_validation_array_lengths() {
194        // 3 rows, max 2 per row => total 6 entries
195        let result = EllMatrix::<f32>::from_host(
196            3,
197            3,
198            2,
199            &[0, 1, 2, -1, -1], // length 5, should be 6
200            &[1.0; 5],
201        );
202        assert!(result.is_err());
203    }
204
205    #[test]
206    fn ell_sentinel_value() {
207        assert_eq!(ELL_SENTINEL, -1);
208    }
209}