use std::collections::HashMap;
use infernum_core::RequestId;
#[derive(Debug, Clone)]
pub struct KVCacheConfig {
pub num_layers: u32,
pub num_kv_heads: u32,
pub head_dim: u32,
pub max_seq_len: u32,
pub block_size: u32,
}
impl Default for KVCacheConfig {
fn default() -> Self {
Self {
num_layers: 32,
num_kv_heads: 8,
head_dim: 128,
max_seq_len: 4096,
block_size: 16,
}
}
}
#[derive(Debug)]
pub struct CacheBlock {
pub id: u32,
pub ref_count: u32,
pub allocated: bool,
}
#[derive(Debug)]
pub struct KVCache {
config: KVCacheConfig,
free_blocks: Vec<u32>,
sequence_blocks: HashMap<RequestId, Vec<u32>>,
num_blocks: u32,
}
impl KVCache {
#[must_use]
pub fn new(config: KVCacheConfig) -> Self {
let num_blocks = config.max_seq_len / config.block_size;
let free_blocks = (0..num_blocks).collect();
Self {
config,
free_blocks,
sequence_blocks: HashMap::new(),
num_blocks,
}
}
pub fn allocate(&mut self, request_id: RequestId, num_tokens: u32) -> Result<(), String> {
let blocks_needed = (num_tokens + self.config.block_size - 1) / self.config.block_size;
if blocks_needed as usize > self.free_blocks.len() {
return Err(format!(
"Not enough free blocks: need {}, have {}",
blocks_needed,
self.free_blocks.len()
));
}
let allocated: Vec<u32> = self.free_blocks.drain(..blocks_needed as usize).collect();
self.sequence_blocks.insert(request_id, allocated);
Ok(())
}
pub fn extend(&mut self, request_id: &RequestId, additional_tokens: u32) -> Result<(), String> {
let blocks = self
.sequence_blocks
.get_mut(request_id)
.ok_or_else(|| format!("Sequence {} not found", request_id))?;
let current_capacity = blocks.len() as u32 * self.config.block_size;
let current_tokens = current_capacity; let total_tokens = current_tokens + additional_tokens;
let total_blocks_needed =
(total_tokens + self.config.block_size - 1) / self.config.block_size;
let additional_blocks = total_blocks_needed.saturating_sub(blocks.len() as u32);
if additional_blocks as usize > self.free_blocks.len() {
return Err(format!(
"Not enough free blocks: need {}, have {}",
additional_blocks,
self.free_blocks.len()
));
}
let new_blocks: Vec<u32> = self
.free_blocks
.drain(..additional_blocks as usize)
.collect();
blocks.extend(new_blocks);
Ok(())
}
pub fn free(&mut self, request_id: &RequestId) {
if let Some(blocks) = self.sequence_blocks.remove(request_id) {
self.free_blocks.extend(blocks);
}
}
#[must_use]
pub fn free_block_count(&self) -> usize {
self.free_blocks.len()
}
#[must_use]
pub fn total_block_count(&self) -> u32 {
self.num_blocks
}
#[must_use]
pub fn utilization(&self) -> f32 {
let used = self.num_blocks as usize - self.free_blocks.len();
used as f32 / self.num_blocks as f32
}
#[must_use]
pub fn get_blocks(&self, request_id: &RequestId) -> Option<&[u32]> {
self.sequence_blocks.get(request_id).map(Vec::as_slice)
}
#[must_use]
pub fn config(&self) -> &KVCacheConfig {
&self.config
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_kv_cache_allocation() {
let config = KVCacheConfig {
block_size: 16,
max_seq_len: 256,
..Default::default()
};
let mut cache = KVCache::new(config);
let request_id = RequestId::new();
cache.allocate(request_id.clone(), 32).unwrap();
assert_eq!(cache.get_blocks(&request_id).unwrap().len(), 2);
cache.free(&request_id);
assert!(cache.get_blocks(&request_id).is_none());
}
}