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