Skip to main content

memra_kv/
lib.rs

1//! memra-kv — the dual KV/recurrent cache, extracted (Phase D, ARCHITECTURE-H100.md §5).
2//!
3//! Moved VERBATIM from memra-engine/src/cache.rs behind the `KvDev` seam: the cache only
4//! ever needed 7 device ops (alloc/copy/set), so the trait is that surface and nothing
5//! more. The append/dequant KERNELS stay in the engine fatbins — this crate owns the
6//! structure, sizing math, and the KV format policy (env-selected, shared by the engine's
7//! fatbin router and every cache consumer). memra-engine re-exports this as `cache` so
8//! call sites are unchanged.
9
10// ---------------- KV format policy (env-selected; moved from memra-engine) ----------------
11
12/// Env-selected KV cache formats (MEMRA_KV_K / MEMRA_KV_V). The engine's flash-fatbin router
13/// and the cache sizing below MUST agree — both read this one function.
14pub fn kv_cache_formats() -> (&'static str, &'static str) {
15    static F: std::sync::OnceLock<(&'static str, &'static str)> = std::sync::OnceLock::new();
16    *F.get_or_init(|| {
17        let k = match std::env::var("MEMRA_KV_K").as_deref() {
18            Ok("fp8") => "fp8",
19            Ok("q8_0") | Ok("") | Err(_) => "q8_0",
20            Ok(o) => panic!("MEMRA_KV_K={o} unsupported (q8_0 | fp8)"),
21        };
22        let v = match std::env::var("MEMRA_KV_V").as_deref() {
23            Ok("q4_0") => "q4_0",
24            Ok("fp8") => "fp8",
25            Ok("q5_1") | Ok("") | Err(_) => "q5_1",
26            Ok(o) => panic!("MEMRA_KV_V={o} unsupported (q5_1 | q4_0 | fp8)"),
27        };
28        if (k, v) != ("q8_0", "q5_1") {
29            eprintln!("[memra] KV cache format: K={k} V={v} (non-default — new numeric config)");
30        }
31        (k, v)
32    })
33}
34
35/// Per-32-element block bytes for the selected (K, V) formats.
36pub fn kv_blk_bytes() -> (usize, usize) {
37    let (k, v) = kv_cache_formats();
38    let kb = match k {
39        "fp8" => 32,
40        _ => 34,
41    };
42    let vb = match v {
43        "q4_0" => 18,
44        "fp8" => 32,
45        _ => 24,
46    };
47    (kb, vb)
48}
49
50/// Exact allocation geometry for one rank of a tensor-parallel KV sidecar.
51///
52/// The context-linear coefficient and fixed allocation bytes are shared by the CUDA allocator
53/// and serving admission. Keeping both consumers on this function prevents a new KV format or
54/// rank width from making the pre-admit estimate disagree with the buffers allocated at decode.
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct TpKvRankAllocationShape {
57    pub kv_dim_k: usize,
58    pub kv_dim_v: usize,
59    pub k_token_bytes: usize,
60    pub v_token_bytes: usize,
61    pub fixed_bytes: usize,
62}
63
64impl TpKvRankAllocationShape {
65    pub fn bytes_per_token(self) -> usize {
66        self.k_token_bytes.saturating_add(self.v_token_bytes)
67    }
68
69    pub fn allocation_bytes(self, capacity: usize) -> usize {
70        self.bytes_per_token()
71            .saturating_mul(capacity)
72            .saturating_add(self.fixed_bytes)
73    }
74}
75
76pub fn tp_kv_rank_allocation_shape(
77    kv_dim_k: usize,
78    kv_dim_v: usize,
79    ranks: usize,
80) -> Result<TpKvRankAllocationShape, String> {
81    if ranks == 0 || kv_dim_k == 0 || kv_dim_v == 0 {
82        return Err(format!(
83            "TP KV dimensions and rank count must be nonzero: k={kv_dim_k} v={kv_dim_v} \
84             ranks={ranks}"
85        ));
86    }
87    if kv_dim_k % ranks != 0 || kv_dim_v % ranks != 0 {
88        return Err(format!(
89            "TP KV dimensions k={kv_dim_k} v={kv_dim_v} are not divisible by TP={ranks}"
90        ));
91    }
92    let local_k = kv_dim_k / ranks;
93    let local_v = kv_dim_v / ranks;
94    if local_k % 32 != 0 || local_v % 32 != 0 {
95        return Err(format!(
96            "TP KV local dimensions k={local_k} v={local_v} must be 32-aligned"
97        ));
98    }
99    let (k_block_bytes, v_block_bytes) = kv_blk_bytes();
100    let k_token_bytes = (local_k / 32)
101        .checked_mul(k_block_bytes)
102        .ok_or("TP KV K token-byte overflow")?;
103    let v_token_bytes = (local_v / 32)
104        .checked_mul(v_block_bytes)
105        .ok_or("TP KV V token-byte overflow")?;
106    Ok(TpKvRankAllocationShape {
107        kv_dim_k: local_k,
108        kv_dim_v: local_v,
109        k_token_bytes,
110        v_token_bytes,
111        // Two CUDA byte planes retain their existing 8-byte tail pads, plus one i32 length
112        // mirror. Allocator alignment remains visible through the device pool high-water.
113        fixed_bytes: 8 + 8 + std::mem::size_of::<i32>(),
114    })
115}
116
117/// FP8-GLOBALS switch (MEMRA_GEMMA_GKV, default ON): gemma global (hd512) layers keep
118/// their KV in e4m3 — the dequant-latency arc (HANDOVER). Windowed layers stay q8_0/q5_1.
119pub fn gkv_on() -> bool {
120    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
121    *ON.get_or_init(|| {
122        std::env::var("MEMRA_GEMMA_GKV")
123            .map(|v| v != "0")
124            .unwrap_or(true)
125    })
126}
127
128/// FP8-WINDOWED switch (MEMRA_GEMMA_WKV; serving-mode default): SPEC serving (MEMRA_DRAFT
129/// set) -> OFF, plain -> ON — the acceptance-vs-depth record lives on the engine-side
130/// history of `Engine::wkv_on` (git). Explicit env always wins.
131pub fn wkv_on() -> bool {
132    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
133    *ON.get_or_init(|| {
134        std::env::var("MEMRA_GEMMA_WKV")
135            .map(|v| v != "0")
136            .unwrap_or_else(|_| std::env::var("MEMRA_DRAFT").is_err())
137    })
138}
139
140/// Per-model FP8-KV door (-1 = unset → env/default off; 0 = off; 1 = on). Set at qwen
141/// model load: the 2026-07-12 arc closed per-model — 9B +0.7-4% scaling with depth,
142/// 27B flat (weight-bound), 35B −2% (fp8 format-gates its v3 dp4a lane off). Explicit
143/// MEMRA_KV_FP8 wins. 9B adoption attempt REVERTED by measurement 2026-07-29 (−1% at 12k
144/// on the then-current build) — loaders currently store 0.
145pub static KV_FP8_FORCE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
146
147/// QWEN FP8-KV switch (MEMRA_KV_FP8 explicit; else the per-model KV_FP8_FORCE door set
148/// at model load; else OFF). Non-gemma full-attn layers hold e4m3 K/V via the kf8vf8
149/// module.
150pub fn kv_fp8_on() -> bool {
151    static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
152    if let Some(v) = *ENV.get_or_init(|| std::env::var("MEMRA_KV_FP8").ok().map(|v| v == "1")) {
153        return v;
154    }
155    matches!(KV_FP8_FORCE.load(std::sync::atomic::Ordering::Relaxed), 1)
156}
157
158/// Step35 SWA ring. Default OFF unless the loader arms the step37 serving default (owner flip
159/// 2026-08-27: the ring frees 16.4 GB on card0 at the natural 262144 context with identical
160/// throughput and ids, and the W8 doors OOM there without it). Architecture-scoped by its call
161/// sites: Gemma4's row-0-addressed window kernels cannot consume a rebased ring view, which is
162/// why the default arms per loaded family rather than globally. `MEMRA_SWA_RING=1` forces ON,
163/// `=0` is the kill switch either way.
164static SWA_RING_DEFAULT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
165
166pub fn set_swa_ring_default(on: bool) {
167    SWA_RING_DEFAULT.store(on, std::sync::atomic::Ordering::Relaxed);
168}
169
170pub fn swa_ring_on() -> bool {
171    static ENV: std::sync::OnceLock<Option<bool>> = std::sync::OnceLock::new();
172    match *ENV.get_or_init(|| match std::env::var("MEMRA_SWA_RING").ok().as_deref() {
173        Some("1") => Some(true),
174        Some("0") => Some(false),
175        _ => None,
176    }) {
177        Some(forced) => forced,
178        None => SWA_RING_DEFAULT.load(std::sync::atomic::Ordering::Relaxed),
179    }
180}
181
182/// With the SWA-ring door open, `prime_chunk_tokens` caps every legal chunk at this bound. The
183/// ring carries one whole maximum-size prime chunk in addition to the reader's window.
184pub const PRIME_CHUNK_MAX_TOKENS: usize = 4096;
185const SWA_VIEW_ALIGNMENT_ROWS: usize = 32;
186
187/// Physical rows required by the Step35 SWA reader contract. Prime starts at
188/// `(base_len - (window - 1)) & !31`, so at most 31 masked rows precede the live window.
189pub fn swa_ring_rows(window: usize, max_ctx: usize) -> usize {
190    // window + max prime chunk + REWIND HEADROOM + alignment. append_plan requires
191    // keep_rows + append_rows <= rows, and keep_rows is now window + SWA_REWIND_SLACK_ROWS, so the
192    // headroom has to be in `rows` or a full-size prime chunk stops fitting. Costs
193    // SWA_REWIND_SLACK_ROWS rows per ring-backed plane.
194    max_ctx.min(
195        window + PRIME_CHUNK_MAX_TOKENS + SWA_REWIND_SLACK_ROWS + (SWA_VIEW_ALIGNMENT_ROWS - 1),
196    )
197}
198
199/// Rows a ring-backed plane keeps BELOW the aligned window start so a backward rewind stays legal.
200///
201/// This is HEADROOM THE RING IS SIZED FOR, not slack scavenged from it. The original geometry
202/// (window + prime chunk + 31) left exactly one alignment block spare once a full prime chunk had
203/// to fit, and one block only covers a rewind shallower than 32 rows. Clamping a deeper request up
204/// to `base` instead makes the append legal while leaving the attention window pointing below rows
205/// the ring no longer holds — which produced all-NaN head logits and seed hiddens at pos 8661
206/// rather than an error. A ring that cannot serve the rewinds its own callers perform is
207/// undersized; the fix is to size it, not to keep redistributing 32 rows.
208pub const SWA_REWIND_SLACK_ROWS: usize = 512;
209
210/// The retain a ring-backed append must request: the aligned window start for `first_row`, minus
211/// the rewind slack, but NEVER below what the ring still holds.
212///
213/// The clamp is the part that took three attempts to find. Asking for slack unconditionally makes
214/// the REWIND legal and then breaks the very next APPEND: after a rewind, `first_row` moves back
215/// while `base` does not, so the ideal retain falls under `base` and append_plan refuses it
216/// ("SWA ring lapped required rows (base 4128, retain 4096, len 4669)"). Rows below `base` are
217/// gone and, being older than the window, are not needed — so clamping up to `base` is both legal
218/// and correct. Slack is an optimisation the ring grants when it can, never a demand.
219pub fn swa_retain_from(first_row: usize, window: usize, base: usize) -> usize {
220    let ideal = first_row
221        .saturating_sub(window.saturating_sub(1))
222        .saturating_sub(SWA_REWIND_SLACK_ROWS)
223        & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
224    ideal.max(base)
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq)]
228pub struct KvRing {
229    rows: usize,
230    window: usize,
231    base: usize,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq)]
235pub enum KvRingAppend {
236    Contiguous {
237        write_row: usize,
238    },
239    Rebase {
240        src_row: usize,
241        keep_rows: usize,
242        new_base: usize,
243        write_row: usize,
244    },
245}
246
247impl KvRing {
248    pub fn new(rows: usize, window: usize) -> Self {
249        assert!(window > 0 && rows > 0, "invalid SWA ring geometry");
250        Self {
251            rows,
252            window,
253            base: 0,
254        }
255    }
256
257    pub fn rows(&self) -> usize {
258        self.rows
259    }
260    pub fn base(&self) -> usize {
261        self.base
262    }
263    pub fn window(&self) -> usize {
264        self.window
265    }
266
267    /// Plan a contiguous physical append. When the tail would wrap, retain the caller's exact
268    /// aligned read prefix at row zero; the following read remains one contiguous CUDA view.
269    pub fn append_plan(
270        &self,
271        len: usize,
272        retain_from: usize,
273        append_rows: usize,
274    ) -> Result<KvRingAppend, String> {
275        if len < self.base || retain_from < self.base || retain_from > len {
276            return Err(format!(
277                "SWA ring lapped required rows (base {}, retain {retain_from}, len {len})",
278                self.base
279            ));
280        }
281        let used = len - self.base;
282        if used > self.rows {
283            return Err(format!(
284                "SWA ring state exceeds capacity ({used} > {})",
285                self.rows
286            ));
287        }
288        if used.saturating_add(append_rows) <= self.rows {
289            return Ok(KvRingAppend::Contiguous {
290                write_row: used % self.rows,
291            });
292        }
293
294        let keep_rows = len - retain_from;
295        if keep_rows.saturating_add(append_rows) > self.rows {
296            return Err(format!(
297                "SWA ring append does not fit (keep {keep_rows} + append {append_rows} > {})",
298                self.rows
299            ));
300        }
301        Ok(KvRingAppend::Rebase {
302            src_row: retain_from - self.base,
303            keep_rows,
304            new_base: retain_from,
305            write_row: keep_rows,
306        })
307    }
308
309    pub fn apply_rebase(&mut self, new_base: usize) {
310        debug_assert!(new_base >= self.base);
311        self.base = new_base;
312    }
313
314    pub fn physical_range(
315        &self,
316        start: usize,
317        end: usize,
318    ) -> Result<std::ops::Range<usize>, String> {
319        if start < self.base || end < start || end - self.base > self.rows {
320            return Err(format!(
321                "SWA ring view [{start},{end}) is outside resident [{},{})",
322                self.base,
323                self.base + self.rows
324            ));
325        }
326        let start_row = (start - self.base) % self.rows;
327        let len = end - start;
328        debug_assert!(
329            start_row + len <= self.rows,
330            "ring view must be contiguous after rebase"
331        );
332        Ok(start_row..start_row + len)
333    }
334
335    /// A rewind is usable only when the next aligned Step35 window view is still resident.
336    pub fn can_rewind_to(&self, len: usize) -> bool {
337        let raw = len.saturating_sub(self.window - 1);
338        let view_start = raw & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
339        view_start >= self.base
340    }
341
342    /// Plan a checkpoint restore into a FRESH ring (base 0): the aligned live window ending at
343    /// absolute `len`, as (new_base, source physical rows). `len` is the ABSOLUTE stream length
344    /// the checkpoint recorded — for a lapped ring it exceeds the physical row count, so a
345    /// restore that copies `len` rows from row zero is an out-of-bounds device slice (the
346    /// 2026-08-29 warm-turn-at-40k GPU-worker panic). Refuses when this ring no longer holds
347    /// the window (checkpoint lapped: the caller must full re-prime).
348    pub fn restore_plan(&self, len: usize) -> Result<(usize, std::ops::Range<usize>), String> {
349        let raw = len.saturating_sub(self.window.saturating_sub(1));
350        let new_base = raw & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
351        let physical = self.physical_range(new_base, len)?;
352        Ok((new_base, physical))
353    }
354}
355
356// ---------------- the device seam ----------------
357
358/// The 7 device ops the cache needs — nothing more. Implemented by the engine (and by
359/// any future backend); all ops are stream-ordered on the implementor's worker stream.
360pub trait KvDev {
361    fn zeros(&self, n: usize) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>>;
362    fn uninit(&self, n: usize) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>>;
363    fn alloc_u8(&self, n: usize) -> Result<CudaSlice<u8>, Box<dyn std::error::Error>>;
364    fn htod_i32(&self, v: &[i32]) -> Result<CudaSlice<i32>, Box<dyn std::error::Error>>;
365    fn clone_dtod(
366        &self,
367        src: &CudaSlice<f32>,
368    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>>;
369    fn copy_into(
370        &self,
371        dst: &mut CudaSlice<f32>,
372        off: usize,
373        src: &CudaSlice<f32>,
374        len: usize,
375    ) -> Result<(), Box<dyn std::error::Error>>;
376    fn set_i32_one(&self, d: &mut CudaSlice<i32>, v: i32)
377    -> Result<(), Box<dyn std::error::Error>>;
378}
379
380use cudarc::driver::CudaSlice;
381use memra_gguf::config::{LayerKind, ModelConfig};
382use memra_gguf::model_plan::{ModelPlan, ResidualTopology, StatePlan};
383
384/// Per-full-attn-layer growing KV cache, resident on GPU. QUANTIZED (KVQUANT-PLAN §B):
385/// K stored q8_0 (34 B/32 elem), V stored q5_1 (24 B/32 elem). Per-token byte layout keeps the
386/// [token, kv_head, dim] element order so a 32-block never straddles a head (assert head_dim%32==0).
387/// Element-within-token index = kv_head*head_dim + d; block = idx/32; lane = idx%32.
388pub struct KvLayer {
389    pub k: CudaSlice<u8>,   // q8_0 packed, capacity max_ctx*k_tok_bytes
390    pub v: CudaSlice<u8>,   // q5_1 packed, capacity max_ctx*v_tok_bytes
391    pub kv_dim_k: usize,    // head_dim_k * n_head_kv  (K elements per token)
392    pub kv_dim_v: usize,    // head_dim_v * n_head_kv  (V elements per token)
393    pub k_tok_bytes: usize, // (kv_dim_k/32)*34
394    pub v_tok_bytes: usize, // (kv_dim_v/32)*24
395    pub len: usize,
396    /// Step35 SWA physical-row state. `len` remains absolute; `None` keeps the original flat
397    /// `[0, max_ctx)` addressing contract.
398    pub ring: Option<KvRing>,
399    /// Device-resident mirror of `len` (CUDA-GRAPH-PLAN Phase 2). Holds the KV write SLOT for the
400    /// append-dc kernel (old len, before this step's append); after `inc_seqlen` it holds the new
401    /// len == t_kv for fa_decode_dc. Kept in lock-step with the host `len`. i32[1].
402    pub len_d: CudaSlice<i32>,
403    /// Device-resident mirror of `ring.base()` (physical row of logical row 0) for the WINDOWED
404    /// device-counter draft arm (`append_kv_quantized_dcw` / `fa_decode_dcw`): the kernels derive
405    /// the SWA view as {lstart = max(0, len - window); physical = row - base} entirely from
406    /// device state, so a captured draft chain replays with zero per-token node updates. Armed
407    /// only on ring-backed draft-scratch planes (step35); `None` keeps the plain `_dc` contract
408    /// (base 0) and costs nothing. The ONE writer is the rebase arm of `prepare_kv_append`
409    /// (rebases are host-side, outside any captured region); rewinds move `len`/`len_d` only,
410    /// never `base`, so no other site touches it. i32[1].
411    pub base_d: Option<CudaSlice<i32>>,
412}
413
414impl KvLayer {
415    pub fn physical_rows(
416        &self,
417        start: usize,
418        end: usize,
419    ) -> Result<std::ops::Range<usize>, String> {
420        match &self.ring {
421            Some(ring) => ring.physical_range(start, end),
422            None => Ok(start..end),
423        }
424    }
425}
426
427/// Per-linear-attn-layer fixed recurrent state.
428/// conv_state and ssm_state are BOTH kept RESIDENT on GPU — the conv ring assemble + roll runs
429/// on-device (conv_assemble_and_roll), so there is no per-step dtoh/htod for either.
430pub struct RecurLayer {
431    pub conv_state: CudaSlice<f32>, // GPU [conv_dim, d_conv-1] (channel c, tap j at c*pad + j)
432    pub ssm_state: CudaSlice<f32>,  // GPU [d_state, d_state, num_v] transposed M[col][i]
433    /// PERSISTENT second SSM-state buffer for the gdn-scan double buffer (DECODE DETERMINISM FIX).
434    /// gdn_scan needs DISTINCT in/out state buffers. The old eager path allocated a fresh
435    /// `state_scratch` via `e.uninit` every step and swapped its pointer into `ssm_state`; that
436    /// per-step alloc/free churned the stream-ordered async pool, and the freed prior `ssm_state`
437    /// block was recycled by the next step's scratch while a kernel referencing the swapped-in state
438    /// was still in flight — a use-after-reuse that produced RUN-TO-RUN nondeterministic decode
439    /// (two identical prompt primes diverged). We instead PING-PONG between two STABLE resident
440    /// buffers (no per-step alloc/free, no pool churn): step writes into the spare, then swaps the
441    /// two owned buffers in place. Stable pointers, identical math. Sized like `ssm_state`.
442    pub ssm_state_alt: CudaSlice<f32>,
443}
444
445pub struct ResidentTpKvCacheRank {
446    k: CudaSlice<u8>,
447    v: CudaSlice<u8>,
448    len_d: CudaSlice<i32>,
449    /// Physical row of LOGICAL row 0 after the last ring rebase (graph increment A: the
450    /// windowed device-counter fa derives its view as {lstart = max(0, len - window);
451    /// physical = lstart - base}). Host-written at rebase (rare) and at cache init; None
452    /// until the graph door first arms it.
453    base_d: Option<CudaSlice<i32>>,
454}
455
456impl ResidentTpKvCacheRank {
457    pub fn new(k: CudaSlice<u8>, v: CudaSlice<u8>, len_d: CudaSlice<i32>) -> Self {
458        Self {
459            k,
460            v,
461            len_d,
462            base_d: None,
463        }
464    }
465
466    pub fn base_d(&self) -> Option<&CudaSlice<i32>> {
467        self.base_d.as_ref()
468    }
469
470    pub fn base_d_mut(&mut self) -> Option<&mut CudaSlice<i32>> {
471        self.base_d.as_mut()
472    }
473
474    pub fn arm_base_d(&mut self, buf: CudaSlice<i32>) {
475        self.base_d = Some(buf);
476    }
477
478    pub fn k(&self) -> &CudaSlice<u8> {
479        &self.k
480    }
481
482    pub fn v(&self) -> &CudaSlice<u8> {
483        &self.v
484    }
485
486    pub fn len_d(&self) -> &CudaSlice<i32> {
487        &self.len_d
488    }
489
490    pub fn k_mut(&mut self) -> &mut CudaSlice<u8> {
491        &mut self.k
492    }
493
494    pub fn v_mut(&mut self) -> &mut CudaSlice<u8> {
495        &mut self.v
496    }
497
498    pub fn planes_mut(&mut self) -> (&mut CudaSlice<u8>, &mut CudaSlice<u8>) {
499        (&mut self.k, &mut self.v)
500    }
501
502    /// Split-borrow for the dcw append: both planes mutably plus the device counters shared.
503    #[allow(clippy::type_complexity)]
504    pub fn planes_and_counters_mut(
505        &mut self,
506    ) -> (
507        &mut CudaSlice<u8>,
508        &mut CudaSlice<u8>,
509        &CudaSlice<i32>,
510        Option<&CudaSlice<i32>>,
511    ) {
512        (&mut self.k, &mut self.v, &self.len_d, self.base_d.as_ref())
513    }
514
515    pub fn len_d_mut(&mut self) -> &mut CudaSlice<i32> {
516        &mut self.len_d
517    }
518}
519
520#[derive(Clone, Copy, Debug, PartialEq, Eq)]
521pub struct TpKvTransaction {
522    generation: u64,
523    base_len: usize,
524}
525
526impl TpKvTransaction {
527    pub fn generation(self) -> u64 {
528        self.generation
529    }
530
531    pub fn base_len(self) -> usize {
532        self.base_len
533    }
534}
535
536#[derive(Clone, Copy, Debug, PartialEq, Eq)]
537pub struct TpKvAppendPlan {
538    transaction: TpKvTransaction,
539    target: usize,
540    write_row: usize,
541    ring_append: Option<KvRingAppend>,
542}
543
544impl TpKvAppendPlan {
545    pub fn target(self) -> usize {
546        self.target
547    }
548
549    pub fn write_row(self) -> usize {
550        self.write_row
551    }
552
553    pub fn ring_append(self) -> Option<KvRingAppend> {
554        self.ring_append
555    }
556}
557
558#[derive(Debug, PartialEq, Eq)]
559pub struct TpKvGrowPlan {
560    rows: usize,
561    source_row: usize,
562    copy_rows: usize,
563    target_base: usize,
564    k_bytes: usize,
565    v_bytes: usize,
566    source_capacity: usize,
567    target_capacity: usize,
568    ring_window: Option<usize>,
569    target_physical_rows: usize,
570    kv_dim_k: usize,
571    kv_dim_v: usize,
572    k_tok_bytes: usize,
573    v_tok_bytes: usize,
574    ranks: usize,
575    next_generation: u64,
576}
577
578impl TpKvGrowPlan {
579    pub fn rows(&self) -> usize {
580        self.rows
581    }
582
583    pub fn source_row(&self) -> usize {
584        self.source_row
585    }
586
587    pub fn copy_rows(&self) -> usize {
588        self.copy_rows
589    }
590
591    pub fn k_bytes(&self) -> usize {
592        self.k_bytes
593    }
594
595    pub fn v_bytes(&self) -> usize {
596        self.v_bytes
597    }
598}
599
600#[derive(Clone, Debug, PartialEq, Eq)]
601struct TpKvTransactionState {
602    committed_len: usize,
603    staged_len: usize,
604    next_generation: u64,
605    active: Option<TpKvTransaction>,
606}
607
608impl TpKvTransactionState {
609    fn new() -> Self {
610        Self {
611            committed_len: 0,
612            staged_len: 0,
613            next_generation: 1,
614            active: None,
615        }
616    }
617
618    fn begin(&mut self) -> Result<TpKvTransaction, String> {
619        if let Some(active) = self.active {
620            return Err(format!(
621                "TP KV transaction generation {} is already active at base {}",
622                active.generation, active.base_len
623            ));
624        }
625        if self.staged_len != self.committed_len {
626            return Err(format!(
627                "TP KV cache is half-committed: staged {} != committed {}",
628                self.staged_len, self.committed_len
629            ));
630        }
631        let transaction = TpKvTransaction {
632            generation: self.next_generation,
633            base_len: self.committed_len,
634        };
635        self.next_generation = self
636            .next_generation
637            .checked_add(1)
638            .ok_or("TP KV transaction generation overflow")?;
639        self.active = Some(transaction);
640        Ok(transaction)
641    }
642
643    fn validate(&self, transaction: TpKvTransaction) -> Result<(), String> {
644        if self.active != Some(transaction) {
645            return Err(format!(
646                "stale TP KV transaction generation {} at base {}",
647                transaction.generation, transaction.base_len
648            ));
649        }
650        if transaction.base_len != self.committed_len {
651            return Err(format!(
652                "TP KV transaction base {} != committed length {}",
653                transaction.base_len, self.committed_len
654            ));
655        }
656        Ok(())
657    }
658
659    fn append_target(
660        &self,
661        transaction: TpKvTransaction,
662        rows: usize,
663        capacity: usize,
664    ) -> Result<usize, String> {
665        self.validate(transaction)?;
666        if rows == 0 {
667            return Err("TP KV append must contain at least one row".into());
668        }
669        let target = self
670            .staged_len
671            .checked_add(rows)
672            .ok_or("TP KV staged length overflow")?;
673        if target > capacity {
674            return Err(format!(
675                "TP KV append exceeds capacity: {target} > {capacity}"
676            ));
677        }
678        Ok(target)
679    }
680
681    fn publish_append(
682        &mut self,
683        transaction: TpKvTransaction,
684        target: usize,
685    ) -> Result<(), String> {
686        self.validate(transaction)?;
687        if target <= self.staged_len {
688            return Err(format!(
689                "TP KV append target {target} must exceed staged length {}",
690                self.staged_len
691            ));
692        }
693        self.staged_len = target;
694        Ok(())
695    }
696
697    fn commit_target(
698        &self,
699        transaction: TpKvTransaction,
700        accepted_rows: usize,
701    ) -> Result<usize, String> {
702        self.validate(transaction)?;
703        let staged_rows = self
704            .staged_len
705            .checked_sub(transaction.base_len)
706            .ok_or("TP KV staged length precedes its transaction base")?;
707        if accepted_rows > staged_rows {
708            return Err(format!(
709                "TP KV commit accepts {accepted_rows} rows from a {staged_rows}-row transaction"
710            ));
711        }
712        transaction
713            .base_len
714            .checked_add(accepted_rows)
715            .ok_or_else(|| "TP KV committed length overflow".to_string())
716    }
717
718    fn publish_finalize(
719        &mut self,
720        transaction: TpKvTransaction,
721        target: usize,
722    ) -> Result<(), String> {
723        self.validate(transaction)?;
724        if target < transaction.base_len || target > self.staged_len {
725            return Err(format!(
726                "TP KV finalize target {target} outside transaction range {}..={}",
727                transaction.base_len, self.staged_len
728            ));
729        }
730        self.committed_len = target;
731        self.staged_len = target;
732        self.active = None;
733        Ok(())
734    }
735
736    fn rewind(&mut self, target: usize, capacity: usize) -> Result<(), String> {
737        if target > capacity {
738            return Err(format!(
739                "TP KV rewind target {target} exceeds capacity {capacity}"
740            ));
741        }
742        self.committed_len = target;
743        self.staged_len = target;
744        self.active = None;
745        Ok(())
746    }
747}
748
749pub struct ResidentTpKvCache {
750    ranks: Vec<ResidentTpKvCacheRank>,
751    kv_dim_k: usize,
752    kv_dim_v: usize,
753    k_tok_bytes: usize,
754    v_tok_bytes: usize,
755    capacity: usize,
756    ring: Option<KvRing>,
757    state: TpKvTransactionState,
758}
759
760impl ResidentTpKvCache {
761    #[allow(clippy::too_many_arguments)]
762    pub fn new(
763        ranks: Vec<ResidentTpKvCacheRank>,
764        kv_dim_k: usize,
765        kv_dim_v: usize,
766        k_tok_bytes: usize,
767        v_tok_bytes: usize,
768        capacity: usize,
769    ) -> Self {
770        Self::new_inner(
771            ranks,
772            kv_dim_k,
773            kv_dim_v,
774            k_tok_bytes,
775            v_tok_bytes,
776            capacity,
777            None,
778        )
779    }
780
781    #[allow(clippy::too_many_arguments)]
782    pub fn new_swa(
783        ranks: Vec<ResidentTpKvCacheRank>,
784        kv_dim_k: usize,
785        kv_dim_v: usize,
786        k_tok_bytes: usize,
787        v_tok_bytes: usize,
788        capacity: usize,
789        window: usize,
790    ) -> Self {
791        Self::new_inner(
792            ranks,
793            kv_dim_k,
794            kv_dim_v,
795            k_tok_bytes,
796            v_tok_bytes,
797            capacity,
798            Some(KvRing::new(swa_ring_rows(window, capacity), window)),
799        )
800    }
801
802    #[allow(clippy::too_many_arguments)]
803    fn new_inner(
804        ranks: Vec<ResidentTpKvCacheRank>,
805        kv_dim_k: usize,
806        kv_dim_v: usize,
807        k_tok_bytes: usize,
808        v_tok_bytes: usize,
809        capacity: usize,
810        ring: Option<KvRing>,
811    ) -> Self {
812        Self {
813            ranks,
814            kv_dim_k,
815            kv_dim_v,
816            k_tok_bytes,
817            v_tok_bytes,
818            capacity,
819            ring,
820            state: TpKvTransactionState::new(),
821        }
822    }
823
824    pub fn begin_transaction(&mut self) -> Result<TpKvTransaction, String> {
825        self.state.begin()
826    }
827
828    pub fn committed_len(&self) -> usize {
829        self.state.committed_len
830    }
831
832    pub fn staged_len(&self) -> usize {
833        self.state.staged_len
834    }
835
836    pub fn capacity(&self) -> usize {
837        self.capacity
838    }
839
840    pub fn physical_capacity(&self) -> usize {
841        self.ring
842            .as_ref()
843            .map(KvRing::rows)
844            .unwrap_or(self.capacity)
845    }
846
847    pub fn ring_window(&self) -> Option<usize> {
848        self.ring.as_ref().map(KvRing::window)
849    }
850
851    pub fn ring_base(&self) -> Option<usize> {
852        self.ring.as_ref().map(KvRing::base)
853    }
854
855    pub fn physical_range(
856        &self,
857        start: usize,
858        end: usize,
859    ) -> Result<std::ops::Range<usize>, String> {
860        match &self.ring {
861            Some(ring) => ring.physical_range(start, end),
862            None => {
863                if end < start || end > self.capacity {
864                    return Err(format!(
865                        "TP KV linear view [{start},{end}) exceeds capacity {}",
866                        self.capacity
867                    ));
868                }
869                Ok(start..end)
870            }
871        }
872    }
873
874    pub fn can_rewind_to(&self, target: usize) -> bool {
875        target <= self.capacity
876            && self
877                .ring
878                .as_ref()
879                .is_none_or(|ring| ring.can_rewind_to(target))
880    }
881
882    pub fn kv_dim_k(&self) -> usize {
883        self.kv_dim_k
884    }
885
886    pub fn kv_dim_v(&self) -> usize {
887        self.kv_dim_v
888    }
889
890    pub fn k_tok_bytes(&self) -> usize {
891        self.k_tok_bytes
892    }
893
894    pub fn v_tok_bytes(&self) -> usize {
895        self.v_tok_bytes
896    }
897
898    pub fn ranks_len(&self) -> usize {
899        self.ranks.len()
900    }
901
902    pub fn rank(&self, rank: usize) -> Option<&ResidentTpKvCacheRank> {
903        self.ranks.get(rank)
904    }
905
906    pub fn rank_mut(&mut self, rank: usize) -> Option<&mut ResidentTpKvCacheRank> {
907        self.ranks.get_mut(rank)
908    }
909
910    pub fn ranks(&self) -> &[ResidentTpKvCacheRank] {
911        &self.ranks
912    }
913
914    pub fn ranks_mut(&mut self) -> &mut [ResidentTpKvCacheRank] {
915        &mut self.ranks
916    }
917
918    pub fn prepare_grow(
919        &self,
920        target_capacity: usize,
921        rows: usize,
922    ) -> Result<TpKvGrowPlan, String> {
923        if let Some(active) = self.state.active {
924            return Err(format!(
925                "TP KV grow refuses active transaction generation {} at base {}",
926                active.generation, active.base_len
927            ));
928        }
929        if self.state.staged_len != self.state.committed_len {
930            return Err(format!(
931                "TP KV grow requires quiescent state, got committed/staged={}/{}",
932                self.state.committed_len, self.state.staged_len
933            ));
934        }
935        if target_capacity <= self.capacity {
936            return Err(format!(
937                "TP KV grow target capacity {target_capacity} must exceed source capacity {}",
938                self.capacity
939            ));
940        }
941        if target_capacity > i32::MAX as usize {
942            return Err(format!(
943                "TP KV grow target capacity {target_capacity} exceeds i32 device mirrors"
944            ));
945        }
946        if rows > self.state.committed_len {
947            return Err(format!(
948                "TP KV grow rows {rows} exceed committed length {}",
949                self.state.committed_len
950            ));
951        }
952        let (source_row, copy_rows, target_base, ring_window, target_physical_rows) =
953            match &self.ring {
954                Some(ring) => {
955                    let raw = rows.saturating_sub(ring.window().saturating_sub(1));
956                    let target_base = raw & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
957                    let physical = ring.physical_range(target_base, rows)?;
958                    (
959                        physical.start,
960                        physical.len(),
961                        target_base,
962                        Some(ring.window()),
963                        swa_ring_rows(ring.window(), target_capacity),
964                    )
965                }
966                None => (0, rows, 0, None, target_capacity),
967            };
968        let k_bytes = copy_rows
969            .checked_mul(self.k_tok_bytes)
970            .ok_or("TP KV grow K byte extent overflow")?;
971        let v_bytes = copy_rows
972            .checked_mul(self.v_tok_bytes)
973            .ok_or("TP KV grow V byte extent overflow")?;
974        Ok(TpKvGrowPlan {
975            rows,
976            source_row,
977            copy_rows,
978            target_base,
979            k_bytes,
980            v_bytes,
981            source_capacity: self.capacity,
982            target_capacity,
983            ring_window,
984            target_physical_rows,
985            kv_dim_k: self.kv_dim_k,
986            kv_dim_v: self.kv_dim_v,
987            k_tok_bytes: self.k_tok_bytes,
988            v_tok_bytes: self.v_tok_bytes,
989            ranks: self.ranks.len(),
990            next_generation: self.state.next_generation,
991        })
992    }
993
994    pub fn publish_grow(&mut self, plan: TpKvGrowPlan) -> Result<(), String> {
995        if self.state != TpKvTransactionState::new() {
996            return Err(format!(
997                "TP KV grow target must be fresh, got committed/staged={}/{} active={}",
998                self.state.committed_len,
999                self.state.staged_len,
1000                self.state.active.is_some()
1001            ));
1002        }
1003        if self.capacity != plan.target_capacity
1004            || self.capacity <= plan.source_capacity
1005            || self.kv_dim_k != plan.kv_dim_k
1006            || self.kv_dim_v != plan.kv_dim_v
1007            || self.k_tok_bytes != plan.k_tok_bytes
1008            || self.v_tok_bytes != plan.v_tok_bytes
1009            || self.ranks.len() != plan.ranks
1010            || self.ring.as_ref().map(KvRing::window) != plan.ring_window
1011            || self.physical_capacity() != plan.target_physical_rows
1012        {
1013            return Err("TP KV grow target layout does not match its source plan".into());
1014        }
1015        if plan.rows > self.capacity {
1016            return Err(format!(
1017                "TP KV grow rows {} exceed target capacity {}",
1018                plan.rows, self.capacity
1019            ));
1020        }
1021        if let Some(ring) = self.ring.as_mut() {
1022            let mut target_ring = *ring;
1023            target_ring.apply_rebase(plan.target_base);
1024            if !target_ring.can_rewind_to(plan.rows) {
1025                return Err(format!(
1026                    "TP KV grow target ring base {} cannot expose committed length {}",
1027                    target_ring.base(),
1028                    plan.rows
1029                ));
1030            }
1031            *ring = target_ring;
1032        }
1033        self.state.committed_len = plan.rows;
1034        self.state.staged_len = plan.rows;
1035        self.state.next_generation = plan.next_generation;
1036        self.state.active = None;
1037        Ok(())
1038    }
1039
1040    pub fn prepare_append(
1041        &self,
1042        transaction: TpKvTransaction,
1043        rows: usize,
1044    ) -> Result<TpKvAppendPlan, String> {
1045        let target = self.state.append_target(transaction, rows, self.capacity)?;
1046        let ring_append = self
1047            .ring
1048            .as_ref()
1049            .map(|ring| {
1050                let staged_retain =
1051                    target.saturating_sub(ring.window()) & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
1052                let rollback_retain = transaction
1053                    .base_len
1054                    .saturating_sub(ring.window().saturating_sub(1))
1055                    & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
1056                ring.append_plan(
1057                    self.state.staged_len,
1058                    staged_retain.min(rollback_retain),
1059                    rows,
1060                )
1061            })
1062            .transpose()?;
1063        let write_row = match ring_append {
1064            Some(KvRingAppend::Contiguous { write_row })
1065            | Some(KvRingAppend::Rebase { write_row, .. }) => write_row,
1066            None => self.state.staged_len,
1067        };
1068        Ok(TpKvAppendPlan {
1069            transaction,
1070            target,
1071            write_row,
1072            ring_append,
1073        })
1074    }
1075
1076    /// Read-only peek at the NEXT append's ring plan: (write_row, would_rebase). The dcw
1077    /// (device-counter) append path uses it to route rebase tokens through the full host
1078    /// path — the in-kernel row (len - base) is only valid for contiguous appends.
1079    pub fn peek_append_ring(&self, rows: usize) -> Result<(usize, bool), String> {
1080        let target = self.state.staged_len + rows;
1081        let plan = self
1082            .ring
1083            .as_ref()
1084            .map(|ring| {
1085                let staged_retain =
1086                    target.saturating_sub(ring.window()) & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
1087                let rollback_retain = self
1088                    .state
1089                    .staged_len
1090                    .saturating_sub(ring.window().saturating_sub(1))
1091                    & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
1092                ring.append_plan(
1093                    self.state.staged_len,
1094                    staged_retain.min(rollback_retain),
1095                    rows,
1096                )
1097            })
1098            .transpose()?;
1099        Ok(match plan {
1100            Some(KvRingAppend::Contiguous { write_row }) => (write_row, false),
1101            Some(KvRingAppend::Rebase { write_row, .. }) => (write_row, true),
1102            None => (self.state.staged_len, false),
1103        })
1104    }
1105
1106    pub fn publish_append_rebase(&mut self, plan: TpKvAppendPlan) -> Result<(), String> {
1107        self.state.validate(plan.transaction)?;
1108        match (self.ring.as_mut(), plan.ring_append) {
1109            (
1110                Some(ring),
1111                Some(KvRingAppend::Rebase {
1112                    new_base,
1113                    keep_rows,
1114                    ..
1115                }),
1116            ) => {
1117                if keep_rows > ring.rows() {
1118                    return Err(format!(
1119                        "TP KV ring rebase keeps {keep_rows} rows in {} physical rows",
1120                        ring.rows()
1121                    ));
1122                }
1123                let mut target_ring = *ring;
1124                target_ring.apply_rebase(new_base);
1125                if !target_ring.can_rewind_to(plan.transaction.base_len) {
1126                    return Err(format!(
1127                        "TP KV ring rebase to {new_base} laps transaction base {}",
1128                        plan.transaction.base_len
1129                    ));
1130                }
1131                *ring = target_ring;
1132                Ok(())
1133            }
1134            (Some(_), Some(KvRingAppend::Contiguous { .. })) | (None, None) => Ok(()),
1135            _ => Err("TP KV append plan does not match cache ring layout".into()),
1136        }
1137    }
1138
1139    pub fn publish_append_plan(&mut self, plan: TpKvAppendPlan) -> Result<(), String> {
1140        if let Some(KvRingAppend::Rebase { new_base, .. }) = plan.ring_append {
1141            if self.ring.as_ref().map(KvRing::base) != Some(new_base) {
1142                return Err(format!(
1143                    "TP KV append rebase {new_base} was not published before its state"
1144                ));
1145            }
1146        }
1147        self.state.publish_append(plan.transaction, plan.target)
1148    }
1149
1150    pub fn publish_hydration(
1151        &mut self,
1152        logical_len: usize,
1153        resident_start: usize,
1154    ) -> Result<(), Box<dyn std::error::Error>> {
1155        if self.state != TpKvTransactionState::new() {
1156            return Err("TP KV hydration target must be fresh".into());
1157        }
1158        if resident_start > logical_len || logical_len > self.capacity {
1159            return Err(format!(
1160                "TP KV hydration range [{resident_start},{logical_len}) exceeds capacity {}",
1161                self.capacity
1162            )
1163            .into());
1164        }
1165        match self.ring.as_mut() {
1166            Some(ring) => {
1167                let rows = logical_len - resident_start;
1168                if rows > ring.rows() {
1169                    return Err(format!(
1170                        "TP KV hydration requires {rows} rows in a {}-row ring",
1171                        ring.rows()
1172                    )
1173                    .into());
1174                }
1175                let mut hydrated_ring = *ring;
1176                hydrated_ring.apply_rebase(resident_start);
1177                if !hydrated_ring.can_rewind_to(logical_len) {
1178                    return Err(format!(
1179                        "TP KV hydration base {resident_start} cannot expose logical length \
1180                         {logical_len}"
1181                    )
1182                    .into());
1183                }
1184                *ring = hydrated_ring;
1185            }
1186            None if resident_start != 0 => {
1187                return Err("linear TP KV hydration must start at absolute row zero".into());
1188            }
1189            None => {}
1190        }
1191        self.rewind_to(logical_len)
1192    }
1193
1194    pub fn append_target(
1195        &self,
1196        transaction: TpKvTransaction,
1197        rows: usize,
1198    ) -> Result<usize, String> {
1199        self.state.append_target(transaction, rows, self.capacity)
1200    }
1201
1202    pub fn publish_append(
1203        &mut self,
1204        transaction: TpKvTransaction,
1205        target: usize,
1206    ) -> Result<(), String> {
1207        self.state.publish_append(transaction, target)
1208    }
1209
1210    pub fn commit_target(
1211        &self,
1212        transaction: TpKvTransaction,
1213        accepted_rows: usize,
1214    ) -> Result<usize, String> {
1215        self.state.commit_target(transaction, accepted_rows)
1216    }
1217
1218    pub fn validate_transaction(&self, transaction: TpKvTransaction) -> Result<(), String> {
1219        self.state.validate(transaction)
1220    }
1221
1222    pub fn publish_finalize(
1223        &mut self,
1224        transaction: TpKvTransaction,
1225        target: usize,
1226    ) -> Result<(), String> {
1227        if !self.can_rewind_to(target) {
1228            return Err(format!(
1229                "TP KV finalize target {target} is outside the resident cache window/capacity"
1230            ));
1231        }
1232        self.state.publish_finalize(transaction, target)
1233    }
1234
1235    pub fn rewind_to(&mut self, target: usize) -> Result<(), Box<dyn std::error::Error>> {
1236        if !self.can_rewind_to(target) {
1237            return Err(format!(
1238                "TP KV rewind target {target} is outside the resident cache window/capacity"
1239            )
1240            .into());
1241        }
1242        let target_i32 =
1243            i32::try_from(target).map_err(|_| "TP KV length exceeds i32 device mirror")?;
1244        for rank in &mut self.ranks {
1245            let stream = rank.len_d.stream().clone();
1246            stream.memcpy_htod(&[target_i32], &mut rank.len_d)?;
1247        }
1248        self.state.rewind(target, self.capacity)?;
1249        Ok(())
1250    }
1251}
1252
1253pub struct Cache {
1254    pub kv: Vec<Option<KvLayer>>,
1255    pub recur: Vec<Option<RecurLayer>>,
1256    /// Optional per-layer tensor-parallel KV planes. The ordinary owning-stage cache remains
1257    /// allocated as the rollback oracle until the distributed serving path is fully qualified.
1258    pub tp_kv: Vec<Option<ResidentTpKvCache>>,
1259    pub pos: usize,
1260    pub max_ctx: usize,
1261    /// BATCHED-TICK increment 2 component 3 (lean logits, 2026-08-01): device-side park of
1262    /// this session's LAST logits row. Device-sampled rows in the batched serving tick skip
1263    /// the [n_vocab] logits D2H entirely; the tick instead dtod-copies the row here (device
1264    /// bandwidth, ~µs) so the ONE consumer that truly needs the final row — the KV-reuse
1265    /// pool's park-at-retire (an empty-suffix resume samples from parked last_logits) —
1266    /// can D2H it once at retire. Lazily allocated on the first lean tick; None on every
1267    /// non-lean path (zero cost). Travels with the Cache into the reuse pool.
1268    pub last_logits_dev: Option<CudaSlice<f32>>,
1269    /// DFlash tap sink (dflash lane, 2026-07-13): when armed, the gemma4 verify/prime
1270    /// trunks copy the residual stream AFTER each tapped layer into `buf` rows
1271    /// ([t, n_taps*hidden] row-major — the drafter fc input layout). None on every
1272    /// non-dflash path (zero cost).
1273    pub dflash_taps: Option<DflashTapSink>,
1274}
1275
1276/// The context-linear K/V layout for one full-attention layer. This is the single sizing source
1277/// used by both `Cache::new_inner` and `cache_bytes_per_token`: admission must never reimplement
1278/// Gemma's per-layer geometry or the active KV-format doors independently from the allocator.
1279#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1280enum FullAttentionClass {
1281    Ordinary,
1282    GemmaGlobal,
1283    GemmaWindowed,
1284}
1285
1286fn full_attention_class(plan: &ModelPlan, il: u32) -> FullAttentionClass {
1287    let layer = plan
1288        .layers
1289        .iter()
1290        .chain(plan.mtp_blocks.iter().map(|block| &block.layer))
1291        .find(|layer| layer.index == il)
1292        .unwrap_or_else(|| panic!("ModelPlan has no layer {il}"));
1293    if !matches!(layer.residual, ResidualTopology::Gemma { .. }) {
1294        return FullAttentionClass::Ordinary;
1295    }
1296    match layer.state {
1297        StatePlan::SlidingKvCache { .. } => FullAttentionClass::GemmaWindowed,
1298        StatePlan::KvCache { .. } => FullAttentionClass::GemmaGlobal,
1299        _ => panic!("Gemma layer {il} does not declare a KV-cache state"),
1300    }
1301}
1302
1303fn full_attention_kv_layout(
1304    cfg: &ModelConfig,
1305    plan: &ModelPlan,
1306    il: u32,
1307) -> (usize, usize, usize, usize) {
1308    debug_assert_eq!(cfg.layer_kind(il), LayerKind::FullAttention);
1309    let class = full_attention_class(plan, il);
1310    let n_head_kv = cfg.n_head_kv as usize;
1311    let (kv_dim_k, kv_dim_v) = match class {
1312        FullAttentionClass::GemmaGlobal | FullAttentionClass::GemmaWindowed => {
1313            let g = cfg
1314                .gemma4
1315                .as_ref()
1316                .expect("Gemma ModelPlan layer requires Gemma cache geometry");
1317            let hd = match class {
1318                FullAttentionClass::GemmaWindowed => g.key_length_swa,
1319                FullAttentionClass::GemmaGlobal => g.key_length_global,
1320                FullAttentionClass::Ordinary => unreachable!(),
1321            } as usize;
1322            // E4B ships a SCALAR head_count_kv (per-layer vec empty; scalar = 2 in
1323            // the gguf, landing in cfg.n_head_kv): kv_dim = hd * 2 for BOTH kinds —
1324            // swa 2x256 = 512, global 2x512 = 1024. The old fallback used
1325            // key_length_global (512) for both, which HALVED the global layers' K/V
1326            // (the attn writes wk.out_features = 1024 rows): every E4B global layer
1327            // stored/attended half its K/V and the batched append read row strides
1328            // wrong — THE cross-mode maxdiff-30 root (2026-07-12 bisect, il=5 slot-1
1329            // byte forensics). 26B/31B keep the per-layer vec.
1330            let d = match g.head_count_kv.get(il as usize) {
1331                Some(n) => hd * *n as usize,
1332                None => hd * n_head_kv,
1333            };
1334            (d, d)
1335        }
1336        FullAttentionClass::Ordinary => (
1337            cfg.head_dim_k as usize * n_head_kv,
1338            cfg.head_dim_v as usize * n_head_kv,
1339        ),
1340    };
1341    assert!(
1342        kv_dim_k % 32 == 0 && kv_dim_v % 32 == 0,
1343        "KVQUANT requires per-layer kv_dim_k%32==0 && kv_dim_v%32==0 \
1344         (layer {il}: k={kv_dim_k} v={kv_dim_v})"
1345    );
1346    let (kbb, vbb) = kv_blk_bytes();
1347    let g4_global_fp8 = gkv_on() && class == FullAttentionClass::GemmaGlobal;
1348    let g4_windowed_fp8 = wkv_on() && class == FullAttentionClass::GemmaWindowed;
1349    let qwen_fp8 = kv_fp8_on() && class == FullAttentionClass::Ordinary;
1350    let (kbb_l, vbb_l) = if g4_global_fp8 || g4_windowed_fp8 || qwen_fp8 {
1351        (32, 32)
1352    } else {
1353        (kbb, vbb)
1354    };
1355    (kv_dim_k, kv_dim_v, kbb_l, vbb_l)
1356}
1357
1358fn kv_plane_allocation_bytes(rows: usize, token_bytes: usize) -> usize {
1359    rows * token_bytes + 8
1360}
1361
1362/// Context-linear bytes allocated by one trunk cache token.
1363///
1364/// Fixed allocations (the 8-byte plane tail pads, `len_d`, recurrent state, and optional lazy
1365/// buffers) are deliberately excluded. Admission adds their measured high-water residual as a
1366/// request-independent activation term; multiplying this coefficient by the request's own
1367/// `ctx_cap` exactly mirrors the context-scaled allocations in `Cache::new_inner`.
1368pub fn cache_bytes_per_token(cfg: &ModelConfig) -> usize {
1369    cache_bytes_per_token_for_layers(cfg, 0, cfg.n_layer as usize)
1370}
1371
1372/// Context-linear cache bytes per token owned by layers in `[lo, hi)`. PP admission uses the
1373/// same layer ranges as `Cache::new_ppn`, so each device is charged for exactly the cache planes
1374/// it allocates rather than for the aggregate model geometry.
1375pub fn cache_bytes_per_token_for_layers(cfg: &ModelConfig, lo: usize, hi: usize) -> usize {
1376    let plan = ModelPlan::compile(cfg).expect("cache sizing requires a compilable ModelPlan");
1377    cache_bytes_per_token_for_plan(cfg, &plan, lo, hi)
1378}
1379
1380pub fn cache_bytes_per_token_for_plan(
1381    cfg: &ModelConfig,
1382    plan: &ModelPlan,
1383    lo: usize,
1384    hi: usize,
1385) -> usize {
1386    assert!(
1387        lo <= hi && hi <= cfg.n_layer as usize,
1388        "cache layer range out of bounds"
1389    );
1390    let shared = cfg.gemma4.as_ref().map(|g| g.shared_kv_layers).unwrap_or(0);
1391    (lo as u32..hi as u32)
1392        .filter(|&il| cfg.layer_kind(il) == LayerKind::FullAttention)
1393        .filter(|&il| shared == 0 || il < cfg.n_layer - shared)
1394        .map(|il| {
1395            let (kv_dim_k, kv_dim_v, kbb, vbb) = full_attention_kv_layout(cfg, plan, il);
1396            (kv_dim_k / 32) * kbb + (kv_dim_v / 32) * vbb
1397        })
1398        .sum()
1399}
1400
1401/// Portion of [`cache_bytes_per_token`] whose physical row count is capped by the Step35 SWA
1402/// ring. Zero with the flag off and for every non-Step35 architecture.
1403pub fn cache_ring_bytes_per_token(cfg: &ModelConfig) -> usize {
1404    cache_ring_bytes_per_token_for_layers(cfg, 0, cfg.n_layer as usize)
1405}
1406
1407/// Ring-capped portion of [`cache_bytes_per_token_for_layers`] for `[lo, hi)`.
1408pub fn cache_ring_bytes_per_token_for_layers(cfg: &ModelConfig, lo: usize, hi: usize) -> usize {
1409    assert!(
1410        lo <= hi && hi <= cfg.n_layer as usize,
1411        "cache layer range out of bounds"
1412    );
1413    let Ok(plan) = memra_gguf::model_plan::ModelPlan::compile(cfg) else {
1414        return 0;
1415    };
1416    cache_ring_bytes_per_token_for_plan(cfg, &plan, lo, hi)
1417}
1418
1419pub fn cache_ring_bytes_per_token_for_plan(
1420    cfg: &ModelConfig,
1421    plan: &ModelPlan,
1422    lo: usize,
1423    hi: usize,
1424) -> usize {
1425    let total = plan.layers.len() + plan.mtp_blocks.len();
1426    assert!(
1427        lo <= hi && hi <= total,
1428        "cache plan layer range out of bounds"
1429    );
1430    if !swa_ring_on() {
1431        return 0;
1432    }
1433    let shared = cfg.gemma4.as_ref().map(|g| g.shared_kv_layers).unwrap_or(0);
1434    plan.layers
1435        .iter()
1436        .chain(plan.mtp_blocks.iter().map(|block| &block.layer))
1437        .filter(|layer| (lo..hi).contains(&(layer.index as usize)))
1438        .filter(|layer| {
1439            matches!(
1440                layer.state,
1441                memra_gguf::model_plan::StatePlan::SlidingKvCache { .. }
1442            )
1443        })
1444        .filter(|layer| shared == 0 || layer.index < cfg.n_layer - shared)
1445        .map(|layer| {
1446            let (kv_dim_k, kv_dim_v, kbb, vbb) = full_attention_kv_layout(cfg, plan, layer.index);
1447            (kv_dim_k / 32) * kbb + (kv_dim_v / 32) * vbb
1448        })
1449        .sum()
1450}
1451
1452/// Physical row cap shared by the Step35 SWA trunk and MTP scratch; zero when no ring is active.
1453pub fn cache_ring_row_cap(cfg: &ModelConfig) -> usize {
1454    let Ok(plan) = memra_gguf::model_plan::ModelPlan::compile(cfg) else {
1455        return 0;
1456    };
1457    cache_ring_row_cap_for_plan(&plan)
1458}
1459
1460pub fn cache_ring_row_cap_for_plan(plan: &memra_gguf::model_plan::ModelPlan) -> usize {
1461    if !swa_ring_on() {
1462        return 0;
1463    }
1464    plan.layers
1465        .iter()
1466        .chain(plan.mtp_blocks.iter().map(|block| &block.layer))
1467        .filter_map(|layer| match layer.state {
1468            memra_gguf::model_plan::StatePlan::SlidingKvCache { window, .. } => {
1469                Some(window as usize)
1470            }
1471            _ => None,
1472        })
1473        .map(|window| swa_ring_rows(window, usize::MAX))
1474        .max()
1475        .unwrap_or(0)
1476}
1477
1478/// See [`Cache::dflash_taps`]. Armed per forward by the dflash round (t = that forward's
1479/// row count); the trunk writes tap slot s of row r at buf[r*n_taps*hidden + s*hidden ..].
1480pub struct DflashTapSink {
1481    pub layer_ids: Vec<usize>,
1482    pub buf: CudaSlice<f32>,
1483    pub hidden: usize,
1484    pub t: usize,
1485    /// Row offset for writers that walk the buffer in windows (the qwen chunked prime):
1486    /// tap rows land at [base..base+t_chunk). Whole-buffer writers leave it 0.
1487    pub base: usize,
1488}
1489
1490/// Snapshot of the dual cache taken BEFORE a spec-decode draft+verify round (MTP-PLAN §C/§D.4).
1491/// - Full-attn KV: only the per-layer `len` is recorded; rollback truncates (append-only,
1492///   position-addressed — no copy). C.1.
1493/// - Linear-attn conv/ssm: real device-to-device COPIES of the recurrent state, because those
1494///   buffers are mutated IN PLACE by the verify pass and have no position index to truncate. C.2.
1495///   (CudaSlice::clone is an Arc refcount, NOT a buffer copy — so we alloc fresh + memcpy_dtod.)
1496pub struct CacheSnapshot {
1497    pub kv_len: Vec<Option<usize>>, // per layer (Some for full-attn layers)
1498    pub tp_kv_len: Vec<Option<usize>>, // per layer (Some for TP full-attn layers)
1499    pub conv: Vec<Option<CudaSlice<f32>>>, // per layer (Some for linear-attn layers, D2D copy)
1500    pub ssm: Vec<Option<CudaSlice<f32>>>,
1501    pub pos: usize,
1502}
1503
1504impl Cache {
1505    /// Allocate GPU-resident caches sized by arch + max context.
1506    pub fn new(
1507        e: &impl KvDev,
1508        cfg: &ModelConfig,
1509        max_ctx: usize,
1510    ) -> Result<Self, Box<dyn std::error::Error>> {
1511        Self::new_inner(&|_| e, cfg, None, max_ctx)
1512    }
1513
1514    pub fn new_planned(
1515        e: &impl KvDev,
1516        cfg: &ModelConfig,
1517        plan: &memra_gguf::model_plan::ModelPlan,
1518        max_ctx: usize,
1519    ) -> Result<Self, Box<dyn std::error::Error>> {
1520        Self::new_inner(&|_| e, cfg, Some(plan), max_ctx)
1521    }
1522
1523    /// M1-PP2 increment 2 (stage-owned KV): layers [0, split) allocate through `dev0`,
1524    /// layers [split, n) through `dev1` — each pipeline stage's cache lives on the
1525    /// device that runs the stage. With dev0 == dev1 this is byte-for-byte `new`
1526    /// (the single-device plumbing gate). Sizing math is IDENTICAL either way.
1527    pub fn new_pp2(
1528        dev0: &dyn KvDev,
1529        dev1: &dyn KvDev,
1530        split: usize,
1531        cfg: &ModelConfig,
1532        max_ctx: usize,
1533    ) -> Result<Self, Box<dyn std::error::Error>> {
1534        Self::new_inner(
1535            &|il| if il < split { dev0 } else { dev1 },
1536            cfg,
1537            None,
1538            max_ctx,
1539        )
1540    }
1541
1542    /// M2 N-stage twin of `new_pp2`: `fence` is the stage map from `memra_engine::pp::
1543    /// pp_cuts` ([0, c1, .., n_trunk]); layer il allocates through the engine of the
1544    /// stage that runs it. Layers at/beyond the fence end (MTP/NextN blocks) allocate
1545    /// through the LAST stage. Sizing math is IDENTICAL to `new` — only the allocating
1546    /// device varies.
1547    pub fn new_ppn<'a>(
1548        devs: &[&'a dyn KvDev],
1549        fence: &[usize],
1550        cfg: &ModelConfig,
1551        max_ctx: usize,
1552    ) -> Result<Self, Box<dyn std::error::Error>> {
1553        assert_eq!(
1554            devs.len() + 1,
1555            fence.len(),
1556            "ppn cache: devs vs fence mismatch"
1557        );
1558        let pick = |il: usize| -> &dyn KvDev {
1559            let s = match fence[1..fence.len() - 1].binary_search(&il) {
1560                Ok(k) => k + 1,
1561                Err(k) => k,
1562            };
1563            devs[s.min(devs.len() - 1)]
1564        };
1565        Self::new_inner(&pick, cfg, None, max_ctx)
1566    }
1567
1568    pub fn new_ppn_planned<'a>(
1569        devs: &[&'a dyn KvDev],
1570        fence: &[usize],
1571        cfg: &ModelConfig,
1572        plan: &memra_gguf::model_plan::ModelPlan,
1573        max_ctx: usize,
1574    ) -> Result<Self, Box<dyn std::error::Error>> {
1575        assert_eq!(
1576            devs.len() + 1,
1577            fence.len(),
1578            "ppn cache: devs vs fence mismatch"
1579        );
1580        let pick = |il: usize| -> &dyn KvDev {
1581            let stage = match fence[1..fence.len() - 1].binary_search(&il) {
1582                Ok(index) => index + 1,
1583                Err(index) => index,
1584            };
1585            devs[stage.min(devs.len() - 1)]
1586        };
1587        Self::new_inner(&pick, cfg, Some(plan), max_ctx)
1588    }
1589
1590    /// Shared allocation walk: `pick(il)` supplies the device that OWNS layer il's
1591    /// cache state (always the same device outside the pp2 door).
1592    fn new_inner<'a>(
1593        pick: &dyn Fn(usize) -> &'a dyn KvDev,
1594        cfg: &ModelConfig,
1595        plan: Option<&memra_gguf::model_plan::ModelPlan>,
1596        max_ctx: usize,
1597    ) -> Result<Self, Box<dyn std::error::Error>> {
1598        let fallback_plan = if plan.is_none() {
1599            Some(ModelPlan::compile(cfg)?)
1600        } else {
1601            None
1602        };
1603        let plan = plan
1604            .or(fallback_plan.as_ref())
1605            .expect("cache allocation requires a ModelPlan");
1606        let n = cfg.n_layer as usize;
1607        let mut kv = Vec::with_capacity(n);
1608        let mut recur = Vec::with_capacity(n);
1609        let head_dim_k = cfg.head_dim_k as usize;
1610        let head_dim_v = cfg.head_dim_v as usize;
1611        assert!(
1612            head_dim_k % 32 == 0 && head_dim_v % 32 == 0,
1613            "KVQUANT requires head_dim_k%32==0 && head_dim_v%32==0 (got k={head_dim_k} v={head_dim_v})"
1614        );
1615        for il in 0..cfg.n_layer {
1616            // stage-owned allocation (pp2): the device that runs this layer allocates it.
1617            let e = pick(il as usize);
1618            let layer = plan
1619                .layers
1620                .iter()
1621                .chain(plan.mtp_blocks.iter().map(|block| &block.layer))
1622                .find(|layer| layer.index == il)
1623                .ok_or_else(|| format!("cache ModelPlan has no layer {il}"))?;
1624            // E4B KV-SHARING: the trailing shared_kv_layers have no k/v of their own — they
1625            // attend an earlier layer's cache (hybrid_forward resolves the target). No KvLayer
1626            // here: any accidental use is a loud unwrap at bring-up, and rewind/len loops
1627            // (iter_mut().flatten()) skip None naturally.
1628            let g4_shared = cfg.gemma4.as_ref().map(|g| g.shared_kv_layers).unwrap_or(0);
1629            if g4_shared > 0 && il >= cfg.n_layer - g4_shared {
1630                kv.push(None);
1631                recur.push(None);
1632                continue;
1633            }
1634            match layer.state {
1635                StatePlan::KvCache { .. } | StatePlan::SlidingKvCache { .. } => {
1636                    // Gemma per-layer geometry and every KV-format door are resolved by the same
1637                    // helper admission uses for its analytic byte coefficient.
1638                    let (kv_dim_k, kv_dim_v, kbb_l, vbb_l) =
1639                        full_attention_kv_layout(cfg, plan, il);
1640                    let k_tok_bytes = (kv_dim_k / 32) * kbb_l;
1641                    let v_tok_bytes = (kv_dim_v / 32) * vbb_l;
1642                    let planned_window = plan
1643                        .layers
1644                        .iter()
1645                        .chain(plan.mtp_blocks.iter().map(|block| &block.layer))
1646                        .find(|layer| layer.index == il)
1647                        .and_then(|layer| match layer.state {
1648                            StatePlan::SlidingKvCache { window, .. } => Some(window),
1649                            _ => None,
1650                        });
1651                    let ring = if swa_ring_on() {
1652                        planned_window.map(|window| {
1653                            let window = window as usize;
1654                            KvRing::new(swa_ring_rows(window, max_ctx), window)
1655                        })
1656                    } else {
1657                        None
1658                    };
1659                    let alloc_rows = ring.as_ref().map(KvRing::rows).unwrap_or(max_ctx);
1660                    kv.push(Some(KvLayer {
1661                        // +8B tail pad: the v4 stage's aligned funnelshift window reads up to
1662                        // 4B past the final block (PR #3's finding, adopted pad-style — the
1663                        // expert-dot precedent; zero hot-loop branches, values discarded).
1664                        k: e.alloc_u8(kv_plane_allocation_bytes(alloc_rows, k_tok_bytes))?,
1665                        v: e.alloc_u8(kv_plane_allocation_bytes(alloc_rows, v_tok_bytes))?,
1666                        kv_dim_k,
1667                        kv_dim_v,
1668                        k_tok_bytes,
1669                        v_tok_bytes,
1670                        len: 0,
1671                        ring,
1672                        len_d: e.htod_i32(&[0])?,
1673                        base_d: None,
1674                    }));
1675                    recur.push(None);
1676                }
1677                StatePlan::Recurrent {
1678                    conv_width,
1679                    conv_kernel,
1680                    state_width,
1681                } => {
1682                    kv.push(None);
1683                    recur.push(Some(RecurLayer {
1684                        conv_state: e.zeros(
1685                            conv_width as usize * (conv_kernel as usize).saturating_sub(1),
1686                        )?,
1687                        ssm_state: e.zeros(state_width as usize)?,
1688                        ssm_state_alt: e.zeros(state_width as usize)?,
1689                    }));
1690                }
1691                ref state => {
1692                    return Err(format!(
1693                        "native cache allocator has no implementation for layer {il} state {state:?}"
1694                    )
1695                    .into());
1696                }
1697            }
1698        }
1699        Ok(Cache {
1700            kv,
1701            recur,
1702            tp_kv: (0..n).map(|_| None).collect(),
1703            pos: 0,
1704            max_ctx,
1705            dflash_taps: None,
1706            last_logits_dev: None,
1707        })
1708    }
1709
1710    pub fn has_swa_ring(&self) -> bool {
1711        self.kv.iter().flatten().any(|layer| layer.ring.is_some())
1712            || self
1713                .tp_kv
1714                .iter()
1715                .flatten()
1716                .any(|layer| layer.ring_window().is_some())
1717    }
1718
1719    pub fn can_rollback(&self, snap: &CacheSnapshot, accept_len: usize) -> bool {
1720        let local = self
1721            .kv
1722            .iter()
1723            .zip(&snap.kv_len)
1724            .all(|(layer, saved)| match (layer, saved) {
1725                (Some(layer), Some(saved)) => layer
1726                    .ring
1727                    .as_ref()
1728                    .is_none_or(|ring| ring.can_rewind_to(saved + accept_len)),
1729                _ => true,
1730            });
1731        let tensor = self
1732            .tp_kv
1733            .iter()
1734            .zip(&snap.tp_kv_len)
1735            .all(|(layer, saved)| match (layer, saved) {
1736                (Some(layer), Some(saved)) => saved
1737                    .checked_add(accept_len)
1738                    .is_some_and(|target| layer.can_rewind_to(target)),
1739                _ => true,
1740            });
1741        local && tensor
1742    }
1743
1744    /// Snapshot the dual cache before a spec-decode draft+verify round (MTP-PLAN §C/§D.4).
1745    /// Records each full-attn `len` (cheap) and makes a REAL device copy of each linear-attn
1746    /// conv_state/ssm_state (a fresh alloc + memcpy_dtod — NOT an Arc clone).
1747    pub fn snapshot(&self, e: &impl KvDev) -> Result<CacheSnapshot, Box<dyn std::error::Error>> {
1748        let n = self.kv.len();
1749        let mut kv_len = Vec::with_capacity(n);
1750        let mut tp_kv_len = Vec::with_capacity(n);
1751        let mut conv = Vec::with_capacity(n);
1752        let mut ssm = Vec::with_capacity(n);
1753        for il in 0..n {
1754            match &self.kv[il] {
1755                Some(kvl) => kv_len.push(Some(kvl.len)),
1756                None => kv_len.push(None),
1757            }
1758            tp_kv_len.push(
1759                self.tp_kv[il]
1760                    .as_ref()
1761                    .map(ResidentTpKvCache::committed_len),
1762            );
1763            match &self.recur[il] {
1764                Some(rl) => {
1765                    conv.push(Some(e.clone_dtod(&rl.conv_state)?));
1766                    ssm.push(Some(e.clone_dtod(&rl.ssm_state)?));
1767                }
1768                None => {
1769                    conv.push(None);
1770                    ssm.push(None);
1771                }
1772            }
1773        }
1774        Ok(CacheSnapshot {
1775            kv_len,
1776            tp_kv_len,
1777            conv,
1778            ssm,
1779            pos: self.pos,
1780        })
1781    }
1782
1783    /// PERSISTENT-BUFFER snapshot (spec-decode hot loop): refresh `snap` IN PLACE — same values as
1784    /// `snapshot()` but the conv/ssm device buffers are reused across rounds (D2D copy-into, ZERO
1785    /// allocations vs 2 fresh clones per linear layer per round). `snap` must come from a prior
1786    /// `snapshot()` of THIS cache (same layer shapes).
1787    pub fn snapshot_into(
1788        &self,
1789        e: &impl KvDev,
1790        snap: &mut CacheSnapshot,
1791    ) -> Result<(), Box<dyn std::error::Error>> {
1792        let n = self.kv.len();
1793        for il in 0..n {
1794            snap.kv_len[il] = self.kv[il].as_ref().map(|kvl| kvl.len);
1795            snap.tp_kv_len[il] = self.tp_kv[il]
1796                .as_ref()
1797                .map(ResidentTpKvCache::committed_len);
1798            if let Some(rl) = &self.recur[il] {
1799                let dc = snap.conv[il]
1800                    .as_mut()
1801                    .expect("snapshot_into: shape mismatch (conv)");
1802                let ds = snap.ssm[il]
1803                    .as_mut()
1804                    .expect("snapshot_into: shape mismatch (ssm)");
1805                let (cn, sn) = (rl.conv_state.len(), rl.ssm_state.len());
1806                e.copy_into(dc, 0, &rl.conv_state, cn)?;
1807                e.copy_into(ds, 0, &rl.ssm_state, sn)?;
1808            }
1809        }
1810        snap.pos = self.pos;
1811        Ok(())
1812    }
1813
1814    /// Roll the cache back to exactly `snap.pos + accept_len` committed tokens (MTP-PLAN §C).
1815    /// - Full-attn KV (C.1): set len = snapshot_len + accept_len (truncate, no copy).
1816    /// - Linear-attn (C.2): RESTORE the snapshot conv/ssm (real D2D copy back into the resident
1817    ///   buffers). The caller must then REPLAY the `accept_len` committed tokens through the full
1818    ///   T=1 decode path to rebuild the recurrent state for those positions. We restore (not
1819    ///   replay here) because replay needs the model; this only resets state to the pre-round value.
1820    /// `cache.pos` is set to `snap.pos` so the caller's replay advances it back to the commit point.
1821    pub fn rollback(
1822        &mut self,
1823        e: &impl KvDev,
1824        snap: &CacheSnapshot,
1825        accept_len: usize,
1826    ) -> Result<(), Box<dyn std::error::Error>> {
1827        if !self.can_rollback(snap, accept_len) {
1828            return Err(
1829                "SWA ring rewind checkpoint has been lapped; full re-prime required".into(),
1830            );
1831        }
1832        for il in 0..self.kv.len() {
1833            if let (Some(kvl), Some(saved)) = (self.kv[il].as_mut(), snap.kv_len[il]) {
1834                kvl.len = saved + accept_len;
1835                // keep the device mirror in lock-step (CUDA-GRAPH-PLAN Phase 2). Set IN PLACE
1836                // (stable pointer): a fresh htod_i32 would reallocate len_d, but its old pointer is
1837                // baked into the captured decode graph's append/inc/fa_decode kernels — replacing it
1838                // strands the graph on a freed buffer (stale-pointer hazard). memcpy_htod in place.
1839                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
1840            }
1841            if let (Some(kvl), Some(saved)) = (self.tp_kv[il].as_mut(), snap.tp_kv_len[il]) {
1842                kvl.rewind_to(saved + accept_len)?;
1843            }
1844            if let Some(rl) = self.recur[il].as_mut() {
1845                if let Some(c) = &snap.conv[il] {
1846                    e.copy_into(&mut rl.conv_state, 0, c, c.len())?;
1847                }
1848                if let Some(s) = &snap.ssm[il] {
1849                    e.copy_into(&mut rl.ssm_state, 0, s, s.len())?;
1850                }
1851            }
1852        }
1853        self.pos = snap.pos;
1854        Ok(())
1855    }
1856}
1857
1858#[cfg(test)]
1859mod tp_transaction_tests {
1860    use super::{
1861        Cache, KvRingAppend, ResidentTpKvCache, TpKvTransactionState, tp_kv_rank_allocation_shape,
1862    };
1863
1864    fn empty_tp_cache(capacity: usize) -> ResidentTpKvCache {
1865        ResidentTpKvCache::new(Vec::new(), 128, 128, 136, 96, capacity)
1866    }
1867
1868    #[test]
1869    fn step_tp8_rank_allocation_matches_the_official_kv_geometry() {
1870        let shape = tp_kv_rank_allocation_shape(8 * 128, 8 * 128, 8).unwrap();
1871        assert_eq!((shape.kv_dim_k, shape.kv_dim_v), (128, 128));
1872        assert_eq!((shape.k_token_bytes, shape.v_token_bytes), (136, 96));
1873        assert_eq!(shape.bytes_per_token(), 232);
1874        assert_eq!(shape.fixed_bytes, 20);
1875        assert_eq!(shape.allocation_bytes(262_144), 232 * 262_144 + 20);
1876    }
1877
1878    #[test]
1879    fn tp_rank_allocation_refuses_non_divisible_and_non_block_aligned_shards() {
1880        assert!(tp_kv_rank_allocation_shape(1024, 1024, 3).is_err());
1881        assert!(tp_kv_rank_allocation_shape(1024, 1024, 64).is_err());
1882        assert!(tp_kv_rank_allocation_shape(0, 1024, 8).is_err());
1883    }
1884
1885    #[test]
1886    fn partial_commit_publishes_only_the_accepted_prefix() {
1887        let mut state = TpKvTransactionState::new();
1888        let transaction = state.begin().unwrap();
1889        let staged = state.append_target(transaction, 3, 8).unwrap();
1890        state.publish_append(transaction, staged).unwrap();
1891        assert_eq!(state.committed_len, 0);
1892        assert_eq!(state.staged_len, 3);
1893
1894        let committed = state.commit_target(transaction, 2).unwrap();
1895        state.publish_finalize(transaction, committed).unwrap();
1896        assert_eq!(state.committed_len, 2);
1897        assert_eq!(state.staged_len, 2);
1898        assert!(state.active.is_none());
1899        assert!(state.validate(transaction).is_err());
1900    }
1901
1902    #[test]
1903    fn rollback_restores_the_committed_boundary() {
1904        let mut state = TpKvTransactionState::new();
1905        let first = state.begin().unwrap();
1906        let staged = state.append_target(first, 1, 8).unwrap();
1907        state.publish_append(first, staged).unwrap();
1908        let committed = state.commit_target(first, 1).unwrap();
1909        state.publish_finalize(first, committed).unwrap();
1910
1911        let speculative = state.begin().unwrap();
1912        let staged = state.append_target(speculative, 2, 8).unwrap();
1913        state.publish_append(speculative, staged).unwrap();
1914        assert_eq!(state.committed_len, 1);
1915        assert_eq!(state.staged_len, 3);
1916        state
1917            .publish_finalize(speculative, speculative.base_len)
1918            .unwrap();
1919        assert_eq!(state.committed_len, 1);
1920        assert_eq!(state.staged_len, 1);
1921        assert!(state.validate(speculative).is_err());
1922    }
1923
1924    #[test]
1925    fn rejects_nested_stale_and_out_of_range_actions() {
1926        let mut state = TpKvTransactionState::new();
1927        let transaction = state.begin().unwrap();
1928        assert!(state.begin().is_err());
1929        assert!(state.append_target(transaction, 0, 2).is_err());
1930        assert!(state.append_target(transaction, 3, 2).is_err());
1931        let staged = state.append_target(transaction, 2, 2).unwrap();
1932        state.publish_append(transaction, staged).unwrap();
1933        assert!(state.commit_target(transaction, 3).is_err());
1934        state.publish_finalize(transaction, 0).unwrap();
1935        assert!(state.publish_append(transaction, 1).is_err());
1936    }
1937
1938    #[test]
1939    fn rewind_resets_visibility_and_invalidates_an_active_transaction() {
1940        let mut state = TpKvTransactionState::new();
1941        let transaction = state.begin().unwrap();
1942        let staged = state.append_target(transaction, 3, 8).unwrap();
1943        state.publish_append(transaction, staged).unwrap();
1944        state.rewind(1, 8).unwrap();
1945        assert_eq!(state.committed_len, 1);
1946        assert_eq!(state.staged_len, 1);
1947        assert!(state.active.is_none());
1948        assert!(state.validate(transaction).is_err());
1949        assert!(state.rewind(9, 8).is_err());
1950    }
1951
1952    #[test]
1953    fn grow_preserves_generation_and_publishes_only_the_checkpoint_prefix() {
1954        let mut source = empty_tp_cache(8);
1955        let first = source.begin_transaction().unwrap();
1956        let staged = source.append_target(first, 5).unwrap();
1957        source.publish_append(first, staged).unwrap();
1958        let committed = source.commit_target(first, 5).unwrap();
1959        source.publish_finalize(first, committed).unwrap();
1960
1961        let rolled_back = source.begin_transaction().unwrap();
1962        source
1963            .publish_finalize(rolled_back, rolled_back.base_len())
1964            .unwrap();
1965        let plan = source.prepare_grow(16, 3).unwrap();
1966        assert_eq!(plan.rows(), 3);
1967        assert_eq!(plan.k_bytes(), 3 * 136);
1968        assert_eq!(plan.v_bytes(), 3 * 96);
1969
1970        let mut target = empty_tp_cache(16);
1971        target.publish_grow(plan).unwrap();
1972        assert_eq!(target.committed_len(), 3);
1973        assert_eq!(target.staged_len(), 3);
1974        assert_eq!(target.capacity(), 16);
1975        let next = target.begin_transaction().unwrap();
1976        assert_eq!(next.generation(), rolled_back.generation() + 1);
1977        assert_eq!(next.base_len(), 3);
1978    }
1979
1980    #[test]
1981    fn grow_refuses_active_source_and_invalid_target_state_or_layout() {
1982        let mut active = empty_tp_cache(8);
1983        active.begin_transaction().unwrap();
1984        assert!(active.prepare_grow(16, 0).is_err());
1985
1986        let mut source = empty_tp_cache(8);
1987        source.rewind_to(5).unwrap();
1988        assert!(source.prepare_grow(8, 5).is_err());
1989        assert!(source.prepare_grow(16, 6).is_err());
1990        let plan = source.prepare_grow(16, 4).unwrap();
1991
1992        let mut wrong_layout = ResidentTpKvCache::new(Vec::new(), 128, 128, 144, 96, 16);
1993        assert!(wrong_layout.publish_grow(plan).is_err());
1994
1995        let plan = source.prepare_grow(16, 4).unwrap();
1996        let mut dirty_target = empty_tp_cache(16);
1997        dirty_target.rewind_to(1).unwrap();
1998        assert!(dirty_target.publish_grow(plan).is_err());
1999    }
2000
2001    #[test]
2002    fn swa_transaction_rebase_preserves_the_rollback_window() {
2003        let mut cache = ResidentTpKvCache::new_swa(Vec::new(), 128, 128, 136, 96, 10_000, 32);
2004        assert_eq!(cache.physical_capacity(), 32 + 4096 + 512 + 31);
2005        // 8250 -> 8762: the extra alignment block moved the wrap point, and at 8250 this append is
2006        // now Contiguous — the test would keep passing while no longer exercising the rebase
2007        // it is named for. Every offset below moves by the same 32 rows; intent unchanged.
2008        cache.publish_hydration(8762, 4096).unwrap();
2009        assert_eq!(cache.ring_base(), Some(4096));
2010
2011        let transaction = cache.begin_transaction().unwrap();
2012        let plan = cache.prepare_append(transaction, 10).unwrap();
2013        assert_eq!(plan.target(), 8772);
2014        assert_eq!(plan.write_row(), 58);
2015        assert_eq!(
2016            plan.ring_append(),
2017            Some(KvRingAppend::Rebase {
2018                src_row: 4608,
2019                keep_rows: 58,
2020                new_base: 8704,
2021                write_row: 58,
2022            })
2023        );
2024        cache.publish_append_rebase(plan).unwrap();
2025        cache.publish_append_plan(plan).unwrap();
2026        assert_eq!(cache.ring_base(), Some(8704));
2027        assert_eq!(cache.physical_range(8740, 8772).unwrap(), 36..68);
2028
2029        let rollback = cache.commit_target(transaction, 0).unwrap();
2030        cache.publish_finalize(transaction, rollback).unwrap();
2031        assert_eq!((cache.committed_len(), cache.staged_len()), (8762, 8762));
2032        assert!(cache.rewind_to(8200).is_err());
2033    }
2034
2035    #[test]
2036    fn swa_grow_normalizes_only_the_live_prefix_and_preserves_generation() {
2037        let mut source = ResidentTpKvCache::new_swa(Vec::new(), 128, 128, 136, 96, 10_000, 32);
2038        source.publish_hydration(8250, 4096).unwrap();
2039        let transaction = source.begin_transaction().unwrap();
2040        source
2041            .publish_finalize(transaction, transaction.base_len())
2042            .unwrap();
2043
2044        let plan = source.prepare_grow(20_000, 8250).unwrap();
2045        assert_eq!(plan.source_row(), 4096);
2046        assert_eq!(plan.copy_rows(), 58);
2047        assert_eq!(plan.k_bytes(), 58 * 136);
2048        assert_eq!(plan.v_bytes(), 58 * 96);
2049
2050        let mut target = ResidentTpKvCache::new_swa(Vec::new(), 128, 128, 136, 96, 20_000, 32);
2051        target.publish_grow(plan).unwrap();
2052        assert_eq!(target.ring_base(), Some(8192));
2053        assert_eq!((target.committed_len(), target.staged_len()), (8250, 8250));
2054        assert_eq!(target.physical_range(8192, 8250).unwrap(), 0..58);
2055        let next = target.begin_transaction().unwrap();
2056        assert_eq!(next.generation(), transaction.generation() + 1);
2057    }
2058
2059    #[test]
2060    fn cache_reports_a_materialized_distributed_swa_ring() {
2061        let mut cache = Cache {
2062            kv: Vec::new(),
2063            recur: Vec::new(),
2064            tp_kv: vec![None],
2065            pos: 0,
2066            max_ctx: 10_000,
2067            dflash_taps: None,
2068            last_logits_dev: None,
2069        };
2070        assert!(!cache.has_swa_ring());
2071        cache.tp_kv[0] = Some(ResidentTpKvCache::new_swa(
2072            Vec::new(),
2073            128,
2074            128,
2075            136,
2076            96,
2077            10_000,
2078            512,
2079        ));
2080        assert!(cache.has_swa_ring());
2081    }
2082}
2083
2084#[cfg(test)]
2085mod swa_ring_tests {
2086    use super::{
2087        KvRing, KvRingAppend, PRIME_CHUNK_MAX_TOKENS, SWA_REWIND_SLACK_ROWS,
2088        SWA_VIEW_ALIGNMENT_ROWS, kv_plane_allocation_bytes, swa_retain_from, swa_ring_rows,
2089    };
2090
2091    #[test]
2092    fn allocation_rows_cover_window_max_prime_and_alignment_slack() {
2093        assert_eq!(swa_ring_rows(512, 262_144), 512 + 4096 + 512 + 31);
2094        assert_eq!(swa_ring_rows(512, 4096), 4096);
2095        assert_eq!(
2096            kv_plane_allocation_bytes(5151, 1088),
2097            5151 * 1088 + 8,
2098            "the Step35 session plane allocates ring rows plus the existing tail pad",
2099        );
2100    }
2101
2102    /// REGRESSION, the SWA-ring MTP lap (2026-08-28) — BOTH steps, which took three attempts to
2103    /// separate on hardware.
2104    ///
2105    /// Step 1, the REWIND. A rebase that retains exactly the window parks `base` at the newest
2106    /// legal value, so the next backward rewind — even by one token — floors an alignment block
2107    /// under it and is refused:
2108    ///   rewind_to=4638 window=512 base=4128 rows=4639 needed_view_start=4096 < base
2109    ///
2110    /// Step 2, the RE-APPEND, which a slack-only fix broke. After a legal rewind `first_row` moves
2111    /// back while `base` does not, so an unclamped ideal retain falls under `base` and the append
2112    /// itself is refused: "SWA ring lapped required rows (base 4128, retain 4096, len 4669)".
2113    /// Slack is something the ring GRANTS when it can, never something a caller may demand.
2114    #[test]
2115    fn retain_grants_rewind_slack_but_never_asks_below_base() {
2116        const WINDOW: usize = 512;
2117        let rows = swa_ring_rows(WINDOW, 262_144);
2118        let len = rows;
2119        let aligned = |pos: usize| (pos - (WINDOW - 1)) & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
2120
2121        // step 1 — from base 0 the retain sits below the aligned window start, so a rewind of up
2122        // to a full alignment block survives the rebase.
2123        let retain = swa_retain_from(len, WINDOW, 0);
2124        assert!(retain <= aligned(len) - SWA_REWIND_SLACK_ROWS);
2125        let mut ring = KvRing::new(rows, WINDOW);
2126        ring.apply_rebase(retain);
2127        assert!(
2128            ring.can_rewind_to(len - 1),
2129            "a one-token rewind must survive the rebase"
2130        );
2131        assert!(ring.can_rewind_to(len - SWA_REWIND_SLACK_ROWS));
2132
2133        // ...and a full prime chunk still fits at that retention, which is why the ring grew.
2134        assert!(len - retain + PRIME_CHUNK_MAX_TOKENS <= rows);
2135
2136        // the headroom is REAL, not clamped away: every rewind within it is legal from a base
2137        // the ring was actually sized to keep. This is what the 32-row version could not do —
2138        // it clamped instead, leaving the window pointing below resident rows (all-NaN logits).
2139        for depth in [1usize, 32, 256, SWA_REWIND_SLACK_ROWS] {
2140            assert!(
2141                ring.can_rewind_to(len - depth),
2142                "a {depth}-row rewind must be resident, not clamped away",
2143            );
2144        }
2145
2146        // step 2 — the property that actually keeps this safe is NOT `retain >= base`, it is that
2147        // the attention WINDOW is fully resident: window_start >= base. The clamp to `base` is
2148        // correct exactly while that holds, and v3's NaN came from clamping with only 32 rows of
2149        // headroom, where a deeper rewind clamped into a window that ran below resident rows.
2150        // With the ring sized for SWA_REWIND_SLACK_ROWS, every rewind inside the headroom keeps a
2151        // complete window — so the clamp is safe by construction rather than by luck.
2152        let base = ring.base();
2153        for depth in [1usize, 32, 256, SWA_REWIND_SLACK_ROWS] {
2154            let window_start = (len - depth - (WINDOW - 1)) & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
2155            assert!(
2156                window_start >= base,
2157                "after a {depth}-row rewind the window starts at {window_start}, below base \
2158                 {base} — clamping here would serve rows the ring no longer holds (the pos-8661 \
2159                 all-NaN case)",
2160            );
2161            assert!(swa_retain_from(len - depth, WINDOW, base) >= base);
2162        }
2163
2164        // and one row past the headroom the window DOES run below base — the case that must stay
2165        // refused rather than clamped, which is what can_rewind_to enforces.
2166        let past = len - (SWA_REWIND_SLACK_ROWS + WINDOW);
2167        let past_start = (past - (WINDOW - 1)) & !(SWA_VIEW_ALIGNMENT_ROWS - 1);
2168        assert!(
2169            past_start < base,
2170            "beyond the headroom the window must fall below base"
2171        );
2172        assert!(
2173            !ring.can_rewind_to(past),
2174            "and can_rewind_to must refuse it"
2175        );
2176    }
2177
2178    #[test]
2179    fn ring_matches_flat_bytes_before_wrap() {
2180        let ring = KvRing::new(swa_ring_rows(512, 262_144), 512);
2181        let flat: Vec<u32> = (0..1024).collect();
2182        let mut physical = vec![u32::MAX; ring.rows()];
2183        let KvRingAppend::Contiguous { write_row } = ring.append_plan(0, 0, flat.len()).unwrap()
2184        else {
2185            panic!("first append unexpectedly wrapped")
2186        };
2187        physical[write_row..write_row + flat.len()].copy_from_slice(&flat);
2188        let view = ring.physical_range(0, flat.len()).unwrap();
2189        assert_eq!(&physical[view], flat.as_slice());
2190    }
2191
2192    #[test]
2193    fn wrap_rebases_the_exact_aligned_prime_view() {
2194        let mut ring = KvRing::new(swa_ring_rows(512, 262_144), 512);
2195        let flat: Vec<u32> = (0..8192).collect();
2196        let mut physical = vec![u32::MAX; ring.rows()];
2197        let KvRingAppend::Contiguous { write_row } = ring.append_plan(0, 0, 4096).unwrap() else {
2198            panic!("first prime chunk unexpectedly wrapped")
2199        };
2200        physical[write_row..write_row + 4096].copy_from_slice(&flat[..4096]);
2201
2202        let off = (4096usize - (512 - 1)) & !31usize;
2203        let KvRingAppend::Rebase {
2204            src_row,
2205            keep_rows,
2206            new_base,
2207            write_row,
2208        } = ring.append_plan(4096, off, 4096).unwrap()
2209        else {
2210            panic!("second prime chunk did not wrap")
2211        };
2212        let retained = physical[src_row..src_row + keep_rows].to_vec();
2213        physical[..keep_rows].copy_from_slice(&retained);
2214        ring.apply_rebase(new_base);
2215        physical[write_row..write_row + 4096].copy_from_slice(&flat[4096..8192]);
2216
2217        let view = ring.physical_range(off, 8192).unwrap();
2218        assert_eq!(&physical[view], &flat[off..8192]);
2219        assert_eq!(ring.base(), off);
2220    }
2221
2222    #[test]
2223    fn rewind_declines_once_the_required_window_was_lapped() {
2224        let mut ring = KvRing::new(swa_ring_rows(512, 262_144), 512);
2225        let KvRingAppend::Rebase { new_base, .. } = ring.append_plan(4096, 3584, 4096).unwrap()
2226        else {
2227            panic!("expected wrap")
2228        };
2229        ring.apply_rebase(new_base);
2230        assert!(ring.can_rewind_to(4095));
2231        assert!(!ring.can_rewind_to(4094));
2232        assert!(!ring.can_rewind_to(0));
2233    }
2234
2235    /// The 2026-08-29 warm-turn-at-40k panic: a checkpoint on a LAPPED ring records an absolute
2236    /// `len` far past the physical rows, and a flat `len`-row restore is an out-of-bounds device
2237    /// slice. The plan must hand back only the aligned live window plus the base to rebase a
2238    /// fresh target to — and refuse once the source ring no longer holds that window.
2239    #[test]
2240    fn restore_plan_copies_the_window_not_the_absolute_length() {
2241        let mut ring = KvRing::new(swa_ring_rows(512, 262_144), 512);
2242        // Before any wrap: the plan is exactly the flat prefix.
2243        let (base, phys) = ring.restore_plan(400).unwrap();
2244        assert_eq!((base, phys), (0, 0..400));
2245
2246        // Lap the ring far past its physical capacity (a 40k-token session), the way a real
2247        // prime does: 4096-row chunks, rebasing whenever the tail would wrap.
2248        let mut live = 0usize;
2249        while live < 40_960 {
2250            let retain = swa_retain_from(live, 512, ring.base());
2251            if let KvRingAppend::Rebase { new_base, .. } =
2252                ring.append_plan(live, retain, 4096).unwrap()
2253            {
2254                ring.apply_rebase(new_base);
2255            }
2256            live += 4096;
2257        }
2258        assert!(ring.base() > 0, "a 40k walk must have lapped the ring");
2259        let (base, phys) = ring.restore_plan(live).unwrap();
2260        assert_eq!(base, (live - (512 - 1)) & !31usize);
2261        assert!(
2262            base >= ring.base(),
2263            "the plan must stay above the ring floor"
2264        );
2265        assert_eq!(phys.len(), live - base);
2266        assert!(
2267            phys.end <= ring.rows(),
2268            "the copy must fit the physical buffer ({} rows), got {:?}",
2269            ring.rows(),
2270            phys
2271        );
2272
2273        // A checkpoint from before the rebase is gone: refuse, never slice.
2274        assert!(ring.restore_plan(400).is_err());
2275    }
2276}