Skip to main content

tract_core/ops/
binary.rs

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        // 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_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        // 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    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        // The unicast fast path indexes each input's storage via at_prefix +
863        // as_slice_mut, which uses `strides[i-1]` to size the resulting slice
864        // (data/src/tensor/view.rs:99). That formula only matches ∏(shape[i..])
865        // when the tensor has natural C-order strides. Producers like
866        // Tensor::insert_axis leave non-natural strides on a tensor (e.g.
867        // shape `[1, 1, 640]` with strides `[1, 1, 1]` after two insert_axis
868        // on a `[640]` tensor), which silently breaks the slice math. Fall
869        // back to the generic broadcasting eval when either operand is not in
870        // natural strides (or has a storage size that doesn't match the
871        // declared shape).
872        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        // b's leading unary axes are the ones a repeats over; past them b matches
883        // a exactly (check_input_shapes), so one kernel call covers b.len()
884        // elements of a and b is consumed in lockstep within each.
885        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/// One step of an [`OptBinChain`] pass: an `OptBinUnicast` ([`BShare::Lockstep`])
924/// or `OptBinByScalar` ([`BShare::PerBlock`]) reduced to its kernel, operand
925/// sharing pattern and mini-op for fallback eval and cost.
926#[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/// Elementwise binary ops folded into a single pass over one accumulator.
948/// Input 0 is `a`; input `i + 1` is step `i`'s `b` operand. Each step streams
949/// the same `a`, so evaluating them back to back per chunk — which
950/// [`tract_linalg::multithread::par_bin_chain`] does — replaces one full memory
951/// pass per step by one pass total, with a bit-identical result. Formed by the
952/// `fuse` pass on `OptBinUnicast`/`OptBinByScalar`/`OptBinChain` chains where
953/// every step keeps `a`'s datum type.
954#[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
990/// Evaluate one chain step with the same fast-path discipline the source op
991/// would have used alone: `par_bin` on natural strides, `repeat_broadcast`
992/// under a short PerBlock period, generic `binop.eval` otherwise.
993fn 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        // A PerBlock operand sharing one scalar with fewer than a vector's
1060        // worth of elements belongs on `repeat_broadcast`; run the chain one
1061        // step at a time so that step can take it.
1062        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
1119/// Collapse a run of `OptBinUnicast`, `OptBinByScalar` and `OptBinChain` nodes
1120/// passing one tensor through each other into a single [`OptBinChain`]. Only
1121/// fires on the head of a run — when the `a` producer could itself absorb this
1122/// node, defer to its `fuse` instead — and only while every step's operand has
1123/// the accumulator's datum type and the step maps it back to that type, which
1124/// is what keeps the pass in place on `a`.
1125fn 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    // Defer to the producer when it could absorb this node itself: only the
1151    // head of the run builds the patch.
1152    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        // our value must be the successor's accumulator, not its operand.
1176        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                    // Same-shape fast path: bypass ndarray Zip when c, a, b
1240                    // share the same shape (and hence same len for plain
1241                    // storage). Iterate over slices directly.
1242                    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                // c and a are same type
1325                $(if $eval_in_a(a, b)? { return Ok(()) } )?
1326                // Same-shape fast path: bypass ndarray Zip when a and b share
1327                // the same shape (and hence same len for plain storage).
1328                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            /// Default simple binary operation for QFormat where
1451            /// we dequantise & apply requested operation in float & requantize it
1452            /// several implementation are provided with pro & con
1453            #[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                    /// Implementation strive to minimise memory allocation and access
1462                    /// we apply only if type is QU8 zp_scale datum type
1463                    /// maybe more suited for large models tensors
1464                    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                    /// Apply to all Q types
1495                    /// Take more memory but hopefully faster than memory_optimised_q_binary_as_float_op
1496                    /// especially once cast_to_dt will have will have vectorized implementations
1497                    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    /// Reproducer for the OptBinUnicast panic seen on Nemotron decoder CI
1574    /// (cuda-lovelace + Darwin). A 1-D tensor that goes through `insert_axis`
1575    /// twice ends up with declared shape `[1, 1, 640]` but strides `[1, 1, 1]`
1576    /// instead of the natural `[640, 640, 1]`. TensorView::at_prefix then
1577    /// returns a view whose `len()` reads `strides[1] = 1`, so the unicast
1578    /// kernel sees `a.len = 1, b.len = 640` and OOBs into the tile buffer.
1579    ///
1580    /// Pre-fix this test panics inside `linalg/src/frame/unicast.rs` with
1581    /// "range end index 640 out of range for slice of length …". With the
1582    /// natural-strides guard in `OptBinUnicast::eval`, the call falls back to
1583    /// `BinMiniOp::eval` and produces correct output.
1584    #[test]
1585    fn opt_bin_unicast_falls_back_on_non_natural_strides() {
1586        // Construct `a` the way the LSTM bias path does: build a 640-element
1587        // 1-D tensor, then insert two leading unit dims.
1588        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        // `b` is a normal contiguous tensor of the same declared shape.
1597        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        // Reset b to natural strides so we exercise only the a-broken path
1602        // and let the b-side go through cleanly.
1603        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    /// A zero-sized outer dim (a symbolic batch or sequence resolving to 0) has
1622    /// no elements to write, so both fast paths must no-op. The kernels cannot
1623    /// be handed the empty tensor: `as_slice_mut` builds a slice from the null
1624    /// data pointer, and the by-scalar kernel reads `b[0]` off a zero-length
1625    /// slice.
1626    #[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    /// A quantized uniform input that dequantizes to 0 is neutral for Add on
1654    /// either side, but the node still re-encodes its variable input into the
1655    /// output quantization parameters. `declutter_neutral` must therefore cast
1656    /// the variable input, which is inlet 1 here, not inlet 0.
1657    #[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        // A quantized zero tensor is filled with its zero point, so this
1666        // constant dequantizes to 0.0 everywhere.
1667        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}