use fuel::lazy::Tensor;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
pub type LayerKvState = (Tensor, Tensor);
type PrefixHash = u64;
struct CacheEntry {
kv_states: Vec<LayerKvState>,
seq_len: usize,
last_access: u64,
}
pub struct PrefixCache {
entries: HashMap<PrefixHash, CacheEntry>,
max_entries: usize,
access_counter: u64,
}
impl PrefixCache {
pub fn new(max_entries: usize) -> Self {
Self {
entries: HashMap::new(),
max_entries: max_entries.max(1),
access_counter: 0,
}
}
pub fn lookup(&mut self, tokens: &[u32]) -> Option<Vec<LayerKvState>> {
let hash = hash_tokens(tokens);
if let Some(entry) = self.entries.get_mut(&hash) {
self.access_counter += 1;
entry.last_access = self.access_counter;
Some(
entry
.kv_states
.iter()
.map(|(k, v): &LayerKvState| (k.clone(), v.clone()))
.collect(),
)
} else {
None
}
}
pub fn insert(&mut self, tokens: &[u32], kv_states: Vec<LayerKvState>) {
let hash = hash_tokens(tokens);
let seq_len = tokens.len();
if !self.entries.contains_key(&hash) && self.entries.len() >= self.max_entries {
self.evict_lru();
}
self.access_counter += 1;
self.entries.insert(
hash,
CacheEntry {
kv_states,
seq_len,
last_access: self.access_counter,
},
);
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn max_entries(&self) -> usize {
self.max_entries
}
pub fn clear(&mut self) {
self.entries.clear();
self.access_counter = 0;
}
pub fn contains(&self, tokens: &[u32]) -> bool {
let hash = hash_tokens(tokens);
self.entries.contains_key(&hash)
}
pub fn cached_seq_len(&self, tokens: &[u32]) -> Option<usize> {
let hash = hash_tokens(tokens);
self.entries.get(&hash).map(|e| e.seq_len)
}
pub fn longest_prefix_match(&mut self, tokens: &[u32]) -> Option<(usize, Vec<LayerKvState>)> {
for len in (1..=tokens.len()).rev() {
let prefix = &tokens[..len];
if let Some(kv) = self.lookup(prefix) {
return Some((len, kv));
}
}
None
}
fn evict_lru(&mut self) {
if let Some((&lru_key, _)) = self
.entries
.iter()
.min_by_key(|(_, entry)| entry.last_access)
{
self.entries.remove(&lru_key);
}
}
}
fn hash_tokens(tokens: &[u32]) -> PrefixHash {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
tokens.hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use fuel::Device;
fn make_kv(layers: usize, seq_len: usize) -> Vec<LayerKvState> {
(0..layers)
.map(|_| {
let dims = fuel::Shape::from_dims(&[1, 4, seq_len, 64]);
let k = Tensor::from_f32(
vec![0.0_f32; 4 * seq_len * 64],
dims.clone(),
&Device::cpu(),
)
.unwrap();
let v = Tensor::from_f32(vec![0.0_f32; 4 * seq_len * 64], dims, &Device::cpu())
.unwrap();
(k, v)
})
.collect()
}
#[test]
fn basic_insert_and_lookup() {
let mut cache = PrefixCache::new(4);
let tokens = vec![1u32, 2, 3, 4, 5];
assert!(cache.lookup(&tokens).is_none());
let kv = make_kv(2, 5);
cache.insert(&tokens, kv);
let cached = cache.lookup(&tokens);
assert!(cached.is_some());
let cached = cached.unwrap();
assert_eq!(cached.len(), 2);
assert_eq!(cached[0].0.shape().dims(), &[1, 4, 5, 64]);
}
#[test]
fn different_prefixes_are_separate() {
let mut cache = PrefixCache::new(4);
let tokens_a = vec![1u32, 2, 3];
let tokens_b = vec![4u32, 5, 6];
cache.insert(&tokens_a, make_kv(1, 3));
cache.insert(&tokens_b, make_kv(1, 3));
assert!(cache.lookup(&tokens_a).is_some());
assert!(cache.lookup(&tokens_b).is_some());
assert_eq!(cache.len(), 2);
}
#[test]
fn lru_eviction() {
let mut cache = PrefixCache::new(2);
let a = vec![1u32];
let b = vec![2u32];
let c = vec![3u32];
cache.insert(&a, make_kv(1, 1));
cache.insert(&b, make_kv(1, 1));
assert_eq!(cache.len(), 2);
cache.lookup(&a);
cache.insert(&c, make_kv(1, 1));
assert_eq!(cache.len(), 2);
assert!(cache.contains(&a));
assert!(!cache.contains(&b));
assert!(cache.contains(&c));
}
#[test]
fn replace_existing_entry() {
let mut cache = PrefixCache::new(4);
let tokens = vec![1u32, 2, 3];
cache.insert(&tokens, make_kv(1, 3));
assert_eq!(cache.len(), 1);
cache.insert(&tokens, make_kv(2, 3));
assert_eq!(cache.len(), 1);
let cached = cache.lookup(&tokens).unwrap();
assert_eq!(cached.len(), 2); }
#[test]
fn clear_empties_cache() {
let mut cache = PrefixCache::new(4);
cache.insert(&[1u32], make_kv(1, 1));
cache.insert(&[2u32], make_kv(1, 1));
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn cached_seq_len() {
let mut cache = PrefixCache::new(4);
let tokens = vec![1u32, 2, 3, 4, 5];
cache.insert(&tokens, make_kv(1, 5));
assert_eq!(cache.cached_seq_len(&tokens), Some(5));
assert_eq!(cache.cached_seq_len(&[99u32]), None);
}
#[test]
fn longest_prefix_match_finds_longest() {
let mut cache = PrefixCache::new(4);
cache.insert(&[1u32, 2], make_kv(1, 2));
cache.insert(&[1u32, 2, 3, 4], make_kv(1, 4));
let result = cache.longest_prefix_match(&[1, 2, 3, 4, 5]);
assert!(result.is_some());
let (matched_len, kv) = result.unwrap();
assert_eq!(matched_len, 4);
assert_eq!(kv[0].0.shape().dims(), &[1, 4, 4, 64]);
}
#[test]
fn longest_prefix_match_no_match() {
let mut cache = PrefixCache::new(4);
cache.insert(&[1u32, 2, 3], make_kv(1, 3));
let result = cache.longest_prefix_match(&[4, 5, 6]);
assert!(result.is_none());
}
#[test]
fn hash_determinism() {
let tokens = vec![42u32, 100, 200, 300];
let h1 = hash_tokens(&tokens);
let h2 = hash_tokens(&tokens);
assert_eq!(h1, h2);
}
#[test]
fn hash_different_sequences() {
let h1 = hash_tokens(&[1, 2, 3]);
let h2 = hash_tokens(&[3, 2, 1]);
assert_ne!(h1, h2);
}
}