1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum KvMode {
14 F32,
15 Q8 {
17 k: bool,
18 v: bool,
19 },
20}
21
22impl KvMode {
23 pub fn from_env() -> Self {
24 match std::env::var("CMF_KV").as_deref() {
25 Ok("q8") | Ok("q8_2f") => KvMode::Q8 { k: true, v: true },
26 Ok("q8k") => KvMode::Q8 { k: true, v: false },
27 Ok("q8v") => KvMode::Q8 { k: false, v: true },
28 _ => KvMode::F32,
29 }
30 }
31
32 fn quant_k(self) -> bool {
33 matches!(self, KvMode::Q8 { k: true, .. })
34 }
35
36 fn quant_v(self) -> bool {
37 matches!(self, KvMode::Q8 { v: true, .. })
38 }
39}
40
41const KV_COL_WARMUP: usize = 64;
44
45const KV_K_GROUP: usize = 32;
50
51#[derive(Debug, Clone)]
62pub enum O1State {
63 Collecting {
64 m: usize,
65 w: usize,
66 sink: usize,
67 rect: crate::nystrom::O1Rect,
68 q_buf: Vec<f32>,
70 },
71 Sealed {
77 groups: Vec<crate::nystrom::NystromState>,
78 },
79}
80
81#[derive(Debug, Clone)]
83pub struct LayerKvCache {
84 pub mode: KvMode,
85 k: Vec<Vec<f32>>,
87 v: Vec<Vec<f32>>,
89 kq: Vec<Vec<i8>>,
91 ks: Vec<Vec<f32>>,
92 vq: Vec<Vec<i8>>,
93 vs: Vec<Vec<f32>>,
94 kcol: Vec<Vec<f32>>,
96 vcol: Vec<Vec<f32>>,
97 imp: Vec<f32>,
100 pub seq_len: usize,
102 pub num_kv_heads: usize,
103 pub head_dim: usize,
104 pub linear_state: Vec<f32>,
106 pub linear_scratch: Vec<f32>,
108 pub o1: Option<O1State>,
110}
111
112impl LayerKvCache {
113 pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
114 Self {
115 mode: KvMode::from_env(),
116 k: vec![Vec::new(); num_kv_heads],
117 v: vec![Vec::new(); num_kv_heads],
118 kq: vec![Vec::new(); num_kv_heads],
119 ks: vec![Vec::new(); num_kv_heads],
120 vq: vec![Vec::new(); num_kv_heads],
121 vs: vec![Vec::new(); num_kv_heads],
122 kcol: vec![Vec::new(); num_kv_heads],
123 vcol: vec![Vec::new(); num_kv_heads],
124 imp: Vec::new(),
125 seq_len: 0,
126 num_kv_heads,
127 head_dim,
128 linear_state: Vec::new(),
129 linear_scratch: Vec::new(),
130 o1: None,
131 }
132 }
133
134 pub fn k_heads(&self) -> &[Vec<f32>] {
136 &self.k
137 }
138 pub fn v_heads(&self) -> &[Vec<f32>] {
140 &self.v
141 }
142
143 pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
147 self.o1 = Some(O1State::Collecting {
148 m,
149 w,
150 sink,
151 rect,
152 q_buf: Vec::new(),
153 });
154 }
155
156 pub fn o1_push_q(&mut self, q_all: &[f32]) {
161 if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
162 q_buf.extend_from_slice(q_all);
163 }
164 }
165
166 pub fn o1_sealed(&self) -> bool {
167 matches!(self.o1, Some(O1State::Sealed { .. }))
168 }
169
170 pub fn o1_seal(&mut self, num_heads: usize) -> bool {
176 if !matches!(self.o1, Some(O1State::Collecting { .. })) {
179 return self.o1_sealed();
180 }
181 let Some(O1State::Collecting {
182 m,
183 w,
184 sink,
185 rect,
186 q_buf,
187 }) = self.o1.take()
188 else {
189 unreachable!("checked above");
190 };
191 let (hd, t) = (self.head_dim, self.seq_len);
192 let nkv = self.num_kv_heads.max(1);
193 let hpk = num_heads / nkv;
194 let ok = t > 0
195 && self.mode == KvMode::F32
196 && q_buf.len() == t * num_heads * hd
197 && hpk * nkv == num_heads
198 && (0..self.num_kv_heads).all(|g| self.head_len(g) == t);
199 if !ok {
200 tracing::warn!(
201 "o1: cannot seal (needs f32 KV mode, dense heads, full query \
202 trace, num_heads divisible by num_kv_heads) — layer keeps \
203 exact attention"
204 );
205 return false;
206 }
207 let mut groups = Vec::with_capacity(self.num_kv_heads);
208 let mut qh = vec![0.0f32; hpk * t * hd];
211 for g in 0..self.num_kv_heads {
212 for hh in 0..hpk {
213 let h = g * hpk + hh;
214 for p in 0..t {
215 let src = (p * num_heads + h) * hd;
216 let dst = (hh * t + p) * hd;
217 qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
218 }
219 }
220 let qs: Vec<&[f32]> = (0..hpk)
221 .map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd])
222 .collect();
223 let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
224 st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
225 groups.push(st);
226 }
227 for h in 0..self.num_kv_heads {
230 self.k[h] = Vec::new();
231 self.v[h] = Vec::new();
232 }
233 self.imp = Vec::new();
234 self.o1 = Some(O1State::Sealed { groups });
235 true
236 }
237
238 pub fn o1_views(&self) -> Option<Vec<crate::nystrom::O1DeviceView<'_>>> {
247 let Some(O1State::Sealed { groups }) = &self.o1 else {
248 return None;
249 };
250 let views: Vec<_> = groups.iter().map(|g| g.device_view()).collect();
251 if views.iter().any(|v| v.exact_only) {
252 return None;
253 }
254 Some(views)
255 }
256
257 pub fn o1_step(
258 &mut self,
259 q_all: &[f32],
260 k_new: &[f32],
261 v_new: &[f32],
262 num_heads: usize,
263 ) -> Vec<f32> {
264 let hd = self.head_dim;
265 let hpk = num_heads / self.num_kv_heads.max(1);
266 let mut out = vec![0.0f32; num_heads * hd];
267 let Some(O1State::Sealed { groups }) = &mut self.o1 else {
268 debug_assert!(false, "o1_step on an unsealed layer");
269 return out;
270 };
271 for (g, st) in groups.iter_mut().enumerate() {
272 let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
273 st.step_group(
274 &q_all[lo..hi],
275 &k_new[g * hd..(g + 1) * hd],
276 &v_new[g * hd..(g + 1) * hd],
277 &mut out[lo..hi],
278 );
279 }
280 self.seq_len += 1;
283 out
284 }
285
286 pub fn o1_memory_bytes(&self) -> usize {
289 match &self.o1 {
290 Some(O1State::Collecting { q_buf, .. }) => q_buf.len() * std::mem::size_of::<f32>(),
291 Some(O1State::Sealed { groups }) => groups.iter().map(|s| s.memory_bytes()).sum(),
292 None => 0,
293 }
294 }
295
296 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>, group: usize) {
299 let mut resid = vec![0.0f32; row.len()];
300 for (d, &x) in row.iter().enumerate() {
301 resid[d] = if col.is_empty() { x } else { x / col[d] };
302 }
303 for g0 in (0..row.len()).step_by(group) {
304 let g1 = (g0 + group).min(row.len());
305 let mut absmax = 0.0f32;
306 for &r in &resid[g0..g1] {
307 absmax = absmax.max(r.abs());
308 }
309 let s = (absmax / 127.0).max(1e-12);
310 sc.push(s);
311 for &r in &resid[g0..g1] {
312 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
313 }
314 }
315 }
316
317 fn freeze_cols(&mut self) {
320 let hd = self.head_dim;
321 let ngk = hd.div_ceil(KV_K_GROUP);
322 for h in 0..self.num_kv_heads {
323 for (qv, sv, colv, group) in [
324 (
325 &mut self.kq[h],
326 &mut self.ks[h],
327 &mut self.kcol[h],
328 KV_K_GROUP,
329 ),
330 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
331 ] {
332 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
334 if n == 0 {
335 continue;
336 }
337 let mut rows = vec![0.0f32; n * hd];
339 for p in 0..n {
340 for d in 0..hd {
341 rows[p * hd + d] = qv[p * hd + d] as f32 * sv[p * spp + d / group];
342 }
343 }
344 let mut col = vec![0.0f32; hd];
345 for p in 0..n {
346 for d in 0..hd {
347 col[d] += rows[p * hd + d] * rows[p * hd + d];
348 }
349 }
350 for c in col.iter_mut() {
351 *c = (*c / n as f32).sqrt().max(1e-6);
352 }
353 qv.clear();
354 sv.clear();
355 for p in 0..n {
356 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
357 }
358 *colv = col;
359 }
360 }
361 }
362
363 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
367 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
368 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
369 if matches!(self.mode, KvMode::Q8 { .. })
375 && self.seq_len >= KV_COL_WARMUP
376 && self.kcol.iter().all(Vec::is_empty)
377 && self.vcol.iter().all(Vec::is_empty)
378 {
379 self.freeze_cols();
380 }
381 for h in 0..self.num_kv_heads {
382 if !alive.get(h).copied().unwrap_or(true) {
383 continue;
384 }
385 let s = h * self.head_dim;
386 if self.mode.quant_k() {
387 Self::quant_row(
388 &k_new[s..s + self.head_dim],
389 &self.kcol[h],
390 &mut self.kq[h],
391 &mut self.ks[h],
392 KV_K_GROUP,
393 );
394 } else {
395 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
396 }
397 if self.mode.quant_v() {
398 Self::quant_row(
399 &v_new[s..s + self.head_dim],
400 &self.vcol[h],
401 &mut self.vq[h],
402 &mut self.vs[h],
403 self.head_dim,
404 );
405 } else {
406 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
407 }
408 }
409 self.imp.push(0.0);
410 self.seq_len += 1;
411 }
412
413 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
418 let hd = self.head_dim;
419 if self.mode == KvMode::F32 {
420 let stored = self.k[kv_head].len() / hd;
421 return crate::attention::attention_head(
422 q,
423 &self.k[kv_head],
424 &self.v[kv_head],
425 hd,
426 stored,
427 );
428 }
429 let stored = self.head_len(kv_head);
430 let scale = 1.0 / (hd as f32).sqrt();
431 let mut scores = vec![0.0f32; stored];
432 if self.mode.quant_k() {
433 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
434 let kcol = &self.kcol[kv_head];
436 let mut qc = vec![0.0f32; hd];
437 for d in 0..hd {
438 qc[d] = if kcol.is_empty() {
439 q[d]
440 } else {
441 q[d] * kcol[d]
442 };
443 }
444 let ng = hd.div_ceil(KV_K_GROUP);
445 for p in 0..stored {
446 let row = &kq[p * hd..(p + 1) * hd];
447 let row_u8 =
450 unsafe { std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len()) };
451 let mut dot = 0.0f32;
452 for g in 0..ng {
453 let g0 = g * KV_K_GROUP;
454 let g1 = (g0 + KV_K_GROUP).min(hd);
455 dot +=
456 crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1]) * ks[p * ng + g];
457 }
458 scores[p] = dot * scale;
459 }
460 } else {
461 let k = &self.k[kv_head];
462 for p in 0..stored {
463 let row = &k[p * hd..(p + 1) * hd];
464 scores[p] = crate::attention::dot_f32(q, row) * scale;
465 }
466 }
467 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
468 let mut sum = 0.0f32;
469 for s in scores.iter_mut() {
470 *s = (*s - max_score).exp();
471 sum += *s;
472 }
473 if sum > 0.0 {
474 for s in scores.iter_mut() {
475 *s /= sum;
476 }
477 }
478 let mut acc = vec![0.0f32; hd];
479 if self.mode.quant_v() {
480 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
481 for p in 0..stored {
482 let w = scores[p] * vs[p];
483 if w.abs() < 1e-12 {
484 continue;
485 }
486 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
487 }
488 let vcol = &self.vcol[kv_head];
489 if !vcol.is_empty() {
490 for d in 0..hd {
491 acc[d] *= vcol[d];
492 }
493 }
494 } else {
495 let v = &self.v[kv_head];
496 for p in 0..stored {
497 let w = scores[p];
498 if w.abs() < 1e-12 {
499 continue;
500 }
501 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
502 }
503 }
504 (acc, scores)
505 }
506
507 #[allow(clippy::too_many_arguments)]
521 pub fn attend_group(
522 &self,
523 q_group: &[f32],
524 kv_head: usize,
525 out: &mut [f32],
526 imp_acc: &mut [f32],
527 scale: f32,
528 first: usize,
529 softcap: f32,
530 ) {
531 let hd = self.head_dim;
532 let nheads = q_group.len() / hd;
533 debug_assert_eq!(out.len(), nheads * hd);
534 let stored = if self.mode == KvMode::F32 {
535 self.k[kv_head].len() / hd
536 } else {
537 self.head_len(kv_head)
538 };
539 if stored == 0 {
540 out.fill(0.0);
541 return;
542 }
543 let first = first.min(stored.saturating_sub(1));
544
545 thread_local! {
546 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
548 const { std::cell::RefCell::new(Vec::new()) };
549 static GQA_QC: std::cell::RefCell<Vec<f32>> =
551 const { std::cell::RefCell::new(Vec::new()) };
552 }
553
554 GQA_SCORES.with(|sc| {
555 let mut scores = sc.borrow_mut();
556 if first > 0 {
557 scores.clear();
560 scores.resize(nheads * stored, f32::NEG_INFINITY);
561 } else {
562 scores.resize(nheads * stored, 0.0);
563 }
564
565 if self.mode.quant_k() {
567 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
568 let kcol = &self.kcol[kv_head];
569 let ng = hd.div_ceil(KV_K_GROUP);
570 GQA_QC.with(|qc| {
571 let mut qcb = qc.borrow_mut();
572 qcb.resize(nheads * hd, 0.0);
573 for h in 0..nheads {
574 for d in 0..hd {
575 let qv = q_group[h * hd + d];
576 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
577 }
578 }
579 for p in first..stored {
580 let row = &kq[p * hd..(p + 1) * hd];
581 let row_u8 = unsafe {
584 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
585 };
586 for h in 0..nheads {
587 let qch = &qcb[h * hd..(h + 1) * hd];
588 let mut dot = 0.0f32;
589 for g in 0..ng {
590 let g0 = g * KV_K_GROUP;
591 let g1 = (g0 + KV_K_GROUP).min(hd);
592 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
593 * ks[p * ng + g];
594 }
595 scores[h * stored + p] = dot * scale;
596 }
597 }
598 });
599 } else {
600 let k = &self.k[kv_head];
601 for p in first..stored {
602 let row = &k[p * hd..(p + 1) * hd];
603 for h in 0..nheads {
604 scores[h * stored + p] =
605 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
606 }
607 }
608 }
609
610 if softcap > 0.0 {
615 for v in scores.iter_mut() {
616 if v.is_finite() {
617 *v = softcap * (*v / softcap).tanh();
618 }
619 }
620 }
621
622 for h in 0..nheads {
624 let s = &mut scores[h * stored..(h + 1) * stored];
625 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
626 let mut sum = 0.0f32;
627 for v in s.iter_mut() {
628 *v = (*v - max_score).exp();
629 sum += *v;
630 }
631 if sum > 0.0 {
632 for v in s.iter_mut() {
633 *v /= sum;
634 }
635 }
636 }
637
638 out.fill(0.0);
640 if self.mode.quant_v() {
641 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
642 for p in first..stored {
643 let row = &vq[p * hd..(p + 1) * hd];
644 for h in 0..nheads {
645 let w = scores[h * stored + p] * vs[p];
646 if w.abs() < 1e-12 {
647 continue;
648 }
649 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
650 }
651 }
652 let vcol = &self.vcol[kv_head];
653 if !vcol.is_empty() {
654 for h in 0..nheads {
655 for d in 0..hd {
656 out[h * hd + d] *= vcol[d];
657 }
658 }
659 }
660 } else {
661 let v = &self.v[kv_head];
662 for p in first..stored {
663 let row = &v[p * hd..(p + 1) * hd];
664 for h in 0..nheads {
665 let w = scores[h * stored + p];
666 if w.abs() < 1e-12 {
667 continue;
668 }
669 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
670 }
671 }
672 }
673
674 let n = imp_acc.len().min(stored);
677 for h in 0..nheads {
678 let s = &scores[h * stored..(h + 1) * stored];
679 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
680 *dst += p;
681 }
682 }
683 });
684 }
685
686 #[cfg(target_arch = "aarch64")]
694 #[allow(clippy::too_many_arguments)]
695 pub fn attend_chunk(
696 &mut self,
697 q_all: &[f32],
698 b: usize,
699 s0: usize,
700 nh: usize,
701 heads_per_kv: usize,
702 hd: usize,
703 out: &mut [f32],
704 pool: Option<&crate::pool::Pool>,
705 scale: f32,
706 window: Option<usize>,
707 ) {
708 let n = s0 + b;
709 struct SendPtr(*mut f32);
710 unsafe impl Send for SendPtr {}
711 unsafe impl Sync for SendPtr {}
712 impl SendPtr {
713 fn at(&self, i: usize) -> *mut f32 {
714 unsafe { self.0.add(i) }
717 }
718 }
719 thread_local! {
720 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
721 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
722 }
723 let neon_gemm = cfg!(not(target_os = "macos"))
728 || std::env::var("CMF_FORCE_NEON_GEMM")
729 .map(|v| v == "1")
730 .unwrap_or(false);
731 SCRATCH.with(|s| {
732 let mut s = s.borrow_mut();
733 let (qpanel, scores, aopanel, ktpack) = &mut *s;
734 let m = heads_per_kv * b;
739 qpanel.resize(m * hd, 0.0);
740 scores.resize(m * n, 0.0);
741 aopanel.resize(m * hd, 0.0);
742 for g in 0..self.num_kv_heads {
743 let kmat = &self.k[g];
744 let vmat = &self.v[g];
745 debug_assert_eq!(kmat.len(), n * hd);
746 for hl in 0..heads_per_kv {
747 let hh = g * heads_per_kv + hl;
748 for bi in 0..b {
749 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
750 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
751 }
752 }
753 if neon_gemm {
754 ktpack.resize(hd * n, 0.0);
755 for p in 0..n {
756 let row = &kmat[p * hd..(p + 1) * hd];
757 for (d, &v) in row.iter().enumerate() {
758 ktpack[d * n + p] = v;
759 }
760 }
761 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
764 let sp_s = SendPtr(scores.as_mut_ptr());
765 let kt = &*ktpack;
766 let run = |start: usize, end: usize| {
767 if end > start {
768 let a = unsafe {
770 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
771 };
772 let c = unsafe {
773 std::slice::from_raw_parts_mut(
774 sp_s.at(start * n),
775 (end - start) * n,
776 )
777 };
778 crate::qtensor::neon_gemm_rm(
779 end - start,
780 n,
781 hd,
782 scale,
783 a,
784 hd,
785 kt,
786 n,
787 false,
788 c,
789 n,
790 );
791 }
792 };
793 match pool {
794 Some(p) if m >= 64 => p.run_rows(m, &run),
795 _ => run(0, m),
796 }
797 } else {
798 crate::qtensor::sgemm_rm(
799 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
800 );
801 }
802 let sp = SendPtr(scores.as_mut_ptr());
804 let run = |start: usize, end: usize| {
805 for r in start..end {
806 let allowed = s0 + (r % b) + 1;
807 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
811 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
813 crate::attention::softmax_row(&mut row[lo..allowed]);
814 row[..lo].fill(0.0);
815 row[allowed..].fill(0.0);
816 }
817 };
818 match pool {
819 Some(p) if m >= 64 => p.run_rows(m, &run),
820 _ => run(0, m),
821 }
822 let ni = self.imp.len().min(n);
826 for r in 0..m {
827 let al = (s0 + (r % b) + 1).min(ni);
828 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
829 *dst += p;
830 }
831 }
832 if neon_gemm {
833 let sp_s = SendPtr(scores.as_mut_ptr());
834 let sp_o = SendPtr(aopanel.as_mut_ptr());
835 let run = |start: usize, end: usize| {
836 if end > start {
837 let a = unsafe {
839 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
840 };
841 let c = unsafe {
842 std::slice::from_raw_parts_mut(
843 sp_o.at(start * hd),
844 (end - start) * hd,
845 )
846 };
847 crate::qtensor::neon_gemm_rm(
848 end - start,
849 hd,
850 n,
851 1.0,
852 a,
853 n,
854 vmat,
855 hd,
856 false,
857 c,
858 hd,
859 );
860 }
861 };
862 match pool {
863 Some(p) if m >= 64 => p.run_rows(m, &run),
864 _ => run(0, m),
865 }
866 } else {
867 crate::qtensor::sgemm_rm(
868 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
869 );
870 }
871 for hl in 0..heads_per_kv {
872 let hh = g * heads_per_kv + hl;
873 for bi in 0..b {
874 out[bi * nh * hd + hh * hd..][..hd]
875 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
876 }
877 }
878 }
879 });
880 }
881
882 pub fn truncate_last(&mut self, n_drop: usize) {
884 let d = n_drop.min(self.seq_len);
885 for h in 0..self.num_kv_heads {
886 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
887 self.k[h].truncate(keep);
888 self.v[h].truncate(keep);
889 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
890 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
891 self.kq[h].truncate(keep_q);
892 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
893 self.vq[h].truncate(keep_vq);
894 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
895 self.ks[h].truncate(keep_ks);
896 let keep_vs = self.vs[h].len().saturating_sub(d);
897 self.vs[h].truncate(keep_vs);
898 }
899 self.imp.truncate(self.imp.len().saturating_sub(d));
900 self.seq_len -= d;
901 }
902
903 pub fn accumulate_imp(&mut self, probs: &[f32]) {
905 for (dst, &p) in self.imp.iter_mut().zip(probs) {
906 *dst += p;
907 }
908 }
909
910 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
912 &self.k[kv_head]
913 }
914
915 pub fn head_values(&self, kv_head: usize) -> &[f32] {
916 &self.v[kv_head]
917 }
918
919 pub fn head_len(&self, kv_head: usize) -> usize {
921 let ng = self.head_dim.div_ceil(KV_K_GROUP);
922 (self.k[kv_head].len() / self.head_dim)
923 .max(self.ks[kv_head].len() / ng)
924 .max(self.vs[kv_head].len())
925 }
926
927 pub fn clear(&mut self) {
929 for h in 0..self.num_kv_heads {
930 self.k[h].clear();
931 self.v[h].clear();
932 self.kq[h].clear();
933 self.ks[h].clear();
934 self.vq[h].clear();
935 self.vs[h].clear();
936 self.kcol[h].clear();
937 self.vcol[h].clear();
938 }
939 self.imp.clear();
940 self.linear_state.clear();
941 self.linear_scratch.clear();
942 self.o1 = None;
945 self.seq_len = 0;
946 }
947
948 pub fn export_wire(&self, f16: bool) -> Result<Vec<u8>, String> {
958 if !matches!(self.mode, KvMode::F32) {
959 return Err("kv export: only the F32 cache is described by this format (CMF_KV=q8 stores int8 rows and per-row scales)"
960 .into());
961 }
962 if self.o1.is_some() {
963 return Err("kv export: an O(1) Nyström overlay is not part of this format — the skeletons are irreversible and would have to travel with it"
964 .into());
965 }
966 if self.kcol.iter().any(|c| !c.is_empty()) || self.vcol.iter().any(|c| !c.is_empty()) {
970 return Err("kv export: frozen columns under an F32 cache — refusing to ship \
971 a state this format does not describe"
972 .into());
973 }
974 let mut out = Vec::with_capacity(self.memory_bytes() / if f16 { 2 } else { 1 } + 64);
975 let mut u = |v: u32, o: &mut Vec<u8>| o.extend_from_slice(&v.to_le_bytes());
976 u(u8::from(f16) as u32, &mut out);
977 u(self.seq_len as u32, &mut out);
978 u(self.num_kv_heads as u32, &mut out);
979 u(self.head_dim as u32, &mut out);
980 u(self.linear_state.len() as u32, &mut out);
981 u(self.imp.len() as u32, &mut out);
986 let push = |xs: &[f32], o: &mut Vec<u8>| {
987 if f16 {
988 for &x in xs {
989 o.extend_from_slice(&cortiq_core::quant::f32_to_f16(x).to_le_bytes());
990 }
991 } else {
992 for &x in xs {
993 o.extend_from_slice(&x.to_le_bytes());
994 }
995 }
996 };
997 for &x in &self.linear_state {
1001 out.extend_from_slice(&x.to_le_bytes());
1002 }
1003 for &x in &self.imp {
1004 out.extend_from_slice(&x.to_le_bytes());
1005 }
1006 for h in 0..self.num_kv_heads {
1007 u(self.k[h].len() as u32, &mut out);
1008 push(&self.k[h], &mut out);
1009 u(self.v[h].len() as u32, &mut out);
1010 push(&self.v[h], &mut out);
1011 }
1012 Ok(out)
1013 }
1014
1015 pub fn import_wire(&mut self, buf: &[u8]) -> Result<(), String> {
1018 let mut o = 0usize;
1019 let mut u32_at = |o: &mut usize| -> Result<u32, String> {
1020 if *o + 4 > buf.len() {
1021 return Err("kv import: truncated header".into());
1022 }
1023 let v = u32::from_le_bytes(buf[*o..*o + 4].try_into().unwrap());
1024 *o += 4;
1025 Ok(v)
1026 };
1027 let f16 = u32_at(&mut o)? != 0;
1028 let seq_len = u32_at(&mut o)? as usize;
1029 let heads = u32_at(&mut o)? as usize;
1030 let hd = u32_at(&mut o)? as usize;
1031 let lin = u32_at(&mut o)? as usize;
1032 let nimp = u32_at(&mut o)? as usize;
1033 if heads != self.num_kv_heads || hd != self.head_dim {
1034 return Err(format!(
1035 "kv import: peer sent {heads}×{hd} per position, this layer is {}×{}",
1036 self.num_kv_heads, self.head_dim
1037 ));
1038 }
1039 let w = if f16 { 2 } else { 4 };
1040 let need = |n: usize, o: usize| -> Result<(), String> {
1041 if o + n > buf.len() {
1042 Err("kv import: truncated payload".into())
1043 } else {
1044 Ok(())
1045 }
1046 };
1047 need(lin * 4, o)?;
1048 self.linear_state = (0..lin)
1049 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1050 .collect();
1051 o += lin * 4;
1052 need(nimp * 4, o)?;
1053 let imp: Vec<f32> = (0..nimp)
1054 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1055 .collect();
1056 o += nimp * 4;
1057 let mut k: Vec<Vec<f32>> = Vec::with_capacity(heads);
1058 let mut v: Vec<Vec<f32>> = Vec::with_capacity(heads);
1059 for _ in 0..heads {
1060 for which in 0..2 {
1061 let n = u32_at(&mut o)? as usize;
1062 need(n * w, o)?;
1063 let xs: Vec<f32> = (0..n)
1064 .map(|i| {
1065 let at = o + i * w;
1066 if f16 {
1067 cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
1068 buf[at..at + 2].try_into().unwrap(),
1069 ))
1070 } else {
1071 f32::from_le_bytes(buf[at..at + 4].try_into().unwrap())
1072 }
1073 })
1074 .collect();
1075 o += n * w;
1076 if which == 0 { k.push(xs) } else { v.push(xs) }
1077 }
1078 }
1079 self.mode = KvMode::F32;
1080 self.k = k;
1081 self.v = v;
1082 self.kq = vec![Vec::new(); heads];
1083 self.ks = vec![Vec::new(); heads];
1084 self.vq = vec![Vec::new(); heads];
1085 self.vs = vec![Vec::new(); heads];
1086 self.kcol = vec![Vec::new(); heads];
1087 self.vcol = vec![Vec::new(); heads];
1088 self.imp = imp;
1089 self.o1 = None;
1090 self.seq_len = seq_len;
1091 Ok(())
1092 }
1093
1094 pub fn memory_bytes(&self) -> usize {
1095 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
1096 + self.v.iter().map(Vec::len).sum::<usize>()
1097 + self.ks.iter().map(Vec::len).sum::<usize>()
1098 + self.vs.iter().map(Vec::len).sum::<usize>()
1099 + self.kcol.iter().map(Vec::len).sum::<usize>()
1100 + self.vcol.iter().map(Vec::len).sum::<usize>();
1101 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
1102 + self.vq.iter().map(Vec::len).sum::<usize>();
1103 floats * std::mem::size_of::<f32>()
1104 + bytes
1105 + self.linear_state.len() * std::mem::size_of::<f32>()
1109 + self.o1_memory_bytes()
1112 }
1113
1114 fn evict(&mut self, keep_last: usize) {
1116 if self.o1_sealed() || self.seq_len <= keep_last {
1120 return;
1121 }
1122 let drop = self.seq_len - keep_last;
1123 for h in 0..self.num_kv_heads {
1124 let stored = self.head_len(h);
1126 let d = drop.min(stored);
1127 let hd = self.head_dim;
1128 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
1129 let n = n.min(v.len());
1130 v.drain(..n);
1131 }
1132 drop_front(&mut self.k[h], d * hd);
1133 drop_front(&mut self.v[h], d * hd);
1134 drop_front(&mut self.kq[h], d * hd);
1135 drop_front(&mut self.vq[h], d * hd);
1136 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
1137 drop_front(&mut self.vs[h], d);
1138 }
1139 let d = drop.min(self.imp.len());
1140 self.imp.drain(..d);
1141 self.seq_len = keep_last;
1142 }
1143
1144 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
1149 if self.o1_sealed() {
1150 return; }
1152 let stored = self.imp.len();
1153 if stored <= keep_last {
1154 return;
1155 }
1156 let sink_n = sink.min(keep_last);
1159 let recent_n = recent.min(keep_last - sink_n);
1160 let mut keep = vec![false; stored];
1161 for k in keep.iter_mut().take(sink_n) {
1162 *k = true;
1163 }
1164 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1165 *k = true;
1166 }
1167 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1168 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1170 order.sort_by(|&a, &b| {
1171 self.imp[b]
1172 .partial_cmp(&self.imp[a])
1173 .unwrap_or(std::cmp::Ordering::Equal)
1174 });
1175 for i in order {
1176 if budget == 0 {
1177 break;
1178 }
1179 keep[i] = true;
1180 budget -= 1;
1181 }
1182
1183 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1184 let hd = self.head_dim;
1185 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1186 let mut out = Vec::with_capacity(kept.len() * step);
1187 for &i in kept {
1188 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1189 }
1190 out
1191 }
1192 for h in 0..self.num_kv_heads {
1197 if !self.k[h].is_empty() {
1198 self.k[h] = gather(&self.k[h], &kept, hd);
1199 }
1200 if !self.v[h].is_empty() {
1201 self.v[h] = gather(&self.v[h], &kept, hd);
1202 }
1203 if !self.kq[h].is_empty() {
1204 self.kq[h] = gather(&self.kq[h], &kept, hd);
1205 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1206 }
1207 if !self.vq[h].is_empty() {
1208 self.vq[h] = gather(&self.vq[h], &kept, hd);
1209 self.vs[h] = gather(&self.vs[h], &kept, 1);
1210 }
1211 }
1212 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1213 self.seq_len = kept.len();
1214 }
1215}
1216
1217#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1219pub enum EvictionPolicy {
1220 Recent,
1222 Born { sink: usize },
1224}
1225
1226#[derive(Debug)]
1228pub struct KvCache {
1229 pub layers: Vec<LayerKvCache>,
1230 pub max_seq_len: usize,
1231 pub policy: EvictionPolicy,
1232}
1233
1234impl KvCache {
1235 pub fn new(
1236 num_layers: usize,
1237 num_kv_heads: usize,
1238 head_dim: usize,
1239 max_seq_len: usize,
1240 ) -> Self {
1241 let layers = (0..num_layers)
1242 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1243 .collect();
1244 Self {
1245 layers,
1246 max_seq_len,
1247 policy: EvictionPolicy::Born { sink: 4 },
1248 }
1249 }
1250
1251 pub fn clear(&mut self) {
1252 for layer in &mut self.layers {
1253 layer.clear();
1254 }
1255 }
1256
1257 pub fn total_memory_bytes(&self) -> usize {
1258 self.layers.iter().map(|l| l.memory_bytes()).sum()
1259 }
1260
1261 pub fn seq_len(&self) -> usize {
1263 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1264 }
1265
1266 pub fn needs_eviction(&self) -> bool {
1267 self.seq_len() >= self.max_seq_len
1268 }
1269
1270 pub fn evict(&mut self, keep_last: usize) {
1272 match self.policy {
1273 EvictionPolicy::Recent => {
1274 for layer in &mut self.layers {
1275 layer.evict(keep_last);
1276 }
1277 }
1278 EvictionPolicy::Born { sink } => {
1279 let recent = (keep_last / 2).max(1);
1280 for layer in &mut self.layers {
1281 layer.evict_born(keep_last, sink, recent);
1282 }
1283 }
1284 }
1285 }
1286}
1287
1288#[cfg(test)]
1289mod tests {
1290 use super::*;
1291
1292 #[test]
1293 fn wire_round_trip_reproduces_attention() {
1294 let (heads, hd) = (2usize, 4usize);
1298 let mut a = LayerKvCache::new(heads, hd);
1299 for p in 0..5 {
1300 let k: Vec<f32> = (0..heads * hd).map(|i| (p * 10 + i) as f32 * 0.031).collect();
1301 let v: Vec<f32> = (0..heads * hd).map(|i| (p * 7 + i) as f32 * -0.017).collect();
1302 a.append(&k, &v, &[true, true]);
1303 }
1304 a.linear_state = vec![0.5, -0.25, 1.0];
1305 let q: Vec<f32> = (0..hd).map(|i| 0.1 * (i as f32 + 1.0)).collect();
1306
1307 let bytes = a.export_wire(false).expect("f32 cache exports");
1308 let mut b = LayerKvCache::new(heads, hd);
1309 b.import_wire(&bytes).expect("import");
1310
1311 assert_eq!(b.seq_len, a.seq_len);
1312 assert_eq!(b.linear_state, a.linear_state);
1313 for h in 0..heads {
1314 let (oa, sa) = a.attend(&q, h);
1315 let (ob, sb) = b.attend(&q, h);
1316 assert_eq!(oa, ob, "head {h} attention output diverged");
1317 assert_eq!(sa, sb, "head {h} attention scores diverged");
1318 }
1319 }
1320
1321 #[test]
1322 fn wire_refuses_what_it_cannot_describe() {
1323 let mut c = LayerKvCache::new(1, 4);
1326 c.mode = KvMode::Q8 { k: true, v: true };
1327 let err = c.export_wire(false).unwrap_err();
1328 assert!(err.contains("F32"), "{err}");
1329 }
1330
1331 #[test]
1332 fn wire_import_checks_geometry() {
1333 let a = LayerKvCache::new(2, 4);
1334 let bytes = a.export_wire(false).unwrap();
1335 let mut wrong = LayerKvCache::new(2, 8);
1336 let err = wrong.import_wire(&bytes).unwrap_err();
1337 assert!(err.contains("2×4"), "{err}");
1338 }
1339
1340 #[test]
1341 fn append_tracks_seq_len_and_layout() {
1342 let mut cache = LayerKvCache::new(4, 8);
1343 cache.mode = KvMode::F32;
1344 assert_eq!(cache.seq_len, 0);
1345
1346 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1347 let v = vec![2.0f32; 32];
1348 cache.append(&k, &v, &[true; 4]);
1349
1350 assert_eq!(cache.seq_len, 1);
1351 assert_eq!(cache.head_len(0), 1);
1352 assert_eq!(cache.head_keys(1), &k[8..16]);
1354 assert_eq!(cache.memory_bytes(), 256);
1355 }
1356
1357 #[test]
1358 fn dead_head_stores_nothing() {
1359 let mut cache = LayerKvCache::new(2, 4);
1360 cache.mode = KvMode::F32;
1361 let k = vec![1.0f32; 8];
1362 let v = vec![2.0f32; 8];
1363 cache.append(&k, &v, &[true, false]);
1364 cache.append(&k, &v, &[true, false]);
1365
1366 assert_eq!(cache.seq_len, 2);
1367 assert_eq!(cache.head_len(0), 2);
1368 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1369 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1370 }
1371
1372 #[test]
1373 fn eviction_keeps_recent() {
1374 let mut cache = KvCache::new(2, 4, 8, 10);
1375 cache.policy = EvictionPolicy::Recent;
1376 for l in &mut cache.layers {
1377 l.mode = KvMode::F32;
1378 }
1379 let k = vec![1.0f32; 32];
1380 let v = vec![2.0f32; 32];
1381 for _ in 0..8 {
1382 for layer in &mut cache.layers {
1383 layer.append(&k, &v, &[true; 4]);
1384 }
1385 }
1386 assert_eq!(cache.seq_len(), 8);
1387 assert!(!cache.needs_eviction());
1388
1389 cache.evict(4);
1390 assert_eq!(cache.seq_len(), 4);
1391 assert_eq!(cache.layers[0].head_len(0), 4);
1392 }
1393
1394 #[test]
1395 fn truncate_rolls_back_speculative_positions() {
1396 let mut cache = LayerKvCache::new(2, 4);
1397 cache.mode = KvMode::F32;
1398 for pos in 0..5 {
1399 let k = vec![pos as f32; 8];
1400 let v = vec![pos as f32; 8];
1401 cache.append(&k, &v, &[true; 2]);
1402 }
1403 cache.truncate_last(2);
1404 assert_eq!(cache.seq_len, 3);
1405 assert_eq!(cache.head_len(0), 3);
1406 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1407 }
1408
1409 #[test]
1413 fn q8_attend_matches_f32_within_grid() {
1414 let (heads, hd) = (2, 32);
1415 let mut f = LayerKvCache::new(heads, hd);
1416 f.mode = KvMode::F32;
1417 let mut q8 = LayerKvCache::new(heads, hd);
1418 q8.mode = KvMode::Q8 { k: true, v: true };
1419
1420 let synth = |p: usize, salt: usize| -> Vec<f32> {
1421 (0..heads * hd)
1422 .map(|i| {
1423 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1424 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1426 })
1427 .collect()
1428 };
1429 for p in 0..100 {
1430 let k = synth(p, 1);
1431 let v = synth(p, 2);
1432 f.append(&k, &v, &[true; 2]);
1433 q8.append(&k, &v, &[true; 2]);
1434 }
1435 let q: Vec<f32> = (0..hd)
1436 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1437 .collect();
1438 for g in 0..heads {
1439 let (of, pf) = f.attend(&q, g);
1440 let (o8, p8) = q8.attend(&q, g);
1441 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1442 for d in 0..hd {
1443 assert!(
1444 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1445 "g{g} d{d}: f32 {} vs q8 {}",
1446 of[d],
1447 o8[d]
1448 );
1449 }
1450 for p in 0..100 {
1451 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1452 }
1453 }
1454 q8.truncate_last(30);
1456 assert_eq!(q8.head_len(0), 70);
1457 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1458 q8.accumulate_imp(&imp);
1459 q8.evict_born(20, 2, 8);
1460 assert_eq!(q8.head_len(0), 20);
1461 let (o, _) = q8.attend(&q, 0);
1462 assert!(o.iter().all(|x| x.is_finite()));
1463 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1465 }
1466
1467 #[test]
1470 fn attend_group_equals_per_head_attend_bitexact() {
1471 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1473 let mut c = LayerKvCache::new(kv_heads, hd);
1474 c.mode = mode;
1475 for p in 0..70 {
1476 let k: Vec<f32> = (0..kv_heads * hd)
1477 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1478 .collect();
1479 let v: Vec<f32> = (0..kv_heads * hd)
1480 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1481 .collect();
1482 c.append(&k, &v, &[true; 2]);
1483 }
1484 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1485 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1486 .collect();
1487 for g in 0..kv_heads {
1488 let span = g * hpk * hd..(g + 1) * hpk * hd;
1489 let mut out = vec![0f32; hpk * hd];
1490 let mut imp = vec![0f32; 70];
1491 c.attend_group(
1492 &q[span.clone()],
1493 g,
1494 &mut out,
1495 &mut imp,
1496 1.0 / (hd as f32).sqrt(),
1497 0,
1498 0.0,
1499 );
1500 let mut imp_ref = vec![0f32; 70];
1501 for h in 0..hpk {
1502 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1503 let (o, probs) = c.attend(qh, g);
1504 assert_eq!(
1505 &out[h * hd..(h + 1) * hd],
1506 &o[..],
1507 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1508 );
1509 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1510 *dst += p;
1511 }
1512 }
1513 assert_eq!(imp, imp_ref, "mode {mode:?} g{g}: Born mass must match");
1514 }
1515 }
1516 }
1517
1518 #[test]
1522 fn born_eviction_mixed_modes_stay_consistent() {
1523 for (mk, mv) in [(false, true), (true, false)] {
1524 let mut c = LayerKvCache::new(1, 4);
1525 c.mode = KvMode::Q8 { k: mk, v: mv };
1526 for p in 0..80 {
1527 let k = vec![p as f32 * 0.01; 4];
1528 let v = vec![p as f32; 4];
1529 c.append(&k, &v, &[true]);
1530 }
1531 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1532 c.accumulate_imp(&imp);
1533 let before = c.memory_bytes();
1534 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1536 assert!(
1537 c.memory_bytes() < before / 2,
1538 "memory must shrink (k={mk} v={mv})"
1539 );
1540 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1543 assert!(
1544 out[0] > 30.0,
1545 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1546 out[0]
1547 );
1548 }
1549 }
1550
1551 #[test]
1552 fn born_eviction_keeps_high_mass_position() {
1553 let mut cache = KvCache::new(1, 1, 2, 16);
1554 cache.policy = EvictionPolicy::Born { sink: 1 };
1555 for l in &mut cache.layers {
1556 l.mode = KvMode::F32;
1557 }
1558 let layer = &mut cache.layers[0];
1559 for pos in 0..8 {
1562 let k = vec![pos as f32; 2];
1563 let v = vec![pos as f32 + 100.0; 2];
1564 layer.append(&k, &v, &[true]);
1565 }
1566 let mut imp = vec![0.05f32; 8];
1568 imp[3] = 5.0;
1569 layer.accumulate_imp(&imp);
1570
1571 cache.evict(4); let layer = &cache.layers[0];
1573 assert_eq!(layer.seq_len, 4);
1574 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1575 assert_eq!(
1576 kept_keys,
1577 vec![0.0, 3.0, 6.0, 7.0],
1578 "kept = sink(0) + Born-top(3) + recent(6,7)"
1579 );
1580 assert_eq!(layer.head_len(0), 4);
1582 }
1583}