Skip to main content

tenferro_cpu/
reduction.rs

1use std::ops::{Add, Mul};
2
3use num_traits::{Float, One, Zero};
4use strided_kernel::reduce_axis;
5
6use super::{typed_host_data, typed_view, typed_view_from_view};
7use tenferro_tensor::{Tensor, TensorRank, TensorRead, TensorView, TypedTensor, TypedTensorView};
8
9fn validate_axes(op: &'static str, axes: &[usize], rank: usize) -> crate::Result<()> {
10    let mut seen = vec![false; rank];
11    for &axis in axes {
12        if axis >= rank {
13            return Err(crate::Error::AxisOutOfBounds { op, axis, rank });
14        }
15        if seen[axis] {
16            return Err(crate::Error::DuplicateAxis {
17                op,
18                axis,
19                role: "axes",
20            });
21        }
22        seen[axis] = true;
23    }
24    Ok(())
25}
26
27fn ensure_host_tensor(op: &'static str, input: &Tensor) -> crate::Result<()> {
28    macro_rules! ensure {
29        ($tensor:expr) => {{
30            typed_host_data(op, $tensor)?;
31            Ok(())
32        }};
33    }
34
35    match input {
36        Tensor::F32(t) => ensure!(t),
37        Tensor::F64(t) => ensure!(t),
38        Tensor::I32(t) => ensure!(t),
39        Tensor::I64(t) => ensure!(t),
40        Tensor::Bool(t) => ensure!(t),
41        Tensor::C32(t) => ensure!(t),
42        Tensor::C64(t) => ensure!(t),
43    }
44}
45
46fn validate_reduced_axes_nonempty(
47    op: &'static str,
48    shape: &[usize],
49    axes: &[usize],
50) -> crate::Result<()> {
51    validate_axes(op, axes, shape.len())?;
52    for &axis in axes {
53        if shape[axis] == 0 {
54            return Err(crate::Error::InvalidConfig {
55                op,
56                message: format!("cannot reduce over zero-length axis {axis}"),
57            });
58        }
59    }
60    Ok(())
61}
62
63fn reduction_empty_axes_noop(
64    op: &'static str,
65    input: &Tensor,
66    axes: &[usize],
67) -> crate::Result<Option<Tensor>> {
68    validate_axes(op, axes, input.shape().len())?;
69    // INVARIANT: empty-axis reduction is semantic identity, but this public
70    // owned-output API must return an independently owned tensor.
71    Ok(axes.is_empty().then(|| input.clone()))
72}
73
74fn reduction_read_empty_axes_noop(
75    op: &'static str,
76    input: &TensorRead<'_>,
77    axes: &[usize],
78) -> crate::Result<Option<Tensor>> {
79    validate_axes(op, axes, input.shape().len())?;
80    if !axes.is_empty() {
81        return Ok(None);
82    }
83
84    match input.clone() {
85        TensorRead::Tensor(input) => {
86            ensure_host_tensor(op, input)?;
87            // INVARIANT: empty-axis reduction is semantic identity, but the
88            // `_read` API returns an owned tensor even for borrowed input.
89            Ok(Some(input.clone()))
90        }
91        TensorRead::View(input) => Ok(Some(view_to_contiguous_tensor(input)?)),
92    }
93}
94
95fn nan_propagating_max<T: Float>(a: T, b: T) -> T {
96    if a.is_nan() || b.is_nan() {
97        T::nan()
98    } else {
99        a.max(b)
100    }
101}
102
103fn nan_propagating_min<T: Float>(a: T, b: T) -> T {
104    if a.is_nan() || b.is_nan() {
105        T::nan()
106    } else {
107        a.min(b)
108    }
109}
110
111trait WrappingReductionElem: Copy + Clone + Send + Sync + Zero + One + 'static {
112    fn wrapping_add_elem(self, other: Self) -> Self;
113    fn wrapping_mul_elem(self, other: Self) -> Self;
114    fn min_value_elem() -> Self;
115    fn max_value_elem() -> Self;
116    fn max_elem(self, other: Self) -> Self;
117    fn min_elem(self, other: Self) -> Self;
118}
119
120macro_rules! impl_wrapping_reduction_elem {
121    ($ty:ty) => {
122        impl WrappingReductionElem for $ty {
123            fn wrapping_add_elem(self, other: Self) -> Self {
124                self.wrapping_add(other)
125            }
126
127            fn wrapping_mul_elem(self, other: Self) -> Self {
128                self.wrapping_mul(other)
129            }
130
131            fn min_value_elem() -> Self {
132                <$ty>::MIN
133            }
134
135            fn max_value_elem() -> Self {
136                <$ty>::MAX
137            }
138
139            fn max_elem(self, other: Self) -> Self {
140                self.max(other)
141            }
142
143            fn min_elem(self, other: Self) -> Self {
144                self.min(other)
145            }
146        }
147    };
148}
149
150impl_wrapping_reduction_elem!(i32);
151impl_wrapping_reduction_elem!(i64);
152
153pub fn reduce_sum(input: &Tensor, axes: &[usize]) -> crate::Result<Tensor> {
154    if let Some(output) = reduction_empty_axes_noop("reduce_sum", input, axes)? {
155        return Ok(output);
156    }
157
158    match input {
159        Tensor::F32(t) => Ok(Tensor::F32(typed_reduce_sum(t, axes)?)),
160        Tensor::F64(t) => Ok(Tensor::F64(typed_reduce_sum(t, axes)?)),
161        Tensor::I32(t) => Ok(Tensor::I32(typed_reduce_sum_wrapping(t, axes)?)),
162        Tensor::I64(t) => Ok(Tensor::I64(typed_reduce_sum_wrapping(t, axes)?)),
163        Tensor::Bool(_) => Err(crate::Error::backend_failure(
164            "reduce_sum",
165            "unsupported dtype Bool",
166        )),
167        Tensor::C32(t) => Ok(Tensor::C32(typed_reduce_sum(t, axes)?)),
168        Tensor::C64(t) => Ok(Tensor::C64(typed_reduce_sum(t, axes)?)),
169    }
170}
171
172pub(crate) fn reduce_sum_read(input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
173    if let Some(output) = reduction_read_empty_axes_noop("reduce_sum", &input, axes)? {
174        return Ok(output);
175    }
176
177    match input {
178        TensorRead::Tensor(input) => {
179            ensure_host_tensor("reduce_sum", input)?;
180            reduce_sum(input, axes)
181        }
182        TensorRead::View(TensorView::F32(t)) => Ok(Tensor::F32(typed_reduce_view(
183            &t,
184            axes,
185            |x| x,
186            |a, b| a + b,
187            f32::zero(),
188            "reduce_sum",
189        )?)),
190        TensorRead::View(TensorView::F64(t)) => Ok(Tensor::F64(typed_reduce_view(
191            &t,
192            axes,
193            |x| x,
194            |a, b| a + b,
195            f64::zero(),
196            "reduce_sum",
197        )?)),
198        TensorRead::View(TensorView::I32(t)) => Ok(Tensor::I32(typed_reduce_view(
199            &t,
200            axes,
201            |x| x,
202            |a, b| a.wrapping_add(b),
203            i32::zero(),
204            "reduce_sum",
205        )?)),
206        TensorRead::View(TensorView::I64(t)) => Ok(Tensor::I64(typed_reduce_view(
207            &t,
208            axes,
209            |x| x,
210            |a, b| a.wrapping_add(b),
211            i64::zero(),
212            "reduce_sum",
213        )?)),
214        TensorRead::View(TensorView::Bool(_)) => Err(crate::Error::backend_failure(
215            "reduce_sum",
216            "unsupported dtype Bool",
217        )),
218        TensorRead::View(TensorView::C32(t)) => Ok(Tensor::C32(typed_reduce_view(
219            &t,
220            axes,
221            |x| x,
222            |a, b| a + b,
223            num_complex::Complex32::zero(),
224            "reduce_sum",
225        )?)),
226        TensorRead::View(TensorView::C64(t)) => Ok(Tensor::C64(typed_reduce_view(
227            &t,
228            axes,
229            |x| x,
230            |a, b| a + b,
231            num_complex::Complex64::zero(),
232            "reduce_sum",
233        )?)),
234    }
235}
236
237pub fn reduce_prod(input: &Tensor, axes: &[usize]) -> crate::Result<Tensor> {
238    if let Some(output) = reduction_empty_axes_noop("reduce_prod", input, axes)? {
239        return Ok(output);
240    }
241
242    match input {
243        Tensor::F32(t) => Ok(Tensor::F32(typed_reduce_prod(t, axes)?)),
244        Tensor::F64(t) => Ok(Tensor::F64(typed_reduce_prod(t, axes)?)),
245        Tensor::I32(t) => Ok(Tensor::I32(typed_reduce_prod_wrapping(t, axes)?)),
246        Tensor::I64(t) => Ok(Tensor::I64(typed_reduce_prod_wrapping(t, axes)?)),
247        Tensor::Bool(_) => Err(crate::Error::backend_failure(
248            "reduce_prod",
249            "unsupported dtype Bool",
250        )),
251        Tensor::C32(t) => Ok(Tensor::C32(typed_reduce_prod(t, axes)?)),
252        Tensor::C64(t) => Ok(Tensor::C64(typed_reduce_prod(t, axes)?)),
253    }
254}
255
256pub(crate) fn reduce_prod_read(input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
257    if let Some(output) = reduction_read_empty_axes_noop("reduce_prod", &input, axes)? {
258        return Ok(output);
259    }
260
261    match input {
262        TensorRead::Tensor(input) => {
263            ensure_host_tensor("reduce_prod", input)?;
264            reduce_prod(input, axes)
265        }
266        TensorRead::View(TensorView::F32(t)) => Ok(Tensor::F32(typed_reduce_view(
267            &t,
268            axes,
269            |x| x,
270            |a, b| a * b,
271            f32::one(),
272            "reduce_prod",
273        )?)),
274        TensorRead::View(TensorView::F64(t)) => Ok(Tensor::F64(typed_reduce_view(
275            &t,
276            axes,
277            |x| x,
278            |a, b| a * b,
279            f64::one(),
280            "reduce_prod",
281        )?)),
282        TensorRead::View(TensorView::I32(t)) => Ok(Tensor::I32(typed_reduce_view(
283            &t,
284            axes,
285            |x| x,
286            |a, b| a.wrapping_mul(b),
287            i32::one(),
288            "reduce_prod",
289        )?)),
290        TensorRead::View(TensorView::I64(t)) => Ok(Tensor::I64(typed_reduce_view(
291            &t,
292            axes,
293            |x| x,
294            |a, b| a.wrapping_mul(b),
295            i64::one(),
296            "reduce_prod",
297        )?)),
298        TensorRead::View(TensorView::Bool(_)) => Err(crate::Error::backend_failure(
299            "reduce_prod",
300            "unsupported dtype Bool",
301        )),
302        TensorRead::View(TensorView::C32(t)) => Ok(Tensor::C32(typed_reduce_view(
303            &t,
304            axes,
305            |x| x,
306            |a, b| a * b,
307            num_complex::Complex32::one(),
308            "reduce_prod",
309        )?)),
310        TensorRead::View(TensorView::C64(t)) => Ok(Tensor::C64(typed_reduce_view(
311            &t,
312            axes,
313            |x| x,
314            |a, b| a * b,
315            num_complex::Complex64::one(),
316            "reduce_prod",
317        )?)),
318    }
319}
320
321pub fn reduce_max(input: &Tensor, axes: &[usize]) -> crate::Result<Tensor> {
322    if let Some(output) = reduction_empty_axes_noop("reduce_max", input, axes)? {
323        return Ok(output);
324    }
325
326    match input {
327        Tensor::F32(tensor) => Ok(Tensor::F32(typed_reduce_max(tensor, axes)?)),
328        Tensor::F64(tensor) => Ok(Tensor::F64(typed_reduce_max(tensor, axes)?)),
329        Tensor::I32(tensor) => Ok(Tensor::I32(typed_reduce_max_integer(tensor, axes)?)),
330        Tensor::I64(tensor) => Ok(Tensor::I64(typed_reduce_max_integer(tensor, axes)?)),
331        Tensor::Bool(_) | Tensor::C32(_) | Tensor::C64(_) => Err(crate::Error::backend_failure(
332            "reduce_max",
333            format!("unsupported dtype {:?}", input.dtype()),
334        )),
335    }
336}
337
338pub(crate) fn reduce_max_read(input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
339    if let Some(output) = reduction_read_empty_axes_noop("reduce_max", &input, axes)? {
340        return Ok(output);
341    }
342
343    match input {
344        TensorRead::Tensor(input) => {
345            ensure_host_tensor("reduce_max", input)?;
346            reduce_max(input, axes)
347        }
348        TensorRead::View(TensorView::F32(t)) => {
349            validate_reduced_axes_nonempty("reduce_max", t.shape(), axes)?;
350            Ok(Tensor::F32(typed_reduce_view(
351                &t,
352                axes,
353                |x| x,
354                nan_propagating_max,
355                f32::neg_infinity(),
356                "reduce_max",
357            )?))
358        }
359        TensorRead::View(TensorView::F64(t)) => {
360            validate_reduced_axes_nonempty("reduce_max", t.shape(), axes)?;
361            Ok(Tensor::F64(typed_reduce_view(
362                &t,
363                axes,
364                |x| x,
365                nan_propagating_max,
366                f64::neg_infinity(),
367                "reduce_max",
368            )?))
369        }
370        TensorRead::View(TensorView::I32(t)) => {
371            validate_reduced_axes_nonempty("reduce_max", t.shape(), axes)?;
372            Ok(Tensor::I32(typed_reduce_view(
373                &t,
374                axes,
375                |x| x,
376                |a, b| a.max_elem(b),
377                i32::min_value_elem(),
378                "reduce_max",
379            )?))
380        }
381        TensorRead::View(TensorView::I64(t)) => {
382            validate_reduced_axes_nonempty("reduce_max", t.shape(), axes)?;
383            Ok(Tensor::I64(typed_reduce_view(
384                &t,
385                axes,
386                |x| x,
387                |a, b| a.max_elem(b),
388                i64::min_value_elem(),
389                "reduce_max",
390            )?))
391        }
392        view => Err(crate::Error::backend_failure(
393            "reduce_max",
394            format!("unsupported dtype {:?}", view.dtype()),
395        )),
396    }
397}
398
399pub fn reduce_min(input: &Tensor, axes: &[usize]) -> crate::Result<Tensor> {
400    if let Some(output) = reduction_empty_axes_noop("reduce_min", input, axes)? {
401        return Ok(output);
402    }
403
404    match input {
405        Tensor::F32(tensor) => Ok(Tensor::F32(typed_reduce_min(tensor, axes)?)),
406        Tensor::F64(tensor) => Ok(Tensor::F64(typed_reduce_min(tensor, axes)?)),
407        Tensor::I32(tensor) => Ok(Tensor::I32(typed_reduce_min_integer(tensor, axes)?)),
408        Tensor::I64(tensor) => Ok(Tensor::I64(typed_reduce_min_integer(tensor, axes)?)),
409        Tensor::Bool(_) | Tensor::C32(_) | Tensor::C64(_) => Err(crate::Error::backend_failure(
410            "reduce_min",
411            format!("unsupported dtype {:?}", input.dtype()),
412        )),
413    }
414}
415
416pub(crate) fn reduce_min_read(input: TensorRead<'_>, axes: &[usize]) -> crate::Result<Tensor> {
417    if let Some(output) = reduction_read_empty_axes_noop("reduce_min", &input, axes)? {
418        return Ok(output);
419    }
420
421    match input {
422        TensorRead::Tensor(input) => {
423            ensure_host_tensor("reduce_min", input)?;
424            reduce_min(input, axes)
425        }
426        TensorRead::View(TensorView::F32(t)) => {
427            validate_reduced_axes_nonempty("reduce_min", t.shape(), axes)?;
428            Ok(Tensor::F32(typed_reduce_view(
429                &t,
430                axes,
431                |x| x,
432                nan_propagating_min,
433                f32::infinity(),
434                "reduce_min",
435            )?))
436        }
437        TensorRead::View(TensorView::F64(t)) => {
438            validate_reduced_axes_nonempty("reduce_min", t.shape(), axes)?;
439            Ok(Tensor::F64(typed_reduce_view(
440                &t,
441                axes,
442                |x| x,
443                nan_propagating_min,
444                f64::infinity(),
445                "reduce_min",
446            )?))
447        }
448        TensorRead::View(TensorView::I32(t)) => {
449            validate_reduced_axes_nonempty("reduce_min", t.shape(), axes)?;
450            Ok(Tensor::I32(typed_reduce_view(
451                &t,
452                axes,
453                |x| x,
454                |a, b| a.min_elem(b),
455                i32::max_value_elem(),
456                "reduce_min",
457            )?))
458        }
459        TensorRead::View(TensorView::I64(t)) => {
460            validate_reduced_axes_nonempty("reduce_min", t.shape(), axes)?;
461            Ok(Tensor::I64(typed_reduce_view(
462                &t,
463                axes,
464                |x| x,
465                |a, b| a.min_elem(b),
466                i64::max_value_elem(),
467                "reduce_min",
468            )?))
469        }
470        view => Err(crate::Error::backend_failure(
471            "reduce_min",
472            format!("unsupported dtype {:?}", view.dtype()),
473        )),
474    }
475}
476
477fn typed_reduce<T, M, R>(
478    input: &TypedTensor<T>,
479    axes: &[usize],
480    map_fn: M,
481    reduce_fn: R,
482    init: T,
483    label: &'static str,
484) -> crate::Result<TypedTensor<T>>
485where
486    T: Copy + Clone + Send + Sync,
487    M: Fn(T) -> T + Copy + Sync,
488    R: Fn(T, T) -> T + Copy + Sync,
489{
490    validate_reduced_axes_nonempty(label, input.shape(), axes)?;
491    if axes.is_empty() {
492        // INVARIANT: empty-axis typed reductions preserve values exactly while
493        // satisfying the owned-output contract.
494        return Ok(input.clone());
495    }
496
497    let output_shape: Vec<usize> = input
498        .shape()
499        .iter()
500        .enumerate()
501        .filter(|&(axis, _)| !axes.contains(&axis))
502        .map(|(_, &dim)| dim)
503        .collect();
504
505    let mut sorted_axes = axes.to_vec();
506    sorted_axes.sort_unstable_by(|a, b| b.cmp(a));
507    let Some((&first_axis, remaining_axes)) = sorted_axes.split_first() else {
508        // INVARIANT: this is the same empty-axis owned identity case handled
509        // above; it remains here to keep split-first control flow total.
510        return Ok(input.clone());
511    };
512
513    let input_view = typed_view(label, input)?;
514    let mut current = reduce_axis(&input_view, first_axis, map_fn, reduce_fn, init)
515        .map_err(|err| crate::Error::backend_failure(label, err))?;
516
517    for &axis in remaining_axes {
518        current = reduce_axis(&current.view(), axis, map_fn, reduce_fn, init)
519            .map_err(|err| crate::Error::backend_failure(label, err))?;
520    }
521
522    TypedTensor::from_vec_col_major(output_shape, current.into_data())
523}
524
525pub(crate) fn typed_reduce_view<T, M, R, TR>(
526    input: &TypedTensorView<'_, T, TR>,
527    axes: &[usize],
528    map_fn: M,
529    reduce_fn: R,
530    init: T,
531    label: &'static str,
532) -> crate::Result<TypedTensor<T>>
533where
534    T: Copy + Clone + Send + Sync + 'static,
535    M: Fn(T) -> T + Copy + Sync,
536    R: Fn(T, T) -> T + Copy + Sync,
537    TR: TensorRank,
538{
539    validate_reduced_axes_nonempty(label, input.shape(), axes)?;
540    if axes.is_empty() {
541        return view_to_dyn_contiguous(input);
542    }
543
544    let output_shape: Vec<usize> = input
545        .shape()
546        .iter()
547        .enumerate()
548        .filter(|&(axis, _)| !axes.contains(&axis))
549        .map(|(_, &dim)| dim)
550        .collect();
551
552    let mut sorted_axes = axes.to_vec();
553    sorted_axes.sort_unstable_by(|a, b| b.cmp(a));
554    let Some((&first_axis, remaining_axes)) = sorted_axes.split_first() else {
555        return view_to_dyn_contiguous(input);
556    };
557
558    let input_view = typed_view_from_view(label, input)?;
559    let mut current = reduce_axis(&input_view, first_axis, map_fn, reduce_fn, init)
560        .map_err(|err| crate::Error::backend_failure(label, err))?;
561
562    for &axis in remaining_axes {
563        current = reduce_axis(&current.view(), axis, map_fn, reduce_fn, init)
564            .map_err(|err| crate::Error::backend_failure(label, err))?;
565    }
566
567    TypedTensor::from_vec_col_major(output_shape, current.into_data())
568}
569
570fn view_to_dyn_contiguous<T, R>(input: &TypedTensorView<'_, T, R>) -> crate::Result<TypedTensor<T>>
571where
572    T: Clone + 'static,
573    R: TensorRank,
574{
575    let compact = input.to_contiguous()?;
576    let (shape, data) = compact.into_vec_col_major()?;
577    TypedTensor::from_vec_col_major(shape, data)
578}
579
580fn view_to_contiguous_tensor(input: TensorView<'_>) -> crate::Result<Tensor> {
581    match input {
582        TensorView::F32(t) => Ok(Tensor::F32(view_to_dyn_contiguous(&t)?)),
583        TensorView::F64(t) => Ok(Tensor::F64(view_to_dyn_contiguous(&t)?)),
584        TensorView::I32(t) => Ok(Tensor::I32(view_to_dyn_contiguous(&t)?)),
585        TensorView::I64(t) => Ok(Tensor::I64(view_to_dyn_contiguous(&t)?)),
586        TensorView::Bool(t) => Ok(Tensor::Bool(view_to_dyn_contiguous(&t)?)),
587        TensorView::C32(t) => Ok(Tensor::C32(view_to_dyn_contiguous(&t)?)),
588        TensorView::C64(t) => Ok(Tensor::C64(view_to_dyn_contiguous(&t)?)),
589    }
590}
591
592pub fn typed_reduce_sum<T>(input: &TypedTensor<T>, axes: &[usize]) -> crate::Result<TypedTensor<T>>
593where
594    T: Copy + Clone + Send + Sync + Zero + Add<Output = T>,
595{
596    typed_reduce(input, axes, |x| x, |a, b| a + b, T::zero(), "reduce_sum")
597}
598
599fn typed_reduce_sum_wrapping<T>(
600    input: &TypedTensor<T>,
601    axes: &[usize],
602) -> crate::Result<TypedTensor<T>>
603where
604    T: WrappingReductionElem,
605{
606    typed_reduce(
607        input,
608        axes,
609        |x| x,
610        |a, b| a.wrapping_add_elem(b),
611        T::zero(),
612        "reduce_sum",
613    )
614}
615
616pub fn typed_reduce_prod<T>(input: &TypedTensor<T>, axes: &[usize]) -> crate::Result<TypedTensor<T>>
617where
618    T: Copy + Clone + Send + Sync + One + Mul<Output = T>,
619{
620    typed_reduce(input, axes, |x| x, |a, b| a * b, T::one(), "reduce_prod")
621}
622
623fn typed_reduce_prod_wrapping<T>(
624    input: &TypedTensor<T>,
625    axes: &[usize],
626) -> crate::Result<TypedTensor<T>>
627where
628    T: WrappingReductionElem,
629{
630    typed_reduce(
631        input,
632        axes,
633        |x| x,
634        |a, b| a.wrapping_mul_elem(b),
635        T::one(),
636        "reduce_prod",
637    )
638}
639
640pub fn typed_reduce_max<T>(input: &TypedTensor<T>, axes: &[usize]) -> crate::Result<TypedTensor<T>>
641where
642    T: Float + Send + Sync,
643{
644    validate_reduced_axes_nonempty("reduce_max", input.shape(), axes)?;
645    typed_reduce(
646        input,
647        axes,
648        |x| x,
649        nan_propagating_max,
650        T::neg_infinity(),
651        "reduce_max",
652    )
653}
654
655fn typed_reduce_max_integer<T>(
656    input: &TypedTensor<T>,
657    axes: &[usize],
658) -> crate::Result<TypedTensor<T>>
659where
660    T: WrappingReductionElem,
661{
662    validate_reduced_axes_nonempty("reduce_max", input.shape(), axes)?;
663    typed_reduce(
664        input,
665        axes,
666        |x| x,
667        |a, b| a.max_elem(b),
668        T::min_value_elem(),
669        "reduce_max",
670    )
671}
672
673pub fn typed_reduce_min<T>(input: &TypedTensor<T>, axes: &[usize]) -> crate::Result<TypedTensor<T>>
674where
675    T: Float + Send + Sync,
676{
677    validate_reduced_axes_nonempty("reduce_min", input.shape(), axes)?;
678    typed_reduce(
679        input,
680        axes,
681        |x| x,
682        nan_propagating_min,
683        T::infinity(),
684        "reduce_min",
685    )
686}
687
688fn typed_reduce_min_integer<T>(
689    input: &TypedTensor<T>,
690    axes: &[usize],
691) -> crate::Result<TypedTensor<T>>
692where
693    T: WrappingReductionElem,
694{
695    validate_reduced_axes_nonempty("reduce_min", input.shape(), axes)?;
696    typed_reduce(
697        input,
698        axes,
699        |x| x,
700        |a, b| a.min_elem(b),
701        T::max_value_elem(),
702        "reduce_min",
703    )
704}