pub fn net_cache_budget_bytes(
memory_ratio: f64,
baseline_free_bytes: u64,
weights_bytes: u64,
fixed_cache_bytes: u64,
) -> i64 {
(memory_ratio * baseline_free_bytes as f64) as i64
- weights_bytes as i64
- fixed_cache_bytes as i64
}
pub fn required_bytes(
moe_cache_slots: u64,
kv_pages: u64,
bytes_per_expert: u64,
bytes_per_page: u64,
) -> i64 {
(moe_cache_slots * bytes_per_expert + kv_pages * bytes_per_page) as i64
}
pub fn startup_kv_budget(memory_ratio: f64, init_free_bytes: u64, new_free_bytes: u64) -> i64 {
(memory_ratio * init_free_bytes as f64) as i64
- (init_free_bytes as i64 - new_free_bytes as i64)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PoolSizes {
pub moe_cache_slots: u64,
pub kv_pages: u64,
pub prefill_overlap: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BudgetTooSmall {
pub needed_bytes: i64,
pub budget_bytes: i64,
pub sizes: PoolSizes,
}
impl std::fmt::Display for BudgetTooSmall {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"requested cache (moe={} slots, kv={} pages) needs {} bytes but the budget is {}; \
the old cache is kept and still serving",
self.sizes.moe_cache_slots, self.sizes.kv_pages, self.needed_bytes, self.budget_bytes
)
}
}
impl std::error::Error for BudgetTooSmall {}
pub fn expert_bytes_per_slot(bank_row_bytes: &[u64]) -> u64 {
bank_row_bytes.iter().sum()
}
#[allow(clippy::too_many_arguments)]
pub fn plan_cache_budget(
budget_bytes: i64,
bytes_per_expert: u64,
bytes_per_page: u64,
num_experts: u64,
total_experts: u64,
prefill_overlap: bool,
kv_reserve_pages: u64,
max_slots: u64,
) -> Result<PoolSizes, BudgetTooSmall> {
assert!(
bytes_per_expert > 0 && bytes_per_page > 0,
"an unpriced pool cannot be sized"
);
let hi = total_experts.min(max_slots);
let mut overlap = prefill_overlap && hi >= 2 * num_experts;
let lo = if overlap {
2 * num_experts
} else {
num_experts
};
assert!(
hi >= lo,
"the expert-cache ceiling of {hi} slots cannot hold the {lo} slots a layer needs"
);
let spare = budget_bytes - (kv_reserve_pages * bytes_per_page) as i64;
let raw = if spare <= 0 {
0
} else {
(spare as u64) / bytes_per_expert
};
let moe_cache_slots = raw.min(hi).max(lo);
overlap = overlap && moe_cache_slots >= 2 * num_experts;
let remaining = budget_bytes - (moe_cache_slots * bytes_per_expert) as i64;
let kv_pages = if remaining <= 0 {
kv_reserve_pages
} else {
((remaining as u64) / bytes_per_page).max(kv_reserve_pages)
};
let sizes = PoolSizes {
moe_cache_slots,
kv_pages,
prefill_overlap: overlap,
};
let needed = required_bytes(moe_cache_slots, kv_pages, bytes_per_expert, bytes_per_page);
if needed > budget_bytes || kv_pages <= 1 {
return Err(BudgetTooSmall {
needed_bytes: needed,
budget_bytes,
sizes,
});
}
Ok(sizes)
}
#[cfg(test)]
mod tests {
use super::*;
const GIB: u64 = 1 << 30;
const MIB: u64 = 1 << 20;
#[test]
fn the_budget_is_free_vram_less_the_weights_and_the_headroom() {
assert_eq!(
net_cache_budget_bytes(0.9, 10 * GIB, 4 * GIB, 0),
(0.9 * (10 * GIB) as f64) as i64 - (4 * GIB) as i64
);
assert!(net_cache_budget_bytes(0.5, GIB, 4 * GIB, 0) < 0);
}
#[test]
fn the_startup_budget_prices_what_loading_actually_consumed() {
assert_eq!(startup_kv_budget(0.9, 1000, 400), 900 - 600);
assert_eq!(startup_kv_budget(1.0, 1000, 1000), 1000);
}
#[test]
fn the_expert_cache_is_filled_before_kv_gets_the_remainder() {
let sizes = plan_cache_budget(
(8 * GIB) as i64,
16 * MIB, MIB, 32, 256, false,
64,
u64::MAX,
)
.expect("fits");
assert_eq!(sizes.moe_cache_slots, 256, "full residency");
let spent = 256 * 16 * MIB;
assert_eq!(sizes.kv_pages, (8 * GIB - spent) / MIB);
}
#[test]
fn the_expert_cache_is_capped_at_full_residency() {
let sizes = plan_cache_budget((64 * GIB) as i64, MIB, MIB, 8, 64, false, 16, u64::MAX)
.expect("fits");
assert_eq!(sizes.moe_cache_slots, 64);
assert!(sizes.kv_pages > 60_000);
}
#[test]
fn a_backend_slot_ceiling_gives_its_bytes_to_kv() {
let capped =
plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 4096, false, 16, 992).expect("fits");
assert_eq!(capped.moe_cache_slots, 992);
let uncapped = plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 4096, false, 16, u64::MAX)
.expect("fits");
assert!(uncapped.moe_cache_slots > capped.moe_cache_slots);
assert!(capped.kv_pages > uncapped.kv_pages);
}
#[test]
fn the_kv_reserve_is_taken_out_before_the_cache_is_sized() {
let reserve = 1024;
let sizes = plan_cache_budget(
(2 * GIB) as i64,
MIB,
MIB,
8,
4096,
false,
reserve,
u64::MAX,
)
.expect("fits");
assert!(sizes.kv_pages >= reserve);
assert!(sizes.moe_cache_slots <= 2048 - reserve);
}
#[test]
fn a_tight_budget_drops_the_prefill_double_buffer() {
let roomy =
plan_cache_budget((4 * GIB) as i64, MIB, MIB, 8, 64, true, 16, u64::MAX).unwrap();
assert!(roomy.prefill_overlap);
let cramped = plan_cache_budget(
(4 * GIB) as i64,
MIB,
MIB,
8,
64,
true,
16,
12, )
.unwrap();
assert!(!cramped.prefill_overlap);
}
#[test]
fn a_budget_that_cannot_hold_one_layer_is_refused() {
let err = plan_cache_budget(MIB as i64, MIB, MIB, 8, 64, false, 16, u64::MAX).unwrap_err();
assert!(err.needed_bytes > err.budget_bytes);
assert_eq!(err.sizes.moe_cache_slots, 8, "one layer is the minimum");
}
#[test]
fn expert_slot_cost_is_the_sum_over_banks() {
assert_eq!(expert_bytes_per_slot(&[512, 256]), 768);
assert_eq!(expert_bytes_per_slot(&[]), 0);
}
}