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 seal_at: Option<usize>,
72 q_buf: Vec<f32>,
74 },
75 Sealed {
81 groups: Vec<crate::nystrom::NystromState>,
82 },
83}
84
85#[derive(Debug, Clone)]
87pub struct LayerKvCache {
88 pub mode: KvMode,
89 k: Vec<Vec<f32>>,
91 v: Vec<Vec<f32>>,
93 kq: Vec<Vec<i8>>,
95 ks: Vec<Vec<f32>>,
96 vq: Vec<Vec<i8>>,
97 vs: Vec<Vec<f32>>,
98 kcol: Vec<Vec<f32>>,
100 vcol: Vec<Vec<f32>>,
101 imp: Vec<f32>,
104 pub seq_len: usize,
106 pub num_kv_heads: usize,
107 pub head_dim: usize,
108 pub linear_state: Vec<f32>,
110 pub linear_scratch: Vec<f32>,
112 pub o1: Option<O1State>,
114 o1_error: Option<String>,
118 o1_transitioned: bool,
121}
122
123impl LayerKvCache {
124 pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
125 Self {
126 mode: KvMode::from_env(),
127 k: vec![Vec::new(); num_kv_heads],
128 v: vec![Vec::new(); num_kv_heads],
129 kq: vec![Vec::new(); num_kv_heads],
130 ks: vec![Vec::new(); num_kv_heads],
131 vq: vec![Vec::new(); num_kv_heads],
132 vs: vec![Vec::new(); num_kv_heads],
133 kcol: vec![Vec::new(); num_kv_heads],
134 vcol: vec![Vec::new(); num_kv_heads],
135 imp: Vec::new(),
136 seq_len: 0,
137 num_kv_heads,
138 head_dim,
139 linear_state: Vec::new(),
140 linear_scratch: Vec::new(),
141 o1: None,
142 o1_error: None,
143 o1_transitioned: false,
144 }
145 }
146
147 pub fn k_heads(&self) -> &[Vec<f32>] {
149 &self.k
150 }
151 pub fn v_heads(&self) -> &[Vec<f32>] {
153 &self.v
154 }
155
156 pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
160 self.o1_begin_with_boundary(m, w, sink, rect, None);
161 }
162
163 pub(crate) fn o1_begin_with_boundary(
167 &mut self,
168 m: usize,
169 w: usize,
170 sink: usize,
171 rect: crate::nystrom::O1Rect,
172 seal_at: Option<usize>,
173 ) {
174 self.o1 = Some(O1State::Collecting {
175 m,
176 w,
177 sink,
178 rect,
179 seal_at,
180 q_buf: Vec::new(),
181 });
182 self.o1_error = None;
183 self.o1_transitioned = false;
184 }
185
186 pub fn o1_push_q(&mut self, q_all: &[f32]) {
191 if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
192 q_buf.extend_from_slice(q_all);
193 }
194 }
195
196 pub fn o1_sealed(&self) -> bool {
197 matches!(self.o1, Some(O1State::Sealed { .. }))
198 }
199
200 pub(crate) fn o1_pending_boundary(&self) -> Option<usize> {
203 match &self.o1 {
204 Some(O1State::Collecting { seal_at, .. }) => *seal_at,
205 _ => None,
206 }
207 }
208
209 pub(crate) fn o1_boundary_crossed_by(&self, count: usize) -> bool {
213 let Some(target) = self.o1_pending_boundary() else {
214 return false;
215 };
216 target <= self.seq_len
217 || self
218 .seq_len
219 .checked_add(count)
220 .map_or(true, |next| next >= target)
221 }
222
223 pub(crate) fn take_o1_transition(&mut self) -> bool {
224 std::mem::take(&mut self.o1_transitioned)
225 }
226
227 pub(crate) fn take_o1_error(&self) -> Option<String> {
228 self.o1_error.clone()
234 }
235
236 pub(crate) fn o1_abort(&mut self, err: String) {
241 self.k.iter_mut().for_each(Vec::clear);
242 self.v.iter_mut().for_each(Vec::clear);
243 self.kq.iter_mut().for_each(Vec::clear);
244 self.ks.iter_mut().for_each(Vec::clear);
245 self.vq.iter_mut().for_each(Vec::clear);
246 self.vs.iter_mut().for_each(Vec::clear);
247 self.kcol.iter_mut().for_each(Vec::clear);
248 self.vcol.iter_mut().for_each(Vec::clear);
249 self.imp.clear();
250 self.o1 = None;
251 self.seq_len = 0;
252 self.o1_transitioned = false;
253 self.o1_error = Some(err);
254 }
255
256 pub fn o1_seal(&mut self, num_heads: usize) -> bool {
263 match self.o1_seal_checked(num_heads) {
264 Ok(sealed) => sealed,
265 Err(err) => {
266 tracing::error!("o1: seal aborted: {err}");
267 self.o1_abort(err);
268 false
269 }
270 }
271 }
272
273 pub(crate) fn o1_seal_checked(&mut self, num_heads: usize) -> Result<bool, String> {
277 if let Some(err) = self.o1_error.clone() {
278 return Err(err);
279 }
280 if !matches!(self.o1, Some(O1State::Collecting { .. })) {
283 return Ok(self.o1_sealed());
284 }
285 let (m, w, sink, requested_boundary, q_len) = match &self.o1 {
286 Some(O1State::Collecting {
287 m,
288 w,
289 sink,
290 rect: _,
291 seal_at,
292 q_buf,
293 }) => (*m, *w, *sink, *seal_at, q_buf.len()),
294 _ => unreachable!("checked above"),
295 };
296 let floor = crate::nystrom::o1_deferred_boundary(w, sink)
297 .ok_or_else(|| "o1 seal: w + sink + slack + 1 overflow".to_string())?;
298 let target = requested_boundary.unwrap_or(floor).max(floor);
299 let t = self.seq_len;
300 if t < target {
301 if let Some(O1State::Collecting { seal_at, .. }) = &mut self.o1 {
302 if *seal_at != Some(target) {
303 *seal_at = Some(target);
304 tracing::info!(
305 "o1 deferred seal: current rows={t}, boundary={target} (floor={floor})"
306 );
307 }
308 }
309 return Ok(false);
310 }
311
312 let hd = self.head_dim;
313 if t == 0 {
314 return Err("o1 seal: cannot seal an empty layer".into());
315 }
316 if self.mode != KvMode::F32 {
317 return Err("o1 seal: requires dense F32 KV storage".into());
318 }
319 if self.num_kv_heads == 0 || num_heads == 0 || num_heads % self.num_kv_heads != 0 {
320 return Err(format!(
321 "o1 seal: invalid GQA geometry num_heads={num_heads} num_kv_heads={}",
322 self.num_kv_heads
323 ));
324 }
325 let hpk = num_heads / self.num_kv_heads;
326 let expected_k = t
327 .checked_mul(hd)
328 .ok_or_else(|| "o1 seal: KV row length overflow".to_string())?;
329 let expected_q = expected_k
330 .checked_mul(num_heads)
331 .ok_or_else(|| "o1 seal: query trace length overflow".to_string())?;
332 if q_len != expected_q {
333 return Err(format!(
334 "o1 seal: query trace has {q_len} values, expected {expected_q}"
335 ));
336 }
337 if (0..self.num_kv_heads)
338 .any(|g| self.k[g].len() != expected_k || self.v[g].len() != expected_k)
339 {
340 return Err("o1 seal: KV heads are not densely populated".into());
341 }
342 if m < 4 || w == 0 {
343 return Err(format!("o1 seal: invalid geometry m={m} w={w}"));
344 }
345
346 let Some(O1State::Collecting {
347 m,
348 w,
349 sink,
350 rect,
351 q_buf,
352 ..
353 }) = self.o1.take()
354 else {
355 unreachable!("collecting state disappeared after validation");
356 };
357 let mut groups = Vec::with_capacity(self.num_kv_heads);
358 let mut qh = vec![0.0f32; hpk * t * hd];
361 for g in 0..self.num_kv_heads {
362 for hh in 0..hpk {
363 let h = g * hpk + hh;
364 for p in 0..t {
365 let src = (p * num_heads + h) * hd;
366 let dst = (hh * t + p) * hd;
367 qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
368 }
369 }
370 let qs: Vec<&[f32]> = (0..hpk)
371 .map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd])
372 .collect();
373 let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
374 st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
375 groups.push(st);
376 }
377 for h in 0..self.num_kv_heads {
380 self.k[h] = Vec::new();
381 self.v[h] = Vec::new();
382 }
383 self.imp = Vec::new();
384 self.o1 = Some(O1State::Sealed { groups });
385 self.o1_transitioned = true;
386 Ok(true)
387 }
388
389 pub fn o1_views(&self) -> Option<Vec<crate::nystrom::O1DeviceView<'_>>> {
398 let Some(O1State::Sealed { groups }) = &self.o1 else {
399 return None;
400 };
401 let views: Vec<_> = groups.iter().map(|g| g.device_view()).collect();
402 if views.iter().any(|v| v.exact_only) {
403 return None;
404 }
405 Some(views)
406 }
407
408 pub fn o1_step(
409 &mut self,
410 q_all: &[f32],
411 k_new: &[f32],
412 v_new: &[f32],
413 num_heads: usize,
414 ) -> Vec<f32> {
415 let hd = self.head_dim;
416 let hpk = num_heads / self.num_kv_heads.max(1);
417 let mut out = vec![0.0f32; num_heads * hd];
418 let Some(O1State::Sealed { groups }) = &mut self.o1 else {
419 debug_assert!(false, "o1_step on an unsealed layer");
420 return out;
421 };
422 for (g, st) in groups.iter_mut().enumerate() {
423 let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
424 st.step_group(
425 &q_all[lo..hi],
426 &k_new[g * hd..(g + 1) * hd],
427 &v_new[g * hd..(g + 1) * hd],
428 &mut out[lo..hi],
429 );
430 }
431 self.seq_len += 1;
434 out
435 }
436
437 pub fn o1_memory_bytes(&self) -> usize {
440 match &self.o1 {
441 Some(O1State::Collecting { q_buf, .. }) => q_buf.len() * std::mem::size_of::<f32>(),
442 Some(O1State::Sealed { groups }) => groups.iter().map(|s| s.memory_bytes()).sum(),
443 None => 0,
444 }
445 }
446
447 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>, group: usize) {
450 let mut resid = vec![0.0f32; row.len()];
451 for (d, &x) in row.iter().enumerate() {
452 resid[d] = if col.is_empty() { x } else { x / col[d] };
453 }
454 for g0 in (0..row.len()).step_by(group) {
455 let g1 = (g0 + group).min(row.len());
456 let mut absmax = 0.0f32;
457 for &r in &resid[g0..g1] {
458 absmax = absmax.max(r.abs());
459 }
460 let s = (absmax / 127.0).max(1e-12);
461 sc.push(s);
462 for &r in &resid[g0..g1] {
463 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
464 }
465 }
466 }
467
468 fn freeze_cols(&mut self) {
471 let hd = self.head_dim;
472 let ngk = hd.div_ceil(KV_K_GROUP);
473 for h in 0..self.num_kv_heads {
474 for (qv, sv, colv, group) in [
475 (
476 &mut self.kq[h],
477 &mut self.ks[h],
478 &mut self.kcol[h],
479 KV_K_GROUP,
480 ),
481 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
482 ] {
483 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
485 if n == 0 {
486 continue;
487 }
488 let mut rows = vec![0.0f32; n * hd];
490 for p in 0..n {
491 for d in 0..hd {
492 rows[p * hd + d] = qv[p * hd + d] as f32 * sv[p * spp + d / group];
493 }
494 }
495 let mut col = vec![0.0f32; hd];
496 for p in 0..n {
497 for d in 0..hd {
498 col[d] += rows[p * hd + d] * rows[p * hd + d];
499 }
500 }
501 for c in col.iter_mut() {
502 *c = (*c / n as f32).sqrt().max(1e-6);
503 }
504 qv.clear();
505 sv.clear();
506 for p in 0..n {
507 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
508 }
509 *colv = col;
510 }
511 }
512 }
513
514 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
518 if self.o1_error.is_some() {
522 return;
523 }
524 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
525 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
526 if matches!(self.mode, KvMode::Q8 { .. })
532 && self.seq_len >= KV_COL_WARMUP
533 && self.kcol.iter().all(Vec::is_empty)
534 && self.vcol.iter().all(Vec::is_empty)
535 {
536 self.freeze_cols();
537 }
538 for h in 0..self.num_kv_heads {
539 if !alive.get(h).copied().unwrap_or(true) {
540 continue;
541 }
542 let s = h * self.head_dim;
543 if self.mode.quant_k() {
544 Self::quant_row(
545 &k_new[s..s + self.head_dim],
546 &self.kcol[h],
547 &mut self.kq[h],
548 &mut self.ks[h],
549 KV_K_GROUP,
550 );
551 } else {
552 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
553 }
554 if self.mode.quant_v() {
555 Self::quant_row(
556 &v_new[s..s + self.head_dim],
557 &self.vcol[h],
558 &mut self.vq[h],
559 &mut self.vs[h],
560 self.head_dim,
561 );
562 } else {
563 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
564 }
565 }
566 self.imp.push(0.0);
567 self.seq_len += 1;
568 }
569
570 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
575 let hd = self.head_dim;
576 if self.mode == KvMode::F32 {
577 let stored = self.k[kv_head].len() / hd;
578 return crate::attention::attention_head(
579 q,
580 &self.k[kv_head],
581 &self.v[kv_head],
582 hd,
583 stored,
584 );
585 }
586 let stored = self.head_len(kv_head);
587 let scale = 1.0 / (hd as f32).sqrt();
588 let mut scores = vec![0.0f32; stored];
589 if self.mode.quant_k() {
590 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
591 let kcol = &self.kcol[kv_head];
593 let mut qc = vec![0.0f32; hd];
594 for d in 0..hd {
595 qc[d] = if kcol.is_empty() {
596 q[d]
597 } else {
598 q[d] * kcol[d]
599 };
600 }
601 let ng = hd.div_ceil(KV_K_GROUP);
602 for p in 0..stored {
603 let row = &kq[p * hd..(p + 1) * hd];
604 let row_u8 =
607 unsafe { std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len()) };
608 let mut dot = 0.0f32;
609 for g in 0..ng {
610 let g0 = g * KV_K_GROUP;
611 let g1 = (g0 + KV_K_GROUP).min(hd);
612 dot +=
613 crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1]) * ks[p * ng + g];
614 }
615 scores[p] = dot * scale;
616 }
617 } else {
618 let k = &self.k[kv_head];
619 for p in 0..stored {
620 let row = &k[p * hd..(p + 1) * hd];
621 scores[p] = crate::attention::dot_f32(q, row) * scale;
622 }
623 }
624 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
625 let mut sum = 0.0f32;
626 for s in scores.iter_mut() {
627 *s = (*s - max_score).exp();
628 sum += *s;
629 }
630 if sum > 0.0 {
631 for s in scores.iter_mut() {
632 *s /= sum;
633 }
634 }
635 let mut acc = vec![0.0f32; hd];
636 if self.mode.quant_v() {
637 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
638 for p in 0..stored {
639 let w = scores[p] * vs[p];
640 if w.abs() < 1e-12 {
641 continue;
642 }
643 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
644 }
645 let vcol = &self.vcol[kv_head];
646 if !vcol.is_empty() {
647 for d in 0..hd {
648 acc[d] *= vcol[d];
649 }
650 }
651 } else {
652 let v = &self.v[kv_head];
653 for p in 0..stored {
654 let w = scores[p];
655 if w.abs() < 1e-12 {
656 continue;
657 }
658 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
659 }
660 }
661 (acc, scores)
662 }
663
664 #[allow(clippy::too_many_arguments)]
678 pub fn attend_group(
679 &self,
680 q_group: &[f32],
681 kv_head: usize,
682 out: &mut [f32],
683 imp_acc: &mut [f32],
684 scale: f32,
685 first: usize,
686 softcap: f32,
687 ) {
688 let hd = self.head_dim;
689 let nheads = q_group.len() / hd;
690 debug_assert_eq!(out.len(), nheads * hd);
691 let stored = if self.mode == KvMode::F32 {
692 self.k[kv_head].len() / hd
693 } else {
694 self.head_len(kv_head)
695 };
696 if stored == 0 {
697 out.fill(0.0);
698 return;
699 }
700 let first = first.min(stored.saturating_sub(1));
701
702 thread_local! {
703 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
705 const { std::cell::RefCell::new(Vec::new()) };
706 static GQA_QC: std::cell::RefCell<Vec<f32>> =
708 const { std::cell::RefCell::new(Vec::new()) };
709 }
710
711 GQA_SCORES.with(|sc| {
712 let mut scores = sc.borrow_mut();
713 if first > 0 {
714 scores.clear();
717 scores.resize(nheads * stored, f32::NEG_INFINITY);
718 } else {
719 scores.resize(nheads * stored, 0.0);
720 }
721
722 if self.mode.quant_k() {
724 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
725 let kcol = &self.kcol[kv_head];
726 let ng = hd.div_ceil(KV_K_GROUP);
727 GQA_QC.with(|qc| {
728 let mut qcb = qc.borrow_mut();
729 qcb.resize(nheads * hd, 0.0);
730 for h in 0..nheads {
731 for d in 0..hd {
732 let qv = q_group[h * hd + d];
733 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
734 }
735 }
736 for p in first..stored {
737 let row = &kq[p * hd..(p + 1) * hd];
738 let row_u8 = unsafe {
741 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
742 };
743 for h in 0..nheads {
744 let qch = &qcb[h * hd..(h + 1) * hd];
745 let mut dot = 0.0f32;
746 for g in 0..ng {
747 let g0 = g * KV_K_GROUP;
748 let g1 = (g0 + KV_K_GROUP).min(hd);
749 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
750 * ks[p * ng + g];
751 }
752 scores[h * stored + p] = dot * scale;
753 }
754 }
755 });
756 } else {
757 let k = &self.k[kv_head];
758 for p in first..stored {
759 let row = &k[p * hd..(p + 1) * hd];
760 for h in 0..nheads {
761 scores[h * stored + p] =
762 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
763 }
764 }
765 }
766
767 if softcap > 0.0 {
772 for v in scores.iter_mut() {
773 if v.is_finite() {
774 *v = softcap * (*v / softcap).tanh();
775 }
776 }
777 }
778
779 for h in 0..nheads {
781 let s = &mut scores[h * stored..(h + 1) * stored];
782 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
783 let mut sum = 0.0f32;
784 for v in s.iter_mut() {
785 *v = (*v - max_score).exp();
786 sum += *v;
787 }
788 if sum > 0.0 {
789 for v in s.iter_mut() {
790 *v /= sum;
791 }
792 }
793 }
794
795 out.fill(0.0);
797 if self.mode.quant_v() {
798 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
799 for p in first..stored {
800 let row = &vq[p * hd..(p + 1) * hd];
801 for h in 0..nheads {
802 let w = scores[h * stored + p] * vs[p];
803 if w.abs() < 1e-12 {
804 continue;
805 }
806 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
807 }
808 }
809 let vcol = &self.vcol[kv_head];
810 if !vcol.is_empty() {
811 for h in 0..nheads {
812 for d in 0..hd {
813 out[h * hd + d] *= vcol[d];
814 }
815 }
816 }
817 } else {
818 let v = &self.v[kv_head];
819 for p in first..stored {
820 let row = &v[p * hd..(p + 1) * hd];
821 for h in 0..nheads {
822 let w = scores[h * stored + p];
823 if w.abs() < 1e-12 {
824 continue;
825 }
826 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
827 }
828 }
829 }
830
831 let n = imp_acc.len().min(stored);
834 for h in 0..nheads {
835 let s = &scores[h * stored..(h + 1) * stored];
836 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
837 *dst += p;
838 }
839 }
840 });
841 }
842
843 #[cfg(target_arch = "aarch64")]
851 #[allow(clippy::too_many_arguments)]
852 pub fn attend_chunk(
853 &mut self,
854 q_all: &[f32],
855 b: usize,
856 s0: usize,
857 nh: usize,
858 heads_per_kv: usize,
859 hd: usize,
860 out: &mut [f32],
861 pool: Option<&crate::pool::Pool>,
862 scale: f32,
863 window: Option<usize>,
864 ) {
865 let n = s0 + b;
866 struct SendPtr(*mut f32);
867 unsafe impl Send for SendPtr {}
868 unsafe impl Sync for SendPtr {}
869 impl SendPtr {
870 fn at(&self, i: usize) -> *mut f32 {
871 unsafe { self.0.add(i) }
874 }
875 }
876 thread_local! {
877 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
878 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
879 }
880 let neon_gemm = cfg!(not(target_os = "macos"))
885 || std::env::var("CMF_FORCE_NEON_GEMM")
886 .map(|v| v == "1")
887 .unwrap_or(false);
888 SCRATCH.with(|s| {
889 let mut s = s.borrow_mut();
890 let (qpanel, scores, aopanel, ktpack) = &mut *s;
891 let m = heads_per_kv * b;
896 qpanel.resize(m * hd, 0.0);
897 scores.resize(m * n, 0.0);
898 aopanel.resize(m * hd, 0.0);
899 for g in 0..self.num_kv_heads {
900 let kmat = &self.k[g];
901 let vmat = &self.v[g];
902 debug_assert_eq!(kmat.len(), n * hd);
903 for hl in 0..heads_per_kv {
904 let hh = g * heads_per_kv + hl;
905 for bi in 0..b {
906 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
907 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
908 }
909 }
910 if neon_gemm {
911 ktpack.resize(hd * n, 0.0);
912 for p in 0..n {
913 let row = &kmat[p * hd..(p + 1) * hd];
914 for (d, &v) in row.iter().enumerate() {
915 ktpack[d * n + p] = v;
916 }
917 }
918 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
921 let sp_s = SendPtr(scores.as_mut_ptr());
922 let kt = &*ktpack;
923 let run = |start: usize, end: usize| {
924 if end > start {
925 let a = unsafe {
927 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
928 };
929 let c = unsafe {
930 std::slice::from_raw_parts_mut(
931 sp_s.at(start * n),
932 (end - start) * n,
933 )
934 };
935 crate::qtensor::neon_gemm_rm(
936 end - start,
937 n,
938 hd,
939 scale,
940 a,
941 hd,
942 kt,
943 n,
944 false,
945 c,
946 n,
947 );
948 }
949 };
950 match pool {
951 Some(p) if m >= 64 => p.run_rows(m, &run),
952 _ => run(0, m),
953 }
954 } else {
955 crate::qtensor::sgemm_rm(
956 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
957 );
958 }
959 let sp = SendPtr(scores.as_mut_ptr());
961 let run = |start: usize, end: usize| {
962 for r in start..end {
963 let allowed = s0 + (r % b) + 1;
964 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
968 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
970 crate::attention::softmax_row(&mut row[lo..allowed]);
971 row[..lo].fill(0.0);
972 row[allowed..].fill(0.0);
973 }
974 };
975 match pool {
976 Some(p) if m >= 64 => p.run_rows(m, &run),
977 _ => run(0, m),
978 }
979 let ni = self.imp.len().min(n);
983 for r in 0..m {
984 let al = (s0 + (r % b) + 1).min(ni);
985 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
986 *dst += p;
987 }
988 }
989 if neon_gemm {
990 let sp_s = SendPtr(scores.as_mut_ptr());
991 let sp_o = SendPtr(aopanel.as_mut_ptr());
992 let run = |start: usize, end: usize| {
993 if end > start {
994 let a = unsafe {
996 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
997 };
998 let c = unsafe {
999 std::slice::from_raw_parts_mut(
1000 sp_o.at(start * hd),
1001 (end - start) * hd,
1002 )
1003 };
1004 crate::qtensor::neon_gemm_rm(
1005 end - start,
1006 hd,
1007 n,
1008 1.0,
1009 a,
1010 n,
1011 vmat,
1012 hd,
1013 false,
1014 c,
1015 hd,
1016 );
1017 }
1018 };
1019 match pool {
1020 Some(p) if m >= 64 => p.run_rows(m, &run),
1021 _ => run(0, m),
1022 }
1023 } else {
1024 crate::qtensor::sgemm_rm(
1025 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
1026 );
1027 }
1028 for hl in 0..heads_per_kv {
1029 let hh = g * heads_per_kv + hl;
1030 for bi in 0..b {
1031 out[bi * nh * hd + hh * hd..][..hd]
1032 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
1033 }
1034 }
1035 }
1036 });
1037 }
1038
1039 pub fn truncate_last(&mut self, n_drop: usize) {
1041 let d = n_drop.min(self.seq_len);
1042 for h in 0..self.num_kv_heads {
1043 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
1044 self.k[h].truncate(keep);
1045 self.v[h].truncate(keep);
1046 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
1047 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
1048 self.kq[h].truncate(keep_q);
1049 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
1050 self.vq[h].truncate(keep_vq);
1051 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
1052 self.ks[h].truncate(keep_ks);
1053 let keep_vs = self.vs[h].len().saturating_sub(d);
1054 self.vs[h].truncate(keep_vs);
1055 }
1056 self.imp.truncate(self.imp.len().saturating_sub(d));
1057 self.seq_len -= d;
1058 }
1059
1060 pub fn accumulate_imp(&mut self, probs: &[f32]) {
1062 for (dst, &p) in self.imp.iter_mut().zip(probs) {
1063 *dst += p;
1064 }
1065 }
1066
1067 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
1069 &self.k[kv_head]
1070 }
1071
1072 pub fn head_values(&self, kv_head: usize) -> &[f32] {
1073 &self.v[kv_head]
1074 }
1075
1076 pub fn head_len(&self, kv_head: usize) -> usize {
1078 let ng = self.head_dim.div_ceil(KV_K_GROUP);
1079 (self.k[kv_head].len() / self.head_dim)
1080 .max(self.ks[kv_head].len() / ng)
1081 .max(self.vs[kv_head].len())
1082 }
1083
1084 pub fn clear(&mut self) {
1086 for h in 0..self.num_kv_heads {
1087 self.k[h].clear();
1088 self.v[h].clear();
1089 self.kq[h].clear();
1090 self.ks[h].clear();
1091 self.vq[h].clear();
1092 self.vs[h].clear();
1093 self.kcol[h].clear();
1094 self.vcol[h].clear();
1095 }
1096 self.imp.clear();
1097 self.linear_state.clear();
1098 self.linear_scratch.clear();
1099 self.o1 = None;
1102 self.o1_error = None;
1103 self.o1_transitioned = false;
1104 self.seq_len = 0;
1105 }
1106
1107 pub fn export_wire(&self, f16: bool) -> Result<Vec<u8>, String> {
1117 if !matches!(self.mode, KvMode::F32) {
1118 return Err("kv export: only the F32 cache is described by this format (CMF_KV=q8 stores int8 rows and per-row scales)"
1119 .into());
1120 }
1121 if self.o1.is_some() {
1122 return Err("kv export: an O(1) Nyström overlay is not part of this format — the skeletons are irreversible and would have to travel with it"
1123 .into());
1124 }
1125 if self.kcol.iter().any(|c| !c.is_empty()) || self.vcol.iter().any(|c| !c.is_empty()) {
1129 return Err(
1130 "kv export: frozen columns under an F32 cache — refusing to ship \
1131 a state this format does not describe"
1132 .into(),
1133 );
1134 }
1135 let mut out = Vec::with_capacity(self.memory_bytes() / if f16 { 2 } else { 1 } + 64);
1136 let u = |v: u32, o: &mut Vec<u8>| o.extend_from_slice(&v.to_le_bytes());
1137 u(u8::from(f16) as u32, &mut out);
1138 u(self.seq_len as u32, &mut out);
1139 u(self.num_kv_heads as u32, &mut out);
1140 u(self.head_dim as u32, &mut out);
1141 u(self.linear_state.len() as u32, &mut out);
1142 u(self.imp.len() as u32, &mut out);
1147 let push = |xs: &[f32], o: &mut Vec<u8>| {
1148 if f16 {
1149 for &x in xs {
1150 o.extend_from_slice(&cortiq_core::quant::f32_to_f16(x).to_le_bytes());
1151 }
1152 } else {
1153 for &x in xs {
1154 o.extend_from_slice(&x.to_le_bytes());
1155 }
1156 }
1157 };
1158 for &x in &self.linear_state {
1162 out.extend_from_slice(&x.to_le_bytes());
1163 }
1164 for &x in &self.imp {
1165 out.extend_from_slice(&x.to_le_bytes());
1166 }
1167 for h in 0..self.num_kv_heads {
1168 u(self.k[h].len() as u32, &mut out);
1169 push(&self.k[h], &mut out);
1170 u(self.v[h].len() as u32, &mut out);
1171 push(&self.v[h], &mut out);
1172 }
1173 Ok(out)
1174 }
1175
1176 pub fn import_wire(&mut self, buf: &[u8]) -> Result<(), String> {
1179 let mut o = 0usize;
1180 let u32_at = |o: &mut usize| -> Result<u32, String> {
1181 if *o + 4 > buf.len() {
1182 return Err("kv import: truncated header".into());
1183 }
1184 let v = u32::from_le_bytes(buf[*o..*o + 4].try_into().unwrap());
1185 *o += 4;
1186 Ok(v)
1187 };
1188 let f16 = u32_at(&mut o)? != 0;
1189 let seq_len = u32_at(&mut o)? as usize;
1190 let heads = u32_at(&mut o)? as usize;
1191 let hd = u32_at(&mut o)? as usize;
1192 let lin = u32_at(&mut o)? as usize;
1193 let nimp = u32_at(&mut o)? as usize;
1194 if heads != self.num_kv_heads || hd != self.head_dim {
1195 return Err(format!(
1196 "kv import: peer sent {heads}×{hd} per position, this layer is {}×{}",
1197 self.num_kv_heads, self.head_dim
1198 ));
1199 }
1200 let w = if f16 { 2 } else { 4 };
1201 let need = |n: usize, o: usize| -> Result<(), String> {
1202 if o + n > buf.len() {
1203 Err("kv import: truncated payload".into())
1204 } else {
1205 Ok(())
1206 }
1207 };
1208 need(lin * 4, o)?;
1209 self.linear_state = (0..lin)
1210 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1211 .collect();
1212 o += lin * 4;
1213 need(nimp * 4, o)?;
1214 let imp: Vec<f32> = (0..nimp)
1215 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1216 .collect();
1217 o += nimp * 4;
1218 let mut k: Vec<Vec<f32>> = Vec::with_capacity(heads);
1219 let mut v: Vec<Vec<f32>> = Vec::with_capacity(heads);
1220 for _ in 0..heads {
1221 for which in 0..2 {
1222 let n = u32_at(&mut o)? as usize;
1223 need(n * w, o)?;
1224 let xs: Vec<f32> = (0..n)
1225 .map(|i| {
1226 let at = o + i * w;
1227 if f16 {
1228 cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
1229 buf[at..at + 2].try_into().unwrap(),
1230 ))
1231 } else {
1232 f32::from_le_bytes(buf[at..at + 4].try_into().unwrap())
1233 }
1234 })
1235 .collect();
1236 o += n * w;
1237 if which == 0 { k.push(xs) } else { v.push(xs) }
1238 }
1239 }
1240 self.mode = KvMode::F32;
1241 self.k = k;
1242 self.v = v;
1243 self.kq = vec![Vec::new(); heads];
1244 self.ks = vec![Vec::new(); heads];
1245 self.vq = vec![Vec::new(); heads];
1246 self.vs = vec![Vec::new(); heads];
1247 self.kcol = vec![Vec::new(); heads];
1248 self.vcol = vec![Vec::new(); heads];
1249 self.imp = imp;
1250 self.o1 = None;
1251 self.o1_error = None;
1252 self.o1_transitioned = false;
1253 self.seq_len = seq_len;
1254 Ok(())
1255 }
1256
1257 pub fn memory_bytes(&self) -> usize {
1258 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
1259 + self.v.iter().map(Vec::len).sum::<usize>()
1260 + self.ks.iter().map(Vec::len).sum::<usize>()
1261 + self.vs.iter().map(Vec::len).sum::<usize>()
1262 + self.kcol.iter().map(Vec::len).sum::<usize>()
1263 + self.vcol.iter().map(Vec::len).sum::<usize>();
1264 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
1265 + self.vq.iter().map(Vec::len).sum::<usize>();
1266 floats * std::mem::size_of::<f32>()
1267 + bytes
1268 + self.linear_state.len() * std::mem::size_of::<f32>()
1272 + self.o1_memory_bytes()
1275 }
1276
1277 fn evict(&mut self, keep_last: usize) {
1279 if self.o1.is_some() || self.seq_len <= keep_last {
1287 return;
1288 }
1289 let drop = self.seq_len - keep_last;
1290 for h in 0..self.num_kv_heads {
1291 let stored = self.head_len(h);
1293 let d = drop.min(stored);
1294 let hd = self.head_dim;
1295 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
1296 let n = n.min(v.len());
1297 v.drain(..n);
1298 }
1299 drop_front(&mut self.k[h], d * hd);
1300 drop_front(&mut self.v[h], d * hd);
1301 drop_front(&mut self.kq[h], d * hd);
1302 drop_front(&mut self.vq[h], d * hd);
1303 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
1304 drop_front(&mut self.vs[h], d);
1305 }
1306 let d = drop.min(self.imp.len());
1307 self.imp.drain(..d);
1308 self.seq_len = keep_last;
1309 }
1310
1311 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
1316 if self.o1.is_some() {
1317 return;
1320 }
1321 let stored = self.imp.len();
1322 if stored <= keep_last {
1323 return;
1324 }
1325 let sink_n = sink.min(keep_last);
1328 let recent_n = recent.min(keep_last - sink_n);
1329 let mut keep = vec![false; stored];
1330 for k in keep.iter_mut().take(sink_n) {
1331 *k = true;
1332 }
1333 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1334 *k = true;
1335 }
1336 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1337 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1339 order.sort_by(|&a, &b| {
1340 self.imp[b]
1341 .partial_cmp(&self.imp[a])
1342 .unwrap_or(std::cmp::Ordering::Equal)
1343 });
1344 for i in order {
1345 if budget == 0 {
1346 break;
1347 }
1348 keep[i] = true;
1349 budget -= 1;
1350 }
1351
1352 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1353 let hd = self.head_dim;
1354 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1355 let mut out = Vec::with_capacity(kept.len() * step);
1356 for &i in kept {
1357 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1358 }
1359 out
1360 }
1361 for h in 0..self.num_kv_heads {
1366 if !self.k[h].is_empty() {
1367 self.k[h] = gather(&self.k[h], &kept, hd);
1368 }
1369 if !self.v[h].is_empty() {
1370 self.v[h] = gather(&self.v[h], &kept, hd);
1371 }
1372 if !self.kq[h].is_empty() {
1373 self.kq[h] = gather(&self.kq[h], &kept, hd);
1374 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1375 }
1376 if !self.vq[h].is_empty() {
1377 self.vq[h] = gather(&self.vq[h], &kept, hd);
1378 self.vs[h] = gather(&self.vs[h], &kept, 1);
1379 }
1380 }
1381 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1382 self.seq_len = kept.len();
1383 }
1384}
1385
1386#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1388pub enum EvictionPolicy {
1389 Recent,
1391 Born { sink: usize },
1393}
1394
1395#[derive(Debug)]
1397pub struct KvCache {
1398 pub layers: Vec<LayerKvCache>,
1399 pub max_seq_len: usize,
1400 pub policy: EvictionPolicy,
1401}
1402
1403impl KvCache {
1404 pub fn new(
1405 num_layers: usize,
1406 num_kv_heads: usize,
1407 head_dim: usize,
1408 max_seq_len: usize,
1409 ) -> Self {
1410 let layers = (0..num_layers)
1411 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1412 .collect();
1413 Self {
1414 layers,
1415 max_seq_len,
1416 policy: EvictionPolicy::Born { sink: 4 },
1417 }
1418 }
1419
1420 pub fn clear(&mut self) {
1421 for layer in &mut self.layers {
1422 layer.clear();
1423 }
1424 }
1425
1426 pub fn total_memory_bytes(&self) -> usize {
1427 self.layers.iter().map(|l| l.memory_bytes()).sum()
1428 }
1429
1430 pub fn seq_len(&self) -> usize {
1432 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1433 }
1434
1435 pub fn needs_eviction(&self) -> bool {
1436 self.seq_len() >= self.max_seq_len
1437 }
1438
1439 pub fn evict(&mut self, keep_last: usize) {
1441 match self.policy {
1442 EvictionPolicy::Recent => {
1443 for layer in &mut self.layers {
1444 layer.evict(keep_last);
1445 }
1446 }
1447 EvictionPolicy::Born { sink } => {
1448 let recent = (keep_last / 2).max(1);
1449 for layer in &mut self.layers {
1450 layer.evict_born(keep_last, sink, recent);
1451 }
1452 }
1453 }
1454 }
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459 use super::*;
1460
1461 #[test]
1462 fn wire_round_trip_reproduces_attention() {
1463 let (heads, hd) = (2usize, 4usize);
1467 let mut a = LayerKvCache::new(heads, hd);
1468 for p in 0..5 {
1469 let k: Vec<f32> = (0..heads * hd)
1470 .map(|i| (p * 10 + i) as f32 * 0.031)
1471 .collect();
1472 let v: Vec<f32> = (0..heads * hd)
1473 .map(|i| (p * 7 + i) as f32 * -0.017)
1474 .collect();
1475 a.append(&k, &v, &[true, true]);
1476 }
1477 a.linear_state = vec![0.5, -0.25, 1.0];
1478 let q: Vec<f32> = (0..hd).map(|i| 0.1 * (i as f32 + 1.0)).collect();
1479
1480 let bytes = a.export_wire(false).expect("f32 cache exports");
1481 let mut b = LayerKvCache::new(heads, hd);
1482 b.import_wire(&bytes).expect("import");
1483
1484 assert_eq!(b.seq_len, a.seq_len);
1485 assert_eq!(b.linear_state, a.linear_state);
1486 for h in 0..heads {
1487 let (oa, sa) = a.attend(&q, h);
1488 let (ob, sb) = b.attend(&q, h);
1489 assert_eq!(oa, ob, "head {h} attention output diverged");
1490 assert_eq!(sa, sb, "head {h} attention scores diverged");
1491 }
1492 }
1493
1494 #[test]
1495 fn wire_refuses_what_it_cannot_describe() {
1496 let mut c = LayerKvCache::new(1, 4);
1499 c.mode = KvMode::Q8 { k: true, v: true };
1500 let err = c.export_wire(false).unwrap_err();
1501 assert!(err.contains("F32"), "{err}");
1502 }
1503
1504 #[test]
1505 fn wire_import_checks_geometry() {
1506 let a = LayerKvCache::new(2, 4);
1507 let bytes = a.export_wire(false).unwrap();
1508 let mut wrong = LayerKvCache::new(2, 8);
1509 let err = wrong.import_wire(&bytes).unwrap_err();
1510 assert!(err.contains("2×4"), "{err}");
1511 }
1512
1513 #[test]
1514 fn append_tracks_seq_len_and_layout() {
1515 let mut cache = LayerKvCache::new(4, 8);
1516 cache.mode = KvMode::F32;
1517 assert_eq!(cache.seq_len, 0);
1518
1519 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1520 let v = vec![2.0f32; 32];
1521 cache.append(&k, &v, &[true; 4]);
1522
1523 assert_eq!(cache.seq_len, 1);
1524 assert_eq!(cache.head_len(0), 1);
1525 assert_eq!(cache.head_keys(1), &k[8..16]);
1527 assert_eq!(cache.memory_bytes(), 256);
1528 }
1529
1530 #[test]
1531 fn dead_head_stores_nothing() {
1532 let mut cache = LayerKvCache::new(2, 4);
1533 cache.mode = KvMode::F32;
1534 let k = vec![1.0f32; 8];
1535 let v = vec![2.0f32; 8];
1536 cache.append(&k, &v, &[true, false]);
1537 cache.append(&k, &v, &[true, false]);
1538
1539 assert_eq!(cache.seq_len, 2);
1540 assert_eq!(cache.head_len(0), 2);
1541 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1542 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1543 }
1544
1545 #[test]
1546 fn eviction_keeps_recent() {
1547 let mut cache = KvCache::new(2, 4, 8, 10);
1548 cache.policy = EvictionPolicy::Recent;
1549 for l in &mut cache.layers {
1550 l.mode = KvMode::F32;
1551 }
1552 let k = vec![1.0f32; 32];
1553 let v = vec![2.0f32; 32];
1554 for _ in 0..8 {
1555 for layer in &mut cache.layers {
1556 layer.append(&k, &v, &[true; 4]);
1557 }
1558 }
1559 assert_eq!(cache.seq_len(), 8);
1560 assert!(!cache.needs_eviction());
1561
1562 cache.evict(4);
1563 assert_eq!(cache.seq_len(), 4);
1564 assert_eq!(cache.layers[0].head_len(0), 4);
1565 }
1566
1567 #[test]
1568 fn collecting_o1_eviction_retains_exact_storage_until_boundary() {
1569 const B: usize = 19;
1570 let q = vec![0.1f32; 8];
1571 let k = vec![0.2f32; 4];
1572 let v = vec![0.3f32; 4];
1573
1574 for policy in [EvictionPolicy::Recent, EvictionPolicy::Born { sink: 2 }] {
1575 let mut cache = KvCache::new(1, 1, 4, 6);
1576 cache.policy = policy;
1577 cache.layers[0].mode = KvMode::F32;
1578 cache.layers[0].o1_begin_with_boundary(
1579 4,
1580 8,
1581 2,
1582 crate::nystrom::O1Rect::Aggregate,
1583 Some(B),
1584 );
1585
1586 for pos in 0..B {
1587 {
1588 let layer = &mut cache.layers[0];
1589 layer.o1_push_q(&q);
1590 layer.append(&k, &v, &[]);
1591 }
1592 if pos + 1 < B {
1593 cache.evict(3);
1594 }
1595 }
1596
1597 let layer = &cache.layers[0];
1598 let rows = B * layer.head_dim;
1599 assert_eq!(layer.seq_len, B, "policy {policy:?} retained depth");
1600 assert_eq!(layer.k[0].len(), rows, "policy {policy:?} K rows");
1601 assert_eq!(layer.v[0].len(), rows, "policy {policy:?} V rows");
1602 assert!(
1603 layer.k[0].capacity() >= rows,
1604 "policy {policy:?} K capacity"
1605 );
1606 assert!(
1607 layer.v[0].capacity() >= rows,
1608 "policy {policy:?} V capacity"
1609 );
1610 let q_capacity = match layer.o1.as_ref() {
1611 Some(O1State::Collecting { q_buf, .. }) => q_buf.capacity(),
1612 other => panic!("policy {policy:?} changed state early: {other:?}"),
1613 };
1614 assert!(
1615 q_capacity >= B * 8,
1616 "policy {policy:?} Q capacity must cover the exact prefix"
1617 );
1618
1619 assert!(cache.layers[0].o1_seal_checked(2).unwrap());
1620 assert_eq!(cache.layers[0].k[0].capacity(), 0, "K released after seal");
1621 assert_eq!(cache.layers[0].v[0].capacity(), 0, "V released after seal");
1622 }
1623 }
1624
1625 #[test]
1626 fn truncate_rolls_back_speculative_positions() {
1627 let mut cache = LayerKvCache::new(2, 4);
1628 cache.mode = KvMode::F32;
1629 for pos in 0..5 {
1630 let k = vec![pos as f32; 8];
1631 let v = vec![pos as f32; 8];
1632 cache.append(&k, &v, &[true; 2]);
1633 }
1634 cache.truncate_last(2);
1635 assert_eq!(cache.seq_len, 3);
1636 assert_eq!(cache.head_len(0), 3);
1637 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1638 }
1639
1640 #[test]
1644 fn q8_attend_matches_f32_within_grid() {
1645 let (heads, hd) = (2, 32);
1646 let mut f = LayerKvCache::new(heads, hd);
1647 f.mode = KvMode::F32;
1648 let mut q8 = LayerKvCache::new(heads, hd);
1649 q8.mode = KvMode::Q8 { k: true, v: true };
1650
1651 let synth = |p: usize, salt: usize| -> Vec<f32> {
1652 (0..heads * hd)
1653 .map(|i| {
1654 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1655 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1657 })
1658 .collect()
1659 };
1660 for p in 0..100 {
1661 let k = synth(p, 1);
1662 let v = synth(p, 2);
1663 f.append(&k, &v, &[true; 2]);
1664 q8.append(&k, &v, &[true; 2]);
1665 }
1666 let q: Vec<f32> = (0..hd)
1667 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1668 .collect();
1669 for g in 0..heads {
1670 let (of, pf) = f.attend(&q, g);
1671 let (o8, p8) = q8.attend(&q, g);
1672 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1673 for d in 0..hd {
1674 assert!(
1675 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1676 "g{g} d{d}: f32 {} vs q8 {}",
1677 of[d],
1678 o8[d]
1679 );
1680 }
1681 for p in 0..100 {
1682 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1683 }
1684 }
1685 q8.truncate_last(30);
1687 assert_eq!(q8.head_len(0), 70);
1688 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1689 q8.accumulate_imp(&imp);
1690 q8.evict_born(20, 2, 8);
1691 assert_eq!(q8.head_len(0), 20);
1692 let (o, _) = q8.attend(&q, 0);
1693 assert!(o.iter().all(|x| x.is_finite()));
1694 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1696 }
1697
1698 #[test]
1701 fn attend_group_equals_per_head_attend_bitexact() {
1702 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1704 let mut c = LayerKvCache::new(kv_heads, hd);
1705 c.mode = mode;
1706 for p in 0..70 {
1707 let k: Vec<f32> = (0..kv_heads * hd)
1708 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1709 .collect();
1710 let v: Vec<f32> = (0..kv_heads * hd)
1711 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1712 .collect();
1713 c.append(&k, &v, &[true; 2]);
1714 }
1715 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1716 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1717 .collect();
1718 for g in 0..kv_heads {
1719 let span = g * hpk * hd..(g + 1) * hpk * hd;
1720 let mut out = vec![0f32; hpk * hd];
1721 let mut imp = vec![0f32; 70];
1722 c.attend_group(
1723 &q[span.clone()],
1724 g,
1725 &mut out,
1726 &mut imp,
1727 1.0 / (hd as f32).sqrt(),
1728 0,
1729 0.0,
1730 );
1731 let mut imp_ref = vec![0f32; 70];
1732 for h in 0..hpk {
1733 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1734 let (o, probs) = c.attend(qh, g);
1735 assert_eq!(
1736 &out[h * hd..(h + 1) * hd],
1737 &o[..],
1738 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1739 );
1740 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1741 *dst += p;
1742 }
1743 }
1744 assert_eq!(
1745 imp, imp_ref,
1746 "mode {mode:?} g{g}: attention mass must match"
1747 );
1748 }
1749 }
1750 }
1751
1752 #[test]
1756 fn born_eviction_mixed_modes_stay_consistent() {
1757 for (mk, mv) in [(false, true), (true, false)] {
1758 let mut c = LayerKvCache::new(1, 4);
1759 c.mode = KvMode::Q8 { k: mk, v: mv };
1760 for p in 0..80 {
1761 let k = vec![p as f32 * 0.01; 4];
1762 let v = vec![p as f32; 4];
1763 c.append(&k, &v, &[true]);
1764 }
1765 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1766 c.accumulate_imp(&imp);
1767 let before = c.memory_bytes();
1768 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1770 assert!(
1771 c.memory_bytes() < before / 2,
1772 "memory must shrink (k={mk} v={mv})"
1773 );
1774 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1777 assert!(
1778 out[0] > 30.0,
1779 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1780 out[0]
1781 );
1782 }
1783 }
1784
1785 #[test]
1786 fn born_eviction_keeps_high_mass_position() {
1787 let mut cache = KvCache::new(1, 1, 2, 16);
1788 cache.policy = EvictionPolicy::Born { sink: 1 };
1789 for l in &mut cache.layers {
1790 l.mode = KvMode::F32;
1791 }
1792 let layer = &mut cache.layers[0];
1793 for pos in 0..8 {
1796 let k = vec![pos as f32; 2];
1797 let v = vec![pos as f32 + 100.0; 2];
1798 layer.append(&k, &v, &[true]);
1799 }
1800 let mut imp = vec![0.05f32; 8];
1802 imp[3] = 5.0;
1803 layer.accumulate_imp(&imp);
1804
1805 cache.evict(4); let layer = &cache.layers[0];
1807 assert_eq!(layer.seq_len, 4);
1808 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1809 assert_eq!(
1810 kept_keys,
1811 vec![0.0, 3.0, 6.0, 7.0],
1812 "kept = sink(0) + mass-top(3) + recent(6,7)"
1813 );
1814 assert_eq!(layer.head_len(0), 4);
1816 }
1817}