Skip to main content

tract_core/ops/einsum/
einsum_matmul.rs

1use std::fmt::Formatter;
2use std::ops::Deref;
3
4use tract_itertools::{izip, multiunzip};
5use tract_linalg::block_quant::PackedBlockQuantFormat;
6
7use super::*;
8use crate::ops::cast::cast;
9use crate::ops::math::add;
10use crate::ops::matmul::ModePicker;
11use crate::ops::matmul::optimized::{
12    AddMatMulGeometry, MapOutputAxisToInput, MatMulOperand, OptMatMul, ProtoFusedSpec,
13};
14use crate::ops::matmul::pack::{OptMatMulPack, OptSimpleMatMulPack};
15use crate::ops::matmul::quant::{
16    combine_scales, compensate_zero_points, requant, wire_ensure_q8_flavour,
17};
18use crate::ops::nn::{Reduce, Reducer};
19
20pub fn merge_consecutive_same_role_axes(model: &mut TypedModel) -> TractResult<()> {
21    Rewriter::default()
22        .with_rule_for("merge-same-role-axes", merge_same_role_axes_rule)
23        .with_rule_for("push-reshape-below-binop", push_reshape_below_binop_rule)
24        .rewrite(&(), model)
25}
26
27fn merge_same_role_axes_rule(
28    _ctx: &(),
29    model: &TypedModel,
30    node: &TypedNode,
31    node_name: &str,
32    op: &EinSum,
33) -> TractResult<Option<TypedModelPatch>> {
34    // Only handle 2-input EinSums (matmul-like)
35    rule_if!(node.inputs.len() == 2);
36
37    // Compute role signature for each axis: (in_input_0, in_input_1, in_output)
38    type Role = (bool, bool, bool);
39    let axes: Vec<(char, Role)> = op
40        .axes
41        .iter_all_axes()
42        .map(|a| {
43            (a.repr, (!a.inputs[0].is_empty(), !a.inputs[1].is_empty(), !a.outputs[0].is_empty()))
44        })
45        .collect();
46
47    // For each input/output slot, get the axis order
48    let a_order: Vec<char> = op.axes.axes(InOut::In(0)).map(|a| a.repr).collect();
49    let b_order: Vec<char> = op.axes.axes(InOut::In(1)).map(|a| a.repr).collect();
50    let c_order: Vec<char> = op.axes.axes(InOut::Out(0)).map(|a| a.repr).collect();
51
52    // Per-input shapes, used both to reject broadcast merges and to gauge
53    // whether a merge earns its reshape / axis-permute cost.
54    let input_facts = model.node_input_facts(node.id)?;
55    let input_shapes = op.actual_input_shapes_from_facts(&input_facts)?;
56    // An axis is "non-unit" if its extent is not statically 1 (symbolic extents
57    // count as potentially large). A fold only reduces the number of matmul
58    // invocations when it combines at least two non-unit axes; folding a unit
59    // axis — the streaming axis at pulse=1, or a batch axis of 1 — leaves the
60    // matmul geometry untouched and only adds reshapes (and, in the k-axis
61    // branch, a MoveAxis), so we decline those.
62    let is_non_unit = |c: &char| -> bool {
63        let dim = a_order
64            .iter()
65            .position(|x| x == c)
66            .map(|p| &input_shapes[0][p])
67            .or_else(|| b_order.iter().position(|x| x == c).map(|p| &input_shapes[1][p]));
68        dim.is_none_or(|d| d.as_i64() != Some(1))
69    };
70
71    // Find first group of 2+ same-role axes that are consecutive in all inputs.
72    // Scan each input's axis order for runs of same-role axes.
73    let role_map: std::collections::HashMap<char, Role> = axes.iter().cloned().collect();
74    let mut best_group: Option<Vec<char>> = None;
75
76    // Try each input order as the primary scan order
77    let all_orders = [&a_order, &b_order];
78    for (primary_idx, primary_order) in all_orders.iter().enumerate() {
79        let mut i = 0;
80        while i < primary_order.len() {
81            let first = primary_order[i];
82            let first_role = role_map[&first];
83            let mut group = vec![first];
84            let mut j = i + 1;
85            while j < primary_order.len() {
86                let candidate = primary_order[j];
87                if role_map[&candidate] != first_role {
88                    break;
89                }
90                // Check consecutive in the OTHER input too
91                let consecutive_in_others = all_orders
92                    .iter()
93                    .enumerate()
94                    .filter(|(idx, _)| *idx != primary_idx)
95                    .all(|(_, order)| {
96                        let positions: Vec<usize> = group
97                            .iter()
98                            .chain(std::iter::once(&candidate))
99                            .filter_map(|c| order.iter().position(|x| x == c))
100                            .collect();
101                        if positions.len() <= 1 {
102                            return true;
103                        }
104                        let mut sorted = positions.clone();
105                        sorted.sort();
106                        sorted == positions
107                            && sorted.last().unwrap() - sorted.first().unwrap() == sorted.len() - 1
108                    });
109                if !consecutive_in_others {
110                    break;
111                }
112                group.push(candidate);
113                j += 1;
114            }
115            if group.len() >= 2 && best_group.as_ref().is_none_or(|bg| group.len() > bg.len()) {
116                best_group = Some(group);
117            }
118            i = j;
119        }
120    }
121
122    if let Some(ref group) = best_group {
123        // Reject the group if any axis has mismatched per-input dims. This
124        // catches broadcasting cases — e.g. GQA `bhgmk,bhgnk->bhgmn` where g has
125        // dim 2 in input[0] and dim 1 in input[1]. Merging would collapse the
126        // broadcast structure into a non-broadcast dim mismatch that downstream
127        // OptMatMul codegen / kernels cannot handle.
128        let dims_match = group.iter().all(|c| {
129            match (a_order.iter().position(|x| x == c), b_order.iter().position(|x| x == c)) {
130                (Some(p0), Some(p1)) => input_shapes[0][p0] == input_shapes[1][p1],
131                _ => true,
132            }
133        });
134        // Decline merges that combine fewer than two non-unit axes: they don't
135        // shrink the matmul loop count, they only add reshapes / a MoveAxis.
136        let worth_merging = group.iter().filter(|c| is_non_unit(c)).count() >= 2;
137        if !dims_match || !worth_merging {
138            best_group = None;
139        }
140    }
141
142    if let Some(group) = best_group {
143        // Found a mergeable group. Emit the patch.
144        let output_shape = super::eval::output_shape(&op.axes, &input_shapes)?;
145
146        let drop_set: Vec<char> = group[1..].to_vec();
147
148        let mut patch = TypedModelPatch::default();
149        let mut wires: TVec<OutletId> = patch.taps(model, &node.inputs)?;
150
151        // Reshape each input to merge the group
152        for (slot, order) in [(0, &a_order), (1, &b_order)] {
153            let positions: Vec<usize> =
154                group.iter().filter_map(|c| order.iter().position(|x| x == c)).collect();
155            if positions.len() < 2 {
156                continue;
157            }
158            let start = positions[0];
159            let from_dims: TVec<TDim> =
160                positions.iter().map(|&p| input_shapes[slot][p].clone()).collect();
161            let merged: TDim = from_dims.iter().product();
162            wires[slot] = patch.wire_node(
163                format!("{node_name}.merge_in{slot}"),
164                AxisOp::Reshape(start, from_dims, tvec![merged]),
165                &[wires[slot]],
166            )?[0];
167        }
168
169        // If group axes aren't consecutive in C, reorder the EinSum output
170        let c_positions: Vec<usize> =
171            group.iter().filter_map(|c| c_order.iter().position(|x| x == c)).collect();
172        let c_needs_reorder = c_positions.len() >= 2 && {
173            let mut sorted = c_positions.clone();
174            sorted.sort();
175            sorted.last().unwrap() - sorted.first().unwrap() != sorted.len() - 1
176                || sorted != c_positions
177        };
178        let mut adjusted_c_order = c_order.clone();
179        if c_needs_reorder {
180            // Move group axes together (put second next to first)
181            for k in 1..c_positions.len() {
182                let cur_pos = adjusted_c_order.iter().position(|&c| c == group[k]).unwrap();
183                let target_pos =
184                    adjusted_c_order.iter().position(|&c| c == group[k - 1]).unwrap() + 1;
185                if cur_pos != target_pos {
186                    let removed = adjusted_c_order.remove(cur_pos);
187                    let insert_at = if cur_pos < target_pos { target_pos - 1 } else { target_pos };
188                    adjusted_c_order.insert(insert_at, removed);
189                }
190            }
191        }
192
193        // Rebuild EinSum formula with adjusted output and dropped axes
194        let in0: String = a_order.iter().collect();
195        let in1: String = b_order.iter().collect();
196        let out: String = adjusted_c_order.iter().collect();
197        let expr = format!("{in0},{in1}->{out}");
198        let mut new_axes: AxesMapping = expr.parse()?;
199        for &drop in &drop_set {
200            new_axes = new_axes.remove_axis(drop)?;
201        }
202        let new_op =
203            EinSum { axes: new_axes, operating_dt: op.operating_dt, q_params: op.q_params };
204        let mut result = patch.wire_node(node_name, new_op, &wires)?;
205
206        // Reshape output to split the merged axis back
207        let merged_c_positions: Vec<usize> =
208            group.iter().filter_map(|c| adjusted_c_order.iter().position(|x| x == c)).collect();
209        if merged_c_positions.len() >= 2 {
210            let start = merged_c_positions[0];
211            // Use original output dims for the group axes
212            let original_c_positions: Vec<usize> =
213                group.iter().filter_map(|c| c_order.iter().position(|x| x == c)).collect();
214            let original_dims: TVec<TDim> =
215                original_c_positions.iter().map(|&p| output_shape[p].clone()).collect();
216            let merged: TDim = original_dims.iter().product();
217            result[0] = patch.wire_node(
218                format!("{node_name}.unmerge_out"),
219                AxisOp::Reshape(start, tvec![merged], original_dims),
220                &[result[0]],
221            )?[0];
222        }
223
224        // Restore original output order if we reordered
225        if c_needs_reorder {
226            // After unmerge, axes are in adjusted_c_order (but with group expanded).
227            // Need to permute back to c_order.
228            // Build the unmerged adjusted order
229            let mut unmerged_adj: Vec<char> = Vec::new();
230            for &c in &adjusted_c_order {
231                if c == group[0] {
232                    unmerged_adj.extend(&group);
233                } else if !group.contains(&c) {
234                    unmerged_adj.push(c);
235                }
236            }
237            // Find what moves are needed to get from unmerged_adj to c_order
238            for (target_pos, &c_target) in c_order.iter().enumerate() {
239                let cur_pos = unmerged_adj.iter().position(|&c| c == c_target).unwrap();
240                if cur_pos != target_pos {
241                    result[0] = patch.wire_node(
242                        format!("{node_name}.restore_out_{target_pos}"),
243                        AxisOp::Move(cur_pos, target_pos),
244                        &[result[0]],
245                    )?[0];
246                    let removed = unmerged_adj.remove(cur_pos);
247                    unmerged_adj.insert(target_pos, removed);
248                }
249            }
250        }
251
252        patch.shunt_outside(model, node.id.into(), result[0])?;
253        return Ok(Some(patch));
254    }
255
256    // Second pass: look for same-role pairs separated by exactly one k-like axis
257    // in a single input. Insert a MoveAxis to push the separator to the end.
258    let k_role: Role = (true, true, false); // present in both inputs, absent from output
259    let role_of = |c: char| axes.iter().find(|(ch, _)| *ch == c).map(|(_, r)| *r);
260
261    for (slot, order) in [(0usize, &a_order), (1, &b_order)] {
262        // Find three consecutive axes in this input where the outer two share a role
263        // and the middle one is a k-axis
264        for w in order.windows(3) {
265            let (left, mid, right) = (w[0], w[1], w[2]);
266            let left_role = role_of(left);
267            let mid_role = role_of(mid);
268            let right_role = role_of(right);
269            if left_role != right_role || mid_role != Some(k_role) {
270                continue;
271            }
272            // Only move the k-axis aside if the resulting merge is worth it:
273            // both axes we'd bring together must be non-unit. Otherwise the
274            // MoveAxis buys nothing (e.g. a 1×1 conv at pulse=1, where the
275            // batch / streaming axes are 1 and the only real axis was already
276            // the matmul m).
277            if !is_non_unit(&left) || !is_non_unit(&right) {
278                continue;
279            }
280            // left and right must also be consecutive in other inputs
281            // (output order is handled by the EinSum formula)
282            let other_input_orders: Vec<&Vec<char>> = [(0, &a_order), (1, &b_order)]
283                .iter()
284                .filter(|(s, _)| *s != slot)
285                .map(|(_, o)| *o)
286                .collect();
287            let consecutive_elsewhere = other_input_orders.iter().all(|order| {
288                let lp = order.iter().position(|&c| c == left);
289                let rp = order.iter().position(|&c| c == right);
290                match (lp, rp) {
291                    (Some(l), Some(r)) => r == l + 1,
292                    _ => true, // one or both absent — no constraint
293                }
294            });
295            if !consecutive_elsewhere {
296                continue;
297            }
298
299            // Move the k-axis to the inner (last) position in inputs and
300            // make left,right adjacent in the output too.
301            let mid_pos = order.iter().position(|&c| c == mid).unwrap();
302            let end_pos = order.len() - 1;
303            if mid_pos == end_pos {
304                continue;
305            }
306
307            // Use change_axes to update the EinSum formula for the input move
308            let move_op = AxisOp::Move(mid_pos, end_pos);
309            let Some(AxisChangeConsequence { substitute_op, .. }) =
310                op.change_axes(model, node, InOut::In(slot), &move_op)?
311            else {
312                continue;
313            };
314            let mut current_op = *substitute_op
315                .unwrap()
316                .downcast::<EinSum>()
317                .map_err(|_| anyhow!("expected EinSum"))?;
318
319            // Also make left,right adjacent in the output if needed
320            let new_c: Vec<char> = current_op.axes.axes(InOut::Out(0)).map(|a| a.repr).collect();
321            let left_c = new_c.iter().position(|&c| c == left);
322            let right_c = new_c.iter().position(|&c| c == right);
323            let need_output_fix = matches!((left_c, right_c), (Some(l), Some(r)) if r != l + 1);
324            if need_output_fix {
325                let r_pos = right_c.unwrap();
326                let l_pos = left_c.unwrap();
327                let target = if r_pos < l_pos { l_pos } else { l_pos + 1 };
328                if let Some(AxisChangeConsequence { substitute_op, .. }) = current_op.change_axes(
329                    model,
330                    node,
331                    InOut::Out(0),
332                    &AxisOp::Move(r_pos, target),
333                )? {
334                    current_op = *substitute_op
335                        .unwrap()
336                        .downcast::<EinSum>()
337                        .map_err(|_| anyhow!("expected EinSum"))?;
338                }
339            }
340
341            let mut patch = TypedModelPatch::default();
342            let mut wires: TVec<OutletId> = patch.taps(model, &node.inputs)?;
343
344            wires[slot] =
345                patch.wire_node(format!("{node_name}.move_k_in{slot}"), move_op, &[wires[slot]])?
346                    [0];
347
348            let final_c: Vec<char> = current_op.axes.axes(InOut::Out(0)).map(|a| a.repr).collect();
349            let mut result = patch.wire_node(node_name, current_op, &wires)?;
350
351            // Restore original output order
352            if need_output_fix {
353                let r_cur = final_c.iter().position(|&c| c == right).unwrap();
354                let r_orig = c_order.iter().position(|&c| c == right).unwrap();
355                if r_cur != r_orig {
356                    result[0] = patch.wire_node(
357                        format!("{node_name}.restore_out"),
358                        AxisOp::Move(r_cur, r_orig),
359                        &[result[0]],
360                    )?[0];
361                }
362            }
363
364            patch.shunt_outside(model, node.id.into(), result[0])?;
365            return Ok(Some(patch));
366        }
367    }
368
369    Ok(None)
370}
371
372/// Slides a `Reshape` past a following binary so a matmul's epilogue — the
373/// summed partial products of a concat-split matmul, the per-channel BatchNorm
374/// affine, the Relu — sits directly on the matmul output and can be fused.
375/// `merge-same-role-axes` folds spatial axes into the matmul M axis and splits
376/// them back with an `unmerge_out` reshape; that split touches only the merged
377/// axis. Two operand shapes ride through it:
378/// - a per-channel constant, which broadcasts over the merged axis (extent 1),
379///   re-shaped through the split — trivial, since those extents are 1;
380/// - the identically-reshaped output of a sibling matmul, where
381///   `reshape(a) ∘ reshape(b) == reshape(a ∘ b)` lets the reshape hoist below
382///   the binary and the accumulation happen in merged space.
383fn push_reshape_below_binop_rule(
384    _ctx: &(),
385    model: &TypedModel,
386    node: &TypedNode,
387    node_name: &str,
388    op: &AxisOp,
389) -> TractResult<Option<TypedModelPatch>> {
390    let AxisOp::Reshape(at, from_dims, to_dims) = op else { return Ok(None) };
391    rule_if!(node.outputs.len() == 1);
392    rule_if!(node.outputs[0].successors.len() == 1);
393    rule_if!(!model.output_outlets()?.contains(&node.id.into()));
394
395    let inlet = node.outputs[0].successors[0];
396    let succ = model.node(inlet.node);
397    rule_if!(succ.inputs.len() == 2);
398    rule_if_some!(binop = succ.op_as::<crate::ops::binary::TypedBinOp>());
399    rule_if!(binop.0.as_linalg_binop().is_some());
400    let other = succ.inputs[1 - inlet.slot];
401
402    let mut patch = TypedModelPatch::new(format!("push {node} below {succ}"));
403    let act = patch.tap_model(model, node.inputs[0])?;
404
405    let sibling = if let Some(k) = model.outlet_fact(other)?.konst.clone() {
406        let at = *at;
407        let (from_len, to_len) = (from_dims.len(), to_dims.len());
408        let kshape = k.shape();
409        rule_if!(at + to_len <= kshape.len());
410        rule_if!(kshape[at..at + to_len].iter().all(|&d| d == 1));
411        let mut new_shape: TVec<usize> = kshape[..at].iter().copied().collect();
412        new_shape.extend(std::iter::repeat_n(1, from_len));
413        new_shape.extend(kshape[at + to_len..].iter().copied());
414        let k = k.as_ref().clone().into_shape(&new_shape)?;
415        patch.add_const(format!("{}.through_reshape", model.node(other.node).name), k)?
416    } else {
417        let other_node = model.node(other.node);
418        rule_if!(other_node.op_as::<AxisOp>() == Some(op));
419        patch.tap_model(model, other_node.inputs[0])?
420    };
421
422    let operands = if inlet.slot == 0 { [act, sibling] } else { [sibling, act] };
423    let applied = crate::ops::change_axes::wire_with_rank_broadcast(
424        format!("{}.prefused", succ.name),
425        &mut patch,
426        binop.clone(),
427        &operands,
428    )?;
429    let reshaped = patch.wire_node(format!("{node_name}.pushed"), op.clone(), &applied)?;
430    patch.shunt_outside(model, inlet.node.into(), reshaped[0])?;
431    Ok(Some(patch))
432}
433
434pub fn detect_all(model: &mut TypedModel) -> TractResult<()> {
435    Rewriter::default().with_rule_for("detect-matmul-einsum", detect_rule).rewrite(&(), model)
436}
437
438pub fn flatten_all(model: &mut TypedModel) -> TractResult<()> {
439    Rewriter::default().with_rule_for("flatten-matmul-einsum", flatten_rule).rewrite(&(), model)
440}
441
442#[derive(Clone, Hash, PartialEq, Eq)]
443pub struct EinSumMatMul {
444    pub op: EinSum,
445    pub m_axis: char,
446    pub k_axis: char,
447    pub n_axis: char,
448    pub m: TDim,
449    pub k: TDim,
450    pub n: TDim,
451}
452
453impl EinSumMatMul {
454    pub fn m_axis(&self) -> &Axis {
455        self.op.axes.axis(self.m_axis).unwrap()
456    }
457    pub fn k_axis(&self) -> &Axis {
458        self.op.axes.axis(self.k_axis).unwrap()
459    }
460    pub fn n_axis(&self) -> &Axis {
461        self.op.axes.axis(self.n_axis).unwrap()
462    }
463    pub fn a_m(&self) -> usize {
464        self.m_axis().inputs[0][0]
465    }
466    pub fn a_k(&self) -> usize {
467        self.k_axis().inputs[0][0]
468    }
469    pub fn b_k(&self) -> usize {
470        self.k_axis().inputs[1][0]
471    }
472    pub fn b_n(&self) -> usize {
473        self.n_axis().inputs[1][0]
474    }
475    pub fn c_m(&self) -> Option<usize> {
476        self.m_axis().outputs[0].first().cloned()
477    }
478    pub fn c_n(&self) -> Option<usize> {
479        self.n_axis().outputs[0].first().cloned()
480    }
481
482    fn new(
483        op: EinSum,
484        m_axis: char,
485        k_axis: char,
486        n_axis: char,
487        m: TDim,
488        k: TDim,
489        n: TDim,
490    ) -> Self {
491        Self { op, m_axis, k_axis, n_axis, m, k, n }
492    }
493}
494
495impl Debug for EinSumMatMul {
496    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
497        write!(
498            f,
499            "EinsumMatMul: {} {:?} m: {}={}; k: {}={}; n: {}={}",
500            self.op.axes,
501            self.op.operating_dt,
502            self.m_axis,
503            self.m,
504            self.k_axis,
505            self.k,
506            self.n_axis,
507            self.n
508        )
509    }
510}
511
512impl Deref for EinSumMatMul {
513    type Target = EinSum;
514    fn deref(&self) -> &Self::Target {
515        &self.op
516    }
517}
518
519impl Op for EinSumMatMul {
520    fn name(&self) -> StaticName {
521        "EinSumMatMul".into()
522    }
523
524    op_as_typed_op!();
525}
526
527impl EvalOp for EinSumMatMul {
528    fn is_stateless(&self) -> bool {
529        true
530    }
531    fn eval_with_session(
532        &self,
533        node_id: usize,
534        session: &TurnState,
535        inputs: TVec<TValue>,
536    ) -> TractResult<TVec<TValue>> {
537        self.op.eval_with_session(node_id, session, inputs)
538    }
539}
540
541impl TypedOp for EinSumMatMul {
542    fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
543        self.op.output_facts(inputs)
544    }
545
546    fn codegen(
547        &self,
548        model: &TypedModel,
549        node: &TypedNode,
550    ) -> TractResult<Option<TypedModelPatch>> {
551        // deal with parametric quantization extra inputs
552        if node.inputs.len() == 9 {
553            ensure!(self.op.q_params.is_some());
554            return dequant(model, node, self).map(Some);
555        }
556        ensure!(node.inputs.len() == 2);
557        let (a, b) = model.node_input_facts(node.id)?.into_iter().collect_tuple().unwrap();
558        // at this stage a and b must NOT be packed yet. if they are exotic, we can assume it's just compression
559        let must_transpose = if let Some(of) = a.exotic_fact() {
560            ensure!(of.is::<BlockQuantFact>());
561            false
562        } else if let Some(of) = b.exotic_fact() {
563            ensure!(of.is::<BlockQuantFact>());
564            true
565        } else if self.m == self.n {
566            false
567        } else {
568            match (self.m.as_i64(), self.n.as_i64()) {
569                (Some(m), Some(n)) => m < n,
570                (None, Some(n)) => n >= 8,
571                (Some(_), _) => false,
572                _ => (self.n.clone() - &self.m).prove_positive_or_zero(),
573            }
574        };
575        if must_transpose {
576            let mut op = self.clone();
577            op.op.axes.iter_all_axes_mut().for_each(|axis| axis.inputs.swap(0, 1));
578            std::mem::swap(&mut op.m_axis, &mut op.n_axis);
579            std::mem::swap(&mut op.m, &mut op.n);
580            return TypedModelPatch::replace_single_op(
581                model,
582                node,
583                &[node.inputs[1], node.inputs[0]],
584                op,
585            )
586            .map(|p| Some(p.with_context("transposing")));
587        }
588        // opt mat mul assumes we have at least one m or n
589        if self.c_m().is_some() || self.c_n().is_some() {
590            return optimized_mat_mul(model, node, self)
591                .map(|opt| opt.map(|p| p.with_context("optimizing")));
592        }
593        Ok(None)
594    }
595
596    as_op!();
597}
598
599pub(crate) fn detect_rule(
600    _ctx: &(),
601    model: &TypedModel,
602    node: &TypedNode,
603    _name: &str,
604    op: &EinSum,
605) -> TractResult<Option<TypedModelPatch>> {
606    rule_if!(node.inputs.len() == (2 + op.q_params.is_some() as usize * 7));
607    let input_facts = model.node_input_facts(node.id)?;
608    let input_shapes = op.actual_input_shapes_from_facts(&input_facts)?;
609    let output_shape = super::eval::output_shape(&op.axes, &input_shapes)?;
610    let k_axes: TVec<&Axis> = op
611        .axes
612        .iter_all_axes()
613        // Filter possible candidates (should be one time in each inputs but not in output)
614        .filter(|a| a.inputs[0].len() == 1 && a.inputs[1].len() == 1 && a.outputs[0].is_empty())
615        .collect();
616
617    let non_trivial_k_axis = k_axes
618        .iter()
619        .filter(|a| {
620            !input_shapes[0][a.inputs[0][0]].is_one() || !input_shapes[1][a.inputs[1][0]].is_one()
621        })
622        .copied()
623        .collect::<TVec<_>>();
624
625    let k_axis = if non_trivial_k_axis.len() > 1 {
626        return regroup_k_axes(op, model, node, non_trivial_k_axis);
627    } else {
628        non_trivial_k_axis.first().or_else(|| k_axes.first()).copied()
629    };
630    let Some(k_axis) = k_axis else { return inject_k_axis(op, model, node).map(Some) };
631
632    let mut possible_m_axes: Vec<_> = op
633        .axes
634        .iter_all_axes()
635        .filter(|a| {
636            a.inputs[0].len() == 1
637                && (a.inputs[1].is_empty() || input_shapes[1][a.inputs[1][0]].is_one())
638                && (a.outputs[0].len() == 1
639                    || (input_shapes[0][a.inputs[0][0]].is_one() && a.inputs[1].is_empty()))
640        })
641        .collect();
642
643    // Prioritize obvious m-axes
644    if possible_m_axes.iter().any(|a| !a.outputs[0].is_empty()) {
645        possible_m_axes.retain(|a| !a.outputs[0].is_empty());
646    }
647
648    let m_axis = possible_m_axes
649        .into_iter()
650        .max_by_key(|a| input_shapes[0][a.inputs[0][0]].as_i64().unwrap_or(i64::MAX));
651
652    let Some(m_axis) = m_axis else {
653        return inject_m_or_n_axis(op, model, node, false).map(Some);
654    };
655
656    let n_axis = op
657        .axes
658        .iter_all_axes()
659        .filter(|a| {
660            (a.inputs[0].is_empty() || input_shapes[0][a.inputs[0][0]].is_one())
661                && a.inputs[1].len() == 1
662                && a.outputs[0].len() == 1
663                && *a != m_axis
664        })
665        .max_by_key(|a| input_shapes[1][a.inputs[1][0]].as_i64().unwrap_or(i64::MAX));
666    let Some(n_axis) = n_axis else {
667        return inject_m_or_n_axis(op, model, node, true).map(Some);
668    };
669    for axis in op.axes.iter_all_axes() {
670        let one = TDim::one();
671        let in_left =
672            axis.inputs[0].first().map(|pos| &input_shapes[0][*pos]).unwrap_or(&one) != &one;
673        let in_right =
674            axis.inputs[1].first().map(|pos| &input_shapes[1][*pos]).unwrap_or(&one) != &one;
675        let in_out = axis.outputs[0].first().map(|pos| &output_shape[*pos]).unwrap_or(&one) != &one;
676        if (in_left ^ in_right) && !in_out {
677            return Ok(None);
678            // return Ok(AxesOrPatch::NotAMatMul(
679            //     "non trivial single-side disappearing axis",
680            //     vec![axis],
681            // ));
682        }
683    }
684    let m = input_shapes[0][m_axis.inputs[0][0]].clone();
685    let k = input_shapes[0][k_axis.inputs[0][0]].clone();
686    let n = input_shapes[1][n_axis.inputs[1][0]].clone();
687    TypedModelPatch::replace_single_op(
688        model,
689        node,
690        &node.inputs,
691        EinSumMatMul::new(op.clone(), m_axis.repr, k_axis.repr, n_axis.repr, m, k, n),
692    )
693    .map(Some)
694}
695
696pub(super) fn inject_k_axis(
697    op: &EinSum,
698    model: &TypedModel,
699    node: &TypedNode,
700) -> TractResult<TypedModelPatch> {
701    let mut new_axes = op.axes.clone();
702    let name = &node.name;
703    let mut patch = TypedModelPatch::new("inject k axis");
704    let mut wire = patch.taps(model, &node.inputs)?;
705    let repr = new_axes.available_label();
706    new_axes = new_axes.with_extra_axis(repr, InOut::In(0), 0)?.with_extra_axis_occurency(
707        repr,
708        InOut::In(1),
709        0,
710    )?;
711    wire[0] = patch.wire_node(format!("{name}.add_k.0"), AxisOp::Add(0), &[wire[0]])?[0];
712    wire[1] = patch.wire_node(format!("{name}.add_k.1"), AxisOp::Add(0), &[wire[1]])?[0];
713    wire = patch.wire_node(&node.name, EinSum { axes: new_axes, ..op.clone() }, &wire)?;
714    patch.shunt_outside(model, node.id.into(), wire[0])?;
715    Ok(patch)
716}
717
718pub(super) fn regroup_k_axes(
719    op: &EinSum,
720    model: &TypedModel,
721    node: &TypedNode,
722    mut k_axes: TVec<&Axis>,
723) -> TractResult<Option<TypedModelPatch>> {
724    let input_facts = model.node_input_facts(node.id)?;
725    let input_shapes = op.actual_input_shapes_from_facts(&input_facts)?;
726    let contig_in_a = k_axes
727        .iter()
728        .map(|axis| axis.inputs[0][0])
729        .sorted()
730        .tuple_windows()
731        .all(|(a, b)| a + 1 == b);
732    if contig_in_a {
733        k_axes.sort_by_key(|ax| ax.inputs[0][0]);
734    } else {
735        k_axes.sort_by_key(|ax| ax.inputs[1][0]);
736    }
737    let k_dims: TVec<_> =
738        k_axes.iter().map(|ax| input_shapes[0][ax.inputs[0][0]].clone()).collect();
739    let k: TDim = k_dims.iter().product();
740    let mut patch = TypedModelPatch::default();
741    let mut wires = patch.taps(model, &node.inputs)?;
742    let mut exprs: Vec<String> =
743        (0..2).map(|slot| op.axes.axes(InOut::In(slot)).map(|ax| ax.repr).join("")).collect();
744    for slot in 0..2 {
745        if k_axes.iter().map(|ax| ax.inputs[slot][0]).tuple_windows().any(|(a, b)| a + 1 != b) {
746            let after = op
747                .axes
748                .axes(InOut::In(slot))
749                .filter(|ax| !k_axes.contains(ax))
750                .chain(k_axes.iter().copied())
751                .map(|ax| ax.repr)
752                .join("");
753            let transpose =
754                AxesMapping::from_strs(&[&exprs[slot]], &[&after])?.translate_to_axis_ops()?;
755            for (ix, op) in transpose.into_iter().enumerate() {
756                wires[slot] = patch.wire_node(
757                    format!("{}.transpose_input_{}.{}", node.name, slot, ix),
758                    op,
759                    &[wires[slot]],
760                )?[0];
761            }
762            exprs[slot] = after;
763        }
764        let pos = exprs[slot].chars().position(|c| k_axes[0].repr == c).unwrap();
765        wires[slot] = patch.wire_node(
766            format!("{}.fold_k_in_input_{}", node.name, slot),
767            AxisOp::Reshape(pos, k_dims.clone(), tvec!(k.clone())),
768            &[wires[slot]],
769        )?[0];
770        exprs[slot] =
771            exprs[slot].chars().filter(|c| !k_axes.iter().any(|k| k.repr == *c)).collect();
772        exprs[slot].insert(pos, k_axes[0].repr);
773    }
774    let old = op.axes.to_string();
775    let (iexpr, oexpr) = old.split_once("->").unwrap();
776    let mut expr: String = exprs.iter().join(",");
777    if node.inputs.len() > 2 {
778        expr = expr + "," + &iexpr.split(",").skip(2).join(",");
779    }
780    expr = expr + "->" + oexpr;
781    let wire = patch.wire_node(
782        &node.name,
783        EinSum { axes: expr.parse().unwrap(), ..op.clone() },
784        &wires,
785    )?[0];
786    patch.shunt_outside(model, node.id.into(), wire)?;
787    Ok(Some(patch))
788}
789
790pub(super) fn inject_m_or_n_axis(
791    op: &EinSum,
792    model: &TypedModel,
793    node: &TypedNode,
794    is_n: bool,
795) -> TractResult<TypedModelPatch> {
796    let input_to_fix = is_n as usize;
797    let label = if is_n { "n" } else { "m" };
798    let name = &node.name;
799    let mut patch = TypedModelPatch::new("Injecting m or n axis");
800    let mut wire = patch.taps(model, &node.inputs)?;
801    let repr = op.axes.available_label();
802    let new_axes = op
803        .axes
804        .clone()
805        .with_extra_axis(repr, InOut::In(input_to_fix), 0)?
806        .with_extra_axis_occurency(repr, InOut::Out(0), 0)?;
807    wire[input_to_fix] =
808        patch.wire_node(format!("{name}.add_{label}"), AxisOp::Add(0), &[wire[input_to_fix]])?[0];
809    wire = patch.wire_node(name, EinSum { axes: new_axes, ..op.clone() }, &wire)?;
810    wire = patch.wire_node(&node.name, AxisOp::Rm(0), &wire)?;
811    patch.shunt_outside(model, node.id.into(), wire[0])?;
812    Ok(patch)
813}
814
815fn wire_axes_fix(
816    patch: &mut TypedModelPatch,
817    name: &str,
818    var: &str,
819    mapping: &AxesMapping,
820    mut outlet: TVec<OutletId>,
821) -> TractResult<TVec<OutletId>> {
822    for (ix, axis_op) in mapping.translate_to_axis_ops()?.into_iter().enumerate() {
823        outlet = patch.wire_node(format!("{name}.fix_{var}.{ix})"), axis_op, &outlet)?;
824    }
825    Ok(outlet)
826}
827
828fn dequant(
829    model: &TypedModel,
830    node: &TypedNode,
831    op: &EinSumMatMul,
832) -> TractResult<TypedModelPatch> {
833    let name = &node.name;
834    let mut patch = TypedModelPatch::new("Dequantizing einsum");
835
836    let k_axis = op.k_axis();
837
838    let mut taps = patch.taps(model, &node.inputs)?;
839    for ab in [0, 1] {
840        let scale_input = 4 + ab * 2;
841        if !patch.outlet_fact(taps[scale_input])?.shape.volume().is_one() {
842            let q_axis_in_output = op.axes.axis((InOut::In(scale_input), 0))?.outputs[0][0];
843            let output_rank = node.outputs[0].fact.rank();
844            for i in 1..(output_rank - q_axis_in_output) {
845                taps[scale_input] = patch.wire_node(
846                    format!("{name}.scale_input{ab}_axis_fix_{i}"),
847                    AxisOp::Add(i),
848                    &[taps[scale_input]],
849                )?[0];
850            }
851        }
852    }
853
854    let [mut a, mut b, bias, mut a0, a_scale, mut b0, b_scale, c0, c_scale] = *taps else {
855        bail!("Expect exactly 9 inputs")
856    };
857
858    wire_ensure_q8_flavour(&mut patch, &node.name, &mut a, "a", &mut a0, i8::datum_type())?;
859    wire_ensure_q8_flavour(&mut patch, &node.name, &mut b, "b", &mut b0, i8::datum_type())?;
860
861    let mut output = patch.wire_node(
862        &node.name,
863        EinSum {
864            q_params: None,
865            axes: op.axes.extract_sub_mapping(&[0, 1], &[0])?,
866            operating_dt: op.operating_dt,
867        },
868        &[a, b],
869    )?;
870
871    let a_i32 = patch.wire_node(format!("{name}.a_as_i32"), cast(i32::datum_type()), &[a])?[0];
872    let b_i32 = patch.wire_node(format!("{name}.b_as_i32"), cast(i32::datum_type()), &[b])?[0];
873    let sum_a = patch.wire_node(
874        format!("{name}.sum_a"),
875        Reduce::new(tvec!(k_axis.inputs[0][0]), Reducer::Sum),
876        &[a_i32],
877    )?;
878    let sum_b = patch.wire_node(
879        format!("{name}.sum_b"),
880        Reduce::new(tvec!(k_axis.inputs[1][0]), Reducer::Sum),
881        &[b_i32],
882    )?;
883
884    let sum_a =
885        wire_axes_fix(&mut patch, name, "sum_a", &op.axes.extract_sub_mapping(&[0], &[0])?, sum_a)?;
886    let sum_b =
887        wire_axes_fix(&mut patch, name, "sum_b", &op.axes.extract_sub_mapping(&[1], &[0])?, sum_b)?;
888    let bias = tvec!(bias);
889    let bias =
890        wire_axes_fix(&mut patch, name, "bias", &op.axes.extract_sub_mapping(&[2], &[0])?, bias)?;
891
892    let abc_scale = combine_scales(&mut patch, name, a_scale, b_scale, c_scale)?;
893
894    output = patch.wire_node(format!("{name}.add_bias"), add(), &[output[0], bias[0]])?;
895
896    let k = model.outlet_fact(node.inputs[0])?.shape[k_axis.inputs[0][0]].clone();
897    let output = compensate_zero_points(&mut patch, name, output[0], k, a0, b0, sum_a[0], sum_b[0])
898        .context("Zero point compensation")?;
899    let output = requant(&mut patch, name, output, op.q_params.unwrap(), abc_scale, c0)?;
900    patch.shunt_outside(model, node.id.into(), output)?;
901    Ok(patch)
902}
903
904fn flatten_rule(
905    _ctx: &(),
906    model: &TypedModel,
907    node: &TypedNode,
908    _name: &str,
909    op: &EinSumMatMul,
910) -> TractResult<Option<TypedModelPatch>> {
911    TypedModelPatch::replace_single_op(model, node, &node.inputs, op.op.clone()).map(Some)
912}
913
914fn optimized_mat_mul(
915    model: &TypedModel,
916    node: &TypedNode,
917    op: &EinSumMatMul,
918) -> TractResult<Option<TypedModelPatch>> {
919    let (mode_picker, left_pack, impls) = kernel_selection::strategize(model, node, op)?;
920    let input_facts = model.node_input_facts(node.id)?;
921    let input_shapes = op.actual_input_shapes_from_facts(&input_facts)?;
922    let prefix = &node.name;
923
924    let mut patch = TypedModelPatch::new("Einsum to OptMatMul");
925    let taps = patch.taps(model, &node.inputs)?;
926    let name = &node.name;
927
928    let pack_a: Box<dyn TypedOp> = if input_facts[0].konst.is_some() {
929        if let Some(packed_format) = left_pack.downcast_ref::<PackedBlockQuantFormat>().cloned() {
930            Box::new(OptSimpleMatMulPack {
931                packed_format,
932                k: input_shapes[0][op.a_k()].to_usize().unwrap(),
933                m: input_shapes[0][op.a_m()].to_usize().unwrap(),
934            })
935        } else {
936            // PackedFormat or a custom packer (e.g. PackedI8K4); OptMatMulPack
937            // dispatches on the concrete format at pack time.
938            Box::new(OptMatMulPack {
939                packers: vec![left_pack],
940                mode_picker: ModePicker::Single,
941                k_axis: op.a_k(),
942                mn_axis: op.a_m(),
943            })
944        }
945    } else {
946        Box::new(OptMatMulPack {
947            packers: impls
948                .iter()
949                .map(|(mmm, p, pe)| {
950                    pe.as_ref()
951                        .map(|pe| pe.from.clone())
952                        .unwrap_or_else(|| mmm.packings()[*p].0.clone())
953                })
954                .collect(),
955            mode_picker: mode_picker.clone(),
956            k_axis: op.a_k(),
957            mn_axis: op.a_m(),
958        })
959    };
960    let pa = patch.wire_node(format!("{prefix}.pack_a"), pack_a, &[taps[0]])?[0];
961
962    let pb = patch.wire_node(
963        format!("{prefix}.pack_b"),
964        OptMatMulPack {
965            k_axis: op.b_k(),
966            mn_axis: op.b_n(),
967            packers: impls.iter().map(|(mmm, p, _)| mmm.packings()[*p].1.clone()).collect(),
968            mode_picker: mode_picker.clone(),
969        },
970        &[taps[1]],
971    )?[0];
972
973    let mut c_to_a_axis_mapping = tvec!();
974    let mut c_to_b_axis_mapping = tvec!();
975    for axis in op
976        .op
977        .axes
978        .iter_all_axes()
979        .filter(|&axis| ![op.m_axis, op.k_axis, op.n_axis].contains(&axis.repr))
980    {
981        if let (&[c], &[a]) = (&*axis.outputs[0], &*axis.inputs[0])
982            && input_shapes[0][a] != 1.to_dim()
983        {
984            let a = a - (a > op.a_m()) as usize - (a > op.a_k()) as usize;
985            c_to_a_axis_mapping.push((c, a));
986        }
987        if let (&[c], &[b]) = (&*axis.outputs[0], &*axis.inputs[1])
988            && input_shapes[1][b] != 1.to_dim()
989        {
990            let b = b - (b > op.b_n()) as usize - (b > op.b_k()) as usize;
991            c_to_b_axis_mapping.push((c, b));
992        }
993    }
994
995    let c_fact = op.output_facts(&input_facts)?.remove(0);
996    let geo = AddMatMulGeometry {
997        k: op.k.clone(),
998        c_to_a_axis_mapping: MapOutputAxisToInput(c_to_a_axis_mapping),
999        c_to_b_axis_mapping: MapOutputAxisToInput(c_to_b_axis_mapping),
1000    };
1001    let (mmms, packings, extractor): (Vec<_>, Vec<_>, Vec<_>) = multiunzip(impls);
1002    let outputs = mmms.iter().map(|mmm| unsafe { mmm.c_view(op.c_m(), op.c_n()) }).collect();
1003    let trivial_packing = mmms.len() == 1
1004        && packings[0] == 0
1005        && extractor[0].is_none()
1006        && input_facts[0].exotic_fact.is_none();
1007    let opt = OptMatMul::new(
1008        mmms,
1009        mode_picker,
1010        c_fact,
1011        op.c_m(),
1012        op.c_n(),
1013        vec![
1014            ProtoFusedSpec::AddMatMul {
1015                geo,
1016                a: MatMulOperand::Input(0),
1017                b: MatMulOperand::Input(1),
1018                packings: izip!(packings, extractor).collect_vec(),
1019            },
1020            ProtoFusedSpec::Store(outputs),
1021        ],
1022        trivial_packing,
1023    )
1024    .context("Creating OptMatMul")?;
1025    let output = patch.wire_node(name, opt, &[pa, pb])?[0];
1026    patch.shunt_outside(model, node.id.into(), output)?;
1027    Ok(Some(patch))
1028}