1#[cfg(test)]
48use crate::Device;
49use crate::{DType, lazy::Tensor};
50use fuel_ir::Shape;
51
52#[derive(Clone, Debug)]
55pub struct LatentCache {
56 layers: Vec<Vec<Tensor>>,
59 slot_trailing: Vec<Vec<usize>>,
62 current_seq_len: usize,
65 max_seq_len: usize,
67}
68
69impl LatentCache {
70 pub fn new(
78 anchor: &Tensor,
79 n_layers: usize,
80 max_seq_len: usize,
81 slot_trailing: Vec<Vec<usize>>,
82 dtype: DType,
83 ) -> std::result::Result<Self, fuel_ir::Error> {
84 if n_layers == 0 {
85 return Err(fuel_ir::Error::Msg("LatentCache::new: n_layers must be ≥ 1".into()).bt());
86 }
87 if max_seq_len == 0 {
88 return Err(
89 fuel_ir::Error::Msg("LatentCache::new: max_seq_len must be ≥ 1".into()).bt(),
90 );
91 }
92 if slot_trailing.is_empty() {
93 return Err(fuel_ir::Error::Msg(
94 "LatentCache::new: need at least one slot (slot_trailing empty)".into(),
95 )
96 .bt());
97 }
98 let mut layers = Vec::with_capacity(n_layers);
99 for _ in 0..n_layers {
100 let mut slots = Vec::with_capacity(slot_trailing.len());
101 for trailing in &slot_trailing {
102 let mut dims = Vec::with_capacity(1 + trailing.len());
103 dims.push(max_seq_len);
104 dims.extend_from_slice(trailing);
105 let shape = Shape::from_dims(&dims);
106 let elems: usize = dims.iter().product();
107 slots.push(zero_const_on(anchor, dtype, shape, elems)?);
108 }
109 layers.push(slots);
110 }
111 Ok(Self {
112 layers,
113 slot_trailing,
114 current_seq_len: 0,
115 max_seq_len,
116 })
117 }
118
119 pub fn append(mut self, layer: usize, new_slots: &[&Tensor]) -> crate::Result<Self> {
134 if layer >= self.layers.len() {
135 crate::bail!(
136 "LatentCache::append: layer {layer} out of bounds (n_layers={})",
137 self.layers.len(),
138 );
139 }
140 let n_slots = self.slot_trailing.len();
141 if new_slots.len() != n_slots {
142 crate::bail!(
143 "LatentCache::append: expected {n_slots} slot tensors, got {}",
144 new_slots.len(),
145 );
146 }
147 let seqlen_new = {
150 let d = new_slots[0].shape().dims().to_vec();
151 if d.is_empty() {
152 crate::bail!(
153 "LatentCache::append: slot 0 must be rank ≥ 1 (leading seq axis), got {d:?}",
154 );
155 }
156 d[0]
157 };
158 for (s, tensor) in new_slots.iter().enumerate() {
159 let got = tensor.shape().dims().to_vec();
160 let mut want = Vec::with_capacity(1 + self.slot_trailing[s].len());
161 want.push(seqlen_new);
162 want.extend_from_slice(&self.slot_trailing[s]);
163 if got != want {
164 crate::bail!(
165 "LatentCache::append: slot {s} shape {got:?} != expected {want:?} \
166 (seqlen_new={seqlen_new}, trailing={:?})",
167 self.slot_trailing[s],
168 );
169 }
170 }
171 let new_end = self.current_seq_len + seqlen_new;
172 if new_end > self.max_seq_len {
173 crate::bail!(
174 "LatentCache::append: appending {seqlen_new} tokens at position {} would \
175 exceed max_seq_len {}",
176 self.current_seq_len,
177 self.max_seq_len,
178 );
179 }
180 let mut updated = Vec::with_capacity(n_slots);
183 for (s, tensor) in new_slots.iter().enumerate() {
184 let mut ranges = Vec::with_capacity(1 + self.slot_trailing[s].len());
185 ranges.push((self.current_seq_len, new_end));
186 for &d in &self.slot_trailing[s] {
187 ranges.push((0, d));
188 }
189 let buffer = self.layers[layer][s].clone();
190 updated.push(buffer.write_slice(tensor, ranges)?);
191 }
192 self.layers[layer] = updated;
193 Ok(self)
194 }
195
196 pub fn advance_by(mut self, n: usize) -> Self {
200 self.current_seq_len = (self.current_seq_len + n).min(self.max_seq_len);
201 self
202 }
203
204 pub fn slot(&self, layer: usize, slot: usize) -> Tensor {
207 self.layers[layer][slot]
208 .slice(0_usize, 0, self.current_seq_len.max(1))
209 .unwrap()
210 }
211
212 pub fn slot_buffer_full(&self, layer: usize, slot: usize) -> Tensor {
215 self.layers[layer][slot].clone()
216 }
217
218 pub fn current_seq_len(&self) -> usize {
220 self.current_seq_len
221 }
222 pub fn max_seq_len(&self) -> usize {
224 self.max_seq_len
225 }
226 pub fn n_layers(&self) -> usize {
228 self.layers.len()
229 }
230 pub fn n_slots(&self) -> usize {
232 self.slot_trailing.len()
233 }
234 pub fn slot_trailing(&self, slot: usize) -> &[usize] {
236 &self.slot_trailing[slot]
237 }
238}
239
240fn zero_const_on(
244 anchor: &Tensor,
245 dtype: DType,
246 shape: Shape,
247 elems: usize,
248) -> std::result::Result<Tensor, fuel_ir::Error> {
249 match dtype {
250 DType::F32 => anchor.const_f32_like(vec![0.0_f32; elems], shape),
251 DType::F64 => anchor.const_f64_like(vec![0.0_f64; elems], shape),
252 DType::BF16 => anchor.const_bf16_like(vec![half::bf16::ZERO; elems], shape),
253 DType::F16 => anchor.const_f16_like(vec![half::f16::ZERO; elems], shape),
254 other => {
255 Err(fuel_ir::Error::Msg(format!("LatentCache: unsupported dtype {other:?}",)).bt())
256 }
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 fn cpu_f32(data: Vec<f32>, shape: &[usize]) -> Tensor {
265 Tensor::from_f32(data, shape.to_vec(), &Device::cpu()).unwrap()
266 }
267
268 #[test]
269 fn new_latent_cache_is_empty() {
270 let anchor = cpu_f32(vec![0.0], &[1]);
271 let cache = LatentCache::new(&anchor, 3, 8, vec![vec![5], vec![2]], DType::F32).unwrap();
274 assert_eq!(cache.n_layers(), 3);
275 assert_eq!(cache.n_slots(), 2);
276 assert_eq!(cache.max_seq_len(), 8);
277 assert_eq!(cache.current_seq_len(), 0);
278 assert_eq!(cache.slot_trailing(0), &[5]);
279 assert_eq!(cache.slot_trailing(1), &[2]);
280 assert_eq!(cache.slot_buffer_full(0, 0).shape().dims(), &[8, 5]);
281 assert_eq!(cache.slot_buffer_full(0, 1).shape().dims(), &[8, 2]);
282 }
283
284 #[test]
290 fn mla_two_unequal_slots_append_and_slice() {
291 let anchor = cpu_f32(vec![0.0], &[1]);
292 let mut cache =
294 LatentCache::new(&anchor, 1, 4, vec![vec![3], vec![2]], DType::F32).unwrap();
295
296 let c1 = anchor
298 .const_f32_like(vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0], vec![2, 3])
299 .unwrap();
300 let p1 = anchor
301 .const_f32_like(vec![10.0, 11.0, 12.0, 13.0], vec![2, 2])
302 .unwrap();
303 cache = cache.append(0, &[&c1, &p1]).unwrap().advance_by(2);
304 assert_eq!(cache.current_seq_len(), 2);
305
306 let c2 = anchor
308 .const_f32_like(vec![7.0, 8.0, 9.0], vec![1, 3])
309 .unwrap();
310 let p2 = anchor.const_f32_like(vec![14.0, 15.0], vec![1, 2]).unwrap();
311 cache = cache.append(0, &[&c2, &p2]).unwrap().advance_by(1);
312 assert_eq!(cache.current_seq_len(), 3);
313
314 let latent = cache.slot(0, 0);
316 assert_eq!(latent.shape().dims(), &[3, 3]);
317 assert_eq!(
318 latent.realize_f32(),
319 vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0],
320 );
321 let kpe = cache.slot(0, 1);
323 assert_eq!(kpe.shape().dims(), &[3, 2]);
324 assert_eq!(kpe.realize_f32(), vec![10.0, 11.0, 12.0, 13.0, 14.0, 15.0],);
325 }
326
327 #[test]
331 fn single_slot_rank3_trailing() {
332 let anchor = cpu_f32(vec![0.0], &[1]);
333 let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2, 2]], DType::F32).unwrap();
335 assert_eq!(cache.n_slots(), 1);
336 let t = anchor
337 .const_f32_like(vec![1.0, 2.0, 3.0, 4.0], vec![1, 2, 2])
338 .unwrap();
339 let cache = cache.append(0, &[&t]).unwrap().advance_by(1);
340 let got = cache.slot(0, 0);
341 assert_eq!(got.shape().dims(), &[1, 2, 2]);
342 assert_eq!(got.realize_f32(), vec![1.0, 2.0, 3.0, 4.0]);
343 }
344
345 #[test]
346 fn multi_layer_isolates_slots() {
347 let anchor = cpu_f32(vec![0.0], &[1]);
348 let cache = LatentCache::new(&anchor, 2, 4, vec![vec![2], vec![1]], DType::F32).unwrap();
349 let a0 = anchor.const_f32_like(vec![1.0, 1.0], vec![1, 2]).unwrap();
351 let b0 = anchor.const_f32_like(vec![9.0], vec![1, 1]).unwrap();
352 let cache = cache.append(0, &[&a0, &b0]).unwrap();
353 let a1 = anchor.const_f32_like(vec![2.0, 2.0], vec![1, 2]).unwrap();
354 let b1 = anchor.const_f32_like(vec![8.0], vec![1, 1]).unwrap();
355 let cache = cache.append(1, &[&a1, &b1]).unwrap().advance_by(1);
356 assert_eq!(cache.current_seq_len(), 1);
357 assert_eq!(cache.slot(0, 0).realize_f32(), vec![1.0, 1.0]);
358 assert_eq!(cache.slot(0, 1).realize_f32(), vec![9.0]);
359 assert_eq!(cache.slot(1, 0).realize_f32(), vec![2.0, 2.0]);
360 assert_eq!(cache.slot(1, 1).realize_f32(), vec![8.0]);
361 }
362
363 #[test]
364 fn append_rejects_oob_layer() {
365 let anchor = cpu_f32(vec![0.0], &[1]);
366 let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2]], DType::F32).unwrap();
367 let t = anchor.const_f32_like(vec![0.0, 0.0], vec![1, 2]).unwrap();
368 assert!(cache.append(5, &[&t]).is_err());
369 }
370
371 #[test]
372 fn append_rejects_wrong_slot_count() {
373 let anchor = cpu_f32(vec![0.0], &[1]);
374 let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2], vec![2]], DType::F32).unwrap();
375 let t = anchor.const_f32_like(vec![0.0, 0.0], vec![1, 2]).unwrap();
376 assert!(cache.append(0, &[&t]).is_err());
378 }
379
380 #[test]
381 fn append_rejects_slot_shape_mismatch() {
382 let anchor = cpu_f32(vec![0.0], &[1]);
383 let cache = LatentCache::new(&anchor, 1, 4, vec![vec![3]], DType::F32).unwrap();
384 let t = anchor.const_f32_like(vec![0.0; 5], vec![1, 5]).unwrap();
386 assert!(cache.append(0, &[&t]).is_err());
387 }
388
389 #[test]
390 fn append_rejects_mismatched_seqlen_across_slots() {
391 let anchor = cpu_f32(vec![0.0], &[1]);
392 let cache = LatentCache::new(&anchor, 1, 4, vec![vec![2], vec![2]], DType::F32).unwrap();
393 let a = anchor.const_f32_like(vec![0.0; 4], vec![2, 2]).unwrap();
395 let b = anchor.const_f32_like(vec![0.0; 2], vec![1, 2]).unwrap();
396 assert!(cache.append(0, &[&a, &b]).is_err());
397 }
398
399 #[test]
400 fn append_rejects_capacity_overflow() {
401 let anchor = cpu_f32(vec![0.0], &[1]);
402 let cache = LatentCache::new(&anchor, 1, 2, vec![vec![1]], DType::F32).unwrap();
403 let t = anchor.const_f32_like(vec![0.0; 3], vec![3, 1]).unwrap();
404 assert!(cache.append(0, &[&t]).is_err()); }
406}