1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum KvMode {
14 F32,
15 Q8 { k: bool, v: bool },
17}
18
19impl KvMode {
20 pub fn from_env() -> Self {
21 match std::env::var("CMF_KV").as_deref() {
22 Ok("q8") | Ok("q8_2f") => KvMode::Q8 { k: true, v: true },
23 Ok("q8k") => KvMode::Q8 { k: true, v: false },
24 Ok("q8v") => KvMode::Q8 { k: false, v: true },
25 _ => KvMode::F32,
26 }
27 }
28
29 fn quant_k(self) -> bool {
30 matches!(self, KvMode::Q8 { k: true, .. })
31 }
32
33 fn quant_v(self) -> bool {
34 matches!(self, KvMode::Q8 { v: true, .. })
35 }
36}
37
38const KV_COL_WARMUP: usize = 64;
41
42const KV_K_GROUP: usize = 32;
47
48#[derive(Debug, Clone)]
59pub enum O1State {
60 Collecting {
61 m: usize,
62 w: usize,
63 sink: usize,
64 rect: crate::nystrom::O1Rect,
65 q_buf: Vec<f32>,
67 },
68 Sealed { groups: Vec<crate::nystrom::NystromState> },
74}
75
76#[derive(Debug, Clone)]
78pub struct LayerKvCache {
79 pub mode: KvMode,
80 k: Vec<Vec<f32>>,
82 v: Vec<Vec<f32>>,
84 kq: Vec<Vec<i8>>,
86 ks: Vec<Vec<f32>>,
87 vq: Vec<Vec<i8>>,
88 vs: Vec<Vec<f32>>,
89 kcol: Vec<Vec<f32>>,
91 vcol: Vec<Vec<f32>>,
92 imp: Vec<f32>,
95 pub seq_len: usize,
97 pub num_kv_heads: usize,
98 pub head_dim: usize,
99 pub linear_state: Vec<f32>,
101 pub linear_scratch: Vec<f32>,
103 pub o1: Option<O1State>,
105}
106
107impl LayerKvCache {
108 pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
109 Self {
110 mode: KvMode::from_env(),
111 k: vec![Vec::new(); num_kv_heads],
112 v: vec![Vec::new(); num_kv_heads],
113 kq: vec![Vec::new(); num_kv_heads],
114 ks: vec![Vec::new(); num_kv_heads],
115 vq: vec![Vec::new(); num_kv_heads],
116 vs: vec![Vec::new(); num_kv_heads],
117 kcol: vec![Vec::new(); num_kv_heads],
118 vcol: vec![Vec::new(); num_kv_heads],
119 imp: Vec::new(),
120 seq_len: 0,
121 num_kv_heads,
122 head_dim,
123 linear_state: Vec::new(),
124 linear_scratch: Vec::new(),
125 o1: None,
126 }
127 }
128
129 pub fn k_heads(&self) -> &[Vec<f32>] {
131 &self.k
132 }
133 pub fn v_heads(&self) -> &[Vec<f32>] {
135 &self.v
136 }
137
138 pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
142 self.o1 = Some(O1State::Collecting { m, w, sink, rect, q_buf: Vec::new() });
143 }
144
145 pub fn o1_push_q(&mut self, q_all: &[f32]) {
150 if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
151 q_buf.extend_from_slice(q_all);
152 }
153 }
154
155 pub fn o1_sealed(&self) -> bool {
156 matches!(self.o1, Some(O1State::Sealed { .. }))
157 }
158
159 pub fn o1_seal(&mut self, num_heads: usize) -> bool {
165 if !matches!(self.o1, Some(O1State::Collecting { .. })) {
168 return self.o1_sealed();
169 }
170 let Some(O1State::Collecting { m, w, sink, rect, q_buf }) = self.o1.take() else {
171 unreachable!("checked above");
172 };
173 let (hd, t) = (self.head_dim, self.seq_len);
174 let nkv = self.num_kv_heads.max(1);
175 let hpk = num_heads / nkv;
176 let ok = t > 0
177 && self.mode == KvMode::F32
178 && q_buf.len() == t * num_heads * hd
179 && hpk * nkv == num_heads
180 && (0..self.num_kv_heads).all(|g| self.head_len(g) == t);
181 if !ok {
182 tracing::warn!(
183 "o1: cannot seal (needs f32 KV mode, dense heads, full query \
184 trace, num_heads divisible by num_kv_heads) — layer keeps \
185 exact attention"
186 );
187 return false;
188 }
189 let mut groups = Vec::with_capacity(self.num_kv_heads);
190 let mut qh = vec![0.0f32; hpk * t * hd];
193 for g in 0..self.num_kv_heads {
194 for hh in 0..hpk {
195 let h = g * hpk + hh;
196 for p in 0..t {
197 let src = (p * num_heads + h) * hd;
198 let dst = (hh * t + p) * hd;
199 qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
200 }
201 }
202 let qs: Vec<&[f32]> = (0..hpk).map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd]).collect();
203 let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
204 st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
205 groups.push(st);
206 }
207 for h in 0..self.num_kv_heads {
210 self.k[h] = Vec::new();
211 self.v[h] = Vec::new();
212 }
213 self.imp = Vec::new();
214 self.o1 = Some(O1State::Sealed { groups });
215 true
216 }
217
218 pub fn o1_step(
224 &mut self,
225 q_all: &[f32],
226 k_new: &[f32],
227 v_new: &[f32],
228 num_heads: usize,
229 ) -> Vec<f32> {
230 let hd = self.head_dim;
231 let hpk = num_heads / self.num_kv_heads.max(1);
232 let mut out = vec![0.0f32; num_heads * hd];
233 let Some(O1State::Sealed { groups }) = &mut self.o1 else {
234 debug_assert!(false, "o1_step on an unsealed layer");
235 return out;
236 };
237 for (g, st) in groups.iter_mut().enumerate() {
238 let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
239 st.step_group(
240 &q_all[lo..hi],
241 &k_new[g * hd..(g + 1) * hd],
242 &v_new[g * hd..(g + 1) * hd],
243 &mut out[lo..hi],
244 );
245 }
246 self.seq_len += 1;
249 out
250 }
251
252 pub fn o1_memory_bytes(&self) -> usize {
255 match &self.o1 {
256 Some(O1State::Collecting { q_buf, .. }) => {
257 q_buf.len() * std::mem::size_of::<f32>()
258 }
259 Some(O1State::Sealed { groups }) => {
260 groups.iter().map(|s| s.memory_bytes()).sum()
261 }
262 None => 0,
263 }
264 }
265
266 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>,
269 group: usize) {
270 let mut resid = vec![0.0f32; row.len()];
271 for (d, &x) in row.iter().enumerate() {
272 resid[d] = if col.is_empty() { x } else { x / col[d] };
273 }
274 for g0 in (0..row.len()).step_by(group) {
275 let g1 = (g0 + group).min(row.len());
276 let mut absmax = 0.0f32;
277 for &r in &resid[g0..g1] {
278 absmax = absmax.max(r.abs());
279 }
280 let s = (absmax / 127.0).max(1e-12);
281 sc.push(s);
282 for &r in &resid[g0..g1] {
283 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
284 }
285 }
286 }
287
288 fn freeze_cols(&mut self) {
291 let hd = self.head_dim;
292 let ngk = hd.div_ceil(KV_K_GROUP);
293 for h in 0..self.num_kv_heads {
294 for (qv, sv, colv, group) in [
295 (&mut self.kq[h], &mut self.ks[h], &mut self.kcol[h], KV_K_GROUP),
296 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
297 ] {
298 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
300 if n == 0 {
301 continue;
302 }
303 let mut rows = vec![0.0f32; n * hd];
305 for p in 0..n {
306 for d in 0..hd {
307 rows[p * hd + d] =
308 qv[p * hd + d] as f32 * sv[p * spp + d / group];
309 }
310 }
311 let mut col = vec![0.0f32; hd];
312 for p in 0..n {
313 for d in 0..hd {
314 col[d] += rows[p * hd + d] * rows[p * hd + d];
315 }
316 }
317 for c in col.iter_mut() {
318 *c = (*c / n as f32).sqrt().max(1e-6);
319 }
320 qv.clear();
321 sv.clear();
322 for p in 0..n {
323 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
324 }
325 *colv = col;
326 }
327 }
328 }
329
330 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
334 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
335 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
336 if matches!(self.mode, KvMode::Q8 { .. })
342 && self.seq_len >= KV_COL_WARMUP
343 && self.kcol.iter().all(Vec::is_empty)
344 && self.vcol.iter().all(Vec::is_empty)
345 {
346 self.freeze_cols();
347 }
348 for h in 0..self.num_kv_heads {
349 if !alive.get(h).copied().unwrap_or(true) {
350 continue;
351 }
352 let s = h * self.head_dim;
353 if self.mode.quant_k() {
354 Self::quant_row(&k_new[s..s + self.head_dim],
355 &self.kcol[h], &mut self.kq[h], &mut self.ks[h],
356 KV_K_GROUP);
357 } else {
358 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
359 }
360 if self.mode.quant_v() {
361 Self::quant_row(&v_new[s..s + self.head_dim],
362 &self.vcol[h], &mut self.vq[h], &mut self.vs[h],
363 self.head_dim);
364 } else {
365 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
366 }
367 }
368 self.imp.push(0.0);
369 self.seq_len += 1;
370 }
371
372 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
377 let hd = self.head_dim;
378 if self.mode == KvMode::F32 {
379 let stored = self.k[kv_head].len() / hd;
380 return crate::attention::attention_head(
381 q, &self.k[kv_head], &self.v[kv_head], hd, stored);
382 }
383 let stored = self.head_len(kv_head);
384 let scale = 1.0 / (hd as f32).sqrt();
385 let mut scores = vec![0.0f32; stored];
386 if self.mode.quant_k() {
387 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
388 let kcol = &self.kcol[kv_head];
390 let mut qc = vec![0.0f32; hd];
391 for d in 0..hd {
392 qc[d] = if kcol.is_empty() { q[d] } else { q[d] * kcol[d] };
393 }
394 let ng = hd.div_ceil(KV_K_GROUP);
395 for p in 0..stored {
396 let row = &kq[p * hd..(p + 1) * hd];
397 let row_u8 = unsafe {
400 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
401 };
402 let mut dot = 0.0f32;
403 for g in 0..ng {
404 let g0 = g * KV_K_GROUP;
405 let g1 = (g0 + KV_K_GROUP).min(hd);
406 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1])
407 * ks[p * ng + g];
408 }
409 scores[p] = dot * scale;
410 }
411 } else {
412 let k = &self.k[kv_head];
413 for p in 0..stored {
414 let row = &k[p * hd..(p + 1) * hd];
415 scores[p] = crate::attention::dot_f32(q, row) * scale;
416 }
417 }
418 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
419 let mut sum = 0.0f32;
420 for s in scores.iter_mut() {
421 *s = (*s - max_score).exp();
422 sum += *s;
423 }
424 if sum > 0.0 {
425 for s in scores.iter_mut() {
426 *s /= sum;
427 }
428 }
429 let mut acc = vec![0.0f32; hd];
430 if self.mode.quant_v() {
431 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
432 for p in 0..stored {
433 let w = scores[p] * vs[p];
434 if w.abs() < 1e-12 {
435 continue;
436 }
437 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
438 }
439 let vcol = &self.vcol[kv_head];
440 if !vcol.is_empty() {
441 for d in 0..hd {
442 acc[d] *= vcol[d];
443 }
444 }
445 } else {
446 let v = &self.v[kv_head];
447 for p in 0..stored {
448 let w = scores[p];
449 if w.abs() < 1e-12 {
450 continue;
451 }
452 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
453 }
454 }
455 (acc, scores)
456 }
457
458 #[allow(clippy::too_many_arguments)]
472 pub fn attend_group(
473 &self,
474 q_group: &[f32],
475 kv_head: usize,
476 out: &mut [f32],
477 imp_acc: &mut [f32],
478 scale: f32,
479 first: usize,
480 ) {
481 let hd = self.head_dim;
482 let nheads = q_group.len() / hd;
483 debug_assert_eq!(out.len(), nheads * hd);
484 let stored = if self.mode == KvMode::F32 {
485 self.k[kv_head].len() / hd
486 } else {
487 self.head_len(kv_head)
488 };
489 if stored == 0 {
490 out.fill(0.0);
491 return;
492 }
493 let first = first.min(stored.saturating_sub(1));
494
495 thread_local! {
496 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
498 const { std::cell::RefCell::new(Vec::new()) };
499 static GQA_QC: std::cell::RefCell<Vec<f32>> =
501 const { std::cell::RefCell::new(Vec::new()) };
502 }
503
504 GQA_SCORES.with(|sc| {
505 let mut scores = sc.borrow_mut();
506 if first > 0 {
507 scores.clear();
510 scores.resize(nheads * stored, f32::NEG_INFINITY);
511 } else {
512 scores.resize(nheads * stored, 0.0);
513 }
514
515 if self.mode.quant_k() {
517 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
518 let kcol = &self.kcol[kv_head];
519 let ng = hd.div_ceil(KV_K_GROUP);
520 GQA_QC.with(|qc| {
521 let mut qcb = qc.borrow_mut();
522 qcb.resize(nheads * hd, 0.0);
523 for h in 0..nheads {
524 for d in 0..hd {
525 let qv = q_group[h * hd + d];
526 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
527 }
528 }
529 for p in first..stored {
530 let row = &kq[p * hd..(p + 1) * hd];
531 let row_u8 = unsafe {
534 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
535 };
536 for h in 0..nheads {
537 let qch = &qcb[h * hd..(h + 1) * hd];
538 let mut dot = 0.0f32;
539 for g in 0..ng {
540 let g0 = g * KV_K_GROUP;
541 let g1 = (g0 + KV_K_GROUP).min(hd);
542 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
543 * ks[p * ng + g];
544 }
545 scores[h * stored + p] = dot * scale;
546 }
547 }
548 });
549 } else {
550 let k = &self.k[kv_head];
551 for p in first..stored {
552 let row = &k[p * hd..(p + 1) * hd];
553 for h in 0..nheads {
554 scores[h * stored + p] =
555 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row)
556 * scale;
557 }
558 }
559 }
560
561 for h in 0..nheads {
563 let s = &mut scores[h * stored..(h + 1) * stored];
564 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
565 let mut sum = 0.0f32;
566 for v in s.iter_mut() {
567 *v = (*v - max_score).exp();
568 sum += *v;
569 }
570 if sum > 0.0 {
571 for v in s.iter_mut() {
572 *v /= sum;
573 }
574 }
575 }
576
577 out.fill(0.0);
579 if self.mode.quant_v() {
580 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
581 for p in first..stored {
582 let row = &vq[p * hd..(p + 1) * hd];
583 for h in 0..nheads {
584 let w = scores[h * stored + p] * vs[p];
585 if w.abs() < 1e-12 {
586 continue;
587 }
588 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
589 }
590 }
591 let vcol = &self.vcol[kv_head];
592 if !vcol.is_empty() {
593 for h in 0..nheads {
594 for d in 0..hd {
595 out[h * hd + d] *= vcol[d];
596 }
597 }
598 }
599 } else {
600 let v = &self.v[kv_head];
601 for p in first..stored {
602 let row = &v[p * hd..(p + 1) * hd];
603 for h in 0..nheads {
604 let w = scores[h * stored + p];
605 if w.abs() < 1e-12 {
606 continue;
607 }
608 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
609 }
610 }
611 }
612
613 let n = imp_acc.len().min(stored);
616 for h in 0..nheads {
617 let s = &scores[h * stored..(h + 1) * stored];
618 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
619 *dst += p;
620 }
621 }
622 });
623 }
624
625 #[cfg(target_arch = "aarch64")]
633 #[allow(clippy::too_many_arguments)]
634 pub fn attend_chunk(
635 &mut self,
636 q_all: &[f32],
637 b: usize,
638 s0: usize,
639 nh: usize,
640 heads_per_kv: usize,
641 hd: usize,
642 out: &mut [f32],
643 pool: Option<&crate::pool::Pool>,
644 scale: f32,
645 window: Option<usize>,
646 ) {
647 let n = s0 + b;
648 struct SendPtr(*mut f32);
649 unsafe impl Send for SendPtr {}
650 unsafe impl Sync for SendPtr {}
651 impl SendPtr {
652 fn at(&self, i: usize) -> *mut f32 {
653 unsafe { self.0.add(i) }
656 }
657 }
658 thread_local! {
659 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
660 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
661 }
662 let neon_gemm = cfg!(not(target_os = "macos"))
667 || std::env::var("CMF_FORCE_NEON_GEMM").map(|v| v == "1").unwrap_or(false);
668 SCRATCH.with(|s| {
669 let mut s = s.borrow_mut();
670 let (qpanel, scores, aopanel, ktpack) = &mut *s;
671 let m = heads_per_kv * b;
676 qpanel.resize(m * hd, 0.0);
677 scores.resize(m * n, 0.0);
678 aopanel.resize(m * hd, 0.0);
679 for g in 0..self.num_kv_heads {
680 let kmat = &self.k[g];
681 let vmat = &self.v[g];
682 debug_assert_eq!(kmat.len(), n * hd);
683 for hl in 0..heads_per_kv {
684 let hh = g * heads_per_kv + hl;
685 for bi in 0..b {
686 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
687 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
688 }
689 }
690 if neon_gemm {
691 ktpack.resize(hd * n, 0.0);
692 for p in 0..n {
693 let row = &kmat[p * hd..(p + 1) * hd];
694 for (d, &v) in row.iter().enumerate() {
695 ktpack[d * n + p] = v;
696 }
697 }
698 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
701 let sp_s = SendPtr(scores.as_mut_ptr());
702 let kt = &*ktpack;
703 let run = |start: usize, end: usize| {
704 if end > start {
705 let a = unsafe {
707 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
708 };
709 let c = unsafe {
710 std::slice::from_raw_parts_mut(sp_s.at(start * n), (end - start) * n)
711 };
712 crate::qtensor::neon_gemm_rm(
713 end - start, n, hd, scale, a, hd, kt, n, false, c, n,
714 );
715 }
716 };
717 match pool {
718 Some(p) if m >= 64 => p.run_rows(m, &run),
719 _ => run(0, m),
720 }
721 } else {
722 crate::qtensor::sgemm_rm(m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n);
723 }
724 let sp = SendPtr(scores.as_mut_ptr());
726 let run = |start: usize, end: usize| {
727 for r in start..end {
728 let allowed = s0 + (r % b) + 1;
729 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
733 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
735 crate::attention::softmax_row(&mut row[lo..allowed]);
736 row[..lo].fill(0.0);
737 row[allowed..].fill(0.0);
738 }
739 };
740 match pool {
741 Some(p) if m >= 64 => p.run_rows(m, &run),
742 _ => run(0, m),
743 }
744 let ni = self.imp.len().min(n);
748 for r in 0..m {
749 let al = (s0 + (r % b) + 1).min(ni);
750 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
751 *dst += p;
752 }
753 }
754 if neon_gemm {
755 let sp_s = SendPtr(scores.as_mut_ptr());
756 let sp_o = SendPtr(aopanel.as_mut_ptr());
757 let run = |start: usize, end: usize| {
758 if end > start {
759 let a = unsafe {
761 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
762 };
763 let c = unsafe {
764 std::slice::from_raw_parts_mut(
765 sp_o.at(start * hd),
766 (end - start) * hd,
767 )
768 };
769 crate::qtensor::neon_gemm_rm(
770 end - start, hd, n, 1.0, a, n, vmat, hd, false, c, hd,
771 );
772 }
773 };
774 match pool {
775 Some(p) if m >= 64 => p.run_rows(m, &run),
776 _ => run(0, m),
777 }
778 } else {
779 crate::qtensor::sgemm_rm(m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd);
780 }
781 for hl in 0..heads_per_kv {
782 let hh = g * heads_per_kv + hl;
783 for bi in 0..b {
784 out[bi * nh * hd + hh * hd..][..hd]
785 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
786 }
787 }
788 }
789 });
790 }
791
792 pub fn truncate_last(&mut self, n_drop: usize) {
794 let d = n_drop.min(self.seq_len);
795 for h in 0..self.num_kv_heads {
796 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
797 self.k[h].truncate(keep);
798 self.v[h].truncate(keep);
799 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
800 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
801 self.kq[h].truncate(keep_q);
802 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
803 self.vq[h].truncate(keep_vq);
804 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
805 self.ks[h].truncate(keep_ks);
806 let keep_vs = self.vs[h].len().saturating_sub(d);
807 self.vs[h].truncate(keep_vs);
808 }
809 self.imp.truncate(self.imp.len().saturating_sub(d));
810 self.seq_len -= d;
811 }
812
813 pub fn accumulate_imp(&mut self, probs: &[f32]) {
815 for (dst, &p) in self.imp.iter_mut().zip(probs) {
816 *dst += p;
817 }
818 }
819
820 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
822 &self.k[kv_head]
823 }
824
825 pub fn head_values(&self, kv_head: usize) -> &[f32] {
826 &self.v[kv_head]
827 }
828
829 pub fn head_len(&self, kv_head: usize) -> usize {
831 let ng = self.head_dim.div_ceil(KV_K_GROUP);
832 (self.k[kv_head].len() / self.head_dim)
833 .max(self.ks[kv_head].len() / ng)
834 .max(self.vs[kv_head].len())
835 }
836
837 pub fn clear(&mut self) {
839 for h in 0..self.num_kv_heads {
840 self.k[h].clear();
841 self.v[h].clear();
842 self.kq[h].clear();
843 self.ks[h].clear();
844 self.vq[h].clear();
845 self.vs[h].clear();
846 self.kcol[h].clear();
847 self.vcol[h].clear();
848 }
849 self.imp.clear();
850 self.linear_state.clear();
851 self.linear_scratch.clear();
852 self.o1 = None;
855 self.seq_len = 0;
856 }
857
858 pub fn memory_bytes(&self) -> usize {
860 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
861 + self.v.iter().map(Vec::len).sum::<usize>()
862 + self.ks.iter().map(Vec::len).sum::<usize>()
863 + self.vs.iter().map(Vec::len).sum::<usize>()
864 + self.kcol.iter().map(Vec::len).sum::<usize>()
865 + self.vcol.iter().map(Vec::len).sum::<usize>();
866 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
867 + self.vq.iter().map(Vec::len).sum::<usize>();
868 floats * std::mem::size_of::<f32>()
869 + bytes
870 + self.linear_state.len() * std::mem::size_of::<f32>()
874 + self.o1_memory_bytes()
877 }
878
879 fn evict(&mut self, keep_last: usize) {
881 if self.o1_sealed() || self.seq_len <= keep_last {
885 return;
886 }
887 let drop = self.seq_len - keep_last;
888 for h in 0..self.num_kv_heads {
889 let stored = self.head_len(h);
891 let d = drop.min(stored);
892 let hd = self.head_dim;
893 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
894 let n = n.min(v.len());
895 v.drain(..n);
896 }
897 drop_front(&mut self.k[h], d * hd);
898 drop_front(&mut self.v[h], d * hd);
899 drop_front(&mut self.kq[h], d * hd);
900 drop_front(&mut self.vq[h], d * hd);
901 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
902 drop_front(&mut self.vs[h], d);
903 }
904 let d = drop.min(self.imp.len());
905 self.imp.drain(..d);
906 self.seq_len = keep_last;
907 }
908
909 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
914 if self.o1_sealed() {
915 return; }
917 let stored = self.imp.len();
918 if stored <= keep_last {
919 return;
920 }
921 let sink_n = sink.min(keep_last);
924 let recent_n = recent.min(keep_last - sink_n);
925 let mut keep = vec![false; stored];
926 for k in keep.iter_mut().take(sink_n) {
927 *k = true;
928 }
929 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
930 *k = true;
931 }
932 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
933 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
935 order.sort_by(|&a, &b| {
936 self.imp[b].partial_cmp(&self.imp[a]).unwrap_or(std::cmp::Ordering::Equal)
937 });
938 for i in order {
939 if budget == 0 {
940 break;
941 }
942 keep[i] = true;
943 budget -= 1;
944 }
945
946 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
947 let hd = self.head_dim;
948 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
949 let mut out = Vec::with_capacity(kept.len() * step);
950 for &i in kept {
951 out.extend_from_slice(&src[i * step..(i + 1) * step]);
952 }
953 out
954 }
955 for h in 0..self.num_kv_heads {
960 if !self.k[h].is_empty() {
961 self.k[h] = gather(&self.k[h], &kept, hd);
962 }
963 if !self.v[h].is_empty() {
964 self.v[h] = gather(&self.v[h], &kept, hd);
965 }
966 if !self.kq[h].is_empty() {
967 self.kq[h] = gather(&self.kq[h], &kept, hd);
968 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
969 }
970 if !self.vq[h].is_empty() {
971 self.vq[h] = gather(&self.vq[h], &kept, hd);
972 self.vs[h] = gather(&self.vs[h], &kept, 1);
973 }
974 }
975 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
976 self.seq_len = kept.len();
977 }
978}
979
980#[derive(Debug, Clone, Copy, PartialEq, Eq)]
982pub enum EvictionPolicy {
983 Recent,
985 Born { sink: usize },
987}
988
989#[derive(Debug)]
991pub struct KvCache {
992 pub layers: Vec<LayerKvCache>,
993 pub max_seq_len: usize,
994 pub policy: EvictionPolicy,
995}
996
997impl KvCache {
998 pub fn new(num_layers: usize, num_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
999 let layers = (0..num_layers)
1000 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1001 .collect();
1002 Self {
1003 layers,
1004 max_seq_len,
1005 policy: EvictionPolicy::Born { sink: 4 },
1006 }
1007 }
1008
1009 pub fn clear(&mut self) {
1010 for layer in &mut self.layers {
1011 layer.clear();
1012 }
1013 }
1014
1015 pub fn total_memory_bytes(&self) -> usize {
1016 self.layers.iter().map(|l| l.memory_bytes()).sum()
1017 }
1018
1019 pub fn seq_len(&self) -> usize {
1021 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1022 }
1023
1024 pub fn needs_eviction(&self) -> bool {
1025 self.seq_len() >= self.max_seq_len
1026 }
1027
1028 pub fn evict(&mut self, keep_last: usize) {
1030 match self.policy {
1031 EvictionPolicy::Recent => {
1032 for layer in &mut self.layers {
1033 layer.evict(keep_last);
1034 }
1035 }
1036 EvictionPolicy::Born { sink } => {
1037 let recent = (keep_last / 2).max(1);
1038 for layer in &mut self.layers {
1039 layer.evict_born(keep_last, sink, recent);
1040 }
1041 }
1042 }
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::*;
1049
1050 #[test]
1051 fn append_tracks_seq_len_and_layout() {
1052 let mut cache = LayerKvCache::new(4, 8);
1053 cache.mode = KvMode::F32;
1054 assert_eq!(cache.seq_len, 0);
1055
1056 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1057 let v = vec![2.0f32; 32];
1058 cache.append(&k, &v, &[true; 4]);
1059
1060 assert_eq!(cache.seq_len, 1);
1061 assert_eq!(cache.head_len(0), 1);
1062 assert_eq!(cache.head_keys(1), &k[8..16]);
1064 assert_eq!(cache.memory_bytes(), 256);
1065 }
1066
1067 #[test]
1068 fn dead_head_stores_nothing() {
1069 let mut cache = LayerKvCache::new(2, 4);
1070 cache.mode = KvMode::F32;
1071 let k = vec![1.0f32; 8];
1072 let v = vec![2.0f32; 8];
1073 cache.append(&k, &v, &[true, false]);
1074 cache.append(&k, &v, &[true, false]);
1075
1076 assert_eq!(cache.seq_len, 2);
1077 assert_eq!(cache.head_len(0), 2);
1078 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1079 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1080 }
1081
1082 #[test]
1083 fn eviction_keeps_recent() {
1084 let mut cache = KvCache::new(2, 4, 8, 10);
1085 cache.policy = EvictionPolicy::Recent;
1086 for l in &mut cache.layers { l.mode = KvMode::F32; }
1087 let k = vec![1.0f32; 32];
1088 let v = vec![2.0f32; 32];
1089 for _ in 0..8 {
1090 for layer in &mut cache.layers {
1091 layer.append(&k, &v, &[true; 4]);
1092 }
1093 }
1094 assert_eq!(cache.seq_len(), 8);
1095 assert!(!cache.needs_eviction());
1096
1097 cache.evict(4);
1098 assert_eq!(cache.seq_len(), 4);
1099 assert_eq!(cache.layers[0].head_len(0), 4);
1100 }
1101
1102 #[test]
1103 fn truncate_rolls_back_speculative_positions() {
1104 let mut cache = LayerKvCache::new(2, 4);
1105 cache.mode = KvMode::F32;
1106 for pos in 0..5 {
1107 let k = vec![pos as f32; 8];
1108 let v = vec![pos as f32; 8];
1109 cache.append(&k, &v, &[true; 2]);
1110 }
1111 cache.truncate_last(2);
1112 assert_eq!(cache.seq_len, 3);
1113 assert_eq!(cache.head_len(0), 3);
1114 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1115 }
1116
1117 #[test]
1121 fn q8_attend_matches_f32_within_grid() {
1122 let (heads, hd) = (2, 32);
1123 let mut f = LayerKvCache::new(heads, hd);
1124 f.mode = KvMode::F32;
1125 let mut q8 = LayerKvCache::new(heads, hd);
1126 q8.mode = KvMode::Q8 { k: true, v: true };
1127
1128 let synth = |p: usize, salt: usize| -> Vec<f32> {
1129 (0..heads * hd)
1130 .map(|i| {
1131 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1132 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1134 })
1135 .collect()
1136 };
1137 for p in 0..100 {
1138 let k = synth(p, 1);
1139 let v = synth(p, 2);
1140 f.append(&k, &v, &[true; 2]);
1141 q8.append(&k, &v, &[true; 2]);
1142 }
1143 let q: Vec<f32> = (0..hd).map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5).collect();
1144 for g in 0..heads {
1145 let (of, pf) = f.attend(&q, g);
1146 let (o8, p8) = q8.attend(&q, g);
1147 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1148 for d in 0..hd {
1149 assert!(
1150 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1151 "g{g} d{d}: f32 {} vs q8 {}", of[d], o8[d]
1152 );
1153 }
1154 for p in 0..100 {
1155 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1156 }
1157 }
1158 q8.truncate_last(30);
1160 assert_eq!(q8.head_len(0), 70);
1161 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1162 q8.accumulate_imp(&imp);
1163 q8.evict_born(20, 2, 8);
1164 assert_eq!(q8.head_len(0), 20);
1165 let (o, _) = q8.attend(&q, 0);
1166 assert!(o.iter().all(|x| x.is_finite()));
1167 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1169 }
1170
1171 #[test]
1174 fn attend_group_equals_per_head_attend_bitexact() {
1175 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1177 let mut c = LayerKvCache::new(kv_heads, hd);
1178 c.mode = mode;
1179 for p in 0..70 {
1180 let k: Vec<f32> = (0..kv_heads * hd)
1181 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1182 .collect();
1183 let v: Vec<f32> = (0..kv_heads * hd)
1184 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1185 .collect();
1186 c.append(&k, &v, &[true; 2]);
1187 }
1188 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1189 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1190 .collect();
1191 for g in 0..kv_heads {
1192 let span = g * hpk * hd..(g + 1) * hpk * hd;
1193 let mut out = vec![0f32; hpk * hd];
1194 let mut imp = vec![0f32; 70];
1195 c.attend_group(
1196 &q[span.clone()],
1197 g,
1198 &mut out,
1199 &mut imp,
1200 1.0 / (hd as f32).sqrt(),
1201 0,
1202 );
1203 let mut imp_ref = vec![0f32; 70];
1204 for h in 0..hpk {
1205 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1206 let (o, probs) = c.attend(qh, g);
1207 assert_eq!(
1208 &out[h * hd..(h + 1) * hd],
1209 &o[..],
1210 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1211 );
1212 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1213 *dst += p;
1214 }
1215 }
1216 assert_eq!(imp, imp_ref, "mode {mode:?} g{g}: Born mass must match");
1217 }
1218 }
1219 }
1220
1221 #[test]
1225 fn born_eviction_mixed_modes_stay_consistent() {
1226 for (mk, mv) in [(false, true), (true, false)] {
1227 let mut c = LayerKvCache::new(1, 4);
1228 c.mode = KvMode::Q8 { k: mk, v: mv };
1229 for p in 0..80 {
1230 let k = vec![p as f32 * 0.01; 4];
1231 let v = vec![p as f32; 4];
1232 c.append(&k, &v, &[true]);
1233 }
1234 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1235 c.accumulate_imp(&imp);
1236 let before = c.memory_bytes();
1237 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1239 assert!(c.memory_bytes() < before / 2,
1240 "memory must shrink (k={mk} v={mv})");
1241 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1244 assert!(out[0] > 30.0,
1245 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1246 out[0]);
1247 }
1248 }
1249
1250 #[test]
1251 fn born_eviction_keeps_high_mass_position() {
1252 let mut cache = KvCache::new(1, 1, 2, 16);
1253 cache.policy = EvictionPolicy::Born { sink: 1 };
1254 for l in &mut cache.layers { l.mode = KvMode::F32; }
1255 let layer = &mut cache.layers[0];
1256 for pos in 0..8 {
1259 let k = vec![pos as f32; 2];
1260 let v = vec![pos as f32 + 100.0; 2];
1261 layer.append(&k, &v, &[true]);
1262 }
1263 let mut imp = vec![0.05f32; 8];
1265 imp[3] = 5.0;
1266 layer.accumulate_imp(&imp);
1267
1268 cache.evict(4); let layer = &cache.layers[0];
1270 assert_eq!(layer.seq_len, 4);
1271 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1272 assert_eq!(
1273 kept_keys,
1274 vec![0.0, 3.0, 6.0, 7.0],
1275 "kept = sink(0) + Born-top(3) + recent(6,7)"
1276 );
1277 assert_eq!(layer.head_len(0), 4);
1279 }
1280}