Skip to main content

strided_basic/
erased.rs

1use crate::erased_common::*;
2use crate::*;
3use core::mem::MaybeUninit;
4use num_complex::{Complex32, Complex64};
5use num_traits::{One, Zero};
6mod arg_reduce;
7mod line;
8mod norm;
9mod reduce_kernel;
10mod scan;
11
12pub use arg_reduce::{ArgReduceOp, ErasedArgReducePlan};
13pub use norm::{ErasedNormPlan, NormKind, NormSpec};
14pub use scan::{ErasedScanPlan, ScanOp, ScanOptions};
15
16use reduce_kernel::{
17    reduce_axes_range, reduce_full_range, AxesRange, FullTraversal, MaxKernel, MinKernel,
18    ReassociatedProductKernel, ReduceKernel, StableProductKernel, StableProductScalar, SumKernel,
19    SumSquaresKernel,
20};
21
22trait ReduceWriter<T> {
23    fn offset(&self) -> isize;
24    /// # Safety
25    /// The pointer may only be used within the validated destination extent.
26    unsafe fn ptr(&mut self) -> *mut T;
27    fn extent(&self) -> usize;
28    /// # Safety
29    /// The offset must be an in-bounds logical reduction destination offset.
30    unsafe fn write_at(&mut self, offset: isize, value: T) {
31        debug_assert!(offset >= 0 && (offset as usize) < self.extent());
32        // SAFETY: reduction layout validation proves the logical offset.
33        unsafe { self.ptr().offset(offset).write(value) }
34    }
35}
36
37struct RawReduceWriter<'a, T> {
38    ptr: *mut T,
39    extent: usize,
40    offset: isize,
41    _marker: core::marker::PhantomData<&'a mut [MaybeUninit<T>]>,
42}
43
44impl<'a, T> ReduceWriter<T> for RawReduceWriter<'a, T> {
45    fn offset(&self) -> isize {
46        self.offset
47    }
48    unsafe fn ptr(&mut self) -> *mut T {
49        self.ptr
50    }
51    fn extent(&self) -> usize {
52        self.extent
53    }
54}
55
56/// Dtype-erased wrapper around [`CopyPlan`].
57#[derive(Clone, Debug)]
58pub struct ErasedCopyPlan {
59    dtype: KernelDType,
60    plan: CopyPlan,
61}
62
63/// Dtype-erased concatenate wrapper.
64#[derive(Clone, Debug)]
65pub struct ErasedConcatenatePlan {
66    dtype: KernelDType,
67    plan: ConcatenatePlan,
68}
69
70impl ErasedCopyPlan {
71    /// Compile a copy plan for one dtype and layout pair.
72    pub fn compile(
73        dtype: KernelDType,
74        dims: &[usize],
75        dst_strides: &[isize],
76        src_strides: &[isize],
77    ) -> Result<Self> {
78        Ok(Self {
79            dtype,
80            plan: CopyPlan::compile(dims, dst_strides, src_strides)?,
81        })
82    }
83
84    #[inline]
85    pub fn dtype(&self) -> KernelDType {
86        self.dtype
87    }
88
89    /// `dest = src` through a non-generic dtype-erased replay boundary.
90    pub fn execute(
91        &self,
92        dest: &mut ErasedRawStridedMut<'_>,
93        src: &ErasedRawStridedRef<'_>,
94        exec: Exec<'_>,
95    ) -> Result<()> {
96        self.check_dtype(dest.dtype())?;
97        self.check_dtype(src.dtype())?;
98
99        match self.dtype {
100            KernelDType::F32 => execute_copy::<f32>(&self.plan, dest, src, exec),
101            KernelDType::F64 => execute_copy::<f64>(&self.plan, dest, src, exec),
102            KernelDType::I32 => execute_copy::<i32>(&self.plan, dest, src, exec),
103            KernelDType::I64 => execute_copy::<i64>(&self.plan, dest, src, exec),
104            KernelDType::Bool => execute_copy::<bool>(&self.plan, dest, src, exec),
105            KernelDType::C32 => execute_copy::<Complex32>(&self.plan, dest, src, exec),
106            KernelDType::C64 => execute_copy::<Complex64>(&self.plan, dest, src, exec),
107            _ => Err(StridedError::UnsupportedDType {
108                dtype: self.dtype.label(),
109            }),
110        }
111    }
112
113    fn check_dtype(&self, actual: KernelDType) -> Result<()> {
114        if actual != self.dtype {
115            return Err(StridedError::DTypeMismatch {
116                expected: self.dtype.label(),
117                actual: actual.label(),
118            });
119        }
120        Ok(())
121    }
122}
123
124impl ErasedConcatenatePlan {
125    /// Validate and store a concatenate plan for one dtype and fixed layout set.
126    pub fn compile(
127        dtype: KernelDType,
128        input_dims: &[&[usize]],
129        input_strides: &[&[isize]],
130        dest_dims: &[usize],
131        dest_strides: &[isize],
132        axis: usize,
133    ) -> Result<Self> {
134        check_static_indexing_dtype(dtype)?;
135        Ok(Self {
136            dtype,
137            plan: ConcatenatePlan::compile(
138                input_dims,
139                input_strides,
140                dest_dims,
141                dest_strides,
142                axis,
143            )?,
144        })
145    }
146
147    #[inline]
148    pub fn dtype(&self) -> KernelDType {
149        self.dtype
150    }
151
152    #[inline]
153    pub fn plan(&self) -> &ConcatenatePlan {
154        &self.plan
155    }
156
157    /// Execute concatenate into an erased output descriptor.
158    pub fn execute(
159        &self,
160        dest: &mut ErasedRawStridedMut<'_>,
161        inputs: &[ErasedRawStridedRef<'_>],
162        exec: Exec<'_>,
163    ) -> Result<()> {
164        check_dtype(self.dtype, dest.dtype())?;
165        for input in inputs {
166            check_dtype(self.dtype, input.dtype())?;
167        }
168
169        match self.dtype {
170            KernelDType::F32 => execute_concatenate::<f32>(&self.plan, dest, inputs, exec),
171            KernelDType::F64 => execute_concatenate::<f64>(&self.plan, dest, inputs, exec),
172            KernelDType::I32 => execute_concatenate::<i32>(&self.plan, dest, inputs, exec),
173            KernelDType::I64 => execute_concatenate::<i64>(&self.plan, dest, inputs, exec),
174            KernelDType::Bool => execute_concatenate::<bool>(&self.plan, dest, inputs, exec),
175            KernelDType::C32 => execute_concatenate::<Complex32>(&self.plan, dest, inputs, exec),
176            KernelDType::C64 => execute_concatenate::<Complex64>(&self.plan, dest, inputs, exec),
177            _ => Err(StridedError::UnsupportedDType {
178                dtype: self.dtype.label(),
179            }),
180        }
181    }
182
183    /// Execute concatenate as a full overwrite of uninitialized output storage.
184    /// On success, every reachable destination slot is fully overwritten;
185    /// unreachable holes are neither read nor initialized. Validation errors
186    /// are returned before any destination write. A panic during execution
187    /// may leave a partially initialized `MaybeUninit` destination, which is
188    /// still safely droppable; no readable value is promised for unwritten
189    /// reachable slots.
190    pub fn execute_uninit(
191        &self,
192        dest: &mut ErasedRawStridedUninitMut<'_>,
193        inputs: &[ErasedRawStridedPtr<'_>],
194        exec: Exec<'_>,
195    ) -> Result<()> {
196        check_dtype(self.dtype, dest.dtype())?;
197        if inputs.len() != self.plan.input_count() {
198            return Err(StridedError::RankMismatch(
199                inputs.len(),
200                self.plan.input_count(),
201            ));
202        }
203        for input in inputs {
204            check_dtype(self.dtype, input.dtype())?;
205        }
206        for (position, input) in inputs.iter().enumerate() {
207            validate_uninit_no_overlap(dest, input, position)?;
208        }
209        for input in inputs {
210            // SAFETY: input/output overlap was rejected before forming references.
211            unsafe { input.try_as_ref_after_no_overlap() }?;
212        }
213
214        match self.dtype {
215            KernelDType::F32 => execute_concatenate_uninit::<f32>(&self.plan, dest, inputs, exec),
216            KernelDType::F64 => execute_concatenate_uninit::<f64>(&self.plan, dest, inputs, exec),
217            KernelDType::I32 => execute_concatenate_uninit::<i32>(&self.plan, dest, inputs, exec),
218            KernelDType::I64 => execute_concatenate_uninit::<i64>(&self.plan, dest, inputs, exec),
219            KernelDType::Bool => execute_concatenate_uninit::<bool>(&self.plan, dest, inputs, exec),
220            KernelDType::C32 => {
221                execute_concatenate_uninit::<Complex32>(&self.plan, dest, inputs, exec)
222            }
223            KernelDType::C64 => {
224                execute_concatenate_uninit::<Complex64>(&self.plan, dest, inputs, exec)
225            }
226            _ => Err(StridedError::UnsupportedDType {
227                dtype: self.dtype.label(),
228            }),
229        }
230    }
231}
232
233/// Runtime reduction operation for dtype-erased full reductions.
234#[non_exhaustive]
235#[derive(Clone, Copy, Debug, Eq, PartialEq)]
236pub enum ReduceOp {
237    Sum,
238    Product,
239    /// Reassociated, overflow/underflow-safe product.
240    ///
241    /// Tracks a signed mantissa and an exact integer exponent per lane, so
242    /// factors outside the dtype range do not overflow or underflow a partial.
243    /// The reduction is reassociated and threaded like [`Self::Sum`].
244    ///
245    /// A zero, subnormal, infinite or NaN input cannot be represented by the
246    /// exponent-tracked accumulator; the affected output is then recomputed in
247    /// source order, keeping IEEE propagation (`0 * inf = NaN`, a zero or an
248    /// infinity result) identical to the sequential fold. For integers this is
249    /// the reassociated wrapping product and for complex dtypes the
250    /// source-order product.
251    StableProduct,
252    /// Sum of same-dtype rounded squares.
253    ///
254    /// Each input is first multiplied by itself without FMA contraction, then
255    /// accumulated under the same association policy as [`Self::Sum`].
256    SumSquares,
257    /// NaN-propagating maximum for `f32`/`f64`, ordered maximum for
258    /// `i32`/`i64`.
259    ///
260    /// Any NaN operand makes the result the canonical NaN of the dtype. The
261    /// identity written for an empty reduction is `-inf` for floats and the
262    /// dtype minimum for integers. Complex and `bool` dtypes are rejected.
263    /// When the reduced values contain both `-0.0` and `+0.0`, which zero is
264    /// returned is unspecified.
265    Max,
266    /// NaN-propagating minimum for `f32`/`f64`, ordered minimum for
267    /// `i32`/`i64`.
268    ///
269    /// Any NaN operand makes the result the canonical NaN of the dtype. The
270    /// identity written for an empty reduction is `+inf` for floats and the
271    /// dtype maximum for integers. Complex and `bool` dtypes are rejected.
272    /// When the reduced values contain both `-0.0` and `+0.0`, which zero is
273    /// returned is unspecified.
274    Min,
275}
276
277/// Dtype-erased reduction wrapper.
278///
279/// This is the erased replay boundary for full-tensor scalar reductions and
280/// axis reductions with a fixed output layout. It supports only operations with
281/// an unambiguous identity value in the selected dtype.
282///
283/// # Evaluation order
284///
285/// The op is matched once per execution; every loop runs a kernel
286/// specialized for one `(dtype, op)` pair. Integer results are exact
287/// (wrapping) and [`ReduceOp::Max`] / [`ReduceOp::Min`] do not depend on
288/// order, so the rules below only affect the rounding of floating point and
289/// complex [`ReduceOp::Sum`], [`ReduceOp::Product`], [`ReduceOp::StableProduct`]
290/// and [`ReduceOp::SumSquares`]. Two details of float max and min follow the
291/// visit order: a NaN input makes the result NaN, but which NaN (payload and
292/// sign) is returned is unspecified, and when the extremum is a zero its sign
293/// is unspecified if both `+0.0` and `-0.0` occur.
294///
295/// [`ReduceOp::Product`] is the reassociated product, matching PyTorch: it
296/// splits each run into lanes, uses the SIMD product where available, threads a
297/// full reduction, and combines the partials in a fixed tree. It is fast and
298/// never recomputes an output, but reassociating can overflow a lane to `inf`
299/// and underflow another to `0`, whose `inf * 0` combine is `NaN`; an
300/// alternating `2.0`/`0.5` input at `2^600`/`2^-600` scale can therefore give
301/// `NaN` where the source-order product is representable (tenferro-rs#2150).
302///
303/// [`ReduceOp::StableProduct`] avoids that. For `f32`/`f64` it tracks a signed
304/// mantissa and an exact integer exponent per lane and across runs and workers,
305/// so a partial cannot overflow or underflow; the final value is scaled with a
306/// correctly rounded `ldexp`. An input that is zero, subnormal, infinite or NaN
307/// cannot be represented this way, so an output that saw one is recomputed in
308/// source order, preserving IEEE propagation. It is reassociated and threaded
309/// like [`ReduceOp::Product`] otherwise. For integers it is the reassociated
310/// wrapping product and for complex dtypes the source-order product.
311///
312/// * Full reductions (from [`Self::compile`], or [`Self::compile_axes`]
313///   with every axis reduced) visit the source in a layout derived order:
314///   extent one axes are dropped, the rest are ordered by increasing
315///   absolute stride and contiguous axes are fused into runs. Each run is
316///   reduced by the SIMD kernel (unit stride, `simd` feature), a sixteen
317///   lane kernel (other unit stride runs) or an eight lane kernel (strided
318///   runs), and runs are combined left to right. A dense column major
319///   source therefore reduces exactly like one contiguous slice.
320/// * With a budget above one and more than `MINTHREADLENGTH` elements, a
321///   full reduction splits the traversal into one deterministic range per
322///   task. Each range runs the same run kernel as the serial path and
323///   the partials are combined in a fixed tree. The result is bitwise
324///   repeatable for a fixed layout and budget, but may differ between
325///   budgets and from the serial result.
326/// * Axis reductions with kept axes compute every output independently, so
327///   the result never depends on the token or its budget. Each output
328///   folds its reduced elements sequentially in caller reduced axis order,
329///   except that a unit stride leading reduced run (after fusing adjacent
330///   reduced axes) of length at least 16 is reduced by the contiguous run
331///   kernel, with runs folded in order. When instead the leading kept axis
332///   has unit stride, adjacent outputs are accumulated as a block from one
333///   contiguous source run per reduced element; each output still folds in
334///   caller reduced axis order.
335#[derive(Clone, Debug)]
336pub struct ErasedReducePlan {
337    dtype: KernelDType,
338    op: ReduceOp,
339    layout: ReduceLayout,
340}
341
342#[derive(Clone, Debug)]
343enum ReduceLayout {
344    Full {
345        dims: crate::inline::InlineVec<usize>,
346        src_strides: crate::inline::InlineVec<isize>,
347        traversal: FullTraversal,
348    },
349    Axes {
350        src_dims: crate::inline::InlineVec<usize>,
351        src_strides: crate::inline::InlineVec<isize>,
352        dest_dims: crate::inline::InlineVec<usize>,
353        dest_strides: crate::inline::InlineVec<isize>,
354        outer_axes: crate::inline::InlineVec<ReduceOuterAxis>,
355        inner_axes: crate::inline::InlineVec<ReduceInnerAxis>,
356        dest_total: usize,
357        reduce_total: usize,
358        /// Set when every source axis is reduced; the plan then runs the
359        /// full-reduction traversal.
360        full: Option<FullTraversal>,
361    },
362}
363
364#[derive(Clone, Copy, Debug, Default)]
365struct ReduceOuterAxis {
366    extent: usize,
367    source_step: isize,
368    source_reset: isize,
369    dest_step: isize,
370    dest_reset: isize,
371}
372#[derive(Clone, Copy, Debug, Default)]
373struct ReduceInnerAxis {
374    extent: usize,
375    source_step: isize,
376    source_reset: isize,
377}
378impl ReduceLayout {
379    fn src_dims(&self) -> &[usize] {
380        match self {
381            Self::Full { dims, .. } => dims,
382            Self::Axes { src_dims, .. } => src_dims,
383        }
384    }
385
386    fn src_strides(&self) -> &[isize] {
387        match self {
388            Self::Full { src_strides, .. } | Self::Axes { src_strides, .. } => src_strides,
389        }
390    }
391
392    fn check_src_layout(&self, src: &ErasedRawStridedRef<'_>) -> Result<()> {
393        if src.dims() != self.src_dims() || src.strides() != self.src_strides() {
394            return Err(StridedError::PlanLayoutMismatch);
395        }
396        Ok(())
397    }
398}
399
400#[derive(Clone, Copy, Debug)]
401struct AxesLayout<'a> {
402    outer_axes: &'a [ReduceOuterAxis],
403    inner_axes: &'a [ReduceInnerAxis],
404    dest_total: usize,
405    reduce_total: usize,
406}
407
408impl ErasedReducePlan {
409    /// Validate and store a full-reduction plan for one dtype and source layout.
410    pub fn compile(
411        dtype: KernelDType,
412        op: ReduceOp,
413        dims: &[usize],
414        src_strides: &[isize],
415    ) -> Result<Self> {
416        check_reduce_op_dtype(dtype, op)?;
417        if dims.len() != src_strides.len() {
418            return Err(StridedError::StrideLengthMismatch);
419        }
420        checked_total_len(dims)?;
421        let traversal = FullTraversal::compile(dims, src_strides)?;
422        Ok(Self {
423            dtype,
424            op,
425            layout: ReduceLayout::Full {
426                dims: dims.into(),
427                src_strides: src_strides.into(),
428                traversal,
429            },
430        })
431    }
432
433    /// Validate and store an axis-reduction plan for one dtype and fixed source/output layouts.
434    ///
435    /// `axes` names the source axes reduced away. Output dimensions must be the
436    /// remaining source dimensions in source-axis order. When all axes are
437    /// reduced, any output layout with exactly one reachable element is accepted.
438    #[allow(clippy::too_many_arguments)]
439    pub fn compile_axes(
440        dtype: KernelDType,
441        op: ReduceOp,
442        src_dims: &[usize],
443        src_strides: &[isize],
444        dest_dims: &[usize],
445        dest_strides: &[isize],
446        axes: &[usize],
447    ) -> Result<Self> {
448        check_reduce_op_dtype(dtype, op)?;
449        if src_dims.len() != src_strides.len() || dest_dims.len() != dest_strides.len() {
450            return Err(StridedError::StrideLengthMismatch);
451        }
452        checked_total_len(src_dims)?;
453        check_reduce_layout_offset_arithmetic(src_dims, src_strides)?;
454        let dest_total = checked_total_len(dest_dims)?;
455        check_reduce_layout_offset_arithmetic(dest_dims, dest_strides)?;
456        if !crate::layout_check::is_injective_layout(dest_dims, dest_strides) {
457            return Err(StridedError::NonInjectiveOutputLayout);
458        }
459        validate_unique_axes(axes, src_dims.len())?;
460
461        let kept_axes: crate::inline::InlineVec<usize> = (0..src_dims.len())
462            .filter(|axis| !axes.contains(axis))
463            .collect();
464        let expected_dest_dims: crate::inline::InlineVec<usize> =
465            kept_axes.iter().map(|&axis| src_dims[axis]).collect();
466        if expected_dest_dims.is_empty() {
467            if dest_total != 1 {
468                return Err(StridedError::ShapeMismatch(
469                    dest_dims.to_vec(),
470                    expected_dest_dims.to_vec(),
471                ));
472            }
473        } else if dest_dims != expected_dest_dims.as_slice() {
474            return Err(StridedError::ShapeMismatch(
475                dest_dims.to_vec(),
476                expected_dest_dims.to_vec(),
477            ));
478        }
479
480        let reduce_total = axes
481            .iter()
482            .try_fold(1usize, |total, &axis| total.checked_mul(src_dims[axis]))
483            .ok_or(StridedError::OffsetOverflow)?;
484        let outer_axes = compress_reduce_outer_axes(
485            kept_axes
486                .iter()
487                .enumerate()
488                .map(|(dest_axis, &src_axis)| {
489                    let extent = src_dims[src_axis];
490                    Ok(ReduceOuterAxis {
491                        extent,
492                        source_step: src_strides[src_axis],
493                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
494                        dest_step: dest_strides[dest_axis],
495                        dest_reset: checked_reduce_reset(extent, dest_strides[dest_axis])?,
496                    })
497                })
498                .collect::<Result<crate::inline::InlineVec<_>>>()?,
499        )?;
500        let inner_axes = compress_reduce_inner_axes(
501            axes.iter()
502                .map(|&src_axis| {
503                    let extent = src_dims[src_axis];
504                    Ok(ReduceInnerAxis {
505                        extent,
506                        source_step: src_strides[src_axis],
507                        source_reset: checked_reduce_reset(extent, src_strides[src_axis])?,
508                    })
509                })
510                .collect::<Result<crate::inline::InlineVec<_>>>()?,
511        )?;
512        let full = if kept_axes.is_empty() {
513            Some(FullTraversal::compile(src_dims, src_strides)?)
514        } else {
515            None
516        };
517        Ok(Self {
518            dtype,
519            op,
520            layout: ReduceLayout::Axes {
521                src_dims: src_dims.into(),
522                src_strides: src_strides.into(),
523                dest_dims: dest_dims.into(),
524                dest_strides: dest_strides.into(),
525                outer_axes,
526                inner_axes,
527                dest_total,
528                reduce_total,
529                full,
530            },
531        })
532    }
533
534    #[inline]
535    pub fn dtype(&self) -> KernelDType {
536        self.dtype
537    }
538
539    #[inline]
540    pub fn op(&self) -> ReduceOp {
541        self.op
542    }
543
544    /// Execute the reduction into an erased output descriptor.
545    pub fn execute(
546        &self,
547        dest: &mut ErasedRawStridedMut<'_>,
548        src: &ErasedRawStridedRef<'_>,
549        exec: Exec<'_>,
550    ) -> Result<()> {
551        check_dtype(self.dtype, dest.dtype())?;
552        check_dtype(self.dtype, src.dtype())?;
553        self.layout.check_src_layout(src)?;
554        match &self.layout {
555            ReduceLayout::Full { .. } => {
556                let dest_len = checked_total_len(dest.dims())?;
557                if dest_len != 1 {
558                    return Err(StridedError::RankMismatch(dest_len, 1));
559                }
560            }
561            ReduceLayout::Axes {
562                dest_dims,
563                dest_strides,
564                ..
565            } => {
566                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
567                {
568                    return Err(StridedError::PlanLayoutMismatch);
569                }
570            }
571        }
572
573        let result = match self.dtype {
574            KernelDType::F32 => {
575                let mut writer = reduce_writer::<f32>(dest)?;
576                dispatch_reduce::<f32, _>(self.op, &self.layout, &mut writer, src, exec)
577            }
578            KernelDType::F64 => {
579                let mut writer = reduce_writer::<f64>(dest)?;
580                dispatch_reduce::<f64, _>(self.op, &self.layout, &mut writer, src, exec)
581            }
582            KernelDType::I32 => {
583                let mut writer = reduce_writer::<i32>(dest)?;
584                dispatch_reduce::<i32, _>(self.op, &self.layout, &mut writer, src, exec)
585            }
586            KernelDType::I64 => {
587                let mut writer = reduce_writer::<i64>(dest)?;
588                dispatch_reduce::<i64, _>(self.op, &self.layout, &mut writer, src, exec)
589            }
590            KernelDType::C32 => {
591                let mut writer = reduce_writer::<Complex32>(dest)?;
592                dispatch_reduce::<Complex32, _>(self.op, &self.layout, &mut writer, src, exec)
593            }
594            KernelDType::C64 => {
595                let mut writer = reduce_writer::<Complex64>(dest)?;
596                dispatch_reduce::<Complex64, _>(self.op, &self.layout, &mut writer, src, exec)
597            }
598            _ => Err(StridedError::UnsupportedDType {
599                dtype: self.dtype.label(),
600            }),
601        };
602        result
603    }
604
605    /// On success, every reachable destination slot is fully overwritten;
606    /// unreachable holes are neither read nor initialized. Validation errors
607    /// are returned before any destination write. A panic during execution
608    /// may leave a partially initialized `MaybeUninit` destination, which is
609    /// still safely droppable; no readable value is promised for unwritten
610    /// reachable slots.
611    pub fn execute_uninit(
612        &self,
613        dest: &mut ErasedRawStridedUninitMut<'_>,
614        src: &ErasedRawStridedPtr<'_>,
615        exec: Exec<'_>,
616    ) -> Result<()> {
617        check_dtype(self.dtype, dest.dtype())?;
618        check_dtype(self.dtype, src.dtype())?;
619        validate_uninit_no_overlap(dest, src, 0)?;
620        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
621        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
622        self.layout.check_src_layout(&src)?;
623        match &self.layout {
624            ReduceLayout::Full { .. } => {
625                let total = checked_total_len(dest.dims())?;
626                if total != 1 {
627                    return Err(StridedError::RankMismatch(total, 1));
628                }
629            }
630            ReduceLayout::Axes {
631                dest_dims,
632                dest_strides,
633                ..
634            } => {
635                if dest.dims() != dest_dims.as_slice() || dest.strides() != dest_strides.as_slice()
636                {
637                    return Err(StridedError::PlanLayoutMismatch);
638                }
639            }
640        }
641        macro_rules! run {
642            ($ty:ty) => {{
643                let mut writer = reduce_uninit_writer::<$ty>(dest)?;
644                dispatch_reduce::<$ty, _>(self.op, &self.layout, &mut writer, &src, exec)
645            }};
646        }
647        match self.dtype {
648            KernelDType::F32 => run!(f32),
649            KernelDType::F64 => run!(f64),
650            KernelDType::I32 => run!(i32),
651            KernelDType::I64 => run!(i64),
652            KernelDType::C32 => run!(Complex32),
653            KernelDType::C64 => run!(Complex64),
654            _ => Err(StridedError::UnsupportedDType {
655                dtype: self.dtype.label(),
656            }),
657        }
658    }
659}
660
661fn reduce_writer<'a, T>(dest: &'a mut ErasedRawStridedMut<'_>) -> Result<RawReduceWriter<'a, T>>
662where
663    T: KernelStorageElement,
664{
665    let offset = dest.offset();
666    let data = dest.data_as_mut::<T>()?;
667    let ptr = data.as_mut_ptr();
668    let extent = data.len();
669    Ok(RawReduceWriter {
670        ptr,
671        extent,
672        offset,
673        _marker: core::marker::PhantomData,
674    })
675}
676
677fn reduce_uninit_writer<'a, T>(
678    dest: &'a mut ErasedRawStridedUninitMut<'_>,
679) -> Result<RawReduceWriter<'a, T>>
680where
681    T: KernelStorageElement,
682{
683    let offset = dest.offset();
684    let data = dest.data_as_uninit_mut::<T>()?;
685    let ptr = data.as_mut_ptr().cast::<T>();
686    let extent = data.len();
687    Ok(RawReduceWriter {
688        ptr,
689        extent,
690        offset,
691        _marker: core::marker::PhantomData,
692    })
693}
694
695fn check_reduce_dtype(dtype: KernelDType) -> Result<()> {
696    match dtype {
697        KernelDType::F32
698        | KernelDType::F64
699        | KernelDType::I32
700        | KernelDType::I64
701        | KernelDType::C32
702        | KernelDType::C64 => Ok(()),
703        _ => Err(StridedError::UnsupportedDType {
704            dtype: dtype.label(),
705        }),
706    }
707}
708
709fn check_reduce_op_dtype(dtype: KernelDType, op: ReduceOp) -> Result<()> {
710    if op == ReduceOp::SumSquares && !matches!(dtype, KernelDType::F32 | KernelDType::F64) {
711        return Err(StridedError::UnsupportedDType {
712            dtype: dtype.label(),
713        });
714    }
715    if matches!(op, ReduceOp::Max | ReduceOp::Min)
716        && !matches!(
717            dtype,
718            KernelDType::F32 | KernelDType::F64 | KernelDType::I32 | KernelDType::I64
719        )
720    {
721        return Err(StridedError::UnsupportedDType {
722            dtype: dtype.label(),
723        });
724    }
725    check_reduce_dtype(dtype)
726}
727
728fn checked_total_len(dims: &[usize]) -> Result<usize> {
729    if dims.is_empty() {
730        return Ok(1);
731    }
732    dims.iter()
733        .try_fold(1usize, |acc, &dim| acc.checked_mul(dim))
734        .ok_or(StridedError::OffsetOverflow)
735}
736
737fn execute_copy<T>(
738    plan: &CopyPlan,
739    dest: &mut ErasedRawStridedMut<'_>,
740    src: &ErasedRawStridedRef<'_>,
741    exec: Exec<'_>,
742) -> Result<()>
743where
744    T: Copy + Send + Sync + KernelStorageElement,
745{
746    let source_data = src.data_as::<T>()?;
747    let dest_dims = dest.dims();
748    let dest_strides = dest.strides();
749    let dest_offset = dest.offset();
750    let dest_data = dest.data_as_mut::<T>()?;
751    let source = unsafe {
752        RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
753    };
754    let mut dest =
755        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
756    plan.execute(&mut dest, &source, exec)
757}
758
759fn execute_concatenate<T>(
760    plan: &ConcatenatePlan,
761    dest: &mut ErasedRawStridedMut<'_>,
762    inputs: &[ErasedRawStridedRef<'_>],
763    mut exec: Exec<'_>,
764) -> Result<()>
765where
766    T: Copy + Send + Sync + KernelStorageElement,
767{
768    if inputs.len() != plan.input_count() {
769        return Err(StridedError::RankMismatch(inputs.len(), plan.input_count()));
770    }
771    let dest_dims = dest.dims();
772    let dest_strides = dest.strides();
773    let dest_offset = dest.offset();
774    let dest_data = dest.data_as_mut::<T>()?;
775    let mut dest_ref =
776        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
777    plan.check_dest_layout(&dest_ref)?;
778
779    for (position, input) in inputs.iter().enumerate() {
780        let input_data = input.data_as::<T>()?;
781        let input_ref = unsafe {
782            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
783        };
784        plan.check_input_layout(position, &input_ref)?;
785        plan.segment_offset(position, dest_offset)?;
786    }
787    if plan.prefers_whole_plan(&exec)
788        && !inputs.is_empty()
789        && inputs.len() <= crate::inline::OPERAND_CAP
790    {
791        // Build the typed descriptors on the stack: `RawStridedRef` is `Copy`,
792        // so this stays allocation-free within the operand bound. The
793        // whole-plan entry then splits the concatenated space and the typed
794        // `try_execute_parallel` reads each segment straight from the plan.
795        let first_data = inputs[0].data_as::<T>()?;
796        let first = unsafe {
797            RawStridedRef::new_unchecked(
798                first_data,
799                inputs[0].dims(),
800                inputs[0].strides(),
801                inputs[0].offset(),
802            )
803        };
804        let mut input_refs = [first; crate::inline::OPERAND_CAP];
805        for (slot, input) in input_refs.iter_mut().zip(inputs) {
806            let input_data = input.data_as::<T>()?;
807            *slot = unsafe {
808                RawStridedRef::new_unchecked(
809                    input_data,
810                    input.dims(),
811                    input.strides(),
812                    input.offset(),
813                )
814            };
815        }
816        return plan.execute(&mut dest_ref, &input_refs[..inputs.len()], exec);
817    }
818    for (position, input) in inputs.iter().enumerate() {
819        let input_data = input.data_as::<T>()?;
820        let input_ref = unsafe {
821            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
822        };
823        plan.execute_segment(position, &mut dest_ref, &input_ref, exec.reborrow())?;
824    }
825    Ok(())
826}
827
828fn execute_concatenate_uninit<T>(
829    plan: &ConcatenatePlan,
830    dest: &mut ErasedRawStridedUninitMut<'_>,
831    inputs: &[ErasedRawStridedPtr<'_>],
832    mut exec: Exec<'_>,
833) -> Result<()>
834where
835    T: Copy + Send + Sync + KernelStorageElement,
836{
837    let dest_dims = dest.dims();
838    let dest_strides = dest.strides();
839    let dest_offset = dest.offset();
840    let dest_data = dest.data_as_uninit_mut::<T>()?;
841    let mut dest_ref =
842        unsafe { RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset) };
843    plan.check_dest_layout(&dest_ref)?;
844
845    for (position, input) in inputs.iter().enumerate() {
846        // SAFETY: input/output overlap was rejected before forming references.
847        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
848        let input_data = input.data_as::<T>()?;
849        let input_ref = unsafe {
850            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
851        };
852        plan.check_input_layout(position, &input_ref)?;
853        plan.segment_offset(position, dest_offset)?;
854    }
855    if plan.prefers_whole_plan(&exec)
856        && !inputs.is_empty()
857        && inputs.len() <= crate::inline::OPERAND_CAP
858    {
859        // SAFETY: input/output overlap was rejected before forming references.
860        let first_erased = unsafe { inputs[0].try_as_ref_after_no_overlap() }?;
861        let mut erased_refs = [first_erased; crate::inline::OPERAND_CAP];
862        for (slot, input) in erased_refs.iter_mut().zip(inputs) {
863            // SAFETY: input/output overlap was rejected before forming references.
864            *slot = unsafe { input.try_as_ref_after_no_overlap() }?;
865        }
866        let first_data = erased_refs[0].data_as::<T>()?;
867        let first = unsafe {
868            RawStridedRef::new_unchecked(
869                first_data,
870                erased_refs[0].dims(),
871                erased_refs[0].strides(),
872                erased_refs[0].offset(),
873            )
874        };
875        let mut input_refs = [first; crate::inline::OPERAND_CAP];
876        for (slot, erased) in input_refs.iter_mut().zip(&erased_refs) {
877            let input_data = erased.data_as::<T>()?;
878            *slot = unsafe {
879                RawStridedRef::new_unchecked(
880                    input_data,
881                    erased.dims(),
882                    erased.strides(),
883                    erased.offset(),
884                )
885            };
886        }
887        return plan.execute_uninit(&mut dest_ref, &input_refs[..inputs.len()], exec);
888    }
889    for (position, input) in inputs.iter().enumerate() {
890        // SAFETY: input/output overlap was rejected before forming references.
891        let input = unsafe { input.try_as_ref_after_no_overlap() }?;
892        let input_data = input.data_as::<T>()?;
893        let input_ref = unsafe {
894            RawStridedRef::new_unchecked(input_data, input.dims(), input.strides(), input.offset())
895        };
896        plan.execute_segment_uninit(position, &mut dest_ref, &input_ref, exec.reborrow())?;
897    }
898    Ok(())
899}
900
901/// Runtime unary operation applied to the source of an [`ErasedUpdatePlan`].
902///
903/// The real dtypes have only [`Self::Identity`]; the complex dtypes also have
904/// [`Self::Conj`], matching the `ElementOp` choices a beta accumulator uses.
905#[non_exhaustive]
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
907pub enum UpdateOp {
908    /// `op(x) = x`.
909    Identity,
910    /// `op(x) = conj(x)`.
911    Conj,
912}
913
914/// Dtype-erased wrapper around [`UpdatePlan`].
915///
916/// The layout traversal is compiled once and stored without a scalar type;
917/// each [`execute`](Self::execute) names the concrete scalar type, resolves the
918/// runtime element op once per call, and then replays monomorphized loops.
919#[derive(Clone, Debug)]
920pub struct ErasedUpdatePlan {
921    dtype: KernelDType,
922    plan: UpdatePlan,
923}
924
925impl ErasedUpdatePlan {
926    /// Compile an update plan for one dtype and layout pair.
927    pub fn compile(
928        dtype: KernelDType,
929        dims: &[usize],
930        dst_strides: &[isize],
931        src_strides: &[isize],
932    ) -> Result<Self> {
933        check_static_indexing_dtype(dtype)?;
934        Ok(Self {
935            dtype,
936            plan: UpdatePlan::compile(dims, dst_strides, src_strides)?,
937        })
938    }
939
940    #[inline]
941    pub fn dtype(&self) -> KernelDType {
942        self.dtype
943    }
944
945    /// The compiled typed traversal.
946    #[inline]
947    pub fn plan(&self) -> &UpdatePlan {
948        &self.plan
949    }
950
951    /// `dest = alpha * op(src) + beta * dest` into an erased destination.
952    ///
953    /// `alpha`, `beta`, and `op` are resolved once per call; the layout check
954    /// and replay are the same allocation-free [`UpdatePlan`] path.
955    ///
956    /// # Examples
957    ///
958    /// ```
959    /// use strided_basic::{
960    ///     ErasedRawStridedMut, ErasedRawStridedRef, ErasedUpdatePlan, Exec, KernelDType,
961    ///     UpdateOp,
962    /// };
963    ///
964    /// let plan = ErasedUpdatePlan::compile(KernelDType::F64, &[2], &[1], &[1]).unwrap();
965    /// let src_data = [1.0_f64, 2.0];
966    /// let mut dst_data = [10.0_f64, 20.0];
967    /// let src = ErasedRawStridedRef::from_slice(&src_data, &[2], &[1], 0).unwrap();
968    /// let mut dst = ErasedRawStridedMut::from_slice_mut(&mut dst_data, &[2], &[1], 0).unwrap();
969    /// // dst = 2 * src + 3 * dst
970    /// plan.execute(&mut dst, &src, 2.0, 3.0, UpdateOp::Identity, Exec::serial())
971    ///     .unwrap();
972    /// drop(dst);
973    /// assert_eq!(dst_data, [32.0, 64.0]);
974    /// ```
975    pub fn execute<T>(
976        &self,
977        dest: &mut ErasedRawStridedMut<'_>,
978        src: &ErasedRawStridedRef<'_>,
979        alpha: T,
980        beta: T,
981        op: UpdateOp,
982        exec: Exec<'_>,
983    ) -> Result<()>
984    where
985        T: Copy
986            + Send
987            + Sync
988            + KernelStorageElement
989            + core::ops::Add<Output = T>
990            + core::ops::Mul<Output = T>
991            + Zero
992            + One
993            + PartialEq
994            + ElementOpApply,
995    {
996        check_dtype(self.dtype, dest.dtype())?;
997        check_dtype(self.dtype, src.dtype())?;
998        let source_data = src.data_as::<T>()?;
999        let dest_dims = dest.dims();
1000        let dest_strides = dest.strides();
1001        let dest_offset = dest.offset();
1002        let dest_data = dest.data_as_mut::<T>()?;
1003        let source = unsafe {
1004            RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
1005        };
1006        let mut dest_ref = unsafe {
1007            RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset)
1008        };
1009        match op {
1010            UpdateOp::Identity => {
1011                self.plan
1012                    .execute(&mut dest_ref, &source, alpha, beta, |x| x, exec)
1013            }
1014            UpdateOp::Conj => {
1015                self.plan
1016                    .execute(&mut dest_ref, &source, alpha, beta, |x| x.conj(), exec)
1017            }
1018        }
1019    }
1020
1021    /// `dest = alpha * op(src)` into an uninitialized erased destination.
1022    ///
1023    /// This is [`execute`](Self::execute) with `beta = 0`: the destination is
1024    /// never read, so a NaN-filled or uninitialized output cannot contaminate
1025    /// the result. On success every reachable destination element is written;
1026    /// validation errors are returned before any write. A panic during
1027    /// execution may leave a partially initialized `MaybeUninit` destination,
1028    /// which is still safely droppable; no readable value is promised for
1029    /// unwritten reachable slots.
1030    ///
1031    /// # Examples
1032    ///
1033    /// ```
1034    /// use core::mem::MaybeUninit;
1035    /// use strided_basic::{
1036    ///     ErasedRawStridedPtr, ErasedRawStridedRef, ErasedRawStridedUninitMut, ErasedUpdatePlan,
1037    ///     Exec, KernelDType, UpdateOp,
1038    /// };
1039    ///
1040    /// let plan = ErasedUpdatePlan::compile(KernelDType::F64, &[2], &[1], &[1]).unwrap();
1041    /// let src_data = [1.0_f64, 2.0];
1042    /// let mut dst_data = [MaybeUninit::<f64>::uninit(); 2];
1043    /// let src = ErasedRawStridedRef::from_slice(&src_data, &[2], &[1], 0).unwrap();
1044    /// let src_ptr = ErasedRawStridedPtr::from_ref(&src);
1045    /// let mut dst =
1046    ///     ErasedRawStridedUninitMut::from_uninit_slice(&mut dst_data, &[2], &[1], 0).unwrap();
1047    /// // dst = 2 * src
1048    /// plan.execute_uninit(&mut dst, &src_ptr, 2.0, UpdateOp::Identity, Exec::serial())
1049    ///     .unwrap();
1050    /// let values: Vec<f64> = dst_data.iter().map(|v| unsafe { v.assume_init() }).collect();
1051    /// assert_eq!(values, [2.0, 4.0]);
1052    /// ```
1053    ///
1054    /// # Errors
1055    ///
1056    /// Returns `DTypeMismatch` or `PlanLayoutMismatch` when a descriptor does
1057    /// not match the plan, and `OverlappingInputOutput` when the source
1058    /// overlaps the destination allocation, all before any destination write.
1059    pub fn execute_uninit<T>(
1060        &self,
1061        dest: &mut ErasedRawStridedUninitMut<'_>,
1062        src: &ErasedRawStridedPtr<'_>,
1063        alpha: T,
1064        op: UpdateOp,
1065        exec: Exec<'_>,
1066    ) -> Result<()>
1067    where
1068        T: Copy
1069            + Send
1070            + Sync
1071            + KernelStorageElement
1072            + core::ops::Mul<Output = T>
1073            + One
1074            + PartialEq
1075            + ElementOpApply,
1076    {
1077        check_dtype(self.dtype, dest.dtype())?;
1078        check_dtype(self.dtype, src.dtype())?;
1079        validate_uninit_no_overlap(dest, src, 0)?;
1080        // SAFETY: the owning erased entry rejected all input/output overlap before conversion.
1081        let src = unsafe { src.try_as_ref_after_no_overlap() }?;
1082        let source_data = src.data_as::<T>()?;
1083        let dest_dims = dest.dims();
1084        let dest_strides = dest.strides();
1085        let dest_offset = dest.offset();
1086        let dest_data = dest.data_as_uninit_mut::<T>()?;
1087        let source = unsafe {
1088            RawStridedRef::new_unchecked(source_data, src.dims(), src.strides(), src.offset())
1089        };
1090        let mut dest_ref = unsafe {
1091            RawStridedMut::new_unchecked(dest_data, dest_dims, dest_strides, dest_offset)
1092        };
1093        match op {
1094            UpdateOp::Identity => {
1095                self.plan
1096                    .execute_uninit(&mut dest_ref, &source, alpha, |x| x, exec)
1097            }
1098            UpdateOp::Conj => {
1099                self.plan
1100                    .execute_uninit(&mut dest_ref, &source, alpha, |x| x.conj(), exec)
1101            }
1102        }
1103    }
1104}
1105
1106fn execute_reduce_full<T, W, K>(
1107    dest: &mut W,
1108    src: &ErasedRawStridedRef<'_>,
1109    traversal: &FullTraversal,
1110    exec: Exec<'_>,
1111) -> Result<bool>
1112where
1113    T: ErasedReduceScalar,
1114    W: ReduceWriter<T>,
1115    K: ReduceKernel<T>,
1116{
1117    let source = src.data_as::<T>()?;
1118    let total = traversal.total();
1119    if total == 0 {
1120        // SAFETY: validated rank-zero destination layout proves the offset.
1121        unsafe { dest.write_at(dest.offset(), K::finish(K::identity())) };
1122        return Ok(false);
1123    }
1124    let acc = reduce_full_split::<T, K>(source, src.offset(), traversal, exec)?;
1125
1126    if K::needs_ordered_fallback(&acc) {
1127        // The caller re-runs this layout with `K::Fallback`; the destination is
1128        // left untouched so nothing observable is written twice.
1129        return Ok(true);
1130    }
1131    // SAFETY: validated rank-zero destination layout proves the offset.
1132    unsafe { dest.write_at(dest.offset(), K::finish(acc)) };
1133    Ok(false)
1134}
1135
1136/// Full reduction of `source` over the traversal, split across the budget of
1137/// `exec` above the repository threshold.
1138///
1139/// Splitting reassociates one output element's factors across ranges, so only
1140/// a kernel that may be reassociated takes it (tenferro-rs#2150). A stable
1141/// accumulator that detects an unrepresentable factor sets the fallback flag,
1142/// and the caller then recomputes the whole output in source order.
1143fn reduce_full_split<T, K>(
1144    source: &[T],
1145    source_base: isize,
1146    traversal: &FullTraversal,
1147    exec: Exec<'_>,
1148) -> Result<K::Acc>
1149where
1150    T: ErasedReduceScalar,
1151    K: ReduceKernel<T>,
1152{
1153    let total = traversal.total();
1154    // INVARIANT: (1) FullTraversal::compile checked every step/reset of the
1155    // source layout; (2) the raw source descriptor validated every reachable
1156    // offset; (3) execute checked exact plan-layout equality.
1157    if !K::reassociable() {
1158        // SAFETY: the three-link invariant proves every traversal offset is
1159        // readable.
1160        return unsafe {
1161            reduce_full_range::<T, K>(source.as_ptr(), source_base, traversal, 0..total)
1162        };
1163    }
1164    let source_ptr = crate::threading::SendPtr(source.as_ptr() as *mut T);
1165    crate::threading::parallel_map_reduce(
1166        0..total,
1167        crate::threading::for_len(exec, total),
1168        &|range| {
1169            // SAFETY: same three-link invariant as above; each range is
1170            // non-empty.
1171            unsafe {
1172                reduce_full_range::<T, K>(source_ptr.as_const(), source_base, traversal, range)
1173            }
1174        },
1175        &|left, right| Ok(K::combine(left?, right?)),
1176    )
1177}
1178
1179fn dispatch_reduce<T, W>(
1180    op: ReduceOp,
1181    layout: &ReduceLayout,
1182    dest: &mut W,
1183    src: &ErasedRawStridedRef<'_>,
1184    exec: Exec<'_>,
1185) -> Result<()>
1186where
1187    T: ErasedReduceScalar,
1188    W: ReduceWriter<T>,
1189{
1190    // The op is matched once per execution; every loop below is generic over
1191    // the selected kernel type.
1192    match op {
1193        ReduceOp::Sum => dispatch_reduce_kernel::<T, W, SumKernel>(layout, dest, src, exec),
1194        ReduceOp::Product => {
1195            dispatch_reduce_kernel::<T, W, ReassociatedProductKernel>(layout, dest, src, exec)
1196        }
1197        ReduceOp::StableProduct => {
1198            dispatch_reduce_kernel::<T, W, StableProductKernel>(layout, dest, src, exec)
1199        }
1200        ReduceOp::SumSquares => {
1201            dispatch_reduce_kernel::<T, W, SumSquaresKernel>(layout, dest, src, exec)
1202        }
1203        ReduceOp::Max => dispatch_reduce_kernel::<T, W, MaxKernel>(layout, dest, src, exec),
1204        ReduceOp::Min => dispatch_reduce_kernel::<T, W, MinKernel>(layout, dest, src, exec),
1205    }
1206}
1207
1208fn dispatch_reduce_kernel<T, W, K>(
1209    layout: &ReduceLayout,
1210    dest: &mut W,
1211    src: &ErasedRawStridedRef<'_>,
1212    mut exec: Exec<'_>,
1213) -> Result<()>
1214where
1215    T: ErasedReduceScalar,
1216    W: ReduceWriter<T>,
1217    K: ReduceKernel<T>,
1218{
1219    let fallback = match layout {
1220        ReduceLayout::Full { traversal, .. } => {
1221            execute_reduce_full::<T, W, K>(dest, src, traversal, exec.reborrow())?
1222        }
1223        ReduceLayout::Axes {
1224            outer_axes,
1225            inner_axes,
1226            dest_total,
1227            reduce_total,
1228            full,
1229            ..
1230        } => {
1231            let axes = AxesLayout {
1232                outer_axes,
1233                inner_axes,
1234                dest_total: *dest_total,
1235                reduce_total: *reduce_total,
1236            };
1237            match full {
1238                Some(traversal) => {
1239                    execute_reduce_full::<T, W, K>(dest, src, traversal, exec.reborrow())?
1240                }
1241                None => {
1242                    // An axis reduction falls back per output inside
1243                    // `reduce_axes_range`; it never asks for a layout-wide
1244                    // recompute.
1245                    execute_reduce_axes::<T, W, K>(dest, src, axes, exec.reborrow())?;
1246                    false
1247                }
1248            }
1249        }
1250    };
1251    if fallback {
1252        // Only a full reduction (one output) requests the layout-wide
1253        // recompute; `K::Fallback` must not itself request a fallback, so this
1254        // recurses at most once. Scalar kernels use `Fallback = Self` and never
1255        // set the flag, so the branch is unreachable for them.
1256        return dispatch_reduce_kernel::<T, W, K::Fallback>(layout, dest, src, exec);
1257    }
1258    Ok(())
1259}
1260
1261fn execute_reduce_axes<T, W, K>(
1262    dest: &mut W,
1263    src: &ErasedRawStridedRef<'_>,
1264    layout: AxesLayout<'_>,
1265    exec: Exec<'_>,
1266) -> Result<()>
1267where
1268    T: ErasedReduceScalar,
1269    W: ReduceWriter<T>,
1270    K: ReduceKernel<T>,
1271{
1272    if layout.dest_total == 0 {
1273        return Ok(());
1274    }
1275
1276    if layout.reduce_total == 0 {
1277        execute_reduce_axes_identity::<T, W, K>(dest, layout, exec)
1278    } else {
1279        execute_reduce_axes_split::<T, W, K>(dest, src, layout, exec)
1280    }
1281}
1282
1283fn execute_reduce_axes_split<T, W, K>(
1284    dest: &mut W,
1285    src: &ErasedRawStridedRef<'_>,
1286    layout: AxesLayout<'_>,
1287    exec: Exec<'_>,
1288) -> Result<()>
1289where
1290    T: ErasedReduceScalar,
1291    W: ReduceWriter<T>,
1292    K: ReduceKernel<T>,
1293{
1294    let source_data = src.data_as::<T>()?;
1295    let source_base = src.offset();
1296    let dest_base = dest.offset();
1297    // SAFETY: the validated reduction writer owns the destination allocation.
1298    let dest_ptr = unsafe { dest.ptr() };
1299    // The threshold counts source elements read (outputs times reduced
1300    // elements), as documented in docs/design/erased-execution-policy.md,
1301    // so a few thousand outputs with long reductions still split. Ranges
1302    // partition the independent outputs; with the output block kernel this
1303    // splits along the contiguous kept axis.
1304    let work = layout.dest_total.saturating_mul(layout.reduce_total);
1305    let exec = crate::threading::for_len(exec, work).limit(
1306        core::num::NonZeroUsize::new(layout.dest_total).unwrap_or(core::num::NonZeroUsize::MIN),
1307    );
1308    if !exec.is_serial() {
1309        let dest_ptr = crate::threading::SendPtr(dest_ptr);
1310        let source_ptr = crate::threading::SendPtr(source_data.as_ptr() as *mut T);
1311        return crate::threading::parallel_map_reduce(
1312            0..layout.dest_total,
1313            exec,
1314            &|range| {
1315                let parts = AxesRange {
1316                    source: source_ptr.as_const(),
1317                    source_base,
1318                    dest: dest_ptr.as_ptr(),
1319                    dest_base,
1320                    outer_axes: layout.outer_axes,
1321                    inner_axes: layout.inner_axes,
1322                    reduce_total: layout.reduce_total,
1323                };
1324                // SAFETY: see `execute_reduce_axes_split` serial call.
1325                unsafe { reduce_axes_range::<T, K>(parts, range) }
1326            },
1327            &|left, right| left.and(right),
1328        );
1329    }
1330
1331    let parts = AxesRange {
1332        source: source_data.as_ptr(),
1333        source_base,
1334        dest: dest_ptr,
1335        dest_base,
1336        outer_axes: layout.outer_axes,
1337        inner_axes: layout.inner_axes,
1338        reduce_total: layout.reduce_total,
1339    };
1340    // INVARIANT: (1) compile_axes checked signed source/destination spans and
1341    // every cursor step/reset; (2) raw input and output descriptors validated
1342    // every reachable offset; (3) execute checked exact plan-layout equality
1343    // before dispatch. Output ranges are disjoint, and the initialized entry
1344    // holds distinct borrows while the uninit entry rejected overlap.
1345    // SAFETY: the three-link invariant above; `reduce_total` is nonzero.
1346    unsafe { reduce_axes_range::<T, K>(parts, 0..layout.dest_total) }
1347}
1348
1349fn execute_reduce_axes_identity_serial<T, W, K>(dest: &mut W, layout: AxesLayout<'_>) -> Result<()>
1350where
1351    T: ErasedReduceScalar,
1352    W: ReduceWriter<T>,
1353    K: ReduceKernel<T>,
1354{
1355    let mut outer = ReduceOuterCursor::decode(0, 0, dest.offset(), layout.outer_axes)?;
1356    for output in 0..layout.dest_total {
1357        // INVARIANT: (1) compile_axes checked the destination span and every
1358        // destination step/reset, including -(extent-1)*stride; (2) the raw
1359        // destination descriptor validated every reachable offset; (3) execute
1360        // checked exact plan-layout equality before dispatch.
1361        // SAFETY: the three-link layout invariant proves this destination
1362        // cursor offset is in bounds; the source pointer is intentionally never
1363        // formed for an empty reduction domain.
1364        unsafe { dest.write_at(outer.dest_offset, K::finish(K::identity())) };
1365        if output + 1 < layout.dest_total {
1366            outer.advance();
1367        }
1368    }
1369    Ok(())
1370}
1371fn execute_reduce_axes_identity<T, W, K>(
1372    dest: &mut W,
1373    layout: AxesLayout<'_>,
1374    exec: Exec<'_>,
1375) -> Result<()>
1376where
1377    T: ErasedReduceScalar,
1378    W: ReduceWriter<T>,
1379    K: ReduceKernel<T>,
1380{
1381    let exec = crate::threading::for_len(exec, layout.dest_total);
1382    if !exec.is_serial() {
1383        return execute_reduce_axes_identity_parallel::<T, W, K>(dest, layout, exec);
1384    }
1385    execute_reduce_axes_identity_serial::<T, W, K>(dest, layout)
1386}
1387
1388fn execute_reduce_axes_identity_parallel<T, W, K>(
1389    dest: &mut W,
1390    layout: AxesLayout<'_>,
1391    exec: Exec<'_>,
1392) -> Result<()>
1393where
1394    T: ErasedReduceScalar,
1395    W: ReduceWriter<T>,
1396    K: ReduceKernel<T>,
1397{
1398    // SAFETY: the validated reduction writer owns the destination allocation.
1399    let dest_ptr = crate::threading::SendPtr(unsafe { dest.ptr() });
1400    let dest_offset_base = dest.offset();
1401    crate::threading::parallel_map_reduce(
1402        0..layout.dest_total,
1403        exec,
1404        &|range| {
1405            let range_end = range.end;
1406            let mut outer =
1407                ReduceOuterCursor::decode(range.start, 0, dest_offset_base, layout.outer_axes)?;
1408            let dest_ptr = dest_ptr.as_ptr();
1409            for output in range {
1410                // INVARIANT: (1) compile_axes checked the destination span and
1411                // every destination step/reset, including
1412                // -(extent-1)*stride; (2) the raw destination descriptor
1413                // validated every reachable pointer offset; (3) execute checked
1414                // exact plan-layout equality before dispatch.
1415                // SAFETY: the three-link layout invariant proves this
1416                // destination offset is in bounds; no source pointer is formed.
1417                unsafe {
1418                    dest_ptr
1419                        .offset(outer.dest_offset)
1420                        .write(K::finish(K::identity()))
1421                };
1422                if output + 1 < range_end {
1423                    outer.advance();
1424                }
1425            }
1426            Ok(())
1427        },
1428        &|left, right| left.and(right),
1429    )
1430}
1431
1432trait ErasedReduceScalar:
1433    KernelStorageElement
1434    + Copy
1435    + One
1436    + Zero
1437    + Send
1438    + Sync
1439    + crate::simd::MaybeSimdOps
1440    + crate::simd::MaybeSimdProduct
1441    + crate::simd::MaybeSimdSumSquares
1442    + crate::simd::MaybeSimdBlockFold
1443    + StableProductScalar
1444{
1445    fn reduce_sum(lhs: Self, rhs: Self) -> Self;
1446    fn reduce_product(lhs: Self, rhs: Self) -> Self;
1447    /// Identity of [`ReduceOp::Max`]; unreachable for dtypes the plan rejects.
1448    fn max_identity() -> Self;
1449    /// Identity of [`ReduceOp::Min`]; unreachable for dtypes the plan rejects.
1450    fn min_identity() -> Self;
1451    fn reduce_max(lhs: Self, rhs: Self) -> Self;
1452    fn reduce_min(lhs: Self, rhs: Self) -> Self;
1453}
1454
1455macro_rules! impl_float_erased_reduce_scalar {
1456    ($($ty:ty),* $(,)?) => {
1457        $(
1458            impl ErasedReduceScalar for $ty {
1459                #[inline(always)]
1460                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1461                    lhs + rhs
1462                }
1463
1464                #[inline(always)]
1465                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1466                    lhs * rhs
1467                }
1468
1469                #[inline(always)]
1470                fn max_identity() -> Self {
1471                    <$ty>::NEG_INFINITY
1472                }
1473
1474                #[inline(always)]
1475                fn min_identity() -> Self {
1476                    <$ty>::INFINITY
1477                }
1478
1479                #[inline(always)]
1480                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1481                    // Select form: both sides are evaluated and the NaN test
1482                    // picks the result, so lane loops lower to compare and
1483                    // blend instead of a data dependent branch.
1484                    // A NaN `lhs` is kept by the second test and a NaN
1485                    // `rhs` fails the comparison, so NaN propagates from
1486                    // either side. Equal values (including +0.0 and -0.0)
1487                    // return `rhs`.
1488                    if (lhs > rhs) | lhs.is_nan() {
1489                        lhs
1490                    } else {
1491                        rhs
1492                    }
1493                }
1494
1495                #[inline(always)]
1496                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1497                    // Select form: both sides are evaluated and the NaN test
1498                    // picks the result, so lane loops lower to compare and
1499                    // blend instead of a data dependent branch.
1500                    // A NaN `lhs` is kept by the second test and a NaN
1501                    // `rhs` fails the comparison, so NaN propagates from
1502                    // either side. Equal values (including +0.0 and -0.0)
1503                    // return `rhs`.
1504                    if (lhs < rhs) | lhs.is_nan() {
1505                        lhs
1506                    } else {
1507                        rhs
1508                    }
1509                }
1510            }
1511        )*
1512    };
1513}
1514
1515macro_rules! impl_complex_erased_reduce_scalar {
1516    ($($ty:ty),* $(,)?) => {
1517        $(
1518            impl ErasedReduceScalar for $ty {
1519                #[inline(always)]
1520                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1521                    lhs + rhs
1522                }
1523
1524                #[inline(always)]
1525                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1526                    lhs * rhs
1527                }
1528
1529                fn max_identity() -> Self {
1530                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1531                    unreachable!("complex max reduction is rejected at plan compile")
1532                }
1533
1534                fn min_identity() -> Self {
1535                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1536                    unreachable!("complex min reduction is rejected at plan compile")
1537                }
1538
1539                fn reduce_max(_lhs: Self, _rhs: Self) -> Self {
1540                    // INVARIANT: check_reduce_op_dtype rejects complex Max at compile time.
1541                    unreachable!("complex max reduction is rejected at plan compile")
1542                }
1543
1544                fn reduce_min(_lhs: Self, _rhs: Self) -> Self {
1545                    // INVARIANT: check_reduce_op_dtype rejects complex Min at compile time.
1546                    unreachable!("complex min reduction is rejected at plan compile")
1547                }
1548            }
1549        )*
1550    };
1551}
1552
1553macro_rules! impl_wrapping_erased_reduce_scalar {
1554    ($($ty:ty),* $(,)?) => {
1555        $(
1556            impl ErasedReduceScalar for $ty {
1557                #[inline(always)]
1558                fn reduce_sum(lhs: Self, rhs: Self) -> Self {
1559                    lhs.wrapping_add(rhs)
1560                }
1561
1562                #[inline(always)]
1563                fn reduce_product(lhs: Self, rhs: Self) -> Self {
1564                    lhs.wrapping_mul(rhs)
1565                }
1566
1567                #[inline(always)]
1568                fn max_identity() -> Self {
1569                    <$ty>::MIN
1570                }
1571
1572                #[inline(always)]
1573                fn min_identity() -> Self {
1574                    <$ty>::MAX
1575                }
1576
1577                #[inline(always)]
1578                fn reduce_max(lhs: Self, rhs: Self) -> Self {
1579                    lhs.max(rhs)
1580                }
1581
1582                #[inline(always)]
1583                fn reduce_min(lhs: Self, rhs: Self) -> Self {
1584                    lhs.min(rhs)
1585                }
1586            }
1587        )*
1588    };
1589}
1590
1591impl_float_erased_reduce_scalar!(f32, f64);
1592
1593impl_complex_erased_reduce_scalar!(Complex32, Complex64);
1594
1595impl_wrapping_erased_reduce_scalar!(i32, i64);
1596
1597fn validate_unique_axes(axes: &[usize], rank: usize) -> Result<()> {
1598    // Inline for the rank bound (P4); above it the spill may allocate.
1599    let mut inline = [false; strided_view::INLINE_RANK];
1600    let mut spill: Vec<bool> = Vec::new();
1601    let seen: &mut [bool] = if rank <= strided_view::INLINE_RANK {
1602        &mut inline[..rank]
1603    } else {
1604        spill.resize(rank, false);
1605        &mut spill
1606    };
1607    for &axis in axes {
1608        if axis >= rank {
1609            return Err(StridedError::InvalidAxis { axis, rank });
1610        }
1611        if seen[axis] {
1612            return Err(StridedError::InvalidAxis { axis, rank });
1613        }
1614        seen[axis] = true;
1615    }
1616    Ok(())
1617}
1618
1619struct ReduceOuterCursor<'a> {
1620    axes: &'a [ReduceOuterAxis],
1621    coords: CoordScratch,
1622    source_offset: isize,
1623    dest_offset: isize,
1624}
1625impl<'a> ReduceOuterCursor<'a> {
1626    fn decode(
1627        mut linear: usize,
1628        source_base: isize,
1629        dest_base: isize,
1630        axes: &'a [ReduceOuterAxis],
1631    ) -> Result<Self> {
1632        let mut coords = CoordScratch::new(axes.len());
1633        let mut source_offset = source_base;
1634        let mut dest_offset = dest_base;
1635        for (coord, axis) in coords.as_mut_slice().iter_mut().zip(axes) {
1636            // INVARIANT: a non-empty destination domain implies every outer
1637            // extent is nonzero before decode; compile-time span checks make
1638            // these one-time checked additions represent valid layout offsets.
1639            debug_assert!(axis.extent != 0);
1640            *coord = linear % axis.extent;
1641            linear /= axis.extent;
1642            source_offset = checked_offset_add(source_offset, axis.source_step, *coord)?;
1643            dest_offset = checked_offset_add(dest_offset, axis.dest_step, *coord)?;
1644        }
1645        Ok(Self {
1646            axes,
1647            coords,
1648            source_offset,
1649            dest_offset,
1650        })
1651    }
1652
1653    /// Coordinate along the first (fastest) compressed outer axis.
1654    #[inline]
1655    fn leading_coord(&self) -> usize {
1656        self.coords.first()
1657    }
1658
1659    #[inline]
1660    fn advance(&mut self) {
1661        // INVARIANT: compile_axes checked every signed step and reset delta;
1662        // descriptor validation plus exact layout equality proves each cursor
1663        // state is a reachable source/destination offset.
1664        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1665            let next = *coord + 1;
1666            if next < axis.extent {
1667                *coord = next;
1668                self.source_offset += axis.source_step;
1669                self.dest_offset += axis.dest_step;
1670                return;
1671            }
1672            *coord = 0;
1673            self.source_offset += axis.source_reset;
1674            self.dest_offset += axis.dest_reset;
1675        }
1676    }
1677}
1678struct ReduceInnerCursor<'a> {
1679    axes: &'a [ReduceInnerAxis],
1680    coords: CoordScratch,
1681    source_offset: isize,
1682}
1683impl<'a> ReduceInnerCursor<'a> {
1684    fn new(source_base: isize, axes: &'a [ReduceInnerAxis]) -> Self {
1685        Self {
1686            axes,
1687            coords: CoordScratch::new(axes.len()),
1688            source_offset: source_base,
1689        }
1690    }
1691
1692    #[inline]
1693    fn reset(&mut self, source_base: isize) {
1694        self.coords.as_mut_slice().fill(0);
1695        self.source_offset = source_base;
1696    }
1697
1698    #[inline]
1699    fn advance(&mut self) {
1700        // INVARIANT: compile_axes checked every signed source step and reset
1701        // delta, and the validated descriptor/layout chain proves each value
1702        // offset is reachable from the current outer source base.
1703        for (coord, axis) in self.coords.as_mut_slice().iter_mut().zip(self.axes) {
1704            let next = *coord + 1;
1705            if next < axis.extent {
1706                *coord = next;
1707                self.source_offset += axis.source_step;
1708                return;
1709            }
1710            *coord = 0;
1711            self.source_offset += axis.source_reset;
1712        }
1713    }
1714}
1715fn check_reduce_layout_offset_arithmetic(dims: &[usize], strides: &[isize]) -> Result<()> {
1716    if dims.len() != strides.len() {
1717        return Err(StridedError::StrideLengthMismatch);
1718    }
1719    let mut min_offset = 0isize;
1720    let mut max_offset = 0isize;
1721    for (&dim, &stride) in dims.iter().zip(strides) {
1722        let last =
1723            isize::try_from(dim.saturating_sub(1)).map_err(|_| StridedError::OffsetOverflow)?;
1724        let extent = stride
1725            .checked_mul(last)
1726            .ok_or(StridedError::OffsetOverflow)?;
1727        if extent < 0 {
1728            min_offset = min_offset
1729                .checked_add(extent)
1730                .ok_or(StridedError::OffsetOverflow)?;
1731        } else {
1732            max_offset = max_offset
1733                .checked_add(extent)
1734                .ok_or(StridedError::OffsetOverflow)?;
1735        }
1736    }
1737    let _ = (min_offset, max_offset);
1738    Ok(())
1739}
1740fn compress_reduce_outer_axes(
1741    axes: crate::inline::InlineVec<ReduceOuterAxis>,
1742) -> Result<crate::inline::InlineVec<ReduceOuterAxis>> {
1743    let mut compressed: crate::inline::InlineVec<ReduceOuterAxis> =
1744        crate::inline::InlineVec::with_capacity(axes.len());
1745    for axis in axes {
1746        if let Some(previous) = compressed.last_mut() {
1747            let previous_extent =
1748                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1749            let expected_source = previous
1750                .source_step
1751                .checked_mul(previous_extent)
1752                .ok_or(StridedError::OffsetOverflow)?;
1753            let expected_dest = previous
1754                .dest_step
1755                .checked_mul(previous_extent)
1756                .ok_or(StridedError::OffsetOverflow)?;
1757            if axis.source_step == expected_source && axis.dest_step == expected_dest {
1758                let fused_extent = previous
1759                    .extent
1760                    .checked_mul(axis.extent)
1761                    .ok_or(StridedError::OffsetOverflow)?;
1762                previous.extent = fused_extent;
1763                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1764                previous.dest_reset = checked_reduce_reset(fused_extent, previous.dest_step)?;
1765                continue;
1766            }
1767        }
1768        compressed.push(axis);
1769    }
1770    Ok(compressed)
1771}
1772fn compress_reduce_inner_axes(
1773    axes: crate::inline::InlineVec<ReduceInnerAxis>,
1774) -> Result<crate::inline::InlineVec<ReduceInnerAxis>> {
1775    let mut compressed: crate::inline::InlineVec<ReduceInnerAxis> =
1776        crate::inline::InlineVec::with_capacity(axes.len());
1777    for axis in axes {
1778        if let Some(previous) = compressed.last_mut() {
1779            let previous_extent =
1780                isize::try_from(previous.extent).map_err(|_| StridedError::OffsetOverflow)?;
1781            let expected_source = previous
1782                .source_step
1783                .checked_mul(previous_extent)
1784                .ok_or(StridedError::OffsetOverflow)?;
1785            if axis.source_step == expected_source {
1786                let fused_extent = previous
1787                    .extent
1788                    .checked_mul(axis.extent)
1789                    .ok_or(StridedError::OffsetOverflow)?;
1790                previous.extent = fused_extent;
1791                previous.source_reset = checked_reduce_reset(fused_extent, previous.source_step)?;
1792                continue;
1793            }
1794        }
1795        compressed.push(axis);
1796    }
1797    Ok(compressed)
1798}
1799fn checked_reduce_reset(extent: usize, stride: isize) -> Result<isize> {
1800    if extent == 0 {
1801        return Ok(0);
1802    }
1803    let last = isize::try_from(extent - 1).map_err(|_| StridedError::OffsetOverflow)?;
1804    stride
1805        .checked_mul(last)
1806        .and_then(isize::checked_neg)
1807        .ok_or(StridedError::OffsetOverflow)
1808}
1809fn checked_offset_add(base: isize, stride: isize, coord: usize) -> Result<isize> {
1810    let coord = isize::try_from(coord).map_err(|_| StridedError::OffsetOverflow)?;
1811    let scaled = stride
1812        .checked_mul(coord)
1813        .ok_or(StridedError::OffsetOverflow)?;
1814    base.checked_add(scaled).ok_or(StridedError::OffsetOverflow)
1815}
1816
1817struct CoordScratch {
1818    inline: [usize; RAW_FUSED_RANK_LIMIT],
1819    heap: Option<Vec<usize>>,
1820    len: usize,
1821}
1822
1823impl CoordScratch {
1824    fn new(len: usize) -> Self {
1825        if len <= RAW_FUSED_RANK_LIMIT {
1826            Self {
1827                inline: [0; RAW_FUSED_RANK_LIMIT],
1828                heap: None,
1829                len,
1830            }
1831        } else {
1832            Self {
1833                inline: [0; RAW_FUSED_RANK_LIMIT],
1834                heap: Some(vec![0; len]),
1835                len,
1836            }
1837        }
1838    }
1839
1840    #[inline]
1841    fn first(&self) -> usize {
1842        match &self.heap {
1843            Some(heap) => heap.first().copied().unwrap_or(0),
1844            None => self.inline[0],
1845        }
1846    }
1847
1848    fn as_mut_slice(&mut self) -> &mut [usize] {
1849        match &mut self.heap {
1850            Some(heap) => heap,
1851            None => &mut self.inline[..self.len],
1852        }
1853    }
1854}