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_step(
244 &mut self,
245 q_all: &[f32],
246 k_new: &[f32],
247 v_new: &[f32],
248 num_heads: usize,
249 ) -> Vec<f32> {
250 let hd = self.head_dim;
251 let hpk = num_heads / self.num_kv_heads.max(1);
252 let mut out = vec![0.0f32; num_heads * hd];
253 let Some(O1State::Sealed { groups }) = &mut self.o1 else {
254 debug_assert!(false, "o1_step on an unsealed layer");
255 return out;
256 };
257 for (g, st) in groups.iter_mut().enumerate() {
258 let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
259 st.step_group(
260 &q_all[lo..hi],
261 &k_new[g * hd..(g + 1) * hd],
262 &v_new[g * hd..(g + 1) * hd],
263 &mut out[lo..hi],
264 );
265 }
266 self.seq_len += 1;
269 out
270 }
271
272 pub fn o1_memory_bytes(&self) -> usize {
275 match &self.o1 {
276 Some(O1State::Collecting { q_buf, .. }) => q_buf.len() * std::mem::size_of::<f32>(),
277 Some(O1State::Sealed { groups }) => groups.iter().map(|s| s.memory_bytes()).sum(),
278 None => 0,
279 }
280 }
281
282 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>, group: usize) {
285 let mut resid = vec![0.0f32; row.len()];
286 for (d, &x) in row.iter().enumerate() {
287 resid[d] = if col.is_empty() { x } else { x / col[d] };
288 }
289 for g0 in (0..row.len()).step_by(group) {
290 let g1 = (g0 + group).min(row.len());
291 let mut absmax = 0.0f32;
292 for &r in &resid[g0..g1] {
293 absmax = absmax.max(r.abs());
294 }
295 let s = (absmax / 127.0).max(1e-12);
296 sc.push(s);
297 for &r in &resid[g0..g1] {
298 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
299 }
300 }
301 }
302
303 fn freeze_cols(&mut self) {
306 let hd = self.head_dim;
307 let ngk = hd.div_ceil(KV_K_GROUP);
308 for h in 0..self.num_kv_heads {
309 for (qv, sv, colv, group) in [
310 (
311 &mut self.kq[h],
312 &mut self.ks[h],
313 &mut self.kcol[h],
314 KV_K_GROUP,
315 ),
316 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
317 ] {
318 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
320 if n == 0 {
321 continue;
322 }
323 let mut rows = vec![0.0f32; n * hd];
325 for p in 0..n {
326 for d in 0..hd {
327 rows[p * hd + d] = qv[p * hd + d] as f32 * sv[p * spp + d / group];
328 }
329 }
330 let mut col = vec![0.0f32; hd];
331 for p in 0..n {
332 for d in 0..hd {
333 col[d] += rows[p * hd + d] * rows[p * hd + d];
334 }
335 }
336 for c in col.iter_mut() {
337 *c = (*c / n as f32).sqrt().max(1e-6);
338 }
339 qv.clear();
340 sv.clear();
341 for p in 0..n {
342 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
343 }
344 *colv = col;
345 }
346 }
347 }
348
349 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
353 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
354 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
355 if matches!(self.mode, KvMode::Q8 { .. })
361 && self.seq_len >= KV_COL_WARMUP
362 && self.kcol.iter().all(Vec::is_empty)
363 && self.vcol.iter().all(Vec::is_empty)
364 {
365 self.freeze_cols();
366 }
367 for h in 0..self.num_kv_heads {
368 if !alive.get(h).copied().unwrap_or(true) {
369 continue;
370 }
371 let s = h * self.head_dim;
372 if self.mode.quant_k() {
373 Self::quant_row(
374 &k_new[s..s + self.head_dim],
375 &self.kcol[h],
376 &mut self.kq[h],
377 &mut self.ks[h],
378 KV_K_GROUP,
379 );
380 } else {
381 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
382 }
383 if self.mode.quant_v() {
384 Self::quant_row(
385 &v_new[s..s + self.head_dim],
386 &self.vcol[h],
387 &mut self.vq[h],
388 &mut self.vs[h],
389 self.head_dim,
390 );
391 } else {
392 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
393 }
394 }
395 self.imp.push(0.0);
396 self.seq_len += 1;
397 }
398
399 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
404 let hd = self.head_dim;
405 if self.mode == KvMode::F32 {
406 let stored = self.k[kv_head].len() / hd;
407 return crate::attention::attention_head(
408 q,
409 &self.k[kv_head],
410 &self.v[kv_head],
411 hd,
412 stored,
413 );
414 }
415 let stored = self.head_len(kv_head);
416 let scale = 1.0 / (hd as f32).sqrt();
417 let mut scores = vec![0.0f32; stored];
418 if self.mode.quant_k() {
419 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
420 let kcol = &self.kcol[kv_head];
422 let mut qc = vec![0.0f32; hd];
423 for d in 0..hd {
424 qc[d] = if kcol.is_empty() {
425 q[d]
426 } else {
427 q[d] * kcol[d]
428 };
429 }
430 let ng = hd.div_ceil(KV_K_GROUP);
431 for p in 0..stored {
432 let row = &kq[p * hd..(p + 1) * hd];
433 let row_u8 =
436 unsafe { std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len()) };
437 let mut dot = 0.0f32;
438 for g in 0..ng {
439 let g0 = g * KV_K_GROUP;
440 let g1 = (g0 + KV_K_GROUP).min(hd);
441 dot +=
442 crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1]) * ks[p * ng + g];
443 }
444 scores[p] = dot * scale;
445 }
446 } else {
447 let k = &self.k[kv_head];
448 for p in 0..stored {
449 let row = &k[p * hd..(p + 1) * hd];
450 scores[p] = crate::attention::dot_f32(q, row) * scale;
451 }
452 }
453 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
454 let mut sum = 0.0f32;
455 for s in scores.iter_mut() {
456 *s = (*s - max_score).exp();
457 sum += *s;
458 }
459 if sum > 0.0 {
460 for s in scores.iter_mut() {
461 *s /= sum;
462 }
463 }
464 let mut acc = vec![0.0f32; hd];
465 if self.mode.quant_v() {
466 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
467 for p in 0..stored {
468 let w = scores[p] * vs[p];
469 if w.abs() < 1e-12 {
470 continue;
471 }
472 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
473 }
474 let vcol = &self.vcol[kv_head];
475 if !vcol.is_empty() {
476 for d in 0..hd {
477 acc[d] *= vcol[d];
478 }
479 }
480 } else {
481 let v = &self.v[kv_head];
482 for p in 0..stored {
483 let w = scores[p];
484 if w.abs() < 1e-12 {
485 continue;
486 }
487 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
488 }
489 }
490 (acc, scores)
491 }
492
493 #[allow(clippy::too_many_arguments)]
507 pub fn attend_group(
508 &self,
509 q_group: &[f32],
510 kv_head: usize,
511 out: &mut [f32],
512 imp_acc: &mut [f32],
513 scale: f32,
514 first: usize,
515 ) {
516 let hd = self.head_dim;
517 let nheads = q_group.len() / hd;
518 debug_assert_eq!(out.len(), nheads * hd);
519 let stored = if self.mode == KvMode::F32 {
520 self.k[kv_head].len() / hd
521 } else {
522 self.head_len(kv_head)
523 };
524 if stored == 0 {
525 out.fill(0.0);
526 return;
527 }
528 let first = first.min(stored.saturating_sub(1));
529
530 thread_local! {
531 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
533 const { std::cell::RefCell::new(Vec::new()) };
534 static GQA_QC: std::cell::RefCell<Vec<f32>> =
536 const { std::cell::RefCell::new(Vec::new()) };
537 }
538
539 GQA_SCORES.with(|sc| {
540 let mut scores = sc.borrow_mut();
541 if first > 0 {
542 scores.clear();
545 scores.resize(nheads * stored, f32::NEG_INFINITY);
546 } else {
547 scores.resize(nheads * stored, 0.0);
548 }
549
550 if self.mode.quant_k() {
552 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
553 let kcol = &self.kcol[kv_head];
554 let ng = hd.div_ceil(KV_K_GROUP);
555 GQA_QC.with(|qc| {
556 let mut qcb = qc.borrow_mut();
557 qcb.resize(nheads * hd, 0.0);
558 for h in 0..nheads {
559 for d in 0..hd {
560 let qv = q_group[h * hd + d];
561 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
562 }
563 }
564 for p in first..stored {
565 let row = &kq[p * hd..(p + 1) * hd];
566 let row_u8 = unsafe {
569 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
570 };
571 for h in 0..nheads {
572 let qch = &qcb[h * hd..(h + 1) * hd];
573 let mut dot = 0.0f32;
574 for g in 0..ng {
575 let g0 = g * KV_K_GROUP;
576 let g1 = (g0 + KV_K_GROUP).min(hd);
577 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
578 * ks[p * ng + g];
579 }
580 scores[h * stored + p] = dot * scale;
581 }
582 }
583 });
584 } else {
585 let k = &self.k[kv_head];
586 for p in first..stored {
587 let row = &k[p * hd..(p + 1) * hd];
588 for h in 0..nheads {
589 scores[h * stored + p] =
590 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
591 }
592 }
593 }
594
595 for h in 0..nheads {
597 let s = &mut scores[h * stored..(h + 1) * stored];
598 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
599 let mut sum = 0.0f32;
600 for v in s.iter_mut() {
601 *v = (*v - max_score).exp();
602 sum += *v;
603 }
604 if sum > 0.0 {
605 for v in s.iter_mut() {
606 *v /= sum;
607 }
608 }
609 }
610
611 out.fill(0.0);
613 if self.mode.quant_v() {
614 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
615 for p in first..stored {
616 let row = &vq[p * hd..(p + 1) * hd];
617 for h in 0..nheads {
618 let w = scores[h * stored + p] * vs[p];
619 if w.abs() < 1e-12 {
620 continue;
621 }
622 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
623 }
624 }
625 let vcol = &self.vcol[kv_head];
626 if !vcol.is_empty() {
627 for h in 0..nheads {
628 for d in 0..hd {
629 out[h * hd + d] *= vcol[d];
630 }
631 }
632 }
633 } else {
634 let v = &self.v[kv_head];
635 for p in first..stored {
636 let row = &v[p * hd..(p + 1) * hd];
637 for h in 0..nheads {
638 let w = scores[h * stored + p];
639 if w.abs() < 1e-12 {
640 continue;
641 }
642 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
643 }
644 }
645 }
646
647 let n = imp_acc.len().min(stored);
650 for h in 0..nheads {
651 let s = &scores[h * stored..(h + 1) * stored];
652 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
653 *dst += p;
654 }
655 }
656 });
657 }
658
659 #[cfg(target_arch = "aarch64")]
667 #[allow(clippy::too_many_arguments)]
668 pub fn attend_chunk(
669 &mut self,
670 q_all: &[f32],
671 b: usize,
672 s0: usize,
673 nh: usize,
674 heads_per_kv: usize,
675 hd: usize,
676 out: &mut [f32],
677 pool: Option<&crate::pool::Pool>,
678 scale: f32,
679 window: Option<usize>,
680 ) {
681 let n = s0 + b;
682 struct SendPtr(*mut f32);
683 unsafe impl Send for SendPtr {}
684 unsafe impl Sync for SendPtr {}
685 impl SendPtr {
686 fn at(&self, i: usize) -> *mut f32 {
687 unsafe { self.0.add(i) }
690 }
691 }
692 thread_local! {
693 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
694 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
695 }
696 let neon_gemm = cfg!(not(target_os = "macos"))
701 || std::env::var("CMF_FORCE_NEON_GEMM")
702 .map(|v| v == "1")
703 .unwrap_or(false);
704 SCRATCH.with(|s| {
705 let mut s = s.borrow_mut();
706 let (qpanel, scores, aopanel, ktpack) = &mut *s;
707 let m = heads_per_kv * b;
712 qpanel.resize(m * hd, 0.0);
713 scores.resize(m * n, 0.0);
714 aopanel.resize(m * hd, 0.0);
715 for g in 0..self.num_kv_heads {
716 let kmat = &self.k[g];
717 let vmat = &self.v[g];
718 debug_assert_eq!(kmat.len(), n * hd);
719 for hl in 0..heads_per_kv {
720 let hh = g * heads_per_kv + hl;
721 for bi in 0..b {
722 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
723 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
724 }
725 }
726 if neon_gemm {
727 ktpack.resize(hd * n, 0.0);
728 for p in 0..n {
729 let row = &kmat[p * hd..(p + 1) * hd];
730 for (d, &v) in row.iter().enumerate() {
731 ktpack[d * n + p] = v;
732 }
733 }
734 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
737 let sp_s = SendPtr(scores.as_mut_ptr());
738 let kt = &*ktpack;
739 let run = |start: usize, end: usize| {
740 if end > start {
741 let a = unsafe {
743 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
744 };
745 let c = unsafe {
746 std::slice::from_raw_parts_mut(
747 sp_s.at(start * n),
748 (end - start) * n,
749 )
750 };
751 crate::qtensor::neon_gemm_rm(
752 end - start,
753 n,
754 hd,
755 scale,
756 a,
757 hd,
758 kt,
759 n,
760 false,
761 c,
762 n,
763 );
764 }
765 };
766 match pool {
767 Some(p) if m >= 64 => p.run_rows(m, &run),
768 _ => run(0, m),
769 }
770 } else {
771 crate::qtensor::sgemm_rm(
772 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
773 );
774 }
775 let sp = SendPtr(scores.as_mut_ptr());
777 let run = |start: usize, end: usize| {
778 for r in start..end {
779 let allowed = s0 + (r % b) + 1;
780 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
784 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
786 crate::attention::softmax_row(&mut row[lo..allowed]);
787 row[..lo].fill(0.0);
788 row[allowed..].fill(0.0);
789 }
790 };
791 match pool {
792 Some(p) if m >= 64 => p.run_rows(m, &run),
793 _ => run(0, m),
794 }
795 let ni = self.imp.len().min(n);
799 for r in 0..m {
800 let al = (s0 + (r % b) + 1).min(ni);
801 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
802 *dst += p;
803 }
804 }
805 if neon_gemm {
806 let sp_s = SendPtr(scores.as_mut_ptr());
807 let sp_o = SendPtr(aopanel.as_mut_ptr());
808 let run = |start: usize, end: usize| {
809 if end > start {
810 let a = unsafe {
812 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
813 };
814 let c = unsafe {
815 std::slice::from_raw_parts_mut(
816 sp_o.at(start * hd),
817 (end - start) * hd,
818 )
819 };
820 crate::qtensor::neon_gemm_rm(
821 end - start,
822 hd,
823 n,
824 1.0,
825 a,
826 n,
827 vmat,
828 hd,
829 false,
830 c,
831 hd,
832 );
833 }
834 };
835 match pool {
836 Some(p) if m >= 64 => p.run_rows(m, &run),
837 _ => run(0, m),
838 }
839 } else {
840 crate::qtensor::sgemm_rm(
841 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
842 );
843 }
844 for hl in 0..heads_per_kv {
845 let hh = g * heads_per_kv + hl;
846 for bi in 0..b {
847 out[bi * nh * hd + hh * hd..][..hd]
848 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
849 }
850 }
851 }
852 });
853 }
854
855 pub fn truncate_last(&mut self, n_drop: usize) {
857 let d = n_drop.min(self.seq_len);
858 for h in 0..self.num_kv_heads {
859 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
860 self.k[h].truncate(keep);
861 self.v[h].truncate(keep);
862 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
863 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
864 self.kq[h].truncate(keep_q);
865 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
866 self.vq[h].truncate(keep_vq);
867 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
868 self.ks[h].truncate(keep_ks);
869 let keep_vs = self.vs[h].len().saturating_sub(d);
870 self.vs[h].truncate(keep_vs);
871 }
872 self.imp.truncate(self.imp.len().saturating_sub(d));
873 self.seq_len -= d;
874 }
875
876 pub fn accumulate_imp(&mut self, probs: &[f32]) {
878 for (dst, &p) in self.imp.iter_mut().zip(probs) {
879 *dst += p;
880 }
881 }
882
883 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
885 &self.k[kv_head]
886 }
887
888 pub fn head_values(&self, kv_head: usize) -> &[f32] {
889 &self.v[kv_head]
890 }
891
892 pub fn head_len(&self, kv_head: usize) -> usize {
894 let ng = self.head_dim.div_ceil(KV_K_GROUP);
895 (self.k[kv_head].len() / self.head_dim)
896 .max(self.ks[kv_head].len() / ng)
897 .max(self.vs[kv_head].len())
898 }
899
900 pub fn clear(&mut self) {
902 for h in 0..self.num_kv_heads {
903 self.k[h].clear();
904 self.v[h].clear();
905 self.kq[h].clear();
906 self.ks[h].clear();
907 self.vq[h].clear();
908 self.vs[h].clear();
909 self.kcol[h].clear();
910 self.vcol[h].clear();
911 }
912 self.imp.clear();
913 self.linear_state.clear();
914 self.linear_scratch.clear();
915 self.o1 = None;
918 self.seq_len = 0;
919 }
920
921 pub fn memory_bytes(&self) -> usize {
923 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
924 + self.v.iter().map(Vec::len).sum::<usize>()
925 + self.ks.iter().map(Vec::len).sum::<usize>()
926 + self.vs.iter().map(Vec::len).sum::<usize>()
927 + self.kcol.iter().map(Vec::len).sum::<usize>()
928 + self.vcol.iter().map(Vec::len).sum::<usize>();
929 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
930 + self.vq.iter().map(Vec::len).sum::<usize>();
931 floats * std::mem::size_of::<f32>()
932 + bytes
933 + self.linear_state.len() * std::mem::size_of::<f32>()
937 + self.o1_memory_bytes()
940 }
941
942 fn evict(&mut self, keep_last: usize) {
944 if self.o1_sealed() || self.seq_len <= keep_last {
948 return;
949 }
950 let drop = self.seq_len - keep_last;
951 for h in 0..self.num_kv_heads {
952 let stored = self.head_len(h);
954 let d = drop.min(stored);
955 let hd = self.head_dim;
956 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
957 let n = n.min(v.len());
958 v.drain(..n);
959 }
960 drop_front(&mut self.k[h], d * hd);
961 drop_front(&mut self.v[h], d * hd);
962 drop_front(&mut self.kq[h], d * hd);
963 drop_front(&mut self.vq[h], d * hd);
964 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
965 drop_front(&mut self.vs[h], d);
966 }
967 let d = drop.min(self.imp.len());
968 self.imp.drain(..d);
969 self.seq_len = keep_last;
970 }
971
972 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
977 if self.o1_sealed() {
978 return; }
980 let stored = self.imp.len();
981 if stored <= keep_last {
982 return;
983 }
984 let sink_n = sink.min(keep_last);
987 let recent_n = recent.min(keep_last - sink_n);
988 let mut keep = vec![false; stored];
989 for k in keep.iter_mut().take(sink_n) {
990 *k = true;
991 }
992 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
993 *k = true;
994 }
995 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
996 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
998 order.sort_by(|&a, &b| {
999 self.imp[b]
1000 .partial_cmp(&self.imp[a])
1001 .unwrap_or(std::cmp::Ordering::Equal)
1002 });
1003 for i in order {
1004 if budget == 0 {
1005 break;
1006 }
1007 keep[i] = true;
1008 budget -= 1;
1009 }
1010
1011 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1012 let hd = self.head_dim;
1013 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1014 let mut out = Vec::with_capacity(kept.len() * step);
1015 for &i in kept {
1016 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1017 }
1018 out
1019 }
1020 for h in 0..self.num_kv_heads {
1025 if !self.k[h].is_empty() {
1026 self.k[h] = gather(&self.k[h], &kept, hd);
1027 }
1028 if !self.v[h].is_empty() {
1029 self.v[h] = gather(&self.v[h], &kept, hd);
1030 }
1031 if !self.kq[h].is_empty() {
1032 self.kq[h] = gather(&self.kq[h], &kept, hd);
1033 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1034 }
1035 if !self.vq[h].is_empty() {
1036 self.vq[h] = gather(&self.vq[h], &kept, hd);
1037 self.vs[h] = gather(&self.vs[h], &kept, 1);
1038 }
1039 }
1040 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1041 self.seq_len = kept.len();
1042 }
1043}
1044
1045#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1047pub enum EvictionPolicy {
1048 Recent,
1050 Born { sink: usize },
1052}
1053
1054#[derive(Debug)]
1056pub struct KvCache {
1057 pub layers: Vec<LayerKvCache>,
1058 pub max_seq_len: usize,
1059 pub policy: EvictionPolicy,
1060}
1061
1062impl KvCache {
1063 pub fn new(
1064 num_layers: usize,
1065 num_kv_heads: usize,
1066 head_dim: usize,
1067 max_seq_len: usize,
1068 ) -> Self {
1069 let layers = (0..num_layers)
1070 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1071 .collect();
1072 Self {
1073 layers,
1074 max_seq_len,
1075 policy: EvictionPolicy::Born { sink: 4 },
1076 }
1077 }
1078
1079 pub fn clear(&mut self) {
1080 for layer in &mut self.layers {
1081 layer.clear();
1082 }
1083 }
1084
1085 pub fn total_memory_bytes(&self) -> usize {
1086 self.layers.iter().map(|l| l.memory_bytes()).sum()
1087 }
1088
1089 pub fn seq_len(&self) -> usize {
1091 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1092 }
1093
1094 pub fn needs_eviction(&self) -> bool {
1095 self.seq_len() >= self.max_seq_len
1096 }
1097
1098 pub fn evict(&mut self, keep_last: usize) {
1100 match self.policy {
1101 EvictionPolicy::Recent => {
1102 for layer in &mut self.layers {
1103 layer.evict(keep_last);
1104 }
1105 }
1106 EvictionPolicy::Born { sink } => {
1107 let recent = (keep_last / 2).max(1);
1108 for layer in &mut self.layers {
1109 layer.evict_born(keep_last, sink, recent);
1110 }
1111 }
1112 }
1113 }
1114}
1115
1116#[cfg(test)]
1117mod tests {
1118 use super::*;
1119
1120 #[test]
1121 fn append_tracks_seq_len_and_layout() {
1122 let mut cache = LayerKvCache::new(4, 8);
1123 cache.mode = KvMode::F32;
1124 assert_eq!(cache.seq_len, 0);
1125
1126 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1127 let v = vec![2.0f32; 32];
1128 cache.append(&k, &v, &[true; 4]);
1129
1130 assert_eq!(cache.seq_len, 1);
1131 assert_eq!(cache.head_len(0), 1);
1132 assert_eq!(cache.head_keys(1), &k[8..16]);
1134 assert_eq!(cache.memory_bytes(), 256);
1135 }
1136
1137 #[test]
1138 fn dead_head_stores_nothing() {
1139 let mut cache = LayerKvCache::new(2, 4);
1140 cache.mode = KvMode::F32;
1141 let k = vec![1.0f32; 8];
1142 let v = vec![2.0f32; 8];
1143 cache.append(&k, &v, &[true, false]);
1144 cache.append(&k, &v, &[true, false]);
1145
1146 assert_eq!(cache.seq_len, 2);
1147 assert_eq!(cache.head_len(0), 2);
1148 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1149 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1150 }
1151
1152 #[test]
1153 fn eviction_keeps_recent() {
1154 let mut cache = KvCache::new(2, 4, 8, 10);
1155 cache.policy = EvictionPolicy::Recent;
1156 for l in &mut cache.layers {
1157 l.mode = KvMode::F32;
1158 }
1159 let k = vec![1.0f32; 32];
1160 let v = vec![2.0f32; 32];
1161 for _ in 0..8 {
1162 for layer in &mut cache.layers {
1163 layer.append(&k, &v, &[true; 4]);
1164 }
1165 }
1166 assert_eq!(cache.seq_len(), 8);
1167 assert!(!cache.needs_eviction());
1168
1169 cache.evict(4);
1170 assert_eq!(cache.seq_len(), 4);
1171 assert_eq!(cache.layers[0].head_len(0), 4);
1172 }
1173
1174 #[test]
1175 fn truncate_rolls_back_speculative_positions() {
1176 let mut cache = LayerKvCache::new(2, 4);
1177 cache.mode = KvMode::F32;
1178 for pos in 0..5 {
1179 let k = vec![pos as f32; 8];
1180 let v = vec![pos as f32; 8];
1181 cache.append(&k, &v, &[true; 2]);
1182 }
1183 cache.truncate_last(2);
1184 assert_eq!(cache.seq_len, 3);
1185 assert_eq!(cache.head_len(0), 3);
1186 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1187 }
1188
1189 #[test]
1193 fn q8_attend_matches_f32_within_grid() {
1194 let (heads, hd) = (2, 32);
1195 let mut f = LayerKvCache::new(heads, hd);
1196 f.mode = KvMode::F32;
1197 let mut q8 = LayerKvCache::new(heads, hd);
1198 q8.mode = KvMode::Q8 { k: true, v: true };
1199
1200 let synth = |p: usize, salt: usize| -> Vec<f32> {
1201 (0..heads * hd)
1202 .map(|i| {
1203 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1204 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1206 })
1207 .collect()
1208 };
1209 for p in 0..100 {
1210 let k = synth(p, 1);
1211 let v = synth(p, 2);
1212 f.append(&k, &v, &[true; 2]);
1213 q8.append(&k, &v, &[true; 2]);
1214 }
1215 let q: Vec<f32> = (0..hd)
1216 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1217 .collect();
1218 for g in 0..heads {
1219 let (of, pf) = f.attend(&q, g);
1220 let (o8, p8) = q8.attend(&q, g);
1221 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1222 for d in 0..hd {
1223 assert!(
1224 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1225 "g{g} d{d}: f32 {} vs q8 {}",
1226 of[d],
1227 o8[d]
1228 );
1229 }
1230 for p in 0..100 {
1231 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1232 }
1233 }
1234 q8.truncate_last(30);
1236 assert_eq!(q8.head_len(0), 70);
1237 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1238 q8.accumulate_imp(&imp);
1239 q8.evict_born(20, 2, 8);
1240 assert_eq!(q8.head_len(0), 20);
1241 let (o, _) = q8.attend(&q, 0);
1242 assert!(o.iter().all(|x| x.is_finite()));
1243 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1245 }
1246
1247 #[test]
1250 fn attend_group_equals_per_head_attend_bitexact() {
1251 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1253 let mut c = LayerKvCache::new(kv_heads, hd);
1254 c.mode = mode;
1255 for p in 0..70 {
1256 let k: Vec<f32> = (0..kv_heads * hd)
1257 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1258 .collect();
1259 let v: Vec<f32> = (0..kv_heads * hd)
1260 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1261 .collect();
1262 c.append(&k, &v, &[true; 2]);
1263 }
1264 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1265 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1266 .collect();
1267 for g in 0..kv_heads {
1268 let span = g * hpk * hd..(g + 1) * hpk * hd;
1269 let mut out = vec![0f32; hpk * hd];
1270 let mut imp = vec![0f32; 70];
1271 c.attend_group(
1272 &q[span.clone()],
1273 g,
1274 &mut out,
1275 &mut imp,
1276 1.0 / (hd as f32).sqrt(),
1277 0,
1278 );
1279 let mut imp_ref = vec![0f32; 70];
1280 for h in 0..hpk {
1281 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1282 let (o, probs) = c.attend(qh, g);
1283 assert_eq!(
1284 &out[h * hd..(h + 1) * hd],
1285 &o[..],
1286 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1287 );
1288 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1289 *dst += p;
1290 }
1291 }
1292 assert_eq!(imp, imp_ref, "mode {mode:?} g{g}: Born mass must match");
1293 }
1294 }
1295 }
1296
1297 #[test]
1301 fn born_eviction_mixed_modes_stay_consistent() {
1302 for (mk, mv) in [(false, true), (true, false)] {
1303 let mut c = LayerKvCache::new(1, 4);
1304 c.mode = KvMode::Q8 { k: mk, v: mv };
1305 for p in 0..80 {
1306 let k = vec![p as f32 * 0.01; 4];
1307 let v = vec![p as f32; 4];
1308 c.append(&k, &v, &[true]);
1309 }
1310 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1311 c.accumulate_imp(&imp);
1312 let before = c.memory_bytes();
1313 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1315 assert!(
1316 c.memory_bytes() < before / 2,
1317 "memory must shrink (k={mk} v={mv})"
1318 );
1319 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1322 assert!(
1323 out[0] > 30.0,
1324 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1325 out[0]
1326 );
1327 }
1328 }
1329
1330 #[test]
1331 fn born_eviction_keeps_high_mass_position() {
1332 let mut cache = KvCache::new(1, 1, 2, 16);
1333 cache.policy = EvictionPolicy::Born { sink: 1 };
1334 for l in &mut cache.layers {
1335 l.mode = KvMode::F32;
1336 }
1337 let layer = &mut cache.layers[0];
1338 for pos in 0..8 {
1341 let k = vec![pos as f32; 2];
1342 let v = vec![pos as f32 + 100.0; 2];
1343 layer.append(&k, &v, &[true]);
1344 }
1345 let mut imp = vec![0.05f32; 8];
1347 imp[3] = 5.0;
1348 layer.accumulate_imp(&imp);
1349
1350 cache.evict(4); let layer = &cache.layers[0];
1352 assert_eq!(layer.seq_len, 4);
1353 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1354 assert_eq!(
1355 kept_keys,
1356 vec![0.0, 3.0, 6.0, 7.0],
1357 "kept = sink(0) + Born-top(3) + recent(6,7)"
1358 );
1359 assert_eq!(layer.head_len(0), 4);
1361 }
1362}