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