Skip to main content

tract_linalg/
isa.rs

1//! Instruction-set features, as data a kernel declares and a machine is asked for.
2//!
3//! A kernel's requirement is a set rather than a closure: it can be printed, enumerated, and
4//! evaluated against a machine other than this one, which is what makes another cohort's
5//! dispatch inspectable. Micro-architecture is deliberately not in here — how many 512-bit FMA
6//! ports a core has is not an instruction set, and preferring a kernel is not the same as being
7//! able to run it: that belongs in [`crate::mmm::MatMatMulKer::preference`].
8
9use std::fmt;
10use std::sync::OnceLock;
11
12/// An architecture tract has a kernel tree for, after its `target_arch`. It is the identity a
13/// kernel tree and a dispatch tier are keyed on; [`Isa::of_arch`] is the same thing as a set
14/// member, and [`IsaSet::arch`] reads it back out of a machine's features.
15///
16/// Naming one is not having kernels for it: an architecture tract does not name at all has no
17/// variant here, and a build whose assembler cannot encode a tree's instructions is
18/// generic-only even though it knows what it is running on. Only the wasm tree hinges on a
19/// build feature — hence the variant naming that feature; the others exist on their arch and
20/// gate individual kernels on runtime probes instead.
21#[allow(non_camel_case_types)]
22#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
23pub enum Arch {
24    Arm,
25    Aarch64,
26    X86_64,
27    RiscV64,
28    /// The only tree gated at build time rather than probed at runtime.
29    Wasm32Simd128,
30}
31
32impl Arch {
33    pub const ALL: [Arch; 5] =
34        [Arch::Arm, Arch::Aarch64, Arch::X86_64, Arch::RiscV64, Arch::Wasm32Simd128];
35
36    pub fn is_native(&self) -> bool {
37        match self {
38            Arch::Arm => cfg!(target_arch = "arm"),
39            Arch::Aarch64 => cfg!(target_arch = "aarch64"),
40            Arch::X86_64 => cfg!(target_arch = "x86_64"),
41            Arch::RiscV64 => cfg!(target_arch = "riscv64"),
42            Arch::Wasm32Simd128 => {
43                cfg!(all(target_arch = "wasm32", target_feature = "simd128"))
44            }
45        }
46    }
47}
48
49impl fmt::Display for Arch {
50    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
51        f.write_str(match self {
52            Arch::Arm => "arm",
53            Arch::Aarch64 => "aarch64",
54            Arch::X86_64 => "x86_64",
55            Arch::RiscV64 => "riscv64",
56            Arch::Wasm32Simd128 => "wasm32+simd128",
57        })
58    }
59}
60
61/// One instruction-set feature a kernel can need, or — at level 0 — the plain architecture
62/// underneath them all.
63#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
64pub enum Isa {
65    /// The plain architectures. One belongs in every set, and it is what says whose kernels the
66    /// set is talking about; two is a contradiction, since no machine implements both.
67    Arm,
68    Aarch64,
69    X86_64,
70    RiscV64,
71    Wasm32,
72    /// A step on armv7 only: on aarch64 Advanced SIMD is baseline, hence unnamed there.
73    ArmNeon,
74    Aarch64Fp16,
75    Aarch64DotProd,
76    Aarch64Sve2,
77    Aarch64Sme,
78    Aarch64Sme2,
79    Aarch64AppleAmx,
80    X86_64Avx,
81    X86_64Avx2,
82    X86_64Fma,
83    X86_64F16c,
84    X86_64Avx512f,
85    X86_64Avx512Vnni,
86    X86_64Avx512Fp16,
87    X86_64AvxVnni,
88    X86_64AmxInt8,
89    X86_64AmxBf16,
90    /// The ratified Vector extension, RVV 1.0, which mandates `VLEN >= 128`.
91    RiscV64V,
92    /// A vector unit at least 256 bits wide, which is what the wide RVV tiles need. Not an
93    /// instruction set feature, but the hart property that decides which tile heights exist.
94    RiscV64Vlen256,
95    /// Zvfh, f16 arithmetic in the vector unit. Zvfhmin, which RVA23 mandates instead, converts
96    /// to f32 and back and so cannot hold an f16 accumulator.
97    RiscV64Zvfh,
98    Wasm32Simd128,
99    Wasm32RelaxedSimd,
100}
101
102impl Isa {
103    pub const ALL: [Isa; 27] = [
104        Isa::Arm,
105        Isa::Aarch64,
106        Isa::X86_64,
107        Isa::RiscV64,
108        Isa::Wasm32,
109        Isa::ArmNeon,
110        Isa::Aarch64Fp16,
111        Isa::Aarch64DotProd,
112        Isa::Aarch64Sve2,
113        Isa::Aarch64Sme,
114        Isa::Aarch64Sme2,
115        Isa::Aarch64AppleAmx,
116        Isa::X86_64Avx,
117        Isa::X86_64Avx2,
118        Isa::X86_64Fma,
119        Isa::X86_64F16c,
120        Isa::X86_64Avx512f,
121        Isa::X86_64Avx512Vnni,
122        Isa::X86_64Avx512Fp16,
123        Isa::X86_64AvxVnni,
124        Isa::X86_64AmxInt8,
125        Isa::X86_64AmxBf16,
126        Isa::RiscV64V,
127        Isa::RiscV64Vlen256,
128        Isa::RiscV64Zvfh,
129        Isa::Wasm32Simd128,
130        Isa::Wasm32RelaxedSimd,
131    ];
132
133    /// The token as it appears in a report and in `TRACT_CPU_ISA`.
134    pub fn name(&self) -> &'static str {
135        match self {
136            Isa::Arm => "arm",
137            Isa::Aarch64 => "aarch64",
138            Isa::X86_64 => "x86_64",
139            Isa::RiscV64 => "riscv64",
140            Isa::Wasm32 => "wasm32",
141            Isa::ArmNeon => "neon",
142            Isa::Aarch64Fp16 => "fp16",
143            Isa::Aarch64DotProd => "dotprod",
144            Isa::Aarch64Sve2 => "sve2",
145            Isa::Aarch64Sme => "sme",
146            Isa::Aarch64Sme2 => "sme2",
147            Isa::Aarch64AppleAmx => "apple-amx",
148            Isa::X86_64Avx => "avx",
149            Isa::X86_64Avx2 => "avx2",
150            Isa::X86_64Fma => "fma",
151            Isa::X86_64F16c => "f16c",
152            Isa::X86_64Avx512f => "avx512f",
153            Isa::X86_64Avx512Vnni => "avx512vnni",
154            Isa::X86_64Avx512Fp16 => "avx512fp16",
155            Isa::X86_64AvxVnni => "avxvnni",
156            Isa::X86_64AmxInt8 => "amx-int8",
157            Isa::X86_64AmxBf16 => "amx-bf16",
158            Isa::RiscV64V => "rvv",
159            Isa::RiscV64Vlen256 => "vlen256",
160            Isa::RiscV64Zvfh => "zvfh",
161            Isa::Wasm32Simd128 => "simd128",
162            Isa::Wasm32RelaxedSimd => "relaxed-simd",
163        }
164    }
165
166    fn from_name(s: &str) -> Option<Isa> {
167        Isa::ALL.into_iter().find(|i| i.name() == s)
168    }
169
170    /// Where this feature sits in its architecture's ladder, each step meaning "a kernel
171    /// written for this needs nothing a kernel written for the step below has, and can do
172    /// more". Steps are compared across an architecture's whole kernel set, so every feature
173    /// a kernel can declare has to be placed: an unplaced feature reads as the baseline and
174    /// would quietly demote its kernels below every sibling in the preference order.
175    ///
176    /// The two architectures share the scale without meeting on it — no host offers features
177    /// from both — so `Neon` and `Avx` both sitting at 1 says nothing about each other.
178    /// Widening the scale means revisiting [`MAX_LEVEL`].
179    ///
180    /// This is tract's own ladder, one step per feature it dispatches on; it is not the psABI's
181    /// `x86-64-v1..v4`, which bundles features into four named levels. Only the word is
182    /// borrowed, never the numbering.
183    pub const fn level(&self) -> u8 {
184        match self {
185            // The plain architecture is the floor every ladder rises from.
186            Isa::Arm | Isa::Aarch64 | Isa::X86_64 | Isa::RiscV64 | Isa::Wasm32 => 0,
187            // x86: each generation subsumes the last, AMX above the VNNI it needs alongside it.
188            Isa::X86_64Avx => 1,
189            Isa::X86_64Avx2 | Isa::X86_64Fma | Isa::X86_64F16c => 2,
190            Isa::X86_64Avx512f | Isa::X86_64AvxVnni => 3,
191            Isa::X86_64Avx512Vnni => 4,
192            // A native-f16 kernel beats the f32 round-trip a plain AVX-512 core is left with,
193            // so this sits a step above the set it extends.
194            Isa::X86_64Avx512Fp16 => 4,
195            Isa::X86_64AmxInt8 | Isa::X86_64AmxBf16 => 5,
196            // riscv: the vector unit, then the width the wide tiles need on top of it, then
197            // native f16, which the parts wide enough to want it are the ones that ship.
198            Isa::RiscV64V => 1,
199            Isa::RiscV64Vlen256 => 2,
200            Isa::RiscV64Zvfh => 3,
201            // arm: NEON is the armv7 step above bare VFP, and baseline on aarch64 where the
202            // ladder continues through the matrix extensions.
203            Isa::ArmNeon => 1,
204            Isa::Aarch64Fp16 | Isa::Aarch64DotProd => 2,
205            Isa::Aarch64Sve2 => 3,
206            Isa::Aarch64Sme | Isa::Aarch64Sme2 => 4,
207            Isa::Aarch64AppleAmx => 5,
208            // relaxed-simd brings the fused multiply-add the baseline proposal lacks.
209            Isa::Wasm32Simd128 => 0,
210            Isa::Wasm32RelaxedSimd => 1,
211        }
212    }
213
214    /// Whether the feature brings f16 arithmetic, rather than the f16 conversions an f32 round
215    /// trip needs. Only on such a machine is a round trip second best.
216    pub const fn fp16_arithmetic(&self) -> bool {
217        matches!(self, Isa::Aarch64Fp16 | Isa::X86_64Avx512Fp16 | Isa::RiscV64Zvfh)
218    }
219
220    /// Whose instruction set this belongs to. Every feature belongs to exactly one architecture —
221    /// that is what makes a set holding two of them a contradiction rather than a rich machine,
222    /// and what lets `TRACT_CPU_ISA` reject a feature the architecture cannot have.
223    pub const fn arch(&self) -> Arch {
224        match self {
225            Isa::Arm | Isa::ArmNeon => Arch::Arm,
226            Isa::Aarch64
227            | Isa::Aarch64Fp16
228            | Isa::Aarch64DotProd
229            | Isa::Aarch64Sve2
230            | Isa::Aarch64Sme
231            | Isa::Aarch64Sme2
232            | Isa::Aarch64AppleAmx => Arch::Aarch64,
233            Isa::X86_64
234            | Isa::X86_64Avx
235            | Isa::X86_64Avx2
236            | Isa::X86_64Fma
237            | Isa::X86_64F16c
238            | Isa::X86_64Avx512f
239            | Isa::X86_64Avx512Vnni
240            | Isa::X86_64Avx512Fp16
241            | Isa::X86_64AvxVnni
242            | Isa::X86_64AmxInt8
243            | Isa::X86_64AmxBf16 => Arch::X86_64,
244            Isa::RiscV64 | Isa::RiscV64V | Isa::RiscV64Vlen256 | Isa::RiscV64Zvfh => Arch::RiscV64,
245            Isa::Wasm32 | Isa::Wasm32Simd128 | Isa::Wasm32RelaxedSimd => Arch::Wasm32Simd128,
246        }
247    }
248
249    /// Whether this is a plain architecture rather than a feature on top of one.
250    pub const fn is_arch(&self) -> bool {
251        matches!(self, Isa::Arm | Isa::Aarch64 | Isa::X86_64 | Isa::RiscV64 | Isa::Wasm32)
252    }
253
254    /// The set member that stands for `arch` itself.
255    pub const fn of_arch(arch: Arch) -> Isa {
256        match arch {
257            Arch::Arm => Isa::Arm,
258            Arch::Aarch64 => Isa::Aarch64,
259            Arch::X86_64 => Isa::X86_64,
260            Arch::RiscV64 => Isa::RiscV64,
261            Arch::Wasm32Simd128 => Isa::Wasm32,
262        }
263    }
264}
265
266impl fmt::Display for Isa {
267    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
268        f.write_str(self.name())
269    }
270}
271
272/// What one machine offers: the architecture it is, and the features it adds on top. A set is
273/// well formed when it holds exactly one architecture — [`IsaSet::of_arch`] is how to start one,
274/// and [`IsaSet::arch`] reads it back.
275#[derive(Copy, Clone, Default, PartialEq, Eq)]
276pub struct IsaSet(u32);
277
278impl IsaSet {
279    pub const fn empty() -> IsaSet {
280        IsaSet(0)
281    }
282
283    /// The plain architecture, with none of its features yet.
284    pub const fn of_arch(arch: Arch) -> IsaSet {
285        IsaSet::empty().with(Isa::of_arch(arch))
286    }
287
288    /// The architecture this set speaks for, `None` for the empty set. Two architectures in one
289    /// set is a contradiction no machine can be, so the first one found wins and the set should
290    /// never have been built that way — see [`IsaSet::of_arch`].
291    pub fn arch(self) -> Option<Arch> {
292        self.iter().find(|i| i.is_arch()).map(|i| i.arch())
293    }
294
295    pub const fn with(self, isa: Isa) -> IsaSet {
296        IsaSet(self.0 | 1 << isa as u32)
297    }
298
299    pub const fn without(self, isa: Isa) -> IsaSet {
300        IsaSet(self.0 & !(1 << isa as u32))
301    }
302
303    pub const fn has(self, isa: Isa) -> bool {
304        self.0 & (1 << isa as u32) != 0
305    }
306
307    /// The machine an architecture's ladder reaches at `level`: that architecture, plus every
308    /// feature of it at or below the step. Not every real part is one of these -- a feature can
309    /// ship without its level-mates -- but these are the generations a kernel set is written
310    /// against, so they are what a matrix column, and a test asking "on which machines", mean by
311    /// a machine.
312    /// Whether this machine computes in f16 at all, which is what says whether an f32 round trip
313    /// is a compromise or simply what there is.
314    pub fn fp16_arithmetic(self) -> bool {
315        self.iter().any(|i| i.fp16_arithmetic())
316    }
317
318    /// The one word a report calls this rung by: the step, not the features it bundles, so
319    /// `avx2,fma,f16c` is `fma` and the aarch64 baseline is `neon`. Two architectures may share a
320    /// nickname -- what tells them apart is the architecture printed beside it.
321    pub fn nickname(self) -> &'static str {
322        let Some(arch) = self.arch() else { return "none" };
323        match (arch, self.level()) {
324            (Arch::Arm, 0) => "vfp",
325            (Arch::Arm, _) => "neon",
326            (Arch::Aarch64, 0 | 1) => "neon",
327            (Arch::Aarch64, 2) => "fp16",
328            (Arch::Aarch64, 3) => "sve2",
329            (Arch::Aarch64, 4) => "sme",
330            (Arch::Aarch64, _) => "amx",
331            (Arch::X86_64, 0) => "sse2",
332            (Arch::X86_64, 1) => "avx",
333            (Arch::X86_64, 2) => "fma",
334            (Arch::X86_64, 3) => "avx512",
335            (Arch::X86_64, 4) => "fp16",
336            (Arch::X86_64, _) => "amx",
337            (Arch::RiscV64, 0) => "rv64",
338            (Arch::RiscV64, 1) => "rvv",
339            (Arch::RiscV64, 2) => "vlen256",
340            (Arch::RiscV64, _) => "zvfh",
341            (Arch::Wasm32Simd128, 0) => "simd",
342            (Arch::Wasm32Simd128, _) => "relaxed",
343        }
344    }
345
346    /// The rung of its architecture's ladder this machine sits on: the most capable feature it
347    /// offers, whether or not any kernel is written against it.
348    pub fn level(self) -> u8 {
349        self.iter().map(|i| i.level()).max().unwrap_or(0)
350    }
351
352    pub fn ladder(arch: Arch, level: u8) -> IsaSet {
353        let mut set = IsaSet::of_arch(arch);
354        for isa in Isa::ALL {
355            if isa.arch() == arch && isa.level() <= level {
356                set = set.with(isa);
357            }
358        }
359        set
360    }
361
362    /// Every generation of every architecture tract has a kernel tree for, as
363    /// [`IsaSet::ladder`] defines one. What a matrix enumerates, and what a question about all
364    /// machines at once ranges over.
365    pub fn every_ladder() -> impl Iterator<Item = IsaSet> {
366        Arch::ALL.into_iter().flat_map(|arch| (0..=MAX_LEVEL).map(move |l| IsaSet::ladder(arch, l)))
367    }
368    pub fn iter(self) -> impl Iterator<Item = Isa> {
369        Isa::ALL.into_iter().filter(move |i| self.has(*i))
370    }
371}
372
373impl fmt::Debug for IsaSet {
374    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
375        if self.0 == 0 {
376            return f.write_str("-");
377        }
378        f.write_str(&self.iter().map(|i| i.name()).collect::<Vec<_>>().join(","))
379    }
380}
381
382/// What a kernel needs from the instruction set to be able to run at all. Whether it is the
383/// *best* thing that can run is a different question, and not this type's business.
384#[derive(Copy, Clone, PartialEq, Eq, Hash)]
385pub struct IsaReq {
386    /// Every one of these must be present.
387    pub needs: &'static [Isa],
388}
389
390impl IsaReq {
391    /// Runs anywhere its arch does.
392    pub const ANY: IsaReq = IsaReq { needs: &[] };
393
394    pub const fn needing(self, needs: &'static [Isa]) -> IsaReq {
395        IsaReq { needs }
396    }
397
398    pub fn satisfied_by(&self, set: IsaSet) -> bool {
399        self.needs.iter().all(|i| set.has(*i))
400    }
401
402    /// The most capable lineage step this kernel sits in, feeding the default half of
403    /// [`crate::mmm::MatMatMulKer::preference`].
404    pub fn level(&self) -> u8 {
405        self.needs.iter().map(|i| i.level()).max().unwrap_or(0)
406    }
407}
408
409/// What one step up the instruction-set ladder is worth when nothing else is known. A kernel
410/// written against a more capable set is assumed better than one written against a less capable
411/// one; this is the size of that assumption, in the same units as a declared `boost`.
412///
413/// A declared boost is how an exception to that assumption is spelled, so it has to cover the
414/// ladder steps it disagrees with -- and only those: a kernel whose competition sits in its own
415/// level disagrees with no step and needs no magnitude at all. Spell the ones that do cross levels
416/// with [`peer_of`] instead of a literal, so the claim survives a ladder that grows a step;
417/// [`NEVER_PREFERRED`] is the far end of the range, for a kernel that must lose every tie.
418pub const LEVEL_BOOST: isize = 10;
419
420/// The deepest step any ladder reaches, bounding what a boost has to be able to cross.
421pub const MAX_LEVEL: u8 = 5;
422
423/// A boost that cancels the ladder between two steps, for a kernel written for `mine` but
424/// measured as a peer of the kernels written for `theirs`. The relation is the claim; the
425/// number is derived from it, and stays right when a step is inserted between the two.
426pub const fn peer_of(mine: Isa, theirs: Isa) -> isize {
427    (theirs.level() as isize - mine.level() as isize) * LEVEL_BOOST
428}
429
430/// A boost no level can make up for, for a kernel that is runnable here but must never be
431/// chosen unless something outside the preference order asks for it by name.
432pub const NEVER_PREFERRED: isize = -(LEVEL_BOOST * MAX_LEVEL as isize) - 1;
433
434impl fmt::Debug for IsaReq {
435    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
436        let needs = self.needs.iter().map(|i| i.name()).collect::<Vec<_>>().join("+");
437        if needs.is_empty() {
438            f.write_str("any")?;
439        } else {
440            f.write_str(&needs)?;
441        }
442        Ok(())
443    }
444}
445
446/// What this machine has: probed once, then edited by `TRACT_CPU_ISA`. Only the running
447/// architecture's tree is asked — a foreign tree compiled in for enumeration would be probing
448/// this host about features it cannot have.
449pub fn native() -> IsaSet {
450    static NATIVE: OnceLock<IsaSet> = OnceLock::new();
451    *NATIVE.get_or_init(|| {
452        let set = forced(probe());
453        log::debug!("ISA: {set:?}");
454        set
455    })
456}
457
458fn probe() -> IsaSet {
459    #[cfg(target_arch = "arm")]
460    return crate::arm32::isa_set();
461    #[cfg(target_arch = "aarch64")]
462    return crate::arm64::isa_set();
463    #[cfg(target_arch = "x86_64")]
464    return crate::x86_64::isa_set();
465    #[cfg(target_arch = "riscv64")]
466    return crate::riscv64::isa_set();
467    #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))]
468    return crate::wasm::isa_set();
469    // An architecture with no kernel tree, or wasm without simd128: nothing to declare, and no
470    // architecture to name either, since none of its kernels would be reachable anyway.
471    #[cfg(not(any(
472        target_arch = "arm",
473        target_arch = "aarch64",
474        target_arch = "x86_64",
475        target_arch = "riscv64",
476        all(target_arch = "wasm32", target_feature = "simd128")
477    )))]
478    IsaSet::empty()
479}
480
481impl std::str::FromStr for IsaSet {
482    type Err = tract_data::internal::TractError;
483
484    /// The comma-separated form a machine prints itself as, so a machine named in a report can be
485    /// pasted back in. The architecture token comes along with any feature that implies it, so
486    /// `avx512f` alone names an x86_64 machine with `avx512f` and nothing else.
487    fn from_str(spec: &str) -> tract_data::internal::TractResult<IsaSet> {
488        let mut set = IsaSet::empty();
489        for token in spec.split(',').map(str::trim).filter(|t| !t.is_empty()) {
490            let Some(isa) = Isa::from_name(token) else {
491                tract_data::internal::bail!("{token:?} is no instruction set tract knows")
492            };
493            if let Some(arch) = set.arch()
494                && isa.arch() != arch
495            {
496                tract_data::internal::bail!(
497                    "{token} belongs to {}, and this machine is {arch}: a machine is one \
498                     architecture",
499                    isa.arch()
500                )
501            }
502            set = set.with(Isa::of_arch(isa.arch())).with(isa);
503        }
504        if set == IsaSet::empty() {
505            tract_data::internal::bail!("{spec:?} names no instruction set")
506        }
507        Ok(set)
508    }
509}
510
511/// `TRACT_CPU_ISA=+sve2,-fp16` edits the probed set, so one knob covers every feature and a
512/// cohort this machine is not can be asked what it would dispatch. Nothing checks that the
513/// result is a CPU that could exist: asking for avx512f without fma describes no hardware, and
514/// dispatch will answer for it anyway.
515/// Apply `TRACT_CPU_ISA` to `set`. A token naming another architecture's feature is a hard error
516/// rather than a warning: it cannot do what it asks for, and silently doing nothing has it look
517/// like the feature was tried and made no difference. To reason about another architecture, start
518/// from its own set — [`IsaSet::of_arch`], which is what [`crate::platform::inspect`] does.
519pub(crate) fn forced(mut set: IsaSet) -> IsaSet {
520    let Some(spec) = crate::knobs::TRACT_CPU_ISA.get() else { return set };
521    for token in spec.split(',').map(str::trim).filter(|t| !t.is_empty()) {
522        let (add, name) = match token.split_at(1) {
523            ("+", name) => (true, name),
524            ("-", name) => (false, name),
525            _ => (true, token),
526        };
527        let Some(isa) = Isa::from_name(name) else {
528            log::warn!("TRACT_CPU_ISA: unknown feature {name:?}, ignored");
529            continue;
530        };
531        if let Some(arch) = set.arch() {
532            assert!(
533                isa.arch() == arch,
534                "TRACT_CPU_ISA: {name} belongs to {}, and this set is {arch} — a machine is one \
535                 architecture, so the token cannot apply",
536                isa.arch()
537            );
538        }
539        set = if add { set.with(isa) } else { set.without(isa) };
540    }
541    set
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    /// `MAX_LEVEL` bounds what a declared boost has to be able to cross, so a ladder step added
549    /// beyond it would make [`NEVER_PREFERRED`] and every [`peer_of`] claim too small.
550    #[test]
551    fn ladder_stays_within_the_bound() {
552        for isa in Isa::ALL {
553            assert!(
554                isa.level() <= MAX_LEVEL,
555                "{isa} is level {}, past MAX_LEVEL {MAX_LEVEL}",
556                isa.level()
557            );
558        }
559    }
560
561    #[test]
562    fn peer_of_cancels_the_steps_between() {
563        assert_eq!(peer_of(Isa::X86_64Fma, Isa::X86_64Avx512f), LEVEL_BOOST);
564        assert_eq!(peer_of(Isa::X86_64Avx, Isa::X86_64Avx512Vnni), 3 * LEVEL_BOOST);
565        assert_eq!(peer_of(Isa::X86_64Avx512f, Isa::X86_64AvxVnni), 0);
566    }
567}