Skip to main content

cortiq_engine/
kv_cache.rs

1//! KV cache — per-layer, head-major storage.
2//!
3//! Layout: one contiguous `Vec<f32>` per KV head (`[pos × head_dim]`),
4//! so per-head attention reads a straight slice — no per-head gather
5//! copies per token. Dead GQA groups (all Q heads masked) store
6//! nothing at all: masked heads cost neither FLOPs nor memory.
7
8/// KV storage mode. `CMF_KV=q8` enables the q8_2f cache: an int8 row per
9/// (position, head) + an f32 scale per row + a per-channel field 𝒲×θ,
10/// frozen after WARMUP positions with retroactive requantization
11/// (D4: "KV-quant 2f"). Memory ×~3.7 smaller than f32.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum KvMode {
14    F32,
15    /// Quantized components: (K, V) — sensitivity diagnostics.
16    Q8 { k: bool, v: bool },
17}
18
19impl KvMode {
20    pub fn from_env() -> Self {
21        match std::env::var("CMF_KV").as_deref() {
22            Ok("q8") | Ok("q8_2f") => KvMode::Q8 { k: true, v: true },
23            Ok("q8k") => KvMode::Q8 { k: true, v: false },
24            Ok("q8v") => KvMode::Q8 { k: false, v: true },
25            _ => KvMode::F32,
26        }
27    }
28
29    fn quant_k(self) -> bool {
30        matches!(self, KvMode::Q8 { k: true, .. })
31    }
32
33    fn quant_v(self) -> bool {
34        matches!(self, KvMode::Q8 { v: true, .. })
35    }
36}
37
38/// Positions before freezing the per-channel field (2f): before — col ≡ 1,
39/// after — col = RMS over channels of the stored rows, old rows are requantized.
40const KV_COL_WARMUP: usize = 64;
41
42/// K-rows are quantized in groups of 32 channels (scale per group):
43/// attention logits are sensitive to the dot-product error, per-group scales
44/// localize it along RoPE bands (35B: +4.6% PPL with a per-row scale
45/// → target <1% with a per-group one). V — per-row scale (measured +0.56%).
46const KV_K_GROUP: usize = 32;
47
48/// Per-layer O(1) Nyström attention state (runtime `attn_type`
49/// override — spec §7 presence-driven pattern, no format change).
50///
51/// Collecting: the prompt pass still runs EXACT cache attention (the
52/// prefill outputs feed the residual stream, so they cannot be
53/// deferred) while the per-position rotated queries are buffered;
54/// `o1_seal()` then freezes landmarks + M from the full prompt, replays
55/// it into per-KV-group streaming states, and DROPS the full KV.
56/// Sealed: decode replaces cache attention with
57/// `NystromState::step_group()`.
58#[derive(Debug, Clone)]
59pub enum O1State {
60    Collecting {
61        m: usize,
62        w: usize,
63        sink: usize,
64        rect: crate::nystrom::O1Rect,
65        /// Rotated post-norm queries, `[pos × num_heads × head_dim]`.
66        q_buf: Vec<f32>,
67    },
68    /// One state per KV GROUP, each holding its group's Q heads. The
69    /// exact window / sinks / K̃ are stored once per group (every Q head
70    /// of the group reads the same k/v rows); only the far field, Q̃ and
71    /// M — the query-dependent pieces — stay per Q head. See
72    /// `NystromState` for which piece is which and why.
73    Sealed { groups: Vec<crate::nystrom::NystromState> },
74}
75
76/// KV cache for a single layer, head-major.
77#[derive(Debug, Clone)]
78pub struct LayerKvCache {
79    pub mode: KvMode,
80    /// Per-KV-head keys: `k[h]` is `[seq_len × head_dim]` (empty if head is dead).
81    k: Vec<Vec<f32>>,
82    /// Per-KV-head values, same layout.
83    v: Vec<Vec<f32>>,
84    /// q8 storage (mode == Q8_2F): int8 rows + f32 scale per row.
85    kq: Vec<Vec<i8>>,
86    ks: Vec<Vec<f32>>,
87    vq: Vec<Vec<i8>>,
88    vs: Vec<Vec<f32>>,
89    /// Per-channel fields 𝒲×θ per head [head_dim]; empty until frozen.
90    kcol: Vec<Vec<f32>>,
91    vcol: Vec<Vec<f32>>,
92    /// Accumulated attention mass per stored position (Born rule:
93    /// importance of a position = how much probability mass reads it).
94    imp: Vec<f32>,
95    /// Positions appended so far (grows once per token, dead heads included).
96    pub seq_len: usize,
97    pub num_kv_heads: usize,
98    pub head_dim: usize,
99    /// Linear-core condensate S (vmf_phase), f64; empty on full layers.
100    pub linear_state: Vec<f32>,
101    /// Tentative lane-2 state during speculative verify.
102    pub linear_scratch: Vec<f32>,
103    /// O(1) Nyström override (None = plain cache attention).
104    pub o1: Option<O1State>,
105}
106
107impl LayerKvCache {
108    pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
109        Self {
110            mode: KvMode::from_env(),
111            k: vec![Vec::new(); num_kv_heads],
112            v: vec![Vec::new(); num_kv_heads],
113            kq: vec![Vec::new(); num_kv_heads],
114            ks: vec![Vec::new(); num_kv_heads],
115            vq: vec![Vec::new(); num_kv_heads],
116            vs: vec![Vec::new(); num_kv_heads],
117            kcol: vec![Vec::new(); num_kv_heads],
118            vcol: vec![Vec::new(); num_kv_heads],
119            imp: Vec::new(),
120            seq_len: 0,
121            num_kv_heads,
122            head_dim,
123            linear_state: Vec::new(),
124            linear_scratch: Vec::new(),
125            o1: None,
126        }
127    }
128
129    // ── O(1) Nyström override ──
130
131    /// Arm query collection for a fresh prompt pass (a cleared cache).
132    pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
133        self.o1 = Some(O1State::Collecting { m, w, sink, rect, q_buf: Vec::new() });
134    }
135
136    /// Record one position's rotated queries (`[num_heads × head_dim]`)
137    /// during the exact prompt pass. No-op unless collecting — the hook
138    /// sits inside qwen_attention so every prefill flavor (sequential,
139    /// batched) feeds the same trace.
140    pub fn o1_push_q(&mut self, q_all: &[f32]) {
141        if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
142            q_buf.extend_from_slice(q_all);
143        }
144    }
145
146    pub fn o1_sealed(&self) -> bool {
147        matches!(self.o1, Some(O1State::Sealed { .. }))
148    }
149
150    /// Freeze the prompt into per-KV-group Nyström states and drop this
151    /// layer's full KV. Returns false (layer stays exact, KV kept) when
152    /// the preconditions fail: the seal needs f32 KV rows (`CMF_KV=q8`
153    /// stores int8), every group densely stored, a full q trace, and a
154    /// GQA fan-out that actually divides.
155    pub fn o1_seal(&mut self, num_heads: usize) -> bool {
156        // Idempotent: sealing a sealed (or plain) layer must not
157        // disturb its state — check before take().
158        if !matches!(self.o1, Some(O1State::Collecting { .. })) {
159            return self.o1_sealed();
160        }
161        let Some(O1State::Collecting { m, w, sink, rect, q_buf }) = self.o1.take() else {
162            unreachable!("checked above");
163        };
164        let (hd, t) = (self.head_dim, self.seq_len);
165        let nkv = self.num_kv_heads.max(1);
166        let hpk = num_heads / nkv;
167        let ok = t > 0
168            && self.mode == KvMode::F32
169            && q_buf.len() == t * num_heads * hd
170            && hpk * nkv == num_heads
171            && (0..self.num_kv_heads).all(|g| self.head_len(g) == t);
172        if !ok {
173            tracing::warn!(
174                "o1: cannot seal (needs f32 KV mode, dense heads, full query \
175                 trace, num_heads divisible by num_kv_heads) — layer keeps \
176                 exact attention"
177            );
178            return false;
179        }
180        let mut groups = Vec::with_capacity(self.num_kv_heads);
181        // Query trace is position-major; the state wants each head's
182        // queries contiguous, so transpose one group at a time.
183        let mut qh = vec![0.0f32; hpk * t * hd];
184        for g in 0..self.num_kv_heads {
185            for hh in 0..hpk {
186                let h = g * hpk + hh;
187                for p in 0..t {
188                    let src = (p * num_heads + h) * hd;
189                    let dst = (hh * t + p) * hd;
190                    qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
191                }
192            }
193            let qs: Vec<&[f32]> = (0..hpk).map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd]).collect();
194            let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
195            st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
196            groups.push(st);
197        }
198        // The states now carry everything decode needs — release the
199        // O(context) storage (this is the memory claim, not a cosmetic).
200        for h in 0..self.num_kv_heads {
201            self.k[h] = Vec::new();
202            self.v[h] = Vec::new();
203        }
204        self.imp = Vec::new();
205        self.o1 = Some(O1State::Sealed { groups });
206        true
207    }
208
209    /// One decode step on a sealed layer: per KV group, insert the
210    /// group's fresh (k, v) ONCE and read every Q head's attention
211    /// output. Returns `[num_heads × head_dim]`. Head h belongs to group
212    /// h/hpk, so a group's Q heads are contiguous in `q_all`/`out` —
213    /// same math as the shared KV row the exact path appends once.
214    pub fn o1_step(
215        &mut self,
216        q_all: &[f32],
217        k_new: &[f32],
218        v_new: &[f32],
219        num_heads: usize,
220    ) -> Vec<f32> {
221        let hd = self.head_dim;
222        let hpk = num_heads / self.num_kv_heads.max(1);
223        let mut out = vec![0.0f32; num_heads * hd];
224        let Some(O1State::Sealed { groups }) = &mut self.o1 else {
225            debug_assert!(false, "o1_step on an unsealed layer");
226            return out;
227        };
228        for (g, st) in groups.iter_mut().enumerate() {
229            let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
230            st.step_group(
231                &q_all[lo..hi],
232                &k_new[g * hd..(g + 1) * hd],
233                &v_new[g * hd..(g + 1) * hd],
234                &mut out[lo..hi],
235            );
236        }
237        // Track the true context depth for the honest memory/seq report
238        // (nothing is stored per position — the state is O(1)).
239        self.seq_len += 1;
240        out
241    }
242
243    /// Bytes held by the O(1) override (query trace while collecting,
244    /// per-KV-group states once sealed).
245    pub fn o1_memory_bytes(&self) -> usize {
246        match &self.o1 {
247            Some(O1State::Collecting { q_buf, .. }) => {
248                q_buf.len() * std::mem::size_of::<f32>()
249            }
250            Some(O1State::Sealed { groups }) => {
251                groups.iter().map(|s| s.memory_bytes()).sum()
252            }
253            None => 0,
254        }
255    }
256
257    /// Quantize one row against the per-channel field (empty col = 1);
258    /// `group` — elements per scale (the whole row or KV_K_GROUP).
259    fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>,
260                 group: usize) {
261        let mut resid = vec![0.0f32; row.len()];
262        for (d, &x) in row.iter().enumerate() {
263            resid[d] = if col.is_empty() { x } else { x / col[d] };
264        }
265        for g0 in (0..row.len()).step_by(group) {
266            let g1 = (g0 + group).min(row.len());
267            let mut absmax = 0.0f32;
268            for &r in &resid[g0..g1] {
269                absmax = absmax.max(r.abs());
270            }
271            let s = (absmax / 127.0).max(1e-12);
272            sc.push(s);
273            for &r in &resid[g0..g1] {
274                q.push((r / s).round().clamp(-127.0, 127.0) as i8);
275            }
276        }
277    }
278
279    /// Freeze the 2f field: col = RMS of channels over stored rows, old
280    /// rows are requantized against the new field (once per conversation).
281    fn freeze_cols(&mut self) {
282        let hd = self.head_dim;
283        let ngk = hd.div_ceil(KV_K_GROUP);
284        for h in 0..self.num_kv_heads {
285            for (qv, sv, colv, group) in [
286                (&mut self.kq[h], &mut self.ks[h], &mut self.kcol[h], KV_K_GROUP),
287                (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
288            ] {
289                let spp = if group == hd { 1 } else { ngk }; // scales per position
290                let n = sv.len() / spp;
291                if n == 0 {
292                    continue;
293                }
294                // Dequantize to f32, RMS over channels, requantize.
295                let mut rows = vec![0.0f32; n * hd];
296                for p in 0..n {
297                    for d in 0..hd {
298                        rows[p * hd + d] =
299                            qv[p * hd + d] as f32 * sv[p * spp + d / group];
300                    }
301                }
302                let mut col = vec![0.0f32; hd];
303                for p in 0..n {
304                    for d in 0..hd {
305                        col[d] += rows[p * hd + d] * rows[p * hd + d];
306                    }
307                }
308                for c in col.iter_mut() {
309                    *c = (*c / n as f32).sqrt().max(1e-6);
310                }
311                qv.clear();
312                sv.clear();
313                for p in 0..n {
314                    Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
315                }
316                *colv = col;
317            }
318        }
319    }
320
321    /// Append K/V for one position. `k_new`/`v_new` are
322    /// `[num_kv_heads × head_dim]`; heads with `alive[h] == false` are
323    /// skipped (their slices stay empty).
324    pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
325        debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
326        debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
327        // Freeze the 2f field AT THE START of append: only rows that
328        // survived verify are visible (a rejected lane-2 draft does not
329        // pollute the field — found in review), and the threshold uses >=
330        // rather than strict equality (in small windows eviction may
331        // oscillate across 64).
332        if matches!(self.mode, KvMode::Q8 { .. })
333            && self.seq_len >= KV_COL_WARMUP
334            && self.kcol.iter().all(Vec::is_empty)
335            && self.vcol.iter().all(Vec::is_empty)
336        {
337            self.freeze_cols();
338        }
339        for h in 0..self.num_kv_heads {
340            if !alive.get(h).copied().unwrap_or(true) {
341                continue;
342            }
343            let s = h * self.head_dim;
344            if self.mode.quant_k() {
345                Self::quant_row(&k_new[s..s + self.head_dim],
346                                &self.kcol[h], &mut self.kq[h], &mut self.ks[h],
347                                KV_K_GROUP);
348            } else {
349                self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
350            }
351            if self.mode.quant_v() {
352                Self::quant_row(&v_new[s..s + self.head_dim],
353                                &self.vcol[h], &mut self.vq[h], &mut self.vs[h],
354                                self.head_dim);
355            } else {
356                self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
357            }
358        }
359        self.imp.push(0.0);
360        self.seq_len += 1;
361    }
362
363    /// Per-head attention over its own storage: the f32 branch is
364    /// bit-for-bit equal to attention_head() over slices; the q8 branch
365    /// computes score = s_k·⟨q⊙col_k, k_q⟩ and the weighted sum of V in i8
366    /// with f32 accumulation. Returns (output [head_dim], probs [stored]).
367    pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
368        let hd = self.head_dim;
369        if self.mode == KvMode::F32 {
370            let stored = self.k[kv_head].len() / hd;
371            return crate::attention::attention_head(
372                q, &self.k[kv_head], &self.v[kv_head], hd, stored);
373        }
374        let stored = self.head_len(kv_head);
375        let scale = 1.0 / (hd as f32).sqrt();
376        let mut scores = vec![0.0f32; stored];
377        if self.mode.quant_k() {
378            let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
379            // q ⊙ col_k — once per call.
380            let kcol = &self.kcol[kv_head];
381            let mut qc = vec![0.0f32; hd];
382            for d in 0..hd {
383                qc[d] = if kcol.is_empty() { q[d] } else { q[d] * kcol[d] };
384            }
385            let ng = hd.div_ceil(KV_K_GROUP);
386            for p in 0..stored {
387                let row = &kq[p * hd..(p + 1) * hd];
388                // SAFETY: i8 and u8 share layout; dot_i8_f32 reads the
389                // bytes back as i8.
390                let row_u8 = unsafe {
391                    std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
392                };
393                let mut dot = 0.0f32;
394                for g in 0..ng {
395                    let g0 = g * KV_K_GROUP;
396                    let g1 = (g0 + KV_K_GROUP).min(hd);
397                    dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1])
398                        * ks[p * ng + g];
399                }
400                scores[p] = dot * scale;
401            }
402        } else {
403            let k = &self.k[kv_head];
404            for p in 0..stored {
405                let row = &k[p * hd..(p + 1) * hd];
406                scores[p] = crate::attention::dot_f32(q, row) * scale;
407            }
408        }
409        let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
410        let mut sum = 0.0f32;
411        for s in scores.iter_mut() {
412            *s = (*s - max_score).exp();
413            sum += *s;
414        }
415        if sum > 0.0 {
416            for s in scores.iter_mut() {
417                *s /= sum;
418            }
419        }
420        let mut acc = vec![0.0f32; hd];
421        if self.mode.quant_v() {
422            let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
423            for p in 0..stored {
424                let w = scores[p] * vs[p];
425                if w.abs() < 1e-12 {
426                    continue;
427                }
428                crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
429            }
430            let vcol = &self.vcol[kv_head];
431            if !vcol.is_empty() {
432                for d in 0..hd {
433                    acc[d] *= vcol[d];
434                }
435            }
436        } else {
437            let v = &self.v[kv_head];
438            for p in 0..stored {
439                let w = scores[p];
440                if w.abs() < 1e-12 {
441                    continue;
442                }
443                crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
444            }
445        }
446        (acc, scores)
447    }
448
449    /// Grouped GQA attention: all Q-heads of one KV group in a single
450    /// pass over the stored K rows and a single pass over the V rows
451    /// (per-head `attend` re-read the shared group storage
452    /// heads_per_kv times — roadmap §3 P1). Per-head score order,
453    /// softmax and V accumulation are IDENTICAL to `attend`, so each
454    /// head's output is bit-for-bit the same.
455    ///
456    /// `q_group`: `[n_heads_in_group × head_dim]` (global head order);
457    /// `out`: same shape; `imp_acc[0..stored]` accumulates the probs of
458    /// every head (Born importance), matching the caller's former loop.
459    pub fn attend_group(
460        &self,
461        q_group: &[f32],
462        kv_head: usize,
463        out: &mut [f32],
464        imp_acc: &mut [f32],
465    ) {
466        let hd = self.head_dim;
467        let nheads = q_group.len() / hd;
468        debug_assert_eq!(out.len(), nheads * hd);
469        let stored = if self.mode == KvMode::F32 {
470            self.k[kv_head].len() / hd
471        } else {
472            self.head_len(kv_head)
473        };
474        if stored == 0 {
475            out.fill(0.0);
476            return;
477        }
478        let scale = 1.0 / (hd as f32).sqrt();
479
480        thread_local! {
481            /// scores [nheads × stored] — reused across layers/tokens.
482            static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
483                const { std::cell::RefCell::new(Vec::new()) };
484            /// q ⊙ col_k per head (q8 K mode).
485            static GQA_QC: std::cell::RefCell<Vec<f32>> =
486                const { std::cell::RefCell::new(Vec::new()) };
487        }
488
489        GQA_SCORES.with(|sc| {
490            let mut scores = sc.borrow_mut();
491            scores.resize(nheads * stored, 0.0);
492
493            // ── score pass: each stored K row is read ONCE for all heads.
494            if self.mode.quant_k() {
495                let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
496                let kcol = &self.kcol[kv_head];
497                let ng = hd.div_ceil(KV_K_GROUP);
498                GQA_QC.with(|qc| {
499                    let mut qcb = qc.borrow_mut();
500                    qcb.resize(nheads * hd, 0.0);
501                    for h in 0..nheads {
502                        for d in 0..hd {
503                            let qv = q_group[h * hd + d];
504                            qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
505                        }
506                    }
507                    for p in 0..stored {
508                        let row = &kq[p * hd..(p + 1) * hd];
509                        // SAFETY: i8 and u8 share layout; dot_i8_f32 reads
510                        // the bytes back as i8.
511                        let row_u8 = unsafe {
512                            std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
513                        };
514                        for h in 0..nheads {
515                            let qch = &qcb[h * hd..(h + 1) * hd];
516                            let mut dot = 0.0f32;
517                            for g in 0..ng {
518                                let g0 = g * KV_K_GROUP;
519                                let g1 = (g0 + KV_K_GROUP).min(hd);
520                                dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
521                                    * ks[p * ng + g];
522                            }
523                            scores[h * stored + p] = dot * scale;
524                        }
525                    }
526                });
527            } else {
528                let k = &self.k[kv_head];
529                for p in 0..stored {
530                    let row = &k[p * hd..(p + 1) * hd];
531                    for h in 0..nheads {
532                        scores[h * stored + p] =
533                            crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row)
534                                * scale;
535                    }
536                }
537            }
538
539            // ── per-head softmax (identical to attend / attention_head).
540            for h in 0..nheads {
541                let s = &mut scores[h * stored..(h + 1) * stored];
542                let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
543                let mut sum = 0.0f32;
544                for v in s.iter_mut() {
545                    *v = (*v - max_score).exp();
546                    sum += *v;
547                }
548                if sum > 0.0 {
549                    for v in s.iter_mut() {
550                        *v /= sum;
551                    }
552                }
553            }
554
555            // ── value pass: each stored V row is read ONCE for all heads.
556            out.fill(0.0);
557            if self.mode.quant_v() {
558                let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
559                for p in 0..stored {
560                    let row = &vq[p * hd..(p + 1) * hd];
561                    for h in 0..nheads {
562                        let w = scores[h * stored + p] * vs[p];
563                        if w.abs() < 1e-12 {
564                            continue;
565                        }
566                        crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
567                    }
568                }
569                let vcol = &self.vcol[kv_head];
570                if !vcol.is_empty() {
571                    for h in 0..nheads {
572                        for d in 0..hd {
573                            out[h * hd + d] *= vcol[d];
574                        }
575                    }
576                }
577            } else {
578                let v = &self.v[kv_head];
579                for p in 0..stored {
580                    let row = &v[p * hd..(p + 1) * hd];
581                    for h in 0..nheads {
582                        let w = scores[h * stored + p];
583                        if w.abs() < 1e-12 {
584                            continue;
585                        }
586                        crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
587                    }
588                }
589            }
590
591            // ── Born-importance accumulation (Σ probs over heads), same
592            // head order as the caller's former per-head loop.
593            let n = imp_acc.len().min(stored);
594            for h in 0..nheads {
595                let s = &scores[h * stored..(h + 1) * stored];
596                for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
597                    *dst += p;
598                }
599            }
600        });
601    }
602
603    /// Roll back the last `n_drop` positions (speculative-decode reject).
604    pub fn truncate_last(&mut self, n_drop: usize) {
605        let d = n_drop.min(self.seq_len);
606        for h in 0..self.num_kv_heads {
607            let keep = self.k[h].len().saturating_sub(d * self.head_dim);
608            self.k[h].truncate(keep);
609            self.v[h].truncate(keep);
610            let ngk = self.head_dim.div_ceil(KV_K_GROUP);
611            let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
612            self.kq[h].truncate(keep_q);
613            let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
614            self.vq[h].truncate(keep_vq);
615            let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
616            self.ks[h].truncate(keep_ks);
617            let keep_vs = self.vs[h].len().saturating_sub(d);
618            self.vs[h].truncate(keep_vs);
619        }
620        self.imp.truncate(self.imp.len().saturating_sub(d));
621        self.seq_len -= d;
622    }
623
624    /// Accumulate attention mass per stored position (summed over heads).
625    pub fn accumulate_imp(&mut self, probs: &[f32]) {
626        for (dst, &p) in self.imp.iter_mut().zip(probs) {
627            *dst += p;
628        }
629    }
630
631    /// Contiguous keys of one head: `[stored_len × head_dim]`.
632    pub fn head_keys(&self, kv_head: usize) -> &[f32] {
633        &self.k[kv_head]
634    }
635
636    pub fn head_values(&self, kv_head: usize) -> &[f32] {
637        &self.v[kv_head]
638    }
639
640    /// Number of positions actually stored for a head (0 for dead heads).
641    pub fn head_len(&self, kv_head: usize) -> usize {
642        let ng = self.head_dim.div_ceil(KV_K_GROUP);
643        (self.k[kv_head].len() / self.head_dim)
644            .max(self.ks[kv_head].len() / ng)
645            .max(self.vs[kv_head].len())
646    }
647
648    /// Clear cache (e.g. on new conversation or task switch).
649    pub fn clear(&mut self) {
650        for h in 0..self.num_kv_heads {
651            self.k[h].clear();
652            self.v[h].clear();
653            self.kq[h].clear();
654            self.ks[h].clear();
655            self.vq[h].clear();
656            self.vs[h].clear();
657            self.kcol[h].clear();
658            self.vcol[h].clear();
659        }
660        self.imp.clear();
661        self.linear_state.clear();
662        self.linear_scratch.clear();
663        // Fresh conversation → the pipeline re-arms collection if the
664        // layer is o1-flagged (landmarks are per-prompt, never reused).
665        self.o1 = None;
666        self.seq_len = 0;
667    }
668
669    /// Memory usage in bytes.
670    pub fn memory_bytes(&self) -> usize {
671        let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
672            + self.v.iter().map(Vec::len).sum::<usize>()
673            + self.ks.iter().map(Vec::len).sum::<usize>()
674            + self.vs.iter().map(Vec::len).sum::<usize>()
675            + self.kcol.iter().map(Vec::len).sum::<usize>()
676            + self.vcol.iter().map(Vec::len).sum::<usize>();
677        let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
678            + self.vq.iter().map(Vec::len).sum::<usize>();
679        floats * std::mem::size_of::<f32>()
680            + bytes
681            // O(1) recurrent state of linear-core layers (vmf_phase/GDN):
682            // constant in context, but real memory — the honest "KV+state"
683            // line must count it (a pure-linear model reported 0 before).
684            + self.linear_state.len() * std::mem::size_of::<f32>()
685            // O(1) Nyström state (window + sinks + skeleton) — same
686            // discipline: constant in context, but real memory.
687            + self.o1_memory_bytes()
688    }
689
690    /// Drop oldest positions, keeping the last `keep_last`.
691    fn evict(&mut self, keep_last: usize) {
692        // A sealed o1 layer stores nothing per position — the Nyström
693        // state IS the eviction policy; resetting seq_len here would lie
694        // about the context depth.
695        if self.o1_sealed() || self.seq_len <= keep_last {
696            return;
697        }
698        let drop = self.seq_len - keep_last;
699        for h in 0..self.num_kv_heads {
700            // Dead heads store fewer positions; drop proportionally.
701            let stored = self.head_len(h);
702            let d = drop.min(stored);
703            let hd = self.head_dim;
704            fn drop_front<T>(v: &mut Vec<T>, n: usize) {
705                let n = n.min(v.len());
706                v.drain(..n);
707            }
708            drop_front(&mut self.k[h], d * hd);
709            drop_front(&mut self.v[h], d * hd);
710            drop_front(&mut self.kq[h], d * hd);
711            drop_front(&mut self.vq[h], d * hd);
712            drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
713            drop_front(&mut self.vs[h], d);
714        }
715        let d = drop.min(self.imp.len());
716        self.imp.drain(..d);
717        self.seq_len = keep_last;
718    }
719
720    /// Born eviction: keep `sink` earliest positions (attention sinks),
721    /// the `recent` latest, and fill the rest of the `keep_last` budget
722    /// with the positions carrying the highest accumulated attention
723    /// mass (vmfcore: PPL 8.342 vs 8.687 for recency-only, full 8.295).
724    fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
725        if self.o1_sealed() {
726            return; // see evict(): the o1 state is its own eviction
727        }
728        let stored = self.imp.len();
729        if stored <= keep_last {
730            return;
731        }
732        // Budget discipline: sinks first, recents next, both clamped so
733        // the total never exceeds keep_last.
734        let sink_n = sink.min(keep_last);
735        let recent_n = recent.min(keep_last - sink_n);
736        let mut keep = vec![false; stored];
737        for k in keep.iter_mut().take(sink_n) {
738            *k = true;
739        }
740        for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
741            *k = true;
742        }
743        let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
744        // Highest accumulated mass first among the middle positions.
745        let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
746        order.sort_by(|&a, &b| {
747            self.imp[b].partial_cmp(&self.imp[a]).unwrap_or(std::cmp::Ordering::Equal)
748        });
749        for i in order {
750            if budget == 0 {
751                break;
752            }
753            keep[i] = true;
754            budget -= 1;
755        }
756
757        let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
758        let hd = self.head_dim;
759        fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
760            let mut out = Vec::with_capacity(kept.len() * step);
761            for &i in kept {
762                out.extend_from_slice(&src[i * step..(i + 1) * step]);
763            }
764            out
765        }
766        // Each storage is gathered INDEPENDENTLY: in mixed modes
767        // (q8k/q8v) K and V live in different storages — the paired branch
768        // panicked (q8v) or silently left V uncompressed (q8k);
769        // found by adversarial review, closed by regression tests.
770        for h in 0..self.num_kv_heads {
771            if !self.k[h].is_empty() {
772                self.k[h] = gather(&self.k[h], &kept, hd);
773            }
774            if !self.v[h].is_empty() {
775                self.v[h] = gather(&self.v[h], &kept, hd);
776            }
777            if !self.kq[h].is_empty() {
778                self.kq[h] = gather(&self.kq[h], &kept, hd);
779                self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
780            }
781            if !self.vq[h].is_empty() {
782                self.vq[h] = gather(&self.vq[h], &kept, hd);
783                self.vs[h] = gather(&self.vs[h], &kept, 1);
784            }
785        }
786        self.imp = kept.iter().map(|&i| self.imp[i]).collect();
787        self.seq_len = kept.len();
788    }
789}
790
791/// Eviction policy for a bounded cache.
792#[derive(Debug, Clone, Copy, PartialEq, Eq)]
793pub enum EvictionPolicy {
794    /// Sliding window: keep only the most recent positions.
795    Recent,
796    /// Born rule: sinks + recents + top accumulated attention mass.
797    Born { sink: usize },
798}
799
800/// Full KV cache for all layers.
801#[derive(Debug)]
802pub struct KvCache {
803    pub layers: Vec<LayerKvCache>,
804    pub max_seq_len: usize,
805    pub policy: EvictionPolicy,
806}
807
808impl KvCache {
809    pub fn new(num_layers: usize, num_kv_heads: usize, head_dim: usize, max_seq_len: usize) -> Self {
810        let layers = (0..num_layers)
811            .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
812            .collect();
813        Self {
814            layers,
815            max_seq_len,
816            policy: EvictionPolicy::Born { sink: 4 },
817        }
818    }
819
820    pub fn clear(&mut self) {
821        for layer in &mut self.layers {
822            layer.clear();
823        }
824    }
825
826    pub fn total_memory_bytes(&self) -> usize {
827        self.layers.iter().map(|l| l.memory_bytes()).sum()
828    }
829
830    /// Current sequence length (max across layers — dead layers may lag).
831    pub fn seq_len(&self) -> usize {
832        self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
833    }
834
835    pub fn needs_eviction(&self) -> bool {
836        self.seq_len() >= self.max_seq_len
837    }
838
839    /// Evict down to `keep_last` positions according to the policy.
840    pub fn evict(&mut self, keep_last: usize) {
841        match self.policy {
842            EvictionPolicy::Recent => {
843                for layer in &mut self.layers {
844                    layer.evict(keep_last);
845                }
846            }
847            EvictionPolicy::Born { sink } => {
848                let recent = (keep_last / 2).max(1);
849                for layer in &mut self.layers {
850                    layer.evict_born(keep_last, sink, recent);
851                }
852            }
853        }
854    }
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    #[test]
862    fn append_tracks_seq_len_and_layout() {
863        let mut cache = LayerKvCache::new(4, 8);
864        cache.mode = KvMode::F32;
865        assert_eq!(cache.seq_len, 0);
866
867        let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
868        let v = vec![2.0f32; 32];
869        cache.append(&k, &v, &[true; 4]);
870
871        assert_eq!(cache.seq_len, 1);
872        assert_eq!(cache.head_len(0), 1);
873        // head 1 slice is contiguous and equals its part of k_new
874        assert_eq!(cache.head_keys(1), &k[8..16]);
875        assert_eq!(cache.memory_bytes(), 256);
876    }
877
878    #[test]
879    fn dead_head_stores_nothing() {
880        let mut cache = LayerKvCache::new(2, 4);
881        cache.mode = KvMode::F32;
882        let k = vec![1.0f32; 8];
883        let v = vec![2.0f32; 8];
884        cache.append(&k, &v, &[true, false]);
885        cache.append(&k, &v, &[true, false]);
886
887        assert_eq!(cache.seq_len, 2);
888        assert_eq!(cache.head_len(0), 2);
889        assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
890        assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
891    }
892
893    #[test]
894    fn eviction_keeps_recent() {
895        let mut cache = KvCache::new(2, 4, 8, 10);
896        cache.policy = EvictionPolicy::Recent;
897        for l in &mut cache.layers { l.mode = KvMode::F32; }
898        let k = vec![1.0f32; 32];
899        let v = vec![2.0f32; 32];
900        for _ in 0..8 {
901            for layer in &mut cache.layers {
902                layer.append(&k, &v, &[true; 4]);
903            }
904        }
905        assert_eq!(cache.seq_len(), 8);
906        assert!(!cache.needs_eviction());
907
908        cache.evict(4);
909        assert_eq!(cache.seq_len(), 4);
910        assert_eq!(cache.layers[0].head_len(0), 4);
911    }
912
913    #[test]
914    fn truncate_rolls_back_speculative_positions() {
915        let mut cache = LayerKvCache::new(2, 4);
916        cache.mode = KvMode::F32;
917        for pos in 0..5 {
918            let k = vec![pos as f32; 8];
919            let v = vec![pos as f32; 8];
920            cache.append(&k, &v, &[true; 2]);
921        }
922        cache.truncate_last(2);
923        assert_eq!(cache.seq_len, 3);
924        assert_eq!(cache.head_len(0), 3);
925        assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
926    }
927
928    /// q8_2f-attend ≈ f32-attend: 100 positions (crosses the field freeze
929    /// at the 64th), pseudo-random vectors, relative tolerance of the
930    /// int8 grid. Plus rollback and Born eviction on the q8 storage.
931    #[test]
932    fn q8_attend_matches_f32_within_grid() {
933        let (heads, hd) = (2, 32);
934        let mut f = LayerKvCache::new(heads, hd);
935        f.mode = KvMode::F32;
936        let mut q8 = LayerKvCache::new(heads, hd);
937        q8.mode = KvMode::Q8 { k: true, v: true };
938
939        let synth = |p: usize, salt: usize| -> Vec<f32> {
940            (0..heads * hd)
941                .map(|i| {
942                    let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
943                    // channel structure: even channels ×4 (checks the 2f field)
944                    if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
945                })
946                .collect()
947        };
948        for p in 0..100 {
949            let k = synth(p, 1);
950            let v = synth(p, 2);
951            f.append(&k, &v, &[true; 2]);
952            q8.append(&k, &v, &[true; 2]);
953        }
954        let q: Vec<f32> = (0..hd).map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5).collect();
955        for g in 0..heads {
956            let (of, pf) = f.attend(&q, g);
957            let (o8, p8) = q8.attend(&q, g);
958            let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
959            for d in 0..hd {
960                assert!(
961                    (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
962                    "g{g} d{d}: f32 {} vs q8 {}", of[d], o8[d]
963                );
964            }
965            for p in 0..100 {
966                assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
967            }
968        }
969        // rollback + eviction live on the q8 storage
970        q8.truncate_last(30);
971        assert_eq!(q8.head_len(0), 70);
972        let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
973        q8.accumulate_imp(&imp);
974        q8.evict_born(20, 2, 8);
975        assert_eq!(q8.head_len(0), 20);
976        let (o, _) = q8.attend(&q, 0);
977        assert!(o.iter().all(|x| x.is_finite()));
978        // memory: q8 ≈ 1 byte/element + scale per row (vs 4 for f32)
979        assert!(q8.memory_bytes() * 3 < f.memory_bytes());
980    }
981
982    /// Grouped GQA attend must be bit-identical to per-head attend in
983    /// every KV mode (it is the same math with rows streamed once).
984    #[test]
985    fn attend_group_equals_per_head_attend_bitexact() {
986        let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); // 6 Q-heads
987        for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
988            let mut c = LayerKvCache::new(kv_heads, hd);
989            c.mode = mode;
990            for p in 0..70 {
991                let k: Vec<f32> = (0..kv_heads * hd)
992                    .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
993                    .collect();
994                let v: Vec<f32> = (0..kv_heads * hd)
995                    .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
996                    .collect();
997                c.append(&k, &v, &[true; 2]);
998            }
999            let q: Vec<f32> = (0..kv_heads * hpk * hd)
1000                .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1001                .collect();
1002            for g in 0..kv_heads {
1003                let span = g * hpk * hd..(g + 1) * hpk * hd;
1004                let mut out = vec![0f32; hpk * hd];
1005                let mut imp = vec![0f32; 70];
1006                c.attend_group(&q[span.clone()], g, &mut out, &mut imp);
1007                let mut imp_ref = vec![0f32; 70];
1008                for h in 0..hpk {
1009                    let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1010                    let (o, probs) = c.attend(qh, g);
1011                    assert_eq!(
1012                        &out[h * hd..(h + 1) * hd],
1013                        &o[..],
1014                        "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1015                    );
1016                    for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1017                        *dst += p;
1018                    }
1019                }
1020                assert_eq!(imp, imp_ref, "mode {mode:?} g{g}: Born mass must match");
1021            }
1022        }
1023    }
1024
1025    /// Review regression: Born eviction in MIXED modes. q8v used to
1026    /// panic (gather over an empty v[h]), q8k silently left raw V
1027    /// uncompressed (stale rows under kept keys + memory leak).
1028    #[test]
1029    fn born_eviction_mixed_modes_stay_consistent() {
1030        for (mk, mv) in [(false, true), (true, false)] {
1031            let mut c = LayerKvCache::new(1, 4);
1032            c.mode = KvMode::Q8 { k: mk, v: mv };
1033            for p in 0..80 {
1034                let k = vec![p as f32 * 0.01; 4];
1035                let v = vec![p as f32; 4];
1036                c.append(&k, &v, &[true]);
1037            }
1038            let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1039            c.accumulate_imp(&imp);
1040            let before = c.memory_bytes();
1041            c.evict_born(20, 4, 8); // q8v: used to panic here
1042            assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1043            assert!(c.memory_bytes() < before / 2,
1044                    "memory must shrink (k={mk} v={mv})");
1045            // V rows match the kept set: the heaviest positions
1046            // (tail 60..79) must be present in the attend output.
1047            let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1048            assert!(out[0] > 30.0,
1049                    "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1050                    out[0]);
1051        }
1052    }
1053
1054    #[test]
1055    fn born_eviction_keeps_high_mass_position() {
1056        let mut cache = KvCache::new(1, 1, 2, 16);
1057        cache.policy = EvictionPolicy::Born { sink: 1 };
1058        for l in &mut cache.layers { l.mode = KvMode::F32; }
1059        let layer = &mut cache.layers[0];
1060        // 8 positions; keys carry the position index so we can verify
1061        // exactly which positions survive the gather.
1062        for pos in 0..8 {
1063            let k = vec![pos as f32; 2];
1064            let v = vec![pos as f32 + 100.0; 2];
1065            layer.append(&k, &v, &[true]);
1066        }
1067        // Position 3 carries the most attention mass (Born importance).
1068        let mut imp = vec![0.05f32; 8];
1069        imp[3] = 5.0;
1070        layer.accumulate_imp(&imp);
1071
1072        cache.evict(4); // sink 1 + recent 2 + 1 top-mass slot
1073        let layer = &cache.layers[0];
1074        assert_eq!(layer.seq_len, 4);
1075        let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1076        assert_eq!(
1077            kept_keys,
1078            vec![0.0, 3.0, 6.0, 7.0],
1079            "kept = sink(0) + Born-top(3) + recent(6,7)"
1080        );
1081        // imp stays aligned with the gathered positions.
1082        assert_eq!(layer.head_len(0), 4);
1083    }
1084}