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
20fn 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#[derive(Clone, Debug, PartialEq, Eq)]
42pub enum MatMulOperand {
43 Input(usize),
44 Const(Box<dyn MMMInputValue>),
45}
46
47impl MatMulOperand {
48 #[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()); #[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 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!(), }
296 }
297
298 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 #[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#[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 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 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 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 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 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}