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}