metrale_bench/hardware/
limits.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: A hardware class's benchmark limits: the numbers certification and
4//! the hardware policies judge a box by, declared in `kernels/<hw>/HARDWARE.toml`
5//! under `[benchmarks.limits]`.
6//!
7//! Owner: bench hardware.
8//! Invariants:
9//! - [`limits`] returns `Some` only with all four sub-tables present, no unknown
10//!   key, every float finite, resume below park, positive tolerances, a free
11//!   fraction inside `(0, 1)` and non-zero allowances.
12//!
13//! The numbers are facts about a box class, so they live beside its other facts
14//! rather than in Rust constants. A class without `[benchmarks.limits]` has no
15//! limits, and callers say so rather than borrowing another class's.
16//! `HARDWARE.toml` is one of the configs hashed into the class's target closures
17//! (`gate::taxon::configs`), so changing a limit re-opens the class's gates.
18
19use std::path::Path;
20
21use anyhow::{Context, Result, bail};
22
23/// 2026-09-26: `[benchmarks.limits]` of one hardware class.
24#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct Limits {
27    pub thermal: ThermalEnvelope,
28    pub memory: MemoryLimits,
29    pub timing: TimingLimits,
30    pub equivalence: EquivalenceLimits,
31}
32
33/// 2026-09-26: `[benchmarks.limits.thermal]`.
34#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct ThermalEnvelope {
37    /// 2026-09-26: The hottest-chassis-zone reading at which certification parks a
38    /// node, °C; also the precheck's chassis ceiling ([`super::policy::TempCeilings::of`]).
39    pub chassis_park_c: f64,
40    /// 2026-09-26: The reading a parked node must fall to before it resumes, °C.
41    pub chassis_resume_c: f64,
42    /// 2026-09-26: The largest hottest-zone difference at which two boxes of this
43    /// class are still one box for Speed numbers, °C.
44    pub chassis_equivalence_delta_c: f64,
45    /// 2026-09-26: The precheck's GPU die ceiling, °C.
46    pub gpu_ceiling_c: f64,
47}
48
49/// 2026-09-26: `[benchmarks.limits.memory]`.
50#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct MemoryLimits {
53    /// 2026-09-26: `MemAvailable / MemTotal` a gate needs before its server may start.
54    pub min_free_fraction: f64,
55}
56
57/// 2026-09-26: `[benchmarks.limits.timing]`, seconds.
58#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct TimingLimits {
61    /// 2026-09-26: Wall time a self-served gate may spend before its first sample
62    /// (server start plus checkpoint load) before its deadline counts.
63    pub serve_allowance_s: u64,
64    /// 2026-09-26: How long a started server may take to name its model.
65    pub boot_timeout_s: u64,
66    /// 2026-09-26: Added to a shard's share of the draw when planning it.
67    pub shard_overhead_s: u64,
68    /// 2026-09-26: The least a shard is planned at, however thin the slice.
69    pub shard_floor_s: u64,
70    /// 2026-09-26: Time a remote node may spend building the anchor before its
71    /// unit's deadline counts.
72    pub build_allowance_s: u64,
73}
74
75/// 2026-09-26: `[benchmarks.limits.equivalence]`: the non-thermal half of "one box".
76#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct EquivalenceLimits {
79    /// 2026-09-26: Largest `|a - b| / max(a, b)` on the clock ceiling.
80    pub clock_spread: f64,
81    /// 2026-09-26: Largest `|a - b| / max(a, b)` on memory.
82    pub mem_spread: f64,
83}
84
85#[derive(serde::Deserialize)]
86struct HardwareToml {
87    benchmarks: Option<Benchmarks>,
88}
89
90#[derive(serde::Deserialize)]
91struct Benchmarks {
92    limits: Option<Limits>,
93}
94
95/// 2026-09-26: The limits `hardware` declares, `None` when its `HARDWARE.toml` has
96/// no `[benchmarks.limits]`.
97///
98/// # Errors
99/// A missing or unparseable `HARDWARE.toml`, a `[benchmarks.limits]` missing one of
100/// its tables or carrying a stray key, a non-finite value, or values that
101/// contradict themselves (resume at or above park, a non-positive tolerance, a
102/// fraction outside `(0, 1)`, a zero serve, boot or build allowance).
103pub fn limits(root: &Path, hardware: &str) -> Result<Option<Limits>> {
104    let path = root.join("kernels").join(hardware).join("HARDWARE.toml");
105    let text =
106        std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
107    let parsed: HardwareToml =
108        toml::from_str(&text).with_context(|| format!("parsing {}", path.display()))?;
109    let Some(l) = parsed.benchmarks.and_then(|b| b.limits) else {
110        return Ok(None);
111    };
112    let at = |what: &str| format!("{}: [benchmarks.limits] {what}", path.display());
113    let t = l.thermal;
114    for (name, v) in [
115        ("thermal.chassis_park_c", t.chassis_park_c),
116        ("thermal.chassis_resume_c", t.chassis_resume_c),
117        (
118            "thermal.chassis_equivalence_delta_c",
119            t.chassis_equivalence_delta_c,
120        ),
121        ("thermal.gpu_ceiling_c", t.gpu_ceiling_c),
122        ("memory.min_free_fraction", l.memory.min_free_fraction),
123        ("equivalence.clock_spread", l.equivalence.clock_spread),
124        ("equivalence.mem_spread", l.equivalence.mem_spread),
125    ] {
126        if !v.is_finite() {
127            bail!("{}", at(&format!("{name} must be finite")));
128        }
129    }
130    if t.chassis_resume_c >= t.chassis_park_c {
131        bail!(
132            "{}",
133            at(&format!(
134                "thermal.chassis_resume_c ({}) must be below chassis_park_c ({}) — without \
135                 hysteresis a box flaps on the line",
136                t.chassis_resume_c, t.chassis_park_c
137            ))
138        );
139    }
140    if t.chassis_equivalence_delta_c <= 0.0 {
141        bail!(
142            "{}",
143            at("thermal.chassis_equivalence_delta_c must be positive")
144        );
145    }
146    let f = l.memory.min_free_fraction;
147    if !(f > 0.0 && f < 1.0) {
148        bail!("{}", at("memory.min_free_fraction must be inside (0, 1)"));
149    }
150    if l.equivalence.clock_spread <= 0.0 || l.equivalence.mem_spread <= 0.0 {
151        bail!("{}", at("equivalence spreads must be positive"));
152    }
153    let ti = l.timing;
154    if ti.serve_allowance_s == 0 || ti.boot_timeout_s == 0 || ti.build_allowance_s == 0 {
155        bail!("{}", at("timing allowances must be positive"));
156    }
157    Ok(Some(l))
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    const GB10: &str = "[benchmarks.limits.thermal]\nchassis_park_c = 80\nchassis_resume_c = 70\n\
165        chassis_equivalence_delta_c = 15\ngpu_ceiling_c = 75\n[benchmarks.limits.memory]\n\
166        min_free_fraction = 0.85\n[benchmarks.limits.timing]\nserve_allowance_s = 600\n\
167        boot_timeout_s = 900\nshard_overhead_s = 420\nshard_floor_s = 300\nbuild_allowance_s = 1800\n\
168        [benchmarks.limits.equivalence]\nclock_spread = 0.01\nmem_spread = 0.05\n";
169
170    fn root_with(hw: &str, body: &str) -> std::path::PathBuf {
171        let d = std::env::temp_dir().join(format!("limits-{}-{}", std::process::id(), hw));
172        std::fs::create_dir_all(d.join("kernels").join(hw)).unwrap();
173        std::fs::write(d.join("kernels").join(hw).join("HARDWARE.toml"), body).unwrap();
174        d
175    }
176
177    #[test]
178    fn declared_limits_are_read_and_absent_ones_are_none() {
179        let r = root_with("x1", &format!("[hardware]\nname = \"x1\"\n{GB10}"));
180        let l = limits(&r, "x1").unwrap().unwrap();
181        assert_eq!(
182            (l.thermal.chassis_park_c, l.thermal.chassis_resume_c),
183            (80.0, 70.0)
184        );
185        assert_eq!(l.memory.min_free_fraction, 0.85);
186        assert_eq!(l.timing.serve_allowance_s, 600);
187        assert_eq!(l.equivalence.clock_spread, 0.01);
188        let r = root_with("x2", "[hardware]\nname = \"x2\"\n");
189        assert_eq!(limits(&r, "x2").unwrap(), None);
190        assert!(limits(&r, "nope").is_err(), "no file is an error, not None");
191    }
192
193    /// 2026-09-26: Negative controls: a missing sub-table, a stray key and each
194    /// contradiction are refused.
195    #[test]
196    fn a_wrong_table_is_refused_by_name() {
197        let r = root_with(
198            "y1",
199            &GB10.replace("[benchmarks.limits.memory]\nmin_free_fraction = 0.85\n", ""),
200        );
201        assert!(format!("{:#}", limits(&r, "y1").unwrap_err()).contains("memory"));
202        let r = root_with("y2", &format!("{GB10}chassis_typo_c = 1\n"));
203        assert!(limits(&r, "y2").is_err());
204        let r = root_with(
205            "y3",
206            &GB10.replace("chassis_resume_c = 70", "chassis_resume_c = 85"),
207        );
208        assert!(
209            limits(&r, "y3")
210                .unwrap_err()
211                .to_string()
212                .contains("hysteresis")
213        );
214        let r = root_with(
215            "y4",
216            &GB10.replace("min_free_fraction = 0.85", "min_free_fraction = 1.5"),
217        );
218        assert!(
219            limits(&r, "y4")
220                .unwrap_err()
221                .to_string()
222                .contains("min_free_fraction")
223        );
224        let r = root_with(
225            "y5",
226            &GB10.replace(
227                "chassis_equivalence_delta_c = 15",
228                "chassis_equivalence_delta_c = 0",
229            ),
230        );
231        assert!(limits(&r, "y5").is_err());
232        let r = root_with(
233            "y6",
234            &GB10.replace("serve_allowance_s = 600", "serve_allowance_s = 0"),
235        );
236        assert!(
237            limits(&r, "y6")
238                .unwrap_err()
239                .to_string()
240                .contains("allowances")
241        );
242    }
243
244    /// 2026-09-26: The committed GB10 table reads as declared; the other classes
245    /// in the tree declare none.
246    #[test]
247    fn gb10_declares_the_measured_limits_and_others_declare_none() {
248        let root = Path::new(env!("CARGO_MANIFEST_DIR"))
249            .parent()
250            .unwrap()
251            .parent()
252            .unwrap();
253        let l = limits(root, "gb10")
254            .unwrap()
255            .expect("gb10 declares [benchmarks.limits]");
256        assert_eq!(
257            (
258                l.thermal.chassis_park_c,
259                l.thermal.chassis_resume_c,
260                l.thermal.chassis_equivalence_delta_c,
261                l.thermal.gpu_ceiling_c
262            ),
263            (80.0, 70.0, 15.0, 75.0)
264        );
265        assert_eq!(l.memory.min_free_fraction, 0.85);
266        assert_eq!(
267            (
268                l.timing.serve_allowance_s,
269                l.timing.boot_timeout_s,
270                l.timing.shard_overhead_s,
271                l.timing.shard_floor_s,
272                l.timing.build_allowance_s
273            ),
274            (600, 900, 420, 300, 1800)
275        );
276        assert_eq!(
277            (l.equivalence.clock_spread, l.equivalence.mem_spread),
278            (0.01, 0.05)
279        );
280        for hw in ["hopper", "b200", "strix", "strix-hip", "metal"] {
281            assert_eq!(limits(root, hw).unwrap(), None, "{hw}");
282        }
283    }
284}