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 scale 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 {
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
41/// Positions before freezing the per-channel field (2f): before — col ≡ 1,
42/// after — col = RMS over channels of the stored rows, old rows are requantized.
43const KV_COL_WARMUP: usize = 64;
44
45/// K-rows are quantized in groups of 32 channels (scale per group):
46/// attention logits are sensitive to the dot-product error, per-group scales
47/// localize it along RoPE bands (35B: +4.6% PPL with a per-row scale
48/// → target <1% with a per-group one). V — per-row scale (measured +0.56%).
49const KV_K_GROUP: usize = 32;
50
51/// Per-layer O(1) Nyström attention state (runtime `attn_type`
52/// override — spec §7 presence-driven pattern, no format change).
53///
54/// Collecting: the prompt pass still runs EXACT cache attention (the
55/// prefill outputs feed the residual stream, so they cannot be
56/// deferred) while the per-position rotated queries are buffered;
57/// `o1_seal()` then freezes landmarks + M from the full prompt, replays
58/// it into per-KV-group streaming states, and DROPS the full KV.
59/// Sealed: decode replaces cache attention with
60/// `NystromState::step_group()`.
61#[derive(Debug, Clone)]
62pub enum O1State {
63    Collecting {
64        m: usize,
65        w: usize,
66        sink: usize,
67        rect: crate::nystrom::O1Rect,
68        /// Optional completed-row barrier. `None` means the caller will
69        /// request a full-prompt seal; `Some(B)` keeps a short prompt exact
70        /// until the first skeleton-safe boundary B.
71        seal_at: Option<usize>,
72        /// Rotated post-norm queries, `[pos × num_heads × head_dim]`.
73        q_buf: Vec<f32>,
74    },
75    /// One state per KV GROUP, each holding its group's Q heads. The
76    /// exact window / sinks / K̃ are stored once per group (every Q head
77    /// of the group reads the same k/v rows); only the far field, Q̃ and
78    /// M — the query-dependent pieces — stay per Q head. See
79    /// `NystromState` for which piece is which and why.
80    Sealed {
81        groups: Vec<crate::nystrom::NystromState>,
82    },
83}
84
85/// KV cache for a single layer, head-major.
86#[derive(Debug, Clone)]
87pub struct LayerKvCache {
88    pub mode: KvMode,
89    /// Per-KV-head keys: `k[h]` is `[seq_len × head_dim]` (empty if head is dead).
90    k: Vec<Vec<f32>>,
91    /// Per-KV-head values, same layout.
92    v: Vec<Vec<f32>>,
93    /// q8 storage (mode == Q8_2F): int8 rows + f32 scale per row.
94    kq: Vec<Vec<i8>>,
95    ks: Vec<Vec<f32>>,
96    vq: Vec<Vec<i8>>,
97    vs: Vec<Vec<f32>>,
98    /// Per-channel scale fields per head [head_dim]; empty until frozen.
99    kcol: Vec<Vec<f32>>,
100    vcol: Vec<Vec<f32>>,
101    /// Accumulated attention mass per stored position: importance of a
102    /// position is how much probability mass reads it.
103    imp: Vec<f32>,
104    /// Positions appended so far (grows once per token, dead heads included).
105    pub seq_len: usize,
106    pub num_kv_heads: usize,
107    pub head_dim: usize,
108    /// Linear-core recurrent state S (vmf_phase), f64; empty on full layers.
109    pub linear_state: Vec<f32>,
110    /// Tentative lane-2 state during speculative verify.
111    pub linear_scratch: Vec<f32>,
112    /// O(1) Nyström override (None = plain cache attention).
113    pub o1: Option<O1State>,
114    /// A deferred seal failure is terminal for the current request. The
115    /// attention functions return only a hidden vector, so the pipeline
116    /// consumes this side channel at its next forward boundary.
117    o1_error: Option<String>,
118    /// Set when a collecting state actually becomes sealed. Pipeline owns
119    /// the epoch bump and consumes this bit after a complete forward.
120    o1_transitioned: bool,
121}
122
123impl LayerKvCache {
124    pub fn new(num_kv_heads: usize, head_dim: usize) -> Self {
125        Self {
126            mode: KvMode::from_env(),
127            k: vec![Vec::new(); num_kv_heads],
128            v: vec![Vec::new(); num_kv_heads],
129            kq: vec![Vec::new(); num_kv_heads],
130            ks: vec![Vec::new(); num_kv_heads],
131            vq: vec![Vec::new(); num_kv_heads],
132            vs: vec![Vec::new(); num_kv_heads],
133            kcol: vec![Vec::new(); num_kv_heads],
134            vcol: vec![Vec::new(); num_kv_heads],
135            imp: Vec::new(),
136            seq_len: 0,
137            num_kv_heads,
138            head_dim,
139            linear_state: Vec::new(),
140            linear_scratch: Vec::new(),
141            o1: None,
142            o1_error: None,
143            o1_transitioned: false,
144        }
145    }
146
147    /// Per-KV-head stored keys `[seq_len × head_dim]` (GPU token graph sync).
148    pub fn k_heads(&self) -> &[Vec<f32>] {
149        &self.k
150    }
151    /// Per-KV-head stored values `[seq_len × head_dim]`.
152    pub fn v_heads(&self) -> &[Vec<f32>] {
153        &self.v
154    }
155
156    // ── O(1) Nyström override ──
157
158    /// Arm query collection for a fresh prompt pass (a cleared cache).
159    pub fn o1_begin(&mut self, m: usize, w: usize, sink: usize, rect: crate::nystrom::O1Rect) {
160        self.o1_begin_with_boundary(m, w, sink, rect, None);
161    }
162
163    /// Arm query collection with an optional completed-row seal barrier.
164    /// The barrier is deliberately part of the existing collecting state:
165    /// no second history or scheduler is introduced for short prompts.
166    pub(crate) fn o1_begin_with_boundary(
167        &mut self,
168        m: usize,
169        w: usize,
170        sink: usize,
171        rect: crate::nystrom::O1Rect,
172        seal_at: Option<usize>,
173    ) {
174        self.o1 = Some(O1State::Collecting {
175            m,
176            w,
177            sink,
178            rect,
179            seal_at,
180            q_buf: Vec::new(),
181        });
182        self.o1_error = None;
183        self.o1_transitioned = false;
184    }
185
186    /// Record one position's rotated queries (`[num_heads × head_dim]`)
187    /// during the exact prompt pass. No-op unless collecting — the hook
188    /// sits inside qwen_attention so every prefill flavor (sequential,
189    /// batched) feeds the same trace.
190    pub fn o1_push_q(&mut self, q_all: &[f32]) {
191        if let Some(O1State::Collecting { q_buf, .. }) = &mut self.o1 {
192            q_buf.extend_from_slice(q_all);
193        }
194    }
195
196    pub fn o1_sealed(&self) -> bool {
197        matches!(self.o1, Some(O1State::Sealed { .. }))
198    }
199
200    /// Pending completed-row barrier, if any. A plain full-prompt seal has
201    /// no barrier until the caller asks to seal.
202    pub(crate) fn o1_pending_boundary(&self) -> Option<usize> {
203        match &self.o1 {
204            Some(O1State::Collecting { seal_at, .. }) => *seal_at,
205            _ => None,
206        }
207    }
208
209    /// Whether a batch of `count` exact rows would cross the deferred
210    /// boundary. This lets batched/pair callers split before row B rather
211    /// than appending exact KV past the point where conversion is required.
212    pub(crate) fn o1_boundary_crossed_by(&self, count: usize) -> bool {
213        let Some(target) = self.o1_pending_boundary() else {
214            return false;
215        };
216        target <= self.seq_len
217            || self
218                .seq_len
219                .checked_add(count)
220                .map_or(true, |next| next >= target)
221    }
222
223    pub(crate) fn take_o1_transition(&mut self) -> bool {
224        std::mem::take(&mut self.o1_transitioned)
225    }
226
227    pub(crate) fn take_o1_error(&self) -> Option<String> {
228        // Error observation is deliberately non-consuming.  The error is the
229        // append guard for this request; removing it would let a caller that
230        // ignored the returned Err resume ordinary KV growth after the
231        // bounded transition dropped its overlay.  `clear()` is the explicit
232        // reset boundary that clears the latch.
233        self.o1_error.clone()
234    }
235
236    /// Abort a malformed deferred transition after attention has already
237    /// produced its current row. Clearing the exact storage and dropping
238    /// the overlay makes the state unrecoverable by continued decode; the
239    /// pipeline then routes through its normal graph/cancel cleanup.
240    pub(crate) fn o1_abort(&mut self, err: String) {
241        self.k.iter_mut().for_each(Vec::clear);
242        self.v.iter_mut().for_each(Vec::clear);
243        self.kq.iter_mut().for_each(Vec::clear);
244        self.ks.iter_mut().for_each(Vec::clear);
245        self.vq.iter_mut().for_each(Vec::clear);
246        self.vs.iter_mut().for_each(Vec::clear);
247        self.kcol.iter_mut().for_each(Vec::clear);
248        self.vcol.iter_mut().for_each(Vec::clear);
249        self.imp.clear();
250        self.o1 = None;
251        self.seq_len = 0;
252        self.o1_transitioned = false;
253        self.o1_error = Some(err);
254    }
255
256    /// Freeze the prompt into per-KV-group Nyström states and drop this
257    /// layer's full KV. Returns false while a short collecting layer is
258    /// below its deferred boundary; malformed prerequisites abort the
259    /// layer instead of silently resuming exact KV growth. The seal needs
260    /// f32 KV rows (`CMF_KV=q8` stores int8), every group densely stored, a
261    /// full q trace, and a GQA fan-out that actually divides.
262    pub fn o1_seal(&mut self, num_heads: usize) -> bool {
263        match self.o1_seal_checked(num_heads) {
264            Ok(sealed) => sealed,
265            Err(err) => {
266                tracing::error!("o1: seal aborted: {err}");
267                self.o1_abort(err);
268                false
269            }
270        }
271    }
272
273    /// Checked seal implementation. Validation happens while the collecting
274    /// state and full KV are still intact; only a valid completed boundary
275    /// is allowed to destructively convert them.
276    pub(crate) fn o1_seal_checked(&mut self, num_heads: usize) -> Result<bool, String> {
277        if let Some(err) = self.o1_error.clone() {
278            return Err(err);
279        }
280        // Idempotent: sealing a sealed (or plain) layer must not disturb its
281        // state, and a plain layer is not an O(1) participant.
282        if !matches!(self.o1, Some(O1State::Collecting { .. })) {
283            return Ok(self.o1_sealed());
284        }
285        let (m, w, sink, requested_boundary, q_len) = match &self.o1 {
286            Some(O1State::Collecting {
287                m,
288                w,
289                sink,
290                rect: _,
291                seal_at,
292                q_buf,
293            }) => (*m, *w, *sink, *seal_at, q_buf.len()),
294            _ => unreachable!("checked above"),
295        };
296        let floor = crate::nystrom::o1_deferred_boundary(w, sink)
297            .ok_or_else(|| "o1 seal: w + sink + slack + 1 overflow".to_string())?;
298        let target = requested_boundary.unwrap_or(floor).max(floor);
299        let t = self.seq_len;
300        if t < target {
301            if let Some(O1State::Collecting { seal_at, .. }) = &mut self.o1 {
302                if *seal_at != Some(target) {
303                    *seal_at = Some(target);
304                    tracing::info!(
305                        "o1 deferred seal: current rows={t}, boundary={target} (floor={floor})"
306                    );
307                }
308            }
309            return Ok(false);
310        }
311
312        let hd = self.head_dim;
313        if t == 0 {
314            return Err("o1 seal: cannot seal an empty layer".into());
315        }
316        if self.mode != KvMode::F32 {
317            return Err("o1 seal: requires dense F32 KV storage".into());
318        }
319        if self.num_kv_heads == 0 || num_heads == 0 || num_heads % self.num_kv_heads != 0 {
320            return Err(format!(
321                "o1 seal: invalid GQA geometry num_heads={num_heads} num_kv_heads={}",
322                self.num_kv_heads
323            ));
324        }
325        let hpk = num_heads / self.num_kv_heads;
326        let expected_k = t
327            .checked_mul(hd)
328            .ok_or_else(|| "o1 seal: KV row length overflow".to_string())?;
329        let expected_q = expected_k
330            .checked_mul(num_heads)
331            .ok_or_else(|| "o1 seal: query trace length overflow".to_string())?;
332        if q_len != expected_q {
333            return Err(format!(
334                "o1 seal: query trace has {q_len} values, expected {expected_q}"
335            ));
336        }
337        if (0..self.num_kv_heads)
338            .any(|g| self.k[g].len() != expected_k || self.v[g].len() != expected_k)
339        {
340            return Err("o1 seal: KV heads are not densely populated".into());
341        }
342        if m < 4 || w == 0 {
343            return Err(format!("o1 seal: invalid geometry m={m} w={w}"));
344        }
345
346        let Some(O1State::Collecting {
347            m,
348            w,
349            sink,
350            rect,
351            q_buf,
352            ..
353        }) = self.o1.take()
354        else {
355            unreachable!("collecting state disappeared after validation");
356        };
357        let mut groups = Vec::with_capacity(self.num_kv_heads);
358        // Query trace is position-major; the state wants each head's
359        // queries contiguous, so transpose one group at a time.
360        let mut qh = vec![0.0f32; hpk * t * hd];
361        for g in 0..self.num_kv_heads {
362            for hh in 0..hpk {
363                let h = g * hpk + hh;
364                for p in 0..t {
365                    let src = (p * num_heads + h) * hd;
366                    let dst = (hh * t + p) * hd;
367                    qh[dst..dst + hd].copy_from_slice(&q_buf[src..src + hd]);
368                }
369            }
370            let qs: Vec<&[f32]> = (0..hpk)
371                .map(|hh| &qh[hh * t * hd..(hh + 1) * t * hd])
372                .collect();
373            let mut st = crate::nystrom::NystromState::new_group(m, w, sink, hpk).with_rect(rect);
374            st.prefill_group(&qs, &self.k[g], &self.v[g], t, hd, hd);
375            groups.push(st);
376        }
377        // The states now carry everything decode needs — release the
378        // O(context) storage (this is the memory claim, not a cosmetic).
379        for h in 0..self.num_kv_heads {
380            self.k[h] = Vec::new();
381            self.v[h] = Vec::new();
382        }
383        self.imp = Vec::new();
384        self.o1 = Some(O1State::Sealed { groups });
385        self.o1_transitioned = true;
386        Ok(true)
387    }
388
389    /// One decode step on a sealed layer: per KV group, insert the
390    /// group's fresh (k, v) ONCE and read every Q head's attention
391    /// output. Returns `[num_heads × head_dim]`. Head h belongs to group
392    /// h/hpk, so a group's Q heads are contiguous in `q_all`/`out` —
393    /// same math as the shared KV row the exact path appends once.
394    /// Device views of the sealed o1 groups, or None when o1 is not
395    /// sealed on this layer (or any group is in the degenerate
396    /// exact-only mode the GPU path does not carry).
397    pub fn o1_views(&self) -> Option<Vec<crate::nystrom::O1DeviceView<'_>>> {
398        let Some(O1State::Sealed { groups }) = &self.o1 else {
399            return None;
400        };
401        let views: Vec<_> = groups.iter().map(|g| g.device_view()).collect();
402        if views.iter().any(|v| v.exact_only) {
403            return None;
404        }
405        Some(views)
406    }
407
408    pub fn o1_step(
409        &mut self,
410        q_all: &[f32],
411        k_new: &[f32],
412        v_new: &[f32],
413        num_heads: usize,
414    ) -> Vec<f32> {
415        let hd = self.head_dim;
416        let hpk = num_heads / self.num_kv_heads.max(1);
417        let mut out = vec![0.0f32; num_heads * hd];
418        let Some(O1State::Sealed { groups }) = &mut self.o1 else {
419            debug_assert!(false, "o1_step on an unsealed layer");
420            return out;
421        };
422        for (g, st) in groups.iter_mut().enumerate() {
423            let (lo, hi) = (g * hpk * hd, (g + 1) * hpk * hd);
424            st.step_group(
425                &q_all[lo..hi],
426                &k_new[g * hd..(g + 1) * hd],
427                &v_new[g * hd..(g + 1) * hd],
428                &mut out[lo..hi],
429            );
430        }
431        // Track the true context depth for the honest memory/seq report
432        // (nothing is stored per position — the state is O(1)).
433        self.seq_len += 1;
434        out
435    }
436
437    /// Bytes held by the O(1) override (query trace while collecting,
438    /// per-KV-group states once sealed).
439    pub fn o1_memory_bytes(&self) -> usize {
440        match &self.o1 {
441            Some(O1State::Collecting { q_buf, .. }) => q_buf.len() * std::mem::size_of::<f32>(),
442            Some(O1State::Sealed { groups }) => groups.iter().map(|s| s.memory_bytes()).sum(),
443            None => 0,
444        }
445    }
446
447    /// Quantize one row against the per-channel field (empty col = 1);
448    /// `group` — elements per scale (the whole row or KV_K_GROUP).
449    fn quant_row(row: &[f32], col: &[f32], q: &mut Vec<i8>, sc: &mut Vec<f32>, group: usize) {
450        let mut resid = vec![0.0f32; row.len()];
451        for (d, &x) in row.iter().enumerate() {
452            resid[d] = if col.is_empty() { x } else { x / col[d] };
453        }
454        for g0 in (0..row.len()).step_by(group) {
455            let g1 = (g0 + group).min(row.len());
456            let mut absmax = 0.0f32;
457            for &r in &resid[g0..g1] {
458                absmax = absmax.max(r.abs());
459            }
460            let s = (absmax / 127.0).max(1e-12);
461            sc.push(s);
462            for &r in &resid[g0..g1] {
463                q.push((r / s).round().clamp(-127.0, 127.0) as i8);
464            }
465        }
466    }
467
468    /// Freeze the 2f field: col = RMS of channels over stored rows, old
469    /// rows are requantized against the new field (once per conversation).
470    fn freeze_cols(&mut self) {
471        let hd = self.head_dim;
472        let ngk = hd.div_ceil(KV_K_GROUP);
473        for h in 0..self.num_kv_heads {
474            for (qv, sv, colv, group) in [
475                (
476                    &mut self.kq[h],
477                    &mut self.ks[h],
478                    &mut self.kcol[h],
479                    KV_K_GROUP,
480                ),
481                (&mut self.vq[h], &mut self.vs[h], &mut self.vcol[h], hd),
482            ] {
483                let spp = if group == hd { 1 } else { ngk }; // scales per position
484                let n = sv.len() / spp;
485                if n == 0 {
486                    continue;
487                }
488                // Dequantize to f32, RMS over channels, requantize.
489                let mut rows = vec![0.0f32; n * hd];
490                for p in 0..n {
491                    for d in 0..hd {
492                        rows[p * hd + d] = qv[p * hd + d] as f32 * sv[p * spp + d / group];
493                    }
494                }
495                let mut col = vec![0.0f32; hd];
496                for p in 0..n {
497                    for d in 0..hd {
498                        col[d] += rows[p * hd + d] * rows[p * hd + d];
499                    }
500                }
501                for c in col.iter_mut() {
502                    *c = (*c / n as f32).sqrt().max(1e-6);
503                }
504                qv.clear();
505                sv.clear();
506                for p in 0..n {
507                    Self::quant_row(&rows[p * hd..(p + 1) * hd], &col, qv, sv, group);
508                }
509                *colv = col;
510            }
511        }
512    }
513
514    /// Append K/V for one position. `k_new`/`v_new` are
515    /// `[num_kv_heads × head_dim]`; heads with `alive[h] == false` are
516    /// skipped (their slices stay empty).
517    pub fn append(&mut self, k_new: &[f32], v_new: &[f32], alive: &[bool]) {
518        // A failed bounded transition is terminal until the sequence is
519        // cleared. Do not let an ignored boolean/result resume plain KV
520        // growth after the O(1) collector has aborted.
521        if self.o1_error.is_some() {
522            return;
523        }
524        debug_assert_eq!(k_new.len(), self.num_kv_heads * self.head_dim);
525        debug_assert_eq!(v_new.len(), self.num_kv_heads * self.head_dim);
526        // Freeze the 2f field AT THE START of append: only rows that
527        // survived verify are visible (a rejected lane-2 draft does not
528        // pollute the field — found in review), and the threshold uses >=
529        // rather than strict equality (in small windows eviction may
530        // oscillate across 64).
531        if matches!(self.mode, KvMode::Q8 { .. })
532            && self.seq_len >= KV_COL_WARMUP
533            && self.kcol.iter().all(Vec::is_empty)
534            && self.vcol.iter().all(Vec::is_empty)
535        {
536            self.freeze_cols();
537        }
538        for h in 0..self.num_kv_heads {
539            if !alive.get(h).copied().unwrap_or(true) {
540                continue;
541            }
542            let s = h * self.head_dim;
543            if self.mode.quant_k() {
544                Self::quant_row(
545                    &k_new[s..s + self.head_dim],
546                    &self.kcol[h],
547                    &mut self.kq[h],
548                    &mut self.ks[h],
549                    KV_K_GROUP,
550                );
551            } else {
552                self.k[h].extend_from_slice(&k_new[s..s + self.head_dim]);
553            }
554            if self.mode.quant_v() {
555                Self::quant_row(
556                    &v_new[s..s + self.head_dim],
557                    &self.vcol[h],
558                    &mut self.vq[h],
559                    &mut self.vs[h],
560                    self.head_dim,
561                );
562            } else {
563                self.v[h].extend_from_slice(&v_new[s..s + self.head_dim]);
564            }
565        }
566        self.imp.push(0.0);
567        self.seq_len += 1;
568    }
569
570    /// Per-head attention over its own storage: the f32 branch is
571    /// bit-for-bit equal to attention_head() over slices; the q8 branch
572    /// computes score = s_k·⟨q⊙col_k, k_q⟩ and the weighted sum of V in i8
573    /// with f32 accumulation. Returns (output [head_dim], probs [stored]).
574    pub fn attend(&self, q: &[f32], kv_head: usize) -> (Vec<f32>, Vec<f32>) {
575        let hd = self.head_dim;
576        if self.mode == KvMode::F32 {
577            let stored = self.k[kv_head].len() / hd;
578            return crate::attention::attention_head(
579                q,
580                &self.k[kv_head],
581                &self.v[kv_head],
582                hd,
583                stored,
584            );
585        }
586        let stored = self.head_len(kv_head);
587        let scale = 1.0 / (hd as f32).sqrt();
588        let mut scores = vec![0.0f32; stored];
589        if self.mode.quant_k() {
590            let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
591            // q ⊙ col_k — once per call.
592            let kcol = &self.kcol[kv_head];
593            let mut qc = vec![0.0f32; hd];
594            for d in 0..hd {
595                qc[d] = if kcol.is_empty() {
596                    q[d]
597                } else {
598                    q[d] * kcol[d]
599                };
600            }
601            let ng = hd.div_ceil(KV_K_GROUP);
602            for p in 0..stored {
603                let row = &kq[p * hd..(p + 1) * hd];
604                // SAFETY: i8 and u8 share layout; dot_i8_f32 reads the
605                // bytes back as i8.
606                let row_u8 =
607                    unsafe { std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len()) };
608                let mut dot = 0.0f32;
609                for g in 0..ng {
610                    let g0 = g * KV_K_GROUP;
611                    let g1 = (g0 + KV_K_GROUP).min(hd);
612                    dot +=
613                        crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qc[g0..g1]) * ks[p * ng + g];
614                }
615                scores[p] = dot * scale;
616            }
617        } else {
618            let k = &self.k[kv_head];
619            for p in 0..stored {
620                let row = &k[p * hd..(p + 1) * hd];
621                scores[p] = crate::attention::dot_f32(q, row) * scale;
622            }
623        }
624        let max_score = scores.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
625        let mut sum = 0.0f32;
626        for s in scores.iter_mut() {
627            *s = (*s - max_score).exp();
628            sum += *s;
629        }
630        if sum > 0.0 {
631            for s in scores.iter_mut() {
632                *s /= sum;
633            }
634        }
635        let mut acc = vec![0.0f32; hd];
636        if self.mode.quant_v() {
637            let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
638            for p in 0..stored {
639                let w = scores[p] * vs[p];
640                if w.abs() < 1e-12 {
641                    continue;
642                }
643                crate::qtensor::axpy_i8_f32(&mut acc, &vq[p * hd..(p + 1) * hd], w);
644            }
645            let vcol = &self.vcol[kv_head];
646            if !vcol.is_empty() {
647                for d in 0..hd {
648                    acc[d] *= vcol[d];
649                }
650            }
651        } else {
652            let v = &self.v[kv_head];
653            for p in 0..stored {
654                let w = scores[p];
655                if w.abs() < 1e-12 {
656                    continue;
657                }
658                crate::attention::axpy_f32(&mut acc, &v[p * hd..(p + 1) * hd], w);
659            }
660        }
661        (acc, scores)
662    }
663
664    /// Grouped GQA attention: all Q-heads of one KV group in a single
665    /// pass over the stored K rows and a single pass over the V rows
666    /// (per-head `attend` re-read the shared group storage
667    /// heads_per_kv times — roadmap §3 P1). Per-head score order,
668    /// softmax and V accumulation are IDENTICAL to `attend`, so each
669    /// head's output is bit-for-bit the same.
670    ///
671    /// `q_group`: `[n_heads_in_group × head_dim]` (global head order);
672    /// `out`: same shape; `imp_acc[0..stored]` accumulates the probabilities
673    /// of every head (attention importance), matching the caller's former loop.
674    /// `scale` is the score scale (1/√hd unless the arch overrides);
675    /// `first` is the earliest visible position — sliding-window layers
676    /// pass `stored − window` so older rows get zero probability.
677    #[allow(clippy::too_many_arguments)]
678    pub fn attend_group(
679        &self,
680        q_group: &[f32],
681        kv_head: usize,
682        out: &mut [f32],
683        imp_acc: &mut [f32],
684        scale: f32,
685        first: usize,
686        softcap: f32,
687    ) {
688        let hd = self.head_dim;
689        let nheads = q_group.len() / hd;
690        debug_assert_eq!(out.len(), nheads * hd);
691        let stored = if self.mode == KvMode::F32 {
692            self.k[kv_head].len() / hd
693        } else {
694            self.head_len(kv_head)
695        };
696        if stored == 0 {
697            out.fill(0.0);
698            return;
699        }
700        let first = first.min(stored.saturating_sub(1));
701
702        thread_local! {
703            /// scores [nheads × stored] — reused across layers/tokens.
704            static GQA_SCORES: std::cell::RefCell<Vec<f32>> =
705                const { std::cell::RefCell::new(Vec::new()) };
706            /// q ⊙ col_k per head (q8 K mode).
707            static GQA_QC: std::cell::RefCell<Vec<f32>> =
708                const { std::cell::RefCell::new(Vec::new()) };
709        }
710
711        GQA_SCORES.with(|sc| {
712            let mut scores = sc.borrow_mut();
713            if first > 0 {
714                // Out-of-window rows stay at −inf → exp gives exactly 0,
715                // so softmax / V / importance need no special-casing.
716                scores.clear();
717                scores.resize(nheads * stored, f32::NEG_INFINITY);
718            } else {
719                scores.resize(nheads * stored, 0.0);
720            }
721
722            // ── score pass: each stored K row is read ONCE for all heads.
723            if self.mode.quant_k() {
724                let (kq, ks) = (&self.kq[kv_head], &self.ks[kv_head]);
725                let kcol = &self.kcol[kv_head];
726                let ng = hd.div_ceil(KV_K_GROUP);
727                GQA_QC.with(|qc| {
728                    let mut qcb = qc.borrow_mut();
729                    qcb.resize(nheads * hd, 0.0);
730                    for h in 0..nheads {
731                        for d in 0..hd {
732                            let qv = q_group[h * hd + d];
733                            qcb[h * hd + d] = if kcol.is_empty() { qv } else { qv * kcol[d] };
734                        }
735                    }
736                    for p in first..stored {
737                        let row = &kq[p * hd..(p + 1) * hd];
738                        // SAFETY: i8 and u8 share layout; dot_i8_f32 reads
739                        // the bytes back as i8.
740                        let row_u8 = unsafe {
741                            std::slice::from_raw_parts(row.as_ptr() as *const u8, row.len())
742                        };
743                        for h in 0..nheads {
744                            let qch = &qcb[h * hd..(h + 1) * hd];
745                            let mut dot = 0.0f32;
746                            for g in 0..ng {
747                                let g0 = g * KV_K_GROUP;
748                                let g1 = (g0 + KV_K_GROUP).min(hd);
749                                dot += crate::qtensor::dot_i8_f32(&row_u8[g0..g1], &qch[g0..g1])
750                                    * ks[p * ng + g];
751                            }
752                            scores[h * stored + p] = dot * scale;
753                        }
754                    }
755                });
756            } else {
757                let k = &self.k[kv_head];
758                for p in first..stored {
759                    let row = &k[p * hd..(p + 1) * hd];
760                    for h in 0..nheads {
761                        scores[h * stored + p] =
762                            crate::attention::dot_f32(&q_group[h * hd..(h + 1) * hd], row) * scale;
763                    }
764                }
765            }
766
767            // Gemma-2 attention-logit soft-capping: tanh-squash the
768            // COMPUTED scores before the softmax. Out-of-window rows sit
769            // at −inf and must stay there (tanh would resurrect them at
770            // −cap), hence the finiteness guard.
771            if softcap > 0.0 {
772                for v in scores.iter_mut() {
773                    if v.is_finite() {
774                        *v = softcap * (*v / softcap).tanh();
775                    }
776                }
777            }
778
779            // ── per-head softmax (identical to attend / attention_head).
780            for h in 0..nheads {
781                let s = &mut scores[h * stored..(h + 1) * stored];
782                let max_score = s.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
783                let mut sum = 0.0f32;
784                for v in s.iter_mut() {
785                    *v = (*v - max_score).exp();
786                    sum += *v;
787                }
788                if sum > 0.0 {
789                    for v in s.iter_mut() {
790                        *v /= sum;
791                    }
792                }
793            }
794
795            // ── value pass: each stored V row is read ONCE for all heads.
796            out.fill(0.0);
797            if self.mode.quant_v() {
798                let (vq, vs) = (&self.vq[kv_head], &self.vs[kv_head]);
799                for p in first..stored {
800                    let row = &vq[p * hd..(p + 1) * hd];
801                    for h in 0..nheads {
802                        let w = scores[h * stored + p] * vs[p];
803                        if w.abs() < 1e-12 {
804                            continue;
805                        }
806                        crate::qtensor::axpy_i8_f32(&mut out[h * hd..(h + 1) * hd], row, w);
807                    }
808                }
809                let vcol = &self.vcol[kv_head];
810                if !vcol.is_empty() {
811                    for h in 0..nheads {
812                        for d in 0..hd {
813                            out[h * hd + d] *= vcol[d];
814                        }
815                    }
816                }
817            } else {
818                let v = &self.v[kv_head];
819                for p in first..stored {
820                    let row = &v[p * hd..(p + 1) * hd];
821                    for h in 0..nheads {
822                        let w = scores[h * stored + p];
823                        if w.abs() < 1e-12 {
824                            continue;
825                        }
826                        crate::attention::axpy_f32(&mut out[h * hd..(h + 1) * hd], row, w);
827                    }
828                }
829            }
830
831            // ── Attention-importance accumulation (Σ probs over heads), same
832            // head order as the caller's former per-head loop.
833            let n = imp_acc.len().min(stored);
834            for h in 0..nheads {
835                let s = &scores[h * stored..(h + 1) * stored];
836                for (dst, &p) in imp_acc[..n].iter_mut().zip(s) {
837                    *dst += p;
838                }
839            }
840        });
841    }
842
843    /// Batched causal attend for a prefill chunk (macOS/AArch64): the
844    /// cache already holds every chunk row (`s0` old + `b` new). Per
845    /// Q-head the scores GEMM `Q·Kᵀ` rides the AMX, the causal softmax
846    /// zeroes the not-yet-visible tail so the `P·V` GEMM needs no
847    /// mask, and attention importance takes the masked column sums. Same
848    /// math as the per-position attend; summation order differs
849    /// (tolerance-class, like the projection GEMMs).
850    #[cfg(target_arch = "aarch64")]
851    #[allow(clippy::too_many_arguments)]
852    pub fn attend_chunk(
853        &mut self,
854        q_all: &[f32],
855        b: usize,
856        s0: usize,
857        nh: usize,
858        heads_per_kv: usize,
859        hd: usize,
860        out: &mut [f32],
861        pool: Option<&crate::pool::Pool>,
862        scale: f32,
863        window: Option<usize>,
864    ) {
865        let n = s0 + b;
866        struct SendPtr(*mut f32);
867        unsafe impl Send for SendPtr {}
868        unsafe impl Sync for SendPtr {}
869        impl SendPtr {
870            fn at(&self, i: usize) -> *mut f32 {
871                // Method receiver keeps the closure capturing &SendPtr
872                // (2021 disjoint capture would grab the raw field).
873                unsafe { self.0.add(i) }
874            }
875        }
876        thread_local! {
877            static SCRATCH: std::cell::RefCell<(Vec<f32>, Vec<f32>, Vec<f32>, Vec<f32>)> =
878                const { std::cell::RefCell::new((Vec::new(), Vec::new(), Vec::new(), Vec::new())) };
879        }
880        // The portable NEON GEMM pays dearly for the gathered Bᵀ loads
881        // of the scores multiply — pack Kᵀ once per (group, chunk) and
882        // hand it the sequential-B fast path instead. Accelerate keeps
883        // the no-copy transposed call.
884        let neon_gemm = cfg!(not(target_os = "macos"))
885            || std::env::var("CMF_FORCE_NEON_GEMM")
886                .map(|v| v == "1")
887                .unwrap_or(false);
888        SCRATCH.with(|s| {
889            let mut s = s.borrow_mut();
890            let (qpanel, scores, aopanel, ktpack) = &mut *s;
891            // The whole KV-group attends in one GEMM pair: the group's
892            // Q-heads stack head-major into one tall panel [hpk·b, hd]
893            // (row hl·b + bi), so each layer costs 2 sgemm calls per
894            // group instead of 2 per head — fat M keeps the AMX fed.
895            let m = heads_per_kv * b;
896            qpanel.resize(m * hd, 0.0);
897            scores.resize(m * n, 0.0);
898            aopanel.resize(m * hd, 0.0);
899            for g in 0..self.num_kv_heads {
900                let kmat = &self.k[g];
901                let vmat = &self.v[g];
902                debug_assert_eq!(kmat.len(), n * hd);
903                for hl in 0..heads_per_kv {
904                    let hh = g * heads_per_kv + hl;
905                    for bi in 0..b {
906                        qpanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]
907                            .copy_from_slice(&q_all[bi * nh * hd + hh * hd..][..hd]);
908                    }
909                }
910                if neon_gemm {
911                    ktpack.resize(hd * n, 0.0);
912                    for p in 0..n {
913                        let row = &kmat[p * hd..(p + 1) * hd];
914                        for (d, &v) in row.iter().enumerate() {
915                            ktpack[d * n + p] = v;
916                        }
917                    }
918                    // Accelerate threads its own GEMM; the NEON kernel
919                    // splits the m rows across the pool instead.
920                    let sp_q = SendPtr(qpanel.as_ptr() as *mut f32);
921                    let sp_s = SendPtr(scores.as_mut_ptr());
922                    let kt = &*ktpack;
923                    let run = |start: usize, end: usize| {
924                        if end > start {
925                            // SAFETY: workers write disjoint score rows.
926                            let a = unsafe {
927                                std::slice::from_raw_parts(sp_q.at(start * hd), (end - start) * hd)
928                            };
929                            let c = unsafe {
930                                std::slice::from_raw_parts_mut(
931                                    sp_s.at(start * n),
932                                    (end - start) * n,
933                                )
934                            };
935                            crate::qtensor::neon_gemm_rm(
936                                end - start,
937                                n,
938                                hd,
939                                scale,
940                                a,
941                                hd,
942                                kt,
943                                n,
944                                false,
945                                c,
946                                n,
947                            );
948                        }
949                    };
950                    match pool {
951                        Some(p) if m >= 64 => p.run_rows(m, &run),
952                        _ => run(0, m),
953                    }
954                } else {
955                    crate::qtensor::sgemm_rm(
956                        m, n, hd, scale, qpanel, hd, kmat, hd, true, scores, n,
957                    );
958                }
959                // Causal softmax, row-parallel (rows are disjoint).
960                let sp = SendPtr(scores.as_mut_ptr());
961                let run = |start: usize, end: usize| {
962                    for r in start..end {
963                        let allowed = s0 + (r % b) + 1;
964                        // Sliding-window layers see only the last W of
965                        // the causal range; the zeroed head contributes
966                        // nothing to P·V or attention importance.
967                        let lo = window.map(|w| allowed.saturating_sub(w)).unwrap_or(0);
968                        // SAFETY: workers cover disjoint row ranges.
969                        let row = unsafe { std::slice::from_raw_parts_mut(sp.at(r * n), n) };
970                        crate::attention::softmax_row(&mut row[lo..allowed]);
971                        row[..lo].fill(0.0);
972                        row[allowed..].fill(0.0);
973                    }
974                };
975                match pool {
976                    Some(p) if m >= 64 => p.run_rows(m, &run),
977                    _ => run(0, m),
978                }
979                // Attention importance: masked column sums (probs of the
980                // zeroed tail contribute nothing, same as the CPU
981                // per-position accumulate).
982                let ni = self.imp.len().min(n);
983                for r in 0..m {
984                    let al = (s0 + (r % b) + 1).min(ni);
985                    for (dst, &p) in self.imp[..al].iter_mut().zip(&scores[r * n..r * n + al]) {
986                        *dst += p;
987                    }
988                }
989                if neon_gemm {
990                    let sp_s = SendPtr(scores.as_mut_ptr());
991                    let sp_o = SendPtr(aopanel.as_mut_ptr());
992                    let run = |start: usize, end: usize| {
993                        if end > start {
994                            // SAFETY: workers write disjoint output rows.
995                            let a = unsafe {
996                                std::slice::from_raw_parts(sp_s.at(start * n), (end - start) * n)
997                            };
998                            let c = unsafe {
999                                std::slice::from_raw_parts_mut(
1000                                    sp_o.at(start * hd),
1001                                    (end - start) * hd,
1002                                )
1003                            };
1004                            crate::qtensor::neon_gemm_rm(
1005                                end - start,
1006                                hd,
1007                                n,
1008                                1.0,
1009                                a,
1010                                n,
1011                                vmat,
1012                                hd,
1013                                false,
1014                                c,
1015                                hd,
1016                            );
1017                        }
1018                    };
1019                    match pool {
1020                        Some(p) if m >= 64 => p.run_rows(m, &run),
1021                        _ => run(0, m),
1022                    }
1023                } else {
1024                    crate::qtensor::sgemm_rm(
1025                        m, hd, n, 1.0, scores, n, vmat, hd, false, aopanel, hd,
1026                    );
1027                }
1028                for hl in 0..heads_per_kv {
1029                    let hh = g * heads_per_kv + hl;
1030                    for bi in 0..b {
1031                        out[bi * nh * hd + hh * hd..][..hd]
1032                            .copy_from_slice(&aopanel[(hl * b + bi) * hd..(hl * b + bi + 1) * hd]);
1033                    }
1034                }
1035            }
1036        });
1037    }
1038
1039    /// Roll back the last `n_drop` positions (speculative-decode reject).
1040    pub fn truncate_last(&mut self, n_drop: usize) {
1041        let d = n_drop.min(self.seq_len);
1042        for h in 0..self.num_kv_heads {
1043            let keep = self.k[h].len().saturating_sub(d * self.head_dim);
1044            self.k[h].truncate(keep);
1045            self.v[h].truncate(keep);
1046            let ngk = self.head_dim.div_ceil(KV_K_GROUP);
1047            let keep_q = self.kq[h].len().saturating_sub(d * self.head_dim);
1048            self.kq[h].truncate(keep_q);
1049            let keep_vq = self.vq[h].len().saturating_sub(d * self.head_dim);
1050            self.vq[h].truncate(keep_vq);
1051            let keep_ks = self.ks[h].len().saturating_sub(d * ngk);
1052            self.ks[h].truncate(keep_ks);
1053            let keep_vs = self.vs[h].len().saturating_sub(d);
1054            self.vs[h].truncate(keep_vs);
1055        }
1056        self.imp.truncate(self.imp.len().saturating_sub(d));
1057        self.seq_len -= d;
1058    }
1059
1060    /// Accumulate attention mass per stored position (summed over heads).
1061    pub fn accumulate_imp(&mut self, probs: &[f32]) {
1062        for (dst, &p) in self.imp.iter_mut().zip(probs) {
1063            *dst += p;
1064        }
1065    }
1066
1067    /// Contiguous keys of one head: `[stored_len × head_dim]`.
1068    pub fn head_keys(&self, kv_head: usize) -> &[f32] {
1069        &self.k[kv_head]
1070    }
1071
1072    pub fn head_values(&self, kv_head: usize) -> &[f32] {
1073        &self.v[kv_head]
1074    }
1075
1076    /// Number of positions actually stored for a head (0 for dead heads).
1077    pub fn head_len(&self, kv_head: usize) -> usize {
1078        let ng = self.head_dim.div_ceil(KV_K_GROUP);
1079        (self.k[kv_head].len() / self.head_dim)
1080            .max(self.ks[kv_head].len() / ng)
1081            .max(self.vs[kv_head].len())
1082    }
1083
1084    /// Clear cache (e.g. on new conversation or task switch).
1085    pub fn clear(&mut self) {
1086        for h in 0..self.num_kv_heads {
1087            self.k[h].clear();
1088            self.v[h].clear();
1089            self.kq[h].clear();
1090            self.ks[h].clear();
1091            self.vq[h].clear();
1092            self.vs[h].clear();
1093            self.kcol[h].clear();
1094            self.vcol[h].clear();
1095        }
1096        self.imp.clear();
1097        self.linear_state.clear();
1098        self.linear_scratch.clear();
1099        // Fresh conversation → the pipeline re-arms collection if the
1100        // layer is o1-flagged (landmarks are per-prompt, never reused).
1101        self.o1 = None;
1102        self.o1_error = None;
1103        self.o1_transitioned = false;
1104        self.seq_len = 0;
1105    }
1106
1107    /// Memory usage in bytes.
1108    /// Serialize this layer's state for the wire: `f16` halves it and is
1109    /// the caller's explicit choice, exactly like the hidden-state wire.
1110    ///
1111    /// REFUSES rather than travelling half-complete. A cache carrying
1112    /// frozen columns, accumulated importance, a Nyström overlay or q8
1113    /// storage holds state this format does not describe, and shipping
1114    /// the rest would land a plausible-looking cache that answers
1115    /// differently — the failure mode this whole format exists to avoid.
1116    pub fn export_wire(&self, f16: bool) -> Result<Vec<u8>, String> {
1117        if !matches!(self.mode, KvMode::F32) {
1118            return Err("kv export: only the F32 cache is described by this                         format (CMF_KV=q8 stores int8 rows and per-row scales)"
1119                .into());
1120        }
1121        if self.o1.is_some() {
1122            return Err("kv export: an O(1) Nyström overlay is not part of                         this format — the skeletons are irreversible and                         would have to travel with it"
1123                .into());
1124        }
1125        // Frozen columns only exist under q8 storage, which is refused
1126        // above. If one shows up under an F32 cache the format is lying
1127        // about something and the transfer must not proceed.
1128        if self.kcol.iter().any(|c| !c.is_empty()) || self.vcol.iter().any(|c| !c.is_empty()) {
1129            return Err(
1130                "kv export: frozen columns under an F32 cache — refusing to ship \
1131                        a state this format does not describe"
1132                    .into(),
1133            );
1134        }
1135        let mut out = Vec::with_capacity(self.memory_bytes() / if f16 { 2 } else { 1 } + 64);
1136        let u = |v: u32, o: &mut Vec<u8>| o.extend_from_slice(&v.to_le_bytes());
1137        u(u8::from(f16) as u32, &mut out);
1138        u(self.seq_len as u32, &mut out);
1139        u(self.num_kv_heads as u32, &mut out);
1140        u(self.head_dim as u32, &mut out);
1141        u(self.linear_state.len() as u32, &mut out);
1142        // Attention importance is ordinary state: every attention call
1143        // accumulates it and eviction reads it. Leaving it behind would
1144        // hand the far side a cache that forgets the RIGHT positions
1145        // later — a divergence that shows up only under pressure.
1146        u(self.imp.len() as u32, &mut out);
1147        let push = |xs: &[f32], o: &mut Vec<u8>| {
1148            if f16 {
1149                for &x in xs {
1150                    o.extend_from_slice(&cortiq_core::quant::f32_to_f16(x).to_le_bytes());
1151                }
1152            } else {
1153                for &x in xs {
1154                    o.extend_from_slice(&x.to_le_bytes());
1155                }
1156            }
1157        };
1158        // The recurrent state stays f32 whatever the wire dtype: it
1159        // is one small vector per layer and it is the ONLY state a linear
1160        // layer has — rounding it rounds the whole history.
1161        for &x in &self.linear_state {
1162            out.extend_from_slice(&x.to_le_bytes());
1163        }
1164        for &x in &self.imp {
1165            out.extend_from_slice(&x.to_le_bytes());
1166        }
1167        for h in 0..self.num_kv_heads {
1168            u(self.k[h].len() as u32, &mut out);
1169            push(&self.k[h], &mut out);
1170            u(self.v[h].len() as u32, &mut out);
1171            push(&self.v[h], &mut out);
1172        }
1173        Ok(out)
1174    }
1175
1176    /// Install a peer's state over this layer. The geometry must match the
1177    /// model both sides hold — it is checked, not assumed.
1178    pub fn import_wire(&mut self, buf: &[u8]) -> Result<(), String> {
1179        let mut o = 0usize;
1180        let u32_at = |o: &mut usize| -> Result<u32, String> {
1181            if *o + 4 > buf.len() {
1182                return Err("kv import: truncated header".into());
1183            }
1184            let v = u32::from_le_bytes(buf[*o..*o + 4].try_into().unwrap());
1185            *o += 4;
1186            Ok(v)
1187        };
1188        let f16 = u32_at(&mut o)? != 0;
1189        let seq_len = u32_at(&mut o)? as usize;
1190        let heads = u32_at(&mut o)? as usize;
1191        let hd = u32_at(&mut o)? as usize;
1192        let lin = u32_at(&mut o)? as usize;
1193        let nimp = u32_at(&mut o)? as usize;
1194        if heads != self.num_kv_heads || hd != self.head_dim {
1195            return Err(format!(
1196                "kv import: peer sent {heads}×{hd} per position, this layer is {}×{}",
1197                self.num_kv_heads, self.head_dim
1198            ));
1199        }
1200        let w = if f16 { 2 } else { 4 };
1201        let need = |n: usize, o: usize| -> Result<(), String> {
1202            if o + n > buf.len() {
1203                Err("kv import: truncated payload".into())
1204            } else {
1205                Ok(())
1206            }
1207        };
1208        need(lin * 4, o)?;
1209        self.linear_state = (0..lin)
1210            .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1211            .collect();
1212        o += lin * 4;
1213        need(nimp * 4, o)?;
1214        let imp: Vec<f32> = (0..nimp)
1215            .map(|i| f32::from_le_bytes(buf[o + i * 4..o + i * 4 + 4].try_into().unwrap()))
1216            .collect();
1217        o += nimp * 4;
1218        let mut k: Vec<Vec<f32>> = Vec::with_capacity(heads);
1219        let mut v: Vec<Vec<f32>> = Vec::with_capacity(heads);
1220        for _ in 0..heads {
1221            for which in 0..2 {
1222                let n = u32_at(&mut o)? as usize;
1223                need(n * w, o)?;
1224                let xs: Vec<f32> = (0..n)
1225                    .map(|i| {
1226                        let at = o + i * w;
1227                        if f16 {
1228                            cortiq_core::quant::f16_to_f32(u16::from_le_bytes(
1229                                buf[at..at + 2].try_into().unwrap(),
1230                            ))
1231                        } else {
1232                            f32::from_le_bytes(buf[at..at + 4].try_into().unwrap())
1233                        }
1234                    })
1235                    .collect();
1236                o += n * w;
1237                if which == 0 { k.push(xs) } else { v.push(xs) }
1238            }
1239        }
1240        self.mode = KvMode::F32;
1241        self.k = k;
1242        self.v = v;
1243        self.kq = vec![Vec::new(); heads];
1244        self.ks = vec![Vec::new(); heads];
1245        self.vq = vec![Vec::new(); heads];
1246        self.vs = vec![Vec::new(); heads];
1247        self.kcol = vec![Vec::new(); heads];
1248        self.vcol = vec![Vec::new(); heads];
1249        self.imp = imp;
1250        self.o1 = None;
1251        self.o1_error = None;
1252        self.o1_transitioned = false;
1253        self.seq_len = seq_len;
1254        Ok(())
1255    }
1256
1257    pub fn memory_bytes(&self) -> usize {
1258        let floats: usize = self.k.iter().map(Vec::len).sum::<usize>()
1259            + self.v.iter().map(Vec::len).sum::<usize>()
1260            + self.ks.iter().map(Vec::len).sum::<usize>()
1261            + self.vs.iter().map(Vec::len).sum::<usize>()
1262            + self.kcol.iter().map(Vec::len).sum::<usize>()
1263            + self.vcol.iter().map(Vec::len).sum::<usize>();
1264        let bytes: usize = self.kq.iter().map(Vec::len).sum::<usize>()
1265            + self.vq.iter().map(Vec::len).sum::<usize>();
1266        floats * std::mem::size_of::<f32>()
1267            + bytes
1268            // O(1) recurrent state of linear-core layers (vmf_phase/GDN):
1269            // constant in context, but real memory — the honest "KV+state"
1270            // line must count it (a pure-linear model reported 0 before).
1271            + self.linear_state.len() * std::mem::size_of::<f32>()
1272            // O(1) Nyström state (window + sinks + skeleton) — same
1273            // discipline: constant in context, but real memory.
1274            + self.o1_memory_bytes()
1275    }
1276
1277    /// Drop oldest positions, keeping the last `keep_last`.
1278    fn evict(&mut self, keep_last: usize) {
1279        // A collecting o1 layer owns a still-needed exact prefix and query
1280        // trace.  Evicting it would lower the effective seal boundary while
1281        // leaving q_buf untouched, so conversion could never match its KV
1282        // rows.  A sealed layer stores nothing per position — the Nyström
1283        // state IS the eviction policy; resetting seq_len here would lie
1284        // about the context depth.  Both states therefore bypass ordinary
1285        // eviction until the transition or explicit reset completes.
1286        if self.o1.is_some() || self.seq_len <= keep_last {
1287            return;
1288        }
1289        let drop = self.seq_len - keep_last;
1290        for h in 0..self.num_kv_heads {
1291            // Dead heads store fewer positions; drop proportionally.
1292            let stored = self.head_len(h);
1293            let d = drop.min(stored);
1294            let hd = self.head_dim;
1295            fn drop_front<T>(v: &mut Vec<T>, n: usize) {
1296                let n = n.min(v.len());
1297                v.drain(..n);
1298            }
1299            drop_front(&mut self.k[h], d * hd);
1300            drop_front(&mut self.v[h], d * hd);
1301            drop_front(&mut self.kq[h], d * hd);
1302            drop_front(&mut self.vq[h], d * hd);
1303            drop_front(&mut self.ks[h], d * hd.div_ceil(KV_K_GROUP));
1304            drop_front(&mut self.vs[h], d);
1305        }
1306        let d = drop.min(self.imp.len());
1307        self.imp.drain(..d);
1308        self.seq_len = keep_last;
1309    }
1310
1311    /// Mass-based eviction: keep `sink` earliest positions (attention sinks),
1312    /// the `recent` latest, and fill the rest of the `keep_last` budget
1313    /// with the positions carrying the highest accumulated attention
1314    /// mass (vmfcore: PPL 8.342 vs 8.687 for recency-only, full 8.295).
1315    fn evict_born(&mut self, keep_last: usize, sink: usize, recent: usize) {
1316        if self.o1.is_some() {
1317            // See evict(): collecting must retain the exact prefix as well as
1318            // sealed O(1) state must retain its own bounded representation.
1319            return;
1320        }
1321        let stored = self.imp.len();
1322        if stored <= keep_last {
1323            return;
1324        }
1325        // Budget discipline: sinks first, recents next, both clamped so
1326        // the total never exceeds keep_last.
1327        let sink_n = sink.min(keep_last);
1328        let recent_n = recent.min(keep_last - sink_n);
1329        let mut keep = vec![false; stored];
1330        for k in keep.iter_mut().take(sink_n) {
1331            *k = true;
1332        }
1333        for k in keep.iter_mut().skip(stored.saturating_sub(recent_n)) {
1334            *k = true;
1335        }
1336        let mut budget = keep_last.saturating_sub(keep.iter().filter(|&&x| x).count());
1337        // Highest accumulated mass first among the middle positions.
1338        let mut order: Vec<usize> = (0..stored).filter(|&i| !keep[i]).collect();
1339        order.sort_by(|&a, &b| {
1340            self.imp[b]
1341                .partial_cmp(&self.imp[a])
1342                .unwrap_or(std::cmp::Ordering::Equal)
1343        });
1344        for i in order {
1345            if budget == 0 {
1346                break;
1347            }
1348            keep[i] = true;
1349            budget -= 1;
1350        }
1351
1352        let kept: Vec<usize> = (0..stored).filter(|&i| keep[i]).collect();
1353        let hd = self.head_dim;
1354        fn gather<T: Copy>(src: &[T], kept: &[usize], step: usize) -> Vec<T> {
1355            let mut out = Vec::with_capacity(kept.len() * step);
1356            for &i in kept {
1357                out.extend_from_slice(&src[i * step..(i + 1) * step]);
1358            }
1359            out
1360        }
1361        // Each storage is gathered INDEPENDENTLY: in mixed modes
1362        // (q8k/q8v) K and V live in different storages — the paired branch
1363        // panicked (q8v) or silently left V uncompressed (q8k);
1364        // found by adversarial review, closed by regression tests.
1365        for h in 0..self.num_kv_heads {
1366            if !self.k[h].is_empty() {
1367                self.k[h] = gather(&self.k[h], &kept, hd);
1368            }
1369            if !self.v[h].is_empty() {
1370                self.v[h] = gather(&self.v[h], &kept, hd);
1371            }
1372            if !self.kq[h].is_empty() {
1373                self.kq[h] = gather(&self.kq[h], &kept, hd);
1374                self.ks[h] = gather(&self.ks[h], &kept, hd.div_ceil(KV_K_GROUP));
1375            }
1376            if !self.vq[h].is_empty() {
1377                self.vq[h] = gather(&self.vq[h], &kept, hd);
1378                self.vs[h] = gather(&self.vs[h], &kept, 1);
1379            }
1380        }
1381        self.imp = kept.iter().map(|&i| self.imp[i]).collect();
1382        self.seq_len = kept.len();
1383    }
1384}
1385
1386/// Eviction policy for a bounded cache.
1387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1388pub enum EvictionPolicy {
1389    /// Sliding window: keep only the most recent positions.
1390    Recent,
1391    /// Mass-based eviction: sinks + recents + top accumulated attention mass.
1392    Born { sink: usize },
1393}
1394
1395/// Full KV cache for all layers.
1396#[derive(Debug)]
1397pub struct KvCache {
1398    pub layers: Vec<LayerKvCache>,
1399    pub max_seq_len: usize,
1400    pub policy: EvictionPolicy,
1401}
1402
1403impl KvCache {
1404    pub fn new(
1405        num_layers: usize,
1406        num_kv_heads: usize,
1407        head_dim: usize,
1408        max_seq_len: usize,
1409    ) -> Self {
1410        let layers = (0..num_layers)
1411            .map(|_| LayerKvCache::new(num_kv_heads, head_dim))
1412            .collect();
1413        Self {
1414            layers,
1415            max_seq_len,
1416            policy: EvictionPolicy::Born { sink: 4 },
1417        }
1418    }
1419
1420    pub fn clear(&mut self) {
1421        for layer in &mut self.layers {
1422            layer.clear();
1423        }
1424    }
1425
1426    pub fn total_memory_bytes(&self) -> usize {
1427        self.layers.iter().map(|l| l.memory_bytes()).sum()
1428    }
1429
1430    /// Current sequence length (max across layers — dead layers may lag).
1431    pub fn seq_len(&self) -> usize {
1432        self.layers.iter().map(|l| l.seq_len).max().unwrap_or(0)
1433    }
1434
1435    pub fn needs_eviction(&self) -> bool {
1436        self.seq_len() >= self.max_seq_len
1437    }
1438
1439    /// Evict down to `keep_last` positions according to the policy.
1440    pub fn evict(&mut self, keep_last: usize) {
1441        match self.policy {
1442            EvictionPolicy::Recent => {
1443                for layer in &mut self.layers {
1444                    layer.evict(keep_last);
1445                }
1446            }
1447            EvictionPolicy::Born { sink } => {
1448                let recent = (keep_last / 2).max(1);
1449                for layer in &mut self.layers {
1450                    layer.evict_born(keep_last, sink, recent);
1451                }
1452            }
1453        }
1454    }
1455}
1456
1457#[cfg(test)]
1458mod tests {
1459    use super::*;
1460
1461    #[test]
1462    fn wire_round_trip_reproduces_attention() {
1463        // The state has to arrive as state, not as something that looks
1464        // like it: the oracle is what the layer ANSWERS, not what it
1465        // stores. Same query, same output, bit for bit.
1466        let (heads, hd) = (2usize, 4usize);
1467        let mut a = LayerKvCache::new(heads, hd);
1468        for p in 0..5 {
1469            let k: Vec<f32> = (0..heads * hd)
1470                .map(|i| (p * 10 + i) as f32 * 0.031)
1471                .collect();
1472            let v: Vec<f32> = (0..heads * hd)
1473                .map(|i| (p * 7 + i) as f32 * -0.017)
1474                .collect();
1475            a.append(&k, &v, &[true, true]);
1476        }
1477        a.linear_state = vec![0.5, -0.25, 1.0];
1478        let q: Vec<f32> = (0..hd).map(|i| 0.1 * (i as f32 + 1.0)).collect();
1479
1480        let bytes = a.export_wire(false).expect("f32 cache exports");
1481        let mut b = LayerKvCache::new(heads, hd);
1482        b.import_wire(&bytes).expect("import");
1483
1484        assert_eq!(b.seq_len, a.seq_len);
1485        assert_eq!(b.linear_state, a.linear_state);
1486        for h in 0..heads {
1487            let (oa, sa) = a.attend(&q, h);
1488            let (ob, sb) = b.attend(&q, h);
1489            assert_eq!(oa, ob, "head {h} attention output diverged");
1490            assert_eq!(sa, sb, "head {h} attention scores diverged");
1491        }
1492    }
1493
1494    #[test]
1495    fn wire_refuses_what_it_cannot_describe() {
1496        // A refusal is the feature: a cache whose extra state this format
1497        // does not carry must not travel looking complete.
1498        let mut c = LayerKvCache::new(1, 4);
1499        c.mode = KvMode::Q8 { k: true, v: true };
1500        let err = c.export_wire(false).unwrap_err();
1501        assert!(err.contains("F32"), "{err}");
1502    }
1503
1504    #[test]
1505    fn wire_import_checks_geometry() {
1506        let a = LayerKvCache::new(2, 4);
1507        let bytes = a.export_wire(false).unwrap();
1508        let mut wrong = LayerKvCache::new(2, 8);
1509        let err = wrong.import_wire(&bytes).unwrap_err();
1510        assert!(err.contains("2×4"), "{err}");
1511    }
1512
1513    #[test]
1514    fn append_tracks_seq_len_and_layout() {
1515        let mut cache = LayerKvCache::new(4, 8);
1516        cache.mode = KvMode::F32;
1517        assert_eq!(cache.seq_len, 0);
1518
1519        let k: Vec<f32> = (0..32).map(|i| i as f32).collect();
1520        let v = vec![2.0f32; 32];
1521        cache.append(&k, &v, &[true; 4]);
1522
1523        assert_eq!(cache.seq_len, 1);
1524        assert_eq!(cache.head_len(0), 1);
1525        // head 1 slice is contiguous and equals its part of k_new
1526        assert_eq!(cache.head_keys(1), &k[8..16]);
1527        assert_eq!(cache.memory_bytes(), 256);
1528    }
1529
1530    #[test]
1531    fn dead_head_stores_nothing() {
1532        let mut cache = LayerKvCache::new(2, 4);
1533        cache.mode = KvMode::F32;
1534        let k = vec![1.0f32; 8];
1535        let v = vec![2.0f32; 8];
1536        cache.append(&k, &v, &[true, false]);
1537        cache.append(&k, &v, &[true, false]);
1538
1539        assert_eq!(cache.seq_len, 2);
1540        assert_eq!(cache.head_len(0), 2);
1541        assert_eq!(cache.head_len(1), 0, "dead head must not store KV");
1542        assert_eq!(cache.memory_bytes(), 2 * 2 * 4 * 4);
1543    }
1544
1545    #[test]
1546    fn eviction_keeps_recent() {
1547        let mut cache = KvCache::new(2, 4, 8, 10);
1548        cache.policy = EvictionPolicy::Recent;
1549        for l in &mut cache.layers {
1550            l.mode = KvMode::F32;
1551        }
1552        let k = vec![1.0f32; 32];
1553        let v = vec![2.0f32; 32];
1554        for _ in 0..8 {
1555            for layer in &mut cache.layers {
1556                layer.append(&k, &v, &[true; 4]);
1557            }
1558        }
1559        assert_eq!(cache.seq_len(), 8);
1560        assert!(!cache.needs_eviction());
1561
1562        cache.evict(4);
1563        assert_eq!(cache.seq_len(), 4);
1564        assert_eq!(cache.layers[0].head_len(0), 4);
1565    }
1566
1567    #[test]
1568    fn collecting_o1_eviction_retains_exact_storage_until_boundary() {
1569        const B: usize = 19;
1570        let q = vec![0.1f32; 8];
1571        let k = vec![0.2f32; 4];
1572        let v = vec![0.3f32; 4];
1573
1574        for policy in [EvictionPolicy::Recent, EvictionPolicy::Born { sink: 2 }] {
1575            let mut cache = KvCache::new(1, 1, 4, 6);
1576            cache.policy = policy;
1577            cache.layers[0].mode = KvMode::F32;
1578            cache.layers[0].o1_begin_with_boundary(
1579                4,
1580                8,
1581                2,
1582                crate::nystrom::O1Rect::Aggregate,
1583                Some(B),
1584            );
1585
1586            for pos in 0..B {
1587                {
1588                    let layer = &mut cache.layers[0];
1589                    layer.o1_push_q(&q);
1590                    layer.append(&k, &v, &[]);
1591                }
1592                if pos + 1 < B {
1593                    cache.evict(3);
1594                }
1595            }
1596
1597            let layer = &cache.layers[0];
1598            let rows = B * layer.head_dim;
1599            assert_eq!(layer.seq_len, B, "policy {policy:?} retained depth");
1600            assert_eq!(layer.k[0].len(), rows, "policy {policy:?} K rows");
1601            assert_eq!(layer.v[0].len(), rows, "policy {policy:?} V rows");
1602            assert!(
1603                layer.k[0].capacity() >= rows,
1604                "policy {policy:?} K capacity"
1605            );
1606            assert!(
1607                layer.v[0].capacity() >= rows,
1608                "policy {policy:?} V capacity"
1609            );
1610            let q_capacity = match layer.o1.as_ref() {
1611                Some(O1State::Collecting { q_buf, .. }) => q_buf.capacity(),
1612                other => panic!("policy {policy:?} changed state early: {other:?}"),
1613            };
1614            assert!(
1615                q_capacity >= B * 8,
1616                "policy {policy:?} Q capacity must cover the exact prefix"
1617            );
1618
1619            assert!(cache.layers[0].o1_seal_checked(2).unwrap());
1620            assert_eq!(cache.layers[0].k[0].capacity(), 0, "K released after seal");
1621            assert_eq!(cache.layers[0].v[0].capacity(), 0, "V released after seal");
1622        }
1623    }
1624
1625    #[test]
1626    fn truncate_rolls_back_speculative_positions() {
1627        let mut cache = LayerKvCache::new(2, 4);
1628        cache.mode = KvMode::F32;
1629        for pos in 0..5 {
1630            let k = vec![pos as f32; 8];
1631            let v = vec![pos as f32; 8];
1632            cache.append(&k, &v, &[true; 2]);
1633        }
1634        cache.truncate_last(2);
1635        assert_eq!(cache.seq_len, 3);
1636        assert_eq!(cache.head_len(0), 3);
1637        assert_eq!(cache.head_keys(0)[2 * 4], 2.0, "position 2 survives");
1638    }
1639
1640    /// q8_2f-attend ≈ f32-attend: 100 positions (crosses the field freeze
1641    /// at the 64th), pseudo-random vectors, relative tolerance of the
1642    /// int8 grid. Plus rollback and mass-based eviction on the q8 storage.
1643    #[test]
1644    fn q8_attend_matches_f32_within_grid() {
1645        let (heads, hd) = (2, 32);
1646        let mut f = LayerKvCache::new(heads, hd);
1647        f.mode = KvMode::F32;
1648        let mut q8 = LayerKvCache::new(heads, hd);
1649        q8.mode = KvMode::Q8 { k: true, v: true };
1650
1651        let synth = |p: usize, salt: usize| -> Vec<f32> {
1652            (0..heads * hd)
1653                .map(|i| {
1654                    let x = ((i * 31 + p * 17 + salt * 7 + 3) % 97) as f32 / 97.0 - 0.5;
1655                    // channel structure: even channels ×4 (checks the 2f field)
1656                    if i % 2 == 0 { x * 4.0 } else { x * 0.25 }
1657                })
1658                .collect()
1659        };
1660        for p in 0..100 {
1661            let k = synth(p, 1);
1662            let v = synth(p, 2);
1663            f.append(&k, &v, &[true; 2]);
1664            q8.append(&k, &v, &[true; 2]);
1665        }
1666        let q: Vec<f32> = (0..hd)
1667            .map(|i| ((i * 13 + 5) % 89) as f32 / 89.0 - 0.5)
1668            .collect();
1669        for g in 0..heads {
1670            let (of, pf) = f.attend(&q, g);
1671            let (o8, p8) = q8.attend(&q, g);
1672            let scale = of.iter().fold(0f32, |m, x| m.max(x.abs())).max(1e-6);
1673            for d in 0..hd {
1674                assert!(
1675                    (of[d] - o8[d]).abs() <= scale * 0.03 + 1e-3,
1676                    "g{g} d{d}: f32 {} vs q8 {}",
1677                    of[d],
1678                    o8[d]
1679                );
1680            }
1681            for p in 0..100 {
1682                assert!((pf[p] - p8[p]).abs() < 0.02, "prob p{p}");
1683            }
1684        }
1685        // rollback + eviction live on the q8 storage
1686        q8.truncate_last(30);
1687        assert_eq!(q8.head_len(0), 70);
1688        let imp: Vec<f32> = (0..70).map(|i| i as f32).collect();
1689        q8.accumulate_imp(&imp);
1690        q8.evict_born(20, 2, 8);
1691        assert_eq!(q8.head_len(0), 20);
1692        let (o, _) = q8.attend(&q, 0);
1693        assert!(o.iter().all(|x| x.is_finite()));
1694        // memory: q8 ≈ 1 byte/element + scale per row (vs 4 for f32)
1695        assert!(q8.memory_bytes() * 3 < f.memory_bytes());
1696    }
1697
1698    /// Grouped GQA attend must be bit-identical to per-head attend in
1699    /// every KV mode (it is the same math with rows streamed once).
1700    #[test]
1701    fn attend_group_equals_per_head_attend_bitexact() {
1702        let (kv_heads, hd, hpk) = (2usize, 32usize, 3usize); // 6 Q-heads
1703        for mode in [KvMode::F32, KvMode::Q8 { k: true, v: true }] {
1704            let mut c = LayerKvCache::new(kv_heads, hd);
1705            c.mode = mode;
1706            for p in 0..70 {
1707                let k: Vec<f32> = (0..kv_heads * hd)
1708                    .map(|i| ((i * 31 + p * 17 + 3) % 97) as f32 / 97.0 - 0.5)
1709                    .collect();
1710                let v: Vec<f32> = (0..kv_heads * hd)
1711                    .map(|i| ((i * 13 + p * 29 + 7) % 89) as f32 / 89.0 - 0.5)
1712                    .collect();
1713                c.append(&k, &v, &[true; 2]);
1714            }
1715            let q: Vec<f32> = (0..kv_heads * hpk * hd)
1716                .map(|i| ((i * 11 + 5) % 83) as f32 / 83.0 - 0.5)
1717                .collect();
1718            for g in 0..kv_heads {
1719                let span = g * hpk * hd..(g + 1) * hpk * hd;
1720                let mut out = vec![0f32; hpk * hd];
1721                let mut imp = vec![0f32; 70];
1722                c.attend_group(
1723                    &q[span.clone()],
1724                    g,
1725                    &mut out,
1726                    &mut imp,
1727                    1.0 / (hd as f32).sqrt(),
1728                    0,
1729                    0.0,
1730                );
1731                let mut imp_ref = vec![0f32; 70];
1732                for h in 0..hpk {
1733                    let qh = &q[span.start + h * hd..span.start + (h + 1) * hd];
1734                    let (o, probs) = c.attend(qh, g);
1735                    assert_eq!(
1736                        &out[h * hd..(h + 1) * hd],
1737                        &o[..],
1738                        "mode {mode:?} g{g} h{h}: grouped attend must be bit-identical"
1739                    );
1740                    for (dst, &p) in imp_ref.iter_mut().zip(&probs) {
1741                        *dst += p;
1742                    }
1743                }
1744                assert_eq!(
1745                    imp, imp_ref,
1746                    "mode {mode:?} g{g}: attention mass must match"
1747                );
1748            }
1749        }
1750    }
1751
1752    /// Review regression: mass-based eviction in MIXED modes. q8v used to
1753    /// panic (gather over an empty v[h]), q8k silently left raw V
1754    /// uncompressed (stale rows under kept keys + memory leak).
1755    #[test]
1756    fn born_eviction_mixed_modes_stay_consistent() {
1757        for (mk, mv) in [(false, true), (true, false)] {
1758            let mut c = LayerKvCache::new(1, 4);
1759            c.mode = KvMode::Q8 { k: mk, v: mv };
1760            for p in 0..80 {
1761                let k = vec![p as f32 * 0.01; 4];
1762                let v = vec![p as f32; 4];
1763                c.append(&k, &v, &[true]);
1764            }
1765            let imp: Vec<f32> = (0..80).map(|i| i as f32).collect();
1766            c.accumulate_imp(&imp);
1767            let before = c.memory_bytes();
1768            c.evict_born(20, 4, 8); // q8v: used to panic here
1769            assert_eq!(c.head_len(0), 20, "k={mk} v={mv}");
1770            assert!(
1771                c.memory_bytes() < before / 2,
1772                "memory must shrink (k={mk} v={mv})"
1773            );
1774            // V rows match the kept set: the heaviest positions
1775            // (tail 60..79) must be present in the attend output.
1776            let (out, _) = c.attend(&[1.0, 1.0, 1.0, 1.0], 0);
1777            assert!(
1778                out[0] > 30.0,
1779                "V from the kept tail, not the stale head (k={mk} v={mv}, out {})",
1780                out[0]
1781            );
1782        }
1783    }
1784
1785    #[test]
1786    fn born_eviction_keeps_high_mass_position() {
1787        let mut cache = KvCache::new(1, 1, 2, 16);
1788        cache.policy = EvictionPolicy::Born { sink: 1 };
1789        for l in &mut cache.layers {
1790            l.mode = KvMode::F32;
1791        }
1792        let layer = &mut cache.layers[0];
1793        // 8 positions; keys carry the position index so we can verify
1794        // exactly which positions survive the gather.
1795        for pos in 0..8 {
1796            let k = vec![pos as f32; 2];
1797            let v = vec![pos as f32 + 100.0; 2];
1798            layer.append(&k, &v, &[true]);
1799        }
1800        // Position 3 carries the most attention mass.
1801        let mut imp = vec![0.05f32; 8];
1802        imp[3] = 5.0;
1803        layer.accumulate_imp(&imp);
1804
1805        cache.evict(4); // sink 1 + recent 2 + 1 top-mass slot
1806        let layer = &cache.layers[0];
1807        assert_eq!(layer.seq_len, 4);
1808        let kept_keys: Vec<f32> = (0..4).map(|i| layer.head_keys(0)[i * 2]).collect();
1809        assert_eq!(
1810            kept_keys,
1811            vec![0.0, 3.0, 6.0, 7.0],
1812            "kept = sink(0) + mass-top(3) + recent(6,7)"
1813        );
1814        // imp stays aligned with the gathered positions.
1815        assert_eq!(layer.head_len(0), 4);
1816    }
1817}