Skip to main content

strided_basic/
erased.rs

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