Skip to main content

hermes_simd_core/view/
ops.rs

1//! Elementwise and transform operations over [`SimdView`](crate::view::SimdView).
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 crate::align::Alignment;
15use crate::arch::SimdArch;
16use crate::execution::ExecutionMode;
17use crate::kernel::SimdKernel;
18use crate::ops::ElementOp;
19use crate::scalar::Scalar;
20use crate::view::{SimdError, SimdView};
21
22/// Output-size threshold (bytes) at or above which [`SimdView::zip_into`]
23/// switches to non-temporal (cache-bypassing) stores on backends that support
24/// them.
25///
26/// Set to 8 MiB — past every consumer L2 — so streaming engages only for
27/// outputs large enough that the read-for-ownership it avoids is not offset by
28/// lost cache residency (a normal store would keep a smaller result hot for
29/// reuse). Measured 1.71× at 64 MiB out-of-LLC (see `streaming_bench`).
30const NT_STORE_MIN_BYTES: usize = 8 * 1024 * 1024;
31
32impl<'a, T: 'a, Arch: SimdArch + SimdKernel<T>, Align: Alignment, Mode: ExecutionMode, Ref: 'a>
33    SimdView<'a, T, Arch, Align, Mode, Ref>
34where
35    T: Scalar,
36{
37    /// Sum all elements in the view.
38    ///
39    /// Iterates in unrolled chunks of `Arch::LANE_COUNT * Arch::UNROLL_FACTOR` elements,
40    /// accumulating into multiple registers in parallel to break loop dependencies.
41    #[inline(always)]
42    pub fn sum(&self) -> T {
43        let data = self.as_slice();
44        let len = data.len();
45        let lane_count = Arch::LANE_COUNT;
46        let unroll_factor = Arch::UNROLL_FACTOR;
47        let chunk_size = lane_count * unroll_factor;
48        let unrolled_simd_len = (len / chunk_size) * chunk_size;
49        let simd_len = (len / lane_count) * lane_count;
50        let mut ptr = data.as_ptr();
51
52        let accumulator = unsafe {
53            if unrolled_simd_len > 0 {
54                let load = |p| {
55                    if crate::align::is_aligned_for_arch::<Arch, Align>() {
56                        Arch::load_aligned(p)
57                    } else {
58                        Arch::load_unaligned(p)
59                    }
60                };
61
62                let mut acc0 = load(ptr);
63                let mut acc1 = load(ptr.add(lane_count));
64                let mut acc2 = load(ptr.add(lane_count * 2));
65                let mut acc3 = load(ptr.add(lane_count * 3));
66                ptr = ptr.add(chunk_size);
67
68                for _ in 1..(unrolled_simd_len / chunk_size) {
69                    let v0 = load(ptr);
70                    let v1 = load(ptr.add(lane_count));
71                    let v2 = load(ptr.add(lane_count * 2));
72                    let v3 = load(ptr.add(lane_count * 3));
73
74                    acc0 = Arch::add(acc0, v0);
75                    acc1 = Arch::add(acc1, v1);
76                    acc2 = Arch::add(acc2, v2);
77                    acc3 = Arch::add(acc3, v3);
78
79                    ptr = ptr.add(chunk_size);
80                }
81
82                let mut acc = Arch::add(acc0, acc1);
83                acc = Arch::add(acc, acc2);
84                acc = Arch::add(acc, acc3);
85                Some(acc)
86            } else {
87                None
88            }
89        };
90
91        let mut acc = if let Some(a) = accumulator {
92            a
93        } else {
94            unsafe { Arch::zero() }
95        };
96
97        // Middle SIMD loop for elements that didn't fit into the unrolled loop
98        unsafe {
99            let mut middle_ptr = data.as_ptr().add(unrolled_simd_len);
100            for _ in 0..((simd_len - unrolled_simd_len) / lane_count) {
101                let val = if crate::align::is_aligned_for_arch::<Arch, Align>() {
102                    Arch::load_aligned(middle_ptr)
103                } else {
104                    Arch::load_unaligned(middle_ptr)
105                };
106                acc = Arch::add(acc, val);
107                middle_ptr = middle_ptr.add(lane_count);
108            }
109        }
110
111        let mut total = unsafe { Arch::sum_reduce(acc) };
112
113        // Scalar tail loop
114        for i in simd_len..len {
115            total += data[i];
116        }
117
118        total
119    }
120
121    /// Compute the dot product between this view and another view of the same architecture and alignment.
122    ///
123    /// # Errors
124    /// Returns `SimdError::LengthMismatch` if the view lengths are not identical.
125    #[inline(always)]
126    pub fn dot<ORef>(
127        &self,
128        other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
129    ) -> Result<T, SimdError>
130    where
131        ORef: 'a,
132    {
133        super::check_lengths_equal(self.len(), other.len())?;
134
135        let len = self.len();
136        let lane_count = Arch::LANE_COUNT;
137        let unroll_factor = Arch::UNROLL_FACTOR;
138        let chunk_size = lane_count * unroll_factor;
139        let unrolled_simd_len = (len / chunk_size) * chunk_size;
140
141        let mut ptr1 = self.as_slice().as_ptr();
142        let mut ptr2 = other.as_slice().as_ptr();
143
144        let accumulator = unsafe {
145            if unrolled_simd_len > 0 {
146                let load = |p| {
147                    if crate::align::is_aligned_for_arch::<Arch, Align>() {
148                        Arch::load_aligned(p)
149                    } else {
150                        Arch::load_unaligned(p)
151                    }
152                };
153
154                let v0_1 = load(ptr1);
155                let v0_2 = load(ptr2);
156                let mut acc0 = Arch::mul(v0_1, v0_2);
157
158                let v1_1 = load(ptr1.add(lane_count));
159                let v1_2 = load(ptr2.add(lane_count));
160                let mut acc1 = Arch::mul(v1_1, v1_2);
161
162                let v2_1 = load(ptr1.add(lane_count * 2));
163                let v2_2 = load(ptr2.add(lane_count * 2));
164                let mut acc2 = Arch::mul(v2_1, v2_2);
165
166                let v3_1 = load(ptr1.add(lane_count * 3));
167                let v3_2 = load(ptr2.add(lane_count * 3));
168                let mut acc3 = Arch::mul(v3_1, v3_2);
169
170                ptr1 = ptr1.add(chunk_size);
171                ptr2 = ptr2.add(chunk_size);
172
173                for _ in 1..(unrolled_simd_len / chunk_size) {
174                    let v0_1 = load(ptr1);
175                    let v0_2 = load(ptr2);
176                    acc0 = Arch::fmadd(v0_1, v0_2, acc0);
177
178                    let v1_1 = load(ptr1.add(lane_count));
179                    let v1_2 = load(ptr2.add(lane_count));
180                    acc1 = Arch::fmadd(v1_1, v1_2, acc1);
181
182                    let v2_1 = load(ptr1.add(lane_count * 2));
183                    let v2_2 = load(ptr2.add(lane_count * 2));
184                    acc2 = Arch::fmadd(v2_1, v2_2, acc2);
185
186                    let v3_1 = load(ptr1.add(lane_count * 3));
187                    let v3_2 = load(ptr2.add(lane_count * 3));
188                    acc3 = Arch::fmadd(v3_1, v3_2, acc3);
189
190                    ptr1 = ptr1.add(chunk_size);
191                    ptr2 = ptr2.add(chunk_size);
192                }
193
194                let mut acc = Arch::add(acc0, acc1);
195                acc = Arch::add(acc, acc2);
196                acc = Arch::add(acc, acc3);
197                Some(acc)
198            } else {
199                None
200            }
201        };
202
203        let simd_len = (len / lane_count) * lane_count;
204
205        // Middle SIMD loop for elements that didn't fit into the unrolled loop.
206        // Continue accumulating into the *vector* register via `fmadd` and reduce
207        // to scalar ONCE at the end — rather than a horizontal `sum_reduce` per
208        // lane group (which serialized the loop on the ~5-7-cycle reduction
209        // latency and dominated small/odd-length dots, e.g. the bidiagonal-SVD
210        // reflector applies).
211        let mut acc_vec = accumulator;
212        unsafe {
213            let load = |p| {
214                if crate::align::is_aligned_for_arch::<Arch, Align>() {
215                    Arch::load_aligned(p)
216                } else {
217                    Arch::load_unaligned(p)
218                }
219            };
220            let mut middle_ptr1 = self.as_slice().as_ptr().add(unrolled_simd_len);
221            let mut middle_ptr2 = other.as_slice().as_ptr().add(unrolled_simd_len);
222            for _ in 0..((simd_len - unrolled_simd_len) / lane_count) {
223                let v1 = load(middle_ptr1);
224                let v2 = load(middle_ptr2);
225                acc_vec = Some(match acc_vec {
226                    Some(a) => Arch::fmadd(v1, v2, a),
227                    None => Arch::mul(v1, v2),
228                });
229                middle_ptr1 = middle_ptr1.add(lane_count);
230                middle_ptr2 = middle_ptr2.add(lane_count);
231            }
232        }
233        let mut total = match acc_vec {
234            Some(acc) => unsafe { Arch::sum_reduce(acc) },
235            None => T::ZERO,
236        };
237
238        // Scalar tail loop
239        let s_slice = self.as_slice();
240        let o_slice = other.as_slice();
241        for i in simd_len..len {
242            total += s_slice[i] * o_slice[i];
243        }
244
245        Ok(total)
246    }
247
248    /// Multiply elementwise with another view and write the output to a mutable slice.
249    ///
250    /// # Errors
251    /// Returns `SimdError::LengthMismatch` if operand lengths do not match, or
252    /// `SimdError::InsufficientOutputLength` if the output slice is smaller than the input view.
253    #[inline(always)]
254    pub fn elementwise_mul<ORef>(
255        &self,
256        other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
257        out: &mut [T],
258    ) -> Result<(), SimdError>
259    where
260        ORef: 'a,
261    {
262        super::check_lengths_equal(self.len(), other.len())?;
263        super::check_output_length(self.len(), out.len())?;
264
265        let len = self.len();
266        let lane_count = Arch::LANE_COUNT;
267        let simd_len = (len / lane_count) * lane_count;
268
269        let mut ptr1 = self.as_slice().as_ptr();
270        let mut ptr2 = other.as_slice().as_ptr();
271        let mut ptr_out = out.as_mut_ptr();
272
273        unsafe {
274            let load = |p| {
275                if crate::align::is_aligned_for_arch::<Arch, Align>() {
276                    Arch::load_aligned(p)
277                } else {
278                    Arch::load_unaligned(p)
279                }
280            };
281
282            let store = |p, val| {
283                let is_out_aligned = crate::align::is_aligned_for_arch::<Arch, Align>()
284                    && (p as usize) % Align::ALIGN_BYTES == 0;
285
286                if is_out_aligned {
287                    Arch::store_aligned(p, val);
288                } else {
289                    Arch::store_unaligned(p, val);
290                }
291            };
292
293            for _ in 0..(simd_len / lane_count) {
294                let v1 = load(ptr1);
295                let v2 = load(ptr2);
296                let res = Arch::mul(v1, v2);
297                store(ptr_out, res);
298
299                ptr1 = ptr1.add(lane_count);
300                ptr2 = ptr2.add(lane_count);
301                ptr_out = ptr_out.add(lane_count);
302            }
303        }
304
305        let s_slice = self.as_slice();
306        let o_slice = other.as_slice();
307        for i in simd_len..len {
308            out[i] = s_slice[i] * o_slice[i];
309        }
310
311        Ok(())
312    }
313
314    /// Pairwise elementwise operation on `self` and `other`, writing results to `out`.
315    ///
316    /// The SIMD vectorized loop covers `floor(len / LANE_COUNT) * LANE_COUNT` elements.
317    /// The scalar tail handles the remaining elements element-by-element.
318    ///
319    /// # Errors
320    /// Returns `SimdError::LengthMismatch` if operand lengths do not match, or
321    /// `SimdError::InsufficientOutputLength` if `out.len() < self.len()`.
322    #[inline(always)]
323    pub fn zip_into<ORef, Op>(
324        &self,
325        other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
326        out: &mut [T],
327        op: Op,
328    ) -> Result<(), SimdError>
329    where
330        ORef: 'a,
331        Op: ElementOp<T>,
332    {
333        super::check_lengths_equal(self.len(), other.len())?;
334        super::check_output_length(self.len(), out.len())?;
335
336        let len = self.len();
337        let lane_count = Arch::LANE_COUNT;
338        let simd_len = (len / lane_count) * lane_count;
339
340        // Route large write-only outputs through non-temporal stores: the write
341        // bypasses the cache, avoiding the read-for-ownership (write-allocate)
342        // traffic that dominates an out-of-LLC elementwise write (measured 1.71×
343        // on AVX2 f32; see `streaming_bench`). Gated so it engages only when the
344        // output clearly exceeds cache — below that, the RFO the NT store avoids
345        // is offset by the cache residency a normal store would keep, so the
346        // conservative path is a net win or wash and never a regression.
347        if Arch::SUPPORTS_NT_STORE
348            && len.saturating_mul(core::mem::size_of::<T>()) >= NT_STORE_MIN_BYTES
349        {
350            // SAFETY: lengths validated above; `zip_into_streaming` peels the
351            // output to the NT-store alignment and issues the write barrier.
352            return unsafe { self.zip_into_streaming(other, out, op, len, simd_len) };
353        }
354
355        let ptr_self = self.as_slice().as_ptr();
356        let ptr_other = other.as_slice().as_ptr();
357        let ptr_out = out.as_mut_ptr();
358
359        unsafe {
360            let load = |p| {
361                if crate::align::is_aligned_for_arch::<Arch, Align>() {
362                    Arch::load_aligned(p)
363                } else {
364                    Arch::load_unaligned(p)
365                }
366            };
367
368            let store = |p, val| {
369                let is_out_aligned = crate::align::is_aligned_for_arch::<Arch, Align>()
370                    && (p as usize) % Align::ALIGN_BYTES == 0;
371                if is_out_aligned {
372                    Arch::store_aligned(p, val);
373                } else {
374                    Arch::store_unaligned(p, val);
375                }
376            };
377
378            for i in (0..simd_len).step_by(lane_count) {
379                let va = load(ptr_self.add(i));
380                let vb = load(ptr_other.add(i));
381                let vr = op.apply::<Arch>(va, vb);
382                store(ptr_out.add(i), vr);
383            }
384        }
385
386        let s_slice = self.as_slice();
387        let o_slice = other.as_slice();
388        for i in simd_len..len {
389            out[i] = op.apply_scalar(s_slice[i], o_slice[i]);
390        }
391
392        Ok(())
393    }
394
395    /// Non-temporal (cache-bypassing) variant of the [`zip_into`](Self::zip_into)
396    /// store loop for out-of-LLC outputs. The result is **byte-identical** to the
397    /// regular path — only the store instruction changes, not the arithmetic.
398    ///
399    /// `out` is prefix-peeled to `LANE_COUNT · size_of::<T>()`-byte alignment
400    /// (NT stores fault otherwise) with scalar ops, the aligned middle is
401    /// streamed, the tail is scalar, and [`stream_write_barrier`] orders the
402    /// weakly ordered stores before the caller reads `out`.
403    ///
404    /// # Safety
405    /// `Arch::SUPPORTS_NT_STORE` must hold; `self`/`other`/`out` share `len`
406    /// (validated by the caller); `simd_len == (len / LANE_COUNT) · LANE_COUNT`.
407    ///
408    /// [`stream_write_barrier`]: crate::kernel::SimdKernel::stream_write_barrier
409    #[inline]
410    unsafe fn zip_into_streaming<ORef, Op>(
411        &self,
412        other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
413        out: &mut [T],
414        op: Op,
415        len: usize,
416        _simd_len: usize,
417    ) -> Result<(), SimdError>
418    where
419        ORef: 'a,
420        Op: ElementOp<T>,
421    {
422        let lane_count = Arch::LANE_COUNT;
423        let s = self.as_slice();
424        let o = other.as_slice();
425        let ptr_self = s.as_ptr();
426        let ptr_other = o.as_ptr();
427        let ptr_out = out.as_mut_ptr();
428
429        // Elements to peel so the streamed region starts on a
430        // `LANE_COUNT · size_of::<T>()` boundary. Slices are aligned to at least
431        // `size_of::<T>()`, so `addr % align_bytes` is a whole number of
432        // elements and the division is exact.
433        let align_bytes = lane_count * core::mem::size_of::<T>();
434        let addr = ptr_out as usize;
435        let head = ((align_bytes - (addr % align_bytes)) % align_bytes) / core::mem::size_of::<T>();
436        let head = head.min(len);
437
438        for i in 0..head {
439            out[i] = op.apply_scalar(s[i], o[i]);
440        }
441
442        let mid_end = head + ((len - head) / lane_count) * lane_count;
443        let mut i = head;
444        while i < mid_end {
445            // SAFETY: `i < mid_end ≤ len`; loads are unaligned; the store target
446            // `ptr_out + i` is aligned to `align_bytes` by construction of `head`.
447            let va = Arch::load_unaligned(ptr_self.add(i));
448            let vb = Arch::load_unaligned(ptr_other.add(i));
449            let vr = op.apply::<Arch>(va, vb);
450            Arch::store_streaming(ptr_out.add(i), vr);
451            i += lane_count;
452        }
453
454        Arch::stream_write_barrier();
455
456        for i in mid_end..len {
457            out[i] = op.apply_scalar(s[i], o[i]);
458        }
459
460        Ok(())
461    }
462
463    /// Pairwise elementwise operation on `self` and `other`, returning a new `AlignedVec<T, Align>`.
464    ///
465    /// One allocation for the output buffer. Monomorphizes per `(T, Arch, Align, Op)` — the
466    /// compiler generates the specialization most efficient for the target ISA and alignment.
467    ///
468    /// # Errors
469    /// Returns `SimdError::LengthMismatch` if operand lengths do not match.
470    pub fn zip_transform<ORef, Op>(
471        &self,
472        other: &SimdView<'_, T, Arch, Align, Mode, ORef>,
473        op: Op,
474    ) -> Result<crate::vec::AlignedVec<T, Align>, SimdError>
475    where
476        ORef: 'a,
477        Op: ElementOp<T>,
478    {
479        super::check_lengths_equal(self.len(), other.len())?;
480        let len = self.len();
481        let mut out = crate::vec::AlignedVec::with_capacity(len);
482        // SAFETY: we write all `len` elements below via `zip_into`.
483        unsafe {
484            out.set_len(len);
485        }
486        self.zip_into(other, out.as_mut_slice(), op)?;
487        Ok(out)
488    }
489}