Skip to main content

tract_core/ops/
binary.rs

1use crate::internal::*;
2use downcast_rs::Downcast;
3use dyn_eq::DynEq;
4use std::fmt::{self, Debug};
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 + dyn_eq::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        // Same-shape fast path: skip `multi_broadcast` allocation when shapes
53        // are already equal (very common: residuals, mask application, etc.).
54        // Correctness: equal shapes imply broadcast shape == a.shape() and the
55        // existing slow path would have taken this same branch.
56        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    /// Override for ops that can evaluate symbolic TDim inputs (comparisons).
100    #[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    /// Override for ops that produce TDim-level comparison expressions (comparisons).
110    #[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        // Comparison ops provide their own TDim combination
157        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()
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        // Integer-valued scalar (including float constants like 2.0, 1.0, 3.0)
177        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        // Mul by reciprocal of integer (e.g. ×0.5 → Div(a, 2))
191        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            // And(a,b) has no TDim kernel; for 0/1 booleans And == Mul
220            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        // Fallback: one side has uniform_tdim, the other is a scalar constant
225        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        // Introduction: Mul (or any op with neutral_element=1) with a mask
247        // that has uniform_tdim → the other input gets that expression as ROI.
248        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        // Bubbling: delegate to the natural blanket implementation.
259        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            // Check if op is quantized
364            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            // Check if it can be evaluated in a
370            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            // Swap input if required
379            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
429/// `a[i*period + j] = op(a[i*period + j], b[i])` in one pass, for the float types
430/// that carry a short trailing broadcast. Returns false when the datum type has
431/// no typed path here, leaving the caller on the generic route.
432fn 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
524/// Shunt a binary op whose uniform input holds the op's neutral element
525/// (`x + 0`, `x * 1`, `x - 0`).
526///
527/// The neutral element is compared on dequantized values, so a quantized node
528/// can be arithmetically neutral while still re-encoding its input: `out_dt`
529/// may carry other quantization parameters than the variable input, and the
530/// same real value is then a different integer. Such a node degrades to a
531/// `Cast` instead of vanishing.
532fn 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    // Non-commutative ops only have a right neutral: x - 0 == x, but 0 - x == -x.
545    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
562/// When one input is the absorbing element (e.g. 0 for Mul, false for And),
563/// replace the entire op with a uniform-value tensor of the output shape.
564///
565/// We can't shunt the uniform input directly: it may be lower-rank or have
566/// broadcast-from-1 dims that don't match the op's output shape (e.g.
567/// `Mul([4, 1], scalar-0)` outputs `[4, 1]`, not `[1]`).  Wire a
568/// `MultiBroadcastTo` from the uniform constant to the output shape;
569/// subsequent declutter folds it into a pure constant when the shape is
570/// fully concrete.
571fn 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            // Fast path: shapes and types match — shunt the absorbing input directly.
588            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            // General path: create a constant encoded in the output type.
597            // This handles both shape mismatches and quantization mismatches
598            // (e.g. absorbing input is QU8(Z:61 S:1) but output is QU8(Z:0 S:0.5)).
599            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        // A short by-scalar group is still worth taking when the tensor is big:
654        // the eval reads b in place for those, so what pays off is the total
655        // element count, not the group. Requiring >= 2 also confirms b really has
656        // a trailing unary axis, which identical shapes satisfy only vacuously.
657        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        // Same as OptBinUnicast: the fast path uses at_prefix + as_slice_mut
721        // and relies on natural C-order strides for the slice math. Fall back
722        // to the generic eval if either operand has non-natural strides or a
723        // storage size that doesn't match its declared shape (e.g. after
724        // Tensor::insert_axis which leaves non-natural strides behind).
725        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        // b carries one scalar per block of a, the blocks being a's axes before
747        // first_unary_axis; check_input_shapes makes b's match a's there.
748        let n_blocks: usize = a.shape()[..first_unary_axis].iter().product();
749        // A zero-sized dim zeroes n_blocks, and par_bin no-ops on an empty a.
750        let period = a.len().checked_div(n_blocks).unwrap_or(0);
751        // A short period would mean one kernel dispatch per handful of elements,
752        // which costs far more than the arithmetic. Read b's scalar straight out
753        // of place instead, one pass, no broadcast buffer.
754        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    as_op!();
781}
782
783#[derive(Clone)]
784pub struct OptBinUnicast {
785    pub binop: Box<dyn BinMiniOp>,
786    eval_fn: Arc<BinFn>,
787}
788
789impl Debug for OptBinUnicast {
790    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
791        f.debug_struct("OptBinUnicast").field("binop", &self.binop).finish()
792    }
793}
794
795impl OptBinUnicast {
796    fn check_b_alignement(a_shape: &[TDim], b_shape: &[TDim]) -> bool {
797        let num_iterations: TDim = a_shape
798            .iter()
799            .zip(b_shape.iter())
800            .take_while(|(_, b_dim)| **b_dim == 1.to_dim())
801            .map(|(a_dim, _)| a_dim)
802            .product();
803
804        if num_iterations.is_one() {
805            return true;
806        }
807
808        let elements_per_iteration: TDim = a_shape
809            .iter()
810            .zip(b_shape.iter())
811            .skip_while(|(_, b_dim)| **b_dim == 1.to_dim())
812            .map(|(_, b_dim)| b_dim)
813            .product();
814
815        if let Ok(num_element) = elements_per_iteration.to_i64() {
816            let required_alignment = vector_size();
817            (num_element as usize).is_multiple_of(required_alignment)
818        } else {
819            false
820        }
821    }
822    fn check_input_shapes(a_shape: &[TDim], b_shape: &[TDim]) -> bool {
823        if a_shape.len() != b_shape.len() {
824            return false;
825        };
826
827        let unicast_possible = a_shape
828            .iter()
829            .zip(b_shape.iter())
830            .skip_while(|(_, b_dim)| **b_dim == 1.to_dim())
831            .all(|(a_dim, b_dim)| a_dim == b_dim);
832        let unicast_is_aligned = Self::check_b_alignement(a_shape, b_shape);
833
834        unicast_possible && unicast_is_aligned
835    }
836}
837
838impl PartialEq for OptBinUnicast {
839    fn eq(&self, other: &Self) -> bool {
840        *self.binop == *other.binop
841    }
842}
843impl Eq for OptBinUnicast {}
844
845impl Op for OptBinUnicast {
846    fn name(&self) -> StaticName {
847        format!("Opt{}Unicast", self.binop.name()).into()
848    }
849
850    op_as_typed_op!();
851}
852
853impl EvalOp for OptBinUnicast {
854    op_out_of_plan!();
855
856    fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
857        let (a, b) = args_2!(inputs);
858        // The unicast fast path indexes each input's storage via at_prefix +
859        // as_slice_mut, which uses `strides[i-1]` to size the resulting slice
860        // (data/src/tensor/view.rs:99). That formula only matches ∏(shape[i..])
861        // when the tensor has natural C-order strides. Producers like
862        // Tensor::insert_axis leave non-natural strides on a tensor (e.g.
863        // shape `[1, 1, 640]` with strides `[1, 1, 1]` after two insert_axis
864        // on a `[640]` tensor), which silently breaks the slice math. Fall
865        // back to the generic broadcasting eval when either operand is not in
866        // natural strides (or has a storage size that doesn't match the
867        // declared shape).
868        let a_natural = a.len() == a.shape().iter().product::<usize>()
869            && a.strides() == &*Tensor::natural_strides(a.shape());
870        let b_natural = b.len() == b.shape().iter().product::<usize>()
871            && b.strides() == &*Tensor::natural_strides(b.shape());
872        if !a_natural || !b_natural {
873            let c_dt = self.binop.result_datum_type(a.datum_type(), b.datum_type())?;
874            return Ok(tvec!(self.binop.eval(a, b, c_dt)?.into_tvalue()));
875        }
876
877        let mut a = a.into_tensor();
878        // b's leading unary axes are the ones a repeats over; past them b matches
879        // a exactly (check_input_shapes), so one kernel call covers b.len()
880        // elements of a and b is consumed in lockstep within each.
881        debug_assert!(
882            b.shape().iter().zip(a.shape()).skip_while(|(b, _)| **b == 1).all(|(b, a)| b == a),
883            "unicast b {:?} does not line up with a {:?}",
884            b.shape(),
885            a.shape()
886        );
887        let period = b.len();
888        tract_linalg::multithread::par_bin(&*self.eval_fn, &mut a, &b, period, BShare::Lockstep)?;
889
890        Ok(tvec!(a.into_tvalue()))
891    }
892}
893
894impl TypedOp for OptBinUnicast {
895    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
896        ensure!(Self::check_input_shapes(&inputs[0].shape, &inputs[1].shape));
897        let out_dt = self.binop.result_datum_type(inputs[0].datum_type, inputs[1].datum_type)?;
898        let out_shape = inputs[0].shape.clone();
899        Ok(tvec!(out_dt.fact(out_shape)))
900    }
901
902    fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
903        let count: TDim = self.output_facts(inputs)?[0].shape.iter().product();
904        Ok(self
905            .binop
906            .cost_per_element(inputs[0].datum_type)
907            .into_iter()
908            .map(|(c, n)| (c, count.clone() * n))
909            .collect())
910    }
911
912    as_op!();
913}
914
915#[macro_export]
916macro_rules! bin_to_super_type {
917    ($func:ident, $Op:ident,
918     $(codegen: $codegen:expr,)?
919     $(cost: $cost:expr,)?
920     $(declutter: $declutter:expr,)?
921     $(eval_in_a: $eval_in_a:expr,)?
922     $(eval_override: $eval_override: expr,)?
923     $(linalg: $linalg:ident,)?
924     $(operating_datum_type: $operating_datum_type:expr,)?
925     $(is_commutative: $is_commutative:expr,)?
926     $(neutral_element: $neutral_element:expr,)?
927     $(absorbing_element: $absorbing_element:expr,)?
928     $(out_of_place: $out_of_place:expr,)?
929     $(validation: $validation:expr,)?
930     $(q: $([$($typ_dt:ident),*] => $cab_dt:expr),* ;)?
931     $(q_op_on_f32: $q_op_on_f32:expr,)?
932     $( [$($typ:ident),*] => $cab:expr),*) => {
933        #[derive(Debug, Clone, Hash, PartialEq, Eq)]
934        pub struct $Op;
935        #[allow(clippy::redundant_closure_call)]
936        impl $crate::ops::binary::BinMiniOp for $Op {
937            fn name(&self) -> &'static str {
938                stringify!($Op)
939            }
940
941            fn eval_out_of_place(&self, c: &mut Tensor, a: &Tensor, b: &Tensor) -> TractResult<()> {
942                $(if $out_of_place(c, a, b)? { return Ok(()) } )?
943                    // Same-shape fast path: bypass ndarray Zip when c, a, b
944                    // share the same shape (and hence same len for plain
945                    // storage). Iterate over slices directly.
946                    if c.shape() == a.shape() && a.shape() == b.shape() {
947                        $(
948                            $(if c.datum_type() == $typ::datum_type() {
949                                let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
950                                let a_plain = a.try_as_plain()?;
951                                let a_slice = a_plain.as_slice::<$typ>()?;
952                                let b_plain = b.try_as_plain()?;
953                                let b_slice = b_plain.as_slice::<$typ>()?;
954                                let mut c_plain = c.try_as_plain_mut()?;
955                                let c_slice = c_plain.as_slice_mut::<$typ>()?;
956                                debug_assert_eq!(c_slice.len(), a_slice.len());
957                                debug_assert_eq!(c_slice.len(), b_slice.len());
958                                let len = c_slice.len();
959                                tract_linalg::multithread::par_chunks_mut(c_slice, 1, len, |first_row, c_chunk| {
960                                    let n = c_chunk.len();
961                                    let a_chunk = &a_slice[first_row..first_row + n];
962                                    let b_chunk = &b_slice[first_row..first_row + n];
963                                    for ((cv, av), bv) in c_chunk.iter_mut().zip(a_chunk.iter()).zip(b_chunk.iter()) {
964                                        cab(cv, av, bv);
965                                    }
966                                    Ok(())
967                                })?;
968                                return Ok(())
969                            })*
970                        )*
971                        $(
972                            $(
973                                $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
974                                    let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
975                                    let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
976                                    let a_plain = a.try_as_plain()?;
977                                    let a_slice = a_plain.as_slice::<$typ_dt>()?;
978                                    let b_plain = b.try_as_plain()?;
979                                    let b_slice = b_plain.as_slice::<$typ_dt>()?;
980                                    let mut c_plain = c.try_as_plain_mut()?;
981                                    let c_slice = c_plain.as_slice_mut::<$typ_dt>()?;
982                                    for ((cv, av), bv) in c_slice.iter_mut().zip(a_slice.iter()).zip(b_slice.iter()) {
983                                        cab(cv, av, bv, zp, scale);
984                                    }
985                                    return Ok(())
986                                })*
987                            )*
988                        )?
989                    }
990                    $(
991                        $(if c.datum_type() == $typ::datum_type() {
992                            let a = a.to_plain_array_view::<$typ>()?;
993                            let b = b.to_plain_array_view::<$typ>()?;
994                            let mut c_plain = c.try_as_plain_mut()?;
995                            let mut c = c_plain.to_array_view_mut::<$typ>()?;
996                            $crate::ndarray::Zip::from(&mut c).and_broadcast(a).and_broadcast(b).for_each($cab);
997                            return Ok(())
998                        })*
999                     )*
1000                    $(
1001                        $(
1002                            $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1003                                let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1004                                let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1005                                let a = a.to_plain_array_view::<$typ_dt>()?;
1006                                let b = b.to_plain_array_view::<$typ_dt>()?;
1007                                let mut c_plain = c.try_as_plain_mut()?;
1008                                let mut c = c_plain.to_array_view_mut::<$typ_dt>()?;
1009                                $crate::ndarray::Zip::from(&mut c).and_broadcast(a).and_broadcast(b).for_each(|c, a, b| cab(c, a, b, zp, scale));
1010                                return Ok(())
1011                            }
1012                            )*
1013                         )*
1014                     )?
1015                    bail!("{} does not support {:?} (out of place)", self.name(), c.datum_type());
1016            }
1017
1018            $(fn is_commutative(&self) -> bool {
1019                $is_commutative
1020            })?
1021            $(fn neutral_element(&self) -> Option<i64> {
1022                Some($neutral_element)
1023            })?
1024            $(fn absorbing_element(&self) -> Option<i64> {
1025                Some($absorbing_element)
1026            })?
1027            fn eval_in_a(&self, a: &mut Tensor, b: &Tensor) -> TractResult<()> {
1028                // c and a are same type
1029                $(if $eval_in_a(a, b)? { return Ok(()) } )?
1030                // Same-shape fast path: bypass ndarray Zip when a and b share
1031                // the same shape (and hence same len for plain storage).
1032                if a.shape() == b.shape() {
1033                    $(
1034                        $(if b.datum_type() == $typ::datum_type() {
1035                            let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
1036                            let b_plain = b.try_as_plain()?;
1037                            let b_slice = b_plain.as_slice::<$typ>()?;
1038                            let mut a_plain = a.try_as_plain_mut()?;
1039                            let a_slice = a_plain.as_slice_mut::<$typ>()?;
1040                            debug_assert_eq!(a_slice.len(), b_slice.len());
1041                            let len = a_slice.len();
1042                            tract_linalg::multithread::par_chunks_mut(a_slice, 1, len, |first_row, a_chunk| {
1043                                let n = a_chunk.len();
1044                                let b_chunk = &b_slice[first_row..first_row + n];
1045                                for (av, bv) in a_chunk.iter_mut().zip(b_chunk.iter()) {
1046                                    cab(av, &av.clone(), bv);
1047                                }
1048                                Ok(())
1049                            })?;
1050                            return Ok(())
1051                        })*
1052                    )*
1053                    $(
1054                        $(
1055                            $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1056                                let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1057                                let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1058                                let b_plain = b.try_as_plain()?;
1059                                let b_slice = b_plain.as_slice::<$typ_dt>()?;
1060                                let mut a_plain = a.try_as_plain_mut()?;
1061                                let a_slice = a_plain.as_slice_mut::<$typ_dt>()?;
1062                                for (av, bv) in a_slice.iter_mut().zip(b_slice.iter()) {
1063                                    cab(av, &(av.clone()), bv, zp, scale);
1064                                }
1065                                return Ok(())
1066                            })*
1067                        )*
1068                    )?
1069                }
1070                $(
1071                    $(if b.datum_type() == $typ::datum_type() {
1072                        let cab: fn(&mut $typ, &$typ, &$typ) -> () = $cab;
1073                        let b = b.to_plain_array_view::<$typ>()?;
1074                        let mut a_plain = a.try_as_plain_mut()?;
1075                        let mut a = a_plain.to_array_view_mut::<$typ>()?;
1076                        $crate::ndarray::Zip::from(&mut a).and_broadcast(b).for_each(|a, b| cab(a, &a.clone(), b));
1077                        return Ok(())
1078                    })*
1079                )*
1080                $(
1081                    $(
1082                        $(if a.datum_type().unquantized() == <$typ_dt>::datum_type().unquantized() {
1083                            let cab: fn(&mut $typ_dt, &$typ_dt, &$typ_dt, i32, f32) -> () = $cab_dt;
1084                            let (zp, scale) = a.datum_type().qparams().map(|q| q.zp_scale()).unwrap_or((0, 1.));
1085                            let mut a_plain = a.try_as_plain_mut()?;
1086                            let mut a = a_plain.to_array_view_mut::<$typ_dt>()?;
1087                            let b = b.to_plain_array_view::<$typ_dt>()?;
1088                            $crate::ndarray::Zip::from(&mut a).and_broadcast(b).for_each(|a, b| {
1089                                cab(a, &(a.clone()), b, zp, scale)
1090                            });
1091                            return Ok(())
1092                        })*
1093                    )*
1094                )?
1095                bail!("{} does not support {:?} (eval in a)", self.name(), a.datum_type());
1096            }
1097
1098            $(fn eval(&self, a: TValue, b: TValue, c_dt: DatumType) -> TractResult<Tensor> {
1099                $eval_override(a, b, c_dt)
1100            })?
1101
1102            fn result_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType> {
1103                if a.unquantized() == b.unquantized() {
1104                    if a.is_quantized() || !b.is_quantized() {
1105                        return Ok(a)
1106                    }
1107                    else {
1108                        return Ok(b)
1109                    }
1110                }
1111                self.operating_datum_type(a, b)
1112            }
1113
1114                $(
1115                    fn declutter(
1116                        &self,
1117                        model: &TypedModel,
1118                        node: &TypedNode,
1119                        ) -> TractResult<Option<TypedModelPatch>> {
1120                        ($declutter)(self, model, node)
1121                    }
1122                 )?
1123                $(
1124                    fn codegen(
1125                        &self,
1126                        model: &TypedModel,
1127                        node: &TypedNode,
1128                        a: &Arc<Tensor>,
1129                        ) -> TractResult<Option<TypedModelPatch>> {
1130                        ($codegen)(self, model, node, a)
1131                    }
1132                 )?
1133                $(
1134                    fn cost_per_element(&self, dt: DatumType) -> TVec<(Cost, usize)> {
1135                        ($cost)(dt)
1136                    }
1137                 )?
1138                $(
1139                    fn validation(&self) -> Validation {
1140                        $validation
1141                    }
1142                 )?
1143                $(
1144                    fn as_linalg_binop(&self) -> Option<tract_linalg::BinOp> {
1145                        Some(tract_linalg::BinOp::$linalg)
1146                    }
1147                 )?
1148                $(
1149                    fn operating_datum_type(&self, a: DatumType, b: DatumType) -> TractResult<DatumType> {
1150                        ($operating_datum_type)(a, b)
1151                    })?
1152
1153
1154            /// Default simple binary operation for QFormat where
1155            /// we dequantise & apply requested operation in float & requantize it
1156            /// several implementation are provided with pro & con
1157            #[allow(unused_variables)]
1158            fn maybe_eval_qbinary_as_float_op(
1159                &self,
1160                a: &TValue,
1161                b: &TValue,
1162                c_dt: &DatumType,
1163            ) -> TractResult<Option<Tensor>> {
1164                $(
1165                    /// Implementation strive to minimise memory allocation and access
1166                    /// we apply only if type is QU8 zp_scale datum type
1167                    /// maybe more suited for large models tensors
1168                    fn memory_optimised_q_binary_as_float_op(
1169                        a: &TValue,
1170                        b: &TValue,
1171                        c_dt: &DatumType,
1172                    ) -> TractResult<Option<Tensor>> {
1173                        if let (DatumType::QU8(QParams::ZpScale {zero_point: a_zp, scale: a_scale}),
1174                                DatumType::QU8(QParams::ZpScale {zero_point: b_zp, scale: b_scale}),
1175                                DatumType::QU8(QParams::ZpScale {zero_point: c_zp, scale: c_scale})) =
1176                            (a.datum_type(), b.datum_type(), c_dt)
1177                        {
1178                            let c_inv_scale = 1.0 / c_scale;
1179                            let a = a.to_plain_array_view::<u8>()?;
1180                            let b = b.to_plain_array_view::<u8>()?;
1181                            let c_shape = $crate::broadcast::multi_broadcast(&[a.shape(), b.shape()])?;
1182                            let mut c = Tensor::zero_dt(*c_dt, &c_shape)?;
1183                            let mut c_plain = c.try_as_plain_mut()?;
1184                            let view = c_plain.to_array_view_mut::<u8>()?;
1185                            $crate::ndarray::Zip::from(view).and_broadcast(a).and_broadcast(b).for_each(|c, a, b| {
1186                                *c = (scale_by($q_op_on_f32(
1187                                            ((*a as i32 - a_zp as i32) as f32 * a_scale),
1188                                            ((*b as i32 - b_zp as i32) as f32 * b_scale),
1189                                ), c_inv_scale) as i32
1190                                    + *c_zp as i32)
1191                                    .clamp_cast()
1192                            });
1193                            return Ok(Some(c));
1194                        }
1195                        Ok(None)
1196                    }
1197
1198                    /// Apply to all Q types
1199                    /// Take more memory but hopefully faster than memory_optimised_q_binary_as_float_op
1200                    /// especially once cast_to_dt will have will have vectorized implementations
1201                    fn generic_q_binary_as_float_op(
1202                        a: &TValue,
1203                        b: &TValue,
1204                        c_dt: &DatumType,
1205                        accumulator_dt: DatumType
1206                    ) -> TractResult<Option<Tensor>> {
1207                        if a.datum_type().is_quantized() && b.datum_type().is_quantized() && c_dt.is_quantized() {
1208                            let a = a.cast_to_dt(accumulator_dt)?.into_owned();
1209                            let b = b.cast_to_dt(accumulator_dt)?.into_owned();
1210                            let c_shape = $crate::broadcast::multi_broadcast(&[a.shape(), b.shape()])?;
1211                            let mut c = Tensor::zero_dt(accumulator_dt, &c_shape)?;
1212                            match accumulator_dt {
1213                                DatumType::F32 => {
1214                                    let mut c_plain = c.try_as_plain_mut()?;
1215                                    let view = c_plain.to_array_view_mut::<f32>()?;
1216                                    $crate::ndarray::Zip::from(view).and_broadcast(a.try_as_plain()?.to_array_view()?).and_broadcast(b.try_as_plain()?.to_array_view()?).for_each(|c, a, b| {
1217                                        *c = $q_op_on_f32(*a,*b);
1218                                    })
1219                                },
1220                                other => bail!("unexpected accumulator data type as {:?}", other)
1221                            };
1222
1223                            return Ok(Some(c.cast_to_dt(*c_dt)?.into_owned()));
1224                        }
1225                        Ok(None)
1226                    }
1227
1228                    if let Some(c) = memory_optimised_q_binary_as_float_op(a, b, c_dt)? {
1229                        return Ok(Some(c));
1230                    }
1231                    if let Some(d) = generic_q_binary_as_float_op(a, b, c_dt, DatumType::F32)? {
1232                        return Ok(Some(d));
1233                    }
1234                )?
1235                Ok(None)
1236            }
1237        }
1238
1239        pub fn $func() -> $crate::ops::binary::TypedBinOp {
1240            $crate::ops::binary::TypedBinOp(Box::new($Op), None)
1241        }
1242    };
1243}
1244
1245#[derive(Debug)]
1246pub(crate) struct OneUniformInput {
1247    pub uni: Arc<Tensor>,
1248    pub var: OutletId,
1249    pub left_is_uniform: bool,
1250}
1251
1252pub(crate) fn one_input_is_uniform(
1253    model: &TypedModel,
1254    node: &TypedNode,
1255) -> TractResult<Option<OneUniformInput>> {
1256    if let &[a, b] = &*model.node_input_facts(node.id)? {
1257        let uni = if let Some(a) = &a.uniform {
1258            OneUniformInput { uni: a.clone(), var: node.inputs[1], left_is_uniform: true }
1259        } else if let Some(b) = &b.uniform {
1260            OneUniformInput { uni: b.clone(), var: node.inputs[0], left_is_uniform: false }
1261        } else {
1262            return Ok(None);
1263        };
1264        let var_fact = [a, b][uni.left_is_uniform as usize];
1265        let uni_fact = [a, b][!uni.left_is_uniform as usize];
1266        if izip!(var_fact.shape.iter(), uni_fact.shape.iter()).all(|(v, u)| u.is_one() || u == v) {
1267            return Ok(Some(uni));
1268        }
1269    }
1270    Ok(None)
1271}
1272
1273#[cfg(test)]
1274mod tests {
1275    use super::*;
1276
1277    /// Reproducer for the OptBinUnicast panic seen on Nemotron decoder CI
1278    /// (cuda-lovelace + Darwin). A 1-D tensor that goes through `insert_axis`
1279    /// twice ends up with declared shape `[1, 1, 640]` but strides `[1, 1, 1]`
1280    /// instead of the natural `[640, 640, 1]`. TensorView::at_prefix then
1281    /// returns a view whose `len()` reads `strides[1] = 1`, so the unicast
1282    /// kernel sees `a.len = 1, b.len = 640` and OOBs into the tile buffer.
1283    ///
1284    /// Pre-fix this test panics inside `linalg/src/frame/unicast.rs` with
1285    /// "range end index 640 out of range for slice of length …". With the
1286    /// natural-strides guard in `OptBinUnicast::eval`, the call falls back to
1287    /// `BinMiniOp::eval` and produces correct output.
1288    #[test]
1289    fn opt_bin_unicast_falls_back_on_non_natural_strides() {
1290        // Construct `a` the way the LSTM bias path does: build a 640-element
1291        // 1-D tensor, then insert two leading unit dims.
1292        let a_data: Vec<f32> = (0..640).map(|i| i as f32).collect();
1293        let mut a = tensor1(&a_data);
1294        a.insert_axis(0).unwrap();
1295        a.insert_axis(0).unwrap();
1296        assert_eq!(a.shape(), &[1, 1, 640]);
1297        assert_eq!(a.strides(), &[1, 1, 1]);
1298        assert_ne!(a.strides(), &*Tensor::natural_strides(a.shape()));
1299
1300        // `b` is a normal contiguous tensor of the same declared shape.
1301        let b_data: Vec<f32> = vec![1.0; 640];
1302        let mut b = tensor1(&b_data);
1303        b.insert_axis(0).unwrap();
1304        b.insert_axis(0).unwrap();
1305        // Reset b to natural strides so we exercise only the a-broken path
1306        // and let the b-side go through cleanly.
1307        b = b.into_shape(&[1, 1, 640]).unwrap();
1308
1309        let linalg_fn = Func::BinUnicast(BinOp::Add)
1310            .bin(f32::datum_type())
1311            .expect("f32 unicast Add kernel available");
1312        let op = OptBinUnicast { binop: Box::new(Add), eval_fn: Arc::from(linalg_fn) };
1313
1314        let out =
1315            op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1316        let out = &out[0];
1317        assert_eq!(out.shape(), &[1, 1, 640]);
1318        let plain = out.try_as_plain().unwrap();
1319        let out_slice = plain.as_slice::<f32>().unwrap();
1320        for (i, v) in out_slice.iter().enumerate() {
1321            assert_eq!(*v, i as f32 + 1.0, "mismatch at {i}");
1322        }
1323    }
1324
1325    /// A zero-sized outer dim (a symbolic batch or sequence resolving to 0) has
1326    /// no elements to write, so both fast paths must no-op. The kernels cannot
1327    /// be handed the empty tensor: `as_slice_mut` builds a slice from the null
1328    /// data pointer, and the by-scalar kernel reads `b[0]` off a zero-length
1329    /// slice.
1330    #[test]
1331    fn zero_sized_outer_dim_is_a_noop() {
1332        let a = Tensor::zero::<f32>(&[0, 4, 8]).unwrap();
1333        let b = Tensor::zero::<f32>(&[0, 4, 1]).unwrap();
1334        let linalg_fn = Func::BinByScalar(BinOp::Add)
1335            .bin(f32::datum_type())
1336            .expect("f32 by_scalar Add kernel available");
1337        let op = OptBinByScalar {
1338            binop: Box::new(Add),
1339            eval_fn: Arc::from(linalg_fn),
1340            linalg_op: BinOp::Add,
1341        };
1342        let out =
1343            op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1344        assert_eq!(out[0].shape(), &[0, 4, 8]);
1345
1346        let a = Tensor::zero::<f32>(&[0, 4, 16]).unwrap();
1347        let b = Tensor::zero::<f32>(&[1, 4, 16]).unwrap();
1348        let linalg_fn = Func::BinUnicast(BinOp::Add)
1349            .bin(f32::datum_type())
1350            .expect("f32 unicast Add kernel available");
1351        let op = OptBinUnicast { binop: Box::new(Add), eval_fn: Arc::from(linalg_fn) };
1352        let out =
1353            op.eval(&EvalContext::out_of_plan(), tvec!(a.into_tvalue(), b.into_tvalue())).unwrap();
1354        assert_eq!(out[0].shape(), &[0, 4, 16]);
1355    }
1356
1357    /// A quantized uniform input that dequantizes to 0 is neutral for Add on
1358    /// either side, but the node still re-encodes its variable input into the
1359    /// output quantization parameters. `declutter_neutral` must therefore cast
1360    /// the variable input, which is inlet 1 here, not inlet 0.
1361    #[test]
1362    fn q_neutral_add_requantizes_the_variable_input() -> TractResult<()> {
1363        let x_dt = DatumType::QU8(QParams::ZpScale { zero_point: 10, scale: 0.02 });
1364        let zero_dt = DatumType::QU8(QParams::ZpScale { zero_point: 61, scale: 1. });
1365        let out_dt = DatumType::QU8(QParams::ZpScale { zero_point: 0, scale: 0.5 });
1366
1367        let mut model = TypedModel::default();
1368        let x = model.add_source("x", x_dt.fact([4]))?;
1369        // A quantized zero tensor is filled with its zero point, so this
1370        // constant dequantizes to 0.0 everywhere.
1371        let zero = model.add_const("zero", Tensor::zero_dt(zero_dt, &[4])?)?;
1372        let add = model.wire_node("add", TypedBinOp(Box::new(Add), Some(out_dt)), &[zero, x])?[0];
1373        model.select_output_outlets(&[add])?;
1374
1375        let mut input = Tensor::zero_dt(x_dt, &[4])?;
1376        input.try_as_plain_mut()?.as_slice_mut::<u8>()?.copy_from_slice(&[10, 35, 60, 200]);
1377        let input = tvec!(input.into_tvalue());
1378
1379        let before = model.clone().into_runnable()?.run(input.clone())?;
1380        let decluttered = model.into_decluttered()?;
1381        assert!(decluttered.nodes().iter().all(|n| n.op_as::<TypedBinOp>().is_none()));
1382        let after = decluttered.into_runnable()?.run(input)?;
1383        assert_eq!(&*before[0], &*after[0]);
1384        Ok(())
1385    }
1386}