#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![no_std]
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KvError {
ZeroDimension(&'static str),
ZeroWorkload(&'static str),
Overflow,
InvalidBudget,
}
impl fmt::Display for KvError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
KvError::ZeroDimension(field) => write!(f, "model config field `{field}` is zero"),
KvError::ZeroWorkload(field) => write!(f, "workload field `{field}` is zero"),
KvError::Overflow => f.write_str("KV cache size does not fit in u64 bytes"),
KvError::InvalidBudget => f.write_str("memory budget must be finite and non-negative"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KvPrecision {
Bf16,
Fp8,
Int4,
}
impl KvPrecision {
#[must_use]
pub fn bytes_per_element(self) -> f64 {
match self {
KvPrecision::Bf16 => 2.0,
KvPrecision::Fp8 => 1.0,
KvPrecision::Int4 => 0.5,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KvCacheConfig {
pub num_hidden_layers: u32,
pub num_key_value_heads: u32,
pub head_dim: u32,
}
impl KvCacheConfig {
pub fn new(
num_hidden_layers: u32,
num_key_value_heads: u32,
head_dim: u32,
) -> Result<Self, KvError> {
if num_hidden_layers == 0 {
return Err(KvError::ZeroDimension("num_hidden_layers"));
}
if num_key_value_heads == 0 {
return Err(KvError::ZeroDimension("num_key_value_heads"));
}
if head_dim == 0 {
return Err(KvError::ZeroDimension("head_dim"));
}
Ok(Self {
num_hidden_layers,
num_key_value_heads,
head_dim,
})
}
#[must_use]
pub fn bytes_per_token(self, precision: KvPrecision) -> f64 {
2.0 * precision.bytes_per_element()
* f64::from(self.num_hidden_layers)
* f64::from(self.num_key_value_heads)
* f64::from(self.head_dim)
}
pub fn total_bytes(
self,
precision: KvPrecision,
context_length: u64,
batch_size: u32,
) -> Result<f64, KvError> {
if context_length == 0 {
return Err(KvError::ZeroWorkload("context_length"));
}
if batch_size == 0 {
return Err(KvError::ZeroWorkload("batch_size"));
}
Ok(self.bytes_per_token(precision) * context_length as f64 * f64::from(batch_size))
}
pub fn total_bytes_u64(
self,
precision: KvPrecision,
context_length: u64,
batch_size: u32,
) -> Result<u64, KvError> {
let bytes = self.total_bytes(precision, context_length, batch_size)?;
if !bytes.is_finite() || bytes >= u64::MAX as f64 {
return Err(KvError::Overflow);
}
Ok(bytes as u64)
}
pub fn max_context(
self,
precision: KvPrecision,
budget_bytes: f64,
batch_size: u32,
) -> Result<u64, KvError> {
if !budget_bytes.is_finite() || budget_bytes < 0.0 {
return Err(KvError::InvalidBudget);
}
if batch_size == 0 {
return Err(KvError::ZeroWorkload("batch_size"));
}
let per_token = self.bytes_per_token(precision) * f64::from(batch_size);
let tokens = budget_bytes / per_token;
if tokens >= u64::MAX as f64 {
return Err(KvError::Overflow);
}
Ok(tokens as u64)
}
#[must_use]
pub fn gqa_overstatement(self, num_attention_heads: u32) -> Option<f64> {
if num_attention_heads == 0 {
return None;
}
Some(f64::from(num_attention_heads) / f64::from(self.num_key_value_heads))
}
}
#[must_use]
pub fn head_dim_from_hidden(hidden_size: u32, num_attention_heads: u32) -> Option<u32> {
if num_attention_heads == 0 || hidden_size == 0 {
return None;
}
if hidden_size % num_attention_heads != 0 {
return None;
}
Some(hidden_size / num_attention_heads)
}
pub const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
pub const MIB: f64 = 1024.0 * 1024.0;
#[cfg(test)]
mod tests {
use super::*;
fn llama31_8b() -> KvCacheConfig {
KvCacheConfig::new(32, 8, 128).unwrap()
}
#[test]
fn llama31_8b_is_128_kib_per_token() {
assert_eq!(llama31_8b().bytes_per_token(KvPrecision::Bf16), 131_072.0);
}
#[test]
fn qwen25_7b_is_56_kib_per_token() {
let cfg = KvCacheConfig::new(28, 4, 128).unwrap();
assert_eq!(cfg.bytes_per_token(KvPrecision::Bf16), 57_344.0);
}
#[test]
fn precision_scales_linearly() {
let cfg = llama31_8b();
let bf16 = cfg.bytes_per_token(KvPrecision::Bf16);
assert_eq!(cfg.bytes_per_token(KvPrecision::Fp8), bf16 / 2.0);
assert_eq!(cfg.bytes_per_token(KvPrecision::Int4), bf16 / 4.0);
}
#[test]
fn eight_k_context_is_one_gib() {
let cfg = llama31_8b();
assert_eq!(cfg.total_bytes(KvPrecision::Bf16, 8192, 1).unwrap(), GIB);
assert_eq!(
cfg.total_bytes_u64(KvPrecision::Bf16, 8192, 1).unwrap(),
1_073_741_824
);
}
#[test]
fn batch_multiplies_the_total() {
let cfg = llama31_8b();
let one = cfg.total_bytes(KvPrecision::Bf16, 4096, 1).unwrap();
let four = cfg.total_bytes(KvPrecision::Bf16, 4096, 4).unwrap();
assert_eq!(four, one * 4.0);
}
#[test]
fn max_context_round_trips_against_total_bytes() {
let cfg = llama31_8b();
let tokens = cfg.max_context(KvPrecision::Fp8, 16.0 * GIB, 4).unwrap();
assert_eq!(tokens, 65_536);
assert!(cfg.total_bytes(KvPrecision::Fp8, tokens, 4).unwrap() <= 16.0 * GIB);
assert!(cfg.total_bytes(KvPrecision::Fp8, tokens + 1, 4).unwrap() > 16.0 * GIB);
}
#[test]
fn max_context_rounds_down_and_can_be_zero() {
let cfg = llama31_8b();
let half = cfg.bytes_per_token(KvPrecision::Bf16) / 2.0;
assert_eq!(cfg.max_context(KvPrecision::Bf16, half, 1).unwrap(), 0);
assert_eq!(cfg.max_context(KvPrecision::Bf16, half * 3.0, 1).unwrap(), 1);
}
#[test]
fn gqa_overstatement_is_the_group_size() {
assert_eq!(llama31_8b().gqa_overstatement(32), Some(4.0));
let mha = KvCacheConfig::new(32, 32, 128).unwrap();
assert_eq!(mha.gqa_overstatement(32), Some(1.0));
assert_eq!(mha.gqa_overstatement(0), None);
}
#[test]
fn zero_dimensions_are_refused_by_name() {
assert_eq!(
KvCacheConfig::new(0, 8, 128),
Err(KvError::ZeroDimension("num_hidden_layers"))
);
assert_eq!(
KvCacheConfig::new(32, 0, 128),
Err(KvError::ZeroDimension("num_key_value_heads"))
);
assert_eq!(
KvCacheConfig::new(32, 8, 0),
Err(KvError::ZeroDimension("head_dim"))
);
}
#[test]
fn zero_workload_is_refused_not_zeroed() {
let cfg = llama31_8b();
assert_eq!(
cfg.total_bytes(KvPrecision::Bf16, 0, 1),
Err(KvError::ZeroWorkload("context_length"))
);
assert_eq!(
cfg.total_bytes(KvPrecision::Bf16, 1024, 0),
Err(KvError::ZeroWorkload("batch_size"))
);
}
#[test]
fn invalid_budgets_are_refused() {
let cfg = llama31_8b();
assert_eq!(
cfg.max_context(KvPrecision::Bf16, -1.0, 1),
Err(KvError::InvalidBudget)
);
assert_eq!(
cfg.max_context(KvPrecision::Bf16, f64::NAN, 1),
Err(KvError::InvalidBudget)
);
assert_eq!(
cfg.max_context(KvPrecision::Bf16, f64::INFINITY, 1),
Err(KvError::InvalidBudget)
);
}
#[test]
fn head_dim_fallback_refuses_non_integer_results() {
assert_eq!(head_dim_from_hidden(4096, 32), Some(128));
assert_eq!(head_dim_from_hidden(4096, 0), None);
assert_eq!(head_dim_from_hidden(0, 32), None);
assert_eq!(head_dim_from_hidden(4096, 33), None);
}
#[test]
fn overflow_is_reported_not_wrapped() {
let cfg = KvCacheConfig::new(u32::MAX, u32::MAX, u32::MAX).unwrap();
assert_eq!(
cfg.total_bytes_u64(KvPrecision::Bf16, u64::MAX, u32::MAX),
Err(KvError::Overflow)
);
}
}