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}