Skip to main content

hermes_simd_core/sparse/
ops.rs

1//! Elementwise operations and value sum/accumulate helpers.
2//!
3//! # Safety
4//!
5//! Every kernel call below is `#[target_feature]`-gated and is therefore sound
6//! only on a host implementing `Arch`. That holds by construction rather than by
7//! inspection: [`SimdView::new`](crate::view::SimdView::new) returns `None` for
8//! an architecture the host cannot execute, and the sparse and copy-on-write
9//! constructors assert the same condition, so possessing one of these
10//! arch-parameterized values *is* the proof. Per-site `SAFETY` comments record
11//! only the obligations that go beyond it — pointer provenance, bounds, and
12//! alignment.
13
14use super::{BlockedCoo, Csr, DenseWithMask, SellP, SparseView};
15use crate::arch::SimdArch;
16use crate::kernel::SimdKernel;
17use crate::scalar::Scalar;
18use crate::sparse::spmv::build_index_vector;
19
20/// Unified trait for elementwise and reduction operations on sparse matrices.
21pub trait SparseOps<T> {
22    /// Compute the sum of all elements stored in the sparse matrix.
23    fn sum_values(&self) -> T;
24
25    /// Elementwise multiply the sparse matrix values by corresponding entries
26    /// in a dense matrix, writing the results to `out_values`.
27    fn elementwise_mul_dense(&self, dense: &[T], out_values: &mut [T]);
28}
29
30impl<'a, T, Arch> SparseOps<T> for SparseView<'a, T, Csr, Arch>
31where
32    T: Scalar,
33    Arch: SimdArch + SimdKernel<T>,
34{
35    #[inline]
36    fn sum_values(&self) -> T {
37        if let Some(view) =
38            crate::view::SimdView::<T, Arch, crate::align::Unaligned>::new(self.data.values)
39        {
40            view.reduce(crate::ops::Sum)
41        } else {
42            T::ZERO
43        }
44    }
45
46    #[inline]
47    fn elementwise_mul_dense(&self, dense: &[T], out_values: &mut [T]) {
48        let d = &self.data;
49        let lane_count = Arch::LANE_COUNT;
50
51        // SOUNDNESS: the SIMD path below gathers `dense[col_indices[j]]` with an
52        // unchecked `Arch::gather`. `SparseView<Csr>` is the *unvalidated* type
53        // (constructible from arbitrary `CsrData` via `from_csr`), so nothing
54        // otherwise guarantees `col_indices[j] < dense.len()` and the gather
55        // could read out of bounds from safe code. Validate the structure
56        // (`col_indices[k] < ncols`) via the SSOT checker and require
57        // `dense.len() >= ncols`, so every gathered index is in bounds. O(nnz)
58        // once per call, matching the SELL-p path in this file.
59        use super::types::SparseValidate;
60        d.validate()
61            .expect("CSR matrix failed structural validation before elementwise_mul_dense");
62        assert!(
63            dense.len() >= d.ncols,
64            "CSR elementwise_mul_dense: dense len {} < ncols {}",
65            dense.len(),
66            d.ncols
67        );
68
69        for r in 0..d.nrows {
70            let start = d.row_ptr[r] as usize;
71            let end = d.row_ptr[r + 1] as usize;
72            let row_nnz = end - start;
73            let vals = &d.values[start..end];
74            let cols = &d.col_indices[start..end];
75            let out = &mut out_values[start..end];
76
77            let simd_len = (row_nnz / lane_count) * lane_count;
78            // SAFETY: `Arch::*` are target-feature kernels (module invariant).
79            // The window `[j, j+LANE_COUNT)` stays within `vals`/`cols`/`out` for
80            // `j < simd_len <= row_nnz`, and `validate` above proved every
81            // `cols[k] < ncols <= dense.len()`, so each gathered `dense[cols[k]]`
82            // is in bounds.
83            let mut j = 0usize;
84            unsafe {
85                while j < simd_len {
86                    let idx = build_index_vector::<T, Arch>(&cols[j..j + lane_count]);
87                    let res_vec = Arch::mul(
88                        Arch::load_unaligned(vals[j..].as_ptr()),
89                        Arch::gather(dense.as_ptr(), idx),
90                    );
91                    Arch::store_unaligned(out[j..].as_mut_ptr(), res_vec);
92                    j += lane_count;
93                }
94            }
95            while j < row_nnz {
96                let c = cols[j] as usize;
97                out[j] = vals[j] * dense[c];
98                j += 1;
99            }
100        }
101    }
102}
103
104impl<'a, T, const C: usize, Arch> SparseOps<T> for SparseView<'a, T, SellP<C>, Arch>
105where
106    T: Scalar,
107    Arch: SimdArch + SimdKernel<T>,
108{
109    #[inline]
110    fn sum_values(&self) -> T {
111        if let Some(view) =
112            crate::view::SimdView::<T, Arch, crate::align::Unaligned>::new(self.data.values)
113        {
114            view.reduce(crate::ops::Sum)
115        } else {
116            T::ZERO
117        }
118    }
119
120    #[inline]
121    fn elementwise_mul_dense(&self, dense: &[T], out_values: &mut [T]) {
122        let d = &self.data;
123        let nslices = d.nslices();
124        let lane_count = Arch::LANE_COUNT;
125
126        if lane_count == C {
127            // SOUNDNESS: the vectorized path loads `values[offset..]` and stores
128            // `out_values[offset..]` as full `C`-lane vectors. Validate SELL-p
129            // slice geometry via the SSOT checker (bounds `offset + C <=
130            // values.len()`) and require the output to be at least as long as the
131            // values array, so both unchecked accesses stay in bounds even for a
132            // caller-constructed matrix with `pub` fields.
133            use super::types::SparseValidate;
134            d.validate()
135                .expect("SELL-p matrix failed structural validation before vectorized kernel");
136            assert!(
137                out_values.len() >= d.values.len(),
138                "SELL-p elementwise_mul_dense: out_values len {} < values len {}",
139                out_values.len(),
140                d.values.len()
141            );
142            for s in 0..nslices {
143                let col_count = d.slice_col_count[s] as usize;
144                let start_offset = d.slice_ptr[s] as usize;
145                let slice_base_r = s * C;
146
147                for col in 0..col_count {
148                    let offset = start_offset + col * C;
149
150                    let mut idx_arr = [0i32; 64];
151                    let mut mask_arr = [false; 64];
152                    for row in 0..C {
153                        let r = slice_base_r + row;
154                        let c = d.col_indices[offset + row] as usize;
155                        let in_bounds = r < d.nrows && c < d.ncols;
156                        mask_arr[row] = in_bounds;
157                        if in_bounds {
158                            idx_arr[row] = (r * d.ncols + c) as i32;
159                        }
160                    }
161
162                    // SAFETY: `Arch::*` are target-feature kernels (module
163                    // invariant). `idx_arr`/`mask_arr` hold `C == LANE_COUNT`
164                    // valid entries; `mask` is set only where `r < nrows && c <
165                    // ncols`, so the masked gather touches `dense` only at those
166                    // computed in-bounds indices. `validate` and the output-length
167                    // assert above keep `values[offset..offset+C]` and the masked
168                    // store into `out_values[offset..]` in bounds.
169                    unsafe {
170                        let idx = build_index_vector::<T, Arch>(&idx_arr[..C]);
171                        let mask = Arch::mask_from_bools(&mask_arr[..C]);
172                        let zero_vec = Arch::zero();
173                        let dense_vec = Arch::gather_masked(dense.as_ptr(), idx, mask, zero_vec);
174                        let res_vec =
175                            Arch::mul(Arch::load_unaligned(d.values[offset..].as_ptr()), dense_vec);
176                        Arch::masked_store_unaligned(
177                            out_values[offset..].as_mut_ptr(),
178                            mask,
179                            res_vec,
180                        );
181                    }
182                }
183            }
184        } else {
185            for s in 0..nslices {
186                let col_count = d.slice_col_count[s] as usize;
187                let start_offset = d.slice_ptr[s] as usize;
188                for col in 0..col_count {
189                    for row in 0..C {
190                        let idx = start_offset + col * C + row;
191                        let c = d.col_indices[idx] as usize;
192                        let r = s * C + row;
193                        if r < d.nrows && c < d.ncols {
194                            out_values[idx] = d.values[idx] * dense[r * d.ncols + c];
195                        }
196                    }
197                }
198            }
199        }
200    }
201}
202
203impl<'a, T, const BM: usize, const BN: usize, Arch> SparseOps<T>
204    for SparseView<'a, T, BlockedCoo<BM, BN>, Arch>
205where
206    T: Scalar,
207    Arch: SimdArch + SimdKernel<T>,
208{
209    #[inline]
210    fn sum_values(&self) -> T {
211        if let Some(view) =
212            crate::view::SimdView::<T, Arch, crate::align::Unaligned>::new(self.data.blocks)
213        {
214            view.reduce(crate::ops::Sum)
215        } else {
216            T::ZERO
217        }
218    }
219
220    #[inline]
221    fn elementwise_mul_dense(&self, dense: &[T], out_values: &mut [T]) {
222        let d = &self.data;
223        let lane_count = Arch::LANE_COUNT;
224
225        // Bounds for the unchecked SIMD loads/stores below: the dense matrix and
226        // the block/output buffers must be large enough, and every block must lie
227        // within the `nrows x ncols` dense extent so each `dense[(br+i)*ncols+bc
228        // .. +BN]` read stays in bounds. O(nblocks), once per call.
229        let block_elems = d.nblocks * BM * BN;
230        assert!(
231            dense.len() >= d.nrows * d.ncols,
232            "dense buffer {} too small for {}x{}",
233            dense.len(),
234            d.nrows,
235            d.ncols
236        );
237        assert!(
238            out_values.len() >= block_elems && d.blocks.len() >= block_elems,
239            "block/output buffers too small for {} block elements",
240            block_elems
241        );
242        for b in 0..d.nblocks {
243            let br = d.block_row[b] as usize;
244            let bc = d.block_col[b] as usize;
245            assert!(
246                bc + BN <= d.ncols && br + BM <= d.nrows,
247                "BlockedCoo block {b} (row {br}+{BM}, col {bc}+{BN}) exceeds {}x{}",
248                d.nrows,
249                d.ncols
250            );
251        }
252
253        if BN == lane_count {
254            // SAFETY: `Arch::*` are target-feature kernels (module invariant).
255            // The asserts above bound each block within the dense extent and the
256            // block/output buffers, so every `LANE_COUNT`-wide load of a block
257            // row `blocks[offset..offset+BN]`, the dense window
258            // `dense[(br+i)*ncols+bc ..][..BN]`, and the matching store into
259            // `out_values` stays in bounds.
260            unsafe {
261                for b in 0..d.nblocks {
262                    let br = d.block_row[b] as usize;
263                    let bc = d.block_col[b] as usize;
264                    for i in 0..BM {
265                        let offset = b * (BM * BN) + i * BN;
266                        let dense_idx = (br + i) * d.ncols + bc;
267                        let res_vec = Arch::mul(
268                            Arch::load_unaligned(d.blocks[offset..].as_ptr()),
269                            Arch::load_unaligned(dense[dense_idx..].as_ptr()),
270                        );
271                        Arch::store_unaligned(out_values[offset..].as_mut_ptr(), res_vec);
272                    }
273                }
274            }
275        } else if BN == lane_count * 2 {
276            // SAFETY: as the `BN == LANE_COUNT` arm, with each block row and its
277            // dense window spanning two `LANE_COUNT` loads; both halves stay
278            // within `blocks`/`dense`/`out_values` by the same block-extent and
279            // buffer-length asserts.
280            unsafe {
281                for b in 0..d.nblocks {
282                    let br = d.block_row[b] as usize;
283                    let bc = d.block_col[b] as usize;
284                    for i in 0..BM {
285                        let offset = b * (BM * BN) + i * BN;
286                        let dense_idx = (br + i) * d.ncols + bc;
287                        let res_vec0 = Arch::mul(
288                            Arch::load_unaligned(d.blocks[offset..].as_ptr()),
289                            Arch::load_unaligned(dense[dense_idx..].as_ptr()),
290                        );
291                        let res_vec1 = Arch::mul(
292                            Arch::load_unaligned(d.blocks[offset + lane_count..].as_ptr()),
293                            Arch::load_unaligned(dense[dense_idx + lane_count..].as_ptr()),
294                        );
295                        Arch::store_unaligned(out_values[offset..].as_mut_ptr(), res_vec0);
296                        Arch::store_unaligned(
297                            out_values[offset + lane_count..].as_mut_ptr(),
298                            res_vec1,
299                        );
300                    }
301                }
302            }
303        } else {
304            for b in 0..d.nblocks {
305                let br = d.block_row[b] as usize;
306                let bc = d.block_col[b] as usize;
307                for i in 0..BM {
308                    for j in 0..BN {
309                        let idx = b * (BM * BN) + i * BN + j;
310                        out_values[idx] = d.blocks[idx] * dense[(br + i) * d.ncols + (bc + j)];
311                    }
312                }
313            }
314        }
315    }
316}
317
318impl<'a, T, Arch> SparseOps<T> for SparseView<'a, T, DenseWithMask, Arch>
319where
320    T: Scalar,
321    Arch: SimdArch + SimdKernel<T>,
322{
323    #[inline]
324    fn sum_values(&self) -> T {
325        let lane_count = Arch::LANE_COUNT;
326        let len = self.data.values.len();
327        let simd_len = (len / lane_count) * lane_count;
328
329        // SAFETY: `Arch::*` are target-feature kernels (module invariant). Every
330        // masked load reads `values[i..i+LANE_COUNT]` and `mask[i..i+LANE_COUNT]`
331        // for `i < simd_len <= len`, which stay within the equal-length `values`
332        // and `mask` buffers.
333        let mut i = 0usize;
334        let acc_vec = unsafe {
335            let zero_vec = Arch::zero();
336            let mut acc_vec = zero_vec;
337            while i < simd_len {
338                let msk = Arch::mask_from_bools(&self.data.mask[i..i + lane_count]);
339                let v_vec =
340                    Arch::masked_load_unaligned(self.data.values[i..].as_ptr(), msk, zero_vec);
341                acc_vec = Arch::add(acc_vec, v_vec);
342                i += lane_count;
343            }
344            acc_vec
345        };
346        // SAFETY: target-feature kernel, covered by the module invariant.
347        let mut s = unsafe { Arch::sum_reduce(acc_vec) };
348        while i < len {
349            if self.data.mask[i] {
350                s += self.data.values[i];
351            }
352            i += 1;
353        }
354        s
355    }
356
357    #[inline]
358    fn elementwise_mul_dense(&self, dense: &[T], out_values: &mut [T]) {
359        let d = &self.data;
360        let len = d.values.len();
361        let lane_count = Arch::LANE_COUNT;
362        let simd_len = (len / lane_count) * lane_count;
363
364        // Bounds for the unchecked loads/stores: a `LANE_COUNT` window at
365        // `i < simd_len <= len` must stay within `dense` and `out_values` as
366        // well as `values`/`mask`. `dense` and the output are elementwise-shaped,
367        // so require them at least as long as `values`.
368        assert!(
369            dense.len() >= len && out_values.len() >= len,
370            "DenseWithMask elementwise_mul_dense: dense {} / out {} shorter than values {}",
371            dense.len(),
372            out_values.len(),
373            len
374        );
375
376        // SAFETY: `Arch::*` are target-feature kernels (module invariant). The
377        // assert above gives every windowed load/store `[i, i+LANE_COUNT)` room
378        // within `dense`, `out_values`, `values`, and `mask` for `i < simd_len`.
379        let mut i = 0usize;
380        unsafe {
381            let zero_vec = Arch::zero();
382            while i < simd_len {
383                let msk = Arch::mask_from_bools(&d.mask[i..i + lane_count]);
384                let res_vec = Arch::masked_mul(
385                    Arch::load_unaligned(d.values[i..].as_ptr()),
386                    Arch::load_unaligned(dense[i..].as_ptr()),
387                    msk,
388                    zero_vec,
389                );
390                Arch::store_unaligned(out_values[i..].as_mut_ptr(), res_vec);
391                i += lane_count;
392            }
393        }
394        while i < len {
395            if d.mask[i] {
396                out_values[i] = d.values[i] * dense[i];
397            } else {
398                out_values[i] = T::ZERO;
399            }
400            i += 1;
401        }
402    }
403}