1use crate::internal::*;
2use downcast_rs::Downcast;
3use std::fmt::{self, Debug};
4use tract_data::dyn_eq::DynEq;
5use tract_data::itertools::izip;
6use tract_itertools::Itertools;
7use tract_linalg::multithread::BShare;
8use tract_linalg::{BinFn, BinOp};
9
10use super::math::{Add, Max, Min, Mul, Sub};
11use super::{cast::cast, math::SubF};
12use tract_linalg::routines::Func;
13
14pub trait BinMiniOp:
15 fmt::Debug + dyn_clone::DynClone + DynEq + Send + Sync + 'static + Downcast
16{
17 fn name(&self) -> &'static str;
18 fn validation(&self) -> Validation {
19 Validation::Accurate
20 }
21 fn operating_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType> {
22 a.common_super_type(b).with_context(|| format_err!("No super type for {:?} and {:?}", a, b))
23 }
24 fn result_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType>;
25 fn eval_in_a(&self, a: &mut Tensor, b: &Tensor) -> TractResult<()>;
26 fn eval_out_of_place(&self, c: &mut Tensor, a: &Tensor, b: &Tensor) -> TractResult<()>;
27
28 fn is_commutative(&self) -> bool {
29 true
30 }
31 fn neutral_element(&self) -> Option<i64> {
32 None
33 }
34 fn absorbing_element(&self) -> Option<i64> {
35 None
36 }
37
38 #[allow(unused_variables)]
39 fn maybe_eval_qbinary_as_float_op(
40 &self,
41 a: &TValue,
42 b: &TValue,
43 c_dt: &DatumType,
44 ) -> TractResult<Option<Tensor>> {
45 Ok(None)
46 }
47
48 fn generic_eval(&self, a: TValue, b: TValue, c_dt: DatumType) -> TractResult<Tensor> {
49 if let Some(tensor) = self.maybe_eval_qbinary_as_float_op(&a, &b, &c_dt)? {
50 return Ok(tensor);
51 }
52 if c_dt == a.datum_type() && a.shape() == b.shape() {
57 let mut a = a.into_tensor();
58 self.eval_in_a(&mut a, &b)?;
59 return Ok(a);
60 }
61 let c_shape = crate::broadcast::multi_broadcast(&[a.shape(), b.shape()])?;
62 if &*c_shape == a.shape() && c_dt == a.datum_type() {
63 let mut a = a.into_tensor();
64 self.eval_in_a(&mut a, &b)?;
65 Ok(a)
66 } else {
67 let mut c = unsafe { Tensor::uninitialized_dt(c_dt, &c_shape)? };
68 self.eval_out_of_place(&mut c, &a, &b)?;
69 Ok(c)
70 }
71 }
72 fn eval(&self, a: TValue, b: TValue, c_dt: DatumType) -> TractResult<Tensor> {
73 self.generic_eval(a, b, c_dt)
74 }
75 #[allow(unused_variables)]
76 fn declutter(
77 &self,
78 model: &TypedModel,
79 node: &TypedNode,
80 ) -> TractResult<Option<TypedModelPatch>> {
81 Ok(None)
82 }
83 #[allow(unused_variables)]
84 fn codegen(
85 &self,
86 model: &TypedModel,
87 node: &TypedNode,
88 ) -> TractResult<Option<TypedModelPatch>> {
89 Ok(None)
90 }
91 #[allow(unused_variables)]
92 fn cost_per_element(&self, dt: DatumType) -> TVec<(Cost, usize)> {
93 tvec!()
94 }
95 fn as_linalg_binop(&self) -> Option<tract_linalg::BinOp> {
96 None
97 }
98
99 #[allow(unused_variables)]
101 fn eval_symbolic(
102 &self,
103 ctx: &EvalContext,
104 inputs: TVec<TValue>,
105 ) -> TractResult<Option<TVec<TValue>>> {
106 Ok(None)
107 }
108
109 #[allow(unused_variables)]
111 fn uniform_tdim_comparison(&self, a: &TDim, b: &TDim) -> Option<TDim> {
112 None
113 }
114}
115dyn_clone::clone_trait_object!(BinMiniOp);
116dyn_eq::eq_trait_object!(BinMiniOp);
117downcast_rs::impl_downcast!(BinMiniOp);
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct TypedBinOp(pub Box<dyn BinMiniOp>, pub Option<DatumType>);
121
122impl Op for TypedBinOp {
123 fn name(&self) -> StaticName {
124 self.0.name().into()
125 }
126
127 fn validation(&self) -> Validation {
128 self.0.validation()
129 }
130
131 op_as_typed_op!();
132}
133
134impl TypedBinOp {
135 fn output_datum_type(&self, a_dt: DatumType, b_dt: DatumType) -> TractResult<DatumType> {
136 if let Some(dt) = self.1 { Ok(dt) } else { self.0.result_datum_type(a_dt, b_dt) }
137 }
138}
139
140impl EvalOp for TypedBinOp {
141 op_out_of_plan!();
142
143 fn eval(&self, ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
144 if let Some(result) = self.0.eval_symbolic(ctx, inputs.clone())? {
145 return Ok(result);
146 }
147 let (a, b) = args_2!(inputs);
148 ensure!(a.rank() == b.rank());
149 let c_dt = self.output_datum_type(a.datum_type(), b.datum_type())?;
150 Ok(tvec!(self.0.eval(a, b, c_dt)?.into_tvalue()))
151 }
152}
153
154impl TypedBinOp {
155 fn combine_uniform_tdim(&self, a: &TDim, b: &TDim) -> Option<TDim> {
156 if let Some(result) = self.0.uniform_tdim_comparison(a, b) {
158 return Some(result);
159 }
160 let a = tensor0(a.clone()).into_tvalue();
161 let b = tensor0(b.clone()).into_tvalue();
162 let result = self.0.eval(a, b, TDim::datum_type()).ok()?;
163 result
164 .try_as_plain_ram()
165 .ok()
166 .and_then(|d| d.as_slice::<TDim>().ok())
167 .and_then(|s| s.first())
168 .cloned()
169 .map(|d| d.reduce())
170 }
171
172 fn combine_uniform_tdim_with_konst(&self, a: &TDim, konst: &Tensor) -> Option<TDim> {
173 if konst.len() != 1 {
174 return None;
175 }
176 let b_int: Option<i64> =
178 if konst.datum_type().is_integer() || konst.datum_type().is::<bool>() {
179 konst.cast_to_scalar::<i64>().ok()
180 } else if konst.datum_type().is_float() {
181 konst.cast_to_scalar::<f64>().ok().and_then(|f| {
182 if (f - f.round()).abs() < 1e-6 { Some(f.round() as i64) } else { None }
183 })
184 } else {
185 None
186 };
187 if let Some(b) = b_int {
188 return self.combine_uniform_tdim(a, &TDim::Val(b));
189 }
190 if self.0.neutral_element() == Some(1)
192 && let Some(f) = konst.cast_to_scalar::<f64>().ok().filter(|&f| f > 0.0)
193 {
194 let n = (1.0 / f).round() as u64;
195 if n >= 2 && (f * n as f64 - 1.0).abs() < 1e-6 {
196 return Some(TDim::Div(Box::new(a.clone()), n).reduce());
197 }
198 }
199 None
200 }
201}
202
203impl TypedOp for TypedBinOp {
204 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
205 if inputs[0].rank() != inputs[1].rank() {
206 bail!(
207 "Typed ops require rank match. Invalid inputs for {}: {}",
208 self.name(),
209 inputs.iter().map(|s| format!("{s:?}")).join(" ; ")
210 );
211 }
212 let out_dt = self.output_datum_type(inputs[0].datum_type, inputs[1].datum_type)?;
213 let mut fact = out_dt.fact(&*crate::broadcast::multi_broadcast(&[
214 &inputs[0].shape.to_tvec(),
215 &inputs[1].shape.to_tvec(),
216 ])?);
217 if let (Some(a), Some(b)) = (&inputs[0].uniform_tdim, &inputs[1].uniform_tdim) {
218 fact.uniform_tdim = self.combine_uniform_tdim(a, b);
219 if fact.uniform_tdim.is_none() && self.0.is::<crate::ops::logic::And>() {
221 fact.uniform_tdim = Some(TDim::Mul(vec![a.clone(), b.clone()]).reduce());
222 }
223 }
224 if fact.uniform_tdim.is_none() {
226 for (expr, konst_fact) in [
227 (inputs[0].uniform_tdim.as_ref(), inputs[1]),
228 (inputs[1].uniform_tdim.as_ref(), inputs[0]),
229 ] {
230 let Some(a) = expr else { continue };
231 let Some(konst) = konst_fact.konst.as_ref() else { continue };
232 fact.uniform_tdim = self.combine_uniform_tdim_with_konst(a, konst);
233 if fact.uniform_tdim.is_some() {
234 break;
235 }
236 }
237 }
238 Ok(tvec!(fact))
239 }
240
241 fn input_roi(
242 &self,
243 model: &TypedModel,
244 node: &TypedNode,
245 ) -> TractResult<Option<TVec<Option<TDim>>>> {
246 if self.0.neutral_element() == Some(1) {
249 for (mask_ix, other_ix) in [(0usize, 1usize), (1, 0)] {
250 let fact = model.outlet_fact(node.inputs[mask_ix])?;
251 if let Some(mask_expr) = &fact.uniform_tdim {
252 let mut rois = tvec![None; node.inputs.len()];
253 rois[other_ix] = Some(mask_expr.clone());
254 return Ok(Some(rois));
255 }
256 }
257 }
258 crate::optim::propagate_roi::bubble_roi(model, node)
260 }
261
262 fn change_axes(
263 &self,
264 model: &TypedModel,
265 node: &TypedNode,
266 _io: InOut,
267 change: &AxisOp,
268 ) -> TractResult<Option<AxisChangeConsequence>> {
269 if let AxisOp::Rm(rm) = change {
270 let (inputs, outputs) = model.node_facts(node.id)?;
271 if inputs.len() >= 2
272 && outputs.len() >= 1
273 && inputs[0].rank() > *rm
274 && inputs[1].rank() > *rm
275 && outputs[0].rank() > *rm
276 {
277 rule_if!(inputs[0].shape[*rm].is_one());
278 rule_if!(inputs[1].shape[*rm].is_one());
279 rule_if!(outputs[0].shape[*rm].is_one());
280 }
281 }
282 Ok(Some(AxisChangeConsequence::new(model, node, None, change)))
283 }
284
285 fn axes_mapping(
286 &self,
287 inputs: &[&TypedFact],
288 outputs: &[&TypedFact],
289 ) -> TractResult<AxesMapping> {
290 AxesMapping::natural(inputs, outputs)
291 }
292
293 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
294 let count: TDim = self.output_facts(inputs)?[0].shape.iter().product();
295 Ok(self
296 .0
297 .cost_per_element(inputs[0].datum_type)
298 .into_iter()
299 .map(|(c, n)| (c, count.clone() * n))
300 .collect())
301 }
302
303 fn slice(
304 &self,
305 patch: &mut TypedModelPatch,
306 _model: &TypedModel,
307 _node: &TypedNode,
308 prefix: &str,
309 inputs: &[OutletId],
310 _output_axis: usize,
311 _start: &TDim,
312 _end: &TDim,
313 ) -> TractResult<Option<TVec<OutletId>>> {
314 Ok(Some(patch.wire_node(prefix, self.clone(), inputs)?))
315 }
316
317 fn declutter(
318 &self,
319 model: &TypedModel,
320 node: &TypedNode,
321 ) -> TractResult<Option<TypedModelPatch>> {
322 let (a_dt, b_dt) = if let &[a, b] = &*model.node_input_facts(node.id)? {
323 (a.datum_type().unwrap(), b.datum_type().unwrap())
324 } else {
325 unreachable!("TypedBinOp has two inputs.")
326 };
327 if let Some(neutral_patch) =
328 declutter_neutral(model, node, self.0.as_ref(), self.output_datum_type(a_dt, b_dt)?)?
329 {
330 return Ok(Some(neutral_patch));
331 }
332 if let Some(absorbing_patch) = declutter_absorbing(model, node, self.0.as_ref())? {
333 return Ok(Some(absorbing_patch));
334 }
335 if let Some(broadcast_patch) =
336 declutter_broadcasting_operand_1(model, node, self.0.clone())?
337 {
338 return Ok(Some(broadcast_patch));
339 }
340 self.0.declutter(model, node)
341 }
342
343 fn codegen(
344 &self,
345 model: &TypedModel,
346 node: &TypedNode,
347 ) -> TractResult<Option<TypedModelPatch>> {
348 if let Some(linalg_bin_op) = self.0.as_linalg_binop() {
349 let input_facts = model.node_input_facts(node.id)?;
350 let must_swap_inputs =
351 input_facts.iter().collect_tuple().is_some_and(|(a_fact, b_fact)| {
352 (a_fact.shape.volume() - b_fact.shape.volume()).prove_strict_negative()
353 });
354 let (operand_1, operand_2) = if must_swap_inputs {
355 (input_facts[1], input_facts[0])
356 } else {
357 (input_facts[0], input_facts[1])
358 };
359
360 let (by_scalar_should_be_efficient, unicast_should_be_efficient) =
361 find_most_efficient_config(model, node, must_swap_inputs)?;
362
363 let c_dt = self.output_datum_type(operand_1.datum_type, operand_2.datum_type)?;
365 let op_is_quant = c_dt.is_quantized()
366 || operand_1.datum_type.is_quantized()
367 || operand_2.datum_type.is_quantized();
368
369 let c_dt = self.output_datum_type(operand_1.datum_type, operand_2.datum_type)?;
371 let c_shape = crate::broadcast::multi_broadcast(&[
372 operand_1.shape.clone(),
373 operand_2.shape.clone(),
374 ])?;
375 let can_eval_in_a =
376 (c_shape.to_vec() == operand_1.shape.to_vec()) && (c_dt == operand_1.datum_type);
377
378 let inputs = if must_swap_inputs {
380 let mut swap_input = node.inputs.clone();
381 swap_input.swap(0, 1);
382 swap_input
383 } else {
384 node.inputs.clone()
385 };
386 let actual_linalg_op =
387 if must_swap_inputs { linalg_bin_op.flip() } else { linalg_bin_op };
388 let actual_core_op = core_op_for_linalg_op(&actual_linalg_op);
389
390 let dt = model.node_input_facts(node.id)?[0].datum_type;
391 if by_scalar_should_be_efficient & can_eval_in_a & !op_is_quant {
392 rule_if_some!(func = Func::BinByScalar(actual_linalg_op).bin(dt));
393 let eval_fn = Arc::from(func);
394 return Ok(Some(
395 TypedModelPatch::replace_single_op(
396 model,
397 node,
398 &inputs,
399 OptBinByScalar {
400 binop: actual_core_op,
401 eval_fn,
402 linalg_op: actual_linalg_op,
403 },
404 )?
405 .with_context("ByScalar"),
406 ));
407 }
408
409 if unicast_should_be_efficient & can_eval_in_a & !op_is_quant {
410 rule_if_some!(func = Func::BinUnicast(actual_linalg_op).bin(dt));
411 let eval_fn = Arc::from(func);
412 return Ok(Some(
413 TypedModelPatch::replace_single_op(
414 model,
415 node,
416 &inputs,
417 OptBinUnicast { binop: actual_core_op, eval_fn },
418 )?
419 .with_context("Unicast"),
420 ));
421 }
422 }
423
424 Ok(None)
425 }
426 as_op!();
427}
428
429fn repeat_broadcast(op: BinOp, a: &mut Tensor, b: &Tensor, period: usize) -> TractResult<bool> {
433 macro_rules! run {
434 ($t:ty) => {{
435 let bview = b.view();
436 let bs: &[$t] = bview.as_slice::<$t>()?;
437 let mut aview = a.view_mut();
438 let av: &mut [$t] = aview.as_slice_mut::<$t>()?;
439 match op {
440 BinOp::Mul => {
441 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
442 c.iter_mut().for_each(|x| *x *= s)
443 }
444 }
445 BinOp::Add => {
446 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
447 c.iter_mut().for_each(|x| *x += s)
448 }
449 }
450 BinOp::Sub => {
451 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
452 c.iter_mut().for_each(|x| *x -= s)
453 }
454 }
455 BinOp::SubF => {
456 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
457 c.iter_mut().for_each(|x| *x = s - *x)
458 }
459 }
460 BinOp::Min => {
461 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
462 c.iter_mut().for_each(|x| *x = if *x < s { *x } else { s })
463 }
464 }
465 BinOp::Max => {
466 for (c, &s) in av.chunks_exact_mut(period).zip(bs) {
467 c.iter_mut().for_each(|x| *x = if *x > s { *x } else { s })
468 }
469 }
470 }
471 return Ok(true);
472 }};
473 }
474 if a.datum_type() != b.datum_type() {
475 return Ok(false);
476 }
477 if a.datum_type() == f32::datum_type() {
478 run!(f32)
479 }
480 if a.datum_type() == f16::datum_type() {
481 run!(f16)
482 }
483 Ok(false)
484}
485
486fn core_op_for_linalg_op(linalg: &BinOp) -> Box<dyn BinMiniOp> {
487 match linalg {
488 BinOp::Min => Box::new(Min),
489 BinOp::Max => Box::new(Max),
490 BinOp::Add => Box::new(Add),
491 BinOp::Mul => Box::new(Mul),
492 BinOp::Sub => Box::new(Sub),
493 BinOp::SubF => Box::new(SubF),
494 }
495}
496fn declutter_broadcasting_operand_1(
497 model: &TypedModel,
498 node: &TypedNode,
499 mini_op: Box<dyn BinMiniOp>,
500) -> TractResult<Option<TypedModelPatch>> {
501 let (a_shape, b_shape) = if let &[a, b] = &*model.node_input_facts(node.id)? {
502 (a.shape.clone(), b.shape.clone())
503 } else {
504 unreachable!("TypedBinOp has two inputs.")
505 };
506
507 let a_num_elements = a_shape.iter().product::<TDim>();
508 let b_num_elements = b_shape.iter().product::<TDim>();
509 let a_should_be_broadcast = (a_num_elements - b_num_elements).prove_strict_negative();
510 if a_should_be_broadcast & mini_op.is_commutative() {
511 let mut swap_input = node.inputs.clone();
512 swap_input.swap(0, 1);
513 return Ok(Some(TypedModelPatch::replace_single_op(
514 model,
515 node,
516 &swap_input,
517 TypedBinOp(mini_op, None),
518 )?));
519 }
520
521 Ok(None)
522}
523
524fn declutter_neutral(
533 model: &TypedModel,
534 node: &TypedNode,
535 mini_op: &dyn BinMiniOp,
536 out_dt: DatumType,
537) -> TractResult<Option<TypedModelPatch>> {
538 let Some(uniform) = crate::ops::binary::one_input_is_uniform(model, node)? else {
539 return Ok(None);
540 };
541 let uni_is_neutral = mini_op
542 .neutral_element()
543 .is_some_and(|neutral| tensor0(neutral).close_enough(&uniform.uni, false).is_ok());
544 let uniform_on_neutral_side = mini_op.is_commutative() || !uniform.left_is_uniform;
546 if !uni_is_neutral || !uniform_on_neutral_side {
547 return Ok(None);
548 }
549 if uniform.uni.datum_type().is_quantized() {
550 return Ok(Some(TypedModelPatch::replace_single_op(
551 model,
552 node,
553 &[uniform.var],
554 cast(out_dt),
555 )?));
556 }
557 Ok(Some(TypedModelPatch::rewire(model, &[uniform.var], &[node.id.into()], &|_, inputs| {
558 Ok(inputs.into())
559 })?))
560}
561
562fn declutter_absorbing(
572 model: &TypedModel,
573 node: &TypedNode,
574 mini_op: &dyn BinMiniOp,
575) -> TractResult<Option<TypedModelPatch>> {
576 if let Some(uniform) = crate::ops::binary::one_input_is_uniform(model, node)? {
577 let is_absorbing = mini_op
578 .absorbing_element()
579 .map(|absorb| tensor0(absorb).close_enough(&uniform.uni, false).is_ok())
580 .unwrap_or(false);
581 if is_absorbing {
582 let output_fact = model.outlet_fact(node.id.into())?;
583 let output_dt = output_fact.datum_type;
584 let output_shape = output_fact.shape.clone();
585 let uni_inlet = if uniform.left_is_uniform { 0 } else { 1 };
586 let uni_input_shape = &model.outlet_fact(node.inputs[uni_inlet])?.shape;
587 if uni_input_shape == &output_shape && uniform.uni.datum_type() == output_dt {
589 return Ok(Some(TypedModelPatch::rewire(
590 model,
591 &[node.inputs[uni_inlet]],
592 &[node.id.into()],
593 &|_, inputs| Ok(inputs.into()),
594 )?));
595 }
596 let absorb_val = mini_op.absorbing_element().unwrap();
600 let absorbing_const =
601 tensor0(absorb_val as f32).cast_to_dt(output_dt)?.into_owned().into_arc_tensor();
602 let mut patch = TypedModelPatch::default();
603 let uni_const =
604 patch.add_const(format!("{}.absorbing_const", node.name), absorbing_const)?;
605 let bcast = patch.wire_node(
606 format!("{}.absorbing_bcast", node.name),
607 crate::ops::array::MultiBroadcastTo { shape: output_shape },
608 &[uni_const],
609 )?[0];
610 patch.shunt_outside(model, node.id.into(), bcast)?;
611 return Ok(Some(patch));
612 }
613 }
614 Ok(None)
615}
616
617fn find_most_efficient_config(
618 model: &TypedModel,
619 node: &TypedNode,
620 swap_input: bool,
621) -> TractResult<(bool, bool)> {
622 if let &[a, b] = &*model.node_input_facts(node.id)? {
623 let a_shape = if swap_input { b.shape.clone() } else { a.shape.clone() };
624 let b_shape = if swap_input { a.shape.clone() } else { b.shape.clone() };
625
626 let by_scalar_is_possible = OptBinByScalar::check_input_shapes(&a_shape, &b_shape);
627 let num_by_scalar_elements = if by_scalar_is_possible {
628 a_shape
629 .iter()
630 .zip(b_shape.iter())
631 .rev()
632 .take_while(|(_, rev_b_dim)| **rev_b_dim == TDim::Val(1))
633 .map(|(rev_a_dim, _)| rev_a_dim)
634 .product::<TDim>()
635 } else {
636 TDim::Val(0)
637 };
638
639 let unicast_is_possible = OptBinUnicast::check_input_shapes(&a_shape, &b_shape);
640 let num_unicast_elements = if unicast_is_possible {
641 a_shape
642 .iter()
643 .zip(b_shape.iter())
644 .rev()
645 .take_while(|(a_dim, b_dim)| a_dim == b_dim)
646 .map(|(a_dim, _)| a_dim)
647 .product::<TDim>()
648 } else {
649 TDim::Val(0)
650 };
651
652 let min_num_elements = 32;
653 let total_elements = a_shape.iter().product::<TDim>();
658 let by_scalar_should_be_efficient =
659 gt_tdim(num_by_scalar_elements.clone(), min_num_elements)
660 || (by_scalar_is_possible
661 && gt_tdim(num_by_scalar_elements, 2)
662 && gt_tdim(total_elements, 256));
663 let unicast_should_be_efficient = gt_tdim(num_unicast_elements, min_num_elements);
664 return Ok((by_scalar_should_be_efficient, unicast_should_be_efficient));
665 }
666 Ok((false, false))
667}
668
669pub fn gt_tdim(x: TDim, min_val: i64) -> bool {
670 TDim::Val(min_val).mini(x).as_i64().is_some_and(|v| v == min_val)
671}
672
673#[derive(Clone)]
674pub struct OptBinByScalar {
675 pub linalg_op: BinOp,
676 pub binop: Box<dyn BinMiniOp>,
677 eval_fn: Arc<BinFn>,
678}
679
680impl Debug for OptBinByScalar {
681 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
682 f.debug_struct("OptBinByScalar").field("binop", &self.binop).finish()
683 }
684}
685
686impl OptBinByScalar {
687 fn check_input_shapes(a_shape: &[TDim], b_shape: &[TDim]) -> bool {
688 if a_shape.len() != b_shape.len() {
689 return false;
690 };
691
692 a_shape
693 .iter()
694 .zip(b_shape.iter())
695 .skip_while(|(a_dim, b_dim)| a_dim == b_dim)
696 .all(|(_, b_dim)| *b_dim == 1.to_dim())
697 }
698}
699
700impl PartialEq for OptBinByScalar {
701 fn eq(&self, other: &Self) -> bool {
702 *self.binop == *other.binop
703 }
704}
705impl Eq for OptBinByScalar {}
706
707impl Op for OptBinByScalar {
708 fn name(&self) -> StaticName {
709 format!("Opt{}ByScalar", self.binop.name()).into()
710 }
711
712 op_as_typed_op!();
713}
714
715impl EvalOp for OptBinByScalar {
716 op_out_of_plan!();
717
718 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
719 let (a, b) = args_2!(inputs);
720 let a_natural = a.len() == a.shape().iter().product::<usize>()
726 && a.strides() == &*Tensor::natural_strides(a.shape());
727 let b_natural = b.len() == b.shape().iter().product::<usize>()
728 && b.strides() == &*Tensor::natural_strides(b.shape());
729 if !a_natural || !b_natural {
730 let c_dt = self.binop.result_datum_type(a.datum_type(), b.datum_type())?;
731 return Ok(tvec!(self.binop.eval(a, b, c_dt)?.into_tvalue()));
732 }
733
734 let mut a = a.into_tensor();
735 let b_shape = b.shape();
736
737 let first_unary_axis = b_shape
738 .iter()
739 .enumerate()
740 .rev()
741 .take_while(|&(_, &dim)| dim == 1)
742 .map(|(i, _)| i)
743 .last()
744 .context("Cannot use by_scalar when no trailing dimensions are unary")?;
745
746 let n_blocks: usize = a.shape()[..first_unary_axis].iter().product();
749 let period = a.len().checked_div(n_blocks).unwrap_or(0);
751 if period > 1 && period < 16 && repeat_broadcast(self.linalg_op, &mut a, &b, period)? {
755 return Ok(tvec!(a.into_tvalue()));
756 }
757 tract_linalg::multithread::par_bin(&*self.eval_fn, &mut a, &b, period, BShare::PerBlock)?;
758 Ok(tvec!(a.into_tvalue()))
759 }
760}
761
762impl TypedOp for OptBinByScalar {
763 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
764 ensure!(Self::check_input_shapes(&inputs[0].shape, &inputs[1].shape));
765 let out_dt = self.binop.result_datum_type(inputs[0].datum_type, inputs[1].datum_type)?;
766 let out_shape = inputs[0].shape.clone();
767 Ok(tvec!(out_dt.fact(out_shape)))
768 }
769
770 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
771 let count: TDim = self.output_facts(inputs)?[0].shape.iter().product();
772 Ok(self
773 .binop
774 .cost_per_element(inputs[0].datum_type)
775 .into_iter()
776 .map(|(c, n)| (c, count.clone() * n))
777 .collect())
778 }
779
780 fn fuse(&self, model: &TypedModel, node: &TypedNode) -> TractResult<Option<TypedModelPatch>> {
781 fuse_elementwise_chain(model, node)
782 }
783
784 as_op!();
785}
786
787#[derive(Clone)]
788pub struct OptBinUnicast {
789 pub binop: Box<dyn BinMiniOp>,
790 eval_fn: Arc<BinFn>,
791}
792
793impl Debug for OptBinUnicast {
794 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
795 f.debug_struct("OptBinUnicast").field("binop", &self.binop).finish()
796 }
797}
798
799impl OptBinUnicast {
800 fn check_b_alignement(a_shape: &[TDim], b_shape: &[TDim]) -> bool {
801 let num_iterations: TDim = a_shape
802 .iter()
803 .zip(b_shape.iter())
804 .take_while(|(_, b_dim)| **b_dim == 1.to_dim())
805 .map(|(a_dim, _)| a_dim)
806 .product();
807
808 if num_iterations.is_one() {
809 return true;
810 }
811
812 let elements_per_iteration: TDim = a_shape
813 .iter()
814 .zip(b_shape.iter())
815 .skip_while(|(_, b_dim)| **b_dim == 1.to_dim())
816 .map(|(_, b_dim)| b_dim)
817 .product();
818
819 if let Ok(num_element) = elements_per_iteration.to_i64() {
820 let required_alignment = vector_size();
821 (num_element as usize).is_multiple_of(required_alignment)
822 } else {
823 false
824 }
825 }
826 fn check_input_shapes(a_shape: &[TDim], b_shape: &[TDim]) -> bool {
827 if a_shape.len() != b_shape.len() {
828 return false;
829 };
830
831 let unicast_possible = a_shape
832 .iter()
833 .zip(b_shape.iter())
834 .skip_while(|(_, b_dim)| **b_dim == 1.to_dim())
835 .all(|(a_dim, b_dim)| a_dim == b_dim);
836 let unicast_is_aligned = Self::check_b_alignement(a_shape, b_shape);
837
838 unicast_possible && unicast_is_aligned
839 }
840}
841
842impl PartialEq for OptBinUnicast {
843 fn eq(&self, other: &Self) -> bool {
844 *self.binop == *other.binop
845 }
846}
847impl Eq for OptBinUnicast {}
848
849impl Op for OptBinUnicast {
850 fn name(&self) -> StaticName {
851 format!("Opt{}Unicast", self.binop.name()).into()
852 }
853
854 op_as_typed_op!();
855}
856
857impl EvalOp for OptBinUnicast {
858 op_out_of_plan!();
859
860 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
861 let (a, b) = args_2!(inputs);
862 let a_natural = a.len() == a.shape().iter().product::<usize>()
873 && a.strides() == &*Tensor::natural_strides(a.shape());
874 let b_natural = b.len() == b.shape().iter().product::<usize>()
875 && b.strides() == &*Tensor::natural_strides(b.shape());
876 if !a_natural || !b_natural {
877 let c_dt = self.binop.result_datum_type(a.datum_type(), b.datum_type())?;
878 return Ok(tvec!(self.binop.eval(a, b, c_dt)?.into_tvalue()));
879 }
880
881 let mut a = a.into_tensor();
882 debug_assert!(
886 b.shape().iter().zip(a.shape()).skip_while(|(b, _)| **b == 1).all(|(b, a)| b == a),
887 "unicast b {:?} does not line up with a {:?}",
888 b.shape(),
889 a.shape()
890 );
891 let period = b.len();
892 tract_linalg::multithread::par_bin(&*self.eval_fn, &mut a, &b, period, BShare::Lockstep)?;
893
894 Ok(tvec!(a.into_tvalue()))
895 }
896}
897
898impl TypedOp for OptBinUnicast {
899 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
900 ensure!(Self::check_input_shapes(&inputs[0].shape, &inputs[1].shape));
901 let out_dt = self.binop.result_datum_type(inputs[0].datum_type, inputs[1].datum_type)?;
902 let out_shape = inputs[0].shape.clone();
903 Ok(tvec!(out_dt.fact(out_shape)))
904 }
905
906 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
907 let count: TDim = self.output_facts(inputs)?[0].shape.iter().product();
908 Ok(self
909 .binop
910 .cost_per_element(inputs[0].datum_type)
911 .into_iter()
912 .map(|(c, n)| (c, count.clone() * n))
913 .collect())
914 }
915
916 fn fuse(&self, model: &TypedModel, node: &TypedNode) -> TractResult<Option<TypedModelPatch>> {
917 fuse_elementwise_chain(model, node)
918 }
919
920 as_op!();
921}
922
923#[derive(Clone)]
927pub struct OptBinChainStep {
928 pub linalg_op: BinOp,
929 pub binop: Box<dyn BinMiniOp>,
930 eval_fn: Arc<BinFn>,
931 pub share: BShare,
932}
933
934impl Debug for OptBinChainStep {
935 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
936 write!(f, "{}@{:?}", self.binop.name(), self.share)
937 }
938}
939
940impl PartialEq for OptBinChainStep {
941 fn eq(&self, other: &Self) -> bool {
942 *self.binop == *other.binop && self.share == other.share
943 }
944}
945impl Eq for OptBinChainStep {}
946
947#[derive(Clone)]
955pub struct OptBinChain {
956 pub steps: TVec<OptBinChainStep>,
957}
958
959impl Debug for OptBinChain {
960 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
961 f.debug_tuple("OptBinChain").field(&self.steps).finish()
962 }
963}
964
965impl PartialEq for OptBinChain {
966 fn eq(&self, other: &Self) -> bool {
967 self.steps == other.steps
968 }
969}
970impl Eq for OptBinChain {}
971
972fn linalg_op_for_binop(binop: &dyn BinMiniOp) -> Option<BinOp> {
973 if binop.is::<Min>() {
974 Some(BinOp::Min)
975 } else if binop.is::<Max>() {
976 Some(BinOp::Max)
977 } else if binop.is::<Add>() {
978 Some(BinOp::Add)
979 } else if binop.is::<Mul>() {
980 Some(BinOp::Mul)
981 } else if binop.is::<Sub>() {
982 Some(BinOp::Sub)
983 } else if binop.is::<SubF>() {
984 Some(BinOp::SubF)
985 } else {
986 None
987 }
988}
989
990fn eval_bin_chain_step(step: &OptBinChainStep, a: TValue, b: TValue) -> TractResult<TValue> {
994 let natural = |t: &Tensor| {
995 t.len() == t.shape().iter().product::<usize>()
996 && t.strides() == &*Tensor::natural_strides(t.shape())
997 };
998 if !natural(&a) || !natural(&b) {
999 let c_dt = step.binop.result_datum_type(a.datum_type(), b.datum_type())?;
1000 return Ok(step.binop.eval(a, b, c_dt)?.into_tvalue());
1001 }
1002 let mut a = a.into_tensor();
1003 match step.share {
1004 BShare::Lockstep => {
1005 tract_linalg::multithread::par_bin(
1006 &*step.eval_fn,
1007 &mut a,
1008 &b,
1009 b.len(),
1010 BShare::Lockstep,
1011 )?;
1012 }
1013 BShare::PerBlock => {
1014 let period = a.len().checked_div(b.len().max(1)).unwrap_or(0);
1015 if period > 1 && period < 16 && repeat_broadcast(step.linalg_op, &mut a, &b, period)? {
1016 return Ok(a.into_tvalue());
1017 }
1018 tract_linalg::multithread::par_bin(
1019 &*step.eval_fn,
1020 &mut a,
1021 &b,
1022 period,
1023 BShare::PerBlock,
1024 )?;
1025 }
1026 }
1027 Ok(a.into_tvalue())
1028}
1029
1030impl Op for OptBinChain {
1031 fn name(&self) -> StaticName {
1032 format!("OptBinChain({})", self.steps.iter().map(|s| s.binop.name()).join("+")).into()
1033 }
1034
1035 op_as_typed_op!();
1036}
1037
1038impl EvalOp for OptBinChain {
1039 op_out_of_plan!();
1040
1041 fn eval(&self, _ctx: &EvalContext, mut inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
1042 let natural = |t: &Tensor| {
1043 t.len() == t.shape().iter().product::<usize>()
1044 && t.strides() == &*Tensor::natural_strides(t.shape())
1045 };
1046 let all_natural = inputs.iter().all(|t| natural(t));
1047 let acc = inputs.remove(0);
1048 let operands = inputs;
1049 let sequential = |acc: TValue| -> TractResult<TValue> {
1050 self.steps
1051 .iter()
1052 .zip(operands.iter())
1053 .try_fold(acc, |acc, (step, b)| eval_bin_chain_step(step, acc, b.clone()))
1054 };
1055 if !all_natural {
1056 return Ok(tvec!(sequential(acc)?));
1057 }
1058 let mut a = acc.into_tensor();
1059 let tiny_period = self.steps.iter().zip(operands.iter()).any(|(s, b)| {
1063 s.share == BShare::PerBlock && {
1064 let period = a.len() / b.len().max(1);
1065 period > 1 && period < 16
1066 }
1067 });
1068 if tiny_period {
1069 return Ok(tvec!(sequential(a.into_tvalue())?));
1070 }
1071 let steps: Vec<tract_linalg::multithread::BinChainStep> = self
1072 .steps
1073 .iter()
1074 .zip(operands.iter())
1075 .map(|(s, b)| tract_linalg::multithread::BinChainStep {
1076 eval_fn: &*s.eval_fn,
1077 b,
1078 period: match s.share {
1079 BShare::Lockstep => b.len(),
1080 BShare::PerBlock => a.len() / b.len().max(1),
1081 },
1082 share: s.share,
1083 })
1084 .collect();
1085 tract_linalg::multithread::par_bin_chain(&steps, &mut a)?;
1086 Ok(tvec!(a.into_tvalue()))
1087 }
1088}
1089
1090impl TypedOp for OptBinChain {
1091 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
1092 ensure!(inputs.len() == self.steps.len() + 1);
1093 for (step, fact) in self.steps.iter().zip(inputs[1..].iter()) {
1094 ensure!(
1095 step.binop.result_datum_type(inputs[0].datum_type, fact.datum_type)?
1096 == inputs[0].datum_type
1097 );
1098 }
1099 Ok(tvec!(inputs[0].without_value()))
1100 }
1101
1102 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
1103 let count: TDim = self.output_facts(inputs)?[0].shape.iter().product();
1104 Ok(self
1105 .steps
1106 .iter()
1107 .flat_map(|s| s.binop.cost_per_element(inputs[0].datum_type))
1108 .map(|(c, n)| (c, count.clone() * n))
1109 .collect())
1110 }
1111
1112 fn fuse(&self, model: &TypedModel, node: &TypedNode) -> TractResult<Option<TypedModelPatch>> {
1113 fuse_elementwise_chain(model, node)
1114 }
1115
1116 as_op!();
1117}
1118
1119fn fuse_elementwise_chain(
1126 model: &TypedModel,
1127 node: &TypedNode,
1128) -> TractResult<Option<TypedModelPatch>> {
1129 fn steps_of(op: &dyn Op) -> Option<TVec<OptBinChainStep>> {
1130 if let Some(o) = op.downcast_ref::<OptBinUnicast>() {
1131 Some(tvec![OptBinChainStep {
1132 linalg_op: linalg_op_for_binop(&*o.binop)?,
1133 binop: o.binop.clone(),
1134 eval_fn: o.eval_fn.clone(),
1135 share: BShare::Lockstep,
1136 }])
1137 } else if let Some(o) = op.downcast_ref::<OptBinByScalar>() {
1138 Some(tvec![OptBinChainStep {
1139 linalg_op: o.linalg_op,
1140 binop: o.binop.clone(),
1141 eval_fn: o.eval_fn.clone(),
1142 share: BShare::PerBlock,
1143 }])
1144 } else {
1145 op.downcast_ref::<OptBinChain>().map(|o| o.steps.clone())
1146 }
1147 }
1148
1149 rule_if_some!(mut steps = steps_of(&*node.op));
1150 let pred_out = node.inputs[0];
1153 let pred = model.node(pred_out.node);
1154 if pred.outputs[pred_out.slot].successors.len() == 1
1155 && !model.output_outlets()?.contains(&pred_out)
1156 && (steps_of(&*pred.op).is_some() || pred.op.fuse(model, pred)?.is_some())
1157 {
1158 return Ok(None);
1159 }
1160 let a_dt = model.outlet_fact(node.inputs[0])?.datum_type;
1161 rule_if!(a_dt.is_float());
1162 let mut inlets: TVec<OutletId> = node.inputs.iter().cloned().collect();
1163 let mut last = node.id;
1164 let mut cursor = node.id;
1165 loop {
1166 let current = model.node(cursor);
1167 if current.outputs.len() != 1 {
1168 break;
1169 }
1170 let outlet = OutletId::new(cursor, 0);
1171 if current.outputs[0].successors.len() != 1 || model.output_outlets()?.contains(&outlet) {
1172 break;
1173 }
1174 let succ_inlet = current.outputs[0].successors[0];
1175 if succ_inlet.slot != 0 {
1177 break;
1178 }
1179 let succ = model.node(succ_inlet.node);
1180 let Some(mut more) = steps_of(&*succ.op) else { break };
1181 let keep_dt = more.iter().enumerate().all(|(ix, s)| {
1182 model
1183 .outlet_fact(succ.inputs[ix + 1])
1184 .ok()
1185 .filter(|f| f.datum_type == a_dt)
1186 .and_then(|_| s.binop.result_datum_type(a_dt, a_dt).ok())
1187 == Some(a_dt)
1188 });
1189 if !keep_dt {
1190 break;
1191 }
1192 inlets.extend(succ.inputs[1..].iter().cloned());
1193 steps.append(&mut more);
1194 last = succ.id;
1195 cursor = succ.id;
1196 if steps.len() >= 16 {
1197 break;
1198 }
1199 }
1200 rule_if!(last != node.id);
1201 let mut patch = TypedModelPatch::new("fusing elementwise chain");
1202 let wires = inlets
1203 .iter()
1204 .map(|i| patch.tap_model(model, *i))
1205 .collect::<TractResult<Vec<OutletId>>>()?;
1206 let wire = patch.wire_node(&model.node(last).name, OptBinChain { steps }, &wires)?[0];
1207 patch.shunt_outside(model, last.into(), wire)?;
1208 Ok(Some(patch))
1209}
1210
1211#[macro_export]
1212macro_rules! bin_to_super_type {
1213 ($func:ident, $Op:ident,
1214 $(codegen: $codegen:expr,)?
1215 $(cost: $cost:expr,)?
1216 $(declutter: $declutter:expr,)?
1217 $(eval_in_a: $eval_in_a:expr,)?
1218 $(eval_override: $eval_override: expr,)?
1219 $(linalg: $linalg:ident,)?
1220 $(operating_datum_type: $operating_datum_type:expr,)?
1221 $(is_commutative: $is_commutative:expr,)?
1222 $(neutral_element: $neutral_element:expr,)?
1223 $(absorbing_element: $absorbing_element:expr,)?
1224 $(out_of_place: $out_of_place:expr,)?
1225 $(validation: $validation:expr,)?
1226 $(q: $([$($typ_dt:ident),*] => $cab_dt:expr),* ;)?
1227 $(q_op_on_f32: $q_op_on_f32:expr,)?
1228 $( [$($typ:ident),*] => $cab:expr),*) => {
1229 #[derive(Debug, Clone, Hash, PartialEq, Eq)]
1230 pub struct $Op;
1231 #[allow(clippy::redundant_closure_call)]
1232 impl $crate::ops::binary::BinMiniOp for $Op {
1233 fn name(&self) -> &'static str {
1234 stringify!($Op)
1235 }
1236
1237 fn eval_out_of_place(&self, c: &mut Tensor, a: &Tensor, b: &Tensor) -> TractResult<()> {
1238 $(if $out_of_place(c, a, b)? { return Ok(()) } )?
1239 if c.shape() == a.shape() && a.shape() == b.shape() {
1243 $(
1244 $(if c.datum_type() == $typ::datum_type() {
1245 let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
1246 let a_plain = a.try_as_plain_ram()?;
1247 let a_slice = a_plain.as_slice::<$typ>()?;
1248 let b_plain = b.try_as_plain_ram()?;
1249 let b_slice = b_plain.as_slice::<$typ>()?;
1250 let mut c_plain = c.try_as_plain_ram_mut()?;
1251 let c_slice = c_plain.as_slice_mut::<$typ>()?;
1252 debug_assert_eq!(c_slice.len(), a_slice.len());
1253 debug_assert_eq!(c_slice.len(), b_slice.len());
1254 let len = c_slice.len();
1255 tract_linalg::multithread::par_chunks_mut(c_slice, 1, len, |first_row, c_chunk| {
1256 let n = c_chunk.len();
1257 let a_chunk = &a_slice[first_row..first_row + n];
1258 let b_chunk = &b_slice[first_row..first_row + n];
1259 for ((cv, av), bv) in c_chunk.iter_mut().zip(a_chunk.iter()).zip(b_chunk.iter()) {
1260 cab(cv, av, bv);
1261 }
1262 Ok(())
1263 })?;
1264 return Ok(())
1265 })*
1266 )*
1267 $(
1268 $(
1269 $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1270 let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1271 let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1272 let a_plain = a.try_as_plain_ram()?;
1273 let a_slice = a_plain.as_slice::<$typ_dt>()?;
1274 let b_plain = b.try_as_plain_ram()?;
1275 let b_slice = b_plain.as_slice::<$typ_dt>()?;
1276 let mut c_plain = c.try_as_plain_ram_mut()?;
1277 let c_slice = c_plain.as_slice_mut::<$typ_dt>()?;
1278 for ((cv, av), bv) in c_slice.iter_mut().zip(a_slice.iter()).zip(b_slice.iter()) {
1279 cab(cv, av, bv, zp, scale);
1280 }
1281 return Ok(())
1282 })*
1283 )*
1284 )?
1285 }
1286 $(
1287 $(if c.datum_type() == $typ::datum_type() {
1288 let a = a.to_plain_array_view::<$typ>()?;
1289 let b = b.to_plain_array_view::<$typ>()?;
1290 let mut c_plain = c.try_as_plain_ram_mut()?;
1291 let mut c = c_plain.to_array_view_mut::<$typ>()?;
1292 $crate::ndarray::Zip::from(&mut c).and_broadcast(a).and_broadcast(b).for_each($cab);
1293 return Ok(())
1294 })*
1295 )*
1296 $(
1297 $(
1298 $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1299 let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1300 let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1301 let a = a.to_plain_array_view::<$typ_dt>()?;
1302 let b = b.to_plain_array_view::<$typ_dt>()?;
1303 let mut c_plain = c.try_as_plain_ram_mut()?;
1304 let mut c = c_plain.to_array_view_mut::<$typ_dt>()?;
1305 $crate::ndarray::Zip::from(&mut c).and_broadcast(a).and_broadcast(b).for_each(|c, a, b| cab(c, a, b, zp, scale));
1306 return Ok(())
1307 }
1308 )*
1309 )*
1310 )?
1311 bail!("{} does not support {:?} (out of place)", self.name(), c.datum_type());
1312 }
1313
1314 $(fn is_commutative(&self) -> bool {
1315 $is_commutative
1316 })?
1317 $(fn neutral_element(&self) -> Option<i64> {
1318 Some($neutral_element)
1319 })?
1320 $(fn absorbing_element(&self) -> Option<i64> {
1321 Some($absorbing_element)
1322 })?
1323 fn eval_in_a(&self, a: &mut Tensor, b: &Tensor) -> TractResult<()> {
1324 $(if $eval_in_a(a, b)? { return Ok(()) } )?
1326 if a.shape() == b.shape() {
1329 $(
1330 $(if b.datum_type() == $typ::datum_type() {
1331 let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
1332 let b_plain = b.try_as_plain_ram()?;
1333 let b_slice = b_plain.as_slice::<$typ>()?;
1334 let mut a_plain = a.try_as_plain_ram_mut()?;
1335 let a_slice = a_plain.as_slice_mut::<$typ>()?;
1336 debug_assert_eq!(a_slice.len(), b_slice.len());
1337 let len = a_slice.len();
1338 tract_linalg::multithread::par_chunks_mut(a_slice, 1, len, |first_row, a_chunk| {
1339 let n = a_chunk.len();
1340 let b_chunk = &b_slice[first_row..first_row + n];
1341 for (av, bv) in a_chunk.iter_mut().zip(b_chunk.iter()) {
1342 cab(av, &av.clone(), bv);
1343 }
1344 Ok(())
1345 })?;
1346 return Ok(())
1347 })*
1348 )*
1349 $(
1350 $(
1351 $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1352 let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1353 let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1354 let b_plain = b.try_as_plain_ram()?;
1355 let b_slice = b_plain.as_slice::<$typ_dt>()?;
1356 let mut a_plain = a.try_as_plain_ram_mut()?;
1357 let a_slice = a_plain.as_slice_mut::<$typ_dt>()?;
1358 for (av, bv) in a_slice.iter_mut().zip(b_slice.iter()) {
1359 cab(av, &(av.clone()), bv, zp, scale);
1360 }
1361 return Ok(())
1362 })*
1363 )*
1364 )?
1365 }
1366 $(
1367 $(if b.datum_type() == $typ::datum_type() {
1368 let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
1369 let b = b.to_plain_array_view::<$typ>()?;
1370 let mut a_plain = a.try_as_plain_ram_mut()?;
1371 let mut a = a_plain.to_array_view_mut::<$typ>()?;
1372 $crate::ndarray::Zip::from(&mut a).and_broadcast(b).for_each(|a, b| cab(a, &a.clone(), b));
1373 return Ok(())
1374 })*
1375 )*
1376 $(
1377 $(
1378 $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1379 let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1380 let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1381 let mut a_plain = a.try_as_plain_ram_mut()?;
1382 let mut a = a_plain.to_array_view_mut::<$typ_dt>()?;
1383 let b = b.to_plain_array_view::<$typ_dt>()?;
1384 $crate::ndarray::Zip::from(&mut a).and_broadcast(b).for_each(|a, b| {
1385 cab(a, &(a.clone()), b, zp, scale)
1386 });
1387 return Ok(())
1388 })*
1389 )*
1390 )?
1391 bail!("{} does not support {:?} (eval in a)", self.name(), a.datum_type());
1392 }
1393
1394 $(fn eval(&self, a: TValue, b: TValue, c_dt: DatumType) -> TractResult<Tensor> {
1395 $eval_override(a, b, c_dt)
1396 })?
1397
1398 fn result_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType> {
1399 if a.unquantized() == b.unquantized() {
1400 if a.is_quantized() || !b.is_quantized() {
1401 return Ok(a)
1402 }
1403 else {
1404 return Ok(b)
1405 }
1406 }
1407 self.operating_datum_type(a, b)
1408 }
1409
1410 $(
1411 fn declutter(
1412 &self,
1413 model: &TypedModel,
1414 node: &TypedNode,
1415 ) -> TractResult<Option<TypedModelPatch>> {
1416 ($declutter)(self, model, node)
1417 }
1418 )?
1419 $(
1420 fn codegen(
1421 &self,
1422 model: &TypedModel,
1423 node: &TypedNode,
1424 a: &Arc<Tensor>,
1425 ) -> TractResult<Option<TypedModelPatch>> {
1426 ($codegen)(self, model, node, a)
1427 }
1428 )?
1429 $(
1430 fn cost_per_element(&self, dt: DatumType) -> TVec<(Cost, usize)> {
1431 ($cost)(dt)
1432 }
1433 )?
1434 $(
1435 fn validation(&self) -> Validation {
1436 $validation
1437 }
1438 )?
1439 $(
1440 fn as_linalg_binop(&self) -> Option<tract_linalg::BinOp> {
1441 Some(tract_linalg::BinOp::$linalg)
1442 }
1443 )?
1444 $(
1445 fn operating_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType> {
1446 ($operating_datum_type)(a, b)
1447 })?
1448
1449
1450 #[allow(unused_variables)]
1454 fn maybe_eval_qbinary_as_float_op(
1455 &self,
1456 a: &TValue,
1457 b: &TValue,
1458 c_dt: &DatumType,
1459 ) -> TractResult<Option<Tensor>> {
1460 $(
1461 fn memory_optimised_q_binary_as_float_op(
1465 a: &TValue,
1466 b: &TValue,
1467 c_dt: &DatumType,
1468 ) -> TractResult<Option<Tensor>> {
1469 if let (DatumType::QU8(QParams::ZpScale {zero_point: a_zp, scale: a_scale}),
1470 DatumType::QU8(QParams::ZpScale {zero_point: b_zp, scale: b_scale}),
1471 DatumType::QU8(QParams::ZpScale {zero_point: c_zp, scale: c_scale})) =
1472 (a.datum_type(), b.datum_type(), c_dt)
1473 {
1474 let c_inv_scale = 1.0 / c_scale;
1475 let a = a.to_plain_array_view::<u8>()?;
1476 let b = b.to_plain_array_view::<u8>()?;
1477 let c_shape = $crate::broadcast::multi_broadcast(&[a.shape(), b.shape()])?;
1478 let mut c = Tensor::zero_dt(*c_dt, &c_shape)?;
1479 let mut c_plain = c.try_as_plain_ram_mut()?;
1480 let view = c_plain.to_array_view_mut::<u8>()?;
1481 $crate::ndarray::Zip::from(view).and_broadcast(a).and_broadcast(b).for_each(|c, a, b| {
1482 *c = (scale_by($q_op_on_f32(
1483 ((*a as i32 - a_zp as i32) as f32 * a_scale),
1484 ((*b as i32 - b_zp as i32) as f32 * b_scale),
1485 ), c_inv_scale) as i32
1486 + *c_zp as i32)
1487 .clamp_cast()
1488 });
1489 return Ok(Some(c));
1490 }
1491 Ok(None)
1492 }
1493
1494 fn generic_q_binary_as_float_op(
1498 a: &TValue,
1499 b: &TValue,
1500 c_dt: &DatumType,
1501 accumulator_dt: DatumType
1502 ) -> TractResult<Option<Tensor>> {
1503 if a.datum_type().is_quantized() && b.datum_type().is_quantized() && c_dt.is_quantized() {
1504 let a = a.cast_to_dt(accumulator_dt)?.into_owned();
1505 let b = b.cast_to_dt(accumulator_dt)?.into_owned();
1506 let c_shape = $crate::broadcast::multi_broadcast(&[a.shape(), b.shape()])?;
1507 let mut c = Tensor::zero_dt(accumulator_dt, &c_shape)?;
1508 match accumulator_dt {
1509 DatumType::F32 => {
1510 let mut c_plain = c.try_as_plain_ram_mut()?;
1511 let view = c_plain.to_array_view_mut::<f32>()?;
1512 $crate::ndarray::Zip::from(view).and_broadcast(a.try_as_plain_ram()?.to_array_view()?).and_broadcast(b.try_as_plain_ram()?.to_array_view()?).for_each(|c, a, b| {
1513 *c = $q_op_on_f32(*a,*b);
1514 })
1515 },
1516 other => bail!("unexpected accumulator data type as {:?}", other)
1517 };
1518
1519 return Ok(Some(c.cast_to_dt(*c_dt)?.into_owned()));
1520 }
1521 Ok(None)
1522 }
1523
1524 if let Some(c) = memory_optimised_q_binary_as_float_op(a, b, c_dt)? {
1525 return Ok(Some(c));
1526 }
1527 if let Some(d) = generic_q_binary_as_float_op(a, b, c_dt, DatumType::F32)? {
1528 return Ok(Some(d));
1529 }
1530 )?
1531 Ok(None)
1532 }
1533 }
1534
1535 pub fn $func() -> $crate::ops::binary::TypedBinOp {
1536 $crate::ops::binary::TypedBinOp(Box::new($Op), None)
1537 }
1538 };
1539}
1540
1541#[derive(Debug)]
1542pub(crate) struct OneUniformInput {
1543 pub uni: Arc<Tensor>,
1544 pub var: OutletId,
1545 pub left_is_uniform: bool,
1546}
1547
1548pub(crate) fn one_input_is_uniform(
1549 model: &TypedModel,
1550 node: &TypedNode,
1551) -> TractResult<Option<OneUniformInput>> {
1552 if let &[a, b] = &*model.node_input_facts(node.id)? {
1553 let uni = if let Some(a) = &a.uniform {
1554 OneUniformInput { uni: a.clone(), var: node.inputs[1], left_is_uniform: true }
1555 } else if let Some(b) = &b.uniform {
1556 OneUniformInput { uni: b.clone(), var: node.inputs[0], left_is_uniform: false }
1557 } else {
1558 return Ok(None);
1559 };
1560 let var_fact = [a, b][uni.left_is_uniform as usize];
1561 let uni_fact = [a, b][!uni.left_is_uniform as usize];
1562 if izip!(var_fact.shape.iter(), uni_fact.shape.iter()).all(|(v, u)| u.is_one() || u == v) {
1563 return Ok(Some(uni));
1564 }
1565 }
1566 Ok(None)
1567}
1568
1569#[cfg(test)]
1570mod tests {
1571 use super::*;
1572
1573 #[test]
1585 fn opt_bin_unicast_falls_back_on_non_natural_strides() {
1586 let a_data: Vec<f32> = (0..640).map(|i| i as f32).collect();
1589 let mut a = tensor1(&a_data);
1590 a.insert_axis(0).unwrap();
1591 a.insert_axis(0).unwrap();
1592 assert_eq!(a.shape(), &[1, 1, 640]);
1593 assert_eq!(a.strides(), &[1, 1, 1]);
1594 assert_ne!(a.strides(), &*Tensor::natural_strides(a.shape()));
1595
1596 let b_data: Vec<f32> = vec![1.0; 640];
1598 let mut b = tensor1(&b_data);
1599 b.insert_axis(0).unwrap();
1600 b.insert_axis(0).unwrap();
1601 b = b.into_shape(&[1, 1, 640]).unwrap();
1604
1605 let linalg_fn = Func::BinUnicast(BinOp::Add)
1606 .bin(f32::datum_type())
1607 .expect("f32 unicast Add kernel available");
1608 let op = OptBinUnicast { binop: Box::new(Add), eval_fn: Arc::from(linalg_fn) };
1609
1610 let out =
1611 op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1612 let out = &out[0];
1613 assert_eq!(out.shape(), &[1, 1, 640]);
1614 let plain = out.try_as_plain_ram().unwrap();
1615 let out_slice = plain.as_slice::<f32>().unwrap();
1616 for (i, v) in out_slice.iter().enumerate() {
1617 assert_eq!(*v, i as f32 + 1.0, "mismatch at {i}");
1618 }
1619 }
1620
1621 #[test]
1627 fn zero_sized_outer_dim_is_a_noop() {
1628 let a = Tensor::zero::<f32>(&[0, 4, 8]).unwrap();
1629 let b = Tensor::zero::<f32>(&[0, 4, 1]).unwrap();
1630 let linalg_fn = Func::BinByScalar(BinOp::Add)
1631 .bin(f32::datum_type())
1632 .expect("f32 by_scalar Add kernel available");
1633 let op = OptBinByScalar {
1634 binop: Box::new(Add),
1635 eval_fn: Arc::from(linalg_fn),
1636 linalg_op: BinOp::Add,
1637 };
1638 let out =
1639 op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1640 assert_eq!(out[0].shape(), &[0, 4, 8]);
1641
1642 let a = Tensor::zero::<f32>(&[0, 4, 16]).unwrap();
1643 let b = Tensor::zero::<f32>(&[1, 4, 16]).unwrap();
1644 let linalg_fn = Func::BinUnicast(BinOp::Add)
1645 .bin(f32::datum_type())
1646 .expect("f32 unicast Add kernel available");
1647 let op = OptBinUnicast { binop: Box::new(Add), eval_fn: Arc::from(linalg_fn) };
1648 let out =
1649 op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1650 assert_eq!(out[0].shape(), &[0, 4, 16]);
1651 }
1652
1653 #[test]
1658 fn q_neutral_add_requantizes_the_variable_input() -> TractResult<()> {
1659 let x_dt = DatumType::QU8(QParams::ZpScale { zero_point: 10, scale: 0.02 });
1660 let zero_dt = DatumType::QU8(QParams::ZpScale { zero_point: 61, scale: 1. });
1661 let out_dt = DatumType::QU8(QParams::ZpScale { zero_point: 0, scale: 0.5 });
1662
1663 let mut model = TypedModel::default();
1664 let x = model.add_source("x", x_dt.fact([4]))?;
1665 let zero = model.add_const("zero", Tensor::zero_dt(zero_dt, &[4])?)?;
1668 let add = model.wire_node("add", TypedBinOp(Box::new(Add), Some(out_dt)), &[zero, x])?[0];
1669 model.select_output_outlets(&[add])?;
1670
1671 let mut input = Tensor::zero_dt(x_dt, &[4])?;
1672 input.try_as_plain_ram_mut()?.as_slice_mut::<u8>()?.copy_from_slice(&[10, 35, 60, 200]);
1673 let input = tvec!(input.into_tvalue());
1674
1675 let before = model.clone().into_runnable()?.run(input.clone())?;
1676 let decluttered = model.into_decluttered()?;
1677 assert!(decluttered.nodes().iter().all(|n| n.op_as::<TypedBinOp>().is_none()));
1678 let after = decluttered.into_runnable()?.run(input)?;
1679 assert_eq!(&*before[0], &*after[0]);
1680 Ok(())
1681 }
1682}