#[cfg(test)]
use crate::Device;
use crate::{DType, lazy::Tensor};
use fuel_ir::Shape;
#[derive(Clone, Debug)]
pub struct LatentCache {
layers: Vec<Vec<Tensor>>,
slot_trailing: Vec<Vec<usize>>,
current_seq_len: usize,
max_seq_len: usize,
}
impl LatentCache {
pub fn new(
anchor: &Tensor,
n_layers: usize,
max_seq_len: usize,
slot_trailing: Vec<Vec<usize>>,
dtype: DType,
) -> std::result::Result<Self, fuel_ir::Error> {
if n_layers == 0 {
return Err(fuel_ir::Error::Msg("LatentCache::new: n_layers must be ≥ 1".into()).bt());
}
if max_seq_len == 0 {
return Err(
fuel_ir::Error::Msg("LatentCache::new: max_seq_len must be ≥ 1".into()).bt(),
);
}
if slot_trailing.is_empty() {
return Err(fuel_ir::Error::Msg(
"LatentCache::new: need at least one slot (slot_trailing empty)".into(),
)
.bt());
}
let mut layers = Vec::with_capacity(n_layers);
for _ in 0..n_layers {
let mut slots = Vec::with_capacity(slot_trailing.len());
for trailing in &slot_trailing {
let mut dims = Vec::with_capacity(1 + trailing.len());
dims.push(max_seq_len);
dims.extend_from_slice(trailing);
let shape = Shape::from_dims(&dims);
let elems: usize = dims.iter().product();
slots.push(zero_const_on(anchor, dtype, shape, elems)?);
}
layers.push(slots);
}
Ok(Self {
layers,
slot_trailing,
current_seq_len: 0,
max_seq_len,
})
}
pub fn append(mut self, layer: usize, new_slots: &[&Tensor]) -> crate::Result<Self> {
if layer >= self.layers.len() {
crate::bail!(
"LatentCache::append: layer {layer} out of bounds (n_layers={})",
self.layers.len(),
);
}
let n_slots = self.slot_trailing.len();
if new_slots.len() != n_slots {
crate::bail!(
"LatentCache::append: expected {n_slots} slot tensors, got {}",
new_slots.len(),
);
}
let seqlen_new = {
let d = new_slots[0].shape().dims().to_vec();
if d.is_empty() {
crate::bail!(
"LatentCache::append: slot 0 must be rank ≥ 1 (leading seq axis), got {d:?}",
);
}
d[0]
};
for (s, tensor) in new_slots.iter().enumerate() {
let got = tensor.shape().dims().to_vec();
let mut want = Vec::with_capacity(1 + self.slot_trailing[s].len());
want.push(seqlen_new);
want.extend_from_slice(&self.slot_trailing[s]);
if got != want {
crate::bail!(
"LatentCache::append: slot {s} shape {got:?} != expected {want:?} \
(seqlen_new={seqlen_new}, trailing={:?})",
self.slot_trailing[s],
);
}
}
let new_end = self.current_seq_len + seqlen_new;
if new_end > self.max_seq_len {
crate::bail!(
"LatentCache::append: appending {seqlen_new} tokens at position {} would \
exceed max_seq_len {}",
self.current_seq_len,
self.max_seq_len,
);
}
let mut updated = Vec::with_capacity(n_slots);
for (s, tensor) in new_slots.iter().enumerate() {
let mut ranges = Vec::with_capacity(1 + self.slot_trailing[s].len());
ranges.push((self.current_seq_len, new_end));
for &d in &self.slot_trailing[s] {
ranges.push((0, d));
}
let buffer = self.layers[layer][s].clone();
updated.push(buffer.write_slice(tensor, ranges)?);
}
self.layers[layer] = updated;
Ok(self)
}
pub fn advance_by(mut self, n: usize) -> Self {
self.current_seq_len = (self.current_seq_len + n).min(self.max_seq_len);
self
}
pub fn slot(&self, layer: usize, slot: usize) -> Tensor {
self.layers[layer][slot]
.slice(0_usize, 0, self.current_seq_len.max(1))
.unwrap()
}
pub fn slot_buffer_full(&self, layer: usize, slot: usize) -> Tensor {
self.layers[layer][slot].clone()
}
pub fn current_seq_len(&self) -> usize {
self.current_seq_len
}
pub fn max_seq_len(&self) -> usize {
self.max_seq_len
}
pub fn n_layers(&self) -> usize {
self.layers.len()
}
pub fn n_slots(&self) -> usize {
self.slot_trailing.len()
}
pub fn slot_trailing(&self, slot: usize) -> &[usize] {
&self.slot_trailing[slot]
}
}
fn zero_const_on(
anchor: &Tensor,
dtype: DType,
shape: Shape,
elems: usize,
) -> std::result::Result<Tensor, fuel_ir::Error> {
match dtype {
DType::F32 => anchor.const_f32_like(vec![0.0_f32; elems], shape),
DType::F64 => anchor.const_f64_like(vec![0.0_f64; elems], shape),
DType::BF16 => anchor.const_bf16_like(vec![half::bf16::ZERO; elems], shape),
DType::F16 => anchor.const_f16_like(vec![half::f16::ZERO; elems], shape),
other => {
Err(fuel_ir::Error::Msg(format!("LatentCache: unsupported dtype {other:?}",)).bt())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
}
#[test]
fn new_latent_cache_is_empty() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 3, 8, vec![vec![5], vec![2]], DType::F32).unwrap();
assert_eq!(cache.n_layers(), 3);
assert_eq!(cache.n_slots(), 2);
assert_eq!(cache.max_seq_len(), 8);
assert_eq!(cache.current_seq_len(), 0);
assert_eq!(cache.slot_trailing(0), &[5]);
assert_eq!(cache.slot_trailing(1), &[2]);
assert_eq!(cache.slot_buffer_full(0, 0).shape().dims(), &[8, 5]);
assert_eq!(cache.slot_buffer_full(0, 1).shape().dims(), &[8, 2]);
}
#[test]
fn mla_two_unequal_slots_append_and_slice() {
let anchor = cpu_f32(vec![0.0], &[1]);
let mut cache =
LatentCache::new(&anchor, 1, 4, vec![vec![3], vec![2]], DType::F32).unwrap();
let c1 = anchor
.const_f32_like(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3])
.unwrap();
let p1 = anchor
.const_f32_like(vec![10.0, 11.0, 12.0, 13.0], vec![2, 2])
.unwrap();
cache = cache.append(0, &[&c1, &p1]).unwrap().advance_by(2);
assert_eq!(cache.current_seq_len(), 2);
let c2 = anchor
.const_f32_like(vec![7.0, 8.0, 9.0], vec![1, 3])
.unwrap();
let p2 = anchor.const_f32_like(vec![14.0, 15.0], vec![1, 2]).unwrap();
cache = cache.append(0, &[&c2, &p2]).unwrap().advance_by(1);
assert_eq!(cache.current_seq_len(), 3);
let latent = cache.slot(0, 0);
assert_eq!(latent.shape().dims(), &[3, 3]);
assert_eq!(
latent.realize_f32(),
vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
);
let kpe = cache.slot(0, 1);
assert_eq!(kpe.shape().dims(), &[3, 2]);
assert_eq!(kpe.realize_f32(), vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0],);
}
#[test]
fn single_slot_rank3_trailing() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2, 2]], DType::F32).unwrap();
assert_eq!(cache.n_slots(), 1);
let t = anchor
.const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![1, 2, 2])
.unwrap();
let cache = cache.append(0, &[&t]).unwrap().advance_by(1);
let got = cache.slot(0, 0);
assert_eq!(got.shape().dims(), &[1, 2, 2]);
assert_eq!(got.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
}
#[test]
fn multi_layer_isolates_slots() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 2, 4, vec![vec![2], vec![1]], DType::F32).unwrap();
let a0 = anchor.const_f32_like(vec![1.0, 1.0], vec![1, 2]).unwrap();
let b0 = anchor.const_f32_like(vec![9.0], vec![1, 1]).unwrap();
let cache = cache.append(0, &[&a0, &b0]).unwrap();
let a1 = anchor.const_f32_like(vec![2.0, 2.0], vec![1, 2]).unwrap();
let b1 = anchor.const_f32_like(vec![8.0], vec![1, 1]).unwrap();
let cache = cache.append(1, &[&a1, &b1]).unwrap().advance_by(1);
assert_eq!(cache.current_seq_len(), 1);
assert_eq!(cache.slot(0, 0).realize_f32(), vec![1.0, 1.0]);
assert_eq!(cache.slot(0, 1).realize_f32(), vec![9.0]);
assert_eq!(cache.slot(1, 0).realize_f32(), vec![2.0, 2.0]);
assert_eq!(cache.slot(1, 1).realize_f32(), vec![8.0]);
}
#[test]
fn append_rejects_oob_layer() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2]], DType::F32).unwrap();
let t = anchor.const_f32_like(vec![0.0, 0.0], vec![1, 2]).unwrap();
assert!(cache.append(5, &[&t]).is_err());
}
#[test]
fn append_rejects_wrong_slot_count() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2], vec![2]], DType::F32).unwrap();
let t = anchor.const_f32_like(vec![0.0, 0.0], vec![1, 2]).unwrap();
assert!(cache.append(0, &[&t]).is_err());
}
#[test]
fn append_rejects_slot_shape_mismatch() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 4, vec![vec![3]], DType::F32).unwrap();
let t = anchor.const_f32_like(vec![0.0; 5], vec![1, 5]).unwrap();
assert!(cache.append(0, &[&t]).is_err());
}
#[test]
fn append_rejects_mismatched_seqlen_across_slots() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2], vec![2]], DType::F32).unwrap();
let a = anchor.const_f32_like(vec![0.0; 4], vec![2, 2]).unwrap();
let b = anchor.const_f32_like(vec![0.0; 2], vec![1, 2]).unwrap();
assert!(cache.append(0, &[&a, &b]).is_err());
}
#[test]
fn append_rejects_capacity_overflow() {
let anchor = cpu_f32(vec![0.0], &[1]);
let cache = LatentCache::new(&anchor, 1, 2, vec![vec![1]], DType::F32).unwrap();
let t = anchor.const_f32_like(vec![0.0; 3], vec![3, 1]).unwrap();
assert!(cache.append(0, &[&t]).is_err()); }
}