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