strided_perm/host.rs
1//! Host class and the serial copy rate of each class.
2//!
3//! The rates of the strided stack are keyed by the kernel dispatch the machine
4//! takes, not by the CPU model. `HostClass` and `HostClass::detect` are the same
5//! enum and the same detection as in `threadteam` (`threadteam::HostClass`), the
6//! `tcontract` and the `faer` cost tables, and are kept in sync with them by
7//! hand; this crate owns the copy of the strided workspace (`strided-basic`
8//! reuses it for its own table, which sits above this crate).
9
10use std::sync::OnceLock;
11
12/// The class of the machine, keyed by the kernel dispatch it will take.
13///
14/// Kept in sync with `threadteam::HostClass`. Detected once per process by
15/// [`HostClass::detect`].
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum HostClass {
18 /// `x86_64` with AVX-512F at run time (`x86_64-avx512`).
19 X86_64Avx512,
20 /// `x86_64` with AVX2 and FMA, without AVX-512F (`x86_64-avx2`).
21 X86_64Avx2,
22 /// Any other `x86_64` (`x86_64-scalar`).
23 X86_64Scalar,
24 /// `aarch64` on macOS (`aarch64-apple`).
25 Aarch64Apple,
26 /// Any other `aarch64` (`aarch64`).
27 Aarch64,
28 /// Everything else (`generic`).
29 Generic,
30}
31
32impl HostClass {
33 /// The class of this host, detected at the first call and cached
34 /// (`OnceLock`; `is_x86_feature_detected!` on x86_64).
35 pub fn detect() -> Self {
36 static CLASS: OnceLock<HostClass> = OnceLock::new();
37 *CLASS.get_or_init(Self::detect_uncached)
38 }
39
40 #[cfg(target_arch = "x86_64")]
41 fn detect_uncached() -> Self {
42 if std::arch::is_x86_feature_detected!("avx512f") {
43 HostClass::X86_64Avx512
44 } else if std::arch::is_x86_feature_detected!("avx2")
45 && std::arch::is_x86_feature_detected!("fma")
46 {
47 HostClass::X86_64Avx2
48 } else {
49 HostClass::X86_64Scalar
50 }
51 }
52
53 #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
54 fn detect_uncached() -> Self {
55 HostClass::Aarch64Apple
56 }
57
58 #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
59 fn detect_uncached() -> Self {
60 HostClass::Aarch64
61 }
62
63 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
64 fn detect_uncached() -> Self {
65 HostClass::Generic
66 }
67
68 /// The class name used in the docs and the tables.
69 pub const fn name(self) -> &'static str {
70 match self {
71 HostClass::X86_64Avx512 => "x86_64-avx512",
72 HostClass::X86_64Avx2 => "x86_64-avx2",
73 HostClass::X86_64Scalar => "x86_64-scalar",
74 HostClass::Aarch64Apple => "aarch64-apple",
75 HostClass::Aarch64 => "aarch64",
76 HostClass::Generic => "generic",
77 }
78 }
79}
80
81/// Serial throughput of a copy in nanoseconds per element, of a host class.
82///
83/// * `x86_64-avx512`: 0.171, the median of `seq` time per element of the strided
84/// `copy` probe at 16384, 32768 and 65536 elements (L2 resident; 0.171, 0.131,
85/// 0.201) on an AMD Ryzen AI 9 HX 470 at 2.0 GHz (boost off), `faer-benchmark`
86/// `data/probes/20261026-awake-team-linux/tables-confirm-exec3.md`.
87/// * `aarch64-apple`: 0.090, Apple M5 Max (thresholds.md section 3.2).
88/// * every other class: the `generic` row, the fastest known value (the minimum
89/// over the measured classes), which keeps work serial: 0.090.
90///
91/// A row's rates and the dispatch costs of the same class in `threadteam` come
92/// from the same machine in the same clock state (boost on or off moves both by
93/// about 1.66x and keeps their ratio within a few percent).
94pub const fn copy_ns_per_element_of(class: HostClass) -> f64 {
95 match class {
96 HostClass::X86_64Avx512 => 0.171,
97 HostClass::Aarch64Apple => 0.090,
98 HostClass::X86_64Avx2
99 | HostClass::X86_64Scalar
100 | HostClass::Aarch64
101 | HostClass::Generic => 0.090,
102 }
103}
104
105/// Serial throughput of a copy in nanoseconds per element on this host
106/// ([`copy_ns_per_element_of`] of [`HostClass::detect`]).
107///
108/// The `Copy` class of the strided stack (`strided_basic::execution::KernelClass`
109/// holds the class table; this crate sits below it and owns the one rate it
110/// needs). `copy_into*` multiply it by the element count and ask the token
111/// whether one section pays.
112pub fn copy_ns_per_element() -> f64 {
113 copy_ns_per_element_of(HostClass::detect())
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn rows_per_class() {
122 assert_eq!(copy_ns_per_element_of(HostClass::X86_64Avx512), 0.171);
123 assert_eq!(copy_ns_per_element_of(HostClass::Aarch64Apple), 0.090);
124 for c in [
125 HostClass::X86_64Avx2,
126 HostClass::X86_64Scalar,
127 HostClass::Aarch64,
128 HostClass::Generic,
129 ] {
130 assert_eq!(copy_ns_per_element_of(c), 0.090, "{}", c.name());
131 }
132 for (c, name) in [
133 (HostClass::X86_64Avx512, "x86_64-avx512"),
134 (HostClass::X86_64Avx2, "x86_64-avx2"),
135 (HostClass::X86_64Scalar, "x86_64-scalar"),
136 (HostClass::Aarch64Apple, "aarch64-apple"),
137 (HostClass::Aarch64, "aarch64"),
138 (HostClass::Generic, "generic"),
139 ] {
140 assert_eq!(c.name(), name);
141 }
142 assert_eq!(HostClass::detect(), HostClass::detect());
143 assert_eq!(
144 copy_ns_per_element(),
145 copy_ns_per_element_of(HostClass::detect())
146 );
147 }
148}