use crate::tensor::{Device, Result, Tensor, TensorError, TensorOptions};
pub(crate) const RAM_SHARE_CEILING: f64 = 0.90;
pub(crate) fn anchored_ram_budget(available: u64, held_bytes: u64, ram_max_usage: f64) -> u64 {
let total = available.saturating_add(held_bytes);
(total as f64 * ram_max_usage.min(RAM_SHARE_CEILING)) as u64
}
pub(crate) fn unified_host_available(available: u64, gpu_reservation: u64, gpu_in_use: u64) -> u64 {
available.saturating_sub(gpu_reservation.saturating_sub(gpu_in_use))
}
pub(crate) fn gpu_ram_reservation(
integrated: bool,
aperture_bytes: u64,
mem_total: u64,
gpu_ram_share: Option<f64>,
) -> u64 {
if !integrated {
return 0;
}
match gpu_ram_share {
Some(share) if share >= 0.0 => (mem_total as f64 * share) as u64,
_ => aperture_bytes,
}
}
pub(crate) fn apu_budget_sizeable(
integrated: bool,
packages: Option<usize>,
gpu_ram_share: Option<f64>,
) -> bool {
if !integrated || gpu_ram_share.is_some() {
return true; }
packages.is_none_or(|n| n <= 1)
}
const UNSIZEABLE_APU: &str = "this is an integrated (APU) GPU on a multi-socket machine, where each package \
carries its own memory pool and its own aperture. Host-RAM budgets read \
system-wide totals, so flodl cannot size them correctly here and would \
over-commit memory. Set an explicit GPU RAM share (a fraction of MemTotal) to \
proceed: `gpu_ram_share` on DataLoaderBuilder, TrainerConfig or DdpRunConfig.";
fn device_budget_sizeable(device: Device, gpu_ram_share: Option<f64>) -> bool {
if !device.is_cuda() {
return true;
}
let integrated = crate::tensor::gpu_is_integrated(device.index() as i32) == Some(true);
apu_budget_sizeable(integrated, crate::sys::cpu_package_count(), gpu_ram_share)
}
pub(crate) fn check_apu_sizing(device: Device, gpu_ram_share: Option<f64>) -> Result<()> {
if device_budget_sizeable(device, gpu_ram_share) {
return Ok(());
}
Err(TensorError::new(UNSIZEABLE_APU))
}
const OVERLAP_PROBE_BYTES: u64 = 512 * 1024 * 1024;
pub(crate) fn unified_overlap_confirmed(device: Device) -> bool {
static CACHED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*CACHED.get_or_init(|| probe_overlap(device).unwrap_or(true))
}
fn probe_overlap(device: Device) -> Result<bool> {
let opts = TensorOptions {
dtype: crate::tensor::DType::Float32,
device,
};
let warm = Tensor::zeros(&[1024], opts)?;
crate::tensor::gpu_synchronize(device.index());
let before = crate::sys::mem_info().map(|m| m.available_bytes);
let n = (OVERLAP_PROBE_BYTES / 4) as i64;
let probe = Tensor::zeros(&[n], opts)?;
crate::tensor::gpu_synchronize(device.index());
let after = crate::sys::mem_info().map(|m| m.available_bytes);
drop(probe);
drop(warm);
match (before, after) {
(Some(b), Some(a)) => Ok(b.saturating_sub(a) > OVERLAP_PROBE_BYTES / 2),
_ => Ok(true),
}
}
pub(crate) fn unified_adjusted_available(
available: u64,
device: Device,
gpu_ram_share: Option<f64>,
) -> u64 {
if !device.is_cuda() {
return available;
}
let idx = device.index() as i32;
if crate::tensor::gpu_is_integrated(idx) != Some(true) {
return available;
}
if !unified_overlap_confirmed(device) {
return available; }
let (in_use, aperture) = crate::tensor::gpu_memory_info_idx(idx).unwrap_or((0, 0));
let mem_total = crate::sys::mem_info().map(|m| m.total_bytes).unwrap_or(0);
let packages = crate::sys::cpu_package_count();
if !apu_budget_sizeable(true, packages, gpu_ram_share) {
warn_unsizeable_once();
let all = aperture.saturating_mul(packages.unwrap_or(1) as u64);
return unified_host_available(available, all, in_use);
}
let reservation = gpu_ram_reservation(true, aperture, mem_total, gpu_ram_share);
unified_host_available(available, reservation, in_use)
}
fn warn_unsizeable_once() {
static ONCE: std::sync::Once = std::sync::Once::new();
ONCE.call_once(|| {
crate::msg!(
"flodl: {UNSIZEABLE_APU} Sizing host RAM pessimistically \
(one aperture per package) until then."
);
});
}
pub(crate) fn sample_cache_budget(
available: u64,
held_bytes: u64,
ring_bytes: u64,
ram_max_usage: f64,
) -> u64 {
anchored_ram_budget(available, held_bytes, ram_max_usage).saturating_sub(ring_bytes)
}
pub(crate) fn stager_ram_budget(
available: u64,
held_bytes: u64,
ram_max_usage: f64,
host_share: f64,
) -> u64 {
(anchored_ram_budget(available, held_bytes, ram_max_usage) as f64 * host_share) as u64
}
pub(crate) fn split_stager_budget(total: usize) -> (usize, usize) {
let stream = total / 4;
(total - stream, stream)
}
pub(crate) const RING_SLOTS_FALLBACK: usize = 4;
pub(crate) const RING_SLOTS_WITH_CACHE: usize = 8;
pub(crate) fn ring_slots_from_ram(
per_sample_bytes: usize,
batch_size: usize,
ram_max_usage: f64,
available: Option<u64>,
epoch_batches: usize,
) -> usize {
if ram_max_usage <= 0.0 {
return 0;
}
let Some(available) = available else {
return RING_SLOTS_FALLBACK.min(epoch_batches);
};
let batch_bytes = per_sample_bytes.saturating_mul(batch_size) as u64;
if batch_bytes == 0 {
return RING_SLOTS_FALLBACK.min(epoch_batches);
}
let budget = anchored_ram_budget(available, 0, ram_max_usage);
(budget / batch_bytes).min(epoch_batches as u64) as usize
}
const DOUBLE_BUFFER: usize = 2;
const FLOOR_FREE_RATIO: usize = 256;
pub(crate) fn prefetch_depth_from_vram(
per_sample_bytes: usize,
batch_size: usize,
device: Device,
max_usage: f64,
activation_reserve: usize,
) -> usize {
if !device.is_cuda() {
return DOUBLE_BUFFER; }
let batch_bytes = per_sample_bytes * batch_size;
if batch_bytes == 0 {
return DOUBLE_BUFFER; }
let idx = device.index() as i32;
let (used, total) = crate::tensor::gpu_memory_info_idx(idx).unwrap_or((u64::MAX, 0));
depth_from_probe(used, total, max_usage, activation_reserve, batch_bytes)
}
fn depth_from_probe(
used: u64,
total: u64,
max_usage: f64,
activation_reserve: usize,
batch_bytes: usize,
) -> usize {
let cap = (total as f64 * max_usage.clamp(0.5, 0.99)) as usize;
let budget = cap.saturating_sub(used as usize + activation_reserve);
let depth = budget / batch_bytes;
if depth > 0 {
return depth;
}
let free = total.saturating_sub(used) as usize;
let floor_bytes = DOUBLE_BUFFER.saturating_mul(batch_bytes);
if floor_bytes.saturating_mul(FLOOR_FREE_RATIO) <= free {
DOUBLE_BUFFER
} else {
0
}
}
const VIEW_SLACK_FACTOR: usize = 2;
pub(crate) fn retained_cost_estimate(rows: &[Tensor]) -> usize {
rows.iter()
.map(|t| {
let logical = t.nbytes();
let storage = t.storage_nbytes();
if storage > VIEW_SLACK_FACTOR.saturating_mul(logical) {
logical
} else {
storage
}
})
.sum()
}
pub(crate) fn retain_rows(rows: &[Tensor]) -> Result<(Vec<Tensor>, usize)> {
let mut out = Vec::with_capacity(rows.len());
let mut cost = 0usize;
for t in rows {
let logical = t.nbytes();
let storage = t.storage_nbytes();
if storage > VIEW_SLACK_FACTOR.saturating_mul(logical) {
let owned = Tensor::empty(
&t.shape(),
TensorOptions {
dtype: t.dtype(),
device: t.device(),
},
)?;
owned.copy_(t, false)?;
out.push(owned);
cost += logical;
} else {
out.push(t.clone());
cost += storage;
}
}
Ok((out, cost))
}
#[cfg(test)]
mod tests {
const MIB: u64 = 1024 * 1024;
const APERTURE: u64 = 15360 * MIB;
const MEM_TOTAL: u64 = 30720 * MIB;
#[test]
fn unified_reserves_exactly_one_aperture_at_every_point_in_the_run() {
let others = 8950 * MIB; for (gpu, host) in [
(0u64, 0u64),
(7500 * MIB, 1500 * MIB),
(APERTURE, 3350 * MIB),
] {
let available = MEM_TOTAL - others - gpu - host;
let got = unified_host_available(available, APERTURE, gpu) + host;
let want = MEM_TOTAL - others - APERTURE;
assert_eq!(
got, want,
"host base must not drift as the GPU fills (gpu={gpu}, host={host})"
);
}
}
#[test]
fn unified_is_identity_when_nothing_is_reserved() {
assert_eq!(unified_host_available(1234, 0, 0), 1234);
}
#[test]
fn unified_saturates_rather_than_underflowing() {
assert_eq!(unified_host_available(MIB, APERTURE, 0), 0);
}
#[test]
fn reservation_is_zero_on_a_discrete_part_even_with_a_knob_set() {
assert_eq!(gpu_ram_reservation(false, APERTURE, MEM_TOTAL, None), 0);
assert_eq!(
gpu_ram_reservation(false, APERTURE, MEM_TOTAL, Some(0.5)),
0
);
}
#[test]
fn reservation_defaults_to_the_reported_aperture() {
assert_eq!(
gpu_ram_reservation(true, APERTURE, MEM_TOTAL, None),
APERTURE
);
}
#[test]
fn knob_overrides_the_aperture_as_a_share_of_mem_total() {
assert_eq!(
gpu_ram_reservation(true, APERTURE, MEM_TOTAL, Some(0.25)),
MEM_TOTAL / 4
);
assert_eq!(
gpu_ram_reservation(true, APERTURE, MEM_TOTAL, Some(1.5)),
MEM_TOTAL + MEM_TOTAL / 2
);
assert_eq!(gpu_ram_reservation(true, APERTURE, MEM_TOTAL, Some(0.0)), 0);
}
#[test]
fn only_a_multi_socket_apu_without_the_knob_is_unsizeable() {
assert!(!apu_budget_sizeable(true, Some(2), None));
assert!(apu_budget_sizeable(false, Some(2), None), "discrete part");
assert!(apu_budget_sizeable(true, Some(1), None), "single socket");
assert!(
apu_budget_sizeable(true, Some(2), Some(0.5)),
"knob resolves it"
);
assert!(apu_budget_sizeable(true, Some(2), Some(0.0)));
}
#[test]
fn a_high_numa_count_on_one_socket_still_sizes() {
for packages in [Some(1), None] {
assert!(
apu_budget_sizeable(true, packages, None),
"packages={packages:?} must size regardless of NUMA layout"
);
}
}
use super::*;
const PASCAL_USED: u64 = 5592 << 20; const PASCAL_TOTAL: u64 = 6_360_465_408; const OLMO_ACTIVATION_PEAK: usize = 1682 << 20;
const OLMO_BATCH_BYTES: usize = 16 << 10;
#[test]
fn a_model_above_the_cap_still_gets_a_double_buffer() {
let cap = (PASCAL_TOTAL as f64 * 0.90) as usize;
assert!(
PASCAL_USED as usize > cap,
"premise: the model alone is over the cap"
);
let depth = depth_from_probe(PASCAL_USED, PASCAL_TOTAL, 0.90, 0, OLMO_BATCH_BYTES);
assert_eq!(depth, DOUBLE_BUFFER);
let with_reserve = depth_from_probe(
PASCAL_USED,
PASCAL_TOTAL,
0.90,
OLMO_ACTIVATION_PEAK,
OLMO_BATCH_BYTES,
);
assert_eq!(with_reserve, DOUBLE_BUFFER);
}
#[test]
fn the_activation_peak_is_what_drove_the_budget_to_zero() {
let used = 3 << 30; let honest = depth_from_probe(used, PASCAL_TOTAL, 0.90, 0, OLMO_BATCH_BYTES);
let double_counted = depth_from_probe(
used,
PASCAL_TOTAL,
0.90,
OLMO_ACTIVATION_PEAK,
OLMO_BATCH_BYTES,
);
assert!(
honest > 100_000,
"honest probe should afford the whole chunk: {honest}"
);
assert!(
double_counted < honest / 2,
"charging the peak twice must visibly shrink the budget: {double_counted} vs {honest}",
);
}
#[test]
fn a_batch_too_large_for_the_headroom_keeps_the_sync_fallback() {
let free = PASCAL_TOTAL - PASCAL_USED; let fat_batch = (free / 4) as usize;
let depth = depth_from_probe(PASCAL_USED, PASCAL_TOTAL, 0.90, 0, fat_batch);
assert_eq!(depth, 0);
}
#[test]
fn a_failed_probe_declines_the_floor() {
let depth = depth_from_probe(u64::MAX, 0, 0.90, 0, OLMO_BATCH_BYTES);
assert_eq!(depth, 0);
}
#[test]
fn anchored_budget_is_a_fixed_point_both_ways() {
let a0: u64 = 1000;
let r = 0.5;
let b0 = anchored_ram_budget(a0, 0, r);
for held in [100u64, 400, 500] {
assert_eq!(anchored_ram_budget(a0 - held, held, r), b0);
}
}
#[test]
fn ram_share_is_ceilinged() {
assert_eq!(anchored_ram_budget(1000, 0, 5.0), 900);
assert_eq!(stager_ram_budget(1000, 0, 5.0, 1.0), 900);
}
#[test]
fn stager_budget_shares_and_anchors() {
assert_eq!(stager_ram_budget(1000, 0, 0.5, 0.5), 250);
assert_eq!(stager_ram_budget(800, 200, 0.5, 0.5), 250);
}
#[test]
fn stager_split_is_three_to_one() {
assert_eq!(split_stager_budget(100), (75, 25));
assert_eq!(split_stager_budget(0), (0, 0));
let (p, s) = split_stager_budget(7);
assert_eq!(p + s, 7);
}
#[test]
fn retention_prices_views_by_storage() {
use crate::tensor::Device;
let base = Tensor::from_f32(&[0.0; 64], &[8, 8], Device::CPU).unwrap();
let (rows, cost) = retain_rows(std::slice::from_ref(&base)).unwrap();
assert_eq!(cost, base.nbytes());
assert_eq!(rows[0].storage_nbytes(), base.storage_nbytes());
let row = base.select(0, 0).unwrap();
assert_eq!(row.nbytes(), 32);
assert!(row.storage_nbytes() >= 256);
assert_eq!(retained_cost_estimate(std::slice::from_ref(&row)), 32);
let (rows, cost) = retain_rows(std::slice::from_ref(&row)).unwrap();
assert_eq!(cost, 32);
assert_eq!(rows[0].storage_nbytes(), 32);
assert_eq!(rows[0].to_f32_vec().unwrap(), row.to_f32_vec().unwrap());
let half = base.narrow(0, 0, 4).unwrap();
assert_eq!(half.nbytes(), 128);
let est = retained_cost_estimate(std::slice::from_ref(&half));
assert_eq!(est, half.storage_nbytes());
let (rows, cost) = retain_rows(std::slice::from_ref(&half)).unwrap();
assert_eq!(cost, half.storage_nbytes());
assert_eq!(rows[0].storage_nbytes(), base.storage_nbytes());
}
}