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 pub sinks: Option<Vec<f32>>,
128}
129
130impl LayerKvCache {
131 pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
132 Self {
133 sinks: None,
134 mode: KvMode::from_env(),
135 k: vec![Vec::new(); num_kv_heads],
136 v: vec![Vec::new(); num_kv_heads],
137 kq: vec![Vec::new(); num_kv_heads],
138 ks: vec![Vec::new(); num_kv_heads],
139 vq: vec![Vec::new(); num_kv_heads],
140 vs: vec![Vec::new(); num_kv_heads],
141 kcol: vec![Vec::new(); num_kv_heads],
142 vcol: vec![Vec::new(); num_kv_heads],
143 imp: Vec::new(),
144 seq_len: 0,
145 num_kv_heads,
146 head_dim,
147 linear_state: Vec::new(),
148 linear_scratch: Vec::new(),
149 o1: None,
150 o1_error: None,
151 o1_transitioned: false,
152 }
153 }
154
155 pub fn k_heads(&self) -> &[Vec<f32>] {
157 &self.k
158 }
159 pub fn v_heads(&self) -> &[Vec<f32>] {
161 &self.v
162 }
163
164 pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
168 self.o1_begin_with_boundary(m, w, sink, rect, None);
169 }
170
171 pub(crate) fn o1_begin_with_boundary(
175 &mut self,
176 m: usize,
177 w: usize,
178 sink: usize,
179 rect: crate::nystrom::O1Rect,
180 seal_at: Option<usize>,
181 ) {
182 self.o1 = Some(O1State::Collecting {
183 m,
184 w,
185 sink,
186 rect,
187 seal_at,
188 q_buf: Vec::new(),
189 });
190 self.o1_error = None;
191 self.o1_transitioned = false;
192 }
193
194 pub fn o1_push_q(&mut self, q_all: &[f32]) {
199 if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
200 q_buf.extend_from_slice(q_all);
201 }
202 }
203
204 pub fn o1_sealed(&self) -> bool {
205 matches!(self.o1, Some(O1State::Sealed { .. }))
206 }
207
208 pub(crate) fn o1_pending_boundary(&self) -> Option<usize> {
211 match &self.o1 {
212 Some(O1State::Collecting { seal_at, .. }) => *seal_at,
213 _ => None,
214 }
215 }
216
217 pub(crate) fn o1_boundary_crossed_by(&self, count: usize) -> bool {
221 let Some(target) = self.o1_pending_boundary() else {
222 return false;
223 };
224 target <= self.seq_len
225 || self
226 .seq_len
227 .checked_add(count)
228 .map_or(true, |next| next >= target)
229 }
230
231 pub(crate) fn take_o1_transition(&mut self) -> bool {
232 std::mem::take(&mut self.o1_transitioned)
233 }
234
235 pub(crate) fn take_o1_error(&self) -> Option<String> {
236 self.o1_error.clone()
242 }
243
244 pub(crate) fn o1_abort(&mut self, err: String) {
249 self.k.iter_mut().for_each(Vec::clear);
250 self.v.iter_mut().for_each(Vec::clear);
251 self.kq.iter_mut().for_each(Vec::clear);
252 self.ks.iter_mut().for_each(Vec::clear);
253 self.vq.iter_mut().for_each(Vec::clear);
254 self.vs.iter_mut().for_each(Vec::clear);
255 self.kcol.iter_mut().for_each(Vec::clear);
256 self.vcol.iter_mut().for_each(Vec::clear);
257 self.imp.clear();
258 self.o1 = None;
259 self.seq_len = 0;
260 self.o1_transitioned = false;
261 self.o1_error = Some(err);
262 }
263
264 pub fn o1_seal(&mut self, num_heads: usize) -> bool {
271 match self.o1_seal_checked(num_heads) {
272 Ok(sealed) => sealed,
273 Err(err) => {
274 tracing::error!("o1: seal aborted: {err}");
275 self.o1_abort(err);
276 false
277 }
278 }
279 }
280
281 pub(crate) fn o1_seal_checked(&mut self, num_heads: usize) -> Result<bool, String> {
285 if let Some(err) = self.o1_error.clone() {
286 return Err(err);
287 }
288 if !matches!(self.o1, Some(O1State::Collecting { .. })) {
291 return Ok(self.o1_sealed());
292 }
293 let (m, w, sink, requested_boundary, q_len) = match &self.o1 {
294 Some(O1State::Collecting {
295 m,
296 w,
297 sink,
298 rect: _,
299 seal_at,
300 q_buf,
301 }) => (*m, *w, *sink, *seal_at, q_buf.len()),
302 _ => unreachable!("checked above"),
303 };
304 let floor = crate::nystrom::o1_deferred_boundary(w, sink)
305 .ok_or_else(|| "o1 seal: w + sink + slack + 1 overflow".to_string())?;
306 let target = requested_boundary.unwrap_or(floor).max(floor);
307 let t = self.seq_len;
308 if t < target {
309 if let Some(O1State::Collecting { seal_at, .. }) = &mut self.o1 {
310 if *seal_at != Some(target) {
311 *seal_at = Some(target);
312 tracing::info!(
313 "o1 deferred seal: current rows={t}, boundary={target} (floor={floor})"
314 );
315 }
316 }
317 return Ok(false);
318 }
319
320 let hd = self.head_dim;
321 if t == 0 {
322 return Err("o1 seal: cannot seal an empty layer".into());
323 }
324 if self.mode != KvMode::F32 {
325 return Err("o1 seal: requires dense F32 KV storage".into());
326 }
327 if self.num_kv_heads == 0 || num_heads == 0 || num_heads % self.num_kv_heads != 0 {
328 return Err(format!(
329 "o1 seal: invalid GQA geometry num_heads={num_heads} num_kv_heads={}",
330 self.num_kv_heads
331 ));
332 }
333 let hpk = num_heads / self.num_kv_heads;
334 let expected_k = t
335 .checked_mul(hd)
336 .ok_or_else(|| "o1 seal: KV row length overflow".to_string())?;
337 let expected_q = expected_k
338 .checked_mul(num_heads)
339 .ok_or_else(|| "o1 seal: query trace length overflow".to_string())?;
340 if q_len != expected_q {
341 return Err(format!(
342 "o1 seal: query trace has {q_len} values, expected {expected_q}"
343 ));
344 }
345 if (0..self.num_kv_heads)
346 .any(|g| self.k[g].len() != expected_k || self.v[g].len() != expected_k)
347 {
348 return Err("o1 seal: KV heads are not densely populated".into());
349 }
350 if m < 4 || w == 0 {
351 return Err(format!("o1 seal: invalid geometry m={m} w={w}"));
352 }
353
354 let Some(O1State::Collecting {
355 m,
356 w,
357 sink,
358 rect,
359 q_buf,
360 ..
361 }) = self.o1.take()
362 else {
363 unreachable!("collecting state disappeared after validation");
364 };
365 let mut groups = Vec::with_capacity(self.num_kv_heads);
366 let mut qh = vec![0.0f32; hpk * t * hd];
369 for g in 0..self.num_kv_heads {
370 for hh in 0..hpk {
371 let h = g * hpk + hh;
372 for p in 0..t {
373 let src = (p * num_heads + h) * hd;
374 let dst = (hh * t + p) * hd;
375 qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
376 }
377 }
378 let qs: Vec<&[f32]> = (0..hpk)
379 .map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd])
380 .collect();
381 let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
382 st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
383 groups.push(st);
384 }
385 for h in 0..self.num_kv_heads {
388 self.k[h] = Vec::new();
389 self.v[h] = Vec::new();
390 }
391 self.imp = Vec::new();
392 self.o1 = Some(O1State::Sealed { groups });
393 self.o1_transitioned = true;
394 Ok(true)
395 }
396
397 pub fn o1_views(&self) -> Option<Vec<crate::nystrom::O1DeviceView<'_>>> {
406 let Some(O1State::Sealed { groups }) = &self.o1 else {
407 return None;
408 };
409 let views: Vec<_> = groups.iter().map(|g| g.device_view()).collect();
410 if views.iter().any(|v| v.exact_only) {
411 return None;
412 }
413 Some(views)
414 }
415
416 pub fn o1_step(
417 &mut self,
418 q_all: &[f32],
419 k_new: &[f32],
420 v_new: &[f32],
421 num_heads: usize,
422 ) -> Vec<f32> {
423 let hd = self.head_dim;
424 let hpk = num_heads / self.num_kv_heads.max(1);
425 let mut out = vec![0.0f32; num_heads * hd];
426 let Some(O1State::Sealed { groups }) = &mut self.o1 else {
427 debug_assert!(false, "o1_step on an unsealed layer");
428 return out;
429 };
430 for (g, st) in groups.iter_mut().enumerate() {
431 let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
432 st.step_group(
433 &q_all[lo..hi],
434 &k_new[g * hd..(g + 1) * hd],
435 &v_new[g * hd..(g + 1) * hd],
436 &mut out[lo..hi],
437 );
438 }
439 self.seq_len += 1;
442 out
443 }
444
445 pub fn o1_memory_bytes(&self) -> usize {
448 match &self.o1 {
449 Some(O1State::Collecting { q_buf, .. }) => q_buf.len() * std::mem::size_of::<f32>(),
450 Some(O1State::Sealed { groups }) => groups.iter().map(|s| s.memory_bytes()).sum(),
451 None => 0,
452 }
453 }
454
455 fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>, group: usize) {
458 let mut resid = vec![0.0f32; row.len()];
459 for (d, &x) in row.iter().enumerate() {
460 resid[d] = if col.is_empty() { x } else { x / col[d] };
461 }
462 for g0 in (0..row.len()).step_by(group) {
463 let g1 = (g0 + group).min(row.len());
464 let mut absmax = 0.0f32;
465 for &r in &resid[g0..g1] {
466 absmax = absmax.max(r.abs());
467 }
468 let s = (absmax / 127.0).max(1e-12);
469 sc.push(s);
470 for &r in &resid[g0..g1] {
471 q.push((r / s).round().clamp(-127.0, 127.0) as i8);
472 }
473 }
474 }
475
476 fn freeze_cols(&mut self) {
479 let hd = self.head_dim;
480 let ngk = hd.div_ceil(KV_K_GROUP);
481 for h in 0..self.num_kv_heads {
482 for (qv, sv, colv, group) in [
483 (
484 &mut self.kq[h],
485 &mut self.ks[h],
486 &mut self.kcol[h],
487 KV_K_GROUP,
488 ),
489 (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
490 ] {
491 let spp = if group == hd { 1 } else { ngk }; let n = sv.len() / spp;
493 if n == 0 {
494 continue;
495 }
496 let mut rows = vec![0.0f32; n * hd];
498 for p in 0..n {
499 for d in 0..hd {
500 rows[p * hd + d] = qv[p * hd + d] as f32 * sv[p * spp + d / group];
501 }
502 }
503 let mut col = vec![0.0f32; hd];
504 for p in 0..n {
505 for d in 0..hd {
506 col[d] += rows[p * hd + d] * rows[p * hd + d];
507 }
508 }
509 for c in col.iter_mut() {
510 *c = (*c / n as f32).sqrt().max(1e-6);
511 }
512 qv.clear();
513 sv.clear();
514 for p in 0..n {
515 Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
516 }
517 *colv = col;
518 }
519 }
520 }
521
522 pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
526 if self.o1_error.is_some() {
530 return;
531 }
532 debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
533 debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
534 if matches!(self.mode, KvMode::Q8 { .. })
540 && self.seq_len >= KV_COL_WARMUP
541 && self.kcol.iter().all(Vec::is_empty)
542 && self.vcol.iter().all(Vec::is_empty)
543 {
544 self.freeze_cols();
545 }
546 for h in 0..self.num_kv_heads {
547 if !alive.get(h).copied().unwrap_or(true) {
548 continue;
549 }
550 let s = h * self.head_dim;
551 if self.mode.quant_k() {
552 Self::quant_row(
553 &k_new[s..s + self.head_dim],
554 &self.kcol[h],
555 &mut self.kq[h],
556 &mut self.ks[h],
557 KV_K_GROUP,
558 );
559 } else {
560 self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
561 }
562 if self.mode.quant_v() {
563 Self::quant_row(
564 &v_new[s..s + self.head_dim],
565 &self.vcol[h],
566 &mut self.vq[h],
567 &mut self.vs[h],
568 self.head_dim,
569 );
570 } else {
571 self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
572 }
573 }
574 self.imp.push(0.0);
575 self.seq_len += 1;
576 }
577
578 pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
583 let hd = self.head_dim;
584 if self.mode == KvMode::F32 {
585 let stored = self.k[kv_head].len() / hd;
586 return crate::attention::attention_head(
587 q,
588 &self.k[kv_head],
589 &self.v[kv_head],
590 hd,
591 stored,
592 );
593 }
594 let stored = self.head_len(kv_head);
595 let scale = 1.0 / (hd as f32).sqrt();
596 let mut scores = vec![0.0f32; stored];
597 if self.mode.quant_k() {
598 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
599 let kcol = &self.kcol[kv_head];
601 let mut qc = vec![0.0f32; hd];
602 for d in 0..hd {
603 qc[d] = if kcol.is_empty() {
604 q[d]
605 } else {
606 q[d] * kcol[d]
607 };
608 }
609 let ng = hd.div_ceil(KV_K_GROUP);
610 for p in 0..stored {
611 let row = &kq[p * hd..(p + 1) * hd];
612 let row_u8 =
615 unsafe { std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len()) };
616 let mut dot = 0.0f32;
617 for g in 0..ng {
618 let g0 = g * KV_K_GROUP;
619 let g1 = (g0 + KV_K_GROUP).min(hd);
620 dot +=
621 crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1]) * ks[p * ng + g];
622 }
623 scores[p] = dot * scale;
624 }
625 } else {
626 let k = &self.k[kv_head];
627 for p in 0..stored {
628 let row = &k[p * hd..(p + 1) * hd];
629 scores[p] = crate::attention::dot_f32(q, row) * scale;
630 }
631 }
632 let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
633 let mut sum = 0.0f32;
634 for s in scores.iter_mut() {
635 *s = (*s - max_score).exp();
636 sum += *s;
637 }
638 if sum > 0.0 {
639 for s in scores.iter_mut() {
640 *s /= sum;
641 }
642 }
643 let mut acc = vec![0.0f32; hd];
644 if self.mode.quant_v() {
645 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
646 for p in 0..stored {
647 let w = scores[p] * vs[p];
648 if w.abs() < 1e-12 {
649 continue;
650 }
651 crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
652 }
653 let vcol = &self.vcol[kv_head];
654 if !vcol.is_empty() {
655 for d in 0..hd {
656 acc[d] *= vcol[d];
657 }
658 }
659 } else {
660 let v = &self.v[kv_head];
661 for p in 0..stored {
662 let w = scores[p];
663 if w.abs() < 1e-12 {
664 continue;
665 }
666 crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
667 }
668 }
669 (acc, scores)
670 }
671
672 #[allow(clippy::too_many_arguments)]
689 pub fn attend_group(
690 &self,
691 q_group: &[f32],
692 kv_head: usize,
693 out: &mut [f32],
694 imp_acc: &mut [f32],
695 scale: f32,
696 first: usize,
697 softcap: f32,
698 sinks: &[f32],
699 ) {
700 self.attend_group_upto(
701 q_group,
702 kv_head,
703 out,
704 imp_acc,
705 scale,
706 first,
707 softcap,
708 usize::MAX,
709 sinks,
710 )
711 }
712
713 #[allow(clippy::too_many_arguments)]
731 pub fn attend_group_upto(
732 &self,
733 q_group: &[f32],
734 kv_head: usize,
735 out: &mut [f32],
736 imp_acc: &mut [f32],
737 scale: f32,
738 first: usize,
739 softcap: f32,
740 upto: usize,
741 sinks: &[f32],
742 ) {
743 let hd = self.head_dim;
744 let nheads = q_group.len() / hd;
745 debug_assert_eq!(out.len(), nheads * hd);
746 assert!(
747 sinks.is_empty() || sinks.len() == nheads,
748 "attend_group: {} sink logits for {nheads} heads",
749 sinks.len()
750 );
751 let stored = if self.mode == KvMode::F32 {
752 self.k[kv_head].len() / hd
753 } else {
754 self.head_len(kv_head)
755 }
756 .min(upto);
757 if stored == 0 {
758 out.fill(0.0);
759 return;
760 }
761 let first = first.min(stored.saturating_sub(1));
762 let span = stored - first;
764
765 thread_local! {
766 static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
768 const { std::cell::RefCell::new(Vec::new()) };
769 static GQA_QC: std::cell::RefCell<Vec<f32>> =
771 const { std::cell::RefCell::new(Vec::new()) };
772 }
773
774 GQA_SCORES.with(|sc| {
775 let mut scores = sc.borrow_mut();
776 scores.resize(nheads * span, 0.0);
777
778 if self.mode.quant_k() {
780 let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
781 let kcol = &self.kcol[kv_head];
782 let ng = hd.div_ceil(KV_K_GROUP);
783 GQA_QC.with(|qc| {
784 let mut qcb = qc.borrow_mut();
785 qcb.resize(nheads * hd, 0.0);
786 for h in 0..nheads {
787 for d in 0..hd {
788 let qv = q_group[h * hd + d];
789 qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
790 }
791 }
792 for p in first..stored {
793 let row = &kq[p * hd..(p + 1) * hd];
794 let row_u8 = unsafe {
797 std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
798 };
799 for h in 0..nheads {
800 let qch = &qcb[h * hd..(h + 1) * hd];
801 let mut dot = 0.0f32;
802 for g in 0..ng {
803 let g0 = g * KV_K_GROUP;
804 let g1 = (g0 + KV_K_GROUP).min(hd);
805 dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
806 * ks[p * ng + g];
807 }
808 scores[h * span + (p - first)] = dot * scale;
809 }
810 }
811 });
812 } else {
813 let k = &self.k[kv_head];
814 for p in first..stored {
815 let row = &k[p * hd..(p + 1) * hd];
816 for h in 0..nheads {
817 scores[h * span + (p - first)] =
818 crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
819 }
820 }
821 }
822
823 if softcap > 0.0 {
827 for v in scores.iter_mut() {
828 *v = softcap * (*v / softcap).tanh();
829 }
830 }
831
832 for h in 0..nheads {
835 let s = &mut scores[h * span..(h + 1) * span];
836 let row_max = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
837 let sink = sinks.get(h).copied();
838 let max_score = match sink {
839 Some(z) => row_max.max(z),
840 None => row_max,
841 };
842 let mut sum = 0.0f32;
843 for v in s.iter_mut() {
844 *v = (*v - max_score).exp();
845 sum += *v;
846 }
847 if let Some(z) = sink {
848 sum += (z - max_score).exp();
849 }
850 if sum > 0.0 {
851 for v in s.iter_mut() {
852 *v /= sum;
853 }
854 }
855 }
856
857 out.fill(0.0);
859 if self.mode.quant_v() {
860 let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
861 for p in first..stored {
862 let row = &vq[p * hd..(p + 1) * hd];
863 for h in 0..nheads {
864 let w = scores[h * span + (p - first)] * vs[p];
865 if w.abs() < 1e-12 {
866 continue;
867 }
868 crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
869 }
870 }
871 let vcol = &self.vcol[kv_head];
872 if !vcol.is_empty() {
873 for h in 0..nheads {
874 for d in 0..hd {
875 out[h * hd + d] *= vcol[d];
876 }
877 }
878 }
879 } else {
880 let v = &self.v[kv_head];
881 for p in first..stored {
882 let row = &v[p * hd..(p + 1) * hd];
883 for h in 0..nheads {
884 let w = scores[h * span + (p - first)];
885 if w.abs() < 1e-12 {
886 continue;
887 }
888 crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
889 }
890 }
891 }
892
893 let n = imp_acc.len().min(stored);
897 if n > first {
898 for h in 0..nheads {
899 let s = &scores[h * span..(h + 1) * span];
900 for (dst, &p) in imp_acc[first..n].iter_mut().zip(s) {
901 *dst += p;
902 }
903 }
904 }
905 });
906 }
907
908 #[cfg(target_arch = "aarch64")]
916 #[allow(clippy::too_many_arguments)]
917 pub fn attend_chunk(
918 &mut self,
919 q_all: &[f32],
920 b: usize,
921 s0: usize,
922 nh: usize,
923 heads_per_kv: usize,
924 hd: usize,
925 out: &mut [f32],
926 pool: Option<&crate::pool::Pool>,
927 scale: f32,
928 window: Option<usize>,
929 ) {
930 let n = s0 + b;
931 struct SendPtr(*mut f32);
932 unsafe impl Send for SendPtr {}
933 unsafe impl Sync for SendPtr {}
934 impl SendPtr {
935 fn at(&self, i: usize) -> *mut f32 {
936 unsafe { self.0.add(i) }
939 }
940 }
941 thread_local! {
942 static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
943 const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
944 }
945 let neon_gemm = cfg!(not(target_os = "macos"))
950 || std::env::var("CMF_FORCE_NEON_GEMM")
951 .map(|v| v == "1")
952 .unwrap_or(false);
953 SCRATCH.with(|s| {
954 let mut s = s.borrow_mut();
955 let (qpanel, scores, aopanel, ktpack) = &mut *s;
956 let m = heads_per_kv * b;
961 qpanel.resize(m * hd, 0.0);
962 scores.resize(m * n, 0.0);
963 aopanel.resize(m * hd, 0.0);
964 for g in 0..self.num_kv_heads {
965 let kmat = &self.k[g];
966 let vmat = &self.v[g];
967 debug_assert_eq!(kmat.len(), n * hd);
968 for hl in 0..heads_per_kv {
969 let hh = g * heads_per_kv + hl;
970 for bi in 0..b {
971 qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
972 .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
973 }
974 }
975 if neon_gemm {
976 ktpack.resize(hd * n, 0.0);
977 for p in 0..n {
978 let row = &kmat[p * hd..(p + 1) * hd];
979 for (d, &v) in row.iter().enumerate() {
980 ktpack[d * n + p] = v;
981 }
982 }
983 let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
986 let sp_s = SendPtr(scores.as_mut_ptr());
987 let kt = &*ktpack;
988 let run = |start: usize, end: usize| {
989 if end > start {
990 let a = unsafe {
992 std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
993 };
994 let c = unsafe {
995 std::slice::from_raw_parts_mut(
996 sp_s.at(start * n),
997 (end - start) * n,
998 )
999 };
1000 crate::qtensor::neon_gemm_rm(
1001 end - start,
1002 n,
1003 hd,
1004 scale,
1005 a,
1006 hd,
1007 kt,
1008 n,
1009 false,
1010 c,
1011 n,
1012 );
1013 }
1014 };
1015 match pool {
1016 Some(p) if m >= 64 => p.run_rows(m, &run),
1017 _ => run(0, m),
1018 }
1019 } else {
1020 crate::qtensor::sgemm_rm(
1021 m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
1022 );
1023 }
1024 let sp = SendPtr(scores.as_mut_ptr());
1026 let run = |start: usize, end: usize| {
1027 for r in start..end {
1028 let allowed = s0 + (r % b) + 1;
1029 let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
1033 let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
1035 crate::attention::softmax_row(&mut row[lo..allowed]);
1036 row[..lo].fill(0.0);
1037 row[allowed..].fill(0.0);
1038 }
1039 };
1040 match pool {
1041 Some(p) if m >= 64 => p.run_rows(m, &run),
1042 _ => run(0, m),
1043 }
1044 let ni = self.imp.len().min(n);
1048 for r in 0..m {
1049 let al = (s0 + (r % b) + 1).min(ni);
1050 for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
1051 *dst += p;
1052 }
1053 }
1054 if neon_gemm {
1055 let sp_s = SendPtr(scores.as_mut_ptr());
1056 let sp_o = SendPtr(aopanel.as_mut_ptr());
1057 let run = |start: usize, end: usize| {
1058 if end > start {
1059 let a = unsafe {
1061 std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
1062 };
1063 let c = unsafe {
1064 std::slice::from_raw_parts_mut(
1065 sp_o.at(start * hd),
1066 (end - start) * hd,
1067 )
1068 };
1069 crate::qtensor::neon_gemm_rm(
1070 end - start,
1071 hd,
1072 n,
1073 1.0,
1074 a,
1075 n,
1076 vmat,
1077 hd,
1078 false,
1079 c,
1080 hd,
1081 );
1082 }
1083 };
1084 match pool {
1085 Some(p) if m >= 64 => p.run_rows(m, &run),
1086 _ => run(0, m),
1087 }
1088 } else {
1089 crate::qtensor::sgemm_rm(
1090 m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
1091 );
1092 }
1093 for hl in 0..heads_per_kv {
1094 let hh = g * heads_per_kv + hl;
1095 for bi in 0..b {
1096 out[bi * nh * hd + hh * hd..][..hd]
1097 .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
1098 }
1099 }
1100 }
1101 });
1102 }
1103
1104 pub fn truncate_last(&mut self, n_drop: usize) {
1106 let d = n_drop.min(self.seq_len);
1107 for h in 0..self.num_kv_heads {
1108 let keep = self.k[h].len().saturating_sub(d * self.head_dim);
1109 self.k[h].truncate(keep);
1110 self.v[h].truncate(keep);
1111 let ngk = self.head_dim.div_ceil(KV_K_GROUP);
1112 let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
1113 self.kq[h].truncate(keep_q);
1114 let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
1115 self.vq[h].truncate(keep_vq);
1116 let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
1117 self.ks[h].truncate(keep_ks);
1118 let keep_vs = self.vs[h].len().saturating_sub(d);
1119 self.vs[h].truncate(keep_vs);
1120 }
1121 self.imp.truncate(self.imp.len().saturating_sub(d));
1122 self.seq_len -= d;
1123 }
1124
1125 pub fn accumulate_imp(&mut self, probs: &[f32]) {
1127 for (dst, &p) in self.imp.iter_mut().zip(probs) {
1128 *dst += p;
1129 }
1130 }
1131
1132 pub fn head_keys(&self, kv_head: usize) -> &[f32] {
1134 &self.k[kv_head]
1135 }
1136
1137 pub fn head_values(&self, kv_head: usize) -> &[f32] {
1138 &self.v[kv_head]
1139 }
1140
1141 pub fn head_len(&self, kv_head: usize) -> usize {
1143 let ng = self.head_dim.div_ceil(KV_K_GROUP);
1144 (self.k[kv_head].len() / self.head_dim)
1145 .max(self.ks[kv_head].len() / ng)
1146 .max(self.vs[kv_head].len())
1147 }
1148
1149 pub fn clear(&mut self) {
1151 for h in 0..self.num_kv_heads {
1152 self.k[h].clear();
1153 self.v[h].clear();
1154 self.kq[h].clear();
1155 self.ks[h].clear();
1156 self.vq[h].clear();
1157 self.vs[h].clear();
1158 self.kcol[h].clear();
1159 self.vcol[h].clear();
1160 }
1161 self.imp.clear();
1162 self.linear_state.clear();
1163 self.linear_scratch.clear();
1164 self.o1 = None;
1167 self.o1_error = None;
1168 self.o1_transitioned = false;
1169 self.seq_len = 0;
1170 }
1171
1172 pub fn export_wire(&self, f16: bool) -> Result<Vec<u8>, String> {
1182 if !matches!(self.mode, KvMode::F32) {
1183 return Err("kv export: only the F32 cache is described by this format (CMF_KV=q8 stores int8 rows and per-row scales)"
1184 .into());
1185 }
1186 if self.o1.is_some() {
1187 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"
1188 .into());
1189 }
1190 if self.kcol.iter().any(|c| !c.is_empty()) || self.vcol.iter().any(|c| !c.is_empty()) {
1194 return Err(
1195 "kv export: frozen columns under an F32 cache — refusing to ship \
1196 a state this format does not describe"
1197 .into(),
1198 );
1199 }
1200 let mut out = Vec::with_capacity(self.memory_bytes() / if f16 { 2 } else { 1 } + 64);
1201 let u = |v: u32, o: &mut Vec<u8>| o.extend_from_slice(&v.to_le_bytes());
1202 u(u8::from(f16) as u32, &mut out);
1203 u(self.seq_len as u32, &mut out);
1204 u(self.num_kv_heads as u32, &mut out);
1205 u(self.head_dim as u32, &mut out);
1206 u(self.linear_state.len() as u32, &mut out);
1207 u(self.imp.len() as u32, &mut out);
1212 let push = |xs: &[f32], o: &mut Vec<u8>| {
1213 if f16 {
1214 for &x in xs {
1215 o.extend_from_slice(&cortiq_core::quant::f32_to_f16(x).to_le_bytes());
1216 }
1217 } else {
1218 for &x in xs {
1219 o.extend_from_slice(&x.to_le_bytes());
1220 }
1221 }
1222 };
1223 for &x in &self.linear_state {
1227 out.extend_from_slice(&x.to_le_bytes());
1228 }
1229 for &x in &self.imp {
1230 out.extend_from_slice(&x.to_le_bytes());
1231 }
1232 for h in 0..self.num_kv_heads {
1233 u(self.k[h].len() as u32, &mut out);
1234 push(&self.k[h], &mut out);
1235 u(self.v[h].len() as u32, &mut out);
1236 push(&self.v[h], &mut out);
1237 }
1238 Ok(out)
1239 }
1240
1241 pub fn import_wire(&mut self, buf: &[u8]) -> Result<(), String> {
1244 let mut o = 0usize;
1245 let u32_at = |o: &mut usize| -> Result<u32, String> {
1246 if *o + 4 > buf.len() {
1247 return Err("kv import: truncated header".into());
1248 }
1249 let v = u32::from_le_bytes(buf[*o..*o + 4].try_into().unwrap());
1250 *o += 4;
1251 Ok(v)
1252 };
1253 let f16 = u32_at(&mut o)? != 0;
1254 let seq_len = u32_at(&mut o)? as usize;
1255 let heads = u32_at(&mut o)? as usize;
1256 let hd = u32_at(&mut o)? as usize;
1257 let lin = u32_at(&mut o)? as usize;
1258 let nimp = u32_at(&mut o)? as usize;
1259 if heads != self.num_kv_heads || hd != self.head_dim {
1260 return Err(format!(
1261 "kv import: peer sent {heads}×{hd} per position, this layer is {}×{}",
1262 self.num_kv_heads, self.head_dim
1263 ));
1264 }
1265 let w = if f16 { 2 } else { 4 };
1266 let need = |n: usize, o: usize| -> Result<(), String> {
1267 if o + n > buf.len() {
1268 Err("kv import: truncated payload".into())
1269 } else {
1270 Ok(())
1271 }
1272 };
1273 need(lin * 4, o)?;
1274 self.linear_state = (0..lin)
1275 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1276 .collect();
1277 o += lin * 4;
1278 need(nimp * 4, o)?;
1279 let imp: Vec<f32> = (0..nimp)
1280 .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1281 .collect();
1282 o += nimp * 4;
1283 let mut k: Vec<Vec<f32>> = Vec::with_capacity(heads);
1284 let mut v: Vec<Vec<f32>> = Vec::with_capacity(heads);
1285 for _ in 0..heads {
1286 for which in 0..2 {
1287 let n = u32_at(&mut o)? as usize;
1288 need(n * w, o)?;
1289 let xs: Vec<f32> = (0..n)
1290 .map(|i| {
1291 let at = o + i * w;
1292 if f16 {
1293 cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
1294 buf[at..at + 2].try_into().unwrap(),
1295 ))
1296 } else {
1297 f32::from_le_bytes(buf[at..at + 4].try_into().unwrap())
1298 }
1299 })
1300 .collect();
1301 o += n * w;
1302 if which == 0 { k.push(xs) } else { v.push(xs) }
1303 }
1304 }
1305 self.mode = KvMode::F32;
1306 self.k = k;
1307 self.v = v;
1308 self.kq = vec![Vec::new(); heads];
1309 self.ks = vec![Vec::new(); heads];
1310 self.vq = vec![Vec::new(); heads];
1311 self.vs = vec![Vec::new(); heads];
1312 self.kcol = vec![Vec::new(); heads];
1313 self.vcol = vec![Vec::new(); heads];
1314 self.imp = imp;
1315 self.o1 = None;
1316 self.o1_error = None;
1317 self.o1_transitioned = false;
1318 self.seq_len = seq_len;
1319 Ok(())
1320 }
1321
1322 pub fn memory_bytes(&self) -> usize {
1323 let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
1324 + self.v.iter().map(Vec::len).sum::<usize>()
1325 + self.ks.iter().map(Vec::len).sum::<usize>()
1326 + self.vs.iter().map(Vec::len).sum::<usize>()
1327 + self.kcol.iter().map(Vec::len).sum::<usize>()
1328 + self.vcol.iter().map(Vec::len).sum::<usize>();
1329 let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
1330 + self.vq.iter().map(Vec::len).sum::<usize>();
1331 floats * std::mem::size_of::<f32>()
1332 + bytes
1333 + self.linear_state.len() * std::mem::size_of::<f32>()
1337 + self.o1_memory_bytes()
1340 }
1341
1342 fn evict(&mut self, keep_last: usize) {
1344 if self.o1.is_some() || self.seq_len <= keep_last {
1352 return;
1353 }
1354 let drop = self.seq_len - keep_last;
1355 for h in 0..self.num_kv_heads {
1356 let stored = self.head_len(h);
1358 let d = drop.min(stored);
1359 let hd = self.head_dim;
1360 fn drop_front<T>(v: &mut Vec<T>, n: usize) {
1361 let n = n.min(v.len());
1362 v.drain(..n);
1363 }
1364 drop_front(&mut self.k[h], d * hd);
1365 drop_front(&mut self.v[h], d * hd);
1366 drop_front(&mut self.kq[h], d * hd);
1367 drop_front(&mut self.vq[h], d * hd);
1368 drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
1369 drop_front(&mut self.vs[h], d);
1370 }
1371 let d = drop.min(self.imp.len());
1372 self.imp.drain(..d);
1373 self.seq_len = keep_last;
1374 }
1375
1376 fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
1381 if self.o1.is_some() {
1382 return;
1385 }
1386 let stored = self.imp.len();
1387 if stored <= keep_last {
1388 return;
1389 }
1390 let sink_n = sink.min(keep_last);
1393 let recent_n = recent.min(keep_last - sink_n);
1394 let mut keep = vec![false; stored];
1395 for k in keep.iter_mut().take(sink_n) {
1396 *k = true;
1397 }
1398 for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1399 *k = true;
1400 }
1401 let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1402 let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1404 order.sort_by(|&a, &b| {
1405 self.imp[b]
1406 .partial_cmp(&self.imp[a])
1407 .unwrap_or(std::cmp::Ordering::Equal)
1408 });
1409 for i in order {
1410 if budget == 0 {
1411 break;
1412 }
1413 keep[i] = true;
1414 budget -= 1;
1415 }
1416
1417 let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1418 let hd = self.head_dim;
1419 fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1420 let mut out = Vec::with_capacity(kept.len() * step);
1421 for &i in kept {
1422 out.extend_from_slice(&src[i * step..(i + 1) * step]);
1423 }
1424 out
1425 }
1426 for h in 0..self.num_kv_heads {
1431 if !self.k[h].is_empty() {
1432 self.k[h] = gather(&self.k[h], &kept, hd);
1433 }
1434 if !self.v[h].is_empty() {
1435 self.v[h] = gather(&self.v[h], &kept, hd);
1436 }
1437 if !self.kq[h].is_empty() {
1438 self.kq[h] = gather(&self.kq[h], &kept, hd);
1439 self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1440 }
1441 if !self.vq[h].is_empty() {
1442 self.vq[h] = gather(&self.vq[h], &kept, hd);
1443 self.vs[h] = gather(&self.vs[h], &kept, 1);
1444 }
1445 }
1446 self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1447 self.seq_len = kept.len();
1448 }
1449}
1450
1451#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1453pub enum EvictionPolicy {
1454 Recent,
1456 Born { sink: usize },
1458}
1459
1460#[derive(Debug)]
1462pub struct KvCache {
1463 pub layers: Vec<LayerKvCache>,
1464 pub max_seq_len: usize,
1465 pub policy: EvictionPolicy,
1466}
1467
1468impl KvCache {
1469 pub fn new(
1470 num_layers: usize,
1471 num_kv_heads: usize,
1472 head_dim: usize,
1473 max_seq_len: usize,
1474 ) -> Self {
1475 let layers = (0..num_layers)
1476 .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1477 .collect();
1478 Self {
1479 layers,
1480 max_seq_len,
1481 policy: EvictionPolicy::Born { sink: 4 },
1482 }
1483 }
1484
1485 pub fn clear(&mut self) {
1486 for layer in &mut self.layers {
1487 layer.clear();
1488 }
1489 }
1490
1491 pub fn total_memory_bytes(&self) -> usize {
1492 self.layers.iter().map(|l| l.memory_bytes()).sum()
1493 }
1494
1495 pub fn seq_len(&self) -> usize {
1497 self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1498 }
1499
1500 pub fn needs_eviction(&self) -> bool {
1501 self.seq_len() >= self.max_seq_len
1502 }
1503
1504 pub fn evict(&mut self, keep_last: usize) {
1506 match self.policy {
1507 EvictionPolicy::Recent => {
1508 for layer in &mut self.layers {
1509 layer.evict(keep_last);
1510 }
1511 }
1512 EvictionPolicy::Born { sink } => {
1513 let recent = (keep_last / 2).max(1);
1514 for layer in &mut self.layers {
1515 layer.evict_born(keep_last, sink, recent);
1516 }
1517 }
1518 }
1519 }
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525
1526 #[test]
1527 fn wire_round_trip_reproduces_attention() {
1528 let (heads, hd) = (2usize, 4usize);
1532 let mut a = LayerKvCache::new(heads, hd);
1533 for p in 0..5 {
1534 let k: Vec<f32> = (0..heads * hd)
1535 .map(|i| (p * 10 + i) as f32 * 0.031)
1536 .collect();
1537 let v: Vec<f32> = (0..heads * hd)
1538 .map(|i| (p * 7 + i) as f32 * -0.017)
1539 .collect();
1540 a.append(&k, &v, &[true, true]);
1541 }
1542 a.linear_state = vec![0.5, -0.25, 1.0];
1543 let q: Vec<f32> = (0..hd).map(|i| 0.1 * (i as f32 + 1.0)).collect();
1544
1545 let bytes = a.export_wire(false).expect("f32 cache exports");
1546 let mut b = LayerKvCache::new(heads, hd);
1547 b.import_wire(&bytes).expect("import");
1548
1549 assert_eq!(b.seq_len, a.seq_len);
1550 assert_eq!(b.linear_state, a.linear_state);
1551 for h in 0..heads {
1552 let (oa, sa) = a.attend(&q, h);
1553 let (ob, sb) = b.attend(&q, h);
1554 assert_eq!(oa, ob, "head {h} attention output diverged");
1555 assert_eq!(sa, sb, "head {h} attention scores diverged");
1556 }
1557 }
1558
1559 #[test]
1560 fn wire_refuses_what_it_cannot_describe() {
1561 let mut c = LayerKvCache::new(1, 4);
1564 c.mode = KvMode::Q8 { k: true, v: true };
1565 let err = c.export_wire(false).unwrap_err();
1566 assert!(err.contains("F32"), "{err}");
1567 }
1568
1569 #[test]
1570 fn wire_import_checks_geometry() {
1571 let a = LayerKvCache::new(2, 4);
1572 let bytes = a.export_wire(false).unwrap();
1573 let mut wrong = LayerKvCache::new(2, 8);
1574 let err = wrong.import_wire(&bytes).unwrap_err();
1575 assert!(err.contains("2×4"), "{err}");
1576 }
1577
1578 #[test]
1579 fn append_tracks_seq_len_and_layout() {
1580 let mut cache = LayerKvCache::new(4, 8);
1581 cache.mode = KvMode::F32;
1582 assert_eq!(cache.seq_len, 0);
1583
1584 let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1585 let v = vec![2.0f32; 32];
1586 cache.append(&k, &v, &[true; 4]);
1587
1588 assert_eq!(cache.seq_len, 1);
1589 assert_eq!(cache.head_len(0), 1);
1590 assert_eq!(cache.head_keys(1), &k[8..16]);
1592 assert_eq!(cache.memory_bytes(), 256);
1593 }
1594
1595 #[test]
1596 fn dead_head_stores_nothing() {
1597 let mut cache = LayerKvCache::new(2, 4);
1598 cache.mode = KvMode::F32;
1599 let k = vec![1.0f32; 8];
1600 let v = vec![2.0f32; 8];
1601 cache.append(&k, &v, &[true, false]);
1602 cache.append(&k, &v, &[true, false]);
1603
1604 assert_eq!(cache.seq_len, 2);
1605 assert_eq!(cache.head_len(0), 2);
1606 assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1607 assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1608 }
1609
1610 #[test]
1611 fn eviction_keeps_recent() {
1612 let mut cache = KvCache::new(2, 4, 8, 10);
1613 cache.policy = EvictionPolicy::Recent;
1614 for l in &mut cache.layers {
1615 l.mode = KvMode::F32;
1616 }
1617 let k = vec![1.0f32; 32];
1618 let v = vec![2.0f32; 32];
1619 for _ in 0..8 {
1620 for layer in &mut cache.layers {
1621 layer.append(&k, &v, &[true; 4]);
1622 }
1623 }
1624 assert_eq!(cache.seq_len(), 8);
1625 assert!(!cache.needs_eviction());
1626
1627 cache.evict(4);
1628 assert_eq!(cache.seq_len(), 4);
1629 assert_eq!(cache.layers[0].head_len(0), 4);
1630 }
1631
1632 #[test]
1633 fn collecting_o1_eviction_retains_exact_storage_until_boundary() {
1634 const B: usize = 19;
1635 let q = vec![0.1f32; 8];
1636 let k = vec![0.2f32; 4];
1637 let v = vec![0.3f32; 4];
1638
1639 for policy in [EvictionPolicy::Recent, EvictionPolicy::Born { sink: 2 }] {
1640 let mut cache = KvCache::new(1, 1, 4, 6);
1641 cache.policy = policy;
1642 cache.layers[0].mode = KvMode::F32;
1643 cache.layers[0].o1_begin_with_boundary(
1644 4,
1645 8,
1646 2,
1647 crate::nystrom::O1Rect::Aggregate,
1648 Some(B),
1649 );
1650
1651 for pos in 0..B {
1652 {
1653 let layer = &mut cache.layers[0];
1654 layer.o1_push_q(&q);
1655 layer.append(&k, &v, &[]);
1656 }
1657 if pos + 1 < B {
1658 cache.evict(3);
1659 }
1660 }
1661
1662 let layer = &cache.layers[0];
1663 let rows = B * layer.head_dim;
1664 assert_eq!(layer.seq_len, B, "policy {policy:?} retained depth");
1665 assert_eq!(layer.k[0].len(), rows, "policy {policy:?} K rows");
1666 assert_eq!(layer.v[0].len(), rows, "policy {policy:?} V rows");
1667 assert!(
1668 layer.k[0].capacity() >= rows,
1669 "policy {policy:?} K capacity"
1670 );
1671 assert!(
1672 layer.v[0].capacity() >= rows,
1673 "policy {policy:?} V capacity"
1674 );
1675 let q_capacity = match layer.o1.as_ref() {
1676 Some(O1State::Collecting { q_buf, .. }) => q_buf.capacity(),
1677 other => panic!("policy {policy:?} changed state early: {other:?}"),
1678 };
1679 assert!(
1680 q_capacity >= B * 8,
1681 "policy {policy:?} Q capacity must cover the exact prefix"
1682 );
1683
1684 assert!(cache.layers[0].o1_seal_checked(2).unwrap());
1685 assert_eq!(cache.layers[0].k[0].capacity(), 0, "K released after seal");
1686 assert_eq!(cache.layers[0].v[0].capacity(), 0, "V released after seal");
1687 }
1688 }
1689
1690 #[test]
1691 fn truncate_rolls_back_speculative_positions() {
1692 let mut cache = LayerKvCache::new(2, 4);
1693 cache.mode = KvMode::F32;
1694 for pos in 0..5 {
1695 let k = vec![pos as f32; 8];
1696 let v = vec![pos as f32; 8];
1697 cache.append(&k, &v, &[true; 2]);
1698 }
1699 cache.truncate_last(2);
1700 assert_eq!(cache.seq_len, 3);
1701 assert_eq!(cache.head_len(0), 3);
1702 assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1703 }
1704
1705 #[test]
1709 fn q8_attend_matches_f32_within_grid() {
1710 let (heads, hd) = (2, 32);
1711 let mut f = LayerKvCache::new(heads, hd);
1712 f.mode = KvMode::F32;
1713 let mut q8 = LayerKvCache::new(heads, hd);
1714 q8.mode = KvMode::Q8 { k: true, v: true };
1715
1716 let synth = |p: usize, salt: usize| -> Vec<f32> {
1717 (0..heads * hd)
1718 .map(|i| {
1719 let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1720 if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1722 })
1723 .collect()
1724 };
1725 for p in 0..100 {
1726 let k = synth(p, 1);
1727 let v = synth(p, 2);
1728 f.append(&k, &v, &[true; 2]);
1729 q8.append(&k, &v, &[true; 2]);
1730 }
1731 let q: Vec<f32> = (0..hd)
1732 .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1733 .collect();
1734 for g in 0..heads {
1735 let (of, pf) = f.attend(&q, g);
1736 let (o8, p8) = q8.attend(&q, g);
1737 let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1738 for d in 0..hd {
1739 assert!(
1740 (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1741 "g{g} d{d}: f32 {} vs q8 {}",
1742 of[d],
1743 o8[d]
1744 );
1745 }
1746 for p in 0..100 {
1747 assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1748 }
1749 }
1750 q8.truncate_last(30);
1752 assert_eq!(q8.head_len(0), 70);
1753 let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1754 q8.accumulate_imp(&imp);
1755 q8.evict_born(20, 2, 8);
1756 assert_eq!(q8.head_len(0), 20);
1757 let (o, _) = q8.attend(&q, 0);
1758 assert!(o.iter().all(|x| x.is_finite()));
1759 assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1761 }
1762
1763 #[test]
1766 fn attend_group_equals_per_head_attend_bitexact() {
1767 let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1769 let mut c = LayerKvCache::new(kv_heads, hd);
1770 c.mode = mode;
1771 for p in 0..70 {
1772 let k: Vec<f32> = (0..kv_heads * hd)
1773 .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1774 .collect();
1775 let v: Vec<f32> = (0..kv_heads * hd)
1776 .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1777 .collect();
1778 c.append(&k, &v, &[true; 2]);
1779 }
1780 let q: Vec<f32> = (0..kv_heads * hpk * hd)
1781 .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1782 .collect();
1783 for g in 0..kv_heads {
1784 let span = g * hpk * hd..(g + 1) * hpk * hd;
1785 let mut out = vec![0f32; hpk * hd];
1786 let mut imp = vec![0f32; 70];
1787 c.attend_group(
1788 &q[span.clone()],
1789 g,
1790 &mut out,
1791 &mut imp,
1792 1.0 / (hd as f32).sqrt(),
1793 0,
1794 0.0,
1795 &[],
1796 );
1797 let mut imp_ref = vec![0f32; 70];
1798 for h in 0..hpk {
1799 let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1800 let (o, probs) = c.attend(qh, g);
1801 assert_eq!(
1802 &out[h * hd..(h + 1) * hd],
1803 &o[..],
1804 "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1805 );
1806 for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1807 *dst += p;
1808 }
1809 }
1810 assert_eq!(
1811 imp, imp_ref,
1812 "mode {mode:?} g{g}: attention mass must match"
1813 );
1814 }
1815 }
1816 }
1817
1818 #[test]
1825 fn sink_attend_matches_explicit_sink_column() {
1826 let (nkv, hd, hpk) = (2usize, 8usize, 3usize);
1827 let rows = 9usize;
1828 let mut c = LayerKvCache::new(nkv, hd);
1829 c.mode = KvMode::F32;
1830 let kv = |r: usize, i: usize, a: usize, m: usize| {
1831 (((r * a + i * 7 + 3) % m) as f32 / m as f32 - 0.5) * 2.0
1832 };
1833 let mut ks = Vec::new();
1834 let mut vs = Vec::new();
1835 for r in 0..rows {
1836 let k: Vec<f32> = (0..nkv * hd).map(|i| kv(r, i, 31, 97)).collect();
1837 let v: Vec<f32> = (0..nkv * hd).map(|i| kv(r, i, 17, 89)).collect();
1838 c.append(&k, &v, &[]);
1839 ks.push(k);
1840 vs.push(v);
1841 }
1842 let q: Vec<f32> = (0..nkv * hpk * hd)
1843 .map(|i| (((i * 11 + 5) % 83) as f32 / 83.0 - 0.5) * 3.0)
1844 .collect();
1845 let sinks = [0.7f32, -1.3, 2.5, 0.0, -4.0, 6.0];
1847 let scale = 1.0 / (hd as f32).sqrt();
1848 let mut checked = 0usize;
1849 for upto in [1usize, 2, 5, 9] {
1850 for window in [None, Some(3usize), Some(1)] {
1851 let first = window.map(|w| upto.saturating_sub(w)).unwrap_or(0);
1852 for g in 0..nkv {
1853 let qg = &q[g * hpk * hd..(g + 1) * hpk * hd];
1854 let sg = &sinks[g * hpk..(g + 1) * hpk];
1855 let mut out = vec![0f32; hpk * hd];
1856 let mut imp = vec![0f32; upto];
1857 c.attend_group_upto(qg, g, &mut out, &mut imp, scale, first, 0.0, upto, sg);
1858 let mut imp_ref = vec![0f64; upto];
1859 for h in 0..hpk {
1860 let qh = &qg[h * hd..(h + 1) * hd];
1861 let mut z: Vec<f64> = (first..upto)
1863 .map(|p| {
1864 let k = &ks[p][g * hd..(g + 1) * hd];
1865 qh.iter()
1866 .zip(k)
1867 .map(|(&a, &b)| a as f64 * b as f64)
1868 .sum::<f64>()
1869 * scale as f64
1870 })
1871 .collect();
1872 z.push(sg[h] as f64);
1873 let m = z.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1874 let e: Vec<f64> = z.iter().map(|&x| (x - m).exp()).collect();
1875 let s: f64 = e.iter().sum();
1876 let p: Vec<f64> = e.iter().map(|&x| x / s).collect();
1877 for d in 0..hd {
1879 let want: f64 = (first..upto)
1880 .map(|r| p[r - first] * vs[r][g * hd + d] as f64)
1881 .sum();
1882 let got = out[h * hd + d] as f64;
1883 assert!(
1884 (got - want).abs() < 1e-6,
1885 "upto {upto} window {window:?} g{g} h{h} d{d}: {got} vs {want}"
1886 );
1887 }
1888 for r in first..upto {
1889 imp_ref[r] += p[r - first];
1890 }
1891 checked += 1;
1892 }
1893 for r in 0..upto {
1894 assert!(
1895 (imp[r] as f64 - imp_ref[r]).abs() < 1e-6,
1896 "imp upto {upto} window {window:?} g{g} row {r}: {} vs {}",
1897 imp[r],
1898 imp_ref[r]
1899 );
1900 }
1901 let row_mass: f32 = imp.iter().sum();
1903 assert!(row_mass < hpk as f32, "sinks must absorb some mass");
1904 }
1905 }
1906 }
1907 assert_eq!(checked, 4 * 3 * nkv * hpk);
1908 }
1909
1910 #[test]
1915 fn windowed_attend_equals_masked_full_row() {
1916 let (nkv, hd, hpk) = (1usize, 16usize, 2usize);
1917 let rows = 40usize;
1918 let mut c = LayerKvCache::new(nkv, hd);
1919 c.mode = KvMode::F32;
1920 for r in 0..rows {
1921 let k: Vec<f32> = (0..hd)
1922 .map(|i| ((r * 13 + i * 5) % 29) as f32 / 29.0 - 0.5)
1923 .collect();
1924 let v: Vec<f32> = (0..hd)
1925 .map(|i| ((r * 7 + i * 3) % 31) as f32 / 31.0 - 0.5)
1926 .collect();
1927 c.append(&k, &v, &[]);
1928 }
1929 let q: Vec<f32> = (0..hpk * hd)
1930 .map(|i| ((i * 19) % 23) as f32 / 23.0 - 0.5)
1931 .collect();
1932 let scale = 0.25f32;
1933 for w in [1usize, 7, 39, 40, 100] {
1934 let first = rows.saturating_sub(w);
1935 let mut out = vec![0f32; hpk * hd];
1936 let mut imp = vec![0f32; rows];
1937 c.attend_group(&q, 0, &mut out, &mut imp, scale, first, 0.0, &[]);
1938 let mut out_ref = vec![0f32; hpk * hd];
1940 let mut imp_ref = vec![0f32; rows];
1941 for h in 0..hpk {
1942 let mut s = vec![f32::NEG_INFINITY; rows];
1943 for p in first..rows {
1944 s[p] = crate::attention::dot_f32(
1945 &q[h * hd..(h + 1) * hd],
1946 &c.head_keys(0)[p * hd..(p + 1) * hd],
1947 ) * scale;
1948 }
1949 let m = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1950 let mut sum = 0f32;
1951 for v in s.iter_mut() {
1952 *v = (*v - m).exp();
1953 sum += *v;
1954 }
1955 for v in s.iter_mut() {
1956 *v /= sum;
1957 }
1958 for p in first..rows {
1959 if s[p].abs() < 1e-12 {
1960 continue;
1961 }
1962 crate::attention::axpy_f32(
1963 &mut out_ref[h * hd..(h + 1) * hd],
1964 &c.head_values(0)[p * hd..(p + 1) * hd],
1965 s[p],
1966 );
1967 }
1968 for (d, &p) in imp_ref.iter_mut().zip(&s) {
1969 *d += p;
1970 }
1971 }
1972 let bits = |v: &[f32]| v.iter().map(|x| x.to_bits()).collect::<Vec<_>>();
1973 assert_eq!(bits(&out), bits(&out_ref), "out window {w}");
1974 assert_eq!(bits(&imp), bits(&imp_ref), "imp window {w}");
1975 }
1976 }
1977
1978 #[test]
1981 fn sinks_survive_clear_and_wire_import() {
1982 let mut c = LayerKvCache::new(1, 4);
1983 c.mode = KvMode::F32;
1984 c.sinks = Some(vec![0.5, -0.5]);
1985 c.append(&[1.0; 4], &[2.0; 4], &[]);
1986 let wire = c.export_wire(false).unwrap();
1987 c.clear();
1988 assert_eq!(c.sinks.as_deref(), Some(&[0.5f32, -0.5][..]));
1989 c.import_wire(&wire).unwrap();
1990 assert_eq!(c.sinks.as_deref(), Some(&[0.5f32, -0.5][..]));
1991 assert_eq!(c.seq_len, 1);
1992 }
1993
1994 #[test]
1998 fn born_eviction_mixed_modes_stay_consistent() {
1999 for (mk, mv) in [(false, true), (true, false)] {
2000 let mut c = LayerKvCache::new(1, 4);
2001 c.mode = KvMode::Q8 { k: mk, v: mv };
2002 for p in 0..80 {
2003 let k = vec![p as f32 * 0.01; 4];
2004 let v = vec![p as f32; 4];
2005 c.append(&k, &v, &[true]);
2006 }
2007 let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
2008 c.accumulate_imp(&imp);
2009 let before = c.memory_bytes();
2010 c.evict_born(20, 4, 8); assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
2012 assert!(
2013 c.memory_bytes() < before / 2,
2014 "memory must shrink (k={mk} v={mv})"
2015 );
2016 let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
2019 assert!(
2020 out[0] > 30.0,
2021 "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
2022 out[0]
2023 );
2024 }
2025 }
2026
2027 #[test]
2028 fn born_eviction_keeps_high_mass_position() {
2029 let mut cache = KvCache::new(1, 1, 2, 16);
2030 cache.policy = EvictionPolicy::Born { sink: 1 };
2031 for l in &mut cache.layers {
2032 l.mode = KvMode::F32;
2033 }
2034 let layer = &mut cache.layers[0];
2035 for pos in 0..8 {
2038 let k = vec![pos as f32; 2];
2039 let v = vec![pos as f32 + 100.0; 2];
2040 layer.append(&k, &v, &[true]);
2041 }
2042 let mut imp = vec![0.05f32; 8];
2044 imp[3] = 5.0;
2045 layer.accumulate_imp(&imp);
2046
2047 cache.evict(4); let layer = &cache.layers[0];
2049 assert_eq!(layer.seq_len, 4);
2050 let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
2051 assert_eq!(
2052 kept_keys,
2053 vec![0.0, 3.0, 6.0, 7.0],
2054 "kept = sink(0) + mass-top(3) + recent(6,7)"
2055 );
2056 assert_eq!(layer.head_len(0), 4);
2058 }
2059}