use oxicuda_blas::GpuFloat;
use oxicuda_memory::DeviceBuffer;
use crate::error::{SparseError, SparseResult};
pub const ELL_SENTINEL: i32 = -1;
pub struct EllMatrix<T: GpuFloat> {
rows: u32,
cols: u32,
max_nnz_per_row: u32,
col_idx: DeviceBuffer<i32>,
values: DeviceBuffer<T>,
}
impl<T: GpuFloat> EllMatrix<T> {
pub fn from_host(
rows: u32,
cols: u32,
max_nnz_per_row: u32,
col_idx: &[i32],
values: &[T],
) -> SparseResult<Self> {
if rows == 0 || cols == 0 {
return Err(SparseError::InvalidFormat(
"rows and cols must be non-zero".to_string(),
));
}
if max_nnz_per_row == 0 {
return Err(SparseError::ZeroNnz);
}
let total = rows as usize * max_nnz_per_row as usize;
if col_idx.len() != total {
return Err(SparseError::InvalidFormat(format!(
"col_idx length ({}) must be rows * max_nnz_per_row ({})",
col_idx.len(),
total
)));
}
if values.len() != total {
return Err(SparseError::InvalidFormat(format!(
"values length ({}) must be rows * max_nnz_per_row ({})",
values.len(),
total
)));
}
for (k, &c) in col_idx.iter().enumerate() {
if c != ELL_SENTINEL && (c < 0 || c as u32 >= cols) {
return Err(SparseError::InvalidFormat(format!(
"col_idx[{k}] = {c} out of range [0, {cols}) and not the sentinel ({ELL_SENTINEL})"
)));
}
}
let d_col_idx = DeviceBuffer::from_host(col_idx)?;
let d_values = DeviceBuffer::from_host(values)?;
Ok(Self {
rows,
cols,
max_nnz_per_row,
col_idx: d_col_idx,
values: d_values,
})
}
pub fn from_csr(csr: &super::CsrMatrix<T>) -> SparseResult<Self> {
let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
let rows = csr.rows();
let cols = csr.cols();
let mut max_nnz: u32 = 0;
for i in 0..rows as usize {
let row_nnz = (h_row_ptr[i + 1] - h_row_ptr[i]) as u32;
if row_nnz > max_nnz {
max_nnz = row_nnz;
}
}
if max_nnz == 0 {
return Err(SparseError::ZeroNnz);
}
let total = rows as usize * max_nnz as usize;
let mut ell_col_idx = vec![ELL_SENTINEL; total];
let mut ell_values = vec![T::gpu_zero(); total];
for i in 0..rows as usize {
let start = h_row_ptr[i] as usize;
let end = h_row_ptr[i + 1] as usize;
for (k, j) in (start..end).enumerate() {
let idx = k * rows as usize + i;
ell_col_idx[idx] = h_col_idx[j];
ell_values[idx] = h_values[j];
}
}
Self::from_host(rows, cols, max_nnz, &ell_col_idx, &ell_values)
}
pub fn to_host(&self) -> SparseResult<(Vec<i32>, Vec<T>)> {
let mut h_col_idx = vec![0i32; self.col_idx.len()];
let mut h_values = vec![T::gpu_zero(); self.values.len()];
self.col_idx.copy_to_host(&mut h_col_idx)?;
self.values.copy_to_host(&mut h_values)?;
Ok((h_col_idx, h_values))
}
#[inline]
pub fn rows(&self) -> u32 {
self.rows
}
#[inline]
pub fn cols(&self) -> u32 {
self.cols
}
#[inline]
pub fn max_nnz_per_row(&self) -> u32 {
self.max_nnz_per_row
}
#[inline]
pub fn col_idx(&self) -> &DeviceBuffer<i32> {
&self.col_idx
}
#[inline]
pub fn values(&self) -> &DeviceBuffer<T> {
&self.values
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ell_validation_array_lengths() {
let result = EllMatrix::<f32>::from_host(
3,
3,
2,
&[0, 1, 2, -1, -1], &[1.0; 5],
);
assert!(result.is_err());
}
#[test]
fn ell_sentinel_value() {
assert_eq!(ELL_SENTINEL, -1);
}
#[test]
fn ell_validation_col_idx_out_of_range() {
let result = EllMatrix::<f32>::from_host(1, 3, 2, &[3, ELL_SENTINEL], &[1.0, 0.0]);
assert!(matches!(result, Err(SparseError::InvalidFormat(_))));
}
}
#[cfg(all(test, feature = "gpu-tests"))]
mod gpu_device_tests {
use super::*;
use crate::gpu_test_support::gpu_handle;
#[test]
fn ell_validation_sentinel_accepted() {
let Some(_handle) = gpu_handle() else {
return;
};
let result = EllMatrix::<f32>::from_host(1, 3, 2, &[0, ELL_SENTINEL], &[1.0, 0.0]);
assert!(result.is_ok());
}
}