1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum BudgetBackend {
40 Cpu,
41 Metal,
42 Cuda,
43}
44
45impl BudgetBackend {
46 pub fn as_str(self) -> &'static str {
47 match self {
48 BudgetBackend::Cpu => "cpu",
49 BudgetBackend::Metal => "metal",
50 BudgetBackend::Cuda => "cuda",
51 }
52 }
53}
54
55impl std::fmt::Display for BudgetBackend {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.write_str(self.as_str())
58 }
59}
60
61impl std::str::FromStr for BudgetBackend {
64 type Err = String;
65
66 fn from_str(value: &str) -> Result<Self, Self::Err> {
67 match value.trim().to_ascii_lowercase().as_str() {
68 "cpu" | "host" => Ok(BudgetBackend::Cpu),
69 "metal" => Ok(BudgetBackend::Metal),
70 "cuda" => Ok(BudgetBackend::Cuda),
71 other => Err(format!("unknown backend `{other}` (cpu, metal, cuda)")),
72 }
73 }
74}
75
76pub const CPU_RESERVE_FRACTION: f64 = 0.2;
80
81pub const DEVICE_RESERVE_FRACTION: f64 = 0.1;
85
86pub const BUDGET_ENV: &str = "FERROX_DEVICE_BUDGET_BYTES";
91
92#[derive(Debug, Clone, PartialEq)]
96pub struct DeviceBudget {
97 pub backend: BudgetBackend,
98 pub total_bytes: u64,
100 pub usable_bytes: u64,
102 pub reserve_fraction: f64,
104 pub source: String,
107 pub approximate: bool,
113}
114
115impl DeviceBudget {
116 pub fn new(backend: BudgetBackend, total_bytes: u64, reserve: f64, source: String) -> Self {
118 let reserve = reserve.clamp(0.0, 1.0);
119 DeviceBudget {
120 backend,
121 total_bytes,
122 usable_bytes: (total_bytes as f64 * (1.0 - reserve)) as u64,
123 reserve_fraction: reserve,
124 source,
125 approximate: true,
126 }
127 }
128
129 pub fn detect(backend: BudgetBackend) -> Self {
137 if let Some(bytes) = env_override() {
138 return DeviceBudget {
139 backend,
140 total_bytes: bytes,
141 usable_bytes: bytes,
142 reserve_fraction: 0.0,
143 source: format!("{BUDGET_ENV} override (no reserve applied)"),
144 approximate: true,
145 };
146 }
147 match backend {
148 BudgetBackend::Metal => metal_budget(),
149 BudgetBackend::Cuda => cuda_budget(),
150 BudgetBackend::Cpu => host_ram_budget(),
151 }
152 }
153
154 pub fn is_unknown(&self) -> bool {
158 self.total_bytes == 0
159 }
160
161 pub fn caveat(&self) -> &'static str {
163 "approximate: ferrox mmaps quantized weights, so their resident cost is the \
164 kernel's page cache to decide -- this charges the whole checkpoint, which is an \
165 upper bound, and the budget itself is a snapshot, not a reservation"
166 }
167}
168
169impl std::fmt::Display for DeviceBudget {
170 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171 if self.is_unknown() {
172 return write!(
173 f,
174 "{} budget: unknown ({}); no ceiling enforced",
175 self.backend, self.source
176 );
177 }
178 write!(
179 f,
180 "{} budget: {} usable of {} total ({:.0}% held back) via {}",
181 self.backend,
182 human(self.usable_bytes),
183 human(self.total_bytes),
184 self.reserve_fraction * 100.0,
185 self.source
186 )
187 }
188}
189
190fn env_override() -> Option<u64> {
191 std::env::var(BUDGET_ENV)
192 .ok()
193 .and_then(|v| v.trim().parse::<u64>().ok())
194 .filter(|v| *v > 0)
195}
196
197fn metal_budget() -> DeviceBudget {
201 let profile = ferrox_metal::MetalProfile::detect();
202 if profile.available && profile.recommended_working_set_bytes > 0 {
203 return DeviceBudget::new(
204 BudgetBackend::Metal,
205 profile.recommended_working_set_bytes,
206 DEVICE_RESERVE_FRACTION,
207 format!(
208 "Metal recommendedMaxWorkingSetSize on {}",
209 profile.device_name.as_deref().unwrap_or("unnamed device")
210 ),
211 );
212 }
213 let mut fallback = host_ram_budget();
214 fallback.backend = BudgetBackend::Metal;
215 fallback.source = format!(
216 "no Metal device query available; fell back to {}",
217 fallback.source
218 );
219 fallback
220}
221
222fn cuda_budget() -> DeviceBudget {
227 let profile = ferrox_cuda::HardwareProfile::detect();
228 if profile.cuda_available && profile.cuda_vram_free_bytes > 0 {
229 return DeviceBudget::new(
230 BudgetBackend::Cuda,
231 profile.cuda_vram_free_bytes,
232 DEVICE_RESERVE_FRACTION,
233 format!(
234 "cuMemGetInfo free VRAM on {} ({} total)",
235 profile.cuda_device_name.as_deref().unwrap_or("device 0"),
236 human(profile.cuda_vram_total_bytes)
237 ),
238 );
239 }
240 let mut fallback = host_ram_budget();
241 fallback.backend = BudgetBackend::Cuda;
242 fallback.source = format!(
243 "no CUDA device query available; fell back to {}",
244 fallback.source
245 );
246 fallback
247}
248
249fn host_ram_budget() -> DeviceBudget {
254 let total = ferrox_cuda::HardwareProfile::detect().host_ram_total_bytes;
255 if total == 0 {
256 return DeviceBudget {
257 backend: BudgetBackend::Cpu,
258 total_bytes: 0,
259 usable_bytes: 0,
260 reserve_fraction: 0.0,
261 source: "host RAM could not be probed on this platform".to_string(),
262 approximate: true,
263 };
264 }
265 DeviceBudget::new(
266 BudgetBackend::Cpu,
267 total,
268 CPU_RESERVE_FRACTION,
269 "total physical host RAM".to_string(),
270 )
271}
272
273pub(crate) fn human(bytes: u64) -> String {
274 const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
275 let mut v = bytes as f64;
276 let mut u = 0;
277 while v >= 1024.0 && u < UNITS.len() - 1 {
278 v /= 1024.0;
279 u += 1;
280 }
281 format!("{v:.2} {}", UNITS[u])
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn reserve_is_applied_and_reported() {
290 let b = DeviceBudget::new(BudgetBackend::Cpu, 1000, 0.2, "test".into());
291 assert_eq!(b.total_bytes, 1000);
292 assert_eq!(b.usable_bytes, 800);
293 assert_eq!(b.reserve_fraction, 0.2);
294 assert!(!b.is_unknown());
295 assert!(b.approximate);
297 }
298
299 #[test]
300 fn a_nonsense_reserve_is_clamped_rather_than_producing_a_negative_budget() {
301 let over = DeviceBudget::new(BudgetBackend::Cpu, 1000, 5.0, "test".into());
302 assert_eq!(over.usable_bytes, 0);
303 let under = DeviceBudget::new(BudgetBackend::Cpu, 1000, -1.0, "test".into());
304 assert_eq!(under.usable_bytes, 1000);
305 }
306
307 #[test]
308 fn zero_total_reads_as_unknown_not_as_a_zero_ceiling() {
309 let b = DeviceBudget::new(BudgetBackend::Cpu, 0, 0.2, "nothing to probe".into());
310 assert!(b.is_unknown());
311 assert!(b.to_string().contains("no ceiling enforced"), "{b}");
312 }
313
314 #[test]
318 fn cpu_budget_is_either_unknown_or_a_plausible_fraction_of_real_ram() {
319 let b = DeviceBudget::detect(BudgetBackend::Cpu);
320 assert_eq!(b.backend, BudgetBackend::Cpu);
321 if b.is_unknown() {
322 assert_eq!(b.usable_bytes, 0);
323 } else {
324 assert!(b.total_bytes > 128 * 1024 * 1024);
325 assert!(b.usable_bytes < b.total_bytes);
326 assert!(b.usable_bytes > b.total_bytes / 2);
327 assert!(b.to_string().contains("host RAM"), "{b}");
328 }
329 }
330
331 #[test]
335 fn accelerator_budgets_fall_back_to_host_ram_when_no_device_answers() {
336 for backend in [BudgetBackend::Metal, BudgetBackend::Cuda] {
337 let b = DeviceBudget::detect(backend);
338 assert_eq!(b.backend, backend);
339 if b.source.contains("fell back") {
340 assert!(b.source.contains("host RAM"), "{b}");
341 }
342 }
343 }
344
345 #[test]
346 fn human_bytes_are_readable_at_every_scale() {
347 assert_eq!(human(0), "0.00 B");
348 assert_eq!(human(1024), "1.00 KiB");
349 assert_eq!(human(3 * 1024 * 1024 * 1024), "3.00 GiB");
350 }
351}