1use crate::internal::Axis;
2use crate::internal::*;
3use crate::ops::binary::TypedBinOp;
4use crate::ops::cast::cast;
5use crate::ops::change_axes::wire_with_rank_broadcast;
6use crate::ops::element_wise::ElementWiseOp;
7use crate::ops::math::{Mul, Square, div, square};
8use std::convert::TryFrom;
9use std::iter::Sum;
10use std::mem::transmute;
11use tract_data::internal::ClampCast;
12use tract_data::itertools::Itertools;
13use tract_linalg::routines::Func;
14use tract_ndarray::prelude::*;
15use tract_num_traits::{AsPrimitive, Bounded};
16
17macro_rules! r {
18 ($($path:ident)::* ($dt:expr) ($($args:expr),*)) => {
19 match $dt {
20 DatumType::U8 => $($path)::*::<u8,_,_,_>($($args),*),
21 DatumType::I8 => $($path)::*::<i8,_,_,_>($($args),*),
22 DatumType::U16 => $($path)::*::<u16,_,_,_>($($args),*),
23 DatumType::I16 => $($path)::*::<i16,_,_,_>($($args),*),
24 DatumType::I32 => $($path)::*::<i32,_,_,_>($($args),*),
25 DatumType::I64 => $($path)::*::<i64,_,_,_>($($args),*),
26 DatumType::F16 => $($path)::*::<f16,_,_,_>($($args),*),
27 DatumType::F32 => $($path)::*::<f32,_,_,_>($($args),*),
28 DatumType::F64 => $($path)::*::<f64,_,_,_>($($args),*),
29 DatumType::QI8(_) => $($path)::*::<i8,_,_,_>($($args),*),
30 DatumType::QU8(_) => $($path)::*::<u8,_,_,_>($($args),*),
31 _ => bail!("{:?} is not a number", $dt)
32 }
33 };
34 ($($path:ident)::* ($dt:expr) ($($args:expr),*); $($q_path:ident)::* ($($q_args:expr),*)) => {
35 match $dt {
36 DatumType::U8 => $($path)::*::<u8,_,_,_>($($args),*),
37 DatumType::I8 => $($path)::*::<i8,_,_,_>($($args),*),
38 DatumType::U16 => $($path)::*::<u16,_,_,_>($($args),*),
39 DatumType::I16 => $($path)::*::<i16,_,_,_>($($args),*),
40 DatumType::I32 => $($path)::*::<i32,_,_,_>($($args),*),
41 DatumType::I64 => $($path)::*::<i64,_,_,_>($($args),*),
42 DatumType::F16 => $($path)::*::<f16,_,_,_>($($args),*),
43 DatumType::F32 => $($path)::*::<f32,_,_,_>($($args),*),
44 DatumType::F64 => $($path)::*::<f64,_,_,_>($($args),*),
45 DatumType::QI8(_) => $($q_path)::*::<i8,_,_,_>($($q_args),*),
46 DatumType::QU8(_) => $($q_path)::*::<u8,_,_,_>($($q_args),*),
47 _ => bail!("{:?} is not a number", $dt)
48 }
49 }
50}
51
52#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
53pub enum Reducer {
54 ArgMax(bool), ArgMin(bool),
56 Max,
57 Min,
58 Prod,
59 Sum,
60 MeanOfSquares,
61 All,
62 Any,
63}
64
65impl Reducer {
66 pub fn reduce(&self, axes: &[usize], input: &Tensor) -> TractResult<Tensor> {
67 use Reducer::*;
68 let dt = input.datum_type();
69 let output_shape: Vec<usize> = input
70 .shape()
71 .iter()
72 .enumerate()
73 .map(|(ax, &d)| if axes.contains(&ax) { 1 } else { d })
74 .collect();
75 let (zp, scale) = input.datum_type().zp_scale();
76 unsafe {
77 let mut t = match self {
78 ArgMax(last) => {
79 r!(Self::reduce_t(dt)(self, axes, &output_shape, input, argmax_t, *last))
80 }
81 ArgMin(last) => {
82 r!(Self::reduce_t(dt)(self, axes, &output_shape, input, argmin_t, *last))
83 }
84 Min => r!(Self::reduce_t(dt)(self, axes, &output_shape, input, min_t, ())),
85 Max => r!(Self::reduce_t(dt)(self, axes, &output_shape, input, max_t, ())),
86 Prod => {
87 r!(Self::reduce_t(dt)(self, axes, &output_shape, input, prod_t, ()); Self::reduce_t(self, axes, &output_shape, input, q_prod_t, (zp, scale)))
88 }
89 Sum => {
90 if dt.is_float() {
91 dispatch_floatlike!(Self::sum(dt)(self, axes, input))
92 } else {
93 r!(Self::reduce_t(dt)(
94 self,
95 axes,
96 &output_shape,
97 input,
98 q_sum_t,
99 (zp, scale)
100 ))
101 }
102 }
103 MeanOfSquares => self.mean_of_squares(axes, input)?,
104 All => Self::reduce_t(self, axes, &output_shape, input, all_bool, ()),
105 Any => Self::reduce_t(self, axes, &output_shape, input, any_bool, ()),
106 };
107 if input.datum_type().is_quantized()
108 && input.datum_type().unquantized() == t.datum_type().unquantized()
109 {
110 t.set_datum_type(input.datum_type());
111 }
112 Ok(t)
113 }
114 }
115
116 unsafe fn reduce_t<T, TO, F, A>(
117 &self,
118 axes: &[usize],
119 output_shape: &[usize],
120 input_tensor: &Tensor,
121 f: F,
122 args: A,
123 ) -> Tensor
124 where
125 F: for<'a> Fn(ArrayViewD<'a, T>, A) -> TO,
126 T: Copy + Datum,
127 TO: Copy + Datum,
128 A: Copy,
129 {
130 use ndarray::*;
131 let input = unsafe { input_tensor.to_array_view_unchecked::<T>() };
132 let result = Array::from_shape_fn(output_shape, |coords| {
133 let slice_spec: Vec<SliceInfoElem> = coords
134 .slice()
135 .iter()
136 .enumerate()
137 .map(|(ax, &d)| if axes.contains(&ax) { (..).into() } else { d.into() })
138 .collect();
139 let slice_info = SliceInfo::<_, IxDyn, IxDyn>::try_from(slice_spec).unwrap();
140 let slice = input.slice(&slice_info);
141 f(slice, args)
142 });
143 result.into_tensor()
144 }
145
146 unsafe fn sum<T>(&self, axes: &[usize], input: &Tensor) -> Tensor
151 where
152 T: Copy + Datum + num_traits::Zero + Sum,
153 f16: AsPrimitive<T>,
154 f32: AsPrimitive<T>,
155 {
156 if axes.len() == 0 {
157 return input.to_owned();
158 }
159
160 if axes.len() > 1 || axes[0] != input.rank() - 1 {
162 if T::datum_type() == f32::datum_type()
167 && axes.iter().enumerate().all(|(ix, &a)| a == ix)
168 && let Some(full) = unsafe { input.to_array_view_unchecked::<f32>() }.as_slice()
169 {
170 let k = axes.len();
171 let rows: usize = input.shape()[..k].iter().product();
172 let inner: usize = input.shape()[k..].iter().product();
173 if rows > 0 && inner > 0 {
174 let block = (32768 / inner).max(1);
175 let n_blocks = rows.div_ceil(block);
176 let mut partials = vec![0f32; n_blocks * inner];
177 tract_linalg::multithread::par_chunks_mut(
178 &mut partials,
179 inner,
180 rows * inner,
181 |first_block, chunk| {
182 for (i, p) in chunk.chunks_mut(inner).enumerate() {
183 let lo = (first_block + i) * block;
184 let hi = (lo + block).min(rows);
185 for r in lo..hi {
186 for (p, v) in p.iter_mut().zip(&full[r * inner..][..inner]) {
187 *p += *v;
188 }
189 }
190 }
191 Ok(())
192 },
193 )
194 .unwrap();
195 let mut out = vec![0f32; inner];
196 for p in partials.chunks_exact(inner) {
197 for (o, v) in out.iter_mut().zip(p) {
198 *o += *v;
199 }
200 }
201 let mut out_shape: Vec<usize> = vec![1; k];
202 out_shape.extend_from_slice(&input.shape()[k..]);
203 return ArrayD::from_shape_vec(IxDyn(&out_shape), out).unwrap().into_tensor();
204 }
205 }
206 let mut operative_axes = vec![];
207 let mut operative_shape: Vec<usize> = vec![];
208 for (ix, dim) in input.shape().iter().enumerate() {
209 if ix > 0 && axes.contains(&ix) && axes.contains(&(ix - 1)) {
211 *operative_shape.last_mut().unwrap() *= *dim;
212 } else if axes.contains(&ix) {
213 operative_axes.push(operative_shape.len());
214 operative_shape.push(*dim);
215 } else {
216 operative_shape.push(*dim);
217 }
218 }
219 let mut output = unsafe {
220 input
221 .to_array_view_unchecked::<T>()
222 .into_shape_with_order(operative_shape)
223 .unwrap()
224 .sum_axis(Axis(*operative_axes.iter().max().unwrap()))
225 };
226
227 for axis in operative_axes.iter().rev().skip(1) {
228 output = output.sum_axis(Axis(*axis));
229 }
230
231 let mut output = output.into_tensor();
232
233 for &axis in axes {
234 output.insert_axis(axis).unwrap();
235 }
236
237 output
238 } else {
239 let mut output: Option<ArrayD<T>> = None;
240 for axis in axes.iter().copied() {
241 let input_view = output
242 .as_ref()
243 .map(|o| o.view())
244 .unwrap_or_else(|| unsafe { input.to_array_view_unchecked::<T>() });
245
246 let reduced_dim = input_view.shape()[axis];
248 let input_stride = input_view.strides()[axis] as usize;
249 let output_shape = input_view
250 .shape()
251 .iter()
252 .enumerate()
253 .map(|(idx, dim)| if idx != axis { *dim } else { 1 })
254 .collect_vec();
255
256 output = Some(if let Some(full) = input_view.as_slice() {
257 let n_rows = full.len() / reduced_dim;
262 let mut out = vec![T::zero(); n_rows];
263 let total = full.len();
264 let sum_f16 = Func::ReduceSum.reduce_f16().unwrap();
266 let sum_f32 = Func::ReduceSum.reduce_f32().unwrap();
267 tract_linalg::multithread::par_chunks_mut(
268 &mut out,
269 1,
270 total,
271 |first_row, o| {
272 let rows = full[first_row * reduced_dim..][..o.len() * reduced_dim]
273 .chunks_exact(reduced_dim);
274 if reduced_dim >= 4 && T::datum_type() == f16::datum_type() {
275 for (x, c) in o.iter_mut().zip(rows) {
276 let c: &[f16] = unsafe { std::mem::transmute(c) };
277 *x = sum_f16.run_with_params(c, ())?.as_();
278 }
279 } else if reduced_dim >= 4 && T::datum_type() == f32::datum_type() {
280 for (x, c) in o.iter_mut().zip(rows) {
281 let c: &[f32] = unsafe { std::mem::transmute(c) };
282 *x = sum_f32.run_with_params(c, ())?.as_();
283 }
284 } else {
285 for (x, c) in o.iter_mut().zip(rows) {
289 *x = c.iter().cloned().sum::<T>();
290 }
291 }
292 Ok(())
293 },
294 )
295 .unwrap();
296 ArrayD::from_shape_vec(output_shape.clone(), out).unwrap()
297 } else {
298 ArrayD::from_shape_fn(output_shape.clone(), |coords| {
299 let first: *const T = &input_view[coords];
300 let mut sum = T::zero();
301 for i in 0..reduced_dim {
302 sum = sum + unsafe { *(first.add(i * input_stride)) };
303 }
304 sum
305 })
306 });
307 }
308 output.unwrap().into_tensor()
309 }
310 }
311
312 fn mean_of_squares(&self, axis: &[usize], input: &Tensor) -> TractResult<Tensor> {
313 let dt = input.datum_type();
314 let mut input = input.cast_to::<f32>()?.into_owned();
315 input.try_as_plain_ram_mut()?.as_slice_mut::<f32>()?.iter_mut().for_each(|x| *x = *x * *x);
316 let mut output = unsafe { self.sum::<f32>(axis, &input) };
317 let norm = output.len() as f32 / input.len() as f32;
318 output.try_as_plain_ram_mut()?.as_slice_mut::<f32>()?.iter_mut().for_each(|x| *x *= norm);
319 Ok(output.cast_to_dt(dt)?.into_owned())
320 }
321}
322
323fn argmax_t<T>(v: ArrayViewD<T>, last: bool) -> i64
324where
325 T: Copy + Datum + num_traits::Bounded + ::std::cmp::PartialOrd,
326{
327 v.iter()
328 .copied()
329 .enumerate()
330 .fold(
331 (0usize, T::min_value()),
332 |acc, v| {
333 if v.1 > acc.1 || (last && acc.1 == v.1) { v } else { acc }
334 },
335 )
336 .0 as i64
337}
338
339fn argmin_t<T>(v: ArrayViewD<T>, last: bool) -> i64
340where
341 T: Copy + Datum + num_traits::Bounded + ::std::cmp::PartialOrd,
342{
343 v.iter()
344 .copied()
345 .enumerate()
346 .fold(
347 (0usize, T::max_value()),
348 |acc, v| {
349 if v.1 < acc.1 || (last && acc.1 == v.1) { v } else { acc }
350 },
351 )
352 .0 as i64
353}
354
355fn max_t<T>(v: ArrayViewD<T>, _: ()) -> T
356where
357 T: Copy + Datum + num_traits::Bounded + ::std::cmp::PartialOrd,
358{
359 if T::datum_type() == f32::datum_type()
360 && let Some(slice) = v.as_slice()
361 && !slice.is_empty()
362 {
363 let slice = unsafe { transmute::<&[T], &[f32]>(slice) };
364 let max = Func::ReduceMax.reduce_f32().unwrap().run(slice).unwrap();
365 return unsafe { std::mem::transmute_copy::<f32, T>(&max) };
367 }
368 v.fold(T::min_value(), |acc, &v| if acc > v { acc } else { v })
369}
370
371fn min_t<T>(v: ArrayViewD<T>, _: ()) -> T
372where
373 T: Copy + Datum + num_traits::Bounded + ::std::cmp::PartialOrd,
374{
375 if T::datum_type() == f32::datum_type()
376 && let Some(slice) = v.as_slice()
377 && !slice.is_empty()
378 {
379 let slice = unsafe { transmute::<&[T], &[f32]>(slice) };
380 let min = Func::ReduceMin.reduce_f32().unwrap().run(slice).unwrap();
381 return unsafe { std::mem::transmute_copy::<f32, T>(&min) };
383 }
384 v.fold(T::max_value(), |acc, &v| if acc < v { acc } else { v })
385}
386
387fn prod_t<T>(v: ArrayViewD<T>, _: ()) -> T
388where
389 T: Copy + Datum + num_traits::One,
390{
391 v.fold(T::one(), |acc, &v| acc * v)
392}
393
394fn q_prod_t<T>(v: ArrayViewD<T>, zp_scale: (i32, f32)) -> T
395where
396 T: Copy + num_traits::AsPrimitive<f32> + Bounded + Datum,
397 f32: num_traits::AsPrimitive<T>,
398{
399 let (zp, scale) = zp_scale;
400 (v.fold(1f32, |acc, &v| acc * (v.as_() - zp as f32)) * scale.powi(v.len() as i32 - 1)
401 + zp as f32)
402 .clamp_cast()
403}
404
405fn q_sum_t<T>(v: ArrayViewD<T>, zp_scale: (i32, f32)) -> T
406where
407 T: Copy + Bounded + num_traits::AsPrimitive<i32> + Datum,
408 i32: num_traits::AsPrimitive<T>,
409{
410 let (zp, _) = zp_scale;
411 (v.fold(0i32, |acc, &v| acc + v.as_()) - zp * (v.len() as i32 - 1)).clamp_cast()
412}
413
414fn all_bool(v: ArrayViewD<bool>, _: ()) -> bool {
415 v.iter().all(|v| *v)
416}
417
418fn any_bool(v: ArrayViewD<bool>, _: ()) -> bool {
419 v.iter().any(|v| *v)
420}
421
422#[derive(Clone, Debug, new, Hash, PartialEq, Eq)]
423pub struct Reduce {
424 pub axes: TVec<usize>,
425 pub reducer: Reducer,
426}
427
428impl Op for Reduce {
429 fn name(&self) -> StaticName {
430 format!("Reduce<{:?}>", self.reducer).into()
431 }
432 fn info(&self) -> TractResult<Vec<String>> {
433 Ok(vec![format!("axes: {:?}", self.axes)])
434 }
435 op_as_typed_op!();
436}
437
438impl EvalOp for Reduce {
439 op_out_of_plan!();
440
441 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
442 Ok(tvec!(self.reducer.reduce(&self.axes, &inputs[0])?.into()))
443 }
444}
445
446impl TypedOp for Reduce {
447 fn input_roi(
448 &self,
449 model: &TypedModel,
450 node: &TypedNode,
451 ) -> TractResult<Option<TVec<Option<TDim>>>> {
452 crate::optim::propagate_roi::bubble_roi(model, node)
453 }
454
455 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
456 ensure!(self.axes.iter().tuple_windows().all(|(a, b)| a < b));
457 if inputs[0].datum_type == TDim::datum_type() {
458 bail!("Reduce input must be cast from TDim to i64 beforehand")
459 }
460 let mut shape: TVec<_> = inputs[0].shape.to_tvec();
461 for &ax in &self.axes {
462 shape[ax] = 1.to_dim();
463 }
464 let dt = if let Reducer::ArgMax(_) | Reducer::ArgMin(_) = self.reducer {
465 DatumType::I64
466 } else {
467 inputs[0].datum_type
468 };
469 Ok(tvec!(dt.fact(shape)))
470 }
471
472 fn declutter(
473 &self,
474 model: &TypedModel,
475 node: &TypedNode,
476 ) -> TractResult<Option<TypedModelPatch>> {
477 if let Some(patch) = self.declutter_mean_of_square(model, node)? {
478 return Ok(Some(patch));
479 }
480 if let Some(patch) = self.declutter_scalar_mul_then_sum(model, node)? {
481 return Ok(Some(patch));
482 }
483 if let Some(patch) = self.declutter_reduce_reduce(model, node)? {
484 return Ok(Some(patch));
485 }
486 if let Some(patch) = super::rms_norm::detect_rms_norm(self, model, node)? {
487 return Ok(Some(patch));
488 }
489 Ok(None)
490 }
491
492 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
493 let dt = inputs[0].datum_type;
494 let count: TDim = inputs[0].shape.iter().product();
495 match self.reducer {
496 Reducer::Sum
497 | Reducer::Prod
498 | Reducer::Min
499 | Reducer::Max
500 | Reducer::All
501 | Reducer::Any => Ok(tvec!((Cost::FMA(dt), count))),
502 Reducer::MeanOfSquares => Ok(tvec!((Cost::FMA(dt), count * 2))),
503 Reducer::ArgMax(_) | Reducer::ArgMin(_) => Ok(tvec!((Cost::FMA(dt), count))),
504 }
505 }
506
507 fn axes_mapping(
508 &self,
509 inputs: &[&TypedFact],
510 outputs: &[&TypedFact],
511 ) -> TractResult<AxesMapping> {
512 let mut letters = 'a'..;
513 let axes = (0..inputs[0].rank())
514 .flat_map(|ix| {
515 if self.axes.contains(&ix) {
516 tvec!(
517 Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
518 .input(0, ix),
519 Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
520 .output(0, ix),
521 )
522 } else {
523 tvec!(
524 Axis::new(letters.next().unwrap(), inputs.len(), outputs.len())
525 .input(0, ix)
526 .output(0, ix)
527 )
528 }
529 .into_iter()
530 })
531 .collect_vec();
532 AxesMapping::new(1, 1, axes)
533 }
534
535 fn change_axes(
536 &self,
537 model: &TypedModel,
538 node: &TypedNode,
539 _io: InOut,
540 change: &AxisOp,
541 ) -> TractResult<Option<AxisChangeConsequence>> {
542 let mut axes = tvec!();
543 for reduced in &self.axes {
544 rule_if_some!(axis = change.transform_axis(*reduced));
545 axes.push(axis);
546 }
547 axes.sort();
548 let op = Some(Box::new(Self { axes, ..self.clone() }) as _);
549 Ok(Some(AxisChangeConsequence::new(model, node, op, change)))
550 }
551
552 fn slice(
553 &self,
554 patch: &mut TypedModelPatch,
555 _model: &TypedModel,
556 node: &TypedNode,
557 _prefix: &str,
558 inputs: &[OutletId],
559 output_axis: usize,
560 _start: &TDim,
561 _end: &TDim,
562 ) -> TractResult<Option<TVec<OutletId>>> {
563 rule_if!(!self.axes.contains(&output_axis));
564 patch.wire_node(&node.name, &node.op, inputs).map(Some)
565 }
566
567 as_op!();
568}
569
570impl Reduce {
571 fn declutter_reduce_reduce(
572 &self,
573 model: &TypedModel,
574 node: &TypedNode,
575 ) -> TractResult<Option<TypedModelPatch>> {
576 use Reducer::*;
577 rule_if_some!(prec = model.linear_prec(node.id)?);
578 rule_if_some!(prec_reduce = prec.op_as::<Self>());
579 rule_if!(prec_reduce.reducer == self.reducer);
580 rule_if!([Sum, Prod, Min, Max].contains(&self.reducer));
581 let mut patch = TypedModelPatch::default();
582 let wire = patch.tap_model(model, prec.inputs[0])?;
583 let wire = patch.wire_node(
584 &node.name,
585 Self {
586 reducer: self.reducer,
587 axes: prec_reduce
588 .axes
589 .iter()
590 .chain(self.axes.iter())
591 .copied()
592 .sorted()
593 .dedup()
594 .collect(),
595 },
596 &[wire],
597 )?;
598 patch.shunt_outside(model, node.id.into(), wire[0])?;
599 Ok(Some(patch))
600 }
601
602 fn declutter_scalar_mul_then_sum(
603 &self,
604 model: &TypedModel,
605 node: &TypedNode,
606 ) -> TractResult<Option<TypedModelPatch>> {
607 if self.reducer == Reducer::Sum {
608 rule_if_some!(prec = model.linear_prec(node.id)?);
609 rule_if_some!(prec_bin = prec.op_as::<TypedBinOp>());
610 rule_if!(prec_bin.0.is::<Mul>());
611 let mul_input_fact = model.node_input_facts(prec.id)?;
612 rule_if_some!(
613 scalar_slot = mul_input_fact
614 .iter()
615 .position(|f| f.konst.as_ref().is_some_and(|k| k.volume() == 1))
616 );
617 let mut patch = TypedModelPatch::default();
618 let scalar = patch.tap_model(model, prec.inputs[scalar_slot])?;
619 let wire = patch.tap_model(model, prec.inputs[1 - scalar_slot])?;
620 let wire = patch.wire_node(&node.name, self.clone(), &[wire])?[0];
621 let wire = patch.wire_node(&prec.name, prec_bin.clone(), &[wire, scalar])?[0];
622 patch.shunt_outside(model, node.id.into(), wire)?;
623 return Ok(Some(patch));
624 }
625 Ok(None)
626 }
627
628 fn declutter_mean_of_square(
629 &self,
630 model: &TypedModel,
631 node: &TypedNode,
632 ) -> TractResult<Option<TypedModelPatch>> {
633 if self.reducer == Reducer::Sum {
634 rule_if_some!(prec = model.linear_prec(node.id)?);
635 rule_if_some!(prec_ew = prec.op_as::<ElementWiseOp>());
636 rule_if!(prec_ew.0.is::<Square>());
637 rule_if!(node.outputs.len() == 1);
638 rule_if!(node.outputs[0].successors.len() == 1);
639 let our_inlet = node.outputs[0].successors[0];
640 let succ = model.node(our_inlet.node);
641 rule_if_some!(succ_bin = succ.op_as::<TypedBinOp>());
642 rule_if!(succ_bin.0.is::<Mul>());
643 let other = succ.inputs[1 - our_inlet.slot];
644 rule_if_some!(other_konst = model.outlet_fact(other)?.uniform.as_ref());
645 let norm: TDim = self.axes.iter().map(|&ax| &prec.outputs[0].fact.shape[ax]).product();
646 rule_if_some!(norm = norm.as_i64());
647 rule_if!(norm > 0);
648 let norm = tensor0((norm as f32).recip());
649 if other_konst.close_enough(&norm, Approximation::Close).is_ok() {
650 let mut patch = TypedModelPatch::default();
651 let wire = patch.tap_model(model, prec.inputs[0])?;
652 let wire = patch.wire_node(
653 &node.name,
654 Reduce::new(self.axes.clone(), Reducer::MeanOfSquares),
655 &[wire],
656 )?[0];
657 patch.shunt_outside(model, succ.id.into(), wire)?;
658 return Ok(Some(patch));
659 }
660 }
661 Ok(None)
662 }
663}
664
665pub fn expand_mean_of_squares(
666 _ctx: &(),
667 model: &TypedModel,
668 node: &TypedNode,
669 name: &str,
670 op: &Reduce,
671) -> TractResult<Option<TypedModelPatch>> {
672 rule_if!(op.reducer == Reducer::MeanOfSquares);
673 let mut patch = TypedModelPatch::default();
674 let mut wire = tvec!(patch.tap_model(model, node.inputs[0])?);
675 let input_fact = model.outlet_fact(node.inputs[0])?;
676 let dt = input_fact.datum_type;
677 if dt != f32::datum_type() {
678 wire = patch.wire_node(format!("{name}.to_f32"), cast(f32::datum_type()), &wire)?;
679 }
680 wire = patch.wire_node(format!("{name}.sqr"), square(), &wire)?;
681 wire = patch.wire_node(
682 format!("{name}.sum"),
683 Reduce::new(op.axes.clone(), Reducer::Sum),
684 &wire,
685 )?;
686 let card = input_fact
687 .shape
688 .iter()
689 .enumerate()
690 .filter(|(ix, _dim)| op.axes.contains(ix))
691 .map(|(_ix, dim)| dim)
692 .product::<TDim>();
693 let card = patch.add_const(format!("{name}.card"), tensor0(card))?;
694 let card = patch.wire_node(format!("{name}.card_to_f32"), cast(f32::datum_type()), &[card])?;
695
696 wire =
697 wire_with_rank_broadcast(format!("{name}.norm"), &mut patch, div(), &[wire[0], card[0]])?;
698 if dt != f32::datum_type() {
699 wire = patch.wire_node(format!("{name}.from_f32"), cast(dt), &wire)?;
700 }
701 patch.shunt_outside(model, node.id.into(), wire[0])?;
702 Ok(Some(patch))
703}
704
705#[cfg(test)]
706mod tests {
707 use super::*;
708
709 #[test]
713 fn reduce_max_f32_contiguous_and_strided() {
714 let (r, c) = (5usize, 37usize); let data: Vec<f32> = (0..r * c).map(|i| ((i * 31 % 97) as f32) - 48.0).collect();
716 let t = Tensor::from_shape(&[r, c], &data).unwrap();
717
718 let got = Reducer::Max.reduce(&[1], &t).unwrap();
720 assert_eq!(got.shape(), &[r, 1]);
721 for (i, &g) in unsafe { got.as_slice_unchecked::<f32>() }.iter().enumerate() {
722 let want = data[i * c..(i + 1) * c].iter().copied().fold(f32::MIN, f32::max);
723 assert_eq!(g, want, "row {i}");
724 }
725
726 let got = Reducer::Max.reduce(&[0], &t).unwrap();
728 assert_eq!(got.shape(), &[1, c]);
729 for (j, &g) in unsafe { got.as_slice_unchecked::<f32>() }.iter().enumerate() {
730 let want = (0..r).map(|i| data[i * c + j]).fold(f32::MIN, f32::max);
731 assert_eq!(g, want, "col {j}");
732 }
733
734 let t1 = Tensor::from_shape(&[3, 1], &[1.0f32, -2.0, 3.0]).unwrap();
736 let got = Reducer::Max.reduce(&[1], &t1).unwrap();
737 assert_eq!(unsafe { got.as_slice_unchecked::<f32>() }, &[1.0, -2.0, 3.0]);
738 }
739
740 #[cfg(not(target_os = "wasi"))]
745 #[test]
746 fn sum_over_leading_axes_matches_ndarray() {
747 for (shape, axes) in [
748 (vec![160usize, 160, 48], vec![0usize, 1]),
749 (vec![7, 5, 3], vec![0]),
750 (vec![2, 3, 4, 5], vec![0, 1, 2]),
751 ] {
752 let len: usize = shape.iter().product();
753 let data: Vec<f32> = (0..len).map(|i| ((i * 37 % 113) as f32) - 56.0).collect();
754 let t = Tensor::from_shape(&shape, &data).unwrap();
755 let got = Reducer::Sum.reduce(&axes, &t).unwrap();
756 let mut want = t.to_plain_array_view::<f32>().unwrap().to_owned();
757 for _ in 0..axes.len() {
758 want = want.sum_axis(tract_ndarray::Axis(0));
759 }
760 let mut want = want.into_tensor();
761 for &ax in &axes {
762 want.insert_axis(ax).unwrap();
763 }
764 assert_eq!(got.shape(), want.shape());
765 got.close_enough(&want, Approximation::Close).unwrap();
766 #[cfg(target_family = "wasm")]
769 let pool = tract_linalg::multithread::Executor::RayonGlobal;
770 #[cfg(not(target_family = "wasm"))]
771 let pool = tract_linalg::multithread::Executor::multithread(4);
772 let got4 = tract_linalg::multithread::multithread_tract_scope(pool, || {
773 Reducer::Sum.reduce(&axes, &t)
774 })
775 .unwrap();
776 assert_eq!(got4, got);
777 }
778 }
779
780 #[test]
782 fn reduce_min_f32_contiguous_and_strided() {
783 let (r, c) = (5usize, 37usize); let data: Vec<f32> = (0..r * c).map(|i| ((i * 31 % 97) as f32) - 48.0).collect();
785 let t = Tensor::from_shape(&[r, c], &data).unwrap();
786
787 let got = Reducer::Min.reduce(&[1], &t).unwrap();
789 assert_eq!(got.shape(), &[r, 1]);
790 for (i, &g) in unsafe { got.as_slice_unchecked::<f32>() }.iter().enumerate() {
791 let want = data[i * c..(i + 1) * c].iter().copied().fold(f32::MAX, f32::min);
792 assert_eq!(g, want, "row {i}");
793 }
794
795 let got = Reducer::Min.reduce(&[0], &t).unwrap();
797 assert_eq!(got.shape(), &[1, c]);
798 for (j, &g) in unsafe { got.as_slice_unchecked::<f32>() }.iter().enumerate() {
799 let want = (0..r).map(|i| data[i * c + j]).fold(f32::MAX, f32::min);
800 assert_eq!(g, want, "col {j}");
801 }
802 }
803}