1use std::borrow::Borrow;
2use std::fmt::Debug;
3
4use crate::internal::*;
5use crate::ops::array::MultiBroadcastTo;
6use crate::tract_data::itertools::Itertools;
7
8mod eval;
9
10pub mod einsum_matmul;
11pub mod kernel_selection;
12pub mod prefix_matmul;
13
14#[cfg(test)]
15mod proptest;
16
17use num_traits::One;
18use tract_linalg::block_quant::{BlockQuantFact, PackedBlockQuantFact};
19use tract_linalg::mmm::PackedExoticFact;
20
21pub fn block_quant_aware_input_shape(fact: &TypedFact) -> TractResult<Cow<'_, [TDim]>> {
22 if fact.is_plain() {
23 return Ok(Cow::Borrowed(&*fact.shape));
24 }
25 let Some(exotic_fact) = fact.exotic_fact() else {
26 bail!("Datum fact is exotic, but no exotic fact was found.")
27 };
28 if let Some(_bqf) = exotic_fact.downcast_ref::<BlockQuantFact>() {
29 Ok(Cow::Borrowed(&*fact.shape))
30 } else if let Some(pof) = exotic_fact.downcast_ref::<PackedBlockQuantFact>() {
31 Ok(Cow::Owned(
32 fact.shape.iter().cloned().chain(pof.shape.iter().map(|i| i.to_dim())).collect_vec(),
33 ))
34 } else if let Some(pof) = exotic_fact.downcast_ref::<PackedExoticFact>() {
35 Ok(Cow::Owned(
36 fact.shape.iter().cloned().chain([pof.mn.clone(), pof.k.to_dim()]).collect_vec(),
37 ))
38 } else {
39 bail!("Unsupported exotic fact {exotic_fact:?}")
40 }
41}
42
43#[derive(Clone, Hash, PartialEq, Eq)]
44pub struct EinSum {
45 pub axes: AxesMapping,
46 pub operating_dt: DatumType,
47 pub q_params: Option<DatumType>,
50}
51
52impl EinSum {
53 pub fn new(axes: AxesMapping, operating_dt: DatumType) -> EinSum {
54 EinSum { axes, operating_dt, q_params: None }
55 }
56
57 pub fn newq(axes: AxesMapping, operating_dt: DatumType, output_type: DatumType) -> EinSum {
58 EinSum { axes, operating_dt, q_params: Some(output_type) }
59 }
60
61 pub fn actual_input_shapes_from_facts<'m>(
62 &self,
63 inputs: &'m [impl Borrow<TypedFact>],
64 ) -> TractResult<TVec<Cow<'m, [TDim]>>> {
65 ensure!(inputs.len() == self.axes.input_count());
66 let shapes: TVec<Cow<[TDim]>> = inputs
67 .iter()
68 .map(|t| block_quant_aware_input_shape(t.borrow()))
69 .collect::<TractResult<_>>()?;
70 ensure!(
71 shapes.iter().enumerate().all(|(ix, fact)| fact.len() == self.axes.rank(InOut::In(ix)))
72 );
73 Ok(shapes)
74 }
75
76 #[allow(unused_variables)]
77 pub(crate) fn propagate_axis(
78 &self,
79 model: &TypedModel,
80 node: &TypedNode,
81 io: InOut,
82 axis: usize,
83 ) -> TractResult<Option<TypedModelPatch>> {
84 let mut new_axis = self.axes.axis((io, axis))?.clone();
85 let repr = new_axis.repr;
86 let mut patch = TypedModelPatch::new(format!("Propagate axis {}", new_axis.repr));
87 let mut taps = tvec!();
88 for (ix, input) in node.inputs.iter().enumerate() {
89 let mut tap = patch.tap_model(model, *input)?;
90 rule_if!(new_axis.inputs[ix].len() <= 1); if new_axis.inputs[ix].is_empty() {
92 let insert_at = self.axes.rank(InOut::In(ix));
93 tap = patch.wire_node(
94 format!("{}.prop_axis.{}.input_{}", node.name, new_axis.repr, ix),
95 AxisOp::Add(insert_at),
96 &[tap],
97 )?[0];
98 new_axis.inputs[ix].push(insert_at);
99 }
100 taps.push(tap);
101 }
102 let must_rm_axis: Option<usize> = if new_axis.outputs[0].len() == 0 {
103 let insert_at = self.axes.rank(InOut::Out(0));
104 new_axis.outputs[0].push(insert_at);
105 Some(insert_at)
106 } else {
107 None
108 };
109 let new_expr = self
110 .axes
111 .iter_all_axes()
112 .map(|it| if it.repr == new_axis.repr { new_axis.clone() } else { it.clone() })
113 .collect_vec();
114 let axes = AxesMapping::new(node.inputs.len(), 1, new_expr)?;
115 let mut wire = patch.wire_node(&node.name, Self { axes, ..self.clone() }, &taps)?;
116 if let Some(position) = must_rm_axis {
117 wire = patch.wire_node(
118 format!("{}.prop_axis.{}.output", node.name, repr),
119 AxisOp::Rm(position),
120 &wire,
121 )?;
122 }
123 patch.shunt_outside(model, node.id.into(), wire[0])?;
124 Ok(Some(patch))
125 }
126
127 pub fn acceptable_accumulators(&self) -> TVec<DatumType> {
128 if self.operating_dt.is_integer() {
129 tvec!(i32::datum_type())
130 } else if self.operating_dt == f16::datum_type() {
131 tvec!(f16::datum_type(), f32::datum_type())
132 } else {
133 tvec!(self.operating_dt)
134 }
135 }
136}
137
138impl Debug for EinSum {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 write!(f, "EinSum {} ({:?})", self.axes, self.operating_dt)
141 }
142}
143
144impl Op for EinSum {
145 fn name(&self) -> StaticName {
146 "EinSum".into()
147 }
148
149 fn info(&self) -> TractResult<Vec<String>> {
150 let mut info = vec![format!("{} ({:?})", self.axes, self.operating_dt)];
151 if let Some(qp) = self.q_params {
152 info.push(format!("Quantized output: {qp:?}"));
153 }
154 Ok(info)
155 }
156
157 op_as_typed_op!();
158}
159
160impl EvalOp for EinSum {
161 op_out_of_plan!();
162
163 fn eval(&self, _ctx: &EvalContext, inputs: TVec<TValue>) -> TractResult<TVec<TValue>> {
164 if inputs.iter().all(|i| i.datum_type().is_number() && i.is_plain()) {
165 let mut adhoc_model = TypedModel::default();
166 let mut wires = tvec!();
167 for (ix, input) in inputs.iter().enumerate() {
168 let fact = TypedFact::shape_and_dt_of(input);
169 let wire = adhoc_model.add_source(format!("input.{ix}"), fact)?;
170 wires.push(wire);
171 }
172 let output = adhoc_model.wire_node("einsum", self.clone(), &wires)?;
173 adhoc_model.select_output_outlets(&output)?;
174 let opti = adhoc_model.into_optimized()?;
175 if opti.nodes.iter().all(|node| !node.op_is::<Self>()) {
176 return opti.into_runnable()?.run(inputs);
177 }
178 }
179
180 let output = if let Some(qp) = self.q_params {
181 eval::eval_q(&self.axes, qp, inputs)
182 } else {
183 dispatch_numbers!(eval::eval_t(self.operating_dt)(&self.axes, inputs))
184 }?;
185 Ok(tvec!(output.into_tvalue()))
186 }
187}
188
189impl TypedOp for EinSum {
190 fn output_facts(&self, inputs: &[&TypedFact]) -> TractResult<TVec<TypedFact>> {
191 let shapes = self.actual_input_shapes_from_facts(inputs)?;
192 for i in 0..inputs.len() {
193 ensure!(shapes[i].len() == self.axes.rank(InOut::In(i)));
194 }
195 for axis in self.axes.iter_all_axes() {
196 assert!(
197 shapes
198 .iter()
199 .enumerate()
200 .flat_map(|(slot, shape)| axis.inputs[slot].iter().map(|a| &shape[*a]))
201 .try_fold(TDim::one(), |a, b| TDim::broadcast(a, b.clone()))
202 .is_ok()
203 );
204 }
205 if let Some(qp) = self.q_params {
206 ensure!(inputs.len() == 9);
207 Ok(tvec!(qp.fact(eval::output_shape(&self.axes, &shapes[0..2])?)))
208 } else {
209 Ok(tvec!(TypedFact::dt_shape(
210 self.operating_dt,
211 eval::output_shape(&self.axes, &shapes)?
212 )))
213 }
214 }
215
216 fn input_roi(
217 &self,
218 model: &TypedModel,
219 node: &TypedNode,
220 ) -> TractResult<Option<TVec<Option<TDim>>>> {
221 let output_fact = model.outlet_fact(OutletId::new(node.id, 0))?;
228 let Some(roi) = &output_fact.region_of_interest else { return Ok(None) };
229 let input_facts: TVec<&TypedFact> =
230 node.inputs.iter().map(|i| model.outlet_fact(*i)).collect::<TractResult<_>>()?;
231 let output_facts = tvec![output_fact];
232 let inputs_ref: Vec<&TypedFact> = input_facts.iter().copied().collect();
233 let outputs_ref: Vec<&TypedFact> = output_facts.iter().copied().collect();
234 let mapping = self.axes_mapping(&inputs_ref, &outputs_ref)?;
235 let roi_coord_axes: Vec<(usize, Symbol)> = roi
236 .symbols()
237 .into_iter()
238 .filter_map(|s| crate::ops::logic::sym_to_coord_axis(&s).map(|k| (k, s)))
239 .collect();
240
241 let project_for_input = |input_ix: usize| -> Option<TDim> {
242 let mut projected: Vec<Symbol> = vec![];
245 let mut preserved: Vec<(Symbol, usize)> = vec![];
246 for (out_pos, sym) in &roi_coord_axes {
247 let logical = mapping
248 .iter_all_axes()
249 .find(|a| a.outputs.first().is_some_and(|o| o.contains(out_pos)))?;
250 match logical.inputs[input_ix].first() {
251 None => projected.push(sym.clone()),
252 Some(&in_pos) => {
253 if input_facts[input_ix].shape[in_pos] != output_fact.shape[*out_pos] {
254 return None;
255 }
256 preserved.push((sym.clone(), in_pos));
257 }
258 }
259 }
260 if projected.is_empty() {
261 let mut sub_map: HashMap<Symbol, TDim> = HashMap::new();
263 for (sym, in_pos) in &preserved {
264 if crate::ops::logic::sym_to_coord_axis(sym) != Some(*in_pos) {
265 let scope = sym.scope()?;
266 sub_map.insert(sym.clone(), TDim::Sym(scope.coord_sym(*in_pos)));
267 }
268 }
269 return if sub_map.is_empty() {
270 Some(roi.clone())
271 } else {
272 roi.substitute_all(&sub_map).ok()
273 };
274 }
275 for p_sym in &projected {
278 for (k_sym, k_in_pos) in &preserved {
279 if let Some(band) = crate::optim::propagate_roi::recognise_chunked_band_project(
280 roi, p_sym, k_sym,
281 ) {
282 if crate::ops::logic::sym_to_coord_axis(k_sym) != Some(*k_in_pos) {
285 let scope = k_sym.scope()?;
286 let mut m: HashMap<Symbol, TDim> = HashMap::new();
287 m.insert(k_sym.clone(), TDim::Sym(scope.coord_sym(*k_in_pos)));
288 return band.substitute_all(&m).ok();
289 }
290 return Some(band);
291 }
292 }
293 }
294 None
295 };
296 let result: TVec<Option<TDim>> = (0..node.inputs.len()).map(project_for_input).collect();
297 Ok(Some(result))
298 }
299
300 fn axes_mapping(
301 &self,
302 inputs: &[&TypedFact],
303 _outputs: &[&TypedFact],
304 ) -> TractResult<AxesMapping> {
305 let mut axes = self.axes.clone();
306 for (slot, i) in inputs.iter().enumerate() {
307 if i.is_exotic()
308 && (i.exotic_fact().is_some_and(|of| {
309 of.is::<PackedExoticFact>() || of.is::<PackedBlockQuantFact>()
310 }))
311 {
312 axes = axes
313 .remove_axis_occurency(InOut::In(slot), i.rank())?
314 .remove_axis_occurency(InOut::In(slot), i.rank())?;
315 }
316 }
317 Ok(axes)
318 }
319
320 fn cost(&self, inputs: &[&TypedFact]) -> TractResult<TVec<(Cost, TDim)>> {
321 let shapes = self.actual_input_shapes_from_facts(inputs)?;
322 let oshape = eval::output_shape(&self.axes, &shapes)?;
323 let ks = self
324 .axes
325 .iter_all_axes()
326 .filter(|axis| axis.outputs[0].len() == 0)
327 .map(|axis| {
328 axis.inputs
329 .iter()
330 .enumerate()
331 .flat_map(|(ix, axes)| {
332 axes.iter()
333 .map(|axis| shapes[ix][*axis].clone())
334 .collect::<TVec<_>>()
335 .into_iter()
336 })
337 .find(|d| !d.is_one())
338 .unwrap_or_else(|| 1.to_dim())
339 })
340 .product::<TDim>();
341 Ok(tvec!((Cost::FMA(self.operating_dt), oshape.iter().product::<TDim>() * ks)))
342 }
343
344 fn slice(
345 &self,
346 patch: &mut TypedModelPatch,
347 model: &TypedModel,
348 node: &TypedNode,
349 prefix: &str,
350 inputs: &[OutletId],
351 output_axis: usize,
352 _start: &TDim,
353 _end: &TDim,
354 ) -> TractResult<Option<TVec<OutletId>>> {
355 let facts = model.node_input_facts(node.id)?;
356 let axis = self.axes.axis((InOut::Out(0), output_axis))?;
357 if facts
358 .iter()
359 .enumerate()
360 .any(|(slot, fact)| axis.inputs[slot].len() > 0 && fact.is_exotic())
361 {
362 Ok(None)
363 } else {
364 patch.wire_node(prefix, self.clone(), inputs).map(Some)
365 }
366 }
367
368 #[allow(unused_variables)]
369 fn change_axes(
370 &self,
371 model: &TypedModel,
372 node: &TypedNode,
373 io: InOut,
374 change: &AxisOp,
375 ) -> TractResult<Option<AxisChangeConsequence>> {
376 let (mut inputs, mut outputs) = self.axes.to_strs();
377 let interface: &mut String = match io {
378 InOut::In(i) => &mut inputs[i],
379 InOut::Out(o) => &mut outputs[o],
380 };
381 let mut axes: Vec<char> = interface.chars().collect();
382 match change {
383 AxisOp::Rm(rm) => {
384 axes.remove(*rm);
385 }
386 AxisOp::Add(add) => axes.insert(*add, self.axes.available_label()),
387 AxisOp::Move(from, to) => {
388 let c = axes.remove(*from);
389 axes.insert(*to, c);
390 }
391 _ => {
392 return Ok(None);
393 }
394 };
395 *interface = axes.into_iter().collect();
396 let axes = AxesMapping::from_strs(&inputs, &outputs)?;
397 Ok(Some(AxisChangeConsequence {
398 substitute_op: Some(Box::new(EinSum { axes, ..self.clone() })),
399 wire_changes: tvec!((io, change.clone())),
400 }))
401 }
402
403 fn declutter_with_session(
404 &self,
405 session: &mut crate::optim::OptimizerSession,
406 model: &TypedModel,
407 node: &TypedNode,
408 ) -> TractResult<Option<TypedModelPatch>> {
409 if let Some(patch) = declutter_reshape_folding_input_axis(self, session, model, node)? {
410 return Ok(Some(patch));
411 }
412 if let Some(patch) = declutter_broadcast(self, session, model, node)? {
413 return Ok(Some(patch));
414 }
415 if let Some(patch) = unit_k_to_broadcast_mul(self, model, node)? {
416 return Ok(Some(patch));
417 }
418 Ok(None)
419 }
420
421 fn codegen(
422 &self,
423 model: &TypedModel,
424 node: &TypedNode,
425 ) -> TractResult<Option<TypedModelPatch>> {
426 rule_if!(
427 (self.q_params.is_none() && node.inputs.len() == 2)
428 || (self.q_params.is_some() && node.inputs.len() == 9)
429 );
430 if let Some(patch) = unit_k_to_broadcast_mul(self, model, node)? {
436 return Ok(Some(patch));
437 }
438 einsum_matmul::detect_rule(&(), model, node, &node.name, self)
439 }
440
441 as_op!();
442}
443
444fn declutter_reshape_folding_input_axis(
445 op: &EinSum,
446 _session: &mut crate::optim::OptimizerSession,
447 model: &TypedModel,
448 node: &TypedNode,
449) -> TractResult<Option<TypedModelPatch>> {
450 for (slot, prec) in node.inputs.iter().map(|n| model.node(n.node)).enumerate() {
451 let Some(&AxisOp::Reshape(at, ref from, ref to)) = prec.op_as() else { continue };
452 if to.len() > 1 {
453 continue;
454 }
455 let mut axes = op.axes.clone();
456 let extra_labels = axes.available_labels().take(from.len() - 1).collect_vec();
457 let extra_input = node.inputs.len();
459 axes = axes.with_extra_input(extra_input)?;
460 for label in &extra_labels {
461 axes = axes.with_extra_axis(*label, InOut::In(extra_input), 0)?;
462 }
463 let folded_axis = op.axes.axis((InOut::In(slot), at))?;
464 rule_if!(folded_axis.outputs[0].len() <= 1);
465 let mut patch = TypedModelPatch::default();
466 let mut taps = patch.taps(model, &node.inputs)?;
467 for (input, tap) in taps.iter_mut().enumerate() {
468 if folded_axis.inputs[input].len() == 0 {
469 continue;
470 };
471 rule_if!(folded_axis.inputs[input].len() <= 1);
472 let pos = folded_axis.inputs[input][0];
473 for label in &extra_labels {
474 axes = axes.with_extra_axis_occurency(*label, InOut::In(input), pos)?;
475 }
476 *tap = patch.wire_node(
477 format!("{}.reshape_folded_input_{}", node.name, input),
478 AxisOp::Reshape(pos, to.clone(), from.clone()),
479 &[*tap],
480 )?[0];
481 }
482 if folded_axis.outputs[0].len() == 1 {
483 let pos = folded_axis.outputs[0][0];
484 for label in &extra_labels {
485 axes = axes.with_extra_axis_occurency(*label, InOut::Out(0), pos)?;
486 }
487 }
488 axes = axes.remove_slot(InOut::In(extra_input))?;
489 let mut wire = patch.wire_node(&node.name, EinSum { axes, ..op.clone() }, &taps)?;
490 if folded_axis.outputs[0].len() == 1 {
491 let pos = folded_axis.outputs[0][0];
492 wire = patch.wire_node(
493 format!("{}.reshape_folded_output", node.name),
494 AxisOp::Reshape(pos, from.clone(), to.clone()),
495 &wire,
496 )?;
497 }
498 patch.shunt_outside(model, node.id.into(), wire[0])?;
499 return Ok(Some(patch));
500 }
501 Ok(None)
502}
503
504fn declutter_broadcast(
505 op: &EinSum,
506 _session: &mut crate::optim::OptimizerSession,
507 model: &TypedModel,
508 node: &TypedNode,
509) -> TractResult<Option<TypedModelPatch>> {
510 for (ix, outlet) in node.inputs.iter().enumerate() {
511 let prec = model.node(outlet.node);
512 if prec.op_is::<MultiBroadcastTo>() && prec.outputs[0].successors.len() == 1 {
513 let mut patch = TypedModelPatch::default();
514 let mut wires = patch.taps(model, &node.inputs)?;
515 wires[ix] = patch.tap_model(model, prec.inputs[0])?;
516 let wire = patch.wire_node(&node.name, op.clone(), &wires)?[0];
517 patch.shunt_outside(model, node.id.into(), wire)?;
518 return Ok(Some(patch));
519 }
520 }
521 Ok(None)
522}
523
524fn unit_k_to_broadcast_mul(
539 op: &EinSum,
540 model: &TypedModel,
541 node: &TypedNode,
542) -> TractResult<Option<TypedModelPatch>> {
543 if op.q_params.is_some() || node.inputs.len() != 2 {
544 return Ok(None);
545 }
546 let input_facts = model.node_input_facts(node.id)?;
547 let input_shapes = op.actual_input_shapes_from_facts(&input_facts)?;
548 let k_axes: TVec<&Axis> = op
549 .axes
550 .iter_all_axes()
551 .filter(|a| a.inputs[0].len() == 1 && a.inputs[1].len() == 1 && a.outputs[0].is_empty())
552 .collect();
553 let any_nontrivial_k = k_axes.iter().any(|a| {
555 !input_shapes[0][a.inputs[0][0]].is_one() || !input_shapes[1][a.inputs[1][0]].is_one()
556 });
557 if any_nontrivial_k {
558 return Ok(None);
559 }
560 let has_deconv_sum_consumer = node.outputs.first().is_some_and(|o| {
568 o.successors.iter().any(|inlet| model.node(inlet.node).op.name() == "DeconvSum")
569 });
570 if !has_deconv_sum_consumer {
571 return Ok(None);
572 }
573
574 let one = TDim::one();
575 for axis in op.axes.iter_all_axes() {
577 let in_left =
578 axis.inputs[0].first().map(|pos| &input_shapes[0][*pos]).unwrap_or(&one) != &one;
579 let in_right =
580 axis.inputs[1].first().map(|pos| &input_shapes[1][*pos]).unwrap_or(&one) != &one;
581 let in_out = !axis.outputs[0].is_empty();
582 if (in_left ^ in_right) && !in_out {
583 return Ok(None);
584 }
585 }
586
587 let c_axes: Vec<char> = op.axes.axes(InOut::Out(0)).map(|a| a.repr).collect();
588 if c_axes.is_empty() {
589 return Ok(None);
590 }
591
592 let k_reprs: TVec<char> = k_axes.iter().map(|a| a.repr).collect();
593 let mut patch = TypedModelPatch::new("EinSum unit-K → broadcast Mul");
594 let mut wires: TVec<OutletId> = patch.taps(model, &node.inputs)?;
595 let name = &node.name;
596
597 for (slot, wire) in wires.iter_mut().enumerate() {
598 let cur_dt = patch.outlet_fact(*wire)?.datum_type;
601 if cur_dt != op.operating_dt {
602 *wire = patch.wire_node(
603 format!("{name}.cast_in{slot}"),
604 crate::ops::cast::cast(op.operating_dt),
605 &[*wire],
606 )?[0];
607 }
608
609 let mut k_positions: Vec<usize> = k_axes.iter().map(|a| a.inputs[slot][0]).collect();
611 k_positions.sort_by(|a, b| b.cmp(a));
612 for (i, pos) in k_positions.into_iter().enumerate() {
613 *wire =
614 patch.wire_node(format!("{name}.rm_k_in{slot}.{i}"), AxisOp::Rm(pos), &[*wire])?[0];
615 }
616
617 let mut current: Vec<char> = op
618 .axes
619 .axes(InOut::In(slot))
620 .map(|a| a.repr)
621 .filter(|c| !k_reprs.contains(c))
622 .collect();
623
624 let mut to_drop: Vec<(usize, char)> = current
626 .iter()
627 .enumerate()
628 .filter(|(_, c)| !c_axes.contains(c))
629 .map(|(i, c)| (i, *c))
630 .collect();
631 to_drop.sort_by_key(|a| std::cmp::Reverse(a.0));
632 for (pos, c) in to_drop {
633 *wire = patch.wire_node(
634 format!("{name}.rm_extra_in{slot}_{c}"),
635 AxisOp::Rm(pos),
636 &[*wire],
637 )?[0];
638 current.remove(pos);
639 }
640
641 for (target_pos, &t) in c_axes.iter().enumerate() {
643 if !current.contains(&t) {
644 *wire = patch.wire_node(
645 format!("{name}.add_in{slot}_{t}"),
646 AxisOp::Add(target_pos),
647 &[*wire],
648 )?[0];
649 current.insert(target_pos, t);
650 }
651 }
652
653 for (target_pos, &t) in c_axes.iter().enumerate() {
655 let cur_pos = current.iter().position(|&c| c == t).unwrap();
656 if cur_pos != target_pos {
657 *wire = patch.wire_node(
658 format!("{name}.move_in{slot}_{t}"),
659 AxisOp::Move(cur_pos, target_pos),
660 &[*wire],
661 )?[0];
662 let removed = current.remove(cur_pos);
663 current.insert(target_pos, removed);
664 }
665 }
666 }
667
668 let result = patch.wire_node(name, crate::ops::math::mul(), &wires)?;
669 patch.shunt_outside(model, node.id.into(), result[0])?;
670 Ok(Some(patch))
671}