Skip to main content

hermes_simd_core/ops/
reduction.rs

1//! Horizontal reduction operation strategies.
2//!
3//! `ReductionOp<T>` is a sealed ZST trait; implementors define how a vector accumulator
4//! is updated and how the final scalar is extracted. All methods are `#[inline(always)]`
5//! and carry no branching — DCE eliminates unused strategies entirely.
6
7use crate::kernel::SimdKernel;
8use crate::scalar::Scalar;
9
10// ---------------------------------------------------------------------------
11// ReductionOp — single-operand fold across lanes
12// ---------------------------------------------------------------------------
13
14/// Sealed ZST trait for SIMD horizontal reduction strategies.
15///
16/// Implementors define how a vector accumulator is updated (`accumulate`) and how
17/// the final scalar result is extracted (`finalize`). Both methods are `#[inline(always)]`
18/// and carry no branching — DCE eliminates unused strategies entirely.
19///
20/// # Identity Element
21///
22/// `identity_scalar()` returns the reduction identity (0 for Sum, `T::MAX_VALUE` for Min,
23/// `T::MIN_VALUE` for Max). It is used for empty-slice fast paths and for combining the
24/// scalar tail with the SIMD result via `scalar_combine`.
25pub trait ReductionOp<T: Scalar>: crate::private::Sealed + Copy + 'static {
26    /// Merge a new data vector `v` into accumulator `acc`.
27    ///
28    /// # Safety
29    /// Processor must support the target feature of `Arch`.
30    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector;
31
32    /// FMA-aware pairwise accumulation: `acc = fuse(acc, a, b)` where `fuse` may use
33    /// a single fused multiply-add instruction rather than a separate `mul` + `accumulate`.
34    ///
35    /// Default: `Self::accumulate(acc, Arch::mul(a, b))` — a correct two-instruction fallback.
36    /// Override this for `Dot` and similar operations that can exploit `Arch::fmadd`.
37    ///
38    /// # Safety
39    /// Processor must support the target feature of `Arch`.
40    #[inline(always)]
41    unsafe fn fma_pair_accumulate<Arch: SimdKernel<T>>(
42        acc: Arch::Vector,
43        a: Arch::Vector,
44        b: Arch::Vector,
45    ) -> Arch::Vector {
46        Self::accumulate::<Arch>(acc, Arch::mul(a, b))
47    }
48
49    /// Reduce the final accumulator to a scalar.
50    ///
51    /// # Safety
52    /// Processor must support the target feature of `Arch`.
53    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T;
54
55    /// The identity element for this reduction as a scalar.
56    ///
57    /// Default: must be overridden. `Sum` returns `T::ZERO`, `Min` returns `T::MAX_VALUE`,
58    /// `Max` returns `T::MIN_VALUE`.
59    fn identity_scalar() -> T;
60
61    /// Combine two scalar partial results using this reduction.
62    ///
63    /// For `Sum`: addition. For `Min`: `min_scalar`. For `Max`: `max_scalar`.
64    fn scalar_combine(a: T, b: T) -> T;
65
66    /// Accumulate a single scalar element `elem` into a scalar accumulator `acc`.
67    ///
68    /// Default: delegates to `Self::scalar_combine(acc, elem)`.
69    /// Override this for reductions whose SIMD `accumulate` applies a per-element transform
70    /// (e.g. `SquaredSum` applies `elem * elem` before adding). The scalar tail path uses this
71    /// method instead of `scalar_combine` to maintain correctness for slices shorter than
72    /// `Arch::LANE_COUNT`.
73    #[inline(always)]
74    fn scalar_accumulate(acc: T, elem: T) -> T {
75        Self::scalar_combine(acc, elem)
76    }
77
78    /// Splat the identity element into a vector register.
79    ///
80    /// Default: `Arch::splat(Self::identity_scalar())`. Backends may override.
81    ///
82    /// # Safety
83    /// Processor must support the target feature of `Arch`.
84    #[inline(always)]
85    unsafe fn identity_vector<Arch: SimdKernel<T>>() -> Arch::Vector {
86        Arch::splat(Self::identity_scalar())
87    }
88
89    /// Per-element lane transform applied before combining (identity by default).
90    ///
91    /// Reductions with a per-element transform (`AbsSum` applies `abs`) override
92    /// this so the reduce loop can seed unrolled accumulators with
93    /// `transform_vector(load(...))` instead of raw loads.
94    ///
95    /// # Safety
96    /// Processor must support the target feature of `Arch`.
97    #[inline(always)]
98    unsafe fn transform_vector<Arch: SimdKernel<T>>(v: Arch::Vector) -> Arch::Vector {
99        v
100    }
101
102    /// Merge two partial accumulators WITHOUT the per-element transform.
103    ///
104    /// `accumulate` is `combine_vectors(acc, transform_vector(v))`; the reduce
105    /// loop's cross-accumulator merge must use this method, because the
106    /// partials are already transformed. Default delegates to `accumulate`,
107    /// which is correct exactly when `transform_vector` is the identity —
108    /// transform-bearing ops must override both.
109    ///
110    /// # Safety
111    /// Processor must support the target feature of `Arch`.
112    #[inline(always)]
113    unsafe fn combine_vectors<Arch: SimdKernel<T>>(
114        a: Arch::Vector,
115        b: Arch::Vector,
116    ) -> Arch::Vector {
117        Self::accumulate::<Arch>(a, b)
118    }
119}
120
121// ---------------------------------------------------------------------------
122// Concrete reduction ZSTs
123// ---------------------------------------------------------------------------
124
125/// Sum reduction: accumulate by adding vectors, finalize with `sum_reduce`.
126///
127/// `view.reduce(Sum)` is equivalent to `view.sum()`.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct Sum;
130
131/// Dot-product pairwise operation: multiply two vectors lane-wise.
132///
133/// Use with `zip_reduce`: `a.zip_reduce(&b, Dot)` equals `a.dot(&b)`.
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct Dot;
136
137/// Horizontal minimum reduction: returns the smallest element.
138///
139/// Identity element: `T::MAX_VALUE` (positive infinity for floats, `i32::MAX` for integers).
140/// Use with `view.reduce(Min)`.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub struct Min;
143
144/// Horizontal maximum reduction: returns the largest element.
145///
146/// Identity element: `T::MIN_VALUE` (negative infinity for floats, `i32::MIN` for integers).
147/// Use with `view.reduce(Max)`.
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub struct Max;
150
151/// Absolute-sum reduction: computes `Σ |data[i]|` (the L1 norm accumulator).
152///
153/// Identity element is `T::ZERO`. The per-element transform is `abs`, applied
154/// lane-wise before the additive fold (`scalar_accumulate` mirrors it on the
155/// tail). Signed-integer `abs` follows `T::abs` semantics, including its
156/// behavior at `T::MIN`.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub struct AbsSum;
159
160/// Absolute-max reduction: computes `max |data[i]|` (the ∞-norm accumulator).
161///
162/// Identity element is `T::ZERO`, which is also the mathematically correct
163/// result for an empty slice since every magnitude is non-negative.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct AbsMax;
166
167/// Multiplicative reduction: computes `∏ data[i]`.
168///
169/// Identity element is `T::ONE`. Uses SIMD `mul` to accumulate lane products, then
170/// reduces horizontally via a scalar lane-extraction loop (no `prod_reduce` on
171/// `SimdKernel` — the hardware does not expose one universally).
172///
173/// # Zero-Cost Guarantee
174///
175/// `size_of::<Product>() == 0`. All branching over `Product` vs other ops is
176/// eliminated via DCE during monomorphization.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub struct Product;
179
180// ---------------------------------------------------------------------------
181// Sealing impls
182// ---------------------------------------------------------------------------
183
184impl crate::private::Sealed for Sum {}
185impl crate::private::Sealed for Dot {}
186impl crate::private::Sealed for Min {}
187impl crate::private::Sealed for Max {}
188impl crate::private::Sealed for AbsSum {}
189impl crate::private::Sealed for AbsMax {}
190impl crate::private::Sealed for Product {}
191
192// ---------------------------------------------------------------------------
193// ReductionOp impls
194// ---------------------------------------------------------------------------
195
196impl<T: Scalar> ReductionOp<T> for Sum {
197    #[inline(always)]
198    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
199        Arch::add(acc, v)
200    }
201    #[inline(always)]
202    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
203        Arch::sum_reduce(acc)
204    }
205    #[inline(always)]
206    fn identity_scalar() -> T {
207        T::ZERO
208    }
209    #[inline(always)]
210    fn scalar_combine(a: T, b: T) -> T {
211        a + b
212    }
213}
214
215impl<T: Scalar> ReductionOp<T> for Dot {
216    /// Dot accumulation: `acc = fmadd(a, b, acc)` — called with the pairwise product vector.
217    ///
218    /// The `zip_reduce` loop computes `v = mul(a_chunk, b_chunk)` then calls `accumulate(acc, v)`.
219    #[inline(always)]
220    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
221        // v already holds a[i]*b[i] product from the zip loop; just add to accumulator.
222        Arch::add(acc, v)
223    }
224
225    /// FMA-accelerated pairwise accumulation for dot product.
226    ///
227    /// Overrides the default `accumulate(acc, mul(a, b))` two-instruction sequence
228    /// with a single `fmadd(a, b, acc)` when the architecture supports it.
229    #[inline(always)]
230    unsafe fn fma_pair_accumulate<Arch: SimdKernel<T>>(
231        acc: Arch::Vector,
232        a: Arch::Vector,
233        b: Arch::Vector,
234    ) -> Arch::Vector {
235        Arch::fmadd(a, b, acc)
236    }
237
238    #[inline(always)]
239    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
240        Arch::sum_reduce(acc)
241    }
242    #[inline(always)]
243    fn identity_scalar() -> T {
244        T::ZERO
245    }
246    #[inline(always)]
247    fn scalar_combine(a: T, b: T) -> T {
248        a + b
249    }
250}
251
252impl<T: Scalar> ReductionOp<T> for Min {
253    #[inline(always)]
254    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
255        Arch::min(acc, v)
256    }
257    #[inline(always)]
258    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
259        Arch::min_reduce(acc)
260    }
261    #[inline(always)]
262    fn identity_scalar() -> T {
263        T::MAX_VALUE
264    }
265    #[inline(always)]
266    fn scalar_combine(a: T, b: T) -> T {
267        a.min_scalar(b)
268    }
269}
270
271impl<T: Scalar> ReductionOp<T> for Max {
272    #[inline(always)]
273    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
274        Arch::max(acc, v)
275    }
276    #[inline(always)]
277    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
278        Arch::max_reduce(acc)
279    }
280    #[inline(always)]
281    fn identity_scalar() -> T {
282        T::MIN_VALUE
283    }
284    #[inline(always)]
285    fn scalar_combine(a: T, b: T) -> T {
286        a.max_scalar(b)
287    }
288}
289
290impl<T: Scalar> ReductionOp<T> for AbsSum {
291    #[inline(always)]
292    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
293        Arch::add(acc, Arch::abs(v))
294    }
295    #[inline(always)]
296    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
297        Arch::sum_reduce(acc)
298    }
299    #[inline(always)]
300    fn identity_scalar() -> T {
301        T::ZERO
302    }
303    #[inline(always)]
304    fn scalar_combine(a: T, b: T) -> T {
305        a + b
306    }
307    #[inline(always)]
308    fn scalar_accumulate(acc: T, elem: T) -> T {
309        acc + elem.abs()
310    }
311    #[inline(always)]
312    unsafe fn transform_vector<Arch: SimdKernel<T>>(v: Arch::Vector) -> Arch::Vector {
313        Arch::abs(v)
314    }
315    #[inline(always)]
316    unsafe fn combine_vectors<Arch: SimdKernel<T>>(
317        a: Arch::Vector,
318        b: Arch::Vector,
319    ) -> Arch::Vector {
320        Arch::add(a, b)
321    }
322}
323
324impl<T: Scalar> ReductionOp<T> for AbsMax {
325    #[inline(always)]
326    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
327        Arch::max(acc, Arch::abs(v))
328    }
329    #[inline(always)]
330    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
331        Arch::max_reduce(acc)
332    }
333    #[inline(always)]
334    fn identity_scalar() -> T {
335        T::ZERO
336    }
337    #[inline(always)]
338    fn scalar_combine(a: T, b: T) -> T {
339        a.max_scalar(b)
340    }
341    #[inline(always)]
342    fn scalar_accumulate(acc: T, elem: T) -> T {
343        acc.max_scalar(elem.abs())
344    }
345    #[inline(always)]
346    unsafe fn transform_vector<Arch: SimdKernel<T>>(v: Arch::Vector) -> Arch::Vector {
347        Arch::abs(v)
348    }
349    #[inline(always)]
350    unsafe fn combine_vectors<Arch: SimdKernel<T>>(
351        a: Arch::Vector,
352        b: Arch::Vector,
353    ) -> Arch::Vector {
354        Arch::max(a, b)
355    }
356}
357
358impl<T: Scalar> ReductionOp<T> for Product {
359    /// Accumulate: `acc = acc * v` (lane-wise multiply).
360    ///
361    /// # Safety
362    /// Processor must support the target feature of `Arch`.
363    #[inline(always)]
364    unsafe fn accumulate<Arch: SimdKernel<T>>(acc: Arch::Vector, v: Arch::Vector) -> Arch::Vector {
365        Arch::mul(acc, v)
366    }
367
368    /// Finalize: store the accumulated product vector and reduce over lanes.
369    ///
370    /// There is no universal `prod_reduce` intrinsic, so this falls back to:
371    /// 1. Store the `LANE_COUNT` partial products into a local stack array.
372    /// 2. Scalar-fold with `*`.
373    ///
374    /// For Scalar arch this is always a single-element store + identity.
375    ///
376    /// # Safety
377    /// Processor must support the target feature of `Arch`.
378    #[inline(always)]
379    unsafe fn finalize<Arch: SimdKernel<T>>(acc: Arch::Vector) -> T {
380        // Compile-time bound (per backend) against the shared scalar-fallback
381        // buffer SSOT, replacing a debug-only runtime assert.
382        const { <Arch as SimdKernel<T>>::LANE_BOUND_CHECK };
383        let mut buf = [T::ZERO; crate::kernel::MAX_SIMD_LANES];
384        Arch::store_unaligned(buf.as_mut_ptr(), acc);
385        let mut result = T::ONE;
386        for i in 0..Arch::LANE_COUNT {
387            result = result * buf[i];
388        }
389        result
390    }
391
392    #[inline(always)]
393    fn identity_scalar() -> T {
394        T::ONE
395    }
396
397    #[inline(always)]
398    fn scalar_combine(a: T, b: T) -> T {
399        a * b
400    }
401}