1use std::path::Path;
20
21use anyhow::{Context, Result, bail};
22
23#[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#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct ThermalEnvelope {
37 pub chassis_park_c: f64,
40 pub chassis_resume_c: f64,
42 pub chassis_equivalence_delta_c: f64,
45 pub gpu_ceiling_c: f64,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
51#[serde(deny_unknown_fields)]
52pub struct MemoryLimits {
53 pub min_free_fraction: f64,
55}
56
57#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct TimingLimits {
61 pub serve_allowance_s: u64,
64 pub boot_timeout_s: u64,
66 pub shard_overhead_s: u64,
68 pub shard_floor_s: u64,
70 pub build_allowance_s: u64,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct EquivalenceLimits {
79 pub clock_spread: f64,
81 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
95pub 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 #[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 #[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}