Skip to main content

tcontract_exec/
host.rs

1//! Host class and the serial GEMM rate of each class.
2//!
3//! The rate the width and batch rules use is keyed by the kernel dispatch the
4//! machine takes, not by the CPU model. `HostClass` and `HostClass::detect` are
5//! the same enum and the same detection as `threadteam::HostClass`, `strided`'s
6//! and `faer`'s cost tables, and are kept in sync with them by hand; the
7//! `tcontract` crate reuses this copy.
8//!
9//! | class | ns per flop | multiply-adds per ns | source |
10//! | --- | --- | --- | --- |
11//! | `x86_64-avx512` | 0.032 | 15.6 | AMD Ryzen AI 9 HX 470 at 2.0 GHz (boost off): GEMM f64 0.0641 ns per MAC (`faer-benchmark` `data/probes/20261026-awake-team-linux/tables-consts-exec3.md`, `gemm_per_mac`; the `tc_matmul` seq times of `tables-confirm-exec3.md` agree: 0.0678, 0.0657, 0.0644 ns per MAC at n = 48, 64, 96), halved to a flop (a MAC is two flops), then 1 / (2 * 0.032) = 15.6 |
12//! | `aarch64-apple` | 0.0185 | 27 | Apple M5 Max, the serial GEMM rate (about 54 GFLOP/s, threadteam `docs/design/thresholds.md` section 3.2) |
13//! | every other class | 0.0185 | 27 | the `generic` row: the fastest known value of each column (the minimum of the ns per flop, the maximum of the multiply-adds per ns over the measured classes); it keeps work serial |
14//!
15//! The rate of a class and the dispatch costs of the same class in `threadteam`
16//! come from the same machine in the same clock state: boost on or off moves both
17//! by about 1.66x and leaves their ratio stable within a few percent
18//! (`raw-exec3-boost`, `tables-*-exec3-boost.md`).
19
20use std::sync::OnceLock;
21
22/// The class of the machine, keyed by the kernel dispatch it will take.
23///
24/// Kept in sync with `threadteam::HostClass`. Detected once per process by
25/// [`HostClass::detect`].
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum HostClass {
28    /// `x86_64` with AVX-512F at run time (`x86_64-avx512`).
29    X86_64Avx512,
30    /// `x86_64` with AVX2 and FMA, without AVX-512F (`x86_64-avx2`).
31    X86_64Avx2,
32    /// Any other `x86_64` (`x86_64-scalar`).
33    X86_64Scalar,
34    /// `aarch64` on macOS (`aarch64-apple`).
35    Aarch64Apple,
36    /// Any other `aarch64` (`aarch64`).
37    Aarch64,
38    /// Everything else (`generic`).
39    Generic,
40}
41
42impl HostClass {
43    /// The class of this host, detected at the first call and cached
44    /// (`OnceLock`; `is_x86_feature_detected!` on x86_64).
45    pub fn detect() -> Self {
46        static CLASS: OnceLock<HostClass> = OnceLock::new();
47        *CLASS.get_or_init(Self::detect_uncached)
48    }
49
50    #[cfg(target_arch = "x86_64")]
51    fn detect_uncached() -> Self {
52        if std::arch::is_x86_feature_detected!("avx512f") {
53            HostClass::X86_64Avx512
54        } else if std::arch::is_x86_feature_detected!("avx2")
55            && std::arch::is_x86_feature_detected!("fma")
56        {
57            HostClass::X86_64Avx2
58        } else {
59            HostClass::X86_64Scalar
60        }
61    }
62
63    #[cfg(all(target_arch = "aarch64", target_os = "macos"))]
64    fn detect_uncached() -> Self {
65        HostClass::Aarch64Apple
66    }
67
68    #[cfg(all(target_arch = "aarch64", not(target_os = "macos")))]
69    fn detect_uncached() -> Self {
70        HostClass::Aarch64
71    }
72
73    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
74    fn detect_uncached() -> Self {
75        HostClass::Generic
76    }
77
78    /// The class name used in the docs and the tables.
79    pub const fn name(self) -> &'static str {
80        match self {
81            HostClass::X86_64Avx512 => "x86_64-avx512",
82            HostClass::X86_64Avx2 => "x86_64-avx2",
83            HostClass::X86_64Scalar => "x86_64-scalar",
84            HostClass::Aarch64Apple => "aarch64-apple",
85            HostClass::Aarch64 => "aarch64",
86            HostClass::Generic => "generic",
87        }
88    }
89}
90
91/// Estimated serial time per real flop of a GEMM item on a host class (the
92/// table in the module documentation).
93pub const fn ns_per_flop_of(class: HostClass) -> f64 {
94    match class {
95        HostClass::X86_64Avx512 => 0.032,
96        HostClass::Aarch64Apple
97        | HostClass::X86_64Avx2
98        | HostClass::X86_64Scalar
99        | HostClass::Aarch64
100        | HostClass::Generic => 0.0185,
101    }
102}
103
104/// Serial multiply-adds per nanosecond of one GEMM item on a host class: the
105/// same rate as [`ns_per_flop_of`] (`1 / ns_per_flop / 2`), rounded as the
106/// table in the module documentation states it.
107pub const fn muladds_per_ns_of(class: HostClass) -> f64 {
108    match class {
109        HostClass::X86_64Avx512 => 15.6,
110        HostClass::Aarch64Apple
111        | HostClass::X86_64Avx2
112        | HostClass::X86_64Scalar
113        | HostClass::Aarch64
114        | HostClass::Generic => 27.0,
115    }
116}
117
118/// Estimated serial time per real flop on this host (`ns_per_flop_of` of
119/// [`HostClass::detect`]); the one default the width choice uses everywhere. The
120/// width itself is the token's decision (`Exec::width_for` over this estimate).
121pub fn ns_per_flop() -> f64 {
122    ns_per_flop_of(HostClass::detect())
123}
124
125/// Serial multiply-adds per nanosecond on this host (`muladds_per_ns_of` of
126/// [`HostClass::detect`]).
127pub fn muladds_per_ns() -> f64 {
128    muladds_per_ns_of(HostClass::detect())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn the_two_rates_of_a_class_agree() {
137        for c in [
138            HostClass::X86_64Avx512,
139            HostClass::X86_64Avx2,
140            HostClass::X86_64Scalar,
141            HostClass::Aarch64Apple,
142            HostClass::Aarch64,
143            HostClass::Generic,
144        ] {
145            let exact = 1.0 / ns_per_flop_of(c) / 2.0;
146            assert!(
147                (muladds_per_ns_of(c) / exact - 1.0).abs() < 0.01,
148                "{}",
149                c.name()
150            );
151        }
152    }
153
154    #[test]
155    fn rows_per_class() {
156        assert_eq!(ns_per_flop_of(HostClass::X86_64Avx512), 0.032);
157        assert_eq!(muladds_per_ns_of(HostClass::X86_64Avx512), 15.6);
158        for c in [
159            HostClass::Aarch64Apple,
160            HostClass::X86_64Avx2,
161            HostClass::X86_64Scalar,
162            HostClass::Aarch64,
163            HostClass::Generic,
164        ] {
165            assert_eq!(ns_per_flop_of(c), 0.0185, "{}", c.name());
166            assert_eq!(muladds_per_ns_of(c), 27.0, "{}", c.name());
167        }
168        // `generic` is the fastest known value of each column.
169        let g = HostClass::Generic;
170        assert!(ns_per_flop_of(g) <= ns_per_flop_of(HostClass::X86_64Avx512));
171        assert!(muladds_per_ns_of(g) >= muladds_per_ns_of(HostClass::X86_64Avx512));
172        assert_eq!(HostClass::detect(), HostClass::detect());
173        assert_eq!(ns_per_flop(), ns_per_flop_of(HostClass::detect()));
174        assert_eq!(muladds_per_ns(), muladds_per_ns_of(HostClass::detect()));
175    }
176}