use tract_data::dyn_eq::DynEq;
use tract_data::itertools::Itertools;
use tract_data::prelude::{DatumType, TVec, tvec};
use super::{MMMInputFormat, MatMatMul, PanelExtractor};
use crate::WeightType;
pub type Suitable = (Box<dyn MatMatMul>, usize, Option<PanelExtractor>);
#[derive(Clone)]
pub struct Query {
pub weight: WeightType,
pub activation: DatumType,
pub accumulators: TVec<DatumType>,
pub store: Option<DatumType>,
pub allow_extractor: bool,
pub m: Option<usize>,
pub k: Option<usize>,
pub n: Option<usize>,
}
impl Query {
pub fn plain(
accumulator: DatumType,
m: Option<usize>,
k: Option<usize>,
n: Option<usize>,
) -> Query {
let operand = if accumulator == DatumType::I32 { DatumType::I8 } else { accumulator };
Query {
weight: operand.into(),
activation: operand,
accumulators: tvec!(accumulator),
store: None,
allow_extractor: true,
m,
k,
n,
}
}
}
pub fn suitable_named(suitable: &[Suitable], name: &str) -> Option<usize> {
suitable.iter().position(|(mmm, _, _)| mmm.name() == name)
}
pub fn retain_best(suitable: &mut Vec<Suitable>) {
fn key(mmm: &dyn MatMatMul) -> (bool, isize) {
(mmm.arch().is_some(), mmm.preference())
}
if let Some(best) = suitable.iter().map(|(mmm, _, _)| key(&**mmm)).max() {
suitable.retain(|(mmm, _, _)| key(&**mmm) == best);
}
}
pub fn pick_by_shape(query: &Query, suitable: &[Suitable]) -> Option<usize> {
match query.n {
Some(1) => suitable
.iter()
.enumerate()
.max_by_key(|(_, (mmm, _, pe))| (mmm.nr() == 1, pe.is_none(), mmm.mr()))
.map(|(ix, _)| ix),
Some(n) if n > 1 => suitable
.iter()
.enumerate()
.max_by_key(|(_, (mmm, _, pe))| (pe.is_none(), mmm.nr() > 1, mmm.nr() * mmm.mr()))
.map(|(ix, _)| ix),
_ => None,
}
}
pub struct MmmDispatch {
isa: crate::isa::IsaSet,
tiers: Vec<&'static crate::mmm_tiers::MmmTier>,
runnable: Vec<Box<dyn MatMatMul>>,
panel_extractors: Vec<PanelExtractor>,
}
impl MmmDispatch {
pub fn for_isa(isa: crate::isa::IsaSet) -> MmmDispatch {
let mut dispatch = MmmDispatch {
isa,
tiers: crate::mmm_tiers::for_isa(&isa),
runnable: vec![],
panel_extractors: crate::mmm_routines::extractors_for(&isa),
};
dispatch.runnable = crate::mmm_routines::runnable_for(&isa);
dispatch
}
pub fn native() -> &'static MmmDispatch {
lazy_static::lazy_static! {
static ref NATIVE: MmmDispatch = MmmDispatch::for_isa(crate::isa::native());
}
&NATIVE
}
pub fn runnable(&self) -> &[Box<dyn MatMatMul>] {
&self.runnable
}
pub fn all_possible_packing(
&self,
weight_type: impl Into<crate::WeightType>,
) -> impl Iterator<Item = &dyn MMMInputFormat> {
let weight_type = weight_type.into();
self.runnable
.iter()
.flat_map(|m| m.packings())
.map(|p| &*p.0)
.flat_map(move |p| {
let mut packs: Vec<&dyn MMMInputFormat> = vec![];
if p.precursor() == weight_type {
packs.push(p)
};
for pe in &self.panel_extractors {
if pe.from.precursor() == weight_type && pe.to.dyn_eq(p) {
packs.push(&*pe.from);
}
}
packs.into_iter()
})
.sorted_by_key(|p| p.to_string())
.dedup()
}
pub fn suitable(&self, query: &Query) -> Vec<Suitable> {
self.runnable
.iter()
.filter(|mmm| {
query.accumulators.contains(&mmm.internal_type())
&& query.store.is_none_or(|s| mmm.stores().contains(&s))
})
.flat_map(|mmm| mmm.packings().iter().enumerate().map(move |(ix, p)| (mmm, ix, p)))
.filter(|(_, _, (_, b))| {
b.precursor().as_dt().is_some_and(|dt| dt == query.activation.unquantized())
})
.filter_map(|(mmm, ix, (a, _))| {
if a.precursor() == query.weight {
Some((mmm.clone(), ix, None))
} else if query.allow_extractor {
self.panel_extractors
.iter()
.find(|pe| pe.from.precursor() == query.weight && pe.to.dyn_eq(&**a))
.map(|pe| (mmm.clone(), ix, Some(pe.clone())))
} else {
None
}
})
.collect()
}
pub fn tiers(&self) -> &[&'static crate::mmm_tiers::MmmTier] {
&self.tiers
}
pub fn preferred(&self, query: &Query, suitable: &[Suitable]) -> Option<Suitable> {
let crate::WeightType::Plain(weight) = &query.weight else { return None };
if weight.unquantized() != query.activation.unquantized() {
return None;
}
let acc = *query.accumulators.first()?;
let ix = crate::mmm_tiers::preferred(&self.isa, &self.tiers, acc, query, suitable)?;
let chosen = &suitable[ix];
chosen.0.arch().is_some().then(|| chosen.clone())
}
pub fn pick(&self, query: &Query) -> Option<Suitable> {
let mut suitable = self.suitable(query);
if let Some(chosen) = self.preferred(query, &suitable) {
return Some(chosen);
}
retain_best(&mut suitable);
if suitable.len() == 1 {
return Some(suitable.remove(0));
}
if let Some(ix) = pick_by_shape(query, &suitable) {
return Some(suitable.swap_remove(ix));
}
let ix = suitable
.iter()
.enumerate()
.max_by_key(|(_, (mmm, _, pe))| (pe.is_none(), mmm.nr() > 1, mmm.nr() * mmm.mr()))
.map(|(ix, _)| ix)?;
Some(suitable.swap_remove(ix))
}
pub fn panel_extractors(&self) -> &[PanelExtractor] {
&self.panel_extractors
}
pub fn preferred_kernel(
&self,
accumulator: DatumType,
m: Option<usize>,
k: Option<usize>,
n: Option<usize>,
) -> Option<Box<dyn MatMatMul>> {
let query = Query::plain(accumulator, m, k, n);
let suitable = self.suitable(&query);
let ix =
crate::mmm_tiers::preferred(&self.isa, &self.tiers, accumulator, &query, &suitable)?;
Some(suitable[ix].0.clone())
}
}
impl crate::isa::Arch {
pub fn inspect(self) -> Option<MmmDispatch> {
if !crate::mmm_routines::declared().any(|r| (r.make)().arch() == Some(self)) {
return None;
}
let isa = if self.is_native() {
crate::isa::native()
} else {
crate::isa::forced(crate::isa::IsaSet::of_arch(self))
};
Some(MmmDispatch::for_isa(isa))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mmm::MMMInputFormat;
use tract_data::prelude::{Datum, f16};
fn accumulators() -> Vec<DatumType> {
vec![f32::datum_type(), f16::datum_type(), i32::datum_type()]
}
#[test]
fn no_machine_is_offered_a_kernel_it_cannot_run() {
for isa in crate::isa::IsaSet::every_ladder() {
for mmm in crate::MmmDispatch::for_isa(isa).runnable() {
assert!(
mmm.runnable_on(&isa),
"{} needs {:?}, offered to {isa:?}",
mmm.name(),
mmm.isa()
);
}
}
}
#[test]
fn the_host_set_holds_nothing_unbuilt() {
for mmm in crate::MmmDispatch::native().runnable() {
assert!(mmm.built(), "{} is in the host's set unbuilt", mmm.name());
}
}
#[test]
fn the_preference_keeps_a_kept_group_usable() {
let dispatch = crate::MmmDispatch::native();
for acc in accumulators() {
let query = Query::plain(acc, None, None, None);
let all = dispatch.suitable(&query);
let Some(best) = all.iter().map(|(mmm, _, _)| mmm.arch().is_some()).max() else {
continue;
};
let peers: Vec<Suitable> =
all.iter().filter(|(mmm, _, _)| mmm.arch().is_some() == best).cloned().collect();
let mut kept = peers.clone();
retain_best(&mut kept);
let kept: Vec<&str> = kept.iter().map(|(mmm, _, _)| mmm.name()).collect();
for group in packing_groups(&peers) {
let matvecs: Vec<&str> =
group.iter().filter(|c| c.0.nr() == 1).map(|c| c.0.name()).collect();
if matvecs.is_empty() || !group.iter().any(|c| kept.contains(&c.0.name())) {
continue;
}
assert!(
matvecs.iter().any(|name| kept.contains(name)),
"preference kept {:?} of the {acc:?} packing group {:?} but dropped its \
matvec kernels {matvecs:?}",
group
.iter()
.filter(|c| kept.contains(&c.0.name()))
.map(|c| c.0.name())
.collect::<Vec<_>>(),
group[0].0.packings()[group[0].1].0
);
}
}
}
fn packing_groups(suitable: &[Suitable]) -> Vec<Vec<&Suitable>> {
let mut groups: Vec<(&dyn MMMInputFormat, Vec<&Suitable>)> = vec![];
'entry: for entry in suitable {
let (mmm, packing, extractor) = entry;
let left: &dyn MMMInputFormat =
extractor.as_ref().map(|pe| &*pe.from).unwrap_or(&*mmm.packings()[*packing].0);
for group in &mut groups {
if let Some(merged) = group.0.merge_with(left) {
group.0 = merged;
group.1.push(entry);
continue 'entry;
}
}
groups.push((left, vec![entry]));
}
groups.into_iter().map(|(_, group)| group).collect()
}
}