Skip to main content

mesh_sieve/accelerator/
csr.rs

1//! Device-ready sparse matrices for solver interoperability.
2
3use std::collections::HashMap;
4
5use crate::algs::assembly::GlobalCsrPattern;
6use crate::discretization::runtime::{ClosureDof, CsrPattern};
7
8use super::plan::{checked_u32, upload};
9use super::{AcceleratorBackend, AcceleratorError, CpuBackend, FvmScalar};
10
11/// A validated CSR matrix with device-resident structure and values.
12pub struct DeviceCsrMatrix<T: FvmScalar, B: AcceleratorBackend> {
13    /// Zero-based row offsets, with `row_count + 1` entries.
14    pub row_offsets: B::Buffer<u32>,
15    /// Zero-based column indices.
16    pub column_indices: B::Buffer<u32>,
17    /// Nonzero values in row-major CSR order.
18    pub values: B::Buffer<T>,
19    /// Number of represented rows.
20    pub row_count: usize,
21    /// Required input vector length.
22    pub column_count: usize,
23    /// Reusable implementation-specific SpMV scratch allocation.
24    pub workspace: Option<B::Buffer<u8>>,
25}
26
27impl<T: FvmScalar, B: AcceleratorBackend> DeviceCsrMatrix<T, B> {
28    /// Build a square matrix from a symbolic closure-DOF pattern.
29    pub fn from_pattern(
30        backend: &B,
31        pattern: &CsrPattern,
32        values: &[T],
33    ) -> Result<Self, AcceleratorError> {
34        if pattern.xadj.len() != pattern.rows.len().saturating_add(1) {
35            return Err(AcceleratorError::InvalidPlan(format!(
36                "CSR has {} row offsets for {} rows",
37                pattern.xadj.len(),
38                pattern.rows.len()
39            )));
40        }
41        let indices: HashMap<ClosureDof, usize> = pattern
42            .rows
43            .iter()
44            .copied()
45            .enumerate()
46            .map(|(index, dof)| (dof, index))
47            .collect();
48        if indices.len() != pattern.rows.len() {
49            return Err(AcceleratorError::InvalidPlan(
50                "CSR pattern contains duplicate row DOFs".into(),
51            ));
52        }
53        let columns = pattern
54            .adjncy
55            .iter()
56            .map(|dof| {
57                indices.get(dof).copied().ok_or_else(|| {
58                    AcceleratorError::InvalidPlan(format!(
59                        "CSR column {dof:?} has no represented row"
60                    ))
61                })
62            })
63            .collect::<Result<Vec<_>, _>>()?;
64        Self::from_indices(backend, &pattern.xadj, &columns, values, pattern.rows.len())
65    }
66
67    /// Build a compact-row matrix from a globally numbered pattern.
68    ///
69    /// Output rows follow `pattern.rows`; input columns retain their global
70    /// numbers, so `column_count` is one greater than the largest column.
71    pub fn from_global_pattern(
72        backend: &B,
73        pattern: &GlobalCsrPattern,
74        values: &[T],
75    ) -> Result<Self, AcceleratorError> {
76        if pattern.xadj.len() != pattern.rows.len().saturating_add(1) {
77            return Err(AcceleratorError::InvalidPlan(format!(
78                "global CSR has {} row offsets for {} rows",
79                pattern.xadj.len(),
80                pattern.rows.len()
81            )));
82        }
83        let column_count = pattern
84            .adjncy
85            .iter()
86            .copied()
87            .max()
88            .map_or(Ok(0), |value| {
89                value.checked_add(1).ok_or(AcceleratorError::IndexOverflow {
90                    what: "global CSR column count",
91                    value,
92                })
93            })?;
94        Self::from_indices(
95            backend,
96            &pattern.xadj,
97            &pattern.adjncy,
98            values,
99            column_count,
100        )
101    }
102
103    fn from_indices(
104        backend: &B,
105        offsets: &[usize],
106        columns: &[usize],
107        values: &[T],
108        column_count: usize,
109    ) -> Result<Self, AcceleratorError> {
110        if offsets.is_empty() || offsets[0] != 0 {
111            return Err(AcceleratorError::InvalidPlan(
112                "CSR row offsets must begin at zero".into(),
113            ));
114        }
115        if offsets.windows(2).any(|pair| pair[0] > pair[1]) {
116            return Err(AcceleratorError::InvalidPlan(
117                "CSR row offsets must be monotone".into(),
118            ));
119        }
120        if offsets.last().copied() != Some(columns.len()) || values.len() != columns.len() {
121            return Err(AcceleratorError::InvalidPlan(format!(
122                "CSR terminal offset/column/value lengths disagree: {:?}/{}/{}",
123                offsets.last(),
124                columns.len(),
125                values.len()
126            )));
127        }
128        if let Some(&column) = columns.iter().find(|&&column| column >= column_count) {
129            return Err(AcceleratorError::InvalidPlan(format!(
130                "CSR column {column} is outside column count {column_count}"
131            )));
132        }
133        checked_u32(offsets.len(), "CSR offset count")?;
134        checked_u32(columns.len(), "CSR nonzero count")?;
135        // cuSPARSE's 32-bit CSR descriptors interpret these buffers as signed
136        // integers. Keep the shared representation valid for both the CPU and
137        // CUDA implementations rather than accepting values whose bit pattern
138        // would become negative on the device.
139        if let Some(&value) = offsets.iter().find(|&&value| value > i32::MAX as usize) {
140            return Err(AcceleratorError::IndexOverflow {
141                what: "CSR row offset",
142                value,
143            });
144        }
145        if let Some(&value) = columns.iter().find(|&&value| value > i32::MAX as usize) {
146            return Err(AcceleratorError::IndexOverflow {
147                what: "CSR column index",
148                value,
149            });
150        }
151        let row_offsets = offsets
152            .iter()
153            .map(|&value| checked_u32(value, "CSR row offset"))
154            .collect::<Result<Vec<_>, _>>()?;
155        let column_indices = columns
156            .iter()
157            .map(|&value| checked_u32(value, "CSR column index"))
158            .collect::<Result<Vec<_>, _>>()?;
159        Ok(Self {
160            row_offsets: upload(backend, &row_offsets)?,
161            column_indices: upload(backend, &column_indices)?,
162            values: upload(backend, values)?,
163            row_count: offsets.len() - 1,
164            column_count,
165            workspace: None,
166        })
167    }
168}
169
170impl<T: FvmScalar> DeviceCsrMatrix<T, CpuBackend> {
171    /// Compute `y = alpha * A * x + beta * y` in deterministic row order.
172    pub fn spmv(
173        &self,
174        alpha: T,
175        x: &<CpuBackend as AcceleratorBackend>::Buffer<T>,
176        beta: T,
177        y: &mut <CpuBackend as AcceleratorBackend>::Buffer<T>,
178    ) -> Result<(), AcceleratorError> {
179        if x.as_slice().len() != self.column_count {
180            return Err(AcceleratorError::LengthMismatch {
181                expected: self.column_count,
182                found: x.as_slice().len(),
183            });
184        }
185        if y.as_slice().len() != self.row_count {
186            return Err(AcceleratorError::LengthMismatch {
187                expected: self.row_count,
188                found: y.as_slice().len(),
189            });
190        }
191        for row in 0..self.row_count {
192            let mut sum = 0.0;
193            for offset in self.row_offsets.as_slice()[row] as usize
194                ..self.row_offsets.as_slice()[row + 1] as usize
195            {
196                let column = self.column_indices.as_slice()[offset] as usize;
197                sum += self.values.as_slice()[offset].to_f64() * x.as_slice()[column].to_f64();
198            }
199            let previous = y.as_slice()[row].to_f64();
200            y.as_mut_slice()[row] = T::from_f64(alpha.to_f64() * sum + beta.to_f64() * previous);
201        }
202        Ok(())
203    }
204}