use metrale_cache::kv_cache::{KvCacheConfig, KvCacheDtype};
use metrale_config::ModelConfig;
use crate::cli;
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
pub(super) const DEFAULT_KV_FLOOR_TOKENS: usize = 4096;
pub(super) fn kv_floor_tokens() -> usize {
match std::env::var("METRALE_KV_FLOOR_TOKENS") {
Ok(v) => v.trim().parse().unwrap_or_else(|_| {
tracing::warn!(
"METRALE_KV_FLOOR_TOKENS='{v}' is not a token count — using the default {}",
DEFAULT_KV_FLOOR_TOKENS,
);
DEFAULT_KV_FLOOR_TOKENS
}),
Err(_) => DEFAULT_KV_FLOOR_TOKENS,
}
}
pub(crate) struct PostLoadInputs<'a> {
pub(crate) total_mem: usize,
pub(crate) model_dir: &'a std::path::Path,
pub(crate) kv_dtype: KvCacheDtype,
pub(crate) w8a8_prefill_kernels: bool,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct Headroom {
pub(super) budget: usize,
pub(super) weights: usize,
pub(super) derived: usize,
pub(super) arena: usize,
pub(super) fixed: usize,
pub(super) kv_floor: usize,
pub(super) headroom: usize,
}
pub(super) enum Yardstick {
PreLoadFree(&'static str),
PostLoad(Headroom),
}
impl Yardstick {
pub(super) fn ladder_basis(
&self,
reserve_without_ring: usize,
free_mem: usize,
) -> (usize, usize) {
match self {
Self::PreLoadFree(_) => (reserve_without_ring, free_mem),
Self::PostLoad(h) => (h.kv_floor, h.headroom),
}
}
pub(super) fn name(&self) -> &'static str {
match self {
Self::PreLoadFree(_) => "pre-load free memory",
Self::PostLoad(_) => "the predicted post-load KV headroom",
}
}
pub(super) fn total_label(&self) -> &'static str {
match self {
Self::PreLoadFree(_) => "reserve",
Self::PostLoad(_) => "ring + KV floor",
}
}
}
pub(super) fn post_load_yardstick(
args: &cli::ServeArgs,
config: &ModelConfig,
inputs: &PostLoadInputs<'_>,
fixed_reserve: usize,
arena: usize,
) -> Yardstick {
let route = metrale_model_arch::weight_loader::predicted_residency::Fp8RouteInputs::from_env(
config,
inputs.w8a8_prefill_kernels,
);
let estimate = metrale_model_arch::weight_loader::predicted_residency::predicted_derived_bytes(
config, &route,
);
let Some(derived) = estimate.bytes() else {
return Yardstick::PreLoadFree(
estimate
.reason()
.unwrap_or("the derived-residency prediction declined"),
);
};
let Some(weights) = checkpoint_bytes(inputs.model_dir) else {
return Yardstick::PreLoadFree("the checkpoint's on-disk size could not be read");
};
let budget = (inputs.total_mem as f64 * args.gpu_memory_utilization) as usize;
let weights = weights as usize;
let derived = derived as usize;
let pre_kv = weights
.saturating_add(derived)
.saturating_add(arena)
.saturating_add(fixed_reserve);
Yardstick::PostLoad(Headroom {
budget,
weights,
derived,
arena,
fixed: fixed_reserve,
kv_floor: kv_floor_bytes(args, config, inputs.kv_dtype),
headroom: budget.saturating_sub(pre_kv),
})
}
pub(super) fn kv_floor_bytes(
args: &cli::ServeArgs,
config: &ModelConfig,
kv_dtype: KvCacheDtype,
) -> usize {
let tokens = kv_floor_tokens().min(args.max_seq_len);
if tokens == 0 || args.max_batch_size.ceiling() == 0 {
return 0;
}
args.max_batch_size.ceiling() * tokens * kv_bytes_per_token(args, config, kv_dtype)
}
pub(super) fn kv_bytes_per_token(
args: &cli::ServeArgs,
config: &ModelConfig,
kv_dtype: KvCacheDtype,
) -> usize {
let (num_kv_heads, head_dim) = if config.kv_lora_rank > 0 {
(1, config.kv_lora_rank + config.qk_rope_head_dim)
} else {
(config.num_key_value_heads, config.head_dim)
};
let block_size = args.block_size.max(1);
let kv = KvCacheConfig {
block_size,
num_kv_heads,
head_dim,
num_layers: config.num_attention_layers(),
dtype: kv_dtype,
layer_dtypes: Vec::new(),
layer_dims: Vec::new(),
cache_blocks_per_seq: None,
v_block_bytes: None,
};
kv.block_bytes_kv_all_layers() / block_size
}
pub(super) fn checkpoint_bytes(model_dir: &std::path::Path) -> Option<u64> {
if let Some(total) = indexed_shard_bytes(model_dir) {
return Some(total);
}
for ext in ["safetensors", "gguf"] {
let total: u64 = std::fs::read_dir(model_dir)
.ok()?
.filter_map(|e| e.ok())
.filter(|e| {
e.path()
.extension()
.and_then(|x| x.to_str())
.is_some_and(|x| x == ext)
})
.filter_map(|e| e.metadata().ok().map(|m| m.len()))
.sum();
if total > 0 {
return Some(total);
}
}
None
}
fn indexed_shard_bytes(model_dir: &std::path::Path) -> Option<u64> {
let index = [
"model.safetensors.index.json",
"consolidated.safetensors.index.json",
]
.into_iter()
.map(|n| model_dir.join(n))
.find(|p| p.exists())?;
let raw = std::fs::read_to_string(&index).ok()?;
let v: serde_json::Value = serde_json::from_str(&raw).ok()?;
let map = v.get("weight_map")?.as_object()?;
let shards: std::collections::HashSet<&str> = map.values().filter_map(|s| s.as_str()).collect();
let total: u64 = shards
.iter()
.filter_map(|s| std::fs::metadata(model_dir.join(s)).ok().map(|m| m.len()))
.sum();
(total > 0).then_some(total)
}
impl Headroom {
pub(super) fn describe(&self, util: f64, ring_bytes: usize, slots: usize) -> String {
let gb = |b: usize| b as f64 / GIB;
format!(
"budget {:.2} GB ({:.0}% util) = weights {:.2} + derived {:.2} + arena {:.2} + \
fixed reserve {:.2} -> headroom {:.2} GB; ring({}) {:.2} + KV floor {:.2} = {:.2} GB",
gb(self.budget),
util * 100.0,
gb(self.weights),
gb(self.derived),
gb(self.arena),
gb(self.fixed),
gb(self.headroom),
slots,
gb(ring_bytes),
gb(self.kv_floor),
gb(ring_bytes + self.kv_floor),
)
}
}
#[cfg(test)]
#[path = "headroom_tests.rs"]
mod tests;