#![allow(clippy::type_complexity)]
use dyn_clone::clone_box;
use tract_itertools::Itertools;
use tract_linalg::WeightType;
use tract_linalg::block_quant::BlockQuantFact;
use tract_linalg::mmm::{MMMInputFormat, Query, Suitable, pick_by_shape, retain_best};
use crate::internal::*;
use crate::ops::matmul::ModePicker;
use super::einsum_matmul::EinSumMatMul;
pub type Impl = Suitable;
pub type Strat = (ModePicker, Box<dyn MMMInputFormat>, Vec<Impl>);
fn single_strat(it: Impl) -> Strat {
(ModePicker::Single, it.0.packings()[it.1].0.clone(), vec![it])
}
fn n_axis_contiguous(model: &TypedModel, node: &TypedNode, op: &EinSumMatMul) -> bool {
let Some(cn) = op.c_n() else {
return true;
};
let Ok(fact) = model.outlet_fact(node.id.into()) else {
return true;
};
fact.shape.iter().skip(cn + 1).all(|d| d.is_one())
}
fn tile_store_needs_n_contiguous(name: &str, nr: usize) -> bool {
nr > 1 && (name.starts_with("sme_mmm") || name.starts_with("sme_qmmm"))
}
fn amx_scatter_dominates(name: &str, nr: usize, query: &Query) -> bool {
let (Some(m), Some(k), Some(n)) = (query.m, query.k, query.n) else {
return false;
};
nr > 1 && name.starts_with("apple_amx") && (k <= 96 || (k <= 256 && m * n >= 1 << 19))
}
fn m_axis_contiguous(model: &TypedModel, node: &TypedNode, op: &EinSumMatMul) -> bool {
let Some(cm) = op.c_m() else {
return true;
};
let Ok(fact) = model.outlet_fact(node.id.into()) else {
return true;
};
fact.shape.iter().skip(cm + 1).all(|d| d.is_one())
}
pub fn strategize(model: &TypedModel, node: &TypedNode, op: &EinSumMatMul) -> TractResult<Strat> {
let query = query(model, node, op)?;
let mut suitable = tract_linalg::MmmDispatch::native().suitable(&query);
ensure!(suitable.len() > 0);
if query.n.is_some()
&& let Some(chosen) = tract_linalg::MmmDispatch::native().preferred(&query, &suitable)
{
let n_ok = n_axis_contiguous(model, node, op);
let m_ok = m_axis_contiguous(model, node, op);
let vertical_sme = |name: &str| m_ok && name == "sme_mmm_f32_32x32";
let scatters = |name: &str, nr: usize| {
(tile_store_needs_n_contiguous(name, nr) && !vertical_sme(name))
|| amx_scatter_dominates(name, nr, &query)
};
if !n_ok && scatters(chosen.0.name(), chosen.0.nr()) {
suitable.retain(|(k, _, _)| !scatters(k.name(), k.nr()));
if let Some(fallback) = tract_linalg::MmmDispatch::native().preferred(&query, &suitable)
{
return Ok(single_strat(fallback));
}
} else {
return Ok(single_strat(chosen));
}
}
retain_best(&mut suitable);
if suitable.len() == 1 {
return Ok(single_strat(suitable.remove(0)));
}
if let Some(ix) = pick_by_shape(&query, &suitable) {
return Ok(single_strat(suitable.swap_remove(ix)));
}
let mut grouped_by_left_packing = Vec::<(&dyn MMMInputFormat, Vec<_>)>::new();
'mmm: for (m, p, pe) in &suitable {
let left_packing: &dyn MMMInputFormat =
pe.as_ref().map(|pe| &*pe.from).unwrap_or(&*m.packings()[*p].0);
for group in &mut grouped_by_left_packing {
if let Some(merged) = group.0.merge_with(left_packing) {
group.0 = merged;
group.1.push((m, p, pe));
continue 'mmm;
}
}
grouped_by_left_packing.push((left_packing, vec![(m, p, pe)]));
}
let (p, mmv, mmm) = grouped_by_left_packing
.iter()
.map(|(p, group)| {
let best_for_mmv =
group.iter().max_by_key(|(m, _, pe)| (m.nr() == 1, pe.is_none())).unwrap();
let best_for_mmm = group.iter().max_by_key(|(m, _, _)| m.nr()).unwrap();
(p, best_for_mmv, best_for_mmm)
})
.max_by_key(|(_, mmv, mmm)| {
(
mmv.0.nr() == 1 && mmm.0.nr() > 1,
mmv.2.is_none(),
mmm.0.nr() > 1,
mmm.0.mr(),
mmm.0.nr(),
)
})
.unwrap();
if mmm == mmv {
Ok((ModePicker::Single, clone_box(*p), vec![(mmv.0.clone(), *mmv.1, mmv.2.clone())]))
} else {
Ok((
ModePicker::VecVsMat,
clone_box(*p),
vec![(mmv.0.clone(), *mmv.1, mmv.2.clone()), (mmm.0.clone(), *mmm.1, mmm.2.clone())],
))
}
}
pub fn query(model: &TypedModel, node: &TypedNode, op: &EinSumMatMul) -> TractResult<Query> {
let (a_fact, b_fact) = model.node_input_facts(node.id)?.into_iter().collect_tuple().unwrap();
let a_dt = a_fact.datum_type;
let b_dt = b_fact.datum_type;
let a_weight: WeightType = if let Some(of) = a_fact.exotic_fact() {
if let Some(bqf) = of.downcast_ref::<BlockQuantFact>() {
WeightType::BlockQuant(bqf.format.clone())
} else {
bail!("Can not translate to matmul operand {a_fact:?}");
}
} else {
a_dt.into()
};
Ok(Query {
weight: a_weight,
activation: b_dt,
accumulators: op.acceptable_accumulators(),
store: Some(op.operating_dt.unquantized()),
allow_extractor: true,
m: op.m.as_i64().map(|d| d as usize),
k: op.k.as_i64().map(|d| d as usize),
n: op.n.as_i64().map(|d| d as usize),
})
}
#[cfg(test)]
mod sme_strided_store {
use super::*;
use crate::ops::einsum::EinSum;
use crate::ops::matmul::optimized::OptMatMul;
fn optimize_mn(m: usize, k: usize, n: usize) -> TypedModel {
let mut model = TypedModel::default();
let a = model.add_source("a", f32::fact([m, k])).unwrap();
let b = model.add_source("b", f32::fact([k, n])).unwrap();
let out = model
.wire_node(
"gemm",
EinSum::new("mk,kn->mn".parse().unwrap(), f32::datum_type()),
&[a, b],
)
.unwrap();
model.select_output_outlets(&out).unwrap();
model.optimize().unwrap();
model
}
fn has_tile_kernel() -> bool {
tract_linalg::MmmDispatch::native()
.runnable()
.iter()
.any(|k| tile_store_needs_n_contiguous(k.name(), k.nr()))
}
#[test]
fn strided_n_does_not_select_scattering_tile() {
if !has_tile_kernel() {
return;
}
let model = optimize_mn(64, 64, 160);
let mm = model.nodes.iter().find_map(|n| n.op_as::<OptMatMul>()).expect("OptMatMul");
let name = mm.mmm[0].name();
let sme_f32 = tract_linalg::MmmDispatch::native()
.runnable()
.iter()
.any(|k| k.name() == "sme_mmm_f32_32x32");
if sme_f32 {
assert_eq!(name, "sme_mmm_f32_32x32", "m-contiguous GEMM selected {name}");
} else {
assert!(
!tile_store_needs_n_contiguous(name, mm.mmm[0].nr()),
"strided-n GEMM selected {name}"
);
}
}
#[test]
fn contiguous_n_selects_wide_tile() {
if !has_tile_kernel() {
return;
}
let model = optimize_mn(160, 64, 64);
let mm = model.nodes.iter().find_map(|n| n.op_as::<OptMatMul>()).expect("OptMatMul");
assert!(
tile_store_needs_n_contiguous(mm.mmm[0].name(), mm.mmm[0].nr()),
"contiguous-n GEMM selected {}",
mm.mmm[0].name()
);
}
fn has_amx_tile() -> bool {
tract_linalg::MmmDispatch::native()
.runnable()
.iter()
.any(|k| k.nr() > 1 && k.name().starts_with("apple_amx"))
}
#[test]
fn short_k_strided_n_avoids_amx() {
if !has_amx_tile() {
return;
}
let model = optimize_mn(64, 64, 160);
let mm = model.nodes.iter().find_map(|n| n.op_as::<OptMatMul>()).expect("OptMatMul");
let name = mm.mmm[0].name();
assert!(!name.starts_with("apple_amx"), "short-K strided-n GEMM selected {name}");
}
#[test]
fn long_k_strided_n_keeps_amx() {
if !has_amx_tile() {
return;
}
let model = optimize_mn(64, 512, 160);
let mm = model.nodes.iter().find_map(|n| n.op_as::<OptMatMul>()).expect("OptMatMul");
let name = mm.mmm[0].name();
assert!(name.starts_with("apple_amx"), "long-K strided-n GEMM selected {name}");
}
}