use crate::autoregressive::KvCacheState;
use std::io::{Read, Write};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct CacheEntry {
pub tokens: Vec<u32>,
pub kv: KvCacheState,
}
impl CacheEntry {
pub fn kv_bytes(&self) -> usize {
let f = |v: &Vec<Vec<f32>>| v.iter().map(|l| l.len()).sum::<usize>() * 4;
f(&self.kv.layers_k) + f(&self.kv.layers_v)
}
}
fn common_prefix_len(a: &[u32], b: &[u32]) -> usize {
a.iter().zip(b).take_while(|(x, y)| x == y).count()
}
pub struct PromptCache {
entries: Vec<CacheEntry>,
ticks: Vec<u64>,
clock: u64,
bytes: usize,
cap_bytes: usize,
}
impl PromptCache {
pub fn new(cap_bytes: usize) -> Self {
Self {
entries: Vec::new(),
ticks: Vec::new(),
clock: 0,
bytes: 0,
cap_bytes,
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn bytes(&self) -> usize {
self.bytes
}
pub fn longest_prefix(&mut self, query: &[u32]) -> Option<(usize, CacheEntry)> {
let mut best: Option<usize> = None;
let mut best_len = 0usize;
for (i, e) in self.entries.iter().enumerate() {
if e.tokens.len() > query.len() {
continue;
}
let cpl = common_prefix_len(&e.tokens, query);
if cpl == e.tokens.len() && cpl > best_len {
best_len = cpl;
best = Some(i);
}
}
let i = best?;
self.clock += 1;
self.ticks[i] = self.clock;
Some((best_len, self.entries[i].clone()))
}
pub fn insert(&mut self, tokens: Vec<u32>, kv: KvCacheState) {
debug_assert_eq!(
kv.past_len,
tokens.len(),
"kv.past_len must equal tokens.len()"
);
let entry = CacheEntry { tokens, kv };
let add = entry.kv_bytes();
if let Some(i) = self.entries.iter().position(|e| e.tokens == entry.tokens) {
self.bytes -= self.entries[i].kv_bytes();
self.bytes += add;
self.entries[i] = entry;
self.clock += 1;
self.ticks[i] = self.clock;
self.evict_to_cap();
return;
}
self.clock += 1;
self.entries.push(entry);
self.ticks.push(self.clock);
self.bytes += add;
self.evict_to_cap();
}
fn evict_to_cap(&mut self) {
while self.bytes > self.cap_bytes && self.entries.len() > 1 {
let lru = self
.ticks
.iter()
.enumerate()
.min_by_key(|(_, t)| **t)
.map(|(i, _)| i)
.unwrap();
self.bytes -= self.entries[lru].kv_bytes();
self.entries.remove(lru);
self.ticks.remove(lru);
}
}
pub fn clear(&mut self) {
self.entries.clear();
self.ticks.clear();
self.bytes = 0;
}
}
const MAGIC: &[u8; 8] = b"RLXKV001";
fn write_u64<W: Write>(w: &mut W, v: u64) -> std::io::Result<()> {
w.write_all(&v.to_le_bytes())
}
fn read_u64<R: Read>(r: &mut R) -> std::io::Result<u64> {
let mut b = [0u8; 8];
r.read_exact(&mut b)?;
Ok(u64::from_le_bytes(b))
}
fn write_f32_block<W: Write>(w: &mut W, data: &[f32]) -> std::io::Result<()> {
write_u64(w, data.len() as u64)?;
for &x in data {
w.write_all(&x.to_le_bytes())?;
}
Ok(())
}
fn read_f32_block<R: Read>(r: &mut R) -> std::io::Result<Vec<f32>> {
let n = read_u64(r)? as usize;
let mut out = Vec::with_capacity(n);
let mut buf = [0u8; 4];
for _ in 0..n {
r.read_exact(&mut buf)?;
out.push(f32::from_le_bytes(buf));
}
Ok(out)
}
pub fn save_entry(path: &Path, entry: &CacheEntry, fingerprint: u64) -> std::io::Result<()> {
let mut f = std::io::BufWriter::new(std::fs::File::create(path)?);
f.write_all(MAGIC)?;
write_u64(&mut f, fingerprint)?;
write_u64(&mut f, entry.kv.past_len as u64)?;
write_u64(&mut f, entry.tokens.len() as u64)?;
for &t in &entry.tokens {
write_u64(&mut f, t as u64)?;
}
let layers = entry.kv.layers_k.len() as u64;
write_u64(&mut f, layers)?;
for k in &entry.kv.layers_k {
write_f32_block(&mut f, k)?;
}
for v in &entry.kv.layers_v {
write_f32_block(&mut f, v)?;
}
f.flush()
}
pub fn load_entry(path: &Path, fingerprint: u64) -> std::io::Result<Option<CacheEntry>> {
let mut f = std::io::BufReader::new(std::fs::File::open(path)?);
let mut magic = [0u8; 8];
f.read_exact(&mut magic)?;
if &magic != MAGIC || read_u64(&mut f)? != fingerprint {
return Ok(None);
}
let past_len = read_u64(&mut f)? as usize;
let n_tokens = read_u64(&mut f)? as usize;
let mut tokens = Vec::with_capacity(n_tokens);
for _ in 0..n_tokens {
tokens.push(read_u64(&mut f)? as u32);
}
let layers = read_u64(&mut f)? as usize;
let mut layers_k = Vec::with_capacity(layers);
for _ in 0..layers {
layers_k.push(read_f32_block(&mut f)?);
}
let mut layers_v = Vec::with_capacity(layers);
for _ in 0..layers {
layers_v.push(read_f32_block(&mut f)?);
}
Ok(Some(CacheEntry {
tokens,
kv: KvCacheState {
past_len,
layers_kv_base: vec![0; layers],
layers_k,
layers_v,
},
}))
}
#[cfg(test)]
mod tests {
use super::*;
fn kv(past: usize, fill: f32) -> KvCacheState {
KvCacheState {
past_len: past,
layers_kv_base: vec![0; 1],
layers_k: vec![vec![fill; past * 2]],
layers_v: vec![vec![fill; past * 2]],
}
}
#[test]
fn longest_prefix_picks_deepest_match() {
let mut c = PromptCache::new(1 << 20);
c.insert(vec![1, 2], kv(2, 0.1));
c.insert(vec![1, 2, 3, 4], kv(4, 0.2));
let (len, e) = c.longest_prefix(&[1, 2, 3, 4, 5, 6]).unwrap();
assert_eq!(len, 4);
assert_eq!(e.tokens, vec![1, 2, 3, 4]);
let (len, _) = c.longest_prefix(&[1, 2, 9]).unwrap();
assert_eq!(len, 2);
}
#[test]
fn no_match_returns_none() {
let mut c = PromptCache::new(1 << 20);
c.insert(vec![5, 6], kv(2, 0.0));
assert!(c.longest_prefix(&[1, 2, 3]).is_none());
assert!(c.longest_prefix(&[5]).is_none());
}
#[test]
fn lru_evicts_when_over_cap() {
let mut c = PromptCache::new(100); c.insert(vec![1, 2, 3, 4], kv(4, 0.1));
c.insert(vec![5, 6, 7, 8], kv(4, 0.2)); assert_eq!(c.len(), 1);
assert!(c.longest_prefix(&[1, 2, 3, 4]).is_none());
assert!(c.longest_prefix(&[5, 6, 7, 8]).is_some());
}
#[test]
fn lru_keeps_recently_used() {
let mut c = PromptCache::new(140); c.insert(vec![1, 1, 1, 1], kv(4, 0.1));
c.insert(vec![2, 2, 2, 2], kv(4, 0.2));
let _ = c.longest_prefix(&[1, 1, 1, 1]); c.insert(vec![3, 3, 3, 3], kv(4, 0.3)); assert!(c.longest_prefix(&[1, 1, 1, 1]).is_some());
assert!(c.longest_prefix(&[2, 2, 2, 2]).is_none());
assert!(c.longest_prefix(&[3, 3, 3, 3]).is_some());
}
#[test]
fn disk_round_trip_with_fingerprint() {
let dir = std::env::temp_dir();
let path = dir.join(format!("rlx_pc_test_{}.bin", std::process::id()));
let entry = CacheEntry {
tokens: vec![7, 8, 9],
kv: kv(3, 0.42),
};
save_entry(&path, &entry, 0xABCD).unwrap();
let back = load_entry(&path, 0xABCD)
.unwrap()
.expect("fingerprint matches");
assert_eq!(back.tokens, entry.tokens);
assert_eq!(back.kv.past_len, 3);
assert_eq!(back.kv.layers_k[0], entry.kv.layers_k[0]);
assert!(load_entry(&path, 0x9999).unwrap().is_none());
let _ = std::fs::remove_file(&path);
}
}