Skip to main content

tract_core/ops/matmul/
optimized.rs

1use crate::internal::*;
2use crate::ops::cast::{Cast, cast};
3use crate::ops::change_axes::wire_with_rank_broadcast;
4use crate::ops::element_wise::ElementWiseOp;
5use crate::ops::nn::LeakyRelu;
6use crate::ops::{FrozenOpState, OpStateFreeze};
7use ndarray::*;
8use tract_itertools::Itertools;
9
10use tract_linalg::mmm::{
11    AsInputValue, EagerPackedInput, FusedSpec, MMMInputValue, MatMatMul, OutputStore,
12    OutputStoreSpec, PackedMatrixStorage, PanelExtractInput, PanelExtractor,
13};
14use tract_linalg::pack::PackedFormat;
15use tract_linalg::{BinOp, Scaler};
16use tract_smallvec::ToSmallVec;
17
18use super::ModePicker;
19
20/// If `new` is `old` with only size-1 axes dropped (non-unit axes untouched, in
21/// order, nothing added or merged), return the removed axis indices; otherwise
22/// `None`. Such a reshape is a pure metadata squeeze the matmul store can absorb.
23fn pure_squeeze_removed(old: &[usize], new: &[usize]) -> Option<TVec<usize>> {
24    let mut removed: TVec<usize> = tvec!();
25    let mut j = 0;
26    for (i, &d) in old.iter().enumerate() {
27        if j < new.len() && d == new[j] {
28            j += 1;
29        } else if d == 1 {
30            removed.push(i);
31        } else {
32            return None;
33        }
34    }
35    (j == new.len() && !removed.is_empty()).then_some(removed)
36}
37
38/// A matmul operand: either an index into the runtime inputs, or a constant
39/// packed value baked in at `fuse()` time (its source outlet was `konst`), so
40/// it is never re-resolved per call. Only non-batched operands are baked.
41#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum MatMulOperand {
43    Input(usize),
44    Const(Box<dyn MMMInputValue>),
45}
46
47impl MatMulOperand {
48    /// Base packed value for the trivial (non-batched) path.
49    #[inline]
50    unsafe fn trivial_value<'t>(&'t self, inputs: &'t [TValue]) -> &'t dyn MMMInputValue {
51        match self {
52            MatMulOperand::Input(i) => unsafe {
53                inputs
54                    .get_unchecked(*i)
55                    .try_storage_as::<PackedMatrixStorage>()
56                    .unwrap_unchecked()
57                    .value()
58            },
59            MatMulOperand::Const(v) => &**v,
60        }
61    }
62}
63
64#[derive(Clone, Debug, PartialEq, Eq)]
65pub enum ProtoFusedSpec {
66    AddMatMul {
67        geo: AddMatMulGeometry,
68        a: MatMulOperand,
69        b: MatMulOperand,
70        packings: Vec<(usize, Option<PanelExtractor>)>,
71    },
72    BinScalar(usize, BinOp),
73    LeakyRelu(usize),
74    BinPerRow(usize, BinOp, MapOutputAxisToInput),
75    BinPerCol(usize, BinOp, MapOutputAxisToInput),
76    AddRowColProducts(usize, usize),
77    AddUnicast(OutputStoreSpec, usize, MapOutputAxisToInput),
78    Scaler(Scaler),
79    Store(Vec<OutputStoreSpec>),
80}
81
82impl ProtoFusedSpec {
83    pub fn format(&self, mmm: &dyn MatMatMul, mode: usize) -> String {
84        use ProtoFusedSpec::*;
85        match self {
86            AddMatMul { geo, packings: packing, .. } => {
87                let (a, b) = &mmm.packings()[packing[mode].0];
88                format!("matmul(k={}, {a:?}•{b:?})", geo.k)
89            }
90            BinScalar(_, op) => format!("scalar{op:?}"),
91            LeakyRelu(alpha) => format!("leaky_relu({alpha:?})"),
92            BinPerRow(_, op, _) => format!("row{op:?}"),
93            BinPerCol(_, op, _) => format!("col{op:?}"),
94            AddRowColProducts(_, _) => "add_row_col_product".to_string(),
95            AddUnicast(_, _, _) => "add_to_matrix".to_string(),
96            Scaler(s) => format!("scale({})", 1f32 * *s),
97            Store(_oss) => "store".to_string(),
98        }
99    }
100
101    pub fn resolve<'t>(
102        &'t self,
103        inputs: &'t [TValue],
104        output_coords: &[usize],
105        output: &Tensor,
106        mmm: &dyn MatMatMul,
107        mode: usize,
108    ) -> FusedSpec<'t> {
109        #[allow(clippy::let_and_return)]
110        let fs = match self {
111            ProtoFusedSpec::AddMatMul { geo, a, b, packings } => {
112                let resolve =
113                    |operand: &'t MatMulOperand, mapping: &MapOutputAxisToInput| match operand {
114                        MatMulOperand::Input(i) => {
115                            let storage =
116                                inputs[*i].try_storage_as::<PackedMatrixStorage>().unwrap();
117                            let idx = mapping.flat_index(output_coords, storage.batch_strides());
118                            storage.value_at_flat(idx)
119                        }
120                        MatMulOperand::Const(v) => &**v,
121                    };
122                let a = resolve(a, &geo.c_to_a_axis_mapping);
123                let b = resolve(b, &geo.c_to_b_axis_mapping);
124
125                let (_a_packing, b_packing) = &mmm.packings()[packings[mode].0];
126                let pa = if let Some(extractor) = &packings[mode].1 {
127                    let data = a.downcast_ref::<EagerPackedInput>().unwrap();
128                    AsInputValue::Owned(Box::new(PanelExtractInput {
129                        format: extractor.clone(),
130                        data: data.clone(),
131                    }))
132                } else {
133                    AsInputValue::Borrowed(a)
134                };
135                assert!(
136                    b_packing.dyn_eq(b.format())
137                        || (b_packing.is::<PackedFormat>() && b_packing.r() == b.format().r())
138                );
139                debug_assert!(pa.k().to_dim().compatible_with(&geo.k.to_dim()));
140                debug_assert!(b.k().to_dim().compatible_with(&geo.k.to_dim()));
141                FusedSpec::AddMatMul {
142                    a: pa,
143                    b: AsInputValue::Borrowed(b),
144                    packing: packings[mode].0,
145                }
146            }
147            ProtoFusedSpec::BinScalar(v, op) => FusedSpec::BinScalar(&inputs[*v], *op),
148            ProtoFusedSpec::LeakyRelu(v) => FusedSpec::LeakyRelu(&inputs[*v]),
149            ProtoFusedSpec::BinPerRow(v, op, map) => {
150                let mut v = inputs[*v].view();
151                unsafe { map.translate_view(output_coords, &mut v) }
152                FusedSpec::BinPerRow(v, *op)
153            }
154            ProtoFusedSpec::BinPerCol(v, op, map) => {
155                let mut v = inputs[*v].view();
156                unsafe { map.translate_view(output_coords, &mut v) }
157                FusedSpec::BinPerCol(v, *op)
158            }
159            ProtoFusedSpec::AddRowColProducts(row, col) => {
160                FusedSpec::AddRowColProducts(&inputs[*row], &inputs[*col])
161            }
162            ProtoFusedSpec::AddUnicast(store, v, map) => unsafe {
163                let mut view = inputs[*v].view();
164                map.translate_view(output_coords, &mut view);
165                FusedSpec::AddUnicast(store.wrap(&view))
166            },
167            ProtoFusedSpec::Scaler(scaler) => scaler.as_fused_spec(),
168            ProtoFusedSpec::Store(oss) => unsafe {
169                let view = output.view_offsetting_unchecked(output_coords);
170                FusedSpec::Store(oss[mode].wrap(&view))
171            },
172        };
173        fs
174    }
175
176    pub fn is_trivial(&self) -> bool {
177        match self {
178            ProtoFusedSpec::AddMatMul { geo, .. } => geo.k.as_i64().is_some(),
179            _ => true,
180        }
181    }
182
183    pub fn resolve_trivial<'t>(
184        &'t self,
185        inputs: &'t [TValue],
186        output: &mut Tensor,
187        _mmm: &dyn MatMatMul,
188        mode: usize,
189    ) -> FusedSpec<'t> {
190        #[allow(clippy::let_and_return)]
191        let fs = match self {
192            ProtoFusedSpec::AddMatMul { a, b, packings, .. } => unsafe {
193                let a = a.trivial_value(inputs);
194                let b = b.trivial_value(inputs);
195                debug_assert!(packings.len() == 1);
196                debug_assert!(packings[0].1.is_none()); // no panel extraction
197                #[cfg(debug_assertions)]
198                {
199                    let (a_packing, b_packing) = &_mmm.packings()[packings[mode].0];
200                    debug_assert!(
201                        a_packing.dyn_eq(a.format())
202                            || (a_packing.is::<PackedFormat>() && a_packing.r() == a.format().r())
203                    );
204                    debug_assert!(
205                        b_packing.dyn_eq(b.format())
206                            || (b_packing.is::<PackedFormat>() && b_packing.r() == b.format().r())
207                    );
208                }
209                FusedSpec::AddMatMul {
210                    a: AsInputValue::Borrowed(a),
211                    b: AsInputValue::Borrowed(b),
212                    packing: packings[mode].0,
213                }
214            },
215            ProtoFusedSpec::BinScalar(v, op) => FusedSpec::BinScalar(&inputs[*v], *op),
216            ProtoFusedSpec::LeakyRelu(v) => FusedSpec::LeakyRelu(&inputs[*v]),
217            ProtoFusedSpec::BinPerRow(v, op, _) => {
218                let v = inputs[*v].view();
219                FusedSpec::BinPerRow(v, *op)
220            }
221            ProtoFusedSpec::BinPerCol(v, op, _) => {
222                let v = inputs[*v].view();
223                FusedSpec::BinPerCol(v, *op)
224            }
225            ProtoFusedSpec::AddRowColProducts(row, col) => {
226                FusedSpec::AddRowColProducts(&inputs[*row], &inputs[*col])
227            }
228            ProtoFusedSpec::AddUnicast(store, v, _) => unsafe {
229                let view = inputs[*v].view();
230                FusedSpec::AddUnicast(store.wrap(&view))
231            },
232            ProtoFusedSpec::Scaler(scaler) => scaler.as_fused_spec(),
233            ProtoFusedSpec::Store(oss) => unsafe {
234                FusedSpec::Store(oss[mode].wrap(&output.view_mut()))
235            },
236        };
237        fs
238    }
239
240    /// Like [`resolve_trivial`], but a `Store` reuses a cached [`OutputStore`]
241    /// whose strides/layout are fixed for the output shape, refreshing only its
242    /// base pointer from `output`. Everything else defers to [`resolve_trivial`]
243    /// (constant operands are already baked into the op, so no per-call work).
244    fn resolve_trivial_cached<'t>(
245        &'t self,
246        inputs: &'t [TValue],
247        output: &mut Tensor,
248        mmm: &dyn MatMatMul,
249        mode: usize,
250        store: Option<OutputStore>,
251    ) -> FusedSpec<'t> {
252        match self {
253            ProtoFusedSpec::Store(oss) => unsafe {
254                FusedSpec::Store(match store {
255                    Some(cached) => cached.with_tensor(&output.view()),
256                    None => oss[mode].wrap(&output.view_mut()),
257                })
258            },
259            _ => self.resolve_trivial(inputs, output, mmm, mode),
260        }
261    }
262
263    fn check_inputs(&self, inputs: &[&TypedFact]) -> TractResult<()> {
264        use ProtoFusedSpec::*;
265        match self {
266            AddMatMul { a, b, .. } => {
267                for operand in [a, b] {
268                    if let MatMulOperand::Input(ix) = operand {
269                        ensure!(inputs[*ix].is_exotic());
270                    }
271                }
272            }
273            BinScalar(v, _)
274            | LeakyRelu(v)
275            | BinPerCol(v, _, _)
276            | BinPerRow(v, _, _)
277            | AddUnicast(_, v, _) => {
278                ensure!(inputs[*v].datum_type.is_number());
279            }
280            AddRowColProducts(row, col) => {
281                ensure!(inputs[*row].datum_type.is_number());
282                ensure!(inputs[*col].datum_type.is_number());
283            }
284            _ => (),
285        };
286        Ok(())
287    }
288
289    fn cost(&self, m: &TDim, n: &TDim, idt: DatumType) -> TVec<(Cost, TDim)> {
290        match self {
291            ProtoFusedSpec::AddMatMul { geo, .. } => {
292                tvec!((Cost::FMA(idt), m.clone() * n * &geo.k))
293            }
294            _ => tvec!(), /* FIXME maybe */
295        }
296    }
297
298    /// Collect the C axes this op reads through an output→input mapping — i.e.
299    /// the matmul batch axes. `rm_c_axis` only shifts indices past a removed
300    /// axis; it assumes none of these is the one being removed, so a fusion that
301    /// folds a C axis away must first check it is absent here.
302    fn push_mapped_c_axes(&self, out: &mut TVec<usize>) {
303        use ProtoFusedSpec::*;
304        match self {
305            AddMatMul { geo, .. } => {
306                out.extend(geo.c_to_a_axis_mapping.0.iter().map(|(c, _)| *c));
307                out.extend(geo.c_to_b_axis_mapping.0.iter().map(|(c, _)| *c));
308            }
309            BinPerRow(_, _, map) | BinPerCol(_, _, map) | AddUnicast(_, _, map) => {
310                out.extend(map.0.iter().map(|(c, _)| *c));
311            }
312            BinScalar(..) | Scaler(..) | AddRowColProducts(_, _) | LeakyRelu(_) | Store(..) => {}
313        }
314    }
315
316    fn rm_c_axis(&mut self, axis: usize) {
317        use ProtoFusedSpec::*;
318        match self {
319            AddMatMul { geo, .. } => {
320                geo.c_to_a_axis_mapping.rm_c_axis(axis);
321                geo.c_to_b_axis_mapping.rm_c_axis(axis);
322            }
323            BinScalar(..) | Scaler(..) | AddRowColProducts(_, _) | LeakyRelu(_) => {}
324            BinPerRow(_, _, map) | BinPerCol(_, _, map) => map.rm_c_axis(axis),
325            AddUnicast(_, _, map) => {
326                map.rm_c_axis(axis);
327            }
328            Store(oss, ..) => {
329                for oss in oss {
330                    match oss {
331                        OutputStoreSpec::View { m_axis, n_axis, .. } => {
332                            if let Some(m) = m_axis {
333                                *m -= (*m > axis) as usize
334                            };
335                            if let Some(n) = n_axis {
336                                *n -= (*n > axis) as usize
337                            }
338                        }
339                        OutputStoreSpec::Strides { .. } => {}
340                    }
341                }
342            }
343        }
344    }
345}
346
347#[derive(Clone, Debug, PartialEq, Eq)]
348pub struct MapOutputAxisToInput(pub TVec<(usize, usize)>);
349
350impl MapOutputAxisToInput {
351    #[inline]
352    unsafe fn translate_view(&self, output_coords: &[usize], v: &mut TensorView) {
353        for &(out_axis, in_axis) in &self.0 {
354            unsafe { v.offset_axis(in_axis, output_coords[out_axis] as isize) }
355        }
356    }
357
358    #[inline]
359    fn rm_c_axis(&mut self, axis: usize) {
360        for (c, _) in &mut self.0 {
361            *c -= (*c > axis) as usize;
362        }
363    }
364
365    /// Compute a flat index into a PackedMatrixStorage from output coordinates and batch strides.
366    #[inline]
367    pub fn flat_index(&self, output_coords: &[usize], batch_strides: &[isize]) -> usize {
368        self.0
369            .iter()
370            .map(|&(out_axis, in_axis)| output_coords[out_axis] * batch_strides[in_axis] as usize)
371            .sum()
372    }
373}
374
375#[derive(Clone, Debug, PartialEq, Eq)]
376pub struct AddMatMulGeometry {
377    pub k: TDim,
378    pub c_to_a_axis_mapping: MapOutputAxisToInput,
379    pub c_to_b_axis_mapping: MapOutputAxisToInput,
380}
381
382#[derive(Clone, Debug, PartialEq, Eq)]
383pub struct OptMatMul {
384    pub c_fact: TypedFact,
385    pub micro_ops: Vec<ProtoFusedSpec>,
386    pub mmm: Vec<Box<dyn MatMatMul>>,
387    pub mode_picker: ModePicker,
388    pub c_m_axis: Option<usize>,
389    pub c_n_axis: Option<usize>,
390    pub trivial_packing: bool,
391    pub trivial_path: bool,
392}
393
394impl Op for OptMatMul {
395    fn name(&self) -> StaticName {
396        "OptMatMul".into()
397    }
398
399    fn info(&self) -> TractResult<Vec<String>> {
400        let m = self.c_m_axis.map(|ix| &self.c_fact.shape[ix]).unwrap_or(&TDim::Val(1));
401        let n = self.c_n_axis.map(|ix| &self.c_fact.shape[ix]).unwrap_or(&TDim::Val(1));
402        let mut infos = vec![format!(
403            "c_shape:{:?}, c_m_axis:{:?} c_n_axis:{:?} m:{} n:{}",
404            self.c_fact, self.c_m_axis, self.c_n_axis, m, n,
405        )];
406        if let Some(k) = self.guess_k() {
407            infos.push(format!("Mult: m:{} k:{} n:{} with {:?}", m, k, n, self.mmm));
408        } else {
409            infos.push(format!("Mult: {:?}", self.mmm));
410        }
411        for (mode, mmm) in self.mmm.iter().enumerate() {
412            infos.push(format!(
413                "Ops: {}",
414                self.micro_ops.iter().map(|o| o.format(&**mmm, mode)).join(" >>> ")
415            ));
416        }
417        Ok(infos)
418    }
419
420    op_as_typed_op!();
421}
422
423/// Per-execution state for [`OptMatMul`]: on the trivial path it caches each
424/// micro-op's `Store` [`OutputStore`] layout (strides fixed for the output
425/// shape), so only the base pointer is refreshed per call. Constant operands
426/// are baked into the op at `fuse()`, so they need no per-call state here. Pure
427/// memoization: dropped on freeze and rebuilt lazily on next eval.
428#[derive(Clone, Debug, Default)]
429pub struct OptMatMulState {
430    trivial_stores: Option<TVec<Option<OutputStore>>>,
431}
432
433impl EvalOp for OptMatMul {
434    fn is_stateless(&self) -> bool {
435        false
436    }
437
438    fn state(
439        &self,
440        _session: &TurnState,
441        _node_id: usize,
442    ) -> TractResult<Option<Box<dyn OpState>>> {
443        Ok(Some(Box::<OptMatMulState>::default()))
444    }
445}
446
447impl OpState for OptMatMulState {
448    fn eval(
449        &mut self,
450        session: &mut TurnState,
451        op: &dyn Op,
452        inputs: TVec<TValue>,
453    ) -> TractResult<TVec<TValue>> {
454        let op = op.downcast_ref::<OptMatMul>().context("OptMatMulState on non-OptMatMul op")?;
455        op.eval_with_state(session, inputs, self)
456    }
457}
458
459#[derive(Clone, Debug)]
460struct FrozenOptMatMulState;
461
462impl FrozenOpState for FrozenOptMatMulState {
463    fn unfreeze(&self) -> Box<dyn OpState> {
464        Box::<OptMatMulState>::default()
465    }
466}
467
468impl OpStateFreeze for OptMatMulState {
469    fn freeze(&self) -> Box<dyn FrozenOpState> {
470        Box::new(FrozenOptMatMulState)
471    }
472}
473
474impl OptMatMul {
475    fn eval_with_state(
476        &self,
477        session: &TurnState,
478        inputs: TVec<TValue>,
479        state: &mut OptMatMulState,
480    ) -> TractResult<TVec<TValue>> {
481        unsafe {
482            let c_shape = self.c_fact.shape.eval_to_usize(&session.resolved_symbols)?;
483            let mut c = Tensor::uninitialized_dt(self.c_fact.datum_type, &c_shape)?;
484            let m = self.c_m_axis.map(|c_m| c.shape()[c_m]).unwrap_or(1);
485            let n = self.c_n_axis.map(|c_n| c.shape()[c_n]).unwrap_or(1);
486            let mode = self.mode_picker.pick(n)?;
487            let mmm = &*self.mmm[mode];
488            let mut cell = session.cached_mmm_scratch_space.borrow_mut();
489            if !cell.as_ref().is_some_and(|scratch| mmm.can_use_scratch_space(&**scratch)) {
490                *cell = None
491            }
492            let scratch = cell.get_or_insert_with(|| mmm.allocate_scratch_space());
493            if self.trivial_path {
494                let stores = state.trivial_stores.get_or_insert_with(|| {
495                    self.micro_ops
496                        .iter()
497                        .map(|o| match o {
498                            ProtoFusedSpec::Store(oss) => Some(oss[mode].wrap(&c.view())),
499                            _ => None,
500                        })
501                        .collect()
502                });
503                let uops: TVec<FusedSpec> = self
504                    .micro_ops
505                    .iter()
506                    .zip(stores.iter())
507                    .map(|(o, store)| o.resolve_trivial_cached(&inputs, &mut c, mmm, mode, *store))
508                    .collect();
509                mmm.run_with_scratch_space(m, n, scratch.as_mut(), &uops)?;
510                Ok(tvec!(c.into_tvalue()))
511            } else {
512                let mut uops = vec![FusedSpec::ShiftLeft(0); self.micro_ops.len()];
513                let mut looping_shape: TVec<usize> = c_shape.to_smallvec();
514                if let Some(ax) = self.c_m_axis {
515                    looping_shape[ax] = 1;
516                }
517                if let Some(ax) = self.c_n_axis {
518                    looping_shape[ax] = 1;
519                }
520                for c_coords in indices(&*looping_shape) {
521                    for ix in 0..self.micro_ops.len() {
522                        *uops.get_unchecked_mut(ix) = self.micro_ops.get_unchecked(ix).resolve(
523                            &inputs,
524                            c_coords.slice(),
525                            &c,
526                            mmm,
527                            mode,
528                        );
529                    }
530                    mmm.run_with_scratch_space(m, n, scratch.as_mut(), &uops)
531                        .context("In mmm.run_with_scratch_space")?;
532                }
533                Ok(tvec!(c.into_tvalue()))
534            }
535        }
536    }
537}
538
539impl TypedOp for OptMatMul {
540    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
541        ensure!(self.c_m_axis.map(|ax| ax < self.c_fact.rank()).unwrap_or(true));
542        ensure!(self.c_n_axis.map(|ax| ax < self.c_fact.rank()).unwrap_or(true));
543        ensure!(self.trivial_path == self.can_use_trivial_path());
544        ensure!(self.mmm.iter().map(|mmm| mmm.internal_type()).all_equal());
545        for op in &self.micro_ops {
546            op.check_inputs(inputs)?;
547        }
548        Ok(tvec!(self.c_fact.clone()))
549    }
550
551    fn cost(&self, _inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
552        let mut sums = HashMap::new();
553        for op in &self.micro_ops {
554            for (cost, count) in op.cost(self.m(), self.n(), self.mmm[0].internal_type()) {
555                *sums.entry(cost).or_default() += count;
556            }
557        }
558        let loops = self
559            .c_fact
560            .shape
561            .iter()
562            .enumerate()
563            .map(|(ix, d)| {
564                if Some(ix) == self.c_m_axis || Some(ix) == self.c_n_axis {
565                    1.to_dim()
566                } else {
567                    d.clone()
568                }
569            })
570            .product::<TDim>();
571        for s in &mut sums.values_mut() {
572            *s *= &loops;
573        }
574        Ok(sums.into_iter().collect())
575    }
576
577    fn fuse(&self, model: &TypedModel, node: &TypedNode) -> TractResult<Option<TypedModelPatch>> {
578        use crate::ops;
579        if let Some(patch) = self.bake_const_operands(model, node)? {
580            return Ok(Some(patch));
581        }
582        rule_if!(node.outputs.len() == 1);
583        rule_if!(node.outputs[0].successors.len() == 1);
584        rule_if!(!model.output_outlets()?.contains(&node.id.into()));
585        let succ = model.node(node.outputs[0].successors[0].node);
586        let mut patch = TypedModelPatch::new(format!("fusing {succ}"));
587
588        if let Some(op) = succ.op_as::<ops::binary::TypedBinOp>() {
589            rule_if_some!(mut binop = op.0.as_linalg_binop());
590            let flipped = succ.inputs[0].node == node.id;
591            if flipped {
592                binop = binop.flip();
593            }
594            let other_outlet = succ.inputs[flipped as usize];
595            return self.fuse_binary(model, node, patch, other_outlet, binop);
596        }
597        if let Some(op) = succ.op_as::<ops::binary::OptBinByScalar>() {
598            rule_if_some!(mut binop = op.binop.as_linalg_binop());
599            let flipped = succ.inputs[0].node == node.id;
600            if flipped {
601                binop = binop.flip();
602            }
603            let other_outlet = succ.inputs[flipped as usize];
604            return self.fuse_binary(model, node, patch, other_outlet, binop);
605        }
606
607        if let Some(op) = succ.op_as::<ops::element_wise::ElementWiseOp>().map(|ew| ew.0.as_ref()) {
608            if let Some(op) = op.downcast_ref::<ops::math::QScale>() {
609                return self.fuse_op(
610                    model,
611                    node,
612                    patch,
613                    vec![ProtoFusedSpec::Scaler(op.scaler)],
614                    &[],
615                );
616            }
617            if let Some(op) = op.downcast_ref::<LeakyRelu>() {
618                rule_if!(
619                    self.mmm
620                        .iter()
621                        .all(|mmm| mmm.can_fuse(&FusedSpec::LeakyRelu(&tensor0(op.alpha))))
622                );
623                let alpha = patch.add_const(
624                    node.name.to_string() + ".alpha",
625                    tensor0(op.alpha).cast_to_dt(self.mmm[0].internal_type())?.into_owned(),
626                )?;
627                return self.fuse_op(
628                    model,
629                    node,
630                    patch,
631                    vec![ProtoFusedSpec::LeakyRelu(node.inputs.len())],
632                    &[alpha],
633                );
634            }
635        }
636        if let Some(cast_to) = succ.op_as::<ops::cast::Cast>().map(|cast| cast.to)
637            && (((cast_to.unquantized() == i8::datum_type()
638                || cast_to.unquantized() == u8::datum_type())
639                && self.c_fact.datum_type == i32::datum_type())
640                || self.mmm.iter().all(|m| m.stores().contains(&cast_to)))
641            && let Some(ProtoFusedSpec::Store(stores)) = self.micro_ops.last()
642        {
643            rule_if!(stores.iter().all(|s| !matches!(s, OutputStoreSpec::Strides { .. })));
644            let c_fact = cast_to.fact(self.c_fact.shape.clone());
645            let mut patch =
646                TypedModelPatch::fuse_with_next(model, node, Self { c_fact, ..self.clone() })?;
647            patch.dont_apply_twice = Some(format!("Fuse {succ} into {node}"));
648            return Ok(Some(patch));
649        }
650        if let Some(AxisOp::Rm(axis)) = succ.op_as::<ops::AxisOp>() {
651            rule_if!(Some(*axis) != self.c_m_axis);
652            rule_if!(Some(*axis) != self.c_n_axis);
653            let mut new_op = self.clone();
654            new_op.c_fact.shape.remove_axis(*axis)?;
655            if let Some(c_m_axis) = &mut new_op.c_m_axis {
656                *c_m_axis -= (*c_m_axis > *axis) as usize;
657            }
658            if let Some(c_n_axis) = &mut new_op.c_n_axis {
659                *c_n_axis -= (*c_n_axis > *axis) as usize;
660            }
661            for uop in &mut new_op.micro_ops {
662                uop.rm_c_axis(*axis);
663            }
664            let mut patch = TypedModelPatch::fuse_with_next(model, node, new_op)?;
665            patch.dont_apply_twice = Some(format!("Fuse {succ} into {node}"));
666            return Ok(Some(patch));
667        }
668        if let Some(into) = succ.op_as::<IntoShape>()
669            && let Some(new_op) = self.absorb_squeeze(into)
670        {
671            let mut patch = TypedModelPatch::fuse_with_next(model, node, new_op)?;
672            patch.dont_apply_twice = Some(format!("Fuse {succ} into {node}"));
673            return Ok(Some(patch));
674        }
675        // Reach over a shape-agnostic elementwise (Tanh/Sigmoid/Cast/…) to absorb
676        // a squeeze reshape into the store: matmul → ew → squeeze becomes
677        // matmul(squeezed) → ew, unchanged since the op is per-element. With the
678        // direct arm above, squeeze reshapes fuse whether before or after the ew.
679        if (succ.op_is::<ElementWiseOp>() || succ.op_is::<Cast>())
680            && succ.outputs.len() == 1
681            && let &[next] = &*succ.outputs[0].successors
682        {
683            let into_node = model.node(next.node);
684            if let Some(into) = into_node.op_as::<IntoShape>()
685                && let Some(new_op) = self.absorb_squeeze(into)
686            {
687                let mut patch = TypedModelPatch::default();
688                let inputs = node
689                    .inputs
690                    .iter()
691                    .map(|i| patch.tap_model(model, *i))
692                    .collect::<TractResult<TVec<_>>>()?;
693                let mm = patch.wire_node(&node.name, new_op, &inputs)?[0];
694                let ew = patch.wire_node(&succ.name, succ.op.clone(), &[mm])?[0];
695                patch.shunt_outside(model, into_node.id.into(), ew)?;
696                patch.dont_apply_twice = Some(format!("Reach {into_node} into {node}"));
697                return Ok(Some(patch));
698            }
699        }
700        if (succ.op_is::<AxisOp>() || succ.op_is::<IntoShape>())
701            && let &[next] = &*succ.outputs[0].successors
702        {
703            let next_node = model.node(next.node);
704            if let Some(cast) = next_node.op_as::<Cast>() {
705                let mut patch = TypedModelPatch::default();
706                let mut wire = patch.tap_model(model, node.id.into())?;
707                wire = patch.wire_node(&next_node.name, cast.clone(), &[wire])?[0];
708                wire = patch.wire_node(&succ.name, succ.op.clone(), &[wire])?[0];
709                patch.shunt_outside(model, next_node.id.into(), wire)?;
710                return Ok(Some(patch));
711            } else if let Some(op) = next_node.op_as::<ops::binary::TypedBinOp>() {
712                rule_if!(op.0.as_linalg_binop().is_some());
713                let flipped = succ.inputs[0].node == node.id;
714                let other_outlet = next_node.inputs[flipped as usize];
715                if let Some(uni) = &model.outlet_fact(other_outlet)?.uniform {
716                    let mut patch = TypedModelPatch::default();
717                    let cst = patch.add_const(&model.node(other_outlet.node).name, uni.clone())?;
718                    let output = patch.tap_model(model, node.id.into())?;
719                    let wire = wire_with_rank_broadcast(
720                        &next_node.name,
721                        &mut patch,
722                        op.clone(),
723                        &if flipped { [output, cst] } else { [cst, output] },
724                    )?;
725                    let wire = patch.wire_node(&succ.name, succ.op.clone(), &wire)?[0];
726                    patch.shunt_outside(model, next_node.id.into(), wire)?;
727                    return Ok(Some(patch));
728                }
729            }
730        }
731        if let Some(op) = succ.op_as::<ops::binary::OptBinUnicast>() {
732            let in_1_fact = model.outlet_fact(succ.inputs[0])?;
733            let in_2_fact = model.outlet_fact(succ.inputs[1])?;
734            if op.binop.is::<ops::math::Add>()
735                && self.mmm.len() == 1
736                && in_1_fact.without_value() == in_2_fact.without_value()
737            {
738                let other_slot = 1 - node.outputs[0].successors[0].slot;
739                let other_input = succ.inputs[other_slot];
740                let other_input = patch.tap_model(model, other_input)?;
741                let other_fact = patch.outlet_fact(other_input)?;
742
743                if other_fact.shape == self.c_fact.shape {
744                    let other_storage = unsafe { self.mmm[0].c_view(self.c_m_axis, self.c_n_axis) };
745                    let mapping =
746                        MapOutputAxisToInput((0..other_fact.rank()).map(|x| (x, x)).collect());
747                    return self.fuse_op(
748                        model,
749                        node,
750                        patch,
751                        vec![ProtoFusedSpec::AddUnicast(other_storage, node.inputs.len(), mapping)],
752                        &[other_input],
753                    );
754                }
755            } else {
756                rule_if_some!(mut binop = op.binop.as_linalg_binop());
757                let flipped = succ.inputs[0].node == node.id;
758                if flipped {
759                    binop = binop.flip();
760                }
761                let other_outlet = succ.inputs[flipped as usize];
762                return self.fuse_binary(model, node, patch, other_outlet, binop);
763            }
764        };
765        Ok(None)
766    }
767
768    as_op!();
769}
770
771impl OptMatMul {
772    pub fn new(
773        mmm: Vec<Box<dyn MatMatMul>>,
774        mode_picker: ModePicker,
775        c_fact: TypedFact,
776        c_m_axis: Option<usize>,
777        c_n_axis: Option<usize>,
778        micro_ops: Vec<ProtoFusedSpec>,
779        trivial_packing: bool,
780    ) -> TractResult<Self> {
781        if let Some(m) = c_m_axis {
782            ensure!(m < c_fact.rank());
783        }
784        if let Some(n) = c_n_axis {
785            ensure!(n < c_fact.rank());
786        }
787        let mut it = OptMatMul {
788            mmm,
789            mode_picker,
790            c_fact,
791            c_m_axis,
792            c_n_axis,
793            micro_ops,
794            trivial_path: false,
795            trivial_packing,
796        };
797        it.update_trivial_path();
798        Ok(it)
799    }
800
801    // for auditing only (may return None if no AddMatMul is found)
802    pub fn guess_k(&self) -> Option<TDim> {
803        self.micro_ops
804            .iter()
805            .find_map(
806                |o| {
807                    if let ProtoFusedSpec::AddMatMul { geo, .. } = o { Some(geo) } else { None }
808                },
809            )
810            .map(|geo| geo.k.clone())
811    }
812
813    #[inline]
814    pub fn m(&self) -> &TDim {
815        self.c_m_axis.map(|ax| &self.c_fact.shape[ax]).unwrap_or(&TDim::Val(1))
816    }
817
818    #[inline]
819    pub fn n(&self) -> &TDim {
820        self.c_n_axis.map(|ax| &self.c_fact.shape[ax]).unwrap_or(&TDim::Val(1))
821    }
822
823    fn update_trivial_path(&mut self) {
824        self.trivial_path = self.can_use_trivial_path();
825    }
826
827    /// If `into` is a pure unit-axis squeeze of this op's (concrete) output that
828    /// leaves the m/n axes intact, return a clone whose store produces the
829    /// squeezed shape directly. `None` when the reshape can't be absorbed.
830    fn absorb_squeeze(&self, into: &IntoShape) -> Option<Self> {
831        if into.strides != Tensor::natural_strides(&into.dims) {
832            return None;
833        }
834        let old = self.c_fact.shape.as_concrete()?;
835        let removed = pure_squeeze_removed(old, &into.dims)?;
836        if removed.iter().any(|ax| Some(*ax) == self.c_m_axis || Some(*ax) == self.c_n_axis) {
837            return None;
838        }
839        // A non-unit matmul batch axis (e.g. grouped conv) makes the packed
840        // inputs per-batch; folding any axis then desyncs that batch indexing.
841        // Only fuse when every batch axis is trivial (size 1).
842        let mut batch_axes: TVec<usize> = tvec!();
843        self.micro_ops.iter().for_each(|uop| uop.push_mapped_c_axes(&mut batch_axes));
844        if batch_axes.iter().any(|ax| old.get(*ax).copied().unwrap_or(1) > 1) {
845            return None;
846        }
847        let mut new_op = self.clone();
848        for axis in removed.iter().rev() {
849            new_op.c_fact.shape.remove_axis(*axis).ok()?;
850            if let Some(c_m_axis) = &mut new_op.c_m_axis {
851                *c_m_axis -= (*c_m_axis > *axis) as usize;
852            }
853            if let Some(c_n_axis) = &mut new_op.c_n_axis {
854                *c_n_axis -= (*c_n_axis > *axis) as usize;
855            }
856            for uop in &mut new_op.micro_ops {
857                uop.rm_c_axis(*axis);
858            }
859        }
860        Some(new_op)
861    }
862
863    fn can_use_trivial_path(&self) -> bool {
864        self.c_fact.shape.is_concrete()
865            && self.c_fact.shape.iter().enumerate().all(|(ax, dim)| {
866                Some(ax) == self.c_m_axis || Some(ax) == self.c_n_axis || dim.is_one()
867            })
868            && self.trivial_packing
869            && self.micro_ops.iter().all(|o| o.is_trivial())
870    }
871
872    /// Bake matmul operands that are fed by a constant, non-batched input into
873    /// the op as [`MatMulOperand::Const`], dropping the corresponding graph
874    /// input. Runs once the packing has const-folded so the operand's outlet
875    /// carries a packed `konst`. Returns a patch that rewires the node with the
876    /// remaining inputs, or `None` if nothing is bakeable.
877    fn bake_const_operands(
878        &self,
879        model: &TypedModel,
880        node: &TypedNode,
881    ) -> TractResult<Option<TypedModelPatch>> {
882        let bakeable = |operand: &MatMulOperand, mapping: &MapOutputAxisToInput| -> bool {
883            if let MatMulOperand::Input(i) = operand {
884                mapping.0.is_empty()
885                    && model.outlet_fact(node.inputs[*i]).is_ok_and(|f| {
886                        f.konst
887                            .as_ref()
888                            .and_then(|k| k.try_storage_as::<PackedMatrixStorage>().ok())
889                            .is_some()
890                    })
891            } else {
892                false
893            }
894        };
895        let mut baked: TVec<usize> = tvec!();
896        for op in &self.micro_ops {
897            if let ProtoFusedSpec::AddMatMul { geo, a, b, .. } = op {
898                if bakeable(a, &geo.c_to_a_axis_mapping) {
899                    let MatMulOperand::Input(i) = a else { unreachable!() };
900                    baked.push(*i);
901                }
902                if bakeable(b, &geo.c_to_b_axis_mapping) {
903                    let MatMulOperand::Input(i) = b else { unreachable!() };
904                    baked.push(*i);
905                }
906            }
907        }
908        if baked.is_empty() {
909            return Ok(None);
910        }
911        baked.sort();
912        baked.dedup();
913        let remap: Vec<Option<usize>> = {
914            let mut ni = 0;
915            (0..node.inputs.len())
916                .map(|i| {
917                    (!baked.contains(&i)).then(|| {
918                        let cur = ni;
919                        ni += 1;
920                        cur
921                    })
922                })
923                .collect()
924        };
925        let const_value = |i: usize| -> TractResult<Box<dyn MMMInputValue>> {
926            let konst = model.outlet_fact(node.inputs[i])?.konst.clone().unwrap();
927            Ok(dyn_clone::clone_box(konst.try_storage_as::<PackedMatrixStorage>()?.value()))
928        };
929        let map_operand = |operand: &MatMulOperand| -> TractResult<MatMulOperand> {
930            Ok(match operand {
931                MatMulOperand::Input(i) if baked.contains(i) => {
932                    MatMulOperand::Const(const_value(*i)?)
933                }
934                MatMulOperand::Input(i) => MatMulOperand::Input(remap[*i].unwrap()),
935                MatMulOperand::Const(v) => MatMulOperand::Const(v.clone()),
936            })
937        };
938        let micro_ops = self
939            .micro_ops
940            .iter()
941            .map(|op| -> TractResult<ProtoFusedSpec> {
942                use ProtoFusedSpec::*;
943                Ok(match op {
944                    AddMatMul { geo, a, b, packings } => AddMatMul {
945                        geo: geo.clone(),
946                        a: map_operand(a)?,
947                        b: map_operand(b)?,
948                        packings: packings.clone(),
949                    },
950                    BinScalar(v, op) => BinScalar(remap[*v].unwrap(), *op),
951                    LeakyRelu(v) => LeakyRelu(remap[*v].unwrap()),
952                    BinPerRow(v, op, m) => BinPerRow(remap[*v].unwrap(), *op, m.clone()),
953                    BinPerCol(v, op, m) => BinPerCol(remap[*v].unwrap(), *op, m.clone()),
954                    AddRowColProducts(r, c) => {
955                        AddRowColProducts(remap[*r].unwrap(), remap[*c].unwrap())
956                    }
957                    AddUnicast(s, v, m) => AddUnicast(*s, remap[*v].unwrap(), m.clone()),
958                    Scaler(s) => Scaler(*s),
959                    Store(o) => Store(o.clone()),
960                })
961            })
962            .collect::<TractResult<Vec<_>>>()?;
963        let new_op = OptMatMul { micro_ops, ..self.clone() };
964        let kept: TVec<OutletId> =
965            (0..node.inputs.len()).filter(|i| !baked.contains(i)).map(|i| node.inputs[i]).collect();
966        let mut patch = TypedModelPatch::new(format!("bake const operands into {}", node.name));
967        let taps = patch.taps(model, &kept)?;
968        let output = patch.wire_node(&node.name, new_op, &taps)?;
969        patch.shunt_outside(model, node.id.into(), output[0])?;
970        Ok(Some(patch))
971    }
972
973    fn fuse_op(
974        &self,
975        model: &TypedModel,
976        node: &TypedNode,
977        mut patch: TypedModelPatch,
978        fused_micro_op: Vec<ProtoFusedSpec>,
979        additional_inputs: &[OutletId],
980    ) -> TractResult<Option<TypedModelPatch>> {
981        let succ = model.node(node.outputs[0].successors[0].node);
982        let mut new_op = self.clone();
983        let before_last = new_op.micro_ops.len() - 1..new_op.micro_ops.len() - 1;
984        new_op.micro_ops.splice(before_last, fused_micro_op);
985        new_op.c_fact = succ.outputs[0].fact.clone();
986        new_op.update_trivial_path();
987        let mut inputs = patch.taps(model, &node.inputs)?;
988        inputs.extend(additional_inputs.iter().cloned());
989        let output = patch.wire_node(&succ.name, new_op, &inputs)?;
990        patch.shunt_outside(model, succ.id.into(), output[0])?;
991        Ok(Some(patch))
992    }
993
994    fn fuse_binary(
995        &self,
996        model: &TypedModel,
997        node: &TypedNode,
998        mut patch: TypedModelPatch,
999        value: OutletId,
1000        binop: BinOp,
1001    ) -> TractResult<Option<TypedModelPatch>> {
1002        let fact = model.outlet_fact(value)?;
1003        let mut v = patch.tap_model(model, value)?;
1004        if fact.datum_type != self.mmm[0].internal_type() {
1005            v = patch.wire_node(
1006                format!("{}.cast-input-{}", node.name, node.inputs.len()),
1007                cast(self.mmm[0].internal_type()),
1008                &[v],
1009            )?[0];
1010        }
1011        let value = node.inputs.len();
1012        let additional_input = tvec!(v);
1013        if fact.shape.volume() == 1.to_dim() {
1014            return self.fuse_op(
1015                model,
1016                node,
1017                patch,
1018                vec![ProtoFusedSpec::BinScalar(value, binop)],
1019                &additional_input,
1020            );
1021        }
1022        let other_shape = fact.shape.to_owned();
1023        if self.c_m_axis.is_some_and(|ax| {
1024            other_shape[ax] == self.c_fact.shape[ax] && other_shape[ax] == other_shape.volume()
1025        }) {
1026            return self.fuse_op(
1027                model,
1028                node,
1029                patch,
1030                vec![ProtoFusedSpec::BinPerRow(
1031                    value,
1032                    binop,
1033                    MapOutputAxisToInput(tvec!((self.c_m_axis.unwrap(), self.c_m_axis.unwrap()))),
1034                )],
1035                &additional_input,
1036            );
1037        }
1038        if self.c_n_axis.is_some_and(|ax| {
1039            other_shape[ax] == self.c_fact.shape[ax] && other_shape[ax] == other_shape.volume()
1040        }) {
1041            return self.fuse_op(
1042                model,
1043                node,
1044                patch,
1045                vec![ProtoFusedSpec::BinPerCol(
1046                    value,
1047                    binop,
1048                    MapOutputAxisToInput(tvec!((self.c_n_axis.unwrap(), self.c_n_axis.unwrap()))),
1049                )],
1050                &additional_input,
1051            );
1052        }
1053        Ok(None)
1054    }
1055}