tract_linalg/mmm_tiers.rs
1//! Cross-arch registry of mmm dispatch tiers.
2//!
3//! A tier is one architecture's opinion about which suitable kernel to run, for the accumulators
4//! and shapes it claims. Every tier is declared as data, so the whole ladder is enumerable on any
5//! host and its precedence is a field rather than an order of registration.
6//!
7//! [`preferred`] asks the applicable tiers in descending [`MmmTier::precedence`] and takes the
8//! first answer that the query can actually reach. A tier with no opinion, or one naming a kernel
9//! this query has no suitable entry for, leaves it to the next tier down — so "only when nothing
10//! better answered" needs no condition of its own, and the generic rules at precedence 0 are
11//! simply the last tier every machine ends on.
12use crate::isa::Arch;
13#[cfg(test)]
14use crate::isa::Isa;
15use crate::isa::IsaSet;
16use crate::mmm::{Query, Suitable};
17use tract_data::prelude::DatumType;
18
19/// One rung of a machine's dispatch ladder.
20pub struct MmmTier {
21 /// Architecture the tier belongs to, `None` for the generic rules every machine ends on.
22 pub arch: Option<Arch>,
23 /// Where this tier sits among the tiers of one architecture: they are asked in descending
24 /// order and the first answer wins. It needs only be right between tiers that speak for the
25 /// same accumulator — two tiers answering different ones never meet.
26 pub precedence: u8,
27 /// What to call this rung when reporting the ladder.
28 pub name: &'static str,
29 /// Whether this tier speaks on this machine at all: the instruction set it needs, the vendor
30 /// or chip it was measured on. Never a shape or an accumulator — those belong to
31 /// [`Self::preferred`], which can decline by answering `None`.
32 pub applies: fn(&IsaSet) -> bool,
33 /// Which kernel this tier would run, by name, `None` for a query it does not claim. A tier
34 /// answers with the kernel it wants and [`preferred`] holds that answer to the suitable list,
35 /// so naming one the query cannot reach is the same as having no opinion: the next tier down
36 /// is asked. Only a tier picking *from* the list — a cost model weighing candidates — needs
37 /// to read `suitable` at all.
38 pub preferred: fn(&IsaSet, DatumType, &Query, &[Suitable]) -> Option<&'static str>,
39}
40
41inventory::collect!(MmmTier);
42
43/// Every tier this build compiled, whichever architecture it speaks for.
44pub fn declared() -> impl Iterator<Item = &'static MmmTier> {
45 inventory::iter::<MmmTier>()
46}
47
48/// The tiers that speak for a machine, highest precedence first. Ties keep declaration order,
49/// which is not stable across builds — two tiers of one architecture must not share a precedence.
50pub fn for_isa(isa: &IsaSet) -> Vec<&'static MmmTier> {
51 let arch = isa.arch();
52 let mut tiers: Vec<&'static MmmTier> = declared()
53 .filter(|t| t.arch.is_none() || t.arch == arch)
54 .filter(|t| (t.applies)(isa))
55 .collect();
56 tiers.sort_by_key(|t| std::cmp::Reverse(t.precedence));
57 log::debug!(
58 "mmm tiers for {isa:?}: {}",
59 tiers.iter().map(|t| t.name).collect::<Vec<_>>().join(" > ")
60 );
61 tiers
62}
63
64/// Which suitable kernel this machine would run: the answer of the highest-precedence tier that
65/// has one. `None` only when no tier claims the query at all.
66pub fn preferred(
67 isa: &IsaSet,
68 tiers: &[&'static MmmTier],
69 accumulator: DatumType,
70 query: &Query,
71 suitable: &[Suitable],
72) -> Option<usize> {
73 tiers.iter().find_map(|t| {
74 let name = (t.preferred)(isa, accumulator, query, suitable)?;
75 crate::mmm::suitable_named(suitable, name)
76 })
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 /// Two tiers of one architecture must not share a precedence: [`for_isa`] sorts by it, and a
84 /// tie falls back on declaration order, which is link order and not stable across builds.
85 /// Precedence is the whole ordering contract, so a collision is a silent coin toss.
86 #[test]
87 fn precedence_is_unique_per_arch() {
88 let tiers: Vec<&MmmTier> = declared().collect();
89 for (ix, a) in tiers.iter().enumerate() {
90 for b in &tiers[ix + 1..] {
91 assert!(
92 a.arch != b.arch || a.precedence != b.precedence,
93 "tiers {} and {} both claim {:?} precedence {}",
94 a.name,
95 b.name,
96 a.arch,
97 a.precedence
98 );
99 }
100 }
101 }
102
103 /// Every feature belongs to exactly one architecture, so a tier's own architecture and the
104 /// features its `applies` asks for cannot disagree — a tier that required another
105 /// architecture's feature would simply never fire.
106 #[test]
107 fn a_tier_only_needs_its_own_architecture() {
108 for tier in declared() {
109 let Some(arch) = tier.arch else { continue };
110 for isa in Isa::ALL.into_iter().filter(|i| !i.is_arch()) {
111 if (tier.applies)(&IsaSet::of_arch(arch).with(isa))
112 != (tier.applies)(&IsaSet::of_arch(arch))
113 {
114 assert_eq!(
115 isa.arch(),
116 arch,
117 "tier {} is {arch:?} but reacts to {isa}, which is {:?}",
118 tier.name,
119 isa.arch()
120 );
121 }
122 }
123 }
124 }
125 /// A tier that names a kernel must somewhere name one the query can reach. Naming an
126 /// unreachable kernel is how a tier defers to the one below, so a tier doing it at every step
127 /// and for every accumulator is dead dispatch that no pick can expose: the machine falls
128 /// through and still runs something. A tier gated on a knob that is off never answers here at
129 /// all, and is not held to this; setting the knob brings it in.
130 ///
131 /// Only the steps this host can execute, unlike the questions asked of the routine registry:
132 /// the mmm pool holds a compiled kernel only where the CPU can run it, so a step above this
133 /// machine has an empty pool and nothing there is reachable by construction.
134 #[test]
135 fn a_tier_that_answers_names_a_reachable_kernel() {
136 let mut answered = std::collections::HashSet::new();
137 let mut reached = std::collections::HashSet::new();
138 let native = crate::isa::native();
139 for isa in IsaSet::every_ladder().filter(|l| l.iter().all(|i| native.has(i))) {
140 let dispatch = crate::MmmDispatch::for_isa(isa);
141 for acc in [DatumType::F32, DatumType::F16, DatumType::I32] {
142 let query = Query::plain(acc, None, None, None);
143 let suitable = dispatch.suitable(&query);
144 for tier in dispatch.tiers() {
145 let Some(name) = (tier.preferred)(&isa, acc, &query, &suitable) else {
146 continue;
147 };
148 answered.insert(tier.name);
149 if crate::mmm::suitable_named(&suitable, name).is_some() {
150 reached.insert(tier.name);
151 }
152 }
153 }
154 }
155 let unheard: Vec<&str> =
156 answered.iter().copied().filter(|name| !reached.contains(name)).collect();
157 assert!(unheard.is_empty(), "these tiers name a kernel no machine can reach: {unheard:?}");
158 }
159}