#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BudgetBackend {
Cpu,
Metal,
Cuda,
}
impl BudgetBackend {
pub fn as_str(self) -> &'static str {
match self {
BudgetBackend::Cpu => "cpu",
BudgetBackend::Metal => "metal",
BudgetBackend::Cuda => "cuda",
}
}
}
impl std::fmt::Display for BudgetBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl std::str::FromStr for BudgetBackend {
type Err = String;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.trim().to_ascii_lowercase().as_str() {
"cpu" | "host" => Ok(BudgetBackend::Cpu),
"metal" => Ok(BudgetBackend::Metal),
"cuda" => Ok(BudgetBackend::Cuda),
other => Err(format!("unknown backend `{other}` (cpu, metal, cuda)")),
}
}
}
pub const CPU_RESERVE_FRACTION: f64 = 0.2;
pub const DEVICE_RESERVE_FRACTION: f64 = 0.1;
pub const BUDGET_ENV: &str = "FERROX_DEVICE_BUDGET_BYTES";
#[derive(Debug, Clone, PartialEq)]
pub struct DeviceBudget {
pub backend: BudgetBackend,
pub total_bytes: u64,
pub usable_bytes: u64,
pub reserve_fraction: f64,
pub source: String,
pub approximate: bool,
}
impl DeviceBudget {
pub fn new(backend: BudgetBackend, total_bytes: u64, reserve: f64, source: String) -> Self {
let reserve = reserve.clamp(0.0, 1.0);
DeviceBudget {
backend,
total_bytes,
usable_bytes: (total_bytes as f64 * (1.0 - reserve)) as u64,
reserve_fraction: reserve,
source,
approximate: true,
}
}
pub fn detect(backend: BudgetBackend) -> Self {
if let Some(bytes) = env_override() {
return DeviceBudget {
backend,
total_bytes: bytes,
usable_bytes: bytes,
reserve_fraction: 0.0,
source: format!("{BUDGET_ENV} override (no reserve applied)"),
approximate: true,
};
}
match backend {
BudgetBackend::Metal => metal_budget(),
BudgetBackend::Cuda => cuda_budget(),
BudgetBackend::Cpu => host_ram_budget(),
}
}
pub fn is_unknown(&self) -> bool {
self.total_bytes == 0
}
pub fn caveat(&self) -> &'static str {
"approximate: ferrox mmaps quantized weights, so their resident cost is the \
kernel's page cache to decide -- this charges the whole checkpoint, which is an \
upper bound, and the budget itself is a snapshot, not a reservation"
}
}
impl std::fmt::Display for DeviceBudget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_unknown() {
return write!(
f,
"{} budget: unknown ({}); no ceiling enforced",
self.backend, self.source
);
}
write!(
f,
"{} budget: {} usable of {} total ({:.0}% held back) via {}",
self.backend,
human(self.usable_bytes),
human(self.total_bytes),
self.reserve_fraction * 100.0,
self.source
)
}
}
fn env_override() -> Option<u64> {
std::env::var(BUDGET_ENV)
.ok()
.and_then(|v| v.trim().parse::<u64>().ok())
.filter(|v| *v > 0)
}
fn metal_budget() -> DeviceBudget {
let profile = ferrox_metal::MetalProfile::detect();
if profile.available && profile.recommended_working_set_bytes > 0 {
return DeviceBudget::new(
BudgetBackend::Metal,
profile.recommended_working_set_bytes,
DEVICE_RESERVE_FRACTION,
format!(
"Metal recommendedMaxWorkingSetSize on {}",
profile.device_name.as_deref().unwrap_or("unnamed device")
),
);
}
let mut fallback = host_ram_budget();
fallback.backend = BudgetBackend::Metal;
fallback.source = format!(
"no Metal device query available; fell back to {}",
fallback.source
);
fallback
}
fn cuda_budget() -> DeviceBudget {
let profile = ferrox_cuda::HardwareProfile::detect();
if profile.cuda_available && profile.cuda_vram_free_bytes > 0 {
return DeviceBudget::new(
BudgetBackend::Cuda,
profile.cuda_vram_free_bytes,
DEVICE_RESERVE_FRACTION,
format!(
"cuMemGetInfo free VRAM on {} ({} total)",
profile.cuda_device_name.as_deref().unwrap_or("device 0"),
human(profile.cuda_vram_total_bytes)
),
);
}
let mut fallback = host_ram_budget();
fallback.backend = BudgetBackend::Cuda;
fallback.source = format!(
"no CUDA device query available; fell back to {}",
fallback.source
);
fallback
}
fn host_ram_budget() -> DeviceBudget {
let total = ferrox_cuda::HardwareProfile::detect().host_ram_total_bytes;
if total == 0 {
return DeviceBudget {
backend: BudgetBackend::Cpu,
total_bytes: 0,
usable_bytes: 0,
reserve_fraction: 0.0,
source: "host RAM could not be probed on this platform".to_string(),
approximate: true,
};
}
DeviceBudget::new(
BudgetBackend::Cpu,
total,
CPU_RESERVE_FRACTION,
"total physical host RAM".to_string(),
)
}
pub(crate) fn human(bytes: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut v = bytes as f64;
let mut u = 0;
while v >= 1024.0 && u < UNITS.len() - 1 {
v /= 1024.0;
u += 1;
}
format!("{v:.2} {}", UNITS[u])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reserve_is_applied_and_reported() {
let b = DeviceBudget::new(BudgetBackend::Cpu, 1000, 0.2, "test".into());
assert_eq!(b.total_bytes, 1000);
assert_eq!(b.usable_bytes, 800);
assert_eq!(b.reserve_fraction, 0.2);
assert!(!b.is_unknown());
assert!(b.approximate);
}
#[test]
fn a_nonsense_reserve_is_clamped_rather_than_producing_a_negative_budget() {
let over = DeviceBudget::new(BudgetBackend::Cpu, 1000, 5.0, "test".into());
assert_eq!(over.usable_bytes, 0);
let under = DeviceBudget::new(BudgetBackend::Cpu, 1000, -1.0, "test".into());
assert_eq!(under.usable_bytes, 1000);
}
#[test]
fn zero_total_reads_as_unknown_not_as_a_zero_ceiling() {
let b = DeviceBudget::new(BudgetBackend::Cpu, 0, 0.2, "nothing to probe".into());
assert!(b.is_unknown());
assert!(b.to_string().contains("no ceiling enforced"), "{b}");
}
#[test]
fn cpu_budget_is_either_unknown_or_a_plausible_fraction_of_real_ram() {
let b = DeviceBudget::detect(BudgetBackend::Cpu);
assert_eq!(b.backend, BudgetBackend::Cpu);
if b.is_unknown() {
assert_eq!(b.usable_bytes, 0);
} else {
assert!(b.total_bytes > 128 * 1024 * 1024);
assert!(b.usable_bytes < b.total_bytes);
assert!(b.usable_bytes > b.total_bytes / 2);
assert!(b.to_string().contains("host RAM"), "{b}");
}
}
#[test]
fn accelerator_budgets_fall_back_to_host_ram_when_no_device_answers() {
for backend in [BudgetBackend::Metal, BudgetBackend::Cuda] {
let b = DeviceBudget::detect(backend);
assert_eq!(b.backend, backend);
if b.source.contains("fell back") {
assert!(b.source.contains("host RAM"), "{b}");
}
}
}
#[test]
fn human_bytes_are_readable_at_every_scale() {
assert_eq!(human(0), "0.00 B");
assert_eq!(human(1024), "1.00 KiB");
assert_eq!(human(3 * 1024 * 1024 * 1024), "3.00 GiB");
}
}