use crate::config::ModelConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KvElem {
F32,
F16,
Q8_0,
Turbo4,
}
impl KvElem {
pub fn bytes_for(self, elems: u64) -> u64 {
match self {
KvElem::F32 => elems * 4,
KvElem::F16 => elems * 2,
KvElem::Q8_0 => {
let blocks = elems.div_ceil(ferrox_quant::Q8_0_BLOCK_ELEMS as u64);
blocks * ferrox_quant::Q8_0_BLOCK_BYTES as u64
}
KvElem::Turbo4 => {
let blocks = elems.div_ceil(ferrox_quant::TURBO4_KV_GROUP as u64);
blocks * ferrox_quant::TURBO4_KV_BLOCK_BYTES as u64
}
}
}
pub fn as_str(self) -> &'static str {
match self {
KvElem::F32 => "f32",
KvElem::F16 => "f16",
KvElem::Q8_0 => "q8_0",
KvElem::Turbo4 => "turbo4",
}
}
pub fn from_ctk(value: &str) -> Self {
match value.trim().to_ascii_lowercase().as_str() {
"f32" => KvElem::F32,
"q8_0" | "turbo8" | "fp8" => KvElem::Q8_0,
"turbo4" => KvElem::Turbo4,
_ => KvElem::F16,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KvLayout {
Gqa { n_kv_heads: usize, head_dim: usize },
MlaLatent {
kv_lora_rank: usize,
qk_rope_head_dim: usize,
},
MlaExpanded {
n_heads: usize,
k_head_dim: usize,
v_head_dim: usize,
},
}
impl KvLayout {
pub fn elems_per_token_per_layer(self) -> u64 {
match self {
KvLayout::Gqa {
n_kv_heads,
head_dim,
} => 2 * n_kv_heads as u64 * head_dim as u64,
KvLayout::MlaLatent {
kv_lora_rank,
qk_rope_head_dim,
} => kv_lora_rank as u64 + qk_rope_head_dim as u64,
KvLayout::MlaExpanded {
n_heads,
k_head_dim,
v_head_dim,
} => n_heads as u64 * (k_head_dim as u64 + v_head_dim as u64),
}
}
pub fn describe(self) -> String {
match self {
KvLayout::Gqa {
n_kv_heads,
head_dim,
} => format!("2 (K+V) x {n_kv_heads} kv-heads x {head_dim} head-dim"),
KvLayout::MlaLatent {
kv_lora_rank,
qk_rope_head_dim,
} => format!(
"MLA latent: {kv_lora_rank} kv_lora_rank + {qk_rope_head_dim} rope-dim \
(one vector, no K/V doubling)"
),
KvLayout::MlaExpanded {
n_heads,
k_head_dim,
v_head_dim,
} => format!(
"MLA expanded: {n_heads} heads x ({k_head_dim} K head-dim + \
{v_head_dim} V head-dim)"
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlidingWindow {
pub window: usize,
pub chunk: usize,
pub pattern: Option<usize>,
}
impl SlidingWindow {
pub fn resident_positions(&self, tokens: usize) -> usize {
tokens.min(self.window + self.chunk.max(1) - 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KvShape {
pub n_layers: usize,
pub layout: KvLayout,
pub elem: KvElem,
pub sliding: Option<SlidingWindow>,
}
impl KvShape {
pub fn from_config(config: &ModelConfig, elem: KvElem, chunk: usize) -> Self {
KvShape {
n_layers: config.n_layers,
layout: KvLayout::Gqa {
n_kv_heads: config.n_kv_heads,
head_dim: config.head_dim,
},
elem,
sliding: config.sliding_window.map(|window| SlidingWindow {
window,
chunk,
pattern: config.swa_pattern,
}),
}
}
pub fn mla_expanded(
n_layers: usize,
n_heads: usize,
qk_nope_head_dim: usize,
qk_rope_head_dim: usize,
v_head_dim: usize,
elem: KvElem,
) -> Self {
KvShape {
n_layers,
layout: KvLayout::MlaExpanded {
n_heads,
k_head_dim: qk_nope_head_dim + qk_rope_head_dim,
v_head_dim,
},
elem,
sliding: None,
}
}
pub fn sliding_layers(&self) -> usize {
match self.sliding {
None => 0,
Some(SlidingWindow { pattern: None, .. }) => self.n_layers,
Some(SlidingWindow {
pattern: Some(period),
..
}) => {
if period <= 1 {
self.n_layers
} else {
self.n_layers - self.n_layers / period
}
}
}
}
pub fn full_attention_layers(&self) -> usize {
self.n_layers - self.sliding_layers()
}
pub fn per_token_kv_bytes(&self) -> u64 {
self.n_layers as u64 * self.elem.bytes_for(self.layout.elems_per_token_per_layer())
}
pub fn marginal_per_token_bytes(&self) -> u64 {
self.full_attention_layers() as u64
* self.elem.bytes_for(self.layout.elems_per_token_per_layer())
}
pub fn kv_bytes_for_tokens(&self, tokens: usize) -> u64 {
let per_layer = self.layout.elems_per_token_per_layer();
let full =
self.full_attention_layers() as u64 * self.elem.bytes_for(per_layer * tokens as u64);
let sliding = match self.sliding {
None => 0,
Some(w) => {
self.sliding_layers() as u64
* self
.elem
.bytes_for(per_layer * w.resident_positions(tokens) as u64)
}
};
full + sliding
}
pub fn describe(&self) -> String {
let base = format!(
"{} layers x [{}] x {} = {} bytes/token",
self.n_layers,
self.layout.describe(),
self.elem.as_str(),
self.per_token_kv_bytes()
);
match self.sliding {
None => base,
Some(w) => format!(
"{base}; {} of {} layers slide and cap at min(tokens, {} window + {} chunk - 1) \
= {} positions, leaving {} bytes/token marginal",
self.sliding_layers(),
self.n_layers,
w.window,
w.chunk,
w.window + w.chunk.max(1) - 1,
self.marginal_per_token_bytes(),
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ceiling {
ContextLength,
DeviceMemory,
}
impl Ceiling {
pub fn code(self) -> &'static str {
match self {
Ceiling::ContextLength => "context_length_exceeded",
Ceiling::DeviceMemory => "device_memory_budget_exceeded",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("{code}: {detail} (estimated {estimated_bytes} bytes vs limit {limit_bytes} bytes)",
code = self.binding.code())]
pub struct KvBudgetError {
pub binding: Ceiling,
pub estimated_bytes: u64,
pub limit_bytes: u64,
pub detail: String,
}
impl KvBudgetError {
pub fn code(&self) -> &'static str {
self.binding.code()
}
pub fn overage_bytes(&self) -> u64 {
self.estimated_bytes.saturating_sub(self.limit_bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KvBudget {
pub weights_bytes: u64,
pub activation_headroom_bytes: u64,
pub device_budget_bytes: u64,
pub shape: KvShape,
pub concurrent_requests: usize,
}
impl KvBudget {
pub fn kv_bytes_available(&self) -> u64 {
self.device_budget_bytes
.saturating_sub(self.weights_bytes)
.saturating_sub(self.activation_headroom_bytes)
}
pub fn estimated_bytes(&self, tokens: usize) -> u64 {
self.weights_bytes
+ self.activation_headroom_bytes
+ self.shape.kv_bytes_for_tokens(tokens) * self.concurrent_requests.max(1) as u64
}
pub fn check(&self, tokens: usize) -> Result<u64, KvBudgetError> {
let estimated = self.estimated_bytes(tokens);
if estimated <= self.device_budget_bytes {
return Ok(estimated);
}
Err(KvBudgetError {
binding: Ceiling::DeviceMemory,
estimated_bytes: estimated,
limit_bytes: self.device_budget_bytes,
detail: format!(
"{} weight bytes + {} KV bytes at {tokens} tokens x{} concurrent + {} \
activation headroom exceeds the {} byte device budget",
self.weights_bytes,
self.shape.kv_bytes_for_tokens(tokens) * self.concurrent_requests.max(1) as u64,
self.concurrent_requests.max(1),
self.activation_headroom_bytes,
self.device_budget_bytes,
),
})
}
pub fn max_context(&self, cap: usize, granularity: usize) -> ContextFit {
let granularity = granularity.max(1);
let concurrency = self.concurrent_requests.max(1) as u64;
let available = self.kv_bytes_available();
let marginal = self.shape.marginal_per_token_bytes() * concurrency;
let saturated_sliding = {
let mut shape = self.shape;
shape.n_layers = shape.sliding_layers();
match shape.sliding {
None => 0,
Some(w) => {
shape.n_layers as u64
* shape.elem.bytes_for(
shape.layout.elems_per_token_per_layer()
* w.resident_positions(cap) as u64,
)
* concurrency
}
}
};
let for_full_layers = available.saturating_sub(saturated_sliding);
let (tokens, capped_by) = if available == 0 || for_full_layers == 0 && marginal > 0 {
(0, ContextCap::DeviceBudget)
} else {
match for_full_layers.checked_div(marginal) {
None => (cap, ContextCap::ModelContextLength),
Some(raw) => {
let raw = raw as usize;
let floored = if raw >= granularity {
(raw / granularity) * granularity
} else {
raw
};
if floored >= cap {
(cap, ContextCap::ModelContextLength)
} else {
(floored, ContextCap::DeviceBudget)
}
}
}
};
ContextFit {
tokens,
cap,
granularity,
capped_by,
kv_available_bytes: available,
marginal_per_token_bytes: self.shape.marginal_per_token_bytes(),
concurrent_requests: concurrency as usize,
kv_bytes: self.shape.kv_bytes_for_tokens(tokens) * concurrency,
weights_bytes: self.weights_bytes,
activation_headroom_bytes: self.activation_headroom_bytes,
device_budget_bytes: self.device_budget_bytes,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextCap {
ModelContextLength,
DeviceBudget,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ContextFit {
pub tokens: usize,
pub cap: usize,
pub granularity: usize,
pub capped_by: ContextCap,
pub kv_available_bytes: u64,
pub marginal_per_token_bytes: u64,
pub concurrent_requests: usize,
pub kv_bytes: u64,
pub weights_bytes: u64,
pub activation_headroom_bytes: u64,
pub device_budget_bytes: u64,
}
impl std::fmt::Display for ContextFit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ctx auto = {} tokens ({}): ({} device budget - {} weights - {} activation headroom) \
= {} for KV; / {} bytes/token/request / {} request(s) -> rounded down to a multiple \
of {} (reported exactly below one step), capped at the model's {} trained context. \
KV at the chosen context: {} bytes.",
self.tokens,
match self.capped_by {
ContextCap::ModelContextLength => "limited by the model's context length",
ContextCap::DeviceBudget => "limited by the device memory budget",
},
self.device_budget_bytes,
self.weights_bytes,
self.activation_headroom_bytes,
self.kv_available_bytes,
self.marginal_per_token_bytes,
self.concurrent_requests,
self.granularity,
self.cap,
self.kv_bytes,
)
}
}
pub const CTX_AUTO_GRANULARITY: usize = 256;
#[cfg(test)]
mod tests {
use super::*;
fn llama31_8b() -> KvShape {
KvShape {
n_layers: 32,
layout: KvLayout::Gqa {
n_kv_heads: 8,
head_dim: 128,
},
elem: KvElem::F32,
sliding: None,
}
}
#[test]
fn gqa_per_token_kv_matches_the_hand_computed_byte_count() {
let shape = llama31_8b();
assert_eq!(shape.layout.elems_per_token_per_layer(), 2 * 8 * 128);
assert_eq!(shape.per_token_kv_bytes(), 32 * 2 * 8 * 128 * 4);
assert_eq!(shape.per_token_kv_bytes(), 262_144);
assert_eq!(
KvShape {
elem: KvElem::F16,
..shape
}
.per_token_kv_bytes(),
131_072
);
assert_eq!(
KvShape {
elem: KvElem::Q8_0,
..shape
}
.per_token_kv_bytes(),
32 * (2 * 8 * 128 / 32) * 34
);
assert_eq!(
KvShape {
elem: KvElem::Turbo4,
..shape
}
.per_token_kv_bytes(),
32 * (2 * 8 * 128 / 32) * 18
);
}
#[test]
fn ctk_names_map_onto_the_widths_metal_really_writes() {
assert_eq!(KvElem::from_ctk("f16"), KvElem::F16);
assert_eq!(KvElem::from_ctk("f32"), KvElem::F32);
assert_eq!(KvElem::from_ctk("Q8_0"), KvElem::Q8_0);
assert_eq!(KvElem::from_ctk("turbo8"), KvElem::Q8_0);
assert_eq!(KvElem::from_ctk("fp8"), KvElem::Q8_0);
assert_eq!(KvElem::from_ctk("turbo4"), KvElem::Turbo4);
assert_eq!(KvElem::from_ctk("turbo3"), KvElem::F16);
assert_eq!(KvElem::from_ctk(" nonsense "), KvElem::F16);
}
#[test]
fn mha_costs_exactly_the_gqa_ratio_more_than_gqa() {
let gqa = llama31_8b();
let mha = KvShape {
layout: KvLayout::Gqa {
n_kv_heads: 32,
head_dim: 128,
},
..gqa
};
assert_eq!(mha.per_token_kv_bytes(), 4 * gqa.per_token_kv_bytes());
assert_eq!(mha.per_token_kv_bytes(), 32 * 2 * 32 * 128 * 4);
}
#[test]
fn sliding_window_layers_saturate_and_full_layers_do_not() {
let shape = KvShape {
n_layers: 32,
layout: KvLayout::Gqa {
n_kv_heads: 8,
head_dim: 128,
},
elem: KvElem::F16,
sliding: Some(SlidingWindow {
window: 4096,
chunk: 1,
pattern: None,
}),
};
assert_eq!(shape.sliding_layers(), 32);
assert_eq!(shape.full_attention_layers(), 0);
assert_eq!(
shape.kv_bytes_for_tokens(1024),
shape.per_token_kv_bytes() * 1024
);
let at_window = shape.kv_bytes_for_tokens(4096);
assert_eq!(shape.kv_bytes_for_tokens(32_768), at_window);
assert_eq!(shape.kv_bytes_for_tokens(1_000_000), at_window);
assert_eq!(shape.marginal_per_token_bytes(), 0);
}
#[test]
fn chunked_prefill_widens_the_sliding_cap_by_chunk_minus_one() {
let base = SlidingWindow {
window: 512,
chunk: 1,
pattern: None,
};
assert_eq!(base.resident_positions(100_000), 512);
let chunked = SlidingWindow { chunk: 256, ..base };
assert_eq!(chunked.resident_positions(100_000), 512 + 256 - 1);
assert_eq!(chunked.resident_positions(300), 300);
}
#[test]
fn gemma_alternating_pattern_leaves_every_sixth_layer_full_attention() {
let shape = KvShape {
n_layers: 30,
layout: KvLayout::Gqa {
n_kv_heads: 4,
head_dim: 256,
},
elem: KvElem::F16,
sliding: Some(SlidingWindow {
window: 1024,
chunk: 1,
pattern: Some(6),
}),
};
assert_eq!(shape.full_attention_layers(), 5);
assert_eq!(shape.sliding_layers(), 25);
let mut cfg = crate::config::test_dense_fixture();
cfg.n_layers = 30;
cfg.sliding_window = Some(1024);
cfg.swa_pattern = Some(6);
let per_layer_full = (0..30)
.filter(|&il| cfg.layer_sliding_window(il).is_none())
.count();
assert_eq!(per_layer_full, shape.full_attention_layers());
let per_layer_token = shape.elem.bytes_for(2 * 4 * 256);
assert_eq!(shape.marginal_per_token_bytes(), 5 * per_layer_token);
assert_eq!(
shape.kv_bytes_for_tokens(8192),
5 * shape.elem.bytes_for(2 * 4 * 256 * 8192)
+ 25 * shape.elem.bytes_for(2 * 4 * 256 * 1024)
);
}
#[test]
fn mla_latent_is_one_vector_and_far_cheaper_than_the_expanded_form() {
let latent = KvShape {
n_layers: 60,
layout: KvLayout::MlaLatent {
kv_lora_rank: 512,
qk_rope_head_dim: 64,
},
elem: KvElem::F32,
sliding: None,
};
assert_eq!(latent.layout.elems_per_token_per_layer(), 576);
assert_eq!(latent.per_token_kv_bytes(), 60 * 576 * 4);
let expanded = KvShape::mla_expanded(60, 128, 128, 64, 128, KvElem::F32);
assert_eq!(
expanded.layout.elems_per_token_per_layer(),
128 * (192 + 128)
);
assert_eq!(expanded.per_token_kv_bytes(), 60 * 40_960 * 4);
assert!(expanded.per_token_kv_bytes() / latent.per_token_kv_bytes() > 70);
let gqa = KvShape {
layout: KvLayout::Gqa {
n_kv_heads: 128,
head_dim: 128,
},
..latent
};
assert_eq!(gqa.per_token_kv_bytes(), 60 * 2 * 128 * 128 * 4);
}
#[test]
fn from_config_reads_layers_heads_and_the_sliding_window() {
let mut cfg = crate::config::test_dense_fixture();
cfg.n_layers = 12;
cfg.n_kv_heads = 2;
cfg.head_dim = 64;
cfg.sliding_window = None;
let shape = KvShape::from_config(&cfg, KvElem::F32, 1);
assert_eq!(shape.n_layers, 12);
assert_eq!(shape.per_token_kv_bytes(), 12 * 2 * 2 * 64 * 4);
assert!(shape.sliding.is_none());
cfg.sliding_window = Some(256);
cfg.swa_pattern = None;
let swa = KvShape::from_config(&cfg, KvElem::F32, 64);
assert_eq!(
swa.sliding,
Some(SlidingWindow {
window: 256,
chunk: 64,
pattern: None
})
);
assert_eq!(swa.sliding_layers(), 12);
}
fn budget(weights: u64, device: u64, shape: KvShape) -> KvBudget {
KvBudget {
weights_bytes: weights,
activation_headroom_bytes: 0,
device_budget_bytes: device,
shape,
concurrent_requests: 1,
}
}
#[test]
fn check_accepts_a_fitting_context_and_names_the_binding_ceiling_otherwise() {
let shape = llama31_8b(); let b = budget(1_000_000, 1_000_000 + 262_144 * 10, shape);
assert_eq!(b.check(10).unwrap(), 1_000_000 + 262_144 * 10);
let err = b.check(11).expect_err("one token past the budget");
assert_eq!(err.binding, Ceiling::DeviceMemory);
assert_eq!(err.code(), "device_memory_budget_exceeded");
assert_eq!(err.estimated_bytes, 1_000_000 + 262_144 * 11);
assert_eq!(err.limit_bytes, 1_000_000 + 262_144 * 10);
assert_eq!(err.overage_bytes(), 262_144);
}
#[test]
fn concurrency_multiplies_kv_but_not_weights() {
let shape = llama31_8b();
let one = budget(1_000, 1 << 40, shape);
let four = KvBudget {
concurrent_requests: 4,
..one
};
assert_eq!(
four.estimated_bytes(100) - 1_000,
4 * (one.estimated_bytes(100) - 1_000)
);
}
#[test]
fn max_context_is_the_closed_form_division_floored_to_granularity() {
let shape = llama31_8b(); let b = budget(5_000_000, 5_000_000 + 262_144 * 1000, shape);
let fit = b.max_context(131_072, 256);
assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
assert_eq!(fit.tokens, 768);
assert_eq!(fit.kv_available_bytes, 262_144 * 1000);
assert_eq!(fit.marginal_per_token_bytes, 262_144);
assert!(b.check(fit.tokens).is_ok());
assert!(b.check(fit.tokens + 256).is_err());
}
#[test]
fn max_context_clamps_to_the_models_trained_context_when_memory_is_plentiful() {
let b = budget(1_000, 1 << 40, llama31_8b());
let fit = b.max_context(8192, 256);
assert_eq!(fit.tokens, 8192);
assert_eq!(fit.capped_by, ContextCap::ModelContextLength);
}
#[test]
fn a_context_under_one_granularity_step_is_reported_exactly_not_floored_away() {
let shape = llama31_8b(); let b = budget(1_000, 1_000 + 262_144 * 100, shape);
let fit = b.max_context(131_072, 256);
assert_eq!(fit.tokens, 100);
assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
assert!(b.check(fit.tokens).is_ok());
assert!(b.check(fit.tokens + 1).is_err());
}
#[test]
fn max_context_is_zero_when_the_weights_alone_do_not_fit() {
let b = budget(10_000_000, 1_000_000, llama31_8b());
let fit = b.max_context(8192, 256);
assert_eq!(fit.tokens, 0);
assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
assert_eq!(fit.kv_available_bytes, 0);
assert!(b.check(0).is_err(), "weights alone already overflow");
}
#[test]
fn an_all_sliding_model_is_limited_only_by_its_context_length() {
let shape = KvShape {
n_layers: 32,
layout: KvLayout::Gqa {
n_kv_heads: 8,
head_dim: 128,
},
elem: KvElem::F16,
sliding: Some(SlidingWindow {
window: 4096,
chunk: 1,
pattern: None,
}),
};
let saturated = shape.kv_bytes_for_tokens(4096);
let b = budget(1_000, 1_000 + saturated * 2, shape);
let fit = b.max_context(1_000_000, 256);
assert_eq!(fit.capped_by, ContextCap::ModelContextLength);
assert_eq!(fit.tokens, 1_000_000);
assert!(b.check(fit.tokens).is_ok());
}
#[test]
fn a_mixed_swa_model_prices_the_saturated_sliding_layers_before_dividing() {
let shape = KvShape {
n_layers: 6,
layout: KvLayout::Gqa {
n_kv_heads: 1,
head_dim: 16,
},
elem: KvElem::F32,
sliding: Some(SlidingWindow {
window: 128,
chunk: 1,
pattern: Some(3),
}),
};
assert_eq!(shape.full_attention_layers(), 2);
let per_layer_token = 2 * 16 * 4;
let sliding_saturated = 4 * per_layer_token * 128;
let full_marginal = 2 * per_layer_token;
let b = budget(0, (sliding_saturated + full_marginal * 512) as u64, shape);
let fit = b.max_context(4096, 256);
assert_eq!(fit.tokens, 512);
assert_eq!(fit.capped_by, ContextCap::DeviceBudget);
assert!(b.check(512).is_ok());
}
#[test]
fn ctx_auto_explanation_names_every_term_it_divided() {
let b = budget(5_000_000, 5_000_000 + 262_144 * 1000, llama31_8b());
let text = b.max_context(131_072, CTX_AUTO_GRANULARITY).to_string();
assert!(text.contains("ctx auto = 768 tokens"), "{text}");
assert!(text.contains("262144"), "per-token divisor missing: {text}");
assert!(text.contains("5000000"), "weights term missing: {text}");
assert!(text.contains("131072"), "model cap missing: {text}");
}
}