Skip to main content

strided_basic/
reduce_view.rs

1//! Reduce operations on dynamic-rank strided views.
2
3use crate::kernel::{
4    build_plan_fused, ensure_same_shape, for_each_inner_block_preordered, same_contiguous_layout,
5    sequential_contiguous_layout, total_len,
6};
7use crate::simd;
8use crate::view::{col_major_strides, StridedArray, StridedView, StridedViewMut};
9use crate::{Exec, Result, StridedError};
10use core::num::NonZeroUsize;
11use std::ops::Range;
12use strided_view::ElementOp;
13
14use crate::cost::KernelClass;
15use crate::fuse::compute_costs;
16use crate::inline::InlineVec;
17use crate::threading::{
18    for_each_inner_block_with_offsets, is_parallel, mapreduce_threaded_fold, SendPtr,
19};
20
21/// Number of independent accumulators of [`fold_contiguous`].
22const FOLD_LANES: usize = 16;
23
24/// Minimum outputs along a unit-stride kept axis for the column sweep of
25/// [`reduce_axis`].
26const AXIS_SWEEP_MIN_LEN: usize = 16;
27
28/// Byte budget of one output block of the column sweep of [`reduce_axis`].
29const AXIS_SWEEP_BLOCK_BYTES: usize = 64 * 1024;
30
31/// Combines the lanes of [`fold_contiguous`] left to right, after `init`.
32///
33/// Not inlined on purpose: the lane array crosses the call by memory, which
34/// keeps the lanes of the surrounding loop in vector registers. Inlined, the
35/// final fold makes the loop half-extract and re-insert every lane (on x86-64
36/// AVX2 `f64` max: 18 shuffles per 16 elements) although it runs once.
37#[inline(never)]
38fn fold_lanes<U, R>(lanes: [U; FOLD_LANES], init: U, reduce_fn: &R) -> U
39where
40    R: Fn(U, U) -> U,
41{
42    lanes.into_iter().fold(init, reduce_fn)
43}
44
45/// Folds a contiguous run with [`FOLD_LANES`] independent accumulators.
46///
47/// Lane `i` starts from element `i` and folds elements `i + FOLD_LANES`,
48/// `i + 2 * FOLD_LANES`, ... in order; the remainder is folded into the first
49/// lanes. `init` is combined exactly once, before lane 0, and the lanes are
50/// then combined left to right by [`fold_lanes`]. Independent lanes let the
51/// compiler keep several vector accumulators in flight instead of one serial
52/// dependency chain, which is what makes a closure-based sum or max approach
53/// the speed of a hand written lane loop.
54#[inline(always)]
55fn fold_contiguous<T, Op, M, R, U>(src: &[T], init: U, map_fn: &M, reduce_fn: &R) -> U
56where
57    T: Copy,
58    Op: ElementOp<T>,
59    M: Fn(T) -> U,
60    R: Fn(U, U) -> U,
61    U: Clone,
62{
63    if src.len() < 2 * FOLD_LANES {
64        let mut acc = init;
65        for &value in src {
66            acc = reduce_fn(acc, map_fn(Op::apply(value)));
67        }
68        return acc;
69    }
70    let (head, rest) = src.split_at(FOLD_LANES);
71    let mut lanes: [U; FOLD_LANES] = core::array::from_fn(|lane| map_fn(Op::apply(head[lane])));
72    let mut chunks = rest.chunks_exact(FOLD_LANES);
73    for chunk in chunks.by_ref() {
74        for (lane, &value) in lanes.iter_mut().zip(chunk) {
75            *lane = reduce_fn(lane.clone(), map_fn(Op::apply(value)));
76        }
77    }
78    for (lane, &value) in lanes.iter_mut().zip(chunks.remainder()) {
79        *lane = reduce_fn(lane.clone(), map_fn(Op::apply(value)));
80    }
81    fold_lanes(lanes, init, reduce_fn)
82}
83
84/// Folds `len` elements starting at `ptr` with step `stride`.
85///
86/// # Safety
87/// `ptr.offset(i * stride)` must be a readable element for every `i < len`.
88#[inline(always)]
89unsafe fn fold_run<T, Op, M, R, U>(
90    ptr: *const T,
91    stride: isize,
92    len: usize,
93    init: U,
94    map_fn: &M,
95    reduce_fn: &R,
96) -> U
97where
98    T: Copy,
99    Op: ElementOp<T>,
100    M: Fn(T) -> U,
101    R: Fn(U, U) -> U,
102    U: Clone,
103{
104    if stride == 1 {
105        // SAFETY: the caller proves `len` readable unit-stride elements.
106        let run = unsafe { std::slice::from_raw_parts(ptr, len) };
107        return fold_contiguous::<T, Op, M, R, U>(run, init, map_fn, reduce_fn);
108    }
109    let mut acc = init;
110    let mut cursor = ptr;
111    for index in 0..len {
112        // SAFETY: the caller proves every element of the run is readable.
113        acc = reduce_fn(acc, map_fn(Op::apply(unsafe { *cursor })));
114        if index + 1 < len {
115            cursor = cursor.wrapping_offset(stride);
116        }
117    }
118    acc
119}
120
121/// Full reduction with map function: `reduce(init, op, map.(src))`.
122///
123/// # Evaluation order
124///
125/// The result is independent of the pool when the call runs with a serial
126/// token ([`Exec::serial`]) or a budget of one: only serial branches run, and
127/// their fold order is fixed by the source layout. With a larger budget the
128/// fold is split over the budget, so the result is repeatable for a fixed
129/// shape, layout and budget but can differ between budgets.
130///
131/// In neither case is this a strict left to right fold: a contiguous source is
132/// folded with sixteen independent lanes (see the crate source for the exact
133/// lane assignment), non-contiguous sources are traversed in a cache-friendly
134/// loop order, and parallel execution folds per-task chunks independently
135/// and combines them in a tree. Floating point results can therefore differ
136/// from a sequential left to right sum in the last bits, and no bitwise
137/// equality across machines or target features is guaranteed.
138///
139/// Because of that, `reduce_fn` must be associative and commutative, and
140/// `init` must be an identity of `reduce_fn` whenever the reduction can run on
141/// several threads.
142///
143/// # Allocation
144///
145/// At rank ≤ [`INLINE_RANK`](strided_view::INLINE_RANK) the view metadata and
146/// the traversal plan are stored inline, so a call performs no heap
147/// allocation. Above that bound the planner may spill to the heap.
148///
149/// Because `map_fn` and `reduce_fn` are opaque closures this entry cannot
150/// select the dtype-specific SIMD kernels of [`crate::ErasedReducePlan`];
151/// callers that reduce with a known operation (sum, product, max, min) on a
152/// supported dtype can use that plan for the fastest path.
153pub fn reduce<T: Copy + Send + Sync, Op: ElementOp<T>, M, R, U>(
154    src: &StridedView<T, Op>,
155    map_fn: M,
156    reduce_fn: R,
157    init: U,
158    exec: Exec<'_>,
159) -> Result<U>
160where
161    M: Fn(T) -> U + Sync,
162    R: Fn(U, U) -> U + Sync,
163    U: Clone + Send + Sync,
164{
165    reduce_impl(src, map_fn, reduce_fn, init, exec)
166}
167
168fn reduce_impl<T: Copy + Send + Sync, Op: ElementOp<T>, M, R, U>(
169    src: &StridedView<T, Op>,
170    map_fn: M,
171    reduce_fn: R,
172    init: U,
173    exec: Exec<'_>,
174) -> Result<U>
175where
176    M: Fn(T) -> U + Sync,
177    R: Fn(U, U) -> U + Sync,
178    U: Clone + Send + Sync,
179{
180    let src_ptr = src.ptr();
181    let src_dims = src.dims();
182    let src_strides = src.strides();
183
184    // A zero-element reduction is exactly `init`. Return before planning: an
185    // empty view is allowed to carry an arbitrarily large stride, which the
186    // block-size arithmetic of the planner cannot represent.
187    if total_len(src_dims)? == 0 {
188        return Ok(init);
189    }
190
191    let contiguous =
192        sequential_contiguous_layout(KernelClass::Reduce, src_dims, &[src_strides], &exec)?;
193    if contiguous.is_some() {
194        let len = total_len(src_dims)?;
195        let src = unsafe { std::slice::from_raw_parts(src_ptr, len) };
196        return Ok(simd::dispatch_if_large(len, || {
197            fold_contiguous::<T, Op, M, R, U>(src, init, &map_fn, &reduce_fn)
198        }));
199    }
200
201    // Parallel contiguous fast path: split into scheduler chunks with slice-based iteration.
202    // This enables LLVM auto-vectorization on each chunk, unlike the general threaded path
203    // which uses scalar pointer-offset loops.
204    let total = total_len(src_dims)?;
205    if is_parallel(KernelClass::Reduce, total, &exec)
206        && same_contiguous_layout(src_dims, &[src_strides]).is_some()
207    {
208        let src_slice = unsafe { std::slice::from_raw_parts(src_ptr, total) };
209        let result = crate::threading::parallel_map_reduce(
210            0..total,
211            crate::cost::for_len_tree(exec, KernelClass::Reduce, total),
212            &|range| {
213                simd::dispatch_if_large(range.len(), || {
214                    fold_contiguous::<T, Op, M, R, U>(
215                        &src_slice[range],
216                        init.clone(),
217                        &map_fn,
218                        &reduce_fn,
219                    )
220                })
221            },
222            &|a, b| reduce_fn(a, b),
223        );
224        return Ok(result);
225    }
226
227    let strides_list: [&[isize]; 1] = [src_strides];
228
229    let (fused_dims, ordered_strides, plan) =
230        build_plan_fused(src_dims, &strides_list, None, std::mem::size_of::<T>());
231
232    let total = total_len(&fused_dims)?;
233    if is_parallel(KernelClass::Reduce, total, &exec) {
234        let src_send = SendPtr(src_ptr as *mut T);
235        let costs = compute_costs(&ordered_strides);
236        let initial_offsets: crate::inline::OperandVec<isize> =
237            ordered_strides.iter().map(|_| 0isize).collect();
238        let map_fn_ref = &map_fn;
239        let reduce_fn_ref = &reduce_fn;
240        let init_ref = &init;
241        // The reduction tree combines each split's partial result, so no
242        // worker-count-sized output-slot vector is needed (#306 scope).
243        let result = mapreduce_threaded_fold(
244            &fused_dims,
245            &plan.block,
246            &ordered_strides,
247            &initial_offsets,
248            &costs,
249            KernelClass::Reduce,
250            exec,
251            &|dims, blocks, strides_list, offsets| {
252                let mut acc = init_ref.clone();
253                for_each_inner_block_with_offsets(
254                    dims,
255                    blocks,
256                    strides_list,
257                    offsets,
258                    |offsets, len, strides| {
259                        let mut ptr = unsafe { src_send.as_const().offset(offsets[0]) };
260                        let stride = strides[0];
261                        for _ in 0..len {
262                            let val = Op::apply(unsafe { *ptr });
263                            let mapped = map_fn_ref(val);
264                            acc = reduce_fn_ref(acc.clone(), mapped);
265                            unsafe {
266                                ptr = ptr.offset(stride);
267                            }
268                        }
269                        Ok(())
270                    },
271                )?;
272                Ok(acc)
273            },
274            &|a, b| reduce_fn_ref(a, b),
275        )?;
276        return Ok(result);
277    }
278
279    let mut acc = init;
280    let initial_offsets: crate::inline::OperandVec<isize> =
281        ordered_strides.iter().map(|_| 0isize).collect();
282    for_each_inner_block_preordered(
283        &fused_dims,
284        &plan.block,
285        &ordered_strides,
286        &initial_offsets,
287        |offsets, len, strides| {
288            let mut ptr = unsafe { src_ptr.offset(offsets[0]) };
289            let stride = strides[0];
290            for _ in 0..len {
291                let val = Op::apply(unsafe { *ptr });
292                let mapped = map_fn(val);
293                acc = reduce_fn(acc.clone(), mapped);
294                unsafe {
295                    ptr = ptr.offset(stride);
296                }
297            }
298            Ok(())
299        },
300    )?;
301
302    Ok(acc)
303}
304
305/// Folds a contiguous run of two operands with [`FOLD_LANES`] independent
306/// accumulators.
307///
308/// Same lane scheme as [`fold_contiguous`]: lane `i` starts from the pair at
309/// index `i` and folds the pairs at `i + FOLD_LANES`, `i + 2 * FOLD_LANES`,
310/// ... in order, the remainder goes to the first lanes, and `init` is combined
311/// once before the lanes are combined left to right.
312///
313/// The scheduling below deliberately mirrors [`fold_contiguous`] instead of
314/// sharing a generic "map index to value" lane helper: routing the existing
315/// single-operand fold through such an accessor was measured at 0.34 ns/element
316/// against 0.11 ns/element on a contiguous `f64` `reduce` (1 MiB, release,
317/// `--release -j 4`), because the `chunks_exact` + `zip` form is what lets the
318/// compiler drop the bound checks and vectorize the 16 lanes. Keep the two
319/// bodies in step by hand; both must use the lane assignment documented here.
320#[inline(always)]
321fn fold_contiguous2<A, B, OpA, OpB, M, R, U>(
322    a: &[A],
323    b: &[B],
324    init: U,
325    map_fn: &M,
326    reduce_fn: &R,
327) -> U
328where
329    A: Copy,
330    B: Copy,
331    OpA: ElementOp<A>,
332    OpB: ElementOp<B>,
333    M: Fn(A, B) -> U,
334    R: Fn(U, U) -> U,
335    U: Clone,
336{
337    debug_assert_eq!(a.len(), b.len());
338    if a.len() < 2 * FOLD_LANES {
339        let mut acc = init;
340        for (&value_a, &value_b) in a.iter().zip(b) {
341            acc = reduce_fn(acc, map_fn(OpA::apply(value_a), OpB::apply(value_b)));
342        }
343        return acc;
344    }
345    let (head_a, rest_a) = a.split_at(FOLD_LANES);
346    let (head_b, rest_b) = b.split_at(FOLD_LANES);
347    let mut lanes: [U; FOLD_LANES] =
348        core::array::from_fn(|lane| map_fn(OpA::apply(head_a[lane]), OpB::apply(head_b[lane])));
349    let mut chunks_a = rest_a.chunks_exact(FOLD_LANES);
350    let mut chunks_b = rest_b.chunks_exact(FOLD_LANES);
351    for (chunk_a, chunk_b) in chunks_a.by_ref().zip(chunks_b.by_ref()) {
352        for ((lane, &value_a), &value_b) in lanes.iter_mut().zip(chunk_a).zip(chunk_b) {
353            *lane = reduce_fn(
354                lane.clone(),
355                map_fn(OpA::apply(value_a), OpB::apply(value_b)),
356            );
357        }
358    }
359    for ((lane, &value_a), &value_b) in lanes
360        .iter_mut()
361        .zip(chunks_a.remainder())
362        .zip(chunks_b.remainder())
363    {
364        *lane = reduce_fn(
365            lane.clone(),
366            map_fn(OpA::apply(value_a), OpB::apply(value_b)),
367        );
368    }
369    lanes.into_iter().fold(init, reduce_fn)
370}
371
372/// Folds `len` operand pairs starting at `a_ptr`/`b_ptr` with the given
373/// strides.
374///
375/// # Safety
376/// `a_ptr.offset(i * a_stride)` and `b_ptr.offset(i * b_stride)` must be
377/// readable elements for every `i < len`.
378#[inline(always)]
379#[allow(clippy::too_many_arguments)]
380unsafe fn fold_run2<A, B, OpA, OpB, M, R, U>(
381    a_ptr: *const A,
382    a_stride: isize,
383    b_ptr: *const B,
384    b_stride: isize,
385    len: usize,
386    init: &U,
387    map_fn: &M,
388    reduce_fn: &R,
389) -> U
390where
391    A: Copy,
392    B: Copy,
393    OpA: ElementOp<A>,
394    OpB: ElementOp<B>,
395    M: Fn(A, B) -> U,
396    R: Fn(U, U) -> U,
397    U: Clone,
398{
399    if a_stride == 1 && b_stride == 1 {
400        // SAFETY: the caller proves `len` readable unit-stride elements of
401        // both operands.
402        let a = unsafe { std::slice::from_raw_parts(a_ptr, len) };
403        let b = unsafe { std::slice::from_raw_parts(b_ptr, len) };
404        return fold_contiguous2::<A, B, OpA, OpB, M, R, U>(a, b, init.clone(), map_fn, reduce_fn);
405    }
406    let mut acc = init.clone();
407    let mut cursor_a = a_ptr;
408    let mut cursor_b = b_ptr;
409    for index in 0..len {
410        // SAFETY: the caller proves every pair of the run is readable.
411        let mapped = map_fn(
412            OpA::apply(unsafe { *cursor_a }),
413            OpB::apply(unsafe { *cursor_b }),
414        );
415        acc = reduce_fn(acc, mapped);
416        if index + 1 < len {
417            cursor_a = cursor_a.wrapping_offset(a_stride);
418            cursor_b = cursor_b.wrapping_offset(b_stride);
419        }
420    }
421    acc
422}
423
424/// Two-input mapped reduction: `reduce(init, op, map.(a[i], b[i]))`.
425///
426/// This is the two-operand sibling of [`reduce`]. It exists so that the
427/// accumulator type `U` is chosen by the caller instead of being tied to the
428/// operand types by `Mul`, which is what makes a widening accumulation
429/// possible — for example an `f32` dot accumulated in `f64`:
430///
431/// ```
432/// use strided_basic::{zip_reduce, Exec, StridedArray};
433///
434/// let a = StridedArray::<f32>::from_fn_col_major(&[2, 3], |i| (1 + i[0] + 2 * i[1]) as f32);
435/// let b = StridedArray::<f32>::from_fn_col_major(&[2, 3], |i| (1 + i[0]) as f32 * 0.5);
436/// let dot: f64 = zip_reduce(
437///     &a.view(),
438///     &b.view(),
439///     |x: f32, y: f32| x as f64 * y as f64,
440///     |x, y| x + y,
441///     0.0,
442///     Exec::serial(),
443/// )
444/// .unwrap();
445/// assert_eq!(dot, 16.5);
446/// ```
447///
448/// Both operands must have the same shape. Each operand's [`ElementOp`] is
449/// applied before `map_fn`, so conjugation and transposition flags are honoured
450/// without materializing a conjugated copy. Permuted, gapped, negative and
451/// stride-0 (broadcast) operands are supported: the traversal is the fused
452/// plan of [`reduce`], extended to two stride lists.
453///
454/// # Evaluation order
455///
456/// The result is independent of the pool when the call runs with a serial
457/// token ([`Exec::serial`]) or a budget of one: only serial branches run, and
458/// their fold order is fixed by the operand layouts. With a larger budget the
459/// fold is split over the budget, so the result is repeatable for a fixed
460/// shape, layout and budget but can differ between budgets.
461///
462/// In neither case is this a strict left to right fold: a contiguous operand
463/// pair is folded with the same sixteen independent lanes as [`reduce`], the
464/// general plan is traversed in a cache-friendly loop order, and parallel
465/// execution folds per-worker chunks independently and combines them in a
466/// tree. Floating point results can therefore differ from a sequential left to
467/// right sum in the last bits, and no bitwise equality across machines or
468/// target features is guaranteed.
469///
470/// Because of that, `reduce_fn` must be associative and commutative, and
471/// `init` must be an identity of `reduce_fn` whenever the reduction can run on
472/// several threads.
473///
474/// # Allocation
475///
476/// At rank ≤ [`INLINE_RANK`](strided_view::INLINE_RANK) the view metadata and
477/// the two-operand traversal plan are stored inline, so a call performs no heap
478/// allocation. Above that bound the planner may spill to the heap.
479///
480/// # Errors
481///
482/// Returns [`StridedError::RankMismatch`] / [`StridedError::ShapeMismatch`] when
483/// the operand shapes differ, and [`StridedError::OffsetOverflow`] for
484/// unrepresentable element counts.
485pub fn zip_reduce<A, B, OpA, OpB, M, R, U>(
486    a: &StridedView<A, OpA>,
487    b: &StridedView<B, OpB>,
488    map_fn: M,
489    reduce_fn: R,
490    init: U,
491    exec: Exec<'_>,
492) -> Result<U>
493where
494    A: Copy + Send + Sync,
495    B: Copy + Send + Sync,
496    OpA: ElementOp<A>,
497    OpB: ElementOp<B>,
498    M: Fn(A, B) -> U + Sync,
499    R: Fn(U, U) -> U + Sync,
500    U: Clone + Send + Sync,
501{
502    ensure_same_shape(a.dims(), b.dims())?;
503
504    let a_ptr = a.ptr();
505    let b_ptr = b.ptr();
506    let dims = a.dims();
507    let a_strides = a.strides();
508    let b_strides = b.strides();
509
510    // A zero-element reduction is exactly `init`. Return before planning: an
511    // empty view is allowed to carry an arbitrarily large stride, which the
512    // block-size arithmetic of the planner cannot represent.
513    let total = total_len(dims)?;
514    if total == 0 {
515        return Ok(init);
516    }
517
518    if sequential_contiguous_layout(KernelClass::Reduce, dims, &[a_strides, b_strides], &exec)?
519        .is_some()
520    {
521        return Ok(simd::dispatch_if_large(total, || {
522            // SAFETY: both operands prove `total` readable elements.
523            let sa = unsafe { std::slice::from_raw_parts(a_ptr, total) };
524            let sb = unsafe { std::slice::from_raw_parts(b_ptr, total) };
525            fold_contiguous2::<A, B, OpA, OpB, M, R, U>(sa, sb, init, &map_fn, &reduce_fn)
526        }));
527    }
528
529    // Parallel contiguous fast path: split into scheduler chunks so each chunk
530    // folds two contiguous slices, matching the serial contiguous kernel.
531    if is_parallel(KernelClass::Reduce, total, &exec)
532        && same_contiguous_layout(dims, &[a_strides, b_strides]).is_some()
533    {
534        // SAFETY: both operands prove `total` readable elements.
535        let sa = unsafe { std::slice::from_raw_parts(a_ptr, total) };
536        let sb = unsafe { std::slice::from_raw_parts(b_ptr, total) };
537        let result = crate::threading::parallel_map_reduce(
538            0..total,
539            crate::cost::for_len_tree(exec, KernelClass::Reduce, total),
540            &|range| {
541                let len = range.len();
542                simd::dispatch_if_large(len, || {
543                    fold_contiguous2::<A, B, OpA, OpB, M, R, U>(
544                        &sa[range.clone()],
545                        &sb[range],
546                        init.clone(),
547                        &map_fn,
548                        &reduce_fn,
549                    )
550                })
551            },
552            &|left, right| reduce_fn(left, right),
553        );
554        return Ok(result);
555    }
556
557    let strides_list: [&[isize]; 2] = [a_strides, b_strides];
558    let elem_size = std::mem::size_of::<A>().max(std::mem::size_of::<B>());
559    let (fused_dims, ordered_strides, plan) =
560        build_plan_fused(dims, &strides_list, None, elem_size);
561
562    let total = total_len(&fused_dims)?;
563    if is_parallel(KernelClass::Reduce, total, &exec) {
564        let a_send = SendPtr(a_ptr as *mut A);
565        let b_send = SendPtr(b_ptr as *mut B);
566        let costs = compute_costs(&ordered_strides);
567        let initial_offsets: crate::inline::OperandVec<isize> =
568            ordered_strides.iter().map(|_| 0isize).collect();
569        let map_fn_ref = &map_fn;
570        let reduce_fn_ref = &reduce_fn;
571        let init_ref = &init;
572        // The reduction tree combines each split's partial result, so no
573        // worker-count-sized output-slot vector is needed (#306 scope).
574        let result = mapreduce_threaded_fold(
575            &fused_dims,
576            &plan.block,
577            &ordered_strides,
578            &initial_offsets,
579            &costs,
580            KernelClass::Reduce,
581            exec,
582            &|dims, blocks, strides_list, offsets| {
583                let mut acc = init_ref.clone();
584                for_each_inner_block_with_offsets(
585                    dims,
586                    blocks,
587                    strides_list,
588                    offsets,
589                    |offsets, len, strides| {
590                        // SAFETY: the plan proves every pair of this block
591                        // is a readable element of both operands.
592                        acc = unsafe {
593                            fold_run2::<A, B, OpA, OpB, M, R, U>(
594                                a_send.as_const().offset(offsets[0]),
595                                strides[0],
596                                b_send.as_const().offset(offsets[1]),
597                                strides[1],
598                                len,
599                                &acc,
600                                map_fn_ref,
601                                reduce_fn_ref,
602                            )
603                        };
604                        Ok(())
605                    },
606                )?;
607                Ok(acc)
608            },
609            &|left, right| reduce_fn_ref(left, right),
610        )?;
611        return Ok(result);
612    }
613
614    let mut acc = init;
615    let initial_offsets: crate::inline::OperandVec<isize> =
616        ordered_strides.iter().map(|_| 0isize).collect();
617    for_each_inner_block_preordered(
618        &fused_dims,
619        &plan.block,
620        &ordered_strides,
621        &initial_offsets,
622        |offsets, len, strides| {
623            // SAFETY: the plan proves every pair of this block is a readable
624            // element of both operands.
625            acc = unsafe {
626                fold_run2::<A, B, OpA, OpB, M, R, U>(
627                    a_ptr.offset(offsets[0]),
628                    strides[0],
629                    b_ptr.offset(offsets[1]),
630                    strides[1],
631                    len,
632                    &acc,
633                    &map_fn,
634                    &reduce_fn,
635                )
636            };
637            Ok(())
638        },
639    )?;
640
641    Ok(acc)
642}
643
644/// Reduce along a single axis, returning a new `StridedArray`.
645///
646/// This allocates a column-major output and delegates to
647/// [`reduce_axis_into`].
648///
649/// Every output folds `init` once and then its reduced elements. When the
650/// reduced axis has unit stride those elements are folded with the lane
651/// scheme of [`reduce`], so `reduce_fn` must be associative and commutative.
652/// When the leading kept axis has unit stride, a block of adjacent outputs
653/// is accumulated one contiguous source column at a time, which keeps the
654/// left to right order per output. Outputs are split across the budget of
655/// `exec` once the number of source elements read exceeds the threading
656/// threshold; each output is computed by one task, so the result does not
657/// depend on the budget.
658pub fn reduce_axis<T: Copy + Send + Sync, Op: ElementOp<T>, M, R, U>(
659    src: &StridedView<T, Op>,
660    axis: usize,
661    map_fn: M,
662    reduce_fn: R,
663    init: U,
664    exec: Exec<'_>,
665) -> Result<StridedArray<U>>
666where
667    M: Fn(T) -> U + Sync,
668    R: Fn(U, U) -> U + Sync,
669    U: Clone + Send + Sync,
670{
671    let rank = src.ndim();
672    if axis >= rank {
673        return Err(StridedError::InvalidAxis { axis, rank });
674    }
675    // Reject an element count beyond `usize` before allocating the output.
676    total_len(src.dims())?;
677    let out_dims: InlineVec<usize> = src
678        .dims()
679        .iter()
680        .enumerate()
681        .filter(|(i, _)| *i != axis)
682        .map(|(_, &d)| d)
683        .collect();
684    // A full reduction keeps the historical rank-1 shape-[1] output; the
685    // `_into` form accepts any single-element destination for that case.
686    let (out_dims, out_strides) = if out_dims.is_empty() {
687        let mut dims = InlineVec::new();
688        dims.push(1usize);
689        let strides = col_major_strides(&dims);
690        (dims, strides)
691    } else {
692        let strides = col_major_strides(&out_dims);
693        (out_dims, strides)
694    };
695    let total_out = total_len(&out_dims)?;
696    let mut out =
697        StridedArray::<U>::from_parts(vec![init.clone(); total_out], &out_dims, &out_strides, 0)?;
698    {
699        let mut dest = out.view_mut();
700        // The fresh column-major destination needs no validation.
701        reduce_axis_into_impl(&mut dest, src, axis, map_fn, reduce_fn, init, exec)?;
702    }
703    Ok(out)
704}
705
706/// Reduce along a single axis into caller-owned storage.
707///
708/// `dest` must have the source's shape with `axis` removed, or any
709/// single-element destination for a full reduction, and an injective layout.
710/// Every destination element is written: an empty reduced axis fills each
711/// output with `init`.
712///
713/// # Errors
714///
715/// Returns [`StridedError::InvalidAxis`] for `axis >= src.ndim()`,
716/// [`StridedError::RankMismatch`] / [`StridedError::ShapeMismatch`] for a
717/// destination shape that is not the reduced source shape,
718/// [`StridedError::NonInjectiveOutputLayout`] for an overlapping destination,
719/// and [`StridedError::OffsetOverflow`] for unrepresentable sizes.
720///
721/// # Examples
722///
723/// ```
724/// use strided_basic::{reduce_axis_into, Exec, StridedArray};
725/// # let src = StridedArray::<f64>::from_fn_col_major(&[2, 3], |i| (i[0] + 2 * i[1]) as f64);
726/// let mut out = StridedArray::<f64>::col_major(&[3]);
727/// reduce_axis_into(&mut out.view_mut(), &src.view(), 0, |x| x, |a, b| a + b, 0.0, Exec::serial()).unwrap();
728/// assert_eq!(out.get(&[0]), 1.0);
729/// ```
730///
731/// Validation runs here; `reduce_axis_into_impl` is the shared kernel. The
732/// array-returning wrapper owns a fresh column-major destination and calls the
733/// kernel directly, so the common path does not re-validate.
734pub fn reduce_axis_into<T: Copy + Send + Sync, Op: ElementOp<T>, M, R, U>(
735    dest: &mut StridedViewMut<U>,
736    src: &StridedView<T, Op>,
737    axis: usize,
738    map_fn: M,
739    reduce_fn: R,
740    init: U,
741    exec: Exec<'_>,
742) -> Result<()>
743where
744    M: Fn(T) -> U + Sync,
745    R: Fn(U, U) -> U + Sync,
746    U: Clone + Send + Sync,
747{
748    let rank = src.ndim();
749    if axis >= rank {
750        return Err(StridedError::InvalidAxis { axis, rank });
751    }
752    let src_dims = src.dims();
753    // Reject an element count beyond usize (a huge stride-0 broadcast) up
754    // front instead of looping over it.
755    total_len(src_dims)?;
756    let kept_len = rank - 1;
757    let dest_len = total_len(dest.dims())?;
758    if kept_len == 0 {
759        if dest_len != 1 {
760            return Err(StridedError::ShapeMismatch(vec![dest_len], vec![1]));
761        }
762    } else {
763        if dest.ndim() != kept_len {
764            return Err(StridedError::RankMismatch(dest.ndim(), kept_len));
765        }
766        let dest_dims = dest.dims();
767        let mut d = 0usize;
768        for (i, &src_dim) in src_dims.iter().enumerate() {
769            if i == axis {
770                continue;
771            }
772            if src_dim != dest_dims[d] {
773                let expected: Vec<usize> = src_dims
774                    .iter()
775                    .enumerate()
776                    .filter(|(i, _)| *i != axis)
777                    .map(|(_, &d)| d)
778                    .collect();
779                return Err(StridedError::ShapeMismatch(dest_dims.to_vec(), expected));
780            }
781            d += 1;
782        }
783        if !crate::layout_check::is_injective_layout(dest.dims(), dest.strides()) {
784            return Err(StridedError::NonInjectiveOutputLayout);
785        }
786    }
787    reduce_axis_into_impl(dest, src, axis, map_fn, reduce_fn, init, exec)
788}
789
790/// Shared kernel of [`reduce_axis_into`] and [`reduce_axis`].
791///
792/// Assumes the destination has the reduced source shape (or one element for a
793/// full reduction) and an injective layout; the public entry validates that,
794/// while the array-returning wrapper owns a freshly allocated column-major
795/// destination and can skip the re-validation.
796fn reduce_axis_into_impl<T: Copy + Send + Sync, Op: ElementOp<T>, M, R, U>(
797    dest: &mut StridedViewMut<U>,
798    src: &StridedView<T, Op>,
799    axis: usize,
800    map_fn: M,
801    reduce_fn: R,
802    init: U,
803    exec: Exec<'_>,
804) -> Result<()>
805where
806    M: Fn(T) -> U + Sync,
807    R: Fn(U, U) -> U + Sync,
808    U: Clone + Send + Sync,
809{
810    let rank = src.ndim();
811    let src_dims = src.dims();
812    let src_strides = src.strides();
813    let src_ptr = src.ptr();
814    let dest_len = total_len(dest.dims())?;
815    let axis_len = src_dims[axis];
816    let axis_stride = src_strides[axis];
817
818    if axis_len == 0 {
819        // No source elements to fold: every output is `init`. Fill the
820        // destination directly rather than traversing the source, because an
821        // empty source view carries a dangling base pointer and `offset` on it
822        // is UB even when the fold reads zero elements.
823        fill_outputs_with(dest, &init);
824        return Ok(());
825    }
826
827    if rank == 1 {
828        // Full reduction into a single destination element.
829        // SAFETY: the view proves `axis_len` readable elements along `axis`.
830        let acc = unsafe {
831            fold_run::<T, Op, M, R, U>(src_ptr, axis_stride, axis_len, init, &map_fn, &reduce_fn)
832        };
833        unsafe { *dest.as_mut_ptr() = acc };
834        return Ok(());
835    }
836    if dest_len == 0 {
837        return Ok(());
838    }
839
840    // Build the kept-axis metadata in one pass. A column-major destination is
841    // the common case (the array-returning wrapper always allocates one); its
842    // offset equals the linear output index, so its stride list is only
843    // materialized for a strided destination.
844    let dest_stride_view = dest.strides();
845    let mut kept: InlineVec<(usize, isize)> = InlineVec::new();
846    let mut dest_linear = true;
847    let mut prefix = 1usize;
848    for (i, (&dim, &src_stride)) in src_dims.iter().zip(src_strides).enumerate() {
849        if i == axis {
850            continue;
851        }
852        let d = kept.len();
853        kept.push((dim, src_stride));
854        if dest_linear && dest_stride_view[d] != prefix as isize {
855            dest_linear = false;
856        }
857        prefix = prefix.saturating_mul(dim);
858    }
859    let dest_strides: InlineVec<isize> = if dest_linear {
860        InlineVec::new()
861    } else {
862        dest_stride_view.iter().copied().collect()
863    };
864    let out_ptr = dest.as_mut_ptr();
865    let parts = AxisParts {
866        kept: &kept,
867        dest_strides: &dest_strides,
868        axis_len,
869        axis_stride,
870        map_fn: &map_fn,
871        reduce_fn: &reduce_fn,
872        init: &init,
873    };
874
875    let work = dest_len.saturating_mul(axis_len);
876    let exec = crate::cost::for_len(exec, KernelClass::Reduce, work)
877        .limit(NonZeroUsize::new(dest_len).unwrap_or(NonZeroUsize::MIN));
878    if !exec.is_serial() {
879        let src_send = SendPtr(src_ptr as *mut T);
880        let out_send = SendPtr(out_ptr);
881        let parts = &parts;
882        if dest_linear {
883            crate::threading::parallel_for_each(0..dest_len, exec, &|range| {
884                // SAFETY: worker ranges are disjoint, the destination is
885                // injective, and the view proves every source offset
886                // reachable from its kept coordinates.
887                unsafe {
888                    reduce_axis_range_linear::<T, Op, M, R, U>(
889                        src_send.as_const(),
890                        out_send.as_ptr(),
891                        range,
892                        parts,
893                    )
894                }
895            });
896        } else {
897            crate::threading::parallel_for_each(0..dest_len, exec, &|range| {
898                // SAFETY: as for the linear path.
899                unsafe {
900                    reduce_axis_range_strided::<T, Op, M, R, U>(
901                        src_send.as_const(),
902                        out_send.as_ptr(),
903                        range,
904                        parts,
905                    )
906                }
907            });
908        }
909        return Ok(());
910    }
911
912    // SAFETY: as for the parallel ranges, with one range covering all outputs.
913    if dest_linear {
914        unsafe {
915            reduce_axis_range_linear::<T, Op, M, R, U>(src_ptr, out_ptr, 0..dest_len, &parts)
916        };
917    } else {
918        unsafe {
919            reduce_axis_range_strided::<T, Op, M, R, U>(src_ptr, out_ptr, 0..dest_len, &parts)
920        };
921    }
922    Ok(())
923}
924
925/// Writes `value` into every logical destination element.
926///
927/// Used when the reduced axis is empty: the fold would read zero source
928/// elements, so the destination is exactly `init` everywhere and no source
929/// pointer may be formed. The odometer keeps the fill allocation-free within
930/// [`strided_view::INLINE_RANK`] and spills above it.
931fn fill_outputs_with<U: Clone>(dest: &mut StridedViewMut<U>, value: &U) {
932    let dims = dest.dims();
933    if dims.iter().any(|&dim| dim == 0) {
934        return;
935    }
936    let strides = dest.strides();
937    let rank = dims.len();
938    let total: usize = dims.iter().product();
939    let base = dest.as_mut_ptr();
940    if rank == 0 {
941        // SAFETY: a rank-0 destination is one writable element.
942        unsafe { *base = value.clone() };
943        return;
944    }
945    let mut coords_inline = [0usize; strided_view::INLINE_RANK];
946    let mut coords_heap: Vec<usize> = Vec::new();
947    let coords: &mut [usize] = if rank <= strided_view::INLINE_RANK {
948        &mut coords_inline[..rank]
949    } else {
950        coords_heap.resize(rank, 0);
951        &mut coords_heap[..]
952    };
953    let mut offset = 0isize;
954    for _ in 0..total {
955        // SAFETY: the destination is an injective view validated by the
956        // caller, and the cursor visits each logical element exactly once, so
957        // every written offset is in bounds.
958        unsafe { *base.offset(offset) = value.clone() };
959        for axis in 0..rank {
960            coords[axis] += 1;
961            offset += strides[axis];
962            if coords[axis] < dims[axis] {
963                break;
964            }
965            coords[axis] = 0;
966            offset -= dims[axis] as isize * strides[axis];
967        }
968    }
969}
970
971/// Shared inputs of [`reduce_axis_range_linear`] and
972/// [`reduce_axis_range_strided`].
973struct AxisParts<'a, M, R, U> {
974    /// `(extent, source stride)` of every kept axis, in output order.
975    kept: &'a [(usize, isize)],
976    /// Destination stride of every kept axis, in the same order.
977    dest_strides: &'a [isize],
978    axis_len: usize,
979    axis_stride: isize,
980    map_fn: &'a M,
981    reduce_fn: &'a R,
982    init: &'a U,
983}
984
985/// Column-major destination cursor: the destination offset is the linear output
986/// index, so no destination stride bookkeeping is needed.
987///
988/// Kept out of line so the common path stays the same single-cursor loop as the
989/// pre-split kernel.
990///
991/// # Safety
992/// `out` must be a column-major destination of `parts.kept` extents, `range` a
993/// subrange of its logical output indices that no other thread writes, and
994/// every source offset reachable from a kept coordinate plus
995/// `k * parts.axis_stride` for `k < parts.axis_len` must be a readable element
996/// of `src`.
997#[inline(never)]
998unsafe fn reduce_axis_range_linear<T, Op, M, R, U>(
999    src: *const T,
1000    out: *mut U,
1001    range: Range<usize>,
1002    parts: &AxisParts<'_, M, R, U>,
1003) where
1004    T: Copy,
1005    Op: ElementOp<T>,
1006    M: Fn(T) -> U,
1007    R: Fn(U, U) -> U,
1008    U: Clone,
1009{
1010    let kept = parts.kept;
1011    let (lead_extent, lead_stride) = kept[0];
1012    let sweep = lead_stride == 1 && lead_extent >= AXIS_SWEEP_MIN_LEN && parts.axis_len > 1;
1013    let block = (AXIS_SWEEP_BLOCK_BYTES / std::mem::size_of::<U>().max(1)).max(AXIS_SWEEP_MIN_LEN);
1014
1015    // Decode the first output once, then advance incrementally. A fixed stack
1016    // array with a spill covers every inline rank without the per-access enum
1017    // match a `TinyVec` cursor would add to the hot loop, and allocates only
1018    // above `INLINE_RANK` (#306 P1).
1019    let coords_len = kept.len();
1020    let mut coords_inline = [0usize; strided_view::INLINE_RANK];
1021    let mut coords_heap: Vec<usize> = Vec::new();
1022    let coords: &mut [usize] = if coords_len <= strided_view::INLINE_RANK {
1023        &mut coords_inline[..coords_len]
1024    } else {
1025        coords_heap.resize(coords_len, 0);
1026        &mut coords_heap[..]
1027    };
1028    let mut rest = range.start;
1029    let mut src_off = 0isize;
1030    for (coord, &(extent, stride)) in coords.iter_mut().zip(kept) {
1031        *coord = rest % extent;
1032        rest /= extent;
1033        src_off += *coord as isize * stride;
1034    }
1035
1036    let mut output = range.start;
1037    while output < range.end {
1038        let len = if sweep {
1039            (lead_extent - coords[0]).min(range.end - output).min(block)
1040        } else {
1041            1
1042        };
1043        if sweep {
1044            // SAFETY: the `len` outputs are inside the caller's range, their
1045            // unit-stride destination elements are contiguous, and their
1046            // unit-stride source columns are readable.
1047            unsafe { sweep_block::<T, Op, M, R, U>(src, src_off, out.add(output), len, parts) };
1048        } else {
1049            // SAFETY: one reachable output and its reduced run.
1050            unsafe {
1051                let acc = fold_run::<T, Op, M, R, U>(
1052                    src.offset(src_off),
1053                    parts.axis_stride,
1054                    parts.axis_len,
1055                    parts.init.clone(),
1056                    parts.map_fn,
1057                    parts.reduce_fn,
1058                );
1059                *out.add(output) = acc;
1060            }
1061        }
1062        output += len;
1063        if output >= range.end {
1064            break;
1065        }
1066        // Advance the kept coordinates by `len` along the leading axis; a
1067        // block never crosses the end of the leading axis.
1068        coords[0] += len;
1069        src_off += len as isize * lead_stride;
1070        let mut axis = 0;
1071        while coords[axis] == kept[axis].0 {
1072            src_off -= kept[axis].0 as isize * kept[axis].1;
1073            coords[axis] = 0;
1074            axis += 1;
1075            coords[axis] += 1;
1076            src_off += kept[axis].1;
1077        }
1078    }
1079}
1080
1081/// General strided destination cursor, tracking a destination offset next to
1082/// the source offset.
1083///
1084/// # Safety
1085/// `out` must be a destination of `parts.kept` extents with injective layout,
1086/// `range` a subrange of its logical output indices that no other thread
1087/// writes, and every source offset reachable from a kept coordinate plus
1088/// `k * parts.axis_stride` for `k < parts.axis_len` must be a readable element
1089/// of `src`.
1090#[inline(never)]
1091unsafe fn reduce_axis_range_strided<T, Op, M, R, U>(
1092    src: *const T,
1093    out: *mut U,
1094    range: Range<usize>,
1095    parts: &AxisParts<'_, M, R, U>,
1096) where
1097    T: Copy,
1098    Op: ElementOp<T>,
1099    M: Fn(T) -> U,
1100    R: Fn(U, U) -> U,
1101    U: Clone,
1102{
1103    let kept = parts.kept;
1104    let dest_strides = parts.dest_strides;
1105    let (lead_extent, lead_stride) = kept[0];
1106    let lead_dest_stride = dest_strides[0];
1107    let sweep = lead_stride == 1
1108        && lead_dest_stride == 1
1109        && lead_extent >= AXIS_SWEEP_MIN_LEN
1110        && parts.axis_len > 1;
1111    let block = (AXIS_SWEEP_BLOCK_BYTES / std::mem::size_of::<U>().max(1)).max(AXIS_SWEEP_MIN_LEN);
1112
1113    // Same fixed-array-plus-spill cursor as `reduce_axis_range_linear`: the
1114    // `TinyVec` enum match is not paid per output access in the hot loop and
1115    // only ranks above `INLINE_RANK` allocate (#306 P1).
1116    let coords_len = kept.len();
1117    let mut coords_inline = [0usize; strided_view::INLINE_RANK];
1118    let mut coords_heap: Vec<usize> = Vec::new();
1119    let coords: &mut [usize] = if coords_len <= strided_view::INLINE_RANK {
1120        &mut coords_inline[..coords_len]
1121    } else {
1122        coords_heap.resize(coords_len, 0);
1123        &mut coords_heap[..]
1124    };
1125    let mut rest = range.start;
1126    let mut src_off = 0isize;
1127    let mut dest_off = 0isize;
1128    for (coord, (&(extent, stride), &dest_stride)) in
1129        coords.iter_mut().zip(kept.iter().zip(dest_strides))
1130    {
1131        *coord = rest % extent;
1132        rest /= extent;
1133        src_off += *coord as isize * stride;
1134        dest_off += *coord as isize * dest_stride;
1135    }
1136
1137    let mut output = range.start;
1138    while output < range.end {
1139        let len = if sweep {
1140            (lead_extent - coords[0]).min(range.end - output).min(block)
1141        } else {
1142            1
1143        };
1144        if sweep {
1145            // SAFETY: as for the linear cursor; `lead_dest_stride == 1` makes
1146            // the `len` destinations contiguous.
1147            unsafe {
1148                sweep_block::<T, Op, M, R, U>(src, src_off, out.offset(dest_off), len, parts)
1149            };
1150        } else {
1151            // SAFETY: one reachable output and its reduced run.
1152            unsafe {
1153                let acc = fold_run::<T, Op, M, R, U>(
1154                    src.offset(src_off),
1155                    parts.axis_stride,
1156                    parts.axis_len,
1157                    parts.init.clone(),
1158                    parts.map_fn,
1159                    parts.reduce_fn,
1160                );
1161                *out.offset(dest_off) = acc;
1162            }
1163        }
1164        output += len;
1165        if output >= range.end {
1166            break;
1167        }
1168        coords[0] += len;
1169        src_off += len as isize * lead_stride;
1170        dest_off += len as isize * lead_dest_stride;
1171        let mut axis = 0;
1172        while coords[axis] == kept[axis].0 {
1173            src_off -= kept[axis].0 as isize * kept[axis].1;
1174            dest_off -= kept[axis].0 as isize * dest_strides[axis];
1175            coords[axis] = 0;
1176            axis += 1;
1177            coords[axis] += 1;
1178            src_off += kept[axis].1;
1179            dest_off += dest_strides[axis];
1180        }
1181    }
1182}
1183
1184/// Accumulates `len` adjacent outputs, one contiguous source column per
1185/// reduced index, keeping the left to right order per output.
1186///
1187/// # Safety
1188/// `out` must be `len` writable initialized outputs and `src + src_off +
1189/// k * parts.axis_stride` must start `len` readable elements for every
1190/// `k < parts.axis_len`.
1191#[inline(always)]
1192unsafe fn sweep_block<T, Op, M, R, U>(
1193    src: *const T,
1194    src_off: isize,
1195    out: *mut U,
1196    len: usize,
1197    parts: &AxisParts<'_, M, R, U>,
1198) where
1199    T: Copy,
1200    Op: ElementOp<T>,
1201    M: Fn(T) -> U,
1202    R: Fn(U, U) -> U,
1203    U: Clone,
1204{
1205    let map_fn = parts.map_fn;
1206    let reduce_fn = parts.reduce_fn;
1207    // SAFETY: forwarded caller contract.
1208    let out = unsafe { std::slice::from_raw_parts_mut(out, len) };
1209    let column = |k: usize| {
1210        // SAFETY: forwarded caller contract for reduced index `k`.
1211        unsafe {
1212            std::slice::from_raw_parts(src.offset(src_off + k as isize * parts.axis_stride), len)
1213        }
1214    };
1215    let first = column(0);
1216    for (slot, &value) in out.iter_mut().zip(first) {
1217        *slot = reduce_fn(parts.init.clone(), map_fn(Op::apply(value)));
1218    }
1219    let mut k = 1;
1220    while k + 4 <= parts.axis_len {
1221        let (c0, c1, c2, c3) = (column(k), column(k + 1), column(k + 2), column(k + 3));
1222        for (index, slot) in out.iter_mut().enumerate() {
1223            let mut acc = reduce_fn(slot.clone(), map_fn(Op::apply(c0[index])));
1224            acc = reduce_fn(acc, map_fn(Op::apply(c1[index])));
1225            acc = reduce_fn(acc, map_fn(Op::apply(c2[index])));
1226            acc = reduce_fn(acc, map_fn(Op::apply(c3[index])));
1227            *slot = acc;
1228        }
1229        k += 4;
1230    }
1231    while k < parts.axis_len {
1232        let values = column(k);
1233        for (slot, &value) in out.iter_mut().zip(values) {
1234            *slot = reduce_fn(slot.clone(), map_fn(Op::apply(value)));
1235        }
1236        k += 1;
1237    }
1238}