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 self.attend_group_upto(q_group, kv_head, out, imp_acc, scale, first, softcap, usize::MAX)
689 }
690
691 #[allow(clippy::too_many_arguments)]
697 pub fn attend_group_upto(
698 &self,
699 q_group: &[f32],
700 kv_head: usize,
701 out: &mut [f32],
702 imp_acc: &mut [f32],
703 scale: f32,
704 first: usize,
705 softcap: f32,
706 upto: usize,
707 ) {
708 let hd = self.head_dim;
709 let nheads = q_group.len() / hd;
710 debug_assert_eq!(out.len(), nheads * hd);
711 let stored = if self.mode == KvMode::F32 {
712 self.k[kv_head].len() / hd
713 } else {
714 self.head_len(kv_head)
715 }
716 .min(upto);
717 if stored == 0 {
718 out.fill(0.0);
719 return;
720 }
721 let first = first.min(stored.saturating_sub(1));
722
723 thread_local! {
724 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
726 const { std::cell::RefCell::new(Vec::new()) };
727 static GQA_QC: std::cell::RefCell<Vec<f32>> =
729 const { std::cell::RefCell::new(Vec::new()) };
730 }
731
732 GQA_SCORES.with(|sc| {
733 let mut scores = sc.borrow_mut();
734 if first > 0 {
735 scores.clear();
738 scores.resize(nheads * stored, f32::NEG_INFINITY);
739 } else {
740 scores.resize(nheads * stored, 0.0);
741 }
742
743 if self.mode.quant_k() {
745 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
746 let kcol = &self.kcol[kv_head];
747 let ng = hd.div_ceil(KV_K_GROUP);
748 GQA_QC.with(|qc| {
749 let mut qcb = qc.borrow_mut();
750 qcb.resize(nheads * hd, 0.0);
751 for h in 0..nheads {
752 for d in 0..hd {
753 let qv = q_group[h * hd + d];
754 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
755 }
756 }
757 for p in first..stored {
758 let row = &kq[p * hd..(p + 1) * hd];
759 let row_u8 = unsafe {
762 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
763 };
764 for h in 0..nheads {
765 let qch = &qcb[h * hd..(h + 1) * hd];
766 let mut dot = 0.0f32;
767 for g in 0..ng {
768 let g0 = g * KV_K_GROUP;
769 let g1 = (g0 + KV_K_GROUP).min(hd);
770 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
771 * ks[p * ng + g];
772 }
773 scores[h * stored + p] = dot * scale;
774 }
775 }
776 });
777 } else {
778 let k = &self.k[kv_head];
779 for p in first..stored {
780 let row = &k[p * hd..(p + 1) * hd];
781 for h in 0..nheads {
782 scores[h * stored + p] =
783 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
784 }
785 }
786 }
787
788 if softcap > 0.0 {
793 for v in scores.iter_mut() {
794 if v.is_finite() {
795 *v = softcap * (*v / softcap).tanh();
796 }
797 }
798 }
799
800 for h in 0..nheads {
802 let s = &mut scores[h * stored..(h + 1) * stored];
803 let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
804 let mut sum = 0.0f32;
805 for v in s.iter_mut() {
806 *v = (*v - max_score).exp();
807 sum += *v;
808 }
809 if sum > 0.0 {
810 for v in s.iter_mut() {
811 *v /= sum;
812 }
813 }
814 }
815
816 out.fill(0.0);
818 if self.mode.quant_v() {
819 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
820 for p in first..stored {
821 let row = &vq[p * hd..(p + 1) * hd];
822 for h in 0..nheads {
823 let w = scores[h * stored + p] * vs[p];
824 if w.abs() < 1e-12 {
825 continue;
826 }
827 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
828 }
829 }
830 let vcol = &self.vcol[kv_head];
831 if !vcol.is_empty() {
832 for h in 0..nheads {
833 for d in 0..hd {
834 out[h * hd + d] *= vcol[d];
835 }
836 }
837 }
838 } else {
839 let v = &self.v[kv_head];
840 for p in first..stored {
841 let row = &v[p * hd..(p + 1) * hd];
842 for h in 0..nheads {
843 let w = scores[h * stored + p];
844 if w.abs() < 1e-12 {
845 continue;
846 }
847 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
848 }
849 }
850 }
851
852 let n = imp_acc.len().min(stored);
855 for h in 0..nheads {
856 let s = &scores[h * stored..(h + 1) * stored];
857 for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
858 *dst += p;
859 }
860 }
861 });
862 }
863
864 #[cfg(target_arch = "aarch64")]
872 #[allow(clippy::too_many_arguments)]
873 pub fn attend_chunk(
874 &mut self,
875 q_all: &[f32],
876 b: usize,
877 s0: usize,
878 nh: usize,
879 heads_per_kv: usize,
880 hd: usize,
881 out: &mut [f32],
882 pool: Option<&crate::pool::Pool>,
883 scale: f32,
884 window: Option<usize>,
885 ) {
886 let n = s0 + b;
887 struct SendPtr(*mut f32);
888 unsafe impl Send for SendPtr {}
889 unsafe impl Sync for SendPtr {}
890 impl SendPtr {
891 fn at(&self, i: usize) -> *mut f32 {
892 unsafe { self.0.add(i) }
895 }
896 }
897 thread_local! {
898 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
899 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
900 }
901 let neon_gemm = cfg!(not(target_os = "macos"))
906 || std::env::var("CMF_FORCE_NEON_GEMM")
907 .map(|v| v == "1")
908 .unwrap_or(false);
909 SCRATCH.with(|s| {
910 let mut s = s.borrow_mut();
911 let (qpanel, scores, aopanel, ktpack) = &mut *s;
912 let m = heads_per_kv * b;
917 qpanel.resize(m * hd, 0.0);
918 scores.resize(m * n, 0.0);
919 aopanel.resize(m * hd, 0.0);
920 for g in 0..self.num_kv_heads {
921 let kmat = &self.k[g];
922 let vmat = &self.v[g];
923 debug_assert_eq!(kmat.len(), n * hd);
924 for hl in 0..heads_per_kv {
925 let hh = g * heads_per_kv + hl;
926 for bi in 0..b {
927 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
928 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
929 }
930 }
931 if neon_gemm {
932 ktpack.resize(hd * n, 0.0);
933 for p in 0..n {
934 let row = &kmat[p * hd..(p + 1) * hd];
935 for (d, &v) in row.iter().enumerate() {
936 ktpack[d * n + p] = v;
937 }
938 }
939 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
942 let sp_s = SendPtr(scores.as_mut_ptr());
943 let kt = &*ktpack;
944 let run = |start: usize, end: usize| {
945 if end > start {
946 let a = unsafe {
948 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
949 };
950 let c = unsafe {
951 std::slice::from_raw_parts_mut(
952 sp_s.at(start * n),
953 (end - start) * n,
954 )
955 };
956 crate::qtensor::neon_gemm_rm(
957 end - start,
958 n,
959 hd,
960 scale,
961 a,
962 hd,
963 kt,
964 n,
965 false,
966 c,
967 n,
968 );
969 }
970 };
971 match pool {
972 Some(p) if m >= 64 => p.run_rows(m, &run),
973 _ => run(0, m),
974 }
975 } else {
976 crate::qtensor::sgemm_rm(
977 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
978 );
979 }
980 let sp = SendPtr(scores.as_mut_ptr());
982 let run = |start: usize, end: usize| {
983 for r in start..end {
984 let allowed = s0 + (r % b) + 1;
985 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
989 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
991 crate::attention::softmax_row(&mut row[lo..allowed]);
992 row[..lo].fill(0.0);
993 row[allowed..].fill(0.0);
994 }
995 };
996 match pool {
997 Some(p) if m >= 64 => p.run_rows(m, &run),
998 _ => run(0, m),
999 }
1000 let ni = self.imp.len().min(n);
1004 for r in 0..m {
1005 let al = (s0 + (r % b) + 1).min(ni);
1006 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
1007 *dst += p;
1008 }
1009 }
1010 if neon_gemm {
1011 let sp_s = SendPtr(scores.as_mut_ptr());
1012 let sp_o = SendPtr(aopanel.as_mut_ptr());
1013 let run = |start: usize, end: usize| {
1014 if end > start {
1015 let a = unsafe {
1017 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
1018 };
1019 let c = unsafe {
1020 std::slice::from_raw_parts_mut(
1021 sp_o.at(start * hd),
1022 (end - start) * hd,
1023 )
1024 };
1025 crate::qtensor::neon_gemm_rm(
1026 end - start,
1027 hd,
1028 n,
1029 1.0,
1030 a,
1031 n,
1032 vmat,
1033 hd,
1034 false,
1035 c,
1036 hd,
1037 );
1038 }
1039 };
1040 match pool {
1041 Some(p) if m >= 64 => p.run_rows(m, &run),
1042 _ => run(0, m),
1043 }
1044 } else {
1045 crate::qtensor::sgemm_rm(
1046 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
1047 );
1048 }
1049 for hl in 0..heads_per_kv {
1050 let hh = g * heads_per_kv + hl;
1051 for bi in 0..b {
1052 out[bi * nh * hd + hh * hd..][..hd]
1053 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
1054 }
1055 }
1056 }
1057 });
1058 }
1059
1060 pub fn truncate_last(&mut self, n_drop: usize) {
1062 let d = n_drop.min(self.seq_len);
1063 for h in 0..self.num_kv_heads {
1064 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
1065 self.k[h].truncate(keep);
1066 self.v[h].truncate(keep);
1067 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
1068 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
1069 self.kq[h].truncate(keep_q);
1070 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
1071 self.vq[h].truncate(keep_vq);
1072 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
1073 self.ks[h].truncate(keep_ks);
1074 let keep_vs = self.vs[h].len().saturating_sub(d);
1075 self.vs[h].truncate(keep_vs);
1076 }
1077 self.imp.truncate(self.imp.len().saturating_sub(d));
1078 self.seq_len -= d;
1079 }
1080
1081 pub fn accumulate_imp(&mut self, probs: &[f32]) {
1083 for (dst, &p) in self.imp.iter_mut().zip(probs) {
1084 *dst += p;
1085 }
1086 }
1087
1088 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
1090 &self.k[kv_head]
1091 }
1092
1093 pub fn head_values(&self, kv_head: usize) -> &[f32] {
1094 &self.v[kv_head]
1095 }
1096
1097 pub fn head_len(&self, kv_head: usize) -> usize {
1099 let ng = self.head_dim.div_ceil(KV_K_GROUP);
1100 (self.k[kv_head].len() / self.head_dim)
1101 .max(self.ks[kv_head].len() / ng)
1102 .max(self.vs[kv_head].len())
1103 }
1104
1105 pub fn clear(&mut self) {
1107 for h in 0..self.num_kv_heads {
1108 self.k[h].clear();
1109 self.v[h].clear();
1110 self.kq[h].clear();
1111 self.ks[h].clear();
1112 self.vq[h].clear();
1113 self.vs[h].clear();
1114 self.kcol[h].clear();
1115 self.vcol[h].clear();
1116 }
1117 self.imp.clear();
1118 self.linear_state.clear();
1119 self.linear_scratch.clear();
1120 self.o1 = None;
1123 self.o1_error = None;
1124 self.o1_transitioned = false;
1125 self.seq_len = 0;
1126 }
1127
1128 pub fn export_wire(&self, f16: bool) -> Result<Vec<u8>, String> {
1138 if !matches!(self.mode, KvMode::F32) {
1139 return Err("kv export: only the F32 cache is described by this format (CMF_KV=q8 stores int8 rows and per-row scales)"
1140 .into());
1141 }
1142 if self.o1.is_some() {
1143 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"
1144 .into());
1145 }
1146 if self.kcol.iter().any(|c| !c.is_empty()) || self.vcol.iter().any(|c| !c.is_empty()) {
1150 return Err(
1151 "kv export: frozen columns under an F32 cache — refusing to ship \
1152 a state this format does not describe"
1153 .into(),
1154 );
1155 }
1156 let mut out = Vec::with_capacity(self.memory_bytes() / if f16 { 2 } else { 1 } + 64);
1157 let u = |v: u32, o: &mut Vec<u8>| o.extend_from_slice(&v.to_le_bytes());
1158 u(u8::from(f16) as u32, &mut out);
1159 u(self.seq_len as u32, &mut out);
1160 u(self.num_kv_heads as u32, &mut out);
1161 u(self.head_dim as u32, &mut out);
1162 u(self.linear_state.len() as u32, &mut out);
1163 u(self.imp.len() as u32, &mut out);
1168 let push = |xs: &[f32], o: &mut Vec<u8>| {
1169 if f16 {
1170 for &x in xs {
1171 o.extend_from_slice(&cortiq_core::quant::f32_to_f16(x).to_le_bytes());
1172 }
1173 } else {
1174 for &x in xs {
1175 o.extend_from_slice(&x.to_le_bytes());
1176 }
1177 }
1178 };
1179 for &x in &self.linear_state {
1183 out.extend_from_slice(&x.to_le_bytes());
1184 }
1185 for &x in &self.imp {
1186 out.extend_from_slice(&x.to_le_bytes());
1187 }
1188 for h in 0..self.num_kv_heads {
1189 u(self.k[h].len() as u32, &mut out);
1190 push(&self.k[h], &mut out);
1191 u(self.v[h].len() as u32, &mut out);
1192 push(&self.v[h], &mut out);
1193 }
1194 Ok(out)
1195 }
1196
1197 pub fn import_wire(&mut self, buf: &[u8]) -> Result<(), String> {
1200 let mut o = 0usize;
1201 let u32_at = |o: &mut usize| -> Result<u32, String> {
1202 if *o + 4 > buf.len() {
1203 return Err("kv import: truncated header".into());
1204 }
1205 let v = u32::from_le_bytes(buf[*o..*o + 4].try_into().unwrap());
1206 *o += 4;
1207 Ok(v)
1208 };
1209 let f16 = u32_at(&mut o)? != 0;
1210 let seq_len = u32_at(&mut o)? as usize;
1211 let heads = u32_at(&mut o)? as usize;
1212 let hd = u32_at(&mut o)? as usize;
1213 let lin = u32_at(&mut o)? as usize;
1214 let nimp = u32_at(&mut o)? as usize;
1215 if heads != self.num_kv_heads || hd != self.head_dim {
1216 return Err(format!(
1217 "kv import: peer sent {heads}×{hd} per position, this layer is {}×{}",
1218 self.num_kv_heads, self.head_dim
1219 ));
1220 }
1221 let w = if f16 { 2 } else { 4 };
1222 let need = |n: usize, o: usize| -> Result<(), String> {
1223 if o + n > buf.len() {
1224 Err("kv import: truncated payload".into())
1225 } else {
1226 Ok(())
1227 }
1228 };
1229 need(lin * 4, o)?;
1230 self.linear_state = (0..lin)
1231 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1232 .collect();
1233 o += lin * 4;
1234 need(nimp * 4, o)?;
1235 let imp: Vec<f32> = (0..nimp)
1236 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1237 .collect();
1238 o += nimp * 4;
1239 let mut k: Vec<Vec<f32>> = Vec::with_capacity(heads);
1240 let mut v: Vec<Vec<f32>> = Vec::with_capacity(heads);
1241 for _ in 0..heads {
1242 for which in 0..2 {
1243 let n = u32_at(&mut o)? as usize;
1244 need(n * w, o)?;
1245 let xs: Vec<f32> = (0..n)
1246 .map(|i| {
1247 let at = o + i * w;
1248 if f16 {
1249 cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
1250 buf[at..at + 2].try_into().unwrap(),
1251 ))
1252 } else {
1253 f32::from_le_bytes(buf[at..at + 4].try_into().unwrap())
1254 }
1255 })
1256 .collect();
1257 o += n * w;
1258 if which == 0 { k.push(xs) } else { v.push(xs) }
1259 }
1260 }
1261 self.mode = KvMode::F32;
1262 self.k = k;
1263 self.v = v;
1264 self.kq = vec![Vec::new(); heads];
1265 self.ks = vec![Vec::new(); heads];
1266 self.vq = vec![Vec::new(); heads];
1267 self.vs = vec![Vec::new(); heads];
1268 self.kcol = vec![Vec::new(); heads];
1269 self.vcol = vec![Vec::new(); heads];
1270 self.imp = imp;
1271 self.o1 = None;
1272 self.o1_error = None;
1273 self.o1_transitioned = false;
1274 self.seq_len = seq_len;
1275 Ok(())
1276 }
1277
1278 pub fn memory_bytes(&self) -> usize {
1279 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
1280 + self.v.iter().map(Vec::len).sum::<usize>()
1281 + self.ks.iter().map(Vec::len).sum::<usize>()
1282 + self.vs.iter().map(Vec::len).sum::<usize>()
1283 + self.kcol.iter().map(Vec::len).sum::<usize>()
1284 + self.vcol.iter().map(Vec::len).sum::<usize>();
1285 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
1286 + self.vq.iter().map(Vec::len).sum::<usize>();
1287 floats * std::mem::size_of::<f32>()
1288 + bytes
1289 + self.linear_state.len() * std::mem::size_of::<f32>()
1293 + self.o1_memory_bytes()
1296 }
1297
1298 fn evict(&mut self, keep_last: usize) {
1300 if self.o1.is_some() || self.seq_len <= keep_last {
1308 return;
1309 }
1310 let drop = self.seq_len - keep_last;
1311 for h in 0..self.num_kv_heads {
1312 let stored = self.head_len(h);
1314 let d = drop.min(stored);
1315 let hd = self.head_dim;
1316 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
1317 let n = n.min(v.len());
1318 v.drain(..n);
1319 }
1320 drop_front(&mut self.k[h], d * hd);
1321 drop_front(&mut self.v[h], d * hd);
1322 drop_front(&mut self.kq[h], d * hd);
1323 drop_front(&mut self.vq[h], d * hd);
1324 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
1325 drop_front(&mut self.vs[h], d);
1326 }
1327 let d = drop.min(self.imp.len());
1328 self.imp.drain(..d);
1329 self.seq_len = keep_last;
1330 }
1331
1332 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
1337 if self.o1.is_some() {
1338 return;
1341 }
1342 let stored = self.imp.len();
1343 if stored <= keep_last {
1344 return;
1345 }
1346 let sink_n = sink.min(keep_last);
1349 let recent_n = recent.min(keep_last - sink_n);
1350 let mut keep = vec![false; stored];
1351 for k in keep.iter_mut().take(sink_n) {
1352 *k = true;
1353 }
1354 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1355 *k = true;
1356 }
1357 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1358 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1360 order.sort_by(|&a, &b| {
1361 self.imp[b]
1362 .partial_cmp(&self.imp[a])
1363 .unwrap_or(std::cmp::Ordering::Equal)
1364 });
1365 for i in order {
1366 if budget == 0 {
1367 break;
1368 }
1369 keep[i] = true;
1370 budget -= 1;
1371 }
1372
1373 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1374 let hd = self.head_dim;
1375 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1376 let mut out = Vec::with_capacity(kept.len() * step);
1377 for &i in kept {
1378 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1379 }
1380 out
1381 }
1382 for h in 0..self.num_kv_heads {
1387 if !self.k[h].is_empty() {
1388 self.k[h] = gather(&self.k[h], &kept, hd);
1389 }
1390 if !self.v[h].is_empty() {
1391 self.v[h] = gather(&self.v[h], &kept, hd);
1392 }
1393 if !self.kq[h].is_empty() {
1394 self.kq[h] = gather(&self.kq[h], &kept, hd);
1395 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1396 }
1397 if !self.vq[h].is_empty() {
1398 self.vq[h] = gather(&self.vq[h], &kept, hd);
1399 self.vs[h] = gather(&self.vs[h], &kept, 1);
1400 }
1401 }
1402 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1403 self.seq_len = kept.len();
1404 }
1405}
1406
1407#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1409pub enum EvictionPolicy {
1410 Recent,
1412 Born { sink: usize },
1414}
1415
1416#[derive(Debug)]
1418pub struct KvCache {
1419 pub layers: Vec<LayerKvCache>,
1420 pub max_seq_len: usize,
1421 pub policy: EvictionPolicy,
1422}
1423
1424impl KvCache {
1425 pub fn new(
1426 num_layers: usize,
1427 num_kv_heads: usize,
1428 head_dim: usize,
1429 max_seq_len: usize,
1430 ) -> Self {
1431 let layers = (0..num_layers)
1432 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1433 .collect();
1434 Self {
1435 layers,
1436 max_seq_len,
1437 policy: EvictionPolicy::Born { sink: 4 },
1438 }
1439 }
1440
1441 pub fn clear(&mut self) {
1442 for layer in &mut self.layers {
1443 layer.clear();
1444 }
1445 }
1446
1447 pub fn total_memory_bytes(&self) -> usize {
1448 self.layers.iter().map(|l| l.memory_bytes()).sum()
1449 }
1450
1451 pub fn seq_len(&self) -> usize {
1453 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1454 }
1455
1456 pub fn needs_eviction(&self) -> bool {
1457 self.seq_len() >= self.max_seq_len
1458 }
1459
1460 pub fn evict(&mut self, keep_last: usize) {
1462 match self.policy {
1463 EvictionPolicy::Recent => {
1464 for layer in &mut self.layers {
1465 layer.evict(keep_last);
1466 }
1467 }
1468 EvictionPolicy::Born { sink } => {
1469 let recent = (keep_last / 2).max(1);
1470 for layer in &mut self.layers {
1471 layer.evict_born(keep_last, sink, recent);
1472 }
1473 }
1474 }
1475 }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::*;
1481
1482 #[test]
1483 fn wire_round_trip_reproduces_attention() {
1484 let (heads, hd) = (2usize, 4usize);
1488 let mut a = LayerKvCache::new(heads, hd);
1489 for p in 0..5 {
1490 let k: Vec<f32> = (0..heads * hd)
1491 .map(|i| (p * 10 + i) as f32 * 0.031)
1492 .collect();
1493 let v: Vec<f32> = (0..heads * hd)
1494 .map(|i| (p * 7 + i) as f32 * -0.017)
1495 .collect();
1496 a.append(&k, &v, &[true, true]);
1497 }
1498 a.linear_state = vec![0.5, -0.25, 1.0];
1499 let q: Vec<f32> = (0..hd).map(|i| 0.1 * (i as f32 + 1.0)).collect();
1500
1501 let bytes = a.export_wire(false).expect("f32 cache exports");
1502 let mut b = LayerKvCache::new(heads, hd);
1503 b.import_wire(&bytes).expect("import");
1504
1505 assert_eq!(b.seq_len, a.seq_len);
1506 assert_eq!(b.linear_state, a.linear_state);
1507 for h in 0..heads {
1508 let (oa, sa) = a.attend(&q, h);
1509 let (ob, sb) = b.attend(&q, h);
1510 assert_eq!(oa, ob, "head {h} attention output diverged");
1511 assert_eq!(sa, sb, "head {h} attention scores diverged");
1512 }
1513 }
1514
1515 #[test]
1516 fn wire_refuses_what_it_cannot_describe() {
1517 let mut c = LayerKvCache::new(1, 4);
1520 c.mode = KvMode::Q8 { k: true, v: true };
1521 let err = c.export_wire(false).unwrap_err();
1522 assert!(err.contains("F32"), "{err}");
1523 }
1524
1525 #[test]
1526 fn wire_import_checks_geometry() {
1527 let a = LayerKvCache::new(2, 4);
1528 let bytes = a.export_wire(false).unwrap();
1529 let mut wrong = LayerKvCache::new(2, 8);
1530 let err = wrong.import_wire(&bytes).unwrap_err();
1531 assert!(err.contains("2×4"), "{err}");
1532 }
1533
1534 #[test]
1535 fn append_tracks_seq_len_and_layout() {
1536 let mut cache = LayerKvCache::new(4, 8);
1537 cache.mode = KvMode::F32;
1538 assert_eq!(cache.seq_len, 0);
1539
1540 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1541 let v = vec![2.0f32; 32];
1542 cache.append(&k, &v, &[true; 4]);
1543
1544 assert_eq!(cache.seq_len, 1);
1545 assert_eq!(cache.head_len(0), 1);
1546 assert_eq!(cache.head_keys(1), &k[8..16]);
1548 assert_eq!(cache.memory_bytes(), 256);
1549 }
1550
1551 #[test]
1552 fn dead_head_stores_nothing() {
1553 let mut cache = LayerKvCache::new(2, 4);
1554 cache.mode = KvMode::F32;
1555 let k = vec![1.0f32; 8];
1556 let v = vec![2.0f32; 8];
1557 cache.append(&k, &v, &[true, false]);
1558 cache.append(&k, &v, &[true, false]);
1559
1560 assert_eq!(cache.seq_len, 2);
1561 assert_eq!(cache.head_len(0), 2);
1562 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1563 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1564 }
1565
1566 #[test]
1567 fn eviction_keeps_recent() {
1568 let mut cache = KvCache::new(2, 4, 8, 10);
1569 cache.policy = EvictionPolicy::Recent;
1570 for l in &mut cache.layers {
1571 l.mode = KvMode::F32;
1572 }
1573 let k = vec![1.0f32; 32];
1574 let v = vec![2.0f32; 32];
1575 for _ in 0..8 {
1576 for layer in &mut cache.layers {
1577 layer.append(&k, &v, &[true; 4]);
1578 }
1579 }
1580 assert_eq!(cache.seq_len(), 8);
1581 assert!(!cache.needs_eviction());
1582
1583 cache.evict(4);
1584 assert_eq!(cache.seq_len(), 4);
1585 assert_eq!(cache.layers[0].head_len(0), 4);
1586 }
1587
1588 #[test]
1589 fn collecting_o1_eviction_retains_exact_storage_until_boundary() {
1590 const B: usize = 19;
1591 let q = vec![0.1f32; 8];
1592 let k = vec![0.2f32; 4];
1593 let v = vec![0.3f32; 4];
1594
1595 for policy in [EvictionPolicy::Recent, EvictionPolicy::Born { sink: 2 }] {
1596 let mut cache = KvCache::new(1, 1, 4, 6);
1597 cache.policy = policy;
1598 cache.layers[0].mode = KvMode::F32;
1599 cache.layers[0].o1_begin_with_boundary(
1600 4,
1601 8,
1602 2,
1603 crate::nystrom::O1Rect::Aggregate,
1604 Some(B),
1605 );
1606
1607 for pos in 0..B {
1608 {
1609 let layer = &mut cache.layers[0];
1610 layer.o1_push_q(&q);
1611 layer.append(&k, &v, &[]);
1612 }
1613 if pos + 1 < B {
1614 cache.evict(3);
1615 }
1616 }
1617
1618 let layer = &cache.layers[0];
1619 let rows = B * layer.head_dim;
1620 assert_eq!(layer.seq_len, B, "policy {policy:?} retained depth");
1621 assert_eq!(layer.k[0].len(), rows, "policy {policy:?} K rows");
1622 assert_eq!(layer.v[0].len(), rows, "policy {policy:?} V rows");
1623 assert!(
1624 layer.k[0].capacity() >= rows,
1625 "policy {policy:?} K capacity"
1626 );
1627 assert!(
1628 layer.v[0].capacity() >= rows,
1629 "policy {policy:?} V capacity"
1630 );
1631 let q_capacity = match layer.o1.as_ref() {
1632 Some(O1State::Collecting { q_buf, .. }) => q_buf.capacity(),
1633 other => panic!("policy {policy:?} changed state early: {other:?}"),
1634 };
1635 assert!(
1636 q_capacity >= B * 8,
1637 "policy {policy:?} Q capacity must cover the exact prefix"
1638 );
1639
1640 assert!(cache.layers[0].o1_seal_checked(2).unwrap());
1641 assert_eq!(cache.layers[0].k[0].capacity(), 0, "K released after seal");
1642 assert_eq!(cache.layers[0].v[0].capacity(), 0, "V released after seal");
1643 }
1644 }
1645
1646 #[test]
1647 fn truncate_rolls_back_speculative_positions() {
1648 let mut cache = LayerKvCache::new(2, 4);
1649 cache.mode = KvMode::F32;
1650 for pos in 0..5 {
1651 let k = vec![pos as f32; 8];
1652 let v = vec![pos as f32; 8];
1653 cache.append(&k, &v, &[true; 2]);
1654 }
1655 cache.truncate_last(2);
1656 assert_eq!(cache.seq_len, 3);
1657 assert_eq!(cache.head_len(0), 3);
1658 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1659 }
1660
1661 #[test]
1665 fn q8_attend_matches_f32_within_grid() {
1666 let (heads, hd) = (2, 32);
1667 let mut f = LayerKvCache::new(heads, hd);
1668 f.mode = KvMode::F32;
1669 let mut q8 = LayerKvCache::new(heads, hd);
1670 q8.mode = KvMode::Q8 { k: true, v: true };
1671
1672 let synth = |p: usize, salt: usize| -> Vec<f32> {
1673 (0..heads * hd)
1674 .map(|i| {
1675 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1676 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1678 })
1679 .collect()
1680 };
1681 for p in 0..100 {
1682 let k = synth(p, 1);
1683 let v = synth(p, 2);
1684 f.append(&k, &v, &[true; 2]);
1685 q8.append(&k, &v, &[true; 2]);
1686 }
1687 let q: Vec<f32> = (0..hd)
1688 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1689 .collect();
1690 for g in 0..heads {
1691 let (of, pf) = f.attend(&q, g);
1692 let (o8, p8) = q8.attend(&q, g);
1693 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1694 for d in 0..hd {
1695 assert!(
1696 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1697 "g{g} d{d}: f32 {} vs q8 {}",
1698 of[d],
1699 o8[d]
1700 );
1701 }
1702 for p in 0..100 {
1703 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1704 }
1705 }
1706 q8.truncate_last(30);
1708 assert_eq!(q8.head_len(0), 70);
1709 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1710 q8.accumulate_imp(&imp);
1711 q8.evict_born(20, 2, 8);
1712 assert_eq!(q8.head_len(0), 20);
1713 let (o, _) = q8.attend(&q, 0);
1714 assert!(o.iter().all(|x| x.is_finite()));
1715 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1717 }
1718
1719 #[test]
1722 fn attend_group_equals_per_head_attend_bitexact() {
1723 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1725 let mut c = LayerKvCache::new(kv_heads, hd);
1726 c.mode = mode;
1727 for p in 0..70 {
1728 let k: Vec<f32> = (0..kv_heads * hd)
1729 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1730 .collect();
1731 let v: Vec<f32> = (0..kv_heads * hd)
1732 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1733 .collect();
1734 c.append(&k, &v, &[true; 2]);
1735 }
1736 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1737 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1738 .collect();
1739 for g in 0..kv_heads {
1740 let span = g * hpk * hd..(g + 1) * hpk * hd;
1741 let mut out = vec![0f32; hpk * hd];
1742 let mut imp = vec![0f32; 70];
1743 c.attend_group(
1744 &q[span.clone()],
1745 g,
1746 &mut out,
1747 &mut imp,
1748 1.0 / (hd as f32).sqrt(),
1749 0,
1750 0.0,
1751 );
1752 let mut imp_ref = vec![0f32; 70];
1753 for h in 0..hpk {
1754 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1755 let (o, probs) = c.attend(qh, g);
1756 assert_eq!(
1757 &out[h * hd..(h + 1) * hd],
1758 &o[..],
1759 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1760 );
1761 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1762 *dst += p;
1763 }
1764 }
1765 assert_eq!(
1766 imp, imp_ref,
1767 "mode {mode:?} g{g}: attention mass must match"
1768 );
1769 }
1770 }
1771 }
1772
1773 #[test]
1777 fn born_eviction_mixed_modes_stay_consistent() {
1778 for (mk, mv) in [(false, true), (true, false)] {
1779 let mut c = LayerKvCache::new(1, 4);
1780 c.mode = KvMode::Q8 { k: mk, v: mv };
1781 for p in 0..80 {
1782 let k = vec![p as f32 * 0.01; 4];
1783 let v = vec![p as f32; 4];
1784 c.append(&k, &v, &[true]);
1785 }
1786 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1787 c.accumulate_imp(&imp);
1788 let before = c.memory_bytes();
1789 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1791 assert!(
1792 c.memory_bytes() < before / 2,
1793 "memory must shrink (k={mk} v={mv})"
1794 );
1795 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1798 assert!(
1799 out[0] > 30.0,
1800 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1801 out[0]
1802 );
1803 }
1804 }
1805
1806 #[test]
1807 fn born_eviction_keeps_high_mass_position() {
1808 let mut cache = KvCache::new(1, 1, 2, 16);
1809 cache.policy = EvictionPolicy::Born { sink: 1 };
1810 for l in &mut cache.layers {
1811 l.mode = KvMode::F32;
1812 }
1813 let layer = &mut cache.layers[0];
1814 for pos in 0..8 {
1817 let k = vec![pos as f32; 2];
1818 let v = vec![pos as f32 + 100.0; 2];
1819 layer.append(&k, &v, &[true]);
1820 }
1821 let mut imp = vec![0.05f32; 8];
1823 imp[3] = 5.0;
1824 layer.accumulate_imp(&imp);
1825
1826 cache.evict(4); let layer = &cache.layers[0];
1828 assert_eq!(layer.seq_len, 4);
1829 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1830 assert_eq!(
1831 kept_keys,
1832 vec![0.0, 3.0, 6.0, 7.0],
1833 "kept = sink(0) + mass-top(3) + recent(6,7)"
1834 );
1835 assert_eq!(layer.head_len(0), 4);
1837 }
1838}