1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum KvMode {
14 F32,
15 Q8 { k: bool, v: bool },
17}
18
19impl KvMode {
20 pub fn from_env() -> Self {
21 match std::env::var("CMF_KV").as_deref() {
22 Ok("q8") | Ok("q8_2f") => KvMode::Q8 { k: true, v: true },
23 Ok("q8k") => KvMode::Q8 { k: true, v: false },
24 Ok("q8v") => KvMode::Q8 { k: false, v: true },
25 _ => KvMode::F32,
26 }
27 }
28
29 fn quant_k(self) -> bool {
30 matches!(self, KvMode::Q8 { k: true, .. })
31 }
32
33 fn quant_v(self) -> bool {
34 matches!(self, KvMode::Q8 { v: true, .. })
35 }
36}
37
38const KV_COL_WARMUP: usize = 64;
41
42const KV_K_GROUP: usize = 32;
47
48#[derive(Debug, Clone)]
58pub enum O1State {
59 Collecting {
60 m: usize,
61 w: usize,
62 sink: usize,
63 q_buf: Vec<f32>,
65 },
66 Sealed { states: Vec<crate::nystrom::NystromState> },
72}
73
74#[derive(Debug, Clone)]
76pub struct LayerKvCache {
77 pub mode: KvMode,
78 k: Vec<Vec<f32>>,
80 v: Vec<Vec<f32>>,
82 kq: Vec<Vec<i8>>,
84 ks: Vec<Vec<f32>>,
85 vq: Vec<Vec<i8>>,
86 vs: Vec<Vec<f32>>,
87 kcol: Vec<Vec<f32>>,
89 vcol: Vec<Vec<f32>>,
90 imp: Vec<f32>,
93 pub seq_len: usize,
95 pub num_kv_heads: usize,
96 pub head_dim: usize,
97 pub linear_state: Vec<f64>,
99 pub linear_scratch: Vec<f64>,
101 pub o1: Option<O1State>,
103}
104
105impl LayerKvCache {
106 pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
107 Self {
108 mode: KvMode::from_env(),
109 k: vec![Vec::new(); num_kv_heads],
110 v: vec![Vec::new(); num_kv_heads],
111 kq: vec![Vec::new(); num_kv_heads],
112 ks: vec![Vec::new(); num_kv_heads],
113 vq: vec![Vec::new(); num_kv_heads],
114 vs: vec![Vec::new(); num_kv_heads],
115 kcol: vec![Vec::new(); num_kv_heads],
116 vcol: vec![Vec::new(); num_kv_heads],
117 imp: Vec::new(),
118 seq_len: 0,
119 num_kv_heads,
120 head_dim,
121 linear_state: Vec::new(),
122 linear_scratch: Vec::new(),
123 o1: None,
124 }
125 }
126
127 pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize) {
131 self.o1 = Some(O1State::Collecting { m, w, sink, q_buf: Vec::new() });
132 }
133
134 pub fn o1_push_q(&mut self, q_all: &[f32]) {
139 if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
140 q_buf.extend_from_slice(q_all);
141 }
142 }
143
144 pub fn o1_sealed(&self) -> bool {
145 matches!(self.o1, Some(O1State::Sealed { .. }))
146 }
147
148 pub fn o1_seal(&mut self, num_heads: usize) -> bool {
153 if !matches!(self.o1, Some(O1State::Collecting { .. })) {
156 return self.o1_sealed();
157 }
158 let Some(O1State::Collecting { m, w, sink, q_buf }) = self.o1.take() else {
159 unreachable!("checked above");
160 };
161 let (hd, t) = (self.head_dim, self.seq_len);
162 let hpk = num_heads / self.num_kv_heads.max(1);
163 let ok = t > 0
164 && self.mode == KvMode::F32
165 && q_buf.len() == t * num_heads * hd
166 && (0..self.num_kv_heads).all(|g| self.head_len(g) == t);
167 if !ok {
168 tracing::warn!(
169 "o1: cannot seal (needs f32 KV mode, dense heads, full query \
170 trace) — layer keeps exact attention"
171 );
172 return false;
173 }
174 let mut states = Vec::with_capacity(num_heads);
175 let mut qh = vec![0.0f32; t * hd];
176 for h in 0..num_heads {
177 let g = h / hpk;
178 for p in 0..t {
179 let src = (p * num_heads + h) * hd;
180 qh[p * hd..(p + 1) * hd].copy_from_slice(&q_buf[src..src + hd]);
181 }
182 let mut st = crate::nystrom::NystromState::new(m, w, sink);
183 st.prefill(&qh, &self.k[g], &self.v[g], t, hd, hd);
184 states.push(st);
185 }
186 for h in 0..self.num_kv_heads {
189 self.k[h] = Vec::new();
190 self.v[h] = Vec::new();
191 }
192 self.imp = Vec::new();
193 self.o1 = Some(O1State::Sealed { states });
194 true
195 }
196
197 pub fn o1_step(
203 &mut self,
204 q_all: &[f32],
205 k_new: &[f32],
206 v_new: &[f32],
207 num_heads: usize,
208 ) -> Vec<f32> {
209 let hd = self.head_dim;
210 let hpk = num_heads / self.num_kv_heads.max(1);
211 let mut out = vec![0.0f32; num_heads * hd];
212 let Some(O1State::Sealed { states }) = &mut self.o1 else {
213 debug_assert!(false, "o1_step on an unsealed layer");
214 return out;
215 };
216 for h in 0..num_heads {
217 let g = h / hpk;
218 states[h].step(
219 &q_all[h * hd..(h + 1) * hd],
220 &k_new[g * hd..(g + 1) * hd],
221 &v_new[g * hd..(g + 1) * hd],
222 &mut out[h * hd..(h + 1) * hd],
223 );
224 }
225 self.seq_len += 1;
228 out
229 }
230
231 pub fn o1_memory_bytes(&self) -> usize {
234 match &self.o1 {
235 Some(O1State::Collecting { q_buf, .. }) => {
236 q_buf.len() * std::mem::size_of::<f32>()
237 }
238 Some(O1State::Sealed { states }) => {
239 states.iter().map(|s| s.memory_bytes()).sum()
240 }
241 None => 0,
242 }
243 }
244
245 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>,
248 group: usize) {
249 let mut resid = vec![0.0f32; row.len()];
250 for (d, &x) in row.iter().enumerate() {
251 resid[d] = if col.is_empty() { x } else { x / col[d] };
252 }
253 for g0 in (0..row.len()).step_by(group) {
254 let g1 = (g0 + group).min(row.len());
255 let mut absmax = 0.0f32;
256 for &r in &resid[g0..g1] {
257 absmax = absmax.max(r.abs());
258 }
259 let s = (absmax / 127.0).max(1e-12);
260 sc.push(s);
261 for &r in &resid[g0..g1] {
262 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
263 }
264 }
265 }
266
267 fn freeze_cols(&mut self) {
270 let hd = self.head_dim;
271 let ngk = hd.div_ceil(KV_K_GROUP);
272 for h in 0..self.num_kv_heads {
273 for (qv, sv, colv, group) in [
274 (&mut self.kq[h], &mut self.ks[h], &mut self.kcol[h], KV_K_GROUP),
275 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
276 ] {
277 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
279 if n == 0 {
280 continue;
281 }
282 let mut rows = vec![0.0f32; n * hd];
284 for p in 0..n {
285 for d in 0..hd {
286 rows[p * hd + d] =
287 qv[p * hd + d] as f32 * sv[p * spp + d / group];
288 }
289 }
290 let mut col = vec![0.0f32; hd];
291 for p in 0..n {
292 for d in 0..hd {
293 col[d] += rows[p * hd + d] * rows[p * hd + d];
294 }
295 }
296 for c in col.iter_mut() {
297 *c = (*c / n as f32).sqrt().max(1e-6);
298 }
299 qv.clear();
300 sv.clear();
301 for p in 0..n {
302 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
303 }
304 *colv = col;
305 }
306 }
307 }
308
309 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
313 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
314 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
315 if matches!(self.mode, KvMode::Q8 { .. })
321 && self.seq_len >= KV_COL_WARMUP
322 && self.kcol.iter().all(Vec::is_empty)
323 && self.vcol.iter().all(Vec::is_empty)
324 {
325 self.freeze_cols();
326 }
327 for h in 0..self.num_kv_heads {
328 if !alive.get(h).copied().unwrap_or(true) {
329 continue;
330 }
331 let s = h * self.head_dim;
332 if self.mode.quant_k() {
333 Self::quant_row(&k_new[s..s + self.head_dim],
334 &self.kcol[h], &mut self.kq[h], &mut self.ks[h],
335 KV_K_GROUP);
336 } else {
337 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
338 }
339 if self.mode.quant_v() {
340 Self::quant_row(&v_new[s..s + self.head_dim],
341 &self.vcol[h], &mut self.vq[h], &mut self.vs[h],
342 self.head_dim);
343 } else {
344 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
345 }
346 }
347 self.imp.push(0.0);
348 self.seq_len += 1;
349 }
350
351 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
356 let hd = self.head_dim;
357 if self.mode == KvMode::F32 {
358 let stored = self.k[kv_head].len() / hd;
359 return crate::attention::attention_head(
360 q, &self.k[kv_head], &self.v[kv_head], hd, stored);
361 }
362 let stored = self.head_len(kv_head);
363 let scale = 1.0 / (hd as f32).sqrt();
364 let mut scores = vec![0.0f32; stored];
365 if self.mode.quant_k() {
366 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
367 let kcol = &self.kcol[kv_head];
369 let mut qc = vec![0.0f32; hd];
370 for d in 0..hd {
371 qc[d] = if kcol.is_empty() { q[d] } else { q[d] * kcol[d] };
372 }
373 let ng = hd.div_ceil(KV_K_GROUP);
374 for p in 0..stored {
375 let row = &kq[p * hd..(p + 1) * hd];
376 let row_u8 = unsafe {
379 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
380 };
381 let mut dot = 0.0f32;
382 for g in 0..ng {
383 let g0 = g * KV_K_GROUP;
384 let g1 = (g0 + KV_K_GROUP).min(hd);
385 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1])
386 * ks[p * ng + g];
387 }
388 scores[p] = dot * scale;
389 }
390 } else {
391 let k = &self.k[kv_head];
392 for p in 0..stored {
393 let row = &k[p * hd..(p + 1) * hd];
394 scores[p] = crate::attention::dot_f32(q, row) * scale;
395 }
396 }
397 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
398 let mut sum = 0.0f32;
399 for s in scores.iter_mut() {
400 *s = (*s - max_score).exp();
401 sum += *s;
402 }
403 if sum > 0.0 {
404 for s in scores.iter_mut() {
405 *s /= sum;
406 }
407 }
408 let mut acc = vec![0.0f32; hd];
409 if self.mode.quant_v() {
410 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
411 for p in 0..stored {
412 let w = scores[p] * vs[p];
413 if w.abs() < 1e-12 {
414 continue;
415 }
416 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
417 }
418 let vcol = &self.vcol[kv_head];
419 if !vcol.is_empty() {
420 for d in 0..hd {
421 acc[d] *= vcol[d];
422 }
423 }
424 } else {
425 let v = &self.v[kv_head];
426 for p in 0..stored {
427 let w = scores[p];
428 if w.abs() < 1e-12 {
429 continue;
430 }
431 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
432 }
433 }
434 (acc, scores)
435 }
436
437 pub fn truncate_last(&mut self, n_drop: usize) {
439 let d = n_drop.min(self.seq_len);
440 for h in 0..self.num_kv_heads {
441 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
442 self.k[h].truncate(keep);
443 self.v[h].truncate(keep);
444 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
445 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
446 self.kq[h].truncate(keep_q);
447 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
448 self.vq[h].truncate(keep_vq);
449 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
450 self.ks[h].truncate(keep_ks);
451 let keep_vs = self.vs[h].len().saturating_sub(d);
452 self.vs[h].truncate(keep_vs);
453 }
454 self.imp.truncate(self.imp.len().saturating_sub(d));
455 self.seq_len -= d;
456 }
457
458 pub fn accumulate_imp(&mut self, probs: &[f32]) {
460 for (dst, &p) in self.imp.iter_mut().zip(probs) {
461 *dst += p;
462 }
463 }
464
465 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
467 &self.k[kv_head]
468 }
469
470 pub fn head_values(&self, kv_head: usize) -> &[f32] {
471 &self.v[kv_head]
472 }
473
474 pub fn head_len(&self, kv_head: usize) -> usize {
476 let ng = self.head_dim.div_ceil(KV_K_GROUP);
477 (self.k[kv_head].len() / self.head_dim)
478 .max(self.ks[kv_head].len() / ng)
479 .max(self.vs[kv_head].len())
480 }
481
482 pub fn clear(&mut self) {
484 for h in 0..self.num_kv_heads {
485 self.k[h].clear();
486 self.v[h].clear();
487 self.kq[h].clear();
488 self.ks[h].clear();
489 self.vq[h].clear();
490 self.vs[h].clear();
491 self.kcol[h].clear();
492 self.vcol[h].clear();
493 }
494 self.imp.clear();
495 self.linear_state.clear();
496 self.linear_scratch.clear();
497 self.o1 = None;
500 self.seq_len = 0;
501 }
502
503 pub fn memory_bytes(&self) -> usize {
505 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
506 + self.v.iter().map(Vec::len).sum::<usize>()
507 + self.ks.iter().map(Vec::len).sum::<usize>()
508 + self.vs.iter().map(Vec::len).sum::<usize>()
509 + self.kcol.iter().map(Vec::len).sum::<usize>()
510 + self.vcol.iter().map(Vec::len).sum::<usize>();
511 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
512 + self.vq.iter().map(Vec::len).sum::<usize>();
513 floats * std::mem::size_of::<f32>()
514 + bytes
515 + self.linear_state.len() * std::mem::size_of::<f64>()
519 + self.o1_memory_bytes()
522 }
523
524 fn evict(&mut self, keep_last: usize) {
526 if self.o1_sealed() || self.seq_len <= keep_last {
530 return;
531 }
532 let drop = self.seq_len - keep_last;
533 for h in 0..self.num_kv_heads {
534 let stored = self.head_len(h);
536 let d = drop.min(stored);
537 let hd = self.head_dim;
538 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
539 let n = n.min(v.len());
540 v.drain(..n);
541 }
542 drop_front(&mut self.k[h], d * hd);
543 drop_front(&mut self.v[h], d * hd);
544 drop_front(&mut self.kq[h], d * hd);
545 drop_front(&mut self.vq[h], d * hd);
546 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
547 drop_front(&mut self.vs[h], d);
548 }
549 let d = drop.min(self.imp.len());
550 self.imp.drain(..d);
551 self.seq_len = keep_last;
552 }
553
554 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
559 if self.o1_sealed() {
560 return; }
562 let stored = self.imp.len();
563 if stored <= keep_last {
564 return;
565 }
566 let sink_n = sink.min(keep_last);
569 let recent_n = recent.min(keep_last - sink_n);
570 let mut keep = vec![false; stored];
571 for k in keep.iter_mut().take(sink_n) {
572 *k = true;
573 }
574 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
575 *k = true;
576 }
577 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
578 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
580 order.sort_by(|&a, &b| {
581 self.imp[b].partial_cmp(&self.imp[a]).unwrap_or(std::cmp::Ordering::Equal)
582 });
583 for i in order {
584 if budget == 0 {
585 break;
586 }
587 keep[i] = true;
588 budget -= 1;
589 }
590
591 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
592 let hd = self.head_dim;
593 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
594 let mut out = Vec::with_capacity(kept.len() * step);
595 for &i in kept {
596 out.extend_from_slice(&src[i * step..(i + 1) * step]);
597 }
598 out
599 }
600 for h in 0..self.num_kv_heads {
605 if !self.k[h].is_empty() {
606 self.k[h] = gather(&self.k[h], &kept, hd);
607 }
608 if !self.v[h].is_empty() {
609 self.v[h] = gather(&self.v[h], &kept, hd);
610 }
611 if !self.kq[h].is_empty() {
612 self.kq[h] = gather(&self.kq[h], &kept, hd);
613 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
614 }
615 if !self.vq[h].is_empty() {
616 self.vq[h] = gather(&self.vq[h], &kept, hd);
617 self.vs[h] = gather(&self.vs[h], &kept, 1);
618 }
619 }
620 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
621 self.seq_len = kept.len();
622 }
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum EvictionPolicy {
628 Recent,
630 Born { sink: usize },
632}
633
634#[derive(Debug)]
636pub struct KvCache {
637 pub layers: Vec<LayerKvCache>,
638 pub max_seq_len: usize,
639 pub policy: EvictionPolicy,
640}
641
642impl KvCache {
643 pub fn new(num_layers: usize, num_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
644 let layers = (0..num_layers)
645 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
646 .collect();
647 Self {
648 layers,
649 max_seq_len,
650 policy: EvictionPolicy::Born { sink: 4 },
651 }
652 }
653
654 pub fn clear(&mut self) {
655 for layer in &mut self.layers {
656 layer.clear();
657 }
658 }
659
660 pub fn total_memory_bytes(&self) -> usize {
661 self.layers.iter().map(|l| l.memory_bytes()).sum()
662 }
663
664 pub fn seq_len(&self) -> usize {
666 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
667 }
668
669 pub fn needs_eviction(&self) -> bool {
670 self.seq_len() >= self.max_seq_len
671 }
672
673 pub fn evict(&mut self, keep_last: usize) {
675 match self.policy {
676 EvictionPolicy::Recent => {
677 for layer in &mut self.layers {
678 layer.evict(keep_last);
679 }
680 }
681 EvictionPolicy::Born { sink } => {
682 let recent = (keep_last / 2).max(1);
683 for layer in &mut self.layers {
684 layer.evict_born(keep_last, sink, recent);
685 }
686 }
687 }
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn append_tracks_seq_len_and_layout() {
697 let mut cache = LayerKvCache::new(4, 8);
698 cache.mode = KvMode::F32;
699 assert_eq!(cache.seq_len, 0);
700
701 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
702 let v = vec![2.0f32; 32];
703 cache.append(&k, &v, &[true; 4]);
704
705 assert_eq!(cache.seq_len, 1);
706 assert_eq!(cache.head_len(0), 1);
707 assert_eq!(cache.head_keys(1), &k[8..16]);
709 assert_eq!(cache.memory_bytes(), 256);
710 }
711
712 #[test]
713 fn dead_head_stores_nothing() {
714 let mut cache = LayerKvCache::new(2, 4);
715 cache.mode = KvMode::F32;
716 let k = vec![1.0f32; 8];
717 let v = vec![2.0f32; 8];
718 cache.append(&k, &v, &[true, false]);
719 cache.append(&k, &v, &[true, false]);
720
721 assert_eq!(cache.seq_len, 2);
722 assert_eq!(cache.head_len(0), 2);
723 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
724 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
725 }
726
727 #[test]
728 fn eviction_keeps_recent() {
729 let mut cache = KvCache::new(2, 4, 8, 10);
730 cache.policy = EvictionPolicy::Recent;
731 for l in &mut cache.layers { l.mode = KvMode::F32; }
732 let k = vec![1.0f32; 32];
733 let v = vec![2.0f32; 32];
734 for _ in 0..8 {
735 for layer in &mut cache.layers {
736 layer.append(&k, &v, &[true; 4]);
737 }
738 }
739 assert_eq!(cache.seq_len(), 8);
740 assert!(!cache.needs_eviction());
741
742 cache.evict(4);
743 assert_eq!(cache.seq_len(), 4);
744 assert_eq!(cache.layers[0].head_len(0), 4);
745 }
746
747 #[test]
748 fn truncate_rolls_back_speculative_positions() {
749 let mut cache = LayerKvCache::new(2, 4);
750 cache.mode = KvMode::F32;
751 for pos in 0..5 {
752 let k = vec![pos as f32; 8];
753 let v = vec![pos as f32; 8];
754 cache.append(&k, &v, &[true; 2]);
755 }
756 cache.truncate_last(2);
757 assert_eq!(cache.seq_len, 3);
758 assert_eq!(cache.head_len(0), 3);
759 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
760 }
761
762 #[test]
766 fn q8_attend_matches_f32_within_grid() {
767 let (heads, hd) = (2, 32);
768 let mut f = LayerKvCache::new(heads, hd);
769 f.mode = KvMode::F32;
770 let mut q8 = LayerKvCache::new(heads, hd);
771 q8.mode = KvMode::Q8 { k: true, v: true };
772
773 let synth = |p: usize, salt: usize| -> Vec<f32> {
774 (0..heads * hd)
775 .map(|i| {
776 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
777 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
779 })
780 .collect()
781 };
782 for p in 0..100 {
783 let k = synth(p, 1);
784 let v = synth(p, 2);
785 f.append(&k, &v, &[true; 2]);
786 q8.append(&k, &v, &[true; 2]);
787 }
788 let q: Vec<f32> = (0..hd).map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5).collect();
789 for g in 0..heads {
790 let (of, pf) = f.attend(&q, g);
791 let (o8, p8) = q8.attend(&q, g);
792 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
793 for d in 0..hd {
794 assert!(
795 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
796 "g{g} d{d}: f32 {} vs q8 {}", of[d], o8[d]
797 );
798 }
799 for p in 0..100 {
800 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
801 }
802 }
803 q8.truncate_last(30);
805 assert_eq!(q8.head_len(0), 70);
806 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
807 q8.accumulate_imp(&imp);
808 q8.evict_born(20, 2, 8);
809 assert_eq!(q8.head_len(0), 20);
810 let (o, _) = q8.attend(&q, 0);
811 assert!(o.iter().all(|x| x.is_finite()));
812 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
814 }
815
816 #[test]
820 fn born_eviction_mixed_modes_stay_consistent() {
821 for (mk, mv) in [(false, true), (true, false)] {
822 let mut c = LayerKvCache::new(1, 4);
823 c.mode = KvMode::Q8 { k: mk, v: mv };
824 for p in 0..80 {
825 let k = vec![p as f32 * 0.01; 4];
826 let v = vec![p as f32; 4];
827 c.append(&k, &v, &[true]);
828 }
829 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
830 c.accumulate_imp(&imp);
831 let before = c.memory_bytes();
832 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
834 assert!(c.memory_bytes() < before / 2,
835 "memory must shrink (k={mk} v={mv})");
836 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
839 assert!(out[0] > 30.0,
840 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
841 out[0]);
842 }
843 }
844
845 #[test]
846 fn born_eviction_keeps_high_mass_position() {
847 let mut cache = KvCache::new(1, 1, 2, 16);
848 cache.policy = EvictionPolicy::Born { sink: 1 };
849 for l in &mut cache.layers { l.mode = KvMode::F32; }
850 let layer = &mut cache.layers[0];
851 for pos in 0..8 {
854 let k = vec![pos as f32; 2];
855 let v = vec![pos as f32 + 100.0; 2];
856 layer.append(&k, &v, &[true]);
857 }
858 let mut imp = vec![0.05f32; 8];
860 imp[3] = 5.0;
861 layer.accumulate_imp(&imp);
862
863 cache.evict(4); let layer = &cache.layers[0];
865 assert_eq!(layer.seq_len, 4);
866 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
867 assert_eq!(
868 kept_keys,
869 vec![0.0, 3.0, 6.0, 7.0],
870 "kept = sink(0) + Born-top(3) + recent(6,7)"
871 );
872 assert_eq!(layer.head_len(0), 4);
874 }
875}