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 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 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 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 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(¤t.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(¤t.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}