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 softcap: f32,
516 ) {
517 let hd = self.head_dim;
518 let nheads = q_group.len() / hd;
519 debug_assert_eq!(out.len(), nheads * hd);
520 let stored = if self.mode == KvMode::F32 {
521 self.k[kv_head].len() / hd
522 } else {
523 self.head_len(kv_head)
524 };
525 if stored == 0 {
526 out.fill(0.0);
527 return;
528 }
529 let first = first.min(stored.saturating_sub(1));
530
531 thread_local! {
532 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
534 const { std::cell::RefCell::new(Vec::new()) };
535 static GQA_QC: std::cell::RefCell<Vec<f32>> =
537 const { std::cell::RefCell::new(Vec::new()) };
538 }
539
540 GQA_SCORES.with(|sc| {
541 let mut scores = sc.borrow_mut();
542 if first > 0 {
543 scores.clear();
546 scores.resize(nheads * stored, f32::NEG_INFINITY);
547 } else {
548 scores.resize(nheads * stored, 0.0);
549 }
550
551 if self.mode.quant_k() {
553 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
554 let kcol = &self.kcol[kv_head];
555 let ng = hd.div_ceil(KV_K_GROUP);
556 GQA_QC.with(|qc| {
557 let mut qcb = qc.borrow_mut();
558 qcb.resize(nheads * hd, 0.0);
559 for h in 0..nheads {
560 for d in 0..hd {
561 let qv = q_group[h * hd + d];
562 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
563 }
564 }
565 for p in first..stored {
566 let row = &kq[p * hd..(p + 1) * hd];
567 let row_u8 = unsafe {
570 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
571 };
572 for h in 0..nheads {
573 let qch = &qcb[h * hd..(h + 1) * hd];
574 let mut dot = 0.0f32;
575 for g in 0..ng {
576 let g0 = g * KV_K_GROUP;
577 let g1 = (g0 + KV_K_GROUP).min(hd);
578 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
579 * ks[p * ng + g];
580 }
581 scores[h * stored + p] = dot * scale;
582 }
583 }
584 });
585 } else {
586 let k = &self.k[kv_head];
587 for p in first..stored {
588 let row = &k[p * hd..(p + 1) * hd];
589 for h in 0..nheads {
590 scores[h * stored + p] =
591 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
592 }
593 }
594 }
595
596 if softcap > 0.0 {
601 for v in scores.iter_mut() {
602 if v.is_finite() {
603 *v = softcap * (*v / softcap).tanh();
604 }
605 }
606 }
607
608 for h in 0..nheads {
610 let s = &mut scores[h * stored..(h + 1) * stored];
611 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
612 let mut sum = 0.0f32;
613 for v in s.iter_mut() {
614 *v = (*v - max_score).exp();
615 sum += *v;
616 }
617 if sum > 0.0 {
618 for v in s.iter_mut() {
619 *v /= sum;
620 }
621 }
622 }
623
624 out.fill(0.0);
626 if self.mode.quant_v() {
627 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
628 for p in first..stored {
629 let row = &vq[p * hd..(p + 1) * hd];
630 for h in 0..nheads {
631 let w = scores[h * stored + p] * vs[p];
632 if w.abs() < 1e-12 {
633 continue;
634 }
635 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
636 }
637 }
638 let vcol = &self.vcol[kv_head];
639 if !vcol.is_empty() {
640 for h in 0..nheads {
641 for d in 0..hd {
642 out[h * hd + d] *= vcol[d];
643 }
644 }
645 }
646 } else {
647 let v = &self.v[kv_head];
648 for p in first..stored {
649 let row = &v[p * hd..(p + 1) * hd];
650 for h in 0..nheads {
651 let w = scores[h * stored + p];
652 if w.abs() < 1e-12 {
653 continue;
654 }
655 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
656 }
657 }
658 }
659
660 let n = imp_acc.len().min(stored);
663 for h in 0..nheads {
664 let s = &scores[h * stored..(h + 1) * stored];
665 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
666 *dst += p;
667 }
668 }
669 });
670 }
671
672 #[cfg(target_arch = "aarch64")]
680 #[allow(clippy::too_many_arguments)]
681 pub fn attend_chunk(
682 &mut self,
683 q_all: &[f32],
684 b: usize,
685 s0: usize,
686 nh: usize,
687 heads_per_kv: usize,
688 hd: usize,
689 out: &mut [f32],
690 pool: Option<&crate::pool::Pool>,
691 scale: f32,
692 window: Option<usize>,
693 ) {
694 let n = s0 + b;
695 struct SendPtr(*mut f32);
696 unsafe impl Send for SendPtr {}
697 unsafe impl Sync for SendPtr {}
698 impl SendPtr {
699 fn at(&self, i: usize) -> *mut f32 {
700 unsafe { self.0.add(i) }
703 }
704 }
705 thread_local! {
706 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
707 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
708 }
709 let neon_gemm = cfg!(not(target_os = "macos"))
714 || std::env::var("CMF_FORCE_NEON_GEMM")
715 .map(|v| v == "1")
716 .unwrap_or(false);
717 SCRATCH.with(|s| {
718 let mut s = s.borrow_mut();
719 let (qpanel, scores, aopanel, ktpack) = &mut *s;
720 let m = heads_per_kv * b;
725 qpanel.resize(m * hd, 0.0);
726 scores.resize(m * n, 0.0);
727 aopanel.resize(m * hd, 0.0);
728 for g in 0..self.num_kv_heads {
729 let kmat = &self.k[g];
730 let vmat = &self.v[g];
731 debug_assert_eq!(kmat.len(), n * hd);
732 for hl in 0..heads_per_kv {
733 let hh = g * heads_per_kv + hl;
734 for bi in 0..b {
735 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
736 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
737 }
738 }
739 if neon_gemm {
740 ktpack.resize(hd * n, 0.0);
741 for p in 0..n {
742 let row = &kmat[p * hd..(p + 1) * hd];
743 for (d, &v) in row.iter().enumerate() {
744 ktpack[d * n + p] = v;
745 }
746 }
747 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
750 let sp_s = SendPtr(scores.as_mut_ptr());
751 let kt = &*ktpack;
752 let run = |start: usize, end: usize| {
753 if end > start {
754 let a = unsafe {
756 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
757 };
758 let c = unsafe {
759 std::slice::from_raw_parts_mut(
760 sp_s.at(start * n),
761 (end - start) * n,
762 )
763 };
764 crate::qtensor::neon_gemm_rm(
765 end - start,
766 n,
767 hd,
768 scale,
769 a,
770 hd,
771 kt,
772 n,
773 false,
774 c,
775 n,
776 );
777 }
778 };
779 match pool {
780 Some(p) if m >= 64 => p.run_rows(m, &run),
781 _ => run(0, m),
782 }
783 } else {
784 crate::qtensor::sgemm_rm(
785 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
786 );
787 }
788 let sp = SendPtr(scores.as_mut_ptr());
790 let run = |start: usize, end: usize| {
791 for r in start..end {
792 let allowed = s0 + (r % b) + 1;
793 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
797 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
799 crate::attention::softmax_row(&mut row[lo..allowed]);
800 row[..lo].fill(0.0);
801 row[allowed..].fill(0.0);
802 }
803 };
804 match pool {
805 Some(p) if m >= 64 => p.run_rows(m, &run),
806 _ => run(0, m),
807 }
808 let ni = self.imp.len().min(n);
812 for r in 0..m {
813 let al = (s0 + (r % b) + 1).min(ni);
814 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
815 *dst += p;
816 }
817 }
818 if neon_gemm {
819 let sp_s = SendPtr(scores.as_mut_ptr());
820 let sp_o = SendPtr(aopanel.as_mut_ptr());
821 let run = |start: usize, end: usize| {
822 if end > start {
823 let a = unsafe {
825 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
826 };
827 let c = unsafe {
828 std::slice::from_raw_parts_mut(
829 sp_o.at(start * hd),
830 (end - start) * hd,
831 )
832 };
833 crate::qtensor::neon_gemm_rm(
834 end - start,
835 hd,
836 n,
837 1.0,
838 a,
839 n,
840 vmat,
841 hd,
842 false,
843 c,
844 hd,
845 );
846 }
847 };
848 match pool {
849 Some(p) if m >= 64 => p.run_rows(m, &run),
850 _ => run(0, m),
851 }
852 } else {
853 crate::qtensor::sgemm_rm(
854 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
855 );
856 }
857 for hl in 0..heads_per_kv {
858 let hh = g * heads_per_kv + hl;
859 for bi in 0..b {
860 out[bi * nh * hd + hh * hd..][..hd]
861 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
862 }
863 }
864 }
865 });
866 }
867
868 pub fn truncate_last(&mut self, n_drop: usize) {
870 let d = n_drop.min(self.seq_len);
871 for h in 0..self.num_kv_heads {
872 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
873 self.k[h].truncate(keep);
874 self.v[h].truncate(keep);
875 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
876 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
877 self.kq[h].truncate(keep_q);
878 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
879 self.vq[h].truncate(keep_vq);
880 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
881 self.ks[h].truncate(keep_ks);
882 let keep_vs = self.vs[h].len().saturating_sub(d);
883 self.vs[h].truncate(keep_vs);
884 }
885 self.imp.truncate(self.imp.len().saturating_sub(d));
886 self.seq_len -= d;
887 }
888
889 pub fn accumulate_imp(&mut self, probs: &[f32]) {
891 for (dst, &p) in self.imp.iter_mut().zip(probs) {
892 *dst += p;
893 }
894 }
895
896 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
898 &self.k[kv_head]
899 }
900
901 pub fn head_values(&self, kv_head: usize) -> &[f32] {
902 &self.v[kv_head]
903 }
904
905 pub fn head_len(&self, kv_head: usize) -> usize {
907 let ng = self.head_dim.div_ceil(KV_K_GROUP);
908 (self.k[kv_head].len() / self.head_dim)
909 .max(self.ks[kv_head].len() / ng)
910 .max(self.vs[kv_head].len())
911 }
912
913 pub fn clear(&mut self) {
915 for h in 0..self.num_kv_heads {
916 self.k[h].clear();
917 self.v[h].clear();
918 self.kq[h].clear();
919 self.ks[h].clear();
920 self.vq[h].clear();
921 self.vs[h].clear();
922 self.kcol[h].clear();
923 self.vcol[h].clear();
924 }
925 self.imp.clear();
926 self.linear_state.clear();
927 self.linear_scratch.clear();
928 self.o1 = None;
931 self.seq_len = 0;
932 }
933
934 pub fn memory_bytes(&self) -> usize {
936 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
937 + self.v.iter().map(Vec::len).sum::<usize>()
938 + self.ks.iter().map(Vec::len).sum::<usize>()
939 + self.vs.iter().map(Vec::len).sum::<usize>()
940 + self.kcol.iter().map(Vec::len).sum::<usize>()
941 + self.vcol.iter().map(Vec::len).sum::<usize>();
942 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
943 + self.vq.iter().map(Vec::len).sum::<usize>();
944 floats * std::mem::size_of::<f32>()
945 + bytes
946 + self.linear_state.len() * std::mem::size_of::<f32>()
950 + self.o1_memory_bytes()
953 }
954
955 fn evict(&mut self, keep_last: usize) {
957 if self.o1_sealed() || self.seq_len <= keep_last {
961 return;
962 }
963 let drop = self.seq_len - keep_last;
964 for h in 0..self.num_kv_heads {
965 let stored = self.head_len(h);
967 let d = drop.min(stored);
968 let hd = self.head_dim;
969 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
970 let n = n.min(v.len());
971 v.drain(..n);
972 }
973 drop_front(&mut self.k[h], d * hd);
974 drop_front(&mut self.v[h], d * hd);
975 drop_front(&mut self.kq[h], d * hd);
976 drop_front(&mut self.vq[h], d * hd);
977 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
978 drop_front(&mut self.vs[h], d);
979 }
980 let d = drop.min(self.imp.len());
981 self.imp.drain(..d);
982 self.seq_len = keep_last;
983 }
984
985 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
990 if self.o1_sealed() {
991 return; }
993 let stored = self.imp.len();
994 if stored <= keep_last {
995 return;
996 }
997 let sink_n = sink.min(keep_last);
1000 let recent_n = recent.min(keep_last - sink_n);
1001 let mut keep = vec![false; stored];
1002 for k in keep.iter_mut().take(sink_n) {
1003 *k = true;
1004 }
1005 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1006 *k = true;
1007 }
1008 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1009 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1011 order.sort_by(|&a, &b| {
1012 self.imp[b]
1013 .partial_cmp(&self.imp[a])
1014 .unwrap_or(std::cmp::Ordering::Equal)
1015 });
1016 for i in order {
1017 if budget == 0 {
1018 break;
1019 }
1020 keep[i] = true;
1021 budget -= 1;
1022 }
1023
1024 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1025 let hd = self.head_dim;
1026 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1027 let mut out = Vec::with_capacity(kept.len() * step);
1028 for &i in kept {
1029 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1030 }
1031 out
1032 }
1033 for h in 0..self.num_kv_heads {
1038 if !self.k[h].is_empty() {
1039 self.k[h] = gather(&self.k[h], &kept, hd);
1040 }
1041 if !self.v[h].is_empty() {
1042 self.v[h] = gather(&self.v[h], &kept, hd);
1043 }
1044 if !self.kq[h].is_empty() {
1045 self.kq[h] = gather(&self.kq[h], &kept, hd);
1046 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1047 }
1048 if !self.vq[h].is_empty() {
1049 self.vq[h] = gather(&self.vq[h], &kept, hd);
1050 self.vs[h] = gather(&self.vs[h], &kept, 1);
1051 }
1052 }
1053 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1054 self.seq_len = kept.len();
1055 }
1056}
1057
1058#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1060pub enum EvictionPolicy {
1061 Recent,
1063 Born { sink: usize },
1065}
1066
1067#[derive(Debug)]
1069pub struct KvCache {
1070 pub layers: Vec<LayerKvCache>,
1071 pub max_seq_len: usize,
1072 pub policy: EvictionPolicy,
1073}
1074
1075impl KvCache {
1076 pub fn new(
1077 num_layers: usize,
1078 num_kv_heads: usize,
1079 head_dim: usize,
1080 max_seq_len: usize,
1081 ) -> Self {
1082 let layers = (0..num_layers)
1083 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1084 .collect();
1085 Self {
1086 layers,
1087 max_seq_len,
1088 policy: EvictionPolicy::Born { sink: 4 },
1089 }
1090 }
1091
1092 pub fn clear(&mut self) {
1093 for layer in &mut self.layers {
1094 layer.clear();
1095 }
1096 }
1097
1098 pub fn total_memory_bytes(&self) -> usize {
1099 self.layers.iter().map(|l| l.memory_bytes()).sum()
1100 }
1101
1102 pub fn seq_len(&self) -> usize {
1104 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1105 }
1106
1107 pub fn needs_eviction(&self) -> bool {
1108 self.seq_len() >= self.max_seq_len
1109 }
1110
1111 pub fn evict(&mut self, keep_last: usize) {
1113 match self.policy {
1114 EvictionPolicy::Recent => {
1115 for layer in &mut self.layers {
1116 layer.evict(keep_last);
1117 }
1118 }
1119 EvictionPolicy::Born { sink } => {
1120 let recent = (keep_last / 2).max(1);
1121 for layer in &mut self.layers {
1122 layer.evict_born(keep_last, sink, recent);
1123 }
1124 }
1125 }
1126 }
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131 use super::*;
1132
1133 #[test]
1134 fn append_tracks_seq_len_and_layout() {
1135 let mut cache = LayerKvCache::new(4, 8);
1136 cache.mode = KvMode::F32;
1137 assert_eq!(cache.seq_len, 0);
1138
1139 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1140 let v = vec![2.0f32; 32];
1141 cache.append(&k, &v, &[true; 4]);
1142
1143 assert_eq!(cache.seq_len, 1);
1144 assert_eq!(cache.head_len(0), 1);
1145 assert_eq!(cache.head_keys(1), &k[8..16]);
1147 assert_eq!(cache.memory_bytes(), 256);
1148 }
1149
1150 #[test]
1151 fn dead_head_stores_nothing() {
1152 let mut cache = LayerKvCache::new(2, 4);
1153 cache.mode = KvMode::F32;
1154 let k = vec![1.0f32; 8];
1155 let v = vec![2.0f32; 8];
1156 cache.append(&k, &v, &[true, false]);
1157 cache.append(&k, &v, &[true, false]);
1158
1159 assert_eq!(cache.seq_len, 2);
1160 assert_eq!(cache.head_len(0), 2);
1161 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1162 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1163 }
1164
1165 #[test]
1166 fn eviction_keeps_recent() {
1167 let mut cache = KvCache::new(2, 4, 8, 10);
1168 cache.policy = EvictionPolicy::Recent;
1169 for l in &mut cache.layers {
1170 l.mode = KvMode::F32;
1171 }
1172 let k = vec![1.0f32; 32];
1173 let v = vec![2.0f32; 32];
1174 for _ in 0..8 {
1175 for layer in &mut cache.layers {
1176 layer.append(&k, &v, &[true; 4]);
1177 }
1178 }
1179 assert_eq!(cache.seq_len(), 8);
1180 assert!(!cache.needs_eviction());
1181
1182 cache.evict(4);
1183 assert_eq!(cache.seq_len(), 4);
1184 assert_eq!(cache.layers[0].head_len(0), 4);
1185 }
1186
1187 #[test]
1188 fn truncate_rolls_back_speculative_positions() {
1189 let mut cache = LayerKvCache::new(2, 4);
1190 cache.mode = KvMode::F32;
1191 for pos in 0..5 {
1192 let k = vec![pos as f32; 8];
1193 let v = vec![pos as f32; 8];
1194 cache.append(&k, &v, &[true; 2]);
1195 }
1196 cache.truncate_last(2);
1197 assert_eq!(cache.seq_len, 3);
1198 assert_eq!(cache.head_len(0), 3);
1199 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1200 }
1201
1202 #[test]
1206 fn q8_attend_matches_f32_within_grid() {
1207 let (heads, hd) = (2, 32);
1208 let mut f = LayerKvCache::new(heads, hd);
1209 f.mode = KvMode::F32;
1210 let mut q8 = LayerKvCache::new(heads, hd);
1211 q8.mode = KvMode::Q8 { k: true, v: true };
1212
1213 let synth = |p: usize, salt: usize| -> Vec<f32> {
1214 (0..heads * hd)
1215 .map(|i| {
1216 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1217 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1219 })
1220 .collect()
1221 };
1222 for p in 0..100 {
1223 let k = synth(p, 1);
1224 let v = synth(p, 2);
1225 f.append(&k, &v, &[true; 2]);
1226 q8.append(&k, &v, &[true; 2]);
1227 }
1228 let q: Vec<f32> = (0..hd)
1229 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1230 .collect();
1231 for g in 0..heads {
1232 let (of, pf) = f.attend(&q, g);
1233 let (o8, p8) = q8.attend(&q, g);
1234 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1235 for d in 0..hd {
1236 assert!(
1237 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1238 "g{g} d{d}: f32 {} vs q8 {}",
1239 of[d],
1240 o8[d]
1241 );
1242 }
1243 for p in 0..100 {
1244 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1245 }
1246 }
1247 q8.truncate_last(30);
1249 assert_eq!(q8.head_len(0), 70);
1250 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1251 q8.accumulate_imp(&imp);
1252 q8.evict_born(20, 2, 8);
1253 assert_eq!(q8.head_len(0), 20);
1254 let (o, _) = q8.attend(&q, 0);
1255 assert!(o.iter().all(|x| x.is_finite()));
1256 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1258 }
1259
1260 #[test]
1263 fn attend_group_equals_per_head_attend_bitexact() {
1264 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1266 let mut c = LayerKvCache::new(kv_heads, hd);
1267 c.mode = mode;
1268 for p in 0..70 {
1269 let k: Vec<f32> = (0..kv_heads * hd)
1270 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1271 .collect();
1272 let v: Vec<f32> = (0..kv_heads * hd)
1273 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1274 .collect();
1275 c.append(&k, &v, &[true; 2]);
1276 }
1277 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1278 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1279 .collect();
1280 for g in 0..kv_heads {
1281 let span = g * hpk * hd..(g + 1) * hpk * hd;
1282 let mut out = vec![0f32; hpk * hd];
1283 let mut imp = vec![0f32; 70];
1284 c.attend_group(
1285 &q[span.clone()],
1286 g,
1287 &mut out,
1288 &mut imp,
1289 1.0 / (hd as f32).sqrt(),
1290 0,
1291 0.0,
1292 );
1293 let mut imp_ref = vec![0f32; 70];
1294 for h in 0..hpk {
1295 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1296 let (o, probs) = c.attend(qh, g);
1297 assert_eq!(
1298 &out[h * hd..(h + 1) * hd],
1299 &o[..],
1300 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1301 );
1302 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1303 *dst += p;
1304 }
1305 }
1306 assert_eq!(imp, imp_ref, "mode {mode:?} g{g}: Born mass must match");
1307 }
1308 }
1309 }
1310
1311 #[test]
1315 fn born_eviction_mixed_modes_stay_consistent() {
1316 for (mk, mv) in [(false, true), (true, false)] {
1317 let mut c = LayerKvCache::new(1, 4);
1318 c.mode = KvMode::Q8 { k: mk, v: mv };
1319 for p in 0..80 {
1320 let k = vec![p as f32 * 0.01; 4];
1321 let v = vec![p as f32; 4];
1322 c.append(&k, &v, &[true]);
1323 }
1324 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1325 c.accumulate_imp(&imp);
1326 let before = c.memory_bytes();
1327 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1329 assert!(
1330 c.memory_bytes() < before / 2,
1331 "memory must shrink (k={mk} v={mv})"
1332 );
1333 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1336 assert!(
1337 out[0] > 30.0,
1338 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1339 out[0]
1340 );
1341 }
1342 }
1343
1344 #[test]
1345 fn born_eviction_keeps_high_mass_position() {
1346 let mut cache = KvCache::new(1, 1, 2, 16);
1347 cache.policy = EvictionPolicy::Born { sink: 1 };
1348 for l in &mut cache.layers {
1349 l.mode = KvMode::F32;
1350 }
1351 let layer = &mut cache.layers[0];
1352 for pos in 0..8 {
1355 let k = vec![pos as f32; 2];
1356 let v = vec![pos as f32 + 100.0; 2];
1357 layer.append(&k, &v, &[true]);
1358 }
1359 let mut imp = vec![0.05f32; 8];
1361 imp[3] = 5.0;
1362 layer.accumulate_imp(&imp);
1363
1364 cache.evict(4); let layer = &cache.layers[0];
1366 assert_eq!(layer.seq_len, 4);
1367 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1368 assert_eq!(
1369 kept_keys,
1370 vec![0.0, 3.0, 6.0, 7.0],
1371 "kept = sink(0) + Born-top(3) + recent(6,7)"
1372 );
1373 assert_eq!(layer.head_len(0), 4);
1375 }
1376}