Skip to main content

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}