use crate::serve::config::LayerType;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KvDType {
F32,
F16,
}
impl KvDType {
pub fn elem_bytes(self) -> usize {
match self {
KvDType::F32 => 4,
KvDType::F16 => 2,
}
}
pub fn to_mlx_dtype(self) -> mlx_native::DType {
match self {
KvDType::F32 => mlx_native::DType::F32,
KvDType::F16 => mlx_native::DType::F16,
}
}
}
#[derive(Debug, Clone)]
pub struct KvSpillDescriptor {
pub sliding_window: usize,
pub max_decode_tokens: usize,
pub num_layers: usize,
pub layer_types: Vec<LayerType>,
pub nkv_heads: Vec<usize>,
pub head_dim: Vec<usize>,
pub kv_dtype: KvDType,
pub provenance: KvSpillProvenance,
}
#[derive(Debug, Clone, Default)]
pub struct KvSpillProvenance {
pub producer_version: String,
pub source_sha256: String,
pub tokenizer_chat_template_hash: String,
}
impl KvSpillProvenance {
pub fn is_hf2q(&self) -> bool {
!self.producer_version.is_empty() || !self.source_sha256.is_empty()
}
pub fn hash_chat_template(template: &str) -> String {
if template.is_empty() {
return String::new();
}
use sha2::{Digest, Sha256};
let digest = Sha256::digest(template.as_bytes());
let mut s = String::with_capacity(64);
for b in digest.iter() {
use std::fmt::Write;
let _ = write!(&mut s, "{b:02x}");
}
s
}
}
impl KvSpillDescriptor {
pub fn from_gemma_loaded_model(
weights: &crate::inference::models::gemma4::MlxModelWeights,
max_decode_tokens: usize,
kv_dtype: KvDType,
provenance: KvSpillProvenance,
) -> Self {
let num_layers = weights.layers.len();
let mut layer_types = Vec::with_capacity(num_layers);
let mut nkv_heads = Vec::with_capacity(num_layers);
let mut head_dim = Vec::with_capacity(num_layers);
for layer in &weights.layers {
layer_types.push(layer.layer_type);
nkv_heads.push(layer.num_kv_heads);
head_dim.push(layer.head_dim);
}
Self {
sliding_window: weights.sliding_window,
max_decode_tokens,
num_layers,
layer_types,
nkv_heads,
head_dim,
kv_dtype,
provenance,
}
}
}
#[cfg(test)]
mod kv_spill_descriptor_tests {
use super::*;
#[test]
fn kv_dtype_elem_bytes_match_byte_widths() {
assert_eq!(KvDType::F32.elem_bytes(), 4);
assert_eq!(KvDType::F16.elem_bytes(), 2);
}
#[test]
fn kv_dtype_round_trips_through_mlx_native_dtype() {
assert_eq!(KvDType::F32.to_mlx_dtype(), mlx_native::DType::F32);
assert_eq!(KvDType::F16.to_mlx_dtype(), mlx_native::DType::F16);
assert_ne!(KvDType::F32.to_mlx_dtype(), KvDType::F16.to_mlx_dtype());
}
#[test]
fn descriptor_clone_preserves_all_fields() {
let d = KvSpillDescriptor {
sliding_window: 1024,
max_decode_tokens: 512,
num_layers: 4,
layer_types: vec![
LayerType::Sliding,
LayerType::Sliding,
LayerType::Full,
LayerType::Sliding,
],
nkv_heads: vec![2, 2, 1, 2],
head_dim: vec![256, 256, 512, 256],
kv_dtype: KvDType::F32,
provenance: KvSpillProvenance::default(),
};
let c = d.clone();
assert_eq!(c.sliding_window, 1024);
assert_eq!(c.max_decode_tokens, 512);
assert_eq!(c.num_layers, 4);
assert_eq!(c.layer_types.len(), 4);
assert_eq!(c.layer_types[2], LayerType::Full);
assert_eq!(c.nkv_heads, vec![2, 2, 1, 2]);
assert_eq!(c.head_dim, vec![256, 256, 512, 256]);
assert_eq!(c.kv_dtype, KvDType::F32);
assert_eq!(c.provenance.producer_version, "");
}
#[test]
fn descriptor_vector_lengths_must_be_equal_to_num_layers() {
let d = KvSpillDescriptor {
sliding_window: 16,
max_decode_tokens: 32,
num_layers: 3,
layer_types: vec![LayerType::Sliding, LayerType::Full, LayerType::Sliding],
nkv_heads: vec![2, 1, 2],
head_dim: vec![8, 16, 8],
kv_dtype: KvDType::F16,
provenance: KvSpillProvenance::default(),
};
assert_eq!(d.layer_types.len(), d.num_layers);
assert_eq!(d.nkv_heads.len(), d.num_layers);
assert_eq!(d.head_dim.len(), d.num_layers);
}
#[test]
fn f16_descriptor_halves_per_element_bytes() {
let d_f32 = KvSpillDescriptor {
sliding_window: 16,
max_decode_tokens: 32,
num_layers: 1,
layer_types: vec![LayerType::Sliding],
nkv_heads: vec![2],
head_dim: vec![8],
kv_dtype: KvDType::F32,
provenance: KvSpillProvenance::default(),
};
let d_f16 = KvSpillDescriptor {
kv_dtype: KvDType::F16,
..d_f32.clone()
};
let cost_f32 = d_f32.head_dim[0] * d_f32.kv_dtype.elem_bytes();
let cost_f16 = d_f16.head_dim[0] * d_f16.kv_dtype.elem_bytes();
assert_eq!(cost_f32, 32, "8 * 4 bytes per head/token (F32)");
assert_eq!(cost_f16, 16, "8 * 2 bytes per head/token (F16)");
assert_eq!(
cost_f32,
cost_f16 * 2,
"F32 is exactly twice the byte cost of F16 per element"
);
}
}