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}