Skip to main content

tract_linalg/
routines.rs

1//! Cross-arch registry of the single-winner kernels: one function, one datum type, one best
2//! implementation per machine.
3//!
4//! Every element-wise activation, scalar-parameter kernel and reduction declares itself here as
5//! data, so the whole function x target matrix is enumerable on any host -- with the
6//! `foreign-inventory` feature, including the trees this build cannot run. [`best_for`] answers
7//! which one a machine would use, and it takes the machine as an argument rather than reading
8//! the host, so the same query serves dispatch and introspection.
9//!
10//! This is deliberately not the model [`crate::mmm_routines`] uses. A matmul has many co-valid
11//! kernels per machine and the winner depends on the shape, so mmm keeps a pool and a tier
12//! ladder. These have no shape to weigh: one kernel wins outright, by what it is written
13//! against.
14//!
15//! A tree's kernels are declared whether or not this build compiled their bodies, so a
16//! descriptor being here means "such a kernel exists", not "it runs here". Only [`best_for`]'s
17//! answer for the *native* machine may be executed; anything else is metadata and would bail.
18//! What a build could not assemble at all is not declared, so an absent descriptor means "no
19//! such kernel", never "this toolchain skipped it".
20use crate::element_wise::{ElementWise, ElementWiseKer};
21use crate::isa::{Arch, Isa, IsaReq, IsaSet, LEVEL_BOOST};
22use crate::lut::Lut;
23use crate::reduce::{MapReduce, MapReduceKer, Reduce, ReduceKer};
24use tract_data::internal::*;
25
26/// A function a routine computes. One variant per function, whatever the datum types or the
27/// number of implementations behind it.
28#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
29pub enum Func {
30    Sigmoid,
31    Tanh,
32    Silu,
33    Gelu,
34    Erf,
35    Hardswish,
36    LeakyRelu,
37    MulByScalar,
38    ReduceMax,
39    ReduceMin,
40    ReduceSum,
41    Softmax2,
42    RmsNorm,
43    Lut,
44    /// A binary operation with the scalar broadcast over the whole slice.
45    BinByScalar(crate::BinOp),
46    /// A binary operation between two slices of the same length.
47    BinUnicast(crate::BinOp),
48}
49
50impl Func {
51    /// Every binary operation, in both layouts: what [`Self::ALL`] ends with.
52    const BIN: [Func; 12] = {
53        use crate::BinOp::*;
54        [
55            Func::BinByScalar(Min),
56            Func::BinByScalar(Max),
57            Func::BinByScalar(Add),
58            Func::BinByScalar(Mul),
59            Func::BinByScalar(Sub),
60            Func::BinByScalar(SubF),
61            Func::BinUnicast(Min),
62            Func::BinUnicast(Max),
63            Func::BinUnicast(Add),
64            Func::BinUnicast(Mul),
65            Func::BinUnicast(Sub),
66            Func::BinUnicast(SubF),
67        ]
68    };
69
70    pub const ALL: [Func; 26] = [
71        Func::Sigmoid,
72        Func::Tanh,
73        Func::Silu,
74        Func::Gelu,
75        Func::Erf,
76        Func::Hardswish,
77        Func::ReduceMax,
78        Func::ReduceMin,
79        Func::ReduceSum,
80        Func::Softmax2,
81        Func::RmsNorm,
82        Func::Lut,
83        Func::LeakyRelu,
84        Func::MulByScalar,
85        Func::BIN[0],
86        Func::BIN[1],
87        Func::BIN[2],
88        Func::BIN[3],
89        Func::BIN[4],
90        Func::BIN[5],
91        Func::BIN[6],
92        Func::BIN[7],
93        Func::BIN[8],
94        Func::BIN[9],
95        Func::BIN[10],
96        Func::BIN[11],
97    ];
98
99    /// Where this function sits in [`Self::ALL`], which is what indexes the dispatch table.
100    fn slot(self) -> usize {
101        match self {
102            Func::Sigmoid => 0,
103            Func::Tanh => 1,
104            Func::Silu => 2,
105            Func::Gelu => 3,
106            Func::Erf => 4,
107            Func::Hardswish => 5,
108            Func::ReduceMax => 6,
109            Func::ReduceMin => 7,
110            Func::ReduceSum => 8,
111            Func::Softmax2 => 9,
112            Func::RmsNorm => 10,
113            Func::Lut => 11,
114            Func::LeakyRelu => 12,
115            Func::MulByScalar => 13,
116            Func::BinByScalar(op) => 14 + op as usize,
117            Func::BinUnicast(op) => 20 + op as usize,
118        }
119    }
120
121    /// The name the matrix and the logs use.
122    pub fn name(&self) -> &'static str {
123        match self {
124            Func::Sigmoid => "sigmoid",
125            Func::Tanh => "tanh",
126            Func::Silu => "silu",
127            Func::Gelu => "gelu",
128            Func::Erf => "erf",
129            Func::Hardswish => "hardswish",
130            Func::LeakyRelu => "leaky_relu",
131            Func::MulByScalar => "mul_by_scalar",
132            Func::ReduceMax => "reduce_max",
133            Func::ReduceMin => "reduce_min",
134            Func::ReduceSum => "reduce_sum",
135            Func::Softmax2 => "softmax2",
136            Func::RmsNorm => "rms_norm",
137            Func::Lut => "lut",
138            Func::BinByScalar(op) => match op {
139                crate::BinOp::Min => "by_scalar_min",
140                crate::BinOp::Max => "by_scalar_max",
141                crate::BinOp::Add => "by_scalar_add",
142                crate::BinOp::Mul => "by_scalar_mul",
143                crate::BinOp::Sub => "by_scalar_sub",
144                crate::BinOp::SubF => "by_scalar_subf",
145            },
146            Func::BinUnicast(op) => match op {
147                crate::BinOp::Min => "unicast_min",
148                crate::BinOp::Max => "unicast_max",
149                crate::BinOp::Add => "unicast_add",
150                crate::BinOp::Mul => "unicast_mul",
151                crate::BinOp::Sub => "unicast_sub",
152                crate::BinOp::SubF => "unicast_subf",
153            },
154        }
155    }
156
157    /// The kernel this host runs for a function and datum type. An unfilled pair is an error rather
158    /// than a substitution: what a machine has no kernel for is what the matrix is there to show, and
159    /// a caller that quietly computed something else would hide it.
160    fn best_here(self, dt: DatumType) -> TractResult<&'static Routine> {
161        native_best(self, dt).with_context(|| {
162            format!("No {} kernel for {dt:?} on {:?}", self.name(), crate::isa::native())
163        })
164    }
165
166    /// The f32 kernel this host runs for `func`.
167    pub fn ew_f32(self) -> TractResult<Box<dyn ElementWise<f32>>> {
168        match self.best_here(DatumType::F32)?.factory {
169            RoutineFactory::F32(f) => Ok(f()),
170            // `Routine::dt` reads the arm, so `best_for` already filtered the datum type. The
171            // shape it cannot filter: asking a scalar-parameter routine for a plain one is a
172            // caller's mistake, not a missing kernel.
173            _ => bail!("{} is not a plain element-wise kernel", self.name()),
174        }
175    }
176
177    /// The f16 kernel this host runs for `func`.
178    pub fn ew_f16(self) -> TractResult<Box<dyn ElementWise<f16>>> {
179        match self.best_here(DatumType::F16)?.factory {
180            RoutineFactory::F16(f) => Ok(f()),
181            _ => bail!("{} is not a plain element-wise kernel", self.name()),
182        }
183    }
184
185    /// The f32 kernel this host runs for `func`, which takes a scalar parameter.
186    pub fn ew_f32_param(self) -> TractResult<Box<dyn ElementWise<f32, f32>>> {
187        match self.best_here(DatumType::F32)?.factory {
188            RoutineFactory::F32Param(f) => Ok(f()),
189            _ => bail!("{} is not a scalar-parameter kernel", self.name()),
190        }
191    }
192
193    /// The f16 kernel this host runs for `func`, which takes a scalar parameter.
194    pub fn ew_f16_param(self) -> TractResult<Box<dyn ElementWise<f16, f16>>> {
195        match self.best_here(DatumType::F16)?.factory {
196            RoutineFactory::F16Param(f) => Ok(f()),
197            _ => bail!("{} is not a scalar-parameter kernel", self.name()),
198        }
199    }
200
201    /// The f32 reduction this host runs for `func`.
202    pub fn reduce_f32(self) -> TractResult<Box<dyn Reduce<f32>>> {
203        match self.best_here(DatumType::F32)?.factory {
204            RoutineFactory::F32Reduce(f) => Ok(f()),
205            _ => bail!("{} is not a reduction", self.name()),
206        }
207    }
208
209    /// The f16 reduction this host runs for `func`.
210    pub fn reduce_f16(self) -> TractResult<Box<dyn Reduce<f16>>> {
211        match self.best_here(DatumType::F16)?.factory {
212            RoutineFactory::F16Reduce(f) => Ok(f()),
213            _ => bail!("{} is not a reduction", self.name()),
214        }
215    }
216
217    /// The f32 map-reduction this host runs for `func`.
218    pub fn map_reduce_f32(self) -> TractResult<Box<dyn MapReduce<f32, f32>>> {
219        match self.best_here(DatumType::F32)?.factory {
220            RoutineFactory::F32MapReduce(f) => Ok(f()),
221            _ => bail!("{} is not a map-reduction", self.name()),
222        }
223    }
224
225    /// The binary kernel this host runs for `func` and datum type, `None` when it has none. Unlike
226    /// the other accessors this one is optional rather than fallible: its callers rewrite a model
227    /// only when a kernel exists, and having none is an ordinary answer rather than a failure.
228    pub fn bin(self, dt: DatumType) -> Option<Box<crate::BinFn>> {
229        match native_best(self, dt)?.factory {
230            RoutineFactory::BinF32 { make, .. } | RoutineFactory::BinF16 { make, .. } => {
231                Some(make())
232            }
233            _ => None,
234        }
235    }
236}
237
238/// Builds the kernel behind a descriptor. The arm is what says which datum type the descriptor
239/// is for, so nothing repeats it as a field.
240#[allow(clippy::type_complexity)]
241pub enum RoutineFactory {
242    F32(fn() -> Box<dyn ElementWise<f32>>),
243    F16(fn() -> Box<dyn ElementWise<f16>>),
244    /// A kernel taking one scalar of its own datum type, applied to every element.
245    F32Param(fn() -> Box<dyn ElementWise<f32, f32>>),
246    F16Param(fn() -> Box<dyn ElementWise<f16, f16>>),
247    /// A kernel folding a slice to one value.
248    F32Reduce(fn() -> Box<dyn Reduce<f32>>),
249    F16Reduce(fn() -> Box<dyn Reduce<f16>>),
250    /// A kernel rewriting a slice and folding it in the same pass.
251    F32MapReduce(fn() -> Box<dyn MapReduce<f32, f32>>),
252    /// A kernel that is a plain function rather than a boxed object, so its name is a field:
253    /// there is no object to ask for one.
254    RmsNormF32 {
255        name: &'static str,
256        run: fn(&mut [f32], f32),
257    },
258    /// A kernel built around a table, which the caller owns and hands over per op.
259    LutU8 {
260        name: fn() -> &'static str,
261        make: fn(&[u8]) -> Box<dyn Lut>,
262    },
263    /// A binary kernel, over two tensor views. Type-erased like the views themselves, so the
264    /// datum type is the arm and the name a field.
265    BinF32 {
266        name: fn() -> &'static str,
267        make: fn() -> Box<crate::BinFn>,
268    },
269    BinF16 {
270        name: fn() -> &'static str,
271        make: fn() -> Box<crate::BinFn>,
272    },
273}
274
275/// One kernel, enumerable uniformly on every host.
276pub struct Routine {
277    pub func: Func,
278    /// Architecture the kernel is written for, `None` for generic Rust every target builds.
279    pub arch: Option<Arch>,
280    /// What the instruction set must offer for this kernel to run at all. Runnability only:
281    /// a preference spelled here would also move the kernel in the matrix.
282    pub isa: IsaReq,
283    /// Where this kernel sits against its siblings, when the instruction set it needs does not
284    /// say it. Zero for the kernels whose ladder step already ranks them correctly; a measured
285    /// exception spells the steps it disagrees with, via [`crate::isa::peer_of`] or
286    /// [`crate::isa::NEVER_PREFERRED`].
287    pub boost: isize,
288    /// Whether it reaches an f16 answer by converting a chunk to f32, running an f32 kernel over
289    /// it and converting it back. Set by the declaration that writes the round trip, never by
290    /// hand.
291    pub round_trip: bool,
292    pub factory: RoutineFactory,
293}
294
295inventory::collect!(Routine);
296
297impl Routine {
298    pub fn dt(&self) -> DatumType {
299        match self.factory {
300            RoutineFactory::F32(_)
301            | RoutineFactory::F32Param(_)
302            | RoutineFactory::F32Reduce(_)
303            | RoutineFactory::F32MapReduce(_)
304            | RoutineFactory::RmsNormF32 { .. } => DatumType::F32,
305            RoutineFactory::F16(_) | RoutineFactory::F16Param(_) | RoutineFactory::F16Reduce(_) => {
306                DatumType::F16
307            }
308            RoutineFactory::LutU8 { .. } => DatumType::U8,
309            RoutineFactory::BinF32 { .. } => DatumType::F32,
310            RoutineFactory::BinF16 { .. } => DatumType::F16,
311        }
312    }
313
314    /// The kernel's own name. Read from the built object rather than declared, so it cannot
315    /// disagree with the kernel it names. Building is metadata-only work and safe anywhere;
316    /// running what it builds is not.
317    pub fn name(&self) -> &'static str {
318        match self.factory {
319            RoutineFactory::F32(f) => f().name(),
320            RoutineFactory::F16(f) => f().name(),
321            RoutineFactory::F32Param(f) => f().name(),
322            RoutineFactory::F16Param(f) => f().name(),
323            RoutineFactory::F32Reduce(f) => f().name(),
324            RoutineFactory::F16Reduce(f) => f().name(),
325            RoutineFactory::F32MapReduce(f) => f().name(),
326            RoutineFactory::RmsNormF32 { name, .. } => name,
327            RoutineFactory::LutU8 { name, .. } => name(),
328            RoutineFactory::BinF32 { name, .. } | RoutineFactory::BinF16 { name, .. } => name(),
329        }
330    }
331
332    /// Whether `isa` describes a machine this kernel runs on: its architecture, and every
333    /// feature it needs.
334    pub fn runnable_on(&self, isa: &IsaSet) -> bool {
335        self.arch.is_none_or(|a| Some(a) == isa.arch()) && self.isa.satisfied_by(*isa)
336    }
337
338    /// What this kernel is worth on a machine that can run it: its ladder step, plus whatever
339    /// a measurement said the step gets wrong. An arch kernel always outranks a generic one,
340    /// which is a different question and is compared before this.
341    fn preference(&self) -> isize {
342        self.isa.level() as isize * LEVEL_BOOST + self.boost
343    }
344}
345
346/// Every routine this build compiled, whichever architecture it speaks for.
347pub fn declared() -> impl Iterator<Item = &'static Routine> {
348    inventory::iter::<Routine>()
349}
350
351/// The kernel `isa` would run for this function and datum type: an architecture kernel over a
352/// generic one, then the most capable instruction set, then the name, which only settles ties
353/// `inventory`'s link order would otherwise settle differently between builds. `None` when
354/// nothing is declared for the pair at all.
355pub fn best_for(func: Func, dt: DatumType, isa: &IsaSet) -> Option<&'static Routine> {
356    declared()
357        .filter(|r| r.func == func && r.dt() == dt && r.runnable_on(isa))
358        .max_by_key(|r| (r.arch.is_some(), r.preference(), r.name()))
359}
360
361/// A cell of the matrix closed on purpose: on machines offering `isa`, this pair runs the kernel
362/// named here, and a kernel written for that instruction set would not be an improvement.
363///
364/// Declared beside the kernels of the tree it speaks for, so a build carries it exactly when it
365/// carries them, and it pins its winner by name: a machine that resolves the pair to something
366/// else is a different verdict, and needs its own settlement or none.
367pub struct Settled {
368    /// The rung this speaks for, which is also the column the matrix draws it under.
369    pub isa: Isa,
370    pub func: Func,
371    pub dt: DatumType,
372    /// The winner this keeps, by the name the kernel answers with.
373    pub kernel: &'static str,
374    /// One line, in the terms someone would need to disagree with it.
375    pub why: &'static str,
376}
377
378inventory::collect!(Settled);
379
380impl Settled {
381    /// Whether this speaks for `isa`: a machine of its architecture, offering what it names, and
382    /// resolving its pair to the kernel it pins.
383    pub fn covers(&self, isa: &IsaSet) -> bool {
384        Some(self.isa.arch()) == isa.arch()
385            && isa.has(self.isa)
386            && best_for(self.func, self.dt, isa).is_some_and(|r| r.name() == self.kernel)
387    }
388}
389
390/// Every settlement this build compiled, whichever architecture it speaks for.
391pub fn settlements() -> impl Iterator<Item = &'static Settled> {
392    inventory::iter::<Settled>()
393}
394
395/// Why what `isa` runs for this pair is the answer we mean, when a settlement says so. `None`
396/// when nothing settles it, which is what leaves such a cell a gap.
397pub fn settled_for(func: Func, dt: DatumType, isa: &IsaSet) -> Option<&'static Settled> {
398    settlements().find(|s| s.func == func && s.dt == dt && s.covers(isa))
399}
400
401/// What a machine's answer for one pair amounts to, which is what the matrix colours and what
402/// says whether a kernel is left to write.
403#[derive(Copy, Clone, PartialEq, Eq, Debug)]
404pub enum Standing {
405    /// Nothing this build declares serves the pair here.
406    Missing,
407    /// A kernel written for this machine's own rung of the ladder.
408    Dedicated,
409    /// Correct code that was not written for this machine: portable Rust, or an architecture
410    /// kernel from a rung below it.
411    Unspecialized,
412    /// Portable f16 code, whose every operation converts to f32 and back: the right answer at a
413    /// per-element cost no machine has to pay.
414    Emulated,
415    /// Whatever it runs, declared as the answer we mean. [`settled_for`] says why.
416    Settled,
417}
418
419/// What `isa` amounts to for this pair before any settlement speaks for it, which is the question
420/// a settlement answers.
421fn unsettled(func: Func, dt: DatumType, isa: &IsaSet) -> Standing {
422    let Some(routine) = best_for(func, dt, isa) else { return Standing::Missing };
423    // Portable f16 arithmetic goes through `f16`'s operators, which convert to f32 and back
424    // around every single one, whatever the machine underneath. A conversion-free portable f16
425    // kernel -- a table, or bit work -- would need saying so here.
426    if routine.arch.is_none() && dt == DatumType::F16 {
427        Standing::Emulated
428    } else if routine.round_trip && !isa.fp16_arithmetic() {
429        Standing::Settled
430    } else if routine.arch.is_some() && routine.isa.level() == isa.level() {
431        Standing::Dedicated
432    } else {
433        Standing::Unspecialized
434    }
435}
436
437/// What this machine's answer for the pair amounts to.
438pub fn standing(func: Func, dt: DatumType, isa: &IsaSet) -> Standing {
439    let answer = unsettled(func, dt, isa);
440    let settleable = matches!(answer, Standing::Unspecialized | Standing::Emulated);
441    if settleable && settled_for(func, dt, isa).is_some() { Standing::Settled } else { answer }
442}
443
444/// What every chunked round trip answers for on a machine with no f16 arithmetic: converting a
445/// chunk, computing it in f32 and converting it back is the technique, not a compromise.
446const NO_FP16_ARITHMETIC: &str = "no f16 arithmetic here: a chunk through an f32 kernel is it";
447
448/// Why this cell is closed, when it is: what a settlement declared, or the standing answer every
449/// f32 round trip carries on a machine that cannot compute in f16.
450pub fn settled_why(func: Func, dt: DatumType, isa: &IsaSet) -> Option<&'static str> {
451    if standing(func, dt, isa) != Standing::Settled {
452        return None;
453    }
454    Some(settled_for(func, dt, isa).map_or(NO_FP16_ARITHMETIC, |settled| settled.why))
455}
456
457/// The kernel this host runs for a function and datum type, resolved once. Dispatch happens per
458/// eval, so the scan over every declared routine runs at first use and the answers are kept in a
459/// flat table nothing has to hash.
460fn native_best(func: Func, dt: DatumType) -> Option<&'static Routine> {
461    const SLOTS: usize = Func::ALL.len() * 3;
462    static NATIVE: std::sync::OnceLock<[Option<&'static Routine>; SLOTS]> =
463        std::sync::OnceLock::new();
464    let dt_slot = match dt {
465        DatumType::F32 => 0,
466        DatumType::F16 => 1,
467        DatumType::U8 => 2,
468        _ => return None,
469    };
470    NATIVE.get_or_init(|| {
471        let isa = crate::isa::native();
472        let mut table = [None; SLOTS];
473        for func in Func::ALL {
474            for (dt_slot, dt) in [DatumType::F32, DatumType::F16, DatumType::U8].iter().enumerate()
475            {
476                table[func.slot() * 3 + dt_slot] = best_for(func, *dt, &isa);
477            }
478        }
479        table
480    })[func.slot() * 3 + dt_slot]
481}
482
483/// The fused row-wise RmsNorm this host runs: the row and the epsilon, in place.
484pub fn rms_norm_f32() -> TractResult<fn(&mut [f32], f32)> {
485    match Func::RmsNorm.best_here(DatumType::F32)?.factory {
486        RoutineFactory::RmsNormF32 { run, .. } => Ok(run),
487        _ => bail!("rms_norm is not a plain function"),
488    }
489}
490
491/// The look-up table kernel this host runs, over the table the caller owns.
492pub fn lut_u8(table: &[u8]) -> TractResult<Box<dyn Lut>> {
493    match Func::Lut.best_here(DatumType::U8)?.factory {
494        RoutineFactory::LutU8 { make, .. } => Ok(make(table)),
495        _ => bail!("lut is not a table kernel"),
496    }
497}
498
499/// File the descriptor of a kernel declared elsewhere, under the leading architecture ident the
500/// `routine_*` declaration macros take, or with no ident at all for generic Rust every target
501/// builds. Those macros end here; write it directly for a kernel no shape macro emits, and it
502/// carries no test module of its own. The first argument names the factory arm, which is what
503/// says the kernel's shape and datum type; `isa` is omitted for a kernel whose architecture
504/// needs nothing extra to run it, `boost` for one its ladder step already ranks right.
505macro_rules! submit_routine {
506    (arm; $($rest:tt)*) => { submit_routine!(@ Some($crate::isa::Arch::Arm); $($rest)*); };
507    (aarch64; $($rest:tt)*) => { submit_routine!(@ Some($crate::isa::Arch::Aarch64); $($rest)*); };
508    (x86_64; $($rest:tt)*) => { submit_routine!(@ Some($crate::isa::Arch::X86_64); $($rest)*); };
509    (riscv64; $($rest:tt)*) => { submit_routine!(@ Some($crate::isa::Arch::RiscV64); $($rest)*); };
510    (wasm32; $($rest:tt)*) => { submit_routine!(@ Some($crate::isa::Arch::Wasm32Simd128); $($rest)*); };
511    (generic; $($rest:tt)*) => { submit_routine!(@ None; $($rest)*); };
512
513    ($factory:ident, $($rest:tt)*) => { submit_routine!(@ None; $factory, $($rest)*); };
514
515    // One arm per kernel shape: the trait a kernel implements decides how it is built, and
516    // nothing else here varies. The clauses are spelled out rather than forwarded as tokens
517    // because a `path` fragment may only be followed by a comma.
518    (@ $arch:expr; RmsNormF32, $func:ident, $name:literal, $run:path
519     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
520        submit_routine!(@@ $arch, $func,
521            $crate::routines::RoutineFactory::RmsNormF32 { name: $name, run: $run }
522            $(, isa($($isa),+))? $(, boost($boost))?);
523    };
524    (@ $arch:expr; BinF32, BinByScalar($op:ident), $ker:path
525     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
526        submit_routine!(@@ $arch, BinByScalar($crate::BinOp::$op),
527            $crate::routines::RoutineFactory::BinF32 {
528                name: <$ker as $crate::element_wise::ElementWiseKer<f32, f32>>::name,
529                make: <$ker as $crate::by_scalar::ByScalarKer<f32>>::bin,
530            }
531            $(, isa($($isa),+))? $(, boost($boost))?);
532    };
533    (@ $arch:expr; BinF16, BinByScalar($op:ident), $ker:path
534     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
535        submit_routine!(@@ $arch, BinByScalar($crate::BinOp::$op),
536            $crate::routines::RoutineFactory::BinF16 {
537                name: <$ker as $crate::element_wise::ElementWiseKer<$crate::f16, $crate::f16>>::name,
538                make: <$ker as $crate::by_scalar::ByScalarKer<$crate::f16>>::bin,
539            }
540            $(, isa($($isa),+))? $(, boost($boost))?);
541    };
542    (@ $arch:expr; BinF32, BinUnicast($op:ident), $ker:path
543     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
544        submit_routine!(@@ $arch, BinUnicast($crate::BinOp::$op),
545            $crate::routines::RoutineFactory::BinF32 {
546                name: <$ker as $crate::unicast::UnicastKer<f32>>::name,
547                make: <$ker as $crate::unicast::UnicastKer<f32>>::bin,
548            }
549            $(, isa($($isa),+))? $(, boost($boost))?);
550    };
551    (@ $arch:expr; BinF16, BinUnicast($op:ident), $ker:path
552     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
553        submit_routine!(@@ $arch, BinUnicast($crate::BinOp::$op),
554            $crate::routines::RoutineFactory::BinF16 {
555                name: <$ker as $crate::unicast::UnicastKer<$crate::f16>>::name,
556                make: <$ker as $crate::unicast::UnicastKer<$crate::f16>>::bin,
557            }
558            $(, isa($($isa),+))? $(, boost($boost))?);
559    };
560    (@ $arch:expr; LutU8, $func:ident, $ker:path
561     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
562        submit_routine!(@@ $arch, $func,
563            $crate::routines::RoutineFactory::LutU8 {
564                name: <$ker as $crate::lut::LutKer>::name,
565                make: |table| $crate::routines::lut_of::<$ker>(table),
566            }
567            $(, isa($($isa),+))? $(, boost($boost))?);
568    };
569    (@ $arch:expr; F32Reduce, $func:ident, $ker:path
570     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
571        submit_routine!(@@ $arch, $func,
572            $crate::routines::RoutineFactory::F32Reduce(|| $crate::routines::reduce_of::<$ker, _>())
573            $(, isa($($isa),+))? $(, boost($boost))?);
574    };
575    (@ $arch:expr; F16Reduce, $func:ident, $ker:path
576     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
577        submit_routine!(@@ $arch, $func,
578            $crate::routines::RoutineFactory::F16Reduce(|| $crate::routines::reduce_of::<$ker, _>())
579            $(, isa($($isa),+))? $(, boost($boost))?);
580    };
581    (@ $arch:expr; F32MapReduce, $func:ident, $ker:path
582     $(, isa($($isa:ident),+))? $(, boost($boost:expr))?) => {
583        submit_routine!(@@ $arch, $func,
584            $crate::routines::RoutineFactory::F32MapReduce(
585                || $crate::routines::map_reduce_of::<$ker, _>()
586            )
587            $(, isa($($isa),+))? $(, boost($boost))?);
588    };
589    (@ $arch:expr; $factory:ident, $func:ident, $ker:path
590     $(, isa($($isa:ident),+))? $(, boost($boost:expr))? $(, round_trip($round_trip:expr))?) => {
591        submit_routine!(@@ $arch, $func,
592            $crate::routines::RoutineFactory::$factory(
593                || $crate::routines::factory_of::<$ker, _, _>()
594            )
595            $(, isa($($isa),+))? $(, boost($boost))? $(, round_trip($round_trip))?);
596    };
597
598    (@@ $arch:expr, $func:ident $(($($payload:tt)*))?, $factory:expr
599     $(, isa($($isa:ident),+))? $(, boost($boost:expr))? $(, round_trip($round_trip:expr))?) => {
600        inventory::submit! {
601            $crate::routines::Routine {
602                func: $crate::routines::Func::$func $(($($payload)*))?,
603                arch: $arch,
604                isa: $crate::isa::IsaReq::ANY $(.needing(&[$($crate::isa::Isa::$isa),+]))?,
605                boost: {
606                    #[allow(unused_mut, unused_assignments)]
607                    let mut boost = 0;
608                    $(boost = $boost;)?
609                    boost
610                },
611                round_trip: {
612                    #[allow(unused_mut, unused_assignments)]
613                    let mut round_trip = false;
614                    $(round_trip = $round_trip;)?
615                    round_trip
616                },
617                factory: $factory,
618            }
619        }
620    };
621}
622
623/// Close one cell of the matrix on purpose: the machines offering `$isa` run `$kernel` for this
624/// pair, and that is the answer we mean. Write it beside the kernels of the tree it speaks for,
625/// and say in `$why` what a kernel written for the instruction set would have to beat.
626macro_rules! settled {
627    ($isa:ident, $func:ident $(($op:ident))?, $dt:ident, $kernel:ident, $why:literal) => {
628        inventory::submit! {
629            $crate::routines::Settled {
630                isa: $crate::isa::Isa::$isa,
631                func: $crate::routines::Func::$func $(($crate::BinOp::$op))?,
632                dt: tract_data::prelude::DatumType::$dt,
633                kernel: stringify!($kernel),
634                why: $why,
635            }
636        }
637    };
638}
639
640/// A look-up table kernel over `table`, as its factory arm wants it.
641pub fn lut_of<K: crate::lut::LutKer + 'static>(table: &[u8]) -> Box<dyn Lut> {
642    Box::new(crate::lut::LutImpl::<K>::new(table))
643}
644
645/// The `red()` of a reduction kernel, as its factory arm wants it.
646pub fn reduce_of<K, T>() -> Box<dyn Reduce<T>>
647where
648    T: crate::LADatum,
649    K: ReduceKer<T> + Clone,
650{
651    K::red()
652}
653
654/// The `red()` of a map-reduction kernel, as its factory arm wants it.
655pub fn map_reduce_of<K, T>() -> Box<dyn MapReduce<T, T>>
656where
657    T: crate::LADatum,
658    K: MapReduceKer<T, T> + Clone,
659{
660    K::red()
661}
662
663pub fn factory_of<K, T, P>() -> Box<dyn ElementWise<T, P>>
664where
665    T: crate::LADatum,
666    P: Copy + Send + Sync + std::fmt::Debug + 'static + Default,
667    K: ElementWiseKer<T, P> + Clone,
668{
669    K::ew()
670}
671
672#[cfg(test)]
673mod tests {
674    use super::*;
675
676    /// The dispatch table is indexed by [`Func::slot`], so every function must own one slot
677    /// inside it, and the cache must answer what a fresh scan would.
678    #[test]
679    fn every_func_owns_a_slot() {
680        let mut slots = std::collections::HashSet::new();
681        for func in Func::ALL {
682            assert!(func.slot() < Func::ALL.len(), "{} is out of the table", func.name());
683            assert!(slots.insert(func.slot()), "{} shares a slot", func.name());
684        }
685        assert_eq!(slots.len(), Func::ALL.len());
686        let isa = crate::isa::native();
687        for func in Func::ALL {
688            for dt in [DatumType::F32, DatumType::F16, DatumType::U8] {
689                assert_eq!(
690                    native_best(func, dt).map(|r| r.name()),
691                    best_for(func, dt, &isa).map(|r| r.name()),
692                    "{} {dt:?}",
693                    func.name()
694                );
695            }
696        }
697    }
698
699    /// A declared kernel no machine would ever choose is either a mistake -- the wrong instruction
700    /// set, or a sibling that dominates it -- or one kept for its tests, which says so by
701    /// declining. Nothing else is reachable, and nothing would notice.
702    #[test]
703    fn a_kernel_nothing_can_choose_says_so() {
704        let mut chosen = std::collections::HashSet::new();
705        for isa in IsaSet::every_ladder() {
706            for func in Func::ALL {
707                for dt in [DatumType::F32, DatumType::F16, DatumType::U8] {
708                    if let Some(r) = best_for(func, dt, &isa) {
709                        chosen.insert((func, dt, r.name()));
710                    }
711                }
712            }
713        }
714        for r in declared() {
715            assert!(
716                chosen.contains(&(r.func, r.dt(), r.name())) || r.boost < 0,
717                "{} {:?} {} can never be chosen, and does not decline",
718                r.func.name(),
719                r.dt(),
720                r.name()
721            );
722        }
723    }
724
725    /// A pair with no kernel fails rather than falling back on something that computes a
726    /// different thing. f16 erf is the standing example: no tree has one, and core builds a
727    /// look-up table from the f32 kernel instead of asking for it.
728    #[test]
729    fn an_unfilled_pair_fails() {
730        let err = Func::Erf.ew_f16().unwrap_err().to_string();
731        assert!(err.starts_with("No erf kernel for F16 on "), "{err}");
732        // No tree has an f16 minimum either, and no consumer asks for one.
733        let err = Func::ReduceMin.reduce_f16().unwrap_err().to_string();
734        assert!(err.starts_with("No reduce_min kernel for F16 on "), "{err}");
735        // Asking for the wrong shape is a caller's mistake, and says so.
736        let err = Func::ReduceMax.ew_f32().unwrap_err().to_string();
737        assert_eq!(err, "reduce_max is not a plain element-wise kernel");
738    }
739    /// Whatever this machine declares, it can build and run: the registry is dispatch now, so a
740    /// pair whose accessor fails is a cell nothing would notice was dead. Also holds `best_for`
741    /// and the accessors to the same answer, they being two ways to ask one question.
742    #[test]
743    fn what_this_machine_declares_it_can_build() {
744        let isa = crate::isa::native();
745        for func in Func::ALL {
746            for dt in [DatumType::F32, DatumType::F16, DatumType::U8] {
747                let Some(routine) = best_for(func, dt, &isa) else { continue };
748                let built = match routine.factory {
749                    RoutineFactory::F32(_) => func.ew_f32().map(|k| k.name()),
750                    RoutineFactory::F16(_) => func.ew_f16().map(|k| k.name()),
751                    RoutineFactory::F32Param(_) => func.ew_f32_param().map(|k| k.name()),
752                    RoutineFactory::F16Param(_) => func.ew_f16_param().map(|k| k.name()),
753                    RoutineFactory::F32Reduce(_) => func.reduce_f32().map(|k| k.name()),
754                    RoutineFactory::F16Reduce(_) => func.reduce_f16().map(|k| k.name()),
755                    RoutineFactory::F32MapReduce(_) => func.map_reduce_f32().map(|k| k.name()),
756                    RoutineFactory::RmsNormF32 { name, .. } => Ok(name),
757                    RoutineFactory::LutU8 { name, .. } => lut_u8(&[0u8; 256]).map(|_| name()),
758                    RoutineFactory::BinF32 { name, .. } | RoutineFactory::BinF16 { name, .. } => {
759                        func.bin(dt).map(|_| name()).ok_or_else(|| format_err!("no bin kernel"))
760                    }
761                };
762                assert_eq!(
763                    built.map_err(|e| e.to_string()),
764                    Ok(routine.name()),
765                    "{} {dt:?}",
766                    func.name()
767                );
768            }
769        }
770    }
771
772    /// A settlement answers for a cell the matrix would otherwise read as an invitation, so one
773    /// closing no such cell is either wrong about its machine or has outlived the kernel it
774    /// pinned -- someone wrote the kernel it says nobody should.
775    #[test]
776    fn every_settlement_closes_a_cell() {
777        for s in settlements() {
778            assert!(
779                IsaSet::every_ladder().any(|m| s.covers(&m)
780                    && matches!(
781                        unsettled(s.func, s.dt, &m),
782                        Standing::Unspecialized | Standing::Emulated
783                    )),
784                "{} {:?} on {} settles nothing, {} being what it keeps",
785                s.func.name(),
786                s.dt,
787                s.isa,
788                s.kernel
789            );
790        }
791    }
792
793    /// Two settlements over one cell would each answer for it, and the matrix would print
794    /// whichever `inventory` happened to link first.
795    #[test]
796    fn a_cell_is_settled_once() {
797        for isa in IsaSet::every_ladder() {
798            for func in Func::ALL {
799                for dt in [DatumType::F32, DatumType::F16, DatumType::U8] {
800                    let count = settlements()
801                        .filter(|s| s.func == func && s.dt == dt && s.covers(&isa))
802                        .count();
803                    assert!(
804                        count <= 1,
805                        "{} {dt:?} on {isa:?} is settled {count} times",
806                        func.name()
807                    );
808                }
809            }
810        }
811    }
812}