Skip to main content

memra_engine/
decode.rs

1//! Incremental decode (T=1) with the dual cache + greedy generation loop. Serves end-to-end.
2//! Reuses the validated kernels; threads KV (full-attn) and conv/SSM state (linear-attn) across steps.
3
4use crate::Engine;
5use crate::cache::{Cache, RecurLayer};
6use crate::forward::argmax;
7use crate::hybrid::{FullAttnLayer, HybridModel, LinearAttnLayer, Mixer};
8use cudarc::driver::CudaSlice;
9use std::collections::HashMap;
10
11/// Persistent CUDA-graph decode state (CUDA-GRAPH-PLAN Phase 3). Holds the device-resident counters
12/// the captured graph reads/writes (`token_d` = current/next token id, `pos_d` = rope position) — both
13/// at FIXED addresses baked into every captured graph — plus the per-`t_kv`-bucket graph cache. The
14/// bucket key is the eager `(fa_vec, n_splits)` pair (see `Engine::fa_bucket_key`): every t_kv that
15/// maps to the same key reproduces eager's split geometry, so one captured graph replays bit-identically
16/// for the whole bucket. A new key triggers a re-capture (n_splits changes ~every 64 tokens).
17pub struct GraphDecodeState {
18    pub token_d: CudaSlice<u32>, // [1] resident next-token id (argmax writes, embed reads)
19    pub pos_d: CudaSlice<i32>,   // [1] resident rope position counter
20    pub graphs: HashMap<(bool, usize), cudarc::driver::CudaGraph>,
21    pub bucket_max: HashMap<(bool, usize), usize>, // bucket key -> bucket_max fed to the capture
22    pub captures: usize,                           // count of (re)captures, for reporting
23}
24
25/// Long-lived step-wise CUDA-graph decode session (see HybridModel::graph_session_new).
26/// One replay per step(); the only steady-state D2H is the 4-byte next-token read.
27pub struct GraphSession {
28    pub gs: GraphDecodeState,
29    pub cache: Cache,
30    /// LOAD-BEARING hold: the captured graph's embed-gather node references this
31    /// allocation — dropping it would free memory the graph still reads.
32    #[allow(dead_code)]
33    embd_gpu: CudaSlice<u8>,
34    graph: cudarc::driver::CudaGraph,
35    plan: Vec<crate::graph_update::FaMain>,
36    /// session budget: last valid t_kv (pos + max_new + 1 at creation).
37    pub bucket_max: usize,
38    /// current capture's kernel-class segment end — step() recaptures past it
39    /// (round 45: exec-update retunes splits, it cannot swap kernels; see
40    /// graph_decode_loop's SEGMENTS note).
41    seg_end: usize,
42    qt: i32,
43    row_bytes: usize,
44    n_vocab: usize,
45    /// GRAMMAR MASK (constrained decoding, 2026-08-03): packed llguidance bitset the
46    /// captured graph reads (mask_logits_f32 between lm_head and the in-graph argmax).
47    /// STABLE POINTER — baked at capture, carried across recaptures; the caller uploads
48    /// fresh contents (upload_mask) before every step. None = no mask node captured.
49    mask_dev: Option<CudaSlice<u32>>,
50    mask_words: usize,
51}
52
53impl GraphSession {
54    /// One graph-replay decode step. Returns the next token (already fed back into the
55    /// resident token_d — the following step consumes it). Errors past bucket_max
56    /// (the caller sized max_new at capture). Transparently recaptures when the eager
57    /// kernel class changes (fa_vec floor / v4 max / fa512 floor crossings).
58    pub fn step(
59        &mut self,
60        e: &Engine,
61        m: &crate::hybrid::HybridModel,
62    ) -> Result<u32, Box<dyn std::error::Error>> {
63        if self.cache.pos + 1 >= self.bucket_max {
64            return Err("GraphSession: past bucket_max (generation budget exceeded)".into());
65        }
66        if self.cache.pos + 1 > self.seg_end {
67            m.graph_session_recapture(e, self)?;
68        }
69        crate::graph_update::fa_apply(
70            &self.graph,
71            &mut self.plan,
72            self.cache.pos + 1,
73            crate::fa_split_keys,
74        )?;
75        self.graph.launch()?;
76        self.cache.pos += 1;
77        for kvl in self.cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
78            kvl.len += 1;
79        }
80        e.dtoh_u32_one(&self.gs.token_d)
81    }
82
83    /// GRAMMAR MASK upload (constrained graph sessions): fresh packed-bitset contents into
84    /// the STABLE buffer the captured graph reads — call before every step(). The word
85    /// count is a capture-time kernel arg (constant per model: the tokenizer vocab is
86    /// fixed), so the length must match the capture exactly.
87    pub fn upload_mask(
88        &mut self,
89        e: &Engine,
90        words: &[u32],
91    ) -> Result<(), Box<dyn std::error::Error>> {
92        let Some(d) = self.mask_dev.as_mut() else {
93            return Err("upload_mask: session captured without a mask node".into());
94        };
95        if words.len() != self.mask_words {
96            return Err(format!(
97                "upload_mask: {} words != captured {}",
98                words.len(),
99                self.mask_words
100            )
101            .into());
102        }
103        e.htod_u32_into(d, words)
104    }
105
106    /// Profiling decomposition of step() (graph-session-gate MEMRA_GS_PROF): the three
107    /// phases exposed separately. prof_launch is ASYNC (no sync) — prof_read carries the
108    /// sync+D2H. Advances the session exactly like step().
109    pub fn prof_apply(&mut self, _e: &Engine) -> Result<(), Box<dyn std::error::Error>> {
110        crate::graph_update::fa_apply(
111            &self.graph,
112            &mut self.plan,
113            self.cache.pos + 1,
114            crate::fa_split_keys,
115        )
116    }
117    pub fn prof_launch(&mut self) -> Result<(), Box<dyn std::error::Error>> {
118        self.graph.launch()?;
119        self.cache.pos += 1;
120        for kvl in self.cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
121            kvl.len += 1;
122        }
123        Ok(())
124    }
125    pub fn prof_read(&mut self, e: &Engine) -> Result<u32, Box<dyn std::error::Error>> {
126        e.dtoh_u32_one(&self.gs.token_d)
127    }
128}
129
130impl GraphDecodeState {
131    pub fn new(e: &Engine) -> Result<Self, Box<dyn std::error::Error>> {
132        Ok(GraphDecodeState {
133            token_d: e.stream().clone_htod(&[0u32])?,
134            pos_d: e.htod_i32(&[0])?,
135            graphs: HashMap::new(),
136            bucket_max: HashMap::new(),
137            captures: 0,
138        })
139    }
140}
141
142/// Generation parameters for the reusable serving API (`generate_with`).
143#[derive(Clone, Debug)]
144pub struct GenParams {
145    pub max_new: usize,         // hard cap on generated tokens
146    pub max_ctx: Option<usize>, // context-length guard; None => prompt+max_new+8
147    pub eos: Vec<u32>,          // stop on any of these token ids (eos/eog + specials)
148}
149impl Default for GenParams {
150    fn default() -> Self {
151        GenParams {
152            max_new: 128,
153            max_ctx: None,
154            eos: Vec::new(),
155        }
156    }
157}
158
159/// Why generation stopped.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum StopReason {
162    Eos,
163    MaxNew,
164    ContextFull,
165    Callback,
166}
167
168/// Result of `generate_with`: the generated token ids + why it stopped.
169pub struct GenOutput {
170    pub tokens: Vec<u32>,
171    pub stop_reason: StopReason,
172}
173
174/// Diagnostic-only snapshots of Hy3 layer 0 in the eager T=1 serving path.
175/// Each buffer is one residual-width device row captured before the next stage can reuse it.
176pub struct Hy3Layer0Stages {
177    pub attention_output: CudaSlice<f32>,
178    pub after_attention: CudaSlice<f32>,
179    pub mlp_output: CudaSlice<f32>,
180    pub residual: CudaSlice<f32>,
181}
182
183impl HybridModel {
184    /// Device embed table for the dc fast loops (lazy ~0.5GB upload). On OOM — tight fits
185    /// where resident experts + KV leave no headroom (35B ct-NVFP4 artifact at default
186    /// budget, 2026-07-17) — returns None and the caller stays on the host-embd eager loop
187    /// instead of panicking. Double-init race is benign (identical bytes, loser dropped).
188    fn embd_gpu_try(&self, e: &Engine) -> Option<&cudarc::driver::CudaSlice<u8>> {
189        if let Some(v) = self.embd_gpu.get() {
190            return Some(v);
191        }
192        match e.upload_u8(&self.embd.raw) {
193            Ok(buf) => Some(self.embd_gpu.get_or_init(|| buf)),
194            Err(err) => {
195                eprintln!(
196                    "[embd-gpu] upload failed ({err}); dc loop disabled, host-embd eager loop serves"
197                );
198                None
199            }
200        }
201    }
202}
203
204impl HybridModel {
205    /// One decode step for `token` at cache.pos; returns logits [n_vocab] (host f32). Advances cache.
206    pub fn decode_step(
207        &self,
208        e: &Engine,
209        token: u32,
210        cache: &mut Cache,
211    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
212        Ok(self.decode_step_h(e, token, cache)?.0)
213    }
214
215    /// Dense-FFN SwiGLU (T=1 decode): `down @ (silu(gate@z) * (up@z))`. Two fused levers stack here:
216    ///  - RANK3 LEVER 2: gate+up NVFP4 macro-scales fold into ONE `silu_mul_scaled*` launch (via
217    ///    `matmul_pre_noscale`), saving the two separate `scale_inplace` launches.
218    ///  - RANK2 LEVER (q8_1 quant-fold): when ffn_down is ALSO on the q8_1 fast path, the SwiGLU
219    ///    epilogue EMITS the q8_1 quantization of `act` directly (`silu_mul_scaled_q8_1`) and feeds
220    ///    ffn_down via `matmul_pre`, removing ffn_down's standalone `quantize_q8_1` launch (the
221    ///    down-proj activation has one consumer, so the quant folds into its producer for free).
222    /// BIT-IDENTICAL to matmul_pre(gate)+matmul_pre(up)+silu_mul+quantize_q8_1+matmul(down): same
223    /// float silu*mul, same amax/127 q8_1 rounding, same dp4a/mmvq dot. Falls back to the f32 `act`
224    /// + plain matmul(down) path whenever any of the three is off the fast path.
225    #[allow(clippy::too_many_arguments)]
226    fn ffn_swiglu_decode(
227        &self,
228        e: &Engine,
229        ffn_gate: &crate::model::GpuTensor,
230        ffn_up: &crate::model::GpuTensor,
231        ffn_down: &crate::model::GpuTensor,
232        z: &CudaSlice<f32>,
233        n_embd: usize,
234        n_ff: usize,
235        lim: Option<f32>,
236    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
237        // M3 dense layers use swigluoai (clamped) — the silu_mul fused fast paths below encode
238        // plain SiLU; route through ffn_act (macro-scales folded via matmul_pre) until clamped
239        // fused twins exist. step35's per-layer `lim` is the same problem, same escape hatch:
240        // silu_mul_scaled / silu_mul_scaled_q8_1 have no clamped twin.
241        if self.cfg.m3.is_some() || lim.is_some() {
242            let (zq, zd) = e.quantize_q8_1(z, 1, n_embd)?;
243            let gate = e.matmul_pre(ffn_gate, &zq, &zd, z, 1)?;
244            let up = e.matmul_pre(ffn_up, &zq, &zd, z, 1)?;
245            let mut act = e.uninit(n_ff)?;
246            Self::ffn_act_lim(e, &self.cfg, &gate, &up, 1.0, 1.0, lim, &mut act, n_ff)?;
247            return Ok(e.matmul(ffn_down, &act, 1)?);
248        }
249        if e.uses_q8_1_fast(ffn_gate) && e.uses_q8_1_fast(ffn_up) {
250            let (zq, zd) = e.quantize_q8_1(z, 1, n_embd)?;
251            // DUAL mm-fusion first (NVFP4 gate+up in ONE launch), else two noscale launches.
252            let pair = match e.matmul_pre_dual_noscale(ffn_gate, ffn_up, &zq, &zd, 1)? {
253                Some((g, u)) => (Some(g), Some(u)),
254                None => (
255                    e.matmul_pre_noscale(ffn_gate, &zq, &zd, 1)?,
256                    e.matmul_pre_noscale(ffn_up, &zq, &zd, 1)?,
257                ),
258            };
259            match pair {
260                (Some((gate, gs)), Some((up, us))) => {
261                    // RANK2 fold: if ffn_down is q8_1-fast, emit act PRE-QUANTIZED and skip the
262                    // standalone quantize_q8_1 before ffn_down.
263                    if e.uses_q8_1_fast(ffn_down) {
264                        let (aq, ad) = e.silu_mul_scaled_q8_1(&gate, &up, gs, us, n_ff)?;
265                        return Ok(e.matmul_pre(
266                            ffn_down, &aq, &ad, /*x_fallback unused on fast path*/ &gate, 1,
267                        )?);
268                    }
269                    let mut act = e.uninit(n_ff)?;
270                    e.silu_mul_scaled(&gate, &up, gs, us, &mut act, n_ff)?;
271                    return Ok(e.matmul(ffn_down, &act, 1)?);
272                }
273                _ => {
274                    // one (or both) not on the separable-scale fast path: scaled matmul + plain silu_mul.
275                    let gate = e.matmul_pre(ffn_gate, &zq, &zd, z, 1)?;
276                    let up = e.matmul_pre(ffn_up, &zq, &zd, z, 1)?;
277                    let mut act = e.uninit(n_ff)?;
278                    Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, n_ff)?;
279                    return Ok(e.matmul(ffn_down, &act, 1)?);
280                }
281            }
282        }
283        let gate = e.matmul(ffn_gate, z, 1)?;
284        let up = e.matmul(ffn_up, z, 1)?;
285        let mut act = e.uninit(n_ff)?;
286        Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, n_ff)?;
287        Ok(e.matmul(ffn_down, &act, 1)?)
288    }
289
290    /// Like `ffn_swiglu_decode` but the input is ALREADY q8_1-quantized `(zq, zd)` — used by the
291    /// DECODE NORM-FUSION lever where `add_rms_norm_q8_1` emits the post-attn-normed activation
292    /// pre-quantized (no f32 `z` materialized, no standalone quantize_q8_1 launch). Caller GUARANTEES
293    /// ffn_gate and ffn_up are q8_1-fast (so `matmul_pre_noscale` returns Some at m=1). BIT-IDENTICAL
294    /// to ffn_swiglu_decode(z) when (zq,zd) == quantize_q8_1(z): same matmul_pre_noscale, same
295    /// silu_mul_scaled_q8_1 / silu_mul_scaled, same ffn_down dot.
296    fn ffn_swiglu_decode_pre(
297        &self,
298        e: &Engine,
299        ffn_gate: &crate::model::GpuTensor,
300        ffn_up: &crate::model::GpuTensor,
301        ffn_down: &crate::model::GpuTensor,
302        zq: &CudaSlice<i8>,
303        zd: &CudaSlice<f32>,
304        n_ff: usize,
305    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
306        let pair = match e.matmul_pre_dual_noscale(ffn_gate, ffn_up, zq, zd, 1)? {
307            Some((g, u)) => (Some(g), Some(u)),
308            None => (
309                e.matmul_pre_noscale(ffn_gate, zq, zd, 1)?,
310                e.matmul_pre_noscale(ffn_up, zq, zd, 1)?,
311            ),
312        };
313        match pair {
314            (Some((gate, gs)), Some((up, us))) => {
315                if e.uses_q8_1_fast(ffn_down) {
316                    let (aq, ad) = e.silu_mul_scaled_q8_1(&gate, &up, gs, us, n_ff)?;
317                    Ok(e.matmul_pre(ffn_down, &aq, &ad, &gate, 1)?)
318                } else {
319                    let mut act = e.uninit(n_ff)?;
320                    e.silu_mul_scaled(&gate, &up, gs, us, &mut act, n_ff)?;
321                    Ok(e.matmul(ffn_down, &act, 1)?)
322                }
323            }
324            // Unreachable when the caller's q8_1-fast guarantee holds (m==1 + fast => Some). Guard
325            // anyway: re-quant from the dequantized pair would need f32; surface a clear error.
326            _ => Err("ffn_swiglu_decode_pre: gate/up not separable-scale at m=1 (caller must guarantee q8_1-fast)".into()),
327        }
328    }
329
330    /// Shared post-attention residual + post-attn-norm + FFN for ONE decode layer, routed by ALL
331    /// decode loops (eager + dc + dc_cap) so they stay bit-identical by construction. DECODE
332    /// NORM-FUSION LEVER: when the layer is Dense AND ffn_gate/ffn_up are q8_1-fast (the daily NVFP4
333    /// case), fuses residual-add + post_attn_norm + q8_1-quantize into ONE `add_rms_norm_q8_1` launch
334    /// and feeds the FFN the pre-quantized activation (skipping its internal quantize_q8_1) — removing
335    /// 1-2 launches + the f32 `z` HBM round-trip per layer. BIT-IDENTICAL to the unfused
336    /// add_rms_norm(or add+rms_norm) + quantize_q8_1 + ffn (all proven bit-identical in kernel_check).
337    /// MEMRA_NO_FUSE_NORMQ forces the unfused f32 path. Returns (x1 residual f32, ffn_out f32).
338    /// True when ALL of a mixer's input projections are on the q8_1 fast path (so the attn-input
339    /// rms_norm can emit q8_1 directly and the mixer skips its internal quantize_q8_1).
340    pub(crate) fn mixer_in_q8_1_fast(&self, e: &Engine, mixer: &Mixer) -> bool {
341        match mixer {
342            Mixer::Full(fa) => {
343                // step35 also projects its head-wise GATE from the same attn-normed input, so
344                // the fused (h-less) arm requires attn_gate on the q8_1 fast path too — without
345                // this the gate matmul would get a zero-length `h`.
346                let gate_ok = match &fa.attn_gate {
347                    Some(g) => e.uses_q8_1_fast(g),
348                    None => true,
349                };
350                gate_ok
351                    && e.uses_q8_1_fast(&fa.wq)
352                    && e.uses_q8_1_fast(&fa.wk)
353                    && e.uses_q8_1_fast(&fa.wv)
354            }
355            Mixer::Linear(la) => {
356                e.uses_q8_1_fast(&la.wqkv)
357                    && e.uses_q8_1_fast(&la.wqkv_gate)
358                    && e.uses_q8_1_fast(&la.ssm_beta)
359                    && e.uses_q8_1_fast(&la.ssm_alpha)
360            }
361            // MLA (increment 2, loader-only): predicate only — never claim the fused
362            // norm+quantize chain for an arm that has no forward yet.
363            Mixer::Mla(_) => false,
364        }
365    }
366
367    /// attn_norm + mixer for the EAGER loop, with the attn-input NORM-FUSION. MEMRA_NO_FUSE_NORMQ
368    /// forces the unfused (separate rms_norm + mixer-internal quantize) path.
369    fn attn_in_norm_mixer(
370        &self,
371        e: &Engine,
372        layer: &crate::hybrid::HybridLayer,
373        x: &CudaSlice<f32>,
374        pos_d: &CudaSlice<i32>,
375        pos: usize,
376        cache: &mut Cache,
377        il: usize,
378        n_embd: usize,
379        eps: f32,
380    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
381        let anorm = layer.attn_norm.float_data();
382        let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
383            && self.mixer_in_q8_1_fast(e, &layer.mixer);
384        if fuse {
385            let (hq, hd) = e.rms_norm_q8_1(x, anorm, n_embd, 1, eps)?;
386            // h is unused on the fast path (matmul_pre x_fallback only used at m>=16); pass a zero-len.
387            let h0 = e.zeros(0)?;
388            match &layer.mixer {
389                Mixer::Full(fa) => {
390                    self.full_attn_decode_pre(e, fa, &h0, Some((&hq, &hd)), pos_d, pos, cache, il)
391                }
392                Mixer::Linear(la) => {
393                    self.linear_attn_decode_pre(e, la, &h0, &hq, &hd, cache, il, false)
394                }
395                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
396            }
397        } else {
398            let mut h = e.uninit(n_embd)?;
399            e.rms_norm(x, anorm, &mut h, n_embd, 1, eps)?;
400            match &layer.mixer {
401                Mixer::Full(fa) => self.full_attn_decode(e, fa, &h, pos_d, pos, cache, il),
402                Mixer::Linear(la) => self.linear_attn_decode(e, la, &h, cache, il),
403                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
404            }
405        }
406    }
407
408    /// attn_norm + mixer for the DEVICE-COUNTER loop (decode_step_dc). Full-attn uses the dc path;
409    /// linear uses the eager-state path (persistent=false), same as decode_step_dc. NORM-FUSED.
410    fn attn_in_norm_mixer_dc(
411        &self,
412        e: &Engine,
413        layer: &crate::hybrid::HybridLayer,
414        x: &CudaSlice<f32>,
415        pos_d: &CudaSlice<i32>,
416        cache: &mut Cache,
417        il: usize,
418        n_embd: usize,
419        eps: f32,
420    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
421        let anorm = layer.attn_norm.float_data();
422        let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
423            && self.mixer_in_q8_1_fast(e, &layer.mixer);
424        if fuse {
425            let (hq, hd) = e.rms_norm_q8_1(x, anorm, n_embd, 1, eps)?;
426            let h0 = e.zeros(0)?;
427            match &layer.mixer {
428                Mixer::Full(fa) => {
429                    self.full_attn_decode_dc_pre(e, fa, &h0, &hq, &hd, pos_d, cache, il)
430                }
431                Mixer::Linear(la) => {
432                    self.linear_attn_decode_pre(e, la, &h0, &hq, &hd, cache, il, false)
433                }
434                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
435            }
436        } else {
437            let mut h = e.uninit(n_embd)?;
438            e.rms_norm(x, anorm, &mut h, n_embd, 1, eps)?;
439            match &layer.mixer {
440                Mixer::Full(fa) => self.full_attn_decode_dc(e, fa, &h, pos_d, cache, il),
441                Mixer::Linear(la) => self.linear_attn_decode(e, la, &h, cache, il),
442                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
443            }
444        }
445    }
446
447    /// attn_norm + mixer for the CAPTURE loop (decode_step_dc_cap). Full-attn uses the dc_cap path
448    /// (fixed bucket_max); linear uses the persistent-state path. NORM-FUSED; capture-safe (rms_norm_q8_1
449    /// + the *_pre mixers enqueue the same kernels every replay, stable buffers).
450    fn attn_in_norm_mixer_dc_cap(
451        &self,
452        e: &Engine,
453        layer: &crate::hybrid::HybridLayer,
454        x: &CudaSlice<f32>,
455        pos_d: &CudaSlice<i32>,
456        cache: &mut Cache,
457        il: usize,
458        bucket_max: usize,
459        n_embd: usize,
460        eps: f32,
461    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
462        let anorm = layer.attn_norm.float_data();
463        let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
464            && self.mixer_in_q8_1_fast(e, &layer.mixer);
465        if fuse {
466            let (hq, hd) = e.rms_norm_q8_1(x, anorm, n_embd, 1, eps)?;
467            let h0 = e.zeros(0)?;
468            match &layer.mixer {
469                Mixer::Full(fa) => self.full_attn_decode_dc_cap_pre(
470                    e, fa, &h0, &hq, &hd, pos_d, cache, il, bucket_max,
471                ),
472                Mixer::Linear(la) => {
473                    self.linear_attn_decode_pre(e, la, &h0, &hq, &hd, cache, il, true)
474                }
475                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
476            }
477        } else {
478            let mut h = e.uninit(n_embd)?;
479            e.rms_norm(x, anorm, &mut h, n_embd, 1, eps)?;
480            match &layer.mixer {
481                Mixer::Full(fa) => {
482                    self.full_attn_decode_dc_cap(e, fa, &h, pos_d, cache, il, bucket_max)
483                }
484                Mixer::Linear(la) => self.linear_attn_decode_cap(e, la, &h, cache, il),
485                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
486            }
487        }
488    }
489
490    fn residual_norm_ffn(
491        &self,
492        e: &Engine,
493        layer: &crate::hybrid::HybridLayer,
494        x: &CudaSlice<f32>,
495        mixed: &CudaSlice<f32>,
496        n_embd: usize,
497        il: usize,
498        eps: f32,
499    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
500        let pnorm = layer.post_attn_norm.float_data();
501        match &layer.ffn {
502            crate::hybrid::Ffn::Dense {
503                ffn_gate,
504                ffn_up,
505                ffn_down,
506            } => {
507                let n_ff = ffn_gate.out_features();
508                // cfg.m3: the fused-pre chain's silu_mul_scaled* epilogues are plain SiLU —
509                // M3's swigluoai must route through ffn_swiglu_decode's m3 arm (FAST-gate
510                // MISMATCH root cause #2, 2026-07-07: L0 dense FFN clamp skipped under FAST).
511                // step35: SAME failure shape, per LAYER. A dense FFN's limit is the SHEXP array
512                // (upstream's one build_ffn serves dense + shared expert, llama-graph.cpp:1751).
513                let lim = self.cfg.clamp_shexp_at(il as u32);
514                let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
515                    && self.cfg.m3.is_none()
516                    && lim.is_none()
517                    && e.uses_q8_1_fast(ffn_gate)
518                    && e.uses_q8_1_fast(ffn_up);
519                if fuse {
520                    let mut x1 = e.uninit(n_embd)?;
521                    let (zq, zd) = e.add_rms_norm_q8_1(x, mixed, pnorm, &mut x1, n_embd, 1, eps)?;
522                    let ffn_out =
523                        self.ffn_swiglu_decode_pre(e, ffn_gate, ffn_up, ffn_down, &zq, &zd, n_ff)?;
524                    Ok((x1, ffn_out))
525                } else {
526                    let mut x1 = e.uninit(n_embd)?;
527                    let mut z = e.uninit(n_embd)?;
528                    e.add_rms_norm(x, mixed, pnorm, &mut x1, &mut z, n_embd, 1, eps)?;
529                    let ffn_out = self
530                        .ffn_swiglu_decode(e, ffn_gate, ffn_up, ffn_down, &z, n_embd, n_ff, lim)?;
531                    Ok((x1, ffn_out))
532                }
533            }
534            crate::hybrid::Ffn::Moe(m) => {
535                let mut x1 = e.uninit(n_embd)?;
536                let mut z = e.uninit(n_embd)?;
537                // z-quantize fuse (add_rms_norm_zq8) measured NEGATIVE here (158.8 vs 160.6:
538                // the fused warp-per-block quantize pass re-reads z slower than the dedicated
539                // coalesced quantize_q8_1). Kernel + threading kept for graph-capture use where
540                // launch count matters more; eager default = unfused (no gain = no change).
541                e.add_rms_norm(x, mixed, pnorm, &mut x1, &mut z, n_embd, 1, eps)?;
542                let ffn_out = self.moe_ffn_il_zq8(e, m, &z, None, 1, il as u16)?;
543                Ok((x1, ffn_out))
544            }
545        }
546    }
547
548    /// EAGLE3 aux-hidden capture (EAGLE-PLAN N1): one decode step that ALSO returns the trunk
549    /// residual-stream `x` taken AFTER each of the blocks in `aux_layers` (the EAGLE3 encoder feeds
550    /// these 3 layer hiddens through `fc`). Returns (logits[n_vocab] host, aux: Vec<[n_embd] dev>),
551    /// one device buffer per requested aux layer, in `aux_layers` order. The captured tensor is the
552    /// residual `x` produced by that block (`x2` at the loop tail), cloned before the next block
553    /// overwrites it — cheap (one clone_dtod of [n_embd] per aux layer). T=1 decode regime.
554    pub fn decode_step_aux(
555        &self,
556        e: &Engine,
557        token: u32,
558        cache: &mut Cache,
559        aux_layers: &[usize],
560    ) -> Result<(Vec<f32>, Vec<CudaSlice<f32>>), Box<dyn std::error::Error>> {
561        let (logits, aux, _) = self.decode_step_aux_inner(e, token, cache, aux_layers, false)?;
562        Ok((logits, aux))
563    }
564
565    /// Diagnostic-only Hy3 layer-0 trace through the real eager T=1 serving path. Besides the
566    /// final block residual, this captures the attention output before its residual add, the
567    /// after-attention residual, and the dense-MLP output before the final residual add.
568    pub fn decode_step_hy3_layer0_stages(
569        &self,
570        e: &Engine,
571        token: u32,
572        cache: &mut Cache,
573    ) -> Result<(Vec<f32>, Hy3Layer0Stages), Box<dyn std::error::Error>> {
574        if self.cfg.hy3.is_none() {
575            return Err("decode_step_hy3_layer0_stages requires a Hy3 model".into());
576        }
577        if !matches!(
578            self.layers.first().map(|layer| &layer.ffn),
579            Some(crate::hybrid::Ffn::Dense { .. })
580        ) {
581            return Err("Hy3 diagnostic expected layer 0 to use a dense MLP".into());
582        }
583        let (logits, _, stages) = self.decode_step_aux_inner(e, token, cache, &[], true)?;
584        Ok((
585            logits,
586            stages.ok_or("Hy3 layer-0 stages were not captured")?,
587        ))
588    }
589
590    fn decode_step_aux_inner(
591        &self,
592        e: &Engine,
593        token: u32,
594        cache: &mut Cache,
595        aux_layers: &[usize],
596        capture_hy3_layer0: bool,
597    ) -> Result<(Vec<f32>, Vec<CudaSlice<f32>>, Option<Hy3Layer0Stages>), Box<dyn std::error::Error>>
598    {
599        let cfg = &self.cfg;
600        let n_embd = cfg.n_embd as usize;
601        let eps = cfg.rms_eps;
602        let pos = cache.pos;
603        let pos_d = e.htod_i32(&[pos as i32])?;
604
605        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
606        let mut aux: Vec<CudaSlice<f32>> = Vec::with_capacity(aux_layers.len());
607        let mut hy3_layer0 = None;
608
609        for (il, layer) in self.layers.iter().enumerate() {
610            // attn-input NORM-FUSION (eager); shared with decode_step_h.
611            let mixed =
612                self.attn_in_norm_mixer(e, layer, &x, &pos_d, pos, cache, il, n_embd, eps)?;
613            // DECODE NORM-FUSION LEVER (residual_norm_ffn): residual add + post_attn RMSNorm +
614            // q8_1-quantize fused into ONE add_rms_norm_q8_1 launch on the Dense q8_1-fast path, then
615            // the FFN consumes the pre-quantized activation. Bit-identical to the unfused path.
616            let (x1, ffn_out) = self.residual_norm_ffn(e, layer, &x, &mixed, n_embd, il, eps)?;
617            let mut x2 = e.uninit(n_embd)?;
618            e.add(&x1, &ffn_out, &mut x2, n_embd)?;
619            if capture_hy3_layer0 && il == 0 {
620                hy3_layer0 = Some(Hy3Layer0Stages {
621                    attention_output: e.clone_dtod(&mixed)?,
622                    after_attention: e.clone_dtod(&x1)?,
623                    mlp_output: e.clone_dtod(&ffn_out)?,
624                    residual: e.clone_dtod(&x2)?,
625                });
626            }
627            // EAGLE3 N1: capture this block's residual output if it is an aux layer.
628            if aux_layers.contains(&il) {
629                aux.push(e.clone_dtod(&x2)?);
630            }
631            x = x2;
632        }
633        // re-order aux to match aux_layers order (contains() pushes in il order; aux_layers is the
634        // canonical order the encoder concats in — they coincide since aux_layers is ascending).
635        let mut hn = e.uninit(n_embd)?;
636        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
637        let logits = e.matmul(&self.output, &hn, 1)?;
638        let host = e.dtoh(&logits)?;
639        cache.pos += 1;
640        Ok((host, aux, hy3_layer0))
641    }
642
643    /// Like `decode_step`, but ALSO returns the trunk's hidden state `x` taken BEFORE the final
644    /// `output_norm` (MTP-PLAN §A: this is `h_seed` for the NextN head). Device buffer [n_embd].
645    pub fn decode_step_h(
646        &self,
647        e: &Engine,
648        token: u32,
649        cache: &mut Cache,
650    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
651        if self.is_gemma4_e4b() {
652            crate::pp::warn_unwired_once("gemma4-e4b eager decode");
653            return self.gemma4_e4b_decode_step_h(e, token, cache);
654        }
655        if self.cfg.gemma4.is_some() {
656            // pp2 door for the gemma4 arm lives inside gemma4_decode_step_h.
657            return self.gemma4_decode_step_h(e, token, cache);
658        }
659        // M2 ppN door (crate::pp): N-stage split of this walk with an explicit activation
660        // handoff at each boundary. Default OFF — unset env means this branch never taken.
661        if let Some(fence) = crate::pp::pp_cuts(self.layers.len()) {
662            return self.decode_step_h_ppn(e, token, cache, &fence);
663        }
664        let cfg = &self.cfg;
665        let n_embd = cfg.n_embd as usize;
666        let eps = cfg.rms_eps;
667        let pos = cache.pos;
668        let pos_d = e.htod_i32(&[pos as i32])?;
669
670        // embed the single token -> [1, n_embd]
671        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
672
673        // CROSS-LAYER ADD+NORM FUSION (launch-arc 2026-07-07): layer il's post-FFN residual add
674        // (x2 = x1 + ffn_out) and layer il+1's attn_norm+quantize are consecutive row-wise ops —
675        // add_rms_norm_q8_1 does all three in ONE launch (bit-identity proven in kernel_check:
676        // add_rms_norm == add then rms_norm; _q8_1 == then quantize_q8_1). Carry the un-added
677        // (x1, ffn_out) pair into the next iteration; the fused launch materializes x2 (the
678        // residual this layer needs) as its `res` output. Falls back to the separate add when
679        // the next mixer is off the q8_1 fast path.
680        let mut pending: Option<(CudaSlice<f32>, CudaSlice<f32>)> = None;
681        for (il, layer) in self.layers.iter().enumerate() {
682            let anorm = layer.attn_norm.float_data();
683            let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
684                && self.mixer_in_q8_1_fast(e, &layer.mixer);
685            // NOTE: take() FIRST, branch on fuse after — a tuple pattern like
686            // `if let (Some(p), true) = (pending.take(), fuse)` DROPS the taken pair when
687            // fuse is false (pattern fails post-take) and silently loses the residual add.
688            let taken = pending.take();
689            let mixed = match (taken, fuse) {
690                (Some((x1, f1)), true) => {
691                    // fused add + attn_norm + q8_1 (this layer's mixer input), res -> x2
692                    let mut x2 = e.uninit(n_embd)?;
693                    let (hq, hd) = e.add_rms_norm_q8_1(&x1, &f1, anorm, &mut x2, n_embd, 1, eps)?;
694                    x = x2;
695                    let h0 = e.zeros(0)?;
696                    match &layer.mixer {
697                        Mixer::Full(fa) => self.full_attn_decode_pre(
698                            e,
699                            fa,
700                            &h0,
701                            Some((&hq, &hd)),
702                            &pos_d,
703                            pos,
704                            cache,
705                            il,
706                        )?,
707                        Mixer::Linear(la) => {
708                            self.linear_attn_decode_pre(e, la, &h0, &hq, &hd, cache, il, false)?
709                        }
710                        Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
711                    }
712                }
713                (taken, _) => {
714                    if let Some((x1, f1)) = taken {
715                        let mut x2 = e.uninit(n_embd)?;
716                        e.add(&x1, &f1, &mut x2, n_embd)?;
717                        x = x2;
718                    }
719                    self.attn_in_norm_mixer(e, layer, &x, &pos_d, pos, cache, il, n_embd, eps)?
720                }
721            };
722
723            // DECODE NORM-FUSION LEVER (residual_norm_ffn): add+post_attn_norm+q8_1 fused on the Dense
724            // fast path. Bit-identical to add + rms_norm + ffn (add_rms_norm == add then rms_norm,
725            // proven in kernel_check; add_rms_norm_q8_1 == add_rms_norm then quantize_q8_1).
726            let (x1, ffn_out) = self.residual_norm_ffn(e, layer, &x, &mixed, n_embd, il, eps)?;
727            pending = Some((x1, ffn_out));
728        }
729        // final layer's add (no next norm to fuse with — output_norm is f32-out)
730        if let Some((x1, f1)) = pending.take() {
731            let mut x2 = e.uninit(n_embd)?;
732            e.add(&x1, &f1, &mut x2, n_embd)?;
733            x = x2;
734        }
735
736        let mut hn = e.uninit(n_embd)?;
737        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
738        // h_seed = trunk hidden BEFORE output_norm (default, §A) or AFTER it (MEMRA_SPEC_HPOST,
739        // the reference engines' convention — see spec::spec_hpost).
740        let h_seed = if crate::spec::spec_hpost() {
741            e.clone_dtod(&hn)?
742        } else {
743            e.clone_dtod(&x)?
744        };
745        // head-MIPS feasibility probe (MEMRA_DUMP_HN=<path>): append pre-head hiddens for
746        // offline bound analysis. Diagnostic only.
747        if let Ok(path) = std::env::var("MEMRA_DUMP_HN") {
748            let hh = e.dtoh(&hn)?;
749            use std::io::Write;
750            let mut fo = std::fs::OpenOptions::new()
751                .create(true)
752                .append(true)
753                .open(path)?;
754            for v in &hh {
755                fo.write_all(&v.to_le_bytes())?;
756            }
757        }
758        let logits = e.matmul(&self.output, &hn, 1)?;
759        let host = e.dtoh(&logits)?;
760        cache.pos += 1;
761        Ok((host, h_seed))
762    }
763
764    /// M1-PP2 stage subgraph: run layers [lo, hi) of the generic eager walk. Enters with a
765    /// MATERIALIZED residual `x` (no pending fusion pair from outside the range) and exits
766    /// with the range's final residual materialized (the trailing add executed, exactly like
767    /// the last layer of an unsplit walk). Body is the `decode_step_h` loop verbatim with the
768    /// cross-layer add+norm fusion carry LOCAL to the range — so the only state a stage
769    /// boundary has to move is the [n_embd] hidden state. Bit-identity of the cut relies on
770    /// the kernel-check-pinned `add_rms_norm_q8_1 == add then rms_norm_q8_1` identity
771    /// (`pp2-gate` verifies end-to-end on real weights).
772    /// `pub(crate)`: also the B=1 serve fast-path's trunk (decode_batch.rs
773    /// `decode_step_b1_fast`, H3) — shared verbatim so the serve path inherits every m=1
774    /// fusion instead of needing a batched twin per lever.
775    #[allow(clippy::too_many_arguments)]
776    pub(crate) fn decode_layers_eager(
777        &self,
778        e: &Engine,
779        mut x: CudaSlice<f32>,
780        lo: usize,
781        hi: usize,
782        pos_d: &CudaSlice<i32>,
783        pos: usize,
784        cache: &mut Cache,
785    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
786        let n_embd = self.cfg.n_embd as usize;
787        let eps = self.cfg.rms_eps;
788        let mut pending: Option<(CudaSlice<f32>, CudaSlice<f32>)> = None;
789        for il in lo..hi {
790            let layer = &self.layers[il];
791            let anorm = layer.attn_norm.float_data();
792            let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
793                && self.mixer_in_q8_1_fast(e, &layer.mixer);
794            // take() FIRST, branch on fuse after (see decode_step_h: a tuple pattern drops
795            // the taken pair when fuse is false and silently loses the residual add).
796            let taken = pending.take();
797            let mixed = match (taken, fuse) {
798                (Some((x1, f1)), true) => {
799                    let mut x2 = e.uninit(n_embd)?;
800                    let (hq, hd) = e.add_rms_norm_q8_1(&x1, &f1, anorm, &mut x2, n_embd, 1, eps)?;
801                    x = x2;
802                    let h0 = e.zeros(0)?;
803                    match &layer.mixer {
804                        Mixer::Full(fa) => self.full_attn_decode_pre(
805                            e,
806                            fa,
807                            &h0,
808                            Some((&hq, &hd)),
809                            pos_d,
810                            pos,
811                            cache,
812                            il,
813                        )?,
814                        Mixer::Linear(la) => {
815                            self.linear_attn_decode_pre(e, la, &h0, &hq, &hd, cache, il, false)?
816                        }
817                        Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
818                    }
819                }
820                (taken, _) => {
821                    if let Some((x1, f1)) = taken {
822                        let mut x2 = e.uninit(n_embd)?;
823                        e.add(&x1, &f1, &mut x2, n_embd)?;
824                        x = x2;
825                    }
826                    self.attn_in_norm_mixer(e, layer, &x, pos_d, pos, cache, il, n_embd, eps)?
827                }
828            };
829            let (x1, ffn_out) = self.residual_norm_ffn(e, layer, &x, &mixed, n_embd, il, eps)?;
830            pending = Some((x1, ffn_out));
831        }
832        // range's final add (no next norm inside the range to fuse with)
833        if let Some((x1, f1)) = pending.take() {
834            let mut x2 = e.uninit(n_embd)?;
835            e.add(&x1, &f1, &mut x2, n_embd)?;
836            x = x2;
837        }
838        Ok(x)
839    }
840
841    /// M2: `decode_step_h` as N stage subgraphs, each on ITS OWN CUDA stream (and, under
842    /// MEMRA_PP_DEVICES, its own device/engine), with the transport-selected boundary
843    /// handoff at each fence cut. Stage 0 = embed + its layer range; each middle stage
844    /// RXes boundary s-1 (waits its ev_tx), runs its range, TXes boundary s; the last
845    /// stage adds output_norm + lm head. Per-layer KV/linear state stays owned by the
846    /// stage that runs the layer; `cache.pos` is snapshotted once and advanced once.
847    /// MEMRA_PP_STREAMS=0 = the increment-1 same-stream seam.
848    /// Gate: `ppn-gate` (bit-identical logits vs unsplit at every N/knob combination).
849    fn decode_step_h_ppn(
850        &self,
851        e: &Engine,
852        token: u32,
853        cache: &mut Cache,
854        fence: &[usize],
855    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
856        if crate::pp::pp2_streams_off() {
857            return self.decode_step_h_ppn_samestream(e, token, cache, fence);
858        }
859        let rt = crate::pp::PpNRt::get(e)?;
860        let n_st = fence.len() - 1;
861        assert_eq!(
862            rt.n_stages(),
863            n_st,
864            "PpNRt stage count {} != fence stages {n_st}",
865            rt.n_stages()
866        );
867        // #87 REVERSE PUBLICATION (lane/pp2spec-crash): this body's stage-stream
868        // allocations may reuse pool blocks freed from a PREVIOUS ppn call's outputs
869        // (h_seed, verify vx/ckpt) whose primary-stream consumers are still queued —
870        // the reuse-write races the queued read. Order every stage stream behind the
871        // caller's stream before the first stage allocation. Full anatomy:
872        // `PpNRt::fence_stages_behind`.
873        rt.fence_stages_behind(&e.stream())?;
874        let cfg = &self.cfg;
875        let n_embd = cfg.n_embd as usize;
876        let eps = cfg.rms_eps;
877        let pos = cache.pos;
878
879        // PER-STAGE pos_d (M2 pipelining law): every stage uploads its OWN copy of the
880        // step's pos scalar on ITS stream, so the buffer is allocated, consumed, and
881        // freed on one stream (a shared stage-0 pos_d freed at fn return breaks under
882        // deferred readback: the free enqueues on stream 0 while stages 1..N-1 still
883        // dereference it — the 2026-08-02 pipelined-gate all-logits divergence).
884
885        // ---- STAGE 0 (its own stream): embed + layers [0, fence[1]) + boundary-0 TX ----
886        let mut slot = {
887            let _st0 = rt.enter(0);
888            let e0 = rt.engine(0, e);
889            let pos_d = e0.htod_i32(&[pos as i32])?;
890            let x = e0.htod(&self.embd.gather(n_embd, &[token]))?;
891            let x = self.decode_layers_eager(e0, x, fence[0], fence[1], &pos_d, pos, cache)?;
892            rt.tx(0, &x, n_embd)?
893            // x + pos_d drop here: freed stream-ordered on stage-0's stream after use.
894        };
895
896        // ---- MIDDLE STAGES s in [1, n_st-1): RX boundary s-1 -> range -> TX boundary s ----
897        for s in 1..n_st - 1 {
898            let _st = rt.enter(s);
899            let es = rt.engine(s, e);
900            let pos_d = es.htod_i32(&[pos as i32])?;
901            let x = rt.rx(s - 1, slot, n_embd)?;
902            let x = self.decode_layers_eager(es, x, fence[s], fence[s + 1], &pos_d, pos, cache)?;
903            slot = rt.tx(s, &x, n_embd)?;
904        }
905
906        // ---- LAST STAGE: RX + layers [fence[n_st-1], n) + output_norm + lm head ----
907        let _stl = rt.enter(n_st - 1);
908        let el = rt.engine(n_st - 1, e);
909        let pos_d = el.htod_i32(&[pos as i32])?;
910        let x = rt.rx(n_st - 2, slot, n_embd)?;
911        let x =
912            self.decode_layers_eager(el, x, fence[n_st - 1], fence[n_st], &pos_d, pos, cache)?;
913        let e = el; // head runs through the last stage's engine on its stream
914
915        let mut hn = e.uninit(n_embd)?;
916        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
917        let h_seed = if crate::spec::spec_hpost() {
918            e.clone_dtod(&hn)?
919        } else {
920            e.clone_dtod(&x)?
921        };
922        // same diagnostics door as decode_step_h (MEMRA_DUMP_HN) so the arms stay observably
923        // interchangeable.
924        if let Ok(path) = std::env::var("MEMRA_DUMP_HN") {
925            let hh = e.dtoh(&hn)?;
926            use std::io::Write;
927            let mut fo = std::fs::OpenOptions::new()
928                .create(true)
929                .append(true)
930                .open(path)?;
931            for v in &hh {
932                fo.write_all(&v.to_le_bytes())?;
933            }
934        }
935        let logits = e.matmul(&self.output, &hn, 1)?;
936        let host = e.dtoh(&logits)?;
937        cache.pos += 1;
938        Ok((host, h_seed))
939    }
940
941    /// MEMRA_PP_STREAMS=0 rollback seam: the increment-1 body generalized to N — every
942    /// stage subgraph on the ambient compute stream, each boundary = two plain dtod copies.
943    fn decode_step_h_ppn_samestream(
944        &self,
945        e: &Engine,
946        token: u32,
947        cache: &mut Cache,
948        fence: &[usize],
949    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
950        let cfg = &self.cfg;
951        let n_embd = cfg.n_embd as usize;
952        let eps = cfg.rms_eps;
953        let pos = cache.pos;
954        let pos_d = e.htod_i32(&[pos as i32])?;
955
956        // ---- STAGE 0: embed (the table lives with stage 0) + layers [0, fence[1]) ----
957        let x = e.htod(&self.embd.gather(n_embd, &[token]))?;
958        let mut x = self.decode_layers_eager(e, x, fence[0], fence[1], &pos_d, pos, cache)?;
959
960        // ---- each later stage: explicit [n_embd] handoff (TX copy, RX copy) + range ----
961        for s in 1..fence.len() - 1 {
962            let boundary_tx = e.clone_dtod(&x)?;
963            let boundary_rx = e.clone_dtod(&boundary_tx)?;
964            x = self.decode_layers_eager(
965                e,
966                boundary_rx,
967                fence[s],
968                fence[s + 1],
969                &pos_d,
970                pos,
971                cache,
972            )?;
973        }
974
975        let mut hn = e.uninit(n_embd)?;
976        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
977        let h_seed = if crate::spec::spec_hpost() {
978            e.clone_dtod(&hn)?
979        } else {
980            e.clone_dtod(&x)?
981        };
982        if let Ok(path) = std::env::var("MEMRA_DUMP_HN") {
983            let hh = e.dtoh(&hn)?;
984            use std::io::Write;
985            let mut fo = std::fs::OpenOptions::new()
986                .create(true)
987                .append(true)
988                .open(path)?;
989            for v in &hh {
990                fo.write_all(&v.to_le_bytes())?;
991            }
992        }
993        let logits = e.matmul(&self.output, &hn, 1)?;
994        let host = e.dtoh(&logits)?;
995        cache.pos += 1;
996        Ok((host, h_seed))
997    }
998
999    /// M2 increment 3 (DEFERRED READBACK — the pipelining seed): the ppN step WITHOUT the
1000    /// terminal logits D2H. Returns `PendingLogits` (device logits + completion event +
1001    /// the runtime's dedicated readback stream); the caller keeps 2+ tokens in flight by
1002    /// enqueueing step t+1 BEFORE waiting step t (with MEMRA_PP_OVERLAP=1 the
1003    /// double-buffered boundary slots actually alternate, so stage 0 of t+1 runs under
1004    /// stage 1..N-1 of t; the slot ev_tx/ev_rx chain keeps each token's math fully
1005    /// event-ordered either way — enqueueing deeper than 2 is CORRECT, the slots simply
1006    /// serialize device-side).
1007    ///
1008    /// EXACTNESS CONTRACT: per-token logits are BIT-IDENTICAL to the serial arm — same
1009    /// kernels, same per-token event order; only the host-side wait moves (scheduling
1010    /// change, never math). The pipelined replay arm of `ppn-gate` proves it per step.
1011    ///
1012    /// NOT produced here (both are trunk COPIES — no math feeding the logits changes):
1013    /// h_seed and the MEMRA_DUMP_HN diagnostic tap. The serving loop decides their
1014    /// deferred form when it adopts this API.
1015    ///
1016    /// The caller advances the token stream, so `cache.pos` advances at ENQUEUE (host
1017    /// state; device work is event-ordered regardless).
1018    pub fn decode_step_h_ppn_deferred(
1019        &self,
1020        e: &Engine,
1021        token: u32,
1022        cache: &mut Cache,
1023    ) -> Result<crate::pp::PendingLogits, Box<dyn std::error::Error>> {
1024        let fence = crate::pp::pp_cuts(self.layers.len())
1025            .ok_or("ppn deferred: pp door closed (MEMRA_PP_STAGES unset)")?;
1026        if crate::pp::pp2_streams_off() {
1027            return Err("ppn deferred needs per-stage streams (MEMRA_PP_STREAMS=0 set)".into());
1028        }
1029        if self.cfg.gemma4.is_some() {
1030            return Err("ppn deferred: generic eager arm only (gemma4 is 2-stage serial)".into());
1031        }
1032        if crate::pp::pp_multi_stream_same_device()
1033            && std::env::var("MEMRA_PP_FORCE_SAME_DEV_PIPELINED").as_deref() != Ok("1")
1034        {
1035            return Err(
1036                "ppn deferred: refused with 2+ stage streams on one device — repro'd \
1037                 nondeterministic logits (35% flake, 2026-08-02 x20 soak, root cause open: \
1038                 shared-Engine kernels concurrent on co-located streams). Use one device \
1039                 per stage (MEMRA_PP_DEVICES) or the serial arm. \
1040                 MEMRA_PP_FORCE_SAME_DEV_PIPELINED=1 overrides for soak/bisect measurement."
1041                    .into(),
1042            );
1043        }
1044        let rt = crate::pp::PpNRt::get(e)?;
1045        let n_st = fence.len() - 1;
1046        assert_eq!(
1047            rt.n_stages(),
1048            n_st,
1049            "PpNRt stage count {} != fence stages {n_st}",
1050            rt.n_stages()
1051        );
1052        let cfg = &self.cfg;
1053        let n_embd = cfg.n_embd as usize;
1054        let eps = cfg.rms_eps;
1055        let pos = cache.pos;
1056
1057        // Per-stage pos_d — see decode_step_h_ppn: under deferred readback a shared
1058        // pos_d's fn-end free races stages 1..N-1 (the free enqueues on stream 0 at
1059        // ENQUEUE time here, no terminal D2H to drain first). Each stage owns its copy.
1060        let mut slot = {
1061            let _st0 = rt.enter(0);
1062            let e0 = rt.engine(0, e);
1063            let pos_d = e0.htod_i32(&[pos as i32])?;
1064            let x = e0.htod(&self.embd.gather(n_embd, &[token]))?;
1065            let x = self.decode_layers_eager(e0, x, fence[0], fence[1], &pos_d, pos, cache)?;
1066            rt.tx(0, &x, n_embd)?
1067        };
1068        for s in 1..n_st - 1 {
1069            let _st = rt.enter(s);
1070            let es = rt.engine(s, e);
1071            let pos_d = es.htod_i32(&[pos as i32])?;
1072            let x = rt.rx(s - 1, slot, n_embd)?;
1073            let x = self.decode_layers_eager(es, x, fence[s], fence[s + 1], &pos_d, pos, cache)?;
1074            slot = rt.tx(s, &x, n_embd)?;
1075        }
1076        let _stl = rt.enter(n_st - 1);
1077        let el = rt.engine(n_st - 1, e);
1078        let pos_d = el.htod_i32(&[pos as i32])?;
1079        let x = rt.rx(n_st - 2, slot, n_embd)?;
1080        let x =
1081            self.decode_layers_eager(el, x, fence[n_st - 1], fence[n_st], &pos_d, pos, cache)?;
1082
1083        let mut hn = el.uninit(n_embd)?;
1084        el.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
1085        let logits = el.matmul(&self.output, &hn, 1)?;
1086        let ev = rt.record_done()?;
1087        cache.pos += 1;
1088        Ok(crate::pp::PendingLogits::new(
1089            logits,
1090            ev,
1091            rt.readback_stream().clone(),
1092        ))
1093    }
1094
1095    /// LOCKSTEP MULTI-STREAM decode (lane-3 M1): m independent streams advance one token each
1096    /// through a single per-layer walk. Per-stream math is identical to `decode_step_h` (same
1097    /// fusion chain, same mixer and FFN calls against that stream's own `Cache`), so each
1098    /// stream's token sequence is bit-identical to its single-stream run. The lockstep order
1099    /// puts the m streams' layer-il MoE calls adjacent in time, so one stream's expert-cache
1100    /// fill serves its siblings within the step — the measured cross-stream io amortization
1101    /// (1.12x/1.32x/1.66x at m=2/4/8) lands without batching attention or the CPU ABI.
1102    pub fn decode_step_lockstep(
1103        &self,
1104        e: &Engine,
1105        tokens: &[u32],
1106        caches: &mut [Cache],
1107    ) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
1108        if tokens.len() != caches.len() || tokens.is_empty() {
1109            return Err("lockstep needs one token per stream cache".into());
1110        }
1111        if self.cfg.gemma4.is_some() {
1112            return Err("lockstep decode does not support the gemma4 paths".into());
1113        }
1114        let cfg = &self.cfg;
1115        let n_embd = cfg.n_embd as usize;
1116        let eps = cfg.rms_eps;
1117        let m = tokens.len();
1118
1119        let mut pos_d = Vec::with_capacity(m);
1120        let mut x: Vec<CudaSlice<f32>> = Vec::with_capacity(m);
1121        for (s, &token) in tokens.iter().enumerate() {
1122            pos_d.push(e.htod_i32(&[caches[s].pos as i32])?);
1123            x.push(e.htod(&self.embd.gather(n_embd, &[token]))?);
1124        }
1125        let mut pending: Vec<Option<(CudaSlice<f32>, CudaSlice<f32>)>> =
1126            (0..m).map(|_| None).collect();
1127
1128        // M2 (MEMRA_LOCKSTEP_GROUPED=1): MoE layers batch all m rows through
1129        // moe_ffn_lockstep — resident experts amortize weight reads across streams via the
1130        // grouped GEMM machinery; CPU-assigned experts keep per-row companion calls.
1131        let grouped = match std::env::var("MEMRA_LOCKSTEP_GROUPED").as_deref() {
1132            Ok("1") => true,
1133            Ok("0") => false,
1134            // Auto: grouped wins from m>=3 under the default q8 lanes (M2 gate 2026-07-23:
1135            // m=2 6.17 base vs 5.85 grouped; m=3 6.31 grouped; m=4 5.66 vs 5.34).
1136            _ => m >= 3,
1137        };
1138        // M4a (MEMRA_LOCKSTEP_BATCH_ATTN=1): EXPERIMENTAL DOOR, measured flat — default off.
1139        // Full-attention layers run their WEIGHT-BOUND work (q/k/v and output projections) once
1140        // at m instead of m times, KV-bound work stays per stream. Bit-identity PASS, but e2e
1141        // flat at m=2 (4.72/4.72) and -2% at m=3 (5.24 vs 5.35), 2026-07-25: full-attn is the
1142        // minority layer type here (GDN dominates), so the m-band weight-read saving covers few
1143        // layers and is cancelled by the norm->q8_1 fusion this path gives up on exactly those
1144        // layers, plus its gather/scatter copies. The primitive itself
1145        // (`full_attn_decode_batched`) stays as the m-band building block for a serve loop,
1146        // where batching happens across requests at higher m and no fused alternative exists.
1147        let batch_attn = matches!(
1148            std::env::var("MEMRA_LOCKSTEP_BATCH_ATTN").as_deref(),
1149            Ok("1")
1150        ) && m >= 2;
1151        let pos_cat = e.htod_i32(
1152            &caches
1153                .iter()
1154                .take(m)
1155                .map(|c| c.pos as i32)
1156                .collect::<Vec<_>>(),
1157        )?;
1158        let n_embd_total = n_embd * m;
1159        let mut xcat = e.uninit(n_embd_total)?;
1160        for (il, layer) in self.layers.iter().enumerate() {
1161            let anorm = layer.attn_norm.float_data();
1162            let fuse = std::env::var("MEMRA_NO_FUSE_NORMQ").is_err()
1163                && self.mixer_in_q8_1_fast(e, &layer.mixer);
1164            let mut mixed_rows: Vec<Option<CudaSlice<f32>>> = (0..m).map(|_| None).collect();
1165            if batch_attn && matches!(layer.mixer, Mixer::Full(_)) {
1166                // Unfused residual+norm into the contiguous m-band buffer. Bit-identical to the
1167                // fused arm by construction (add_rms_norm_q8_1 == add, rms_norm, quantize_q8_1);
1168                // the batched mixer quantizes all m rows in one call.
1169                for s in 0..m {
1170                    if let Some((x1, f1)) = pending[s].take() {
1171                        let mut x2 = e.uninit(n_embd)?;
1172                        e.add(&x1, &f1, &mut x2, n_embd)?;
1173                        x[s] = x2;
1174                    }
1175                    let mut hn = e.uninit(n_embd)?;
1176                    e.rms_norm(&x[s], anorm, &mut hn, n_embd, 1, eps)?;
1177                    e.copy_into(&mut xcat, s * n_embd, &hn, n_embd)?;
1178                }
1179                let Mixer::Full(fa) = &layer.mixer else {
1180                    unreachable!()
1181                };
1182                let out_cat =
1183                    self.full_attn_decode_batched(e, fa, &xcat, m, &pos_cat, caches, il)?;
1184                for s in 0..m {
1185                    let mut mixed = e.uninit(n_embd)?;
1186                    e.copy_view_into(
1187                        &mut mixed,
1188                        0,
1189                        &out_cat.slice(s * n_embd..(s + 1) * n_embd),
1190                        n_embd,
1191                    )?;
1192                    if grouped && matches!(&layer.ffn, crate::hybrid::Ffn::Moe(_)) {
1193                        mixed_rows[s] = Some(mixed);
1194                    } else {
1195                        let (x1, ffn_out) =
1196                            self.residual_norm_ffn(e, layer, &x[s], &mixed, n_embd, il, eps)?;
1197                        pending[s] = Some((x1, ffn_out));
1198                    }
1199                }
1200            } else {
1201                for s in 0..m {
1202                    let pos = caches[s].pos;
1203                    let taken = pending[s].take();
1204                    let mixed = match (taken, fuse) {
1205                        (Some((x1, f1)), true) => {
1206                            let mut x2 = e.uninit(n_embd)?;
1207                            let (hq, hd) =
1208                                e.add_rms_norm_q8_1(&x1, &f1, anorm, &mut x2, n_embd, 1, eps)?;
1209                            x[s] = x2;
1210                            let h0 = e.zeros(0)?;
1211                            match &layer.mixer {
1212                                Mixer::Full(fa) => self.full_attn_decode_pre(
1213                                    e,
1214                                    fa,
1215                                    &h0,
1216                                    Some((&hq, &hd)),
1217                                    &pos_d[s],
1218                                    pos,
1219                                    &mut caches[s],
1220                                    il,
1221                                )?,
1222                                Mixer::Linear(la) => self.linear_attn_decode_pre(
1223                                    e,
1224                                    la,
1225                                    &h0,
1226                                    &hq,
1227                                    &hd,
1228                                    &mut caches[s],
1229                                    il,
1230                                    false,
1231                                )?,
1232                                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1233                            }
1234                        }
1235                        (taken, _) => {
1236                            if let Some((x1, f1)) = taken {
1237                                let mut x2 = e.uninit(n_embd)?;
1238                                e.add(&x1, &f1, &mut x2, n_embd)?;
1239                                x[s] = x2;
1240                            }
1241                            self.attn_in_norm_mixer(
1242                                e,
1243                                layer,
1244                                &x[s],
1245                                &pos_d[s],
1246                                pos,
1247                                &mut caches[s],
1248                                il,
1249                                n_embd,
1250                                eps,
1251                            )?
1252                        }
1253                    };
1254                    if grouped && matches!(&layer.ffn, crate::hybrid::Ffn::Moe(_)) {
1255                        mixed_rows[s] = Some(mixed);
1256                    } else {
1257                        let (x1, ffn_out) =
1258                            self.residual_norm_ffn(e, layer, &x[s], &mixed, n_embd, il, eps)?;
1259                        pending[s] = Some((x1, ffn_out));
1260                    }
1261                }
1262            }
1263            if grouped {
1264                if let crate::hybrid::Ffn::Moe(moe_weights) = &layer.ffn {
1265                    // Per-stream add+norm (identical math to residual_norm_ffn's MoE arm),
1266                    // rows batched for the cross-stream MoE stage, outputs split back.
1267                    let pnorm = layer.post_attn_norm.float_data();
1268                    let mut zbatch = e.uninit(n_embd_total)?;
1269                    let mut x1s: Vec<CudaSlice<f32>> = Vec::with_capacity(m);
1270                    for s in 0..m {
1271                        let mixed = mixed_rows[s].take().expect("grouped MoE row missing");
1272                        let mut x1 = e.uninit(n_embd)?;
1273                        let mut z = e.uninit(n_embd)?;
1274                        e.add_rms_norm(&x[s], &mixed, pnorm, &mut x1, &mut z, n_embd, 1, eps)?;
1275                        e.copy_view_into(&mut zbatch, s * n_embd, &z.slice(0..n_embd), n_embd)?;
1276                        x1s.push(x1);
1277                    }
1278                    let max_block = self.max_moe_block();
1279                    let ffn_all =
1280                        self.moe_ffn_lockstep(e, moe_weights, &zbatch, m, il as u16, max_block)?;
1281                    for (s, x1) in x1s.into_iter().enumerate() {
1282                        let mut out = e.uninit(n_embd)?;
1283                        e.copy_view_into(
1284                            &mut out,
1285                            0,
1286                            &ffn_all.slice(s * n_embd..(s + 1) * n_embd),
1287                            n_embd,
1288                        )?;
1289                        pending[s] = Some((x1, out));
1290                    }
1291                }
1292            }
1293        }
1294
1295        let mut logits_host = Vec::with_capacity(m);
1296        for s in 0..m {
1297            if let Some((x1, f1)) = pending[s].take() {
1298                let mut x2 = e.uninit(n_embd)?;
1299                e.add(&x1, &f1, &mut x2, n_embd)?;
1300                x[s] = x2;
1301            }
1302            let mut hn = e.uninit(n_embd)?;
1303            e.rms_norm(
1304                &x[s],
1305                self.output_norm.float_data(),
1306                &mut hn,
1307                n_embd,
1308                1,
1309                eps,
1310            )?;
1311            let logits = e.matmul(&self.output, &hn, 1)?;
1312            logits_host.push(e.dtoh(&logits)?);
1313            caches[s].pos += 1;
1314        }
1315        Ok(logits_host)
1316    }
1317
1318    /// DEVICE-COUNTER decode step (CUDA-GRAPH-PLAN Phase 2). A clone of `decode_step_h` that removes
1319    /// the two per-step VARYING host kernel-args by reading them from device counters:
1320    ///   1. the KV-append write slot  -> per-layer `kvl.len_d` (device i32[1])
1321    ///   2. the fa_decode t_kv bound   -> the same `kvl.len_d` after `inc_seqlen`
1322    /// plus it keeps the token id + rope pos DEVICE-RESIDENT (embed_gather_device, device rope pos,
1323    /// argmax_token_device). NO graph capture yet — runs the kernels eagerly through the counter
1324    /// path. Must be BIT-IDENTICAL to `decode_step_h`'s token stream (the gate).
1325    ///
1326    /// Args: `token_d` = resident device token id [1] (this step's input token); `pos_d` = resident
1327    /// device rope pos i32[1] (== cache.pos at entry; INCREMENTED in-path); `embd_gpu` = resident embed
1328    /// table; (qt,row_bytes) from EmbedHost::qt_and_row_bytes. Returns the NEXT token id device buffer.
1329    /// `cache.pos` and each `kvl.len`/`kvl.len_d` are advanced to match `decode_step_h`.
1330    pub fn decode_step_dc(
1331        &self,
1332        e: &Engine,
1333        token_d: &CudaSlice<u32>,
1334        pos_d: &mut CudaSlice<i32>,
1335        embd_gpu: &CudaSlice<u8>,
1336        embd_qt: i32,
1337        embd_row_bytes: usize,
1338        cache: &mut Cache,
1339        n_vocab: usize,
1340    ) -> Result<CudaSlice<u32>, Box<dyn std::error::Error>> {
1341        // Route gemma4 to ITS dc twin (mirrors decode_step_h): the generic walk below is the
1342        // qwen-class layer stack — running gemma weights through it produced the argmax-INIT
1343        // passthrough the round-45 g12 gate caught (first Hopper gating of this lane).
1344        if self.is_gemma4_e4b() {
1345            return Err("e4b has no device-counter decode step (dc/graph unwired)".into());
1346        }
1347        // PP DOOR: fail closed (pp2-hardening 2026-08-06). Same hole the batched path had —
1348        // the dc walk below is `for (il, layer) in self.layers.iter().enumerate()` on one
1349        // stream, with no stage split, so a sharded cross-device placement would peer-read
1350        // every remote layer's weights per step. Sits BEFORE the gemma4 delegate because
1351        // that twin has the same unsplit shape. The graph-capture path (`decode_step_dc_cap*`)
1352        // is covered transitively: it captures this same kernel chain, and its drivers reach
1353        // dc first — but a future capture path that does NOT is why the guard is a shared
1354        // helper (`pp::refuse_unsplit_if_remote`) rather than four copies.
1355        crate::pp::refuse_unsplit_if_remote(
1356            "decode_step_dc",
1357            "use the eager pp arm (decode_step_h), which IS stage-split",
1358        )?;
1359        if self.cfg.gemma4.is_some() {
1360            return self.gemma4_decode_step_dc(
1361                e,
1362                token_d,
1363                pos_d,
1364                embd_gpu,
1365                embd_qt,
1366                embd_row_bytes,
1367                cache,
1368                n_vocab,
1369                None,
1370            );
1371        }
1372        let cfg = &self.cfg;
1373        let n_embd = cfg.n_embd as usize;
1374        let eps = cfg.rms_eps;
1375
1376        // embed the single (DEVICE-resident) token -> [1, n_embd], no host round-trip of the id.
1377        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_row_bytes)?;
1378
1379        for (il, layer) in self.layers.iter().enumerate() {
1380            // attn-input NORM-FUSION (dc path); bit-identical to decode_step_h (Phase-2 gate).
1381            let mixed = self.attn_in_norm_mixer_dc(e, layer, &x, pos_d, cache, il, n_embd, eps)?;
1382
1383            // DECODE NORM-FUSION LEVER (residual_norm_ffn): see decode_step_h. Shared helper -> dc
1384            // path stays bit-identical to decode_step_h's token stream (the Phase-2 gate).
1385            let (x1, ffn_out) = self.residual_norm_ffn(e, layer, &x, &mixed, n_embd, il, eps)?;
1386            let mut x2 = e.uninit(n_embd)?;
1387            e.add(&x1, &ffn_out, &mut x2, n_embd)?;
1388            x = x2;
1389        }
1390
1391        let mut hn = e.uninit(n_embd)?;
1392        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
1393        let logits = e.matmul(&self.output, &hn, 1)?;
1394        // device argmax -> next token id stays resident (no logits dtoh).
1395        let next_tok = e.argmax_token_device(&logits, n_vocab)?;
1396        // advance rope pos counter on-device (replaces the per-step htod_i32(&[pos])).
1397        e.inc_seqlen(pos_d)?;
1398        cache.pos += 1;
1399        Ok(next_tok)
1400    }
1401
1402    /// CAPTURE body for CUDA-graph replay (CUDA-GRAPH-PLAN Phase 3). One full decode step enqueued
1403    /// entirely on `e.stream()` with ZERO host sync and ZERO per-step varying host kernel-args:
1404    ///   - embed reads the PERSISTENT device `token_d` (last step's argmax), writes scratch `x`.
1405    ///   - full-attn layers size n_splits from `bucket_max` (fixed for this capture); the kernel reads
1406    ///     the ACTUAL t_kv from the device counter `kvl.len_d`. KV append + device-counter inc happen
1407    ///     in-graph. The host `kvl.len`/`cache.pos` are NOT advanced here (the driver advances the host
1408    ///     mirrors once per replay; only the DEVICE counters advance inside the graph).
1409    ///   - linear-attn layers use the persistent-state variant (copy-back, stable pointers).
1410    ///   - lm_head -> parallel 2-pass argmax (`argmax_partial_f32`+`argmax_final_f32`) writes the
1411    ///     next id into the PERSISTENT `token_d`.
1412    ///   - `inc_seqlen(pos_d)` advances the rope-pos device counter in-graph.
1413    /// Captured ONCE per `bucket_max`; replayed for every t_kv in that bucket. Bit-identical to eager
1414    /// when `bucket_max` reproduces eager's n_splits for the replayed t_kv (the bucket-key contract).
1415    pub fn decode_step_dc_cap(
1416        &self,
1417        e: &Engine,
1418        token_d: &mut CudaSlice<u32>,
1419        pos_d: &mut CudaSlice<i32>,
1420        embd_gpu: &CudaSlice<u8>,
1421        embd_qt: i32,
1422        embd_row_bytes: usize,
1423        cache: &mut Cache,
1424        n_vocab: usize,
1425        bucket_max: usize,
1426    ) -> Result<(), Box<dyn std::error::Error>> {
1427        self.decode_step_dc_cap_masked(
1428            e,
1429            token_d,
1430            pos_d,
1431            embd_gpu,
1432            embd_qt,
1433            embd_row_bytes,
1434            cache,
1435            n_vocab,
1436            bucket_max,
1437            None,
1438        )
1439    }
1440
1441    /// `decode_step_dc_cap` + GRAMMAR MASK (constrained decoding): with `mask =
1442    /// Some((buf, words))`, mask_logits_f32 bans the packed bitset's unset ids IN the
1443    /// captured graph — a stable-pointer read between lm_head and the in-graph argmax
1444    /// (the KV-pointer pattern: contents change per step, address is baked). `None` is
1445    /// bit-for-bit the unmasked capture.
1446    #[allow(clippy::too_many_arguments)]
1447    pub fn decode_step_dc_cap_masked(
1448        &self,
1449        e: &Engine,
1450        token_d: &mut CudaSlice<u32>,
1451        pos_d: &mut CudaSlice<i32>,
1452        embd_gpu: &CudaSlice<u8>,
1453        embd_qt: i32,
1454        embd_row_bytes: usize,
1455        cache: &mut Cache,
1456        n_vocab: usize,
1457        bucket_max: usize,
1458        mask: Option<(&CudaSlice<u32>, usize)>,
1459    ) -> Result<(), Box<dyn std::error::Error>> {
1460        let cfg = &self.cfg;
1461        let n_embd = cfg.n_embd as usize;
1462        let eps = cfg.rms_eps;
1463
1464        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_row_bytes)?;
1465
1466        for (il, layer) in self.layers.iter().enumerate() {
1467            // attn-input NORM-FUSION (capture path); capture-safe + bit-identical to eager.
1468            let mixed = self.attn_in_norm_mixer_dc_cap(
1469                e, layer, &x, pos_d, cache, il, bucket_max, n_embd, eps,
1470            )?;
1471            // DECODE NORM-FUSION LEVER (residual_norm_ffn): see decode_step_aux. Shared helper keeps
1472            // the capture path bit-identical to eager by construction.
1473            let (x1, ffn_out) = self.residual_norm_ffn(e, layer, &x, &mixed, n_embd, il, eps)?;
1474            let mut x2 = e.uninit(n_embd)?;
1475            e.add(&x1, &ffn_out, &mut x2, n_embd)?;
1476            x = x2;
1477        }
1478
1479        let mut hn = e.uninit(n_embd)?;
1480        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
1481        let mut logits = e.matmul(&self.output, &hn, 1)?;
1482        // GRAMMAR MASK: ban before the argmax reads the row (masked argmax == host
1483        // masked-argmax — -FLT_MAX is the argmax kernels' init sentinel).
1484        if let Some((m, words)) = mask {
1485            e.mask_logits_col(&mut logits, m, 0, n_vocab, words)?;
1486        }
1487        // argmax into the PERSISTENT token_d (next step's embed reads it) — same buffer pointer baked
1488        // at capture, written each replay, so the token id never round-trips to host in steady state.
1489        e.argmax_token_device_into(&logits, token_d, n_vocab)?;
1490        e.inc_seqlen(pos_d)?;
1491        Ok(())
1492    }
1493
1494    /// CUDA-GRAPH decode driver (CUDA-GRAPH-PLAN Phase 3). Primes the prompt EAGERLY (device-counter
1495    /// `decode_step_dc`, advancing host + device counters together), then generates `max_new` tokens by
1496    /// CUDA-graph REPLAY: per step it picks the t_kv bucket key, captures a graph on first sight of that
1497    /// key (re-using the SAME persistent counters/cache so replays continue the sequence), and replays.
1498    /// The argmax-written next token stays device-resident in `gs.token_d`; we read back only the [1]
1499    /// u32 after each launch (the gate compares it; a real server can defer this). Returns the generated
1500    /// token ids. Greedy. Bit-identical to eager `decode_step` (the gate).
1501    ///
1502    /// CAPTURE STATE HYGIENE: `capture_graph` runs the step body 3x (2 warmup + 1 capture), each of
1503    /// which mutates the device KV/conv/ssm/counter state. We SNAPSHOT the cache + device counters +
1504    /// token id before capturing and RESTORE them after, so the 3 throwaway runs leave zero residue and
1505    /// replay resumes from the true pre-capture state.
1506    pub fn generate_graph(
1507        &self,
1508        e: &Engine,
1509        gs: &mut GraphDecodeState,
1510        prompt: &[u32],
1511        max_new: usize,
1512    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1513        let n_embd = self.cfg.n_embd as usize;
1514        let head_dim = self.cfg.head_dim_k as usize;
1515        let (qt, row_bytes) = self.embd.qt_and_row_bytes(n_embd);
1516
1517        // EVENT TRACKING OFF for the WHOLE graph-decode session. cudarc records a per-CudaSlice event
1518        // (the Engine is in multi-stream mode via copy_stream) and inserts `stream.wait(event)` on every
1519        // kernel arg whose buffer was touched — those waits are illegal inside a capture region. The
1520        // captured decode step is strictly single-stream, so this tracking is unnecessary. Disable it
1521        // BEFORE allocating ANY buffer the captured graph will reference (cache, embd, counters,
1522        // scratch) so none of them carry events. SAFETY: decode-dc touches only gpu.stream.
1523        let was_tracking = e.ctx().is_event_tracking();
1524        if was_tracking {
1525            unsafe {
1526                e.ctx().disable_event_tracking();
1527            }
1528        }
1529        let r = self.generate_graph_inner(e, gs, prompt, max_new, n_embd, head_dim, qt, row_bytes);
1530        if was_tracking {
1531            unsafe {
1532                e.ctx().enable_event_tracking();
1533            }
1534        }
1535        r
1536    }
1537
1538    fn generate_graph_inner(
1539        &self,
1540        e: &Engine,
1541        gs: &mut GraphDecodeState,
1542        prompt: &[u32],
1543        max_new: usize,
1544        n_embd: usize,
1545        head_dim: usize,
1546        qt: i32,
1547        row_bytes: usize,
1548    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
1549        let _ = n_embd;
1550        let embd_gpu = e.upload_u8(&self.embd.raw)?;
1551        let max_ctx = prompt.len() + max_new + 8;
1552        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1553
1554        // (Re)create the persistent counters tracking-OFF so they carry no events (the caller's
1555        // GraphDecodeState::new may have allocated them with tracking on).
1556        gs.pos_d = e.htod_i32(&[0])?;
1557        gs.token_d = e.stream().clone_htod(&[0u32])?;
1558        // PRIME eagerly: feed each prompt token; advance host + device counters together.
1559        let mut next_in = 0u32;
1560        for &tok in prompt {
1561            e.set_u32_one(&mut gs.token_d, tok)?;
1562            let nt = self.decode_step_dc(
1563                e,
1564                &gs.token_d,
1565                &mut gs.pos_d,
1566                &embd_gpu,
1567                qt,
1568                row_bytes,
1569                &mut cache,
1570                /*n_vocab*/ self.output.out_features(),
1571            )?;
1572            next_in = e.dtoh_u32_one(&nt)?;
1573        }
1574        // gs.token_d now must hold the first generated INPUT token (= argmax of the last prime step).
1575        e.set_u32_one(&mut gs.token_d, next_in)?;
1576
1577        // gemma4 rides ITS graph machinery (per-bucket captures + alloc-free slots; same token
1578        // stream convention: first generated token is out[0]) — graph_decode_loop below captures
1579        // the qwen-class dc step (the round-45 g12 illegal-address find).
1580        if self.cfg.gemma4.is_some() {
1581            let (toks, _reason) = self.gemma4_generate_graph(
1582                e,
1583                cache.pos,
1584                next_in,
1585                &mut cache,
1586                max_new,
1587                &[],
1588                |_| true,
1589            )?;
1590            gs.captures += 1;
1591            return Ok(toks);
1592        }
1593
1594        let mut out = Vec::with_capacity(max_new);
1595        self.graph_decode_loop(
1596            e,
1597            gs,
1598            &mut cache,
1599            &embd_gpu,
1600            qt,
1601            row_bytes,
1602            head_dim,
1603            max_new,
1604            |tok| {
1605                out.push(tok);
1606                None
1607            },
1608        )?;
1609        Ok(out)
1610    }
1611
1612    /// The CUDA-graph EXEC-UPDATE replay loop over an already-primed cache (2026-07-15,
1613    /// the E4B graph-exec pattern generalized): capture the dc step per KERNEL-CLASS
1614    /// SEGMENT, classify its fa nodes (`graph_update::fa_plan` — symbol list is
1615    /// model-generic), then per token retune the fa split geometry to the LIVE eager
1616    /// ladder (`fa_apply` keeps graph and eager in FP lockstep — bit-exact) and replay.
1617    /// The previous per-bucket-key capture map recaptured on every ladder rung
1618    /// (32 recaptures/256 tokens = 97 vs 128 tok/s eager; decode-bench 2026-07-15).
1619    ///
1620    /// SEGMENTS (round 45, the q35 graph-gate dig): exec-update can retune split counts
1621    /// but can NOT swap kernels — a session spanning an eager KERNEL-CLASS boundary
1622    /// (fa_vec floor, the v4 max, the fa512 floor) replayed the capture-time kernel
1623    /// against a different eager kernel below the boundary: valid softmax, different
1624    /// fold order, and the first near-tie flips the stream (q35: deterministic 144/256
1625    /// from step 110, exactly the scalar->vec crossing; regime pinned either way =
1626    /// BIT-IDENTICAL 256/256). One capture per crossed class boundary (2-3/session,
1627    /// not per rung) keeps graph and eager on the SAME kernel at every t_kv.
1628    ///
1629    /// Callers must have synced gs.token_d (= the FIRST generated token), gs.pos_d
1630    /// (= cache.pos) and every kvl.len_d (= kvl.len). Event tracking must be OFF.
1631    #[allow(clippy::too_many_arguments)]
1632    pub(crate) fn graph_decode_loop(
1633        &self,
1634        e: &Engine,
1635        gs: &mut GraphDecodeState,
1636        cache: &mut Cache,
1637        embd_gpu: &CudaSlice<u8>,
1638        qt: i32,
1639        row_bytes: usize,
1640        head_dim: usize,
1641        max_new: usize,
1642        mut emit: impl FnMut(u32) -> Option<StopReason>,
1643    ) -> Result<StopReason, Box<dyn std::error::Error>> {
1644        let _ = head_dim;
1645        let n_vocab = self.output.out_features();
1646        let final_max = cache.pos + max_new + 1;
1647
1648        // first generated token = argmax of the last prime step (emit before replay 1).
1649        let first = e.dtoh_u32_one(&gs.token_d)?;
1650        if let Some(r) = emit(first) {
1651            return Ok(r);
1652        }
1653        let mut done = 1usize;
1654        while done < max_new {
1655            let (graph, mut plan, seg_end) = self
1656                .graph_capture_segment(e, cache, gs, embd_gpu, qt, row_bytes, n_vocab, final_max)?;
1657
1658            while done < max_new && cache.pos + 1 <= seg_end {
1659                // retune fa geometry to the live t_kv AFTER this replay's in-graph append.
1660                crate::graph_update::fa_apply(
1661                    &graph,
1662                    &mut plan,
1663                    cache.pos + 1,
1664                    crate::fa_split_keys,
1665                )?;
1666                graph.launch()?;
1667                cache.pos += 1;
1668                for kvl in cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
1669                    kvl.len += 1;
1670                }
1671                // read back the [1] u32 next token (the only D2H in steady state).
1672                let tok = e.dtoh_u32_one(&gs.token_d)?;
1673                done += 1;
1674                if let Some(r) = emit(tok) {
1675                    return Ok(r);
1676                }
1677            }
1678        }
1679        Ok(StopReason::MaxNew)
1680    }
1681
1682    /// Step-wise CUDA-graph decode session (ARCHITECTURE-H100.md graph-serving lane,
1683    /// 2026-07-26): generate_graph's prime+capture lifted into a long-lived session so a
1684    /// SERVING scheduler can replay ONE step per tick instead of blocking a whole
1685    /// generation. Serving policy (measured): graphs win only at B=1 (214 solo vs 425
1686    /// aggregate batched-eager at B=4) — this is the single-interactive-session path.
1687    /// Capture discipline is generate_graph's verbatim: event tracking must be OFF for
1688    /// every buffer the graph references (new() toggles it), capture at bucket_max =
1689    /// pos + max_new + 1, fa geometry retuned per step (fa_apply, FP lockstep with eager).
1690    pub fn graph_session_new(
1691        &self,
1692        e: &Engine,
1693        prompt: &[u32],
1694        max_new: usize,
1695    ) -> Result<(GraphSession, u32), Box<dyn std::error::Error>> {
1696        let n_embd = self.cfg.n_embd as usize;
1697        let (qt, row_bytes) = self.embd.qt_and_row_bytes(n_embd);
1698        let was_tracking = e.ctx().is_event_tracking();
1699        if was_tracking {
1700            unsafe {
1701                e.ctx().disable_event_tracking();
1702            }
1703        }
1704        let r = self.graph_session_new_inner(e, prompt, max_new, qt, row_bytes);
1705        if was_tracking {
1706            unsafe {
1707                e.ctx().enable_event_tracking();
1708            }
1709        }
1710        r
1711    }
1712
1713    fn graph_session_new_inner(
1714        &self,
1715        e: &Engine,
1716        prompt: &[u32],
1717        max_new: usize,
1718        qt: i32,
1719        row_bytes: usize,
1720    ) -> Result<(GraphSession, u32), Box<dyn std::error::Error>> {
1721        let n_vocab = self.output.out_features();
1722        let embd_gpu = e.upload_u8(&self.embd.raw)?;
1723        let max_ctx = prompt.len() + max_new + 8;
1724        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
1725        let mut gs = GraphDecodeState::new(e)?;
1726        gs.pos_d = e.htod_i32(&[0])?;
1727        gs.token_d = e.stream().clone_htod(&[0u32])?;
1728        // prime (dc path — device counters advance with the host)
1729        let mut next_in = 0u32;
1730        for &tok in prompt {
1731            e.set_u32_one(&mut gs.token_d, tok)?;
1732            let nt = self.decode_step_dc(
1733                e,
1734                &gs.token_d,
1735                &mut gs.pos_d,
1736                &embd_gpu,
1737                qt,
1738                row_bytes,
1739                &mut cache,
1740                n_vocab,
1741            )?;
1742            next_in = e.dtoh_u32_one(&nt)?;
1743        }
1744        e.set_u32_one(&mut gs.token_d, next_in)?;
1745        self.graph_session_capture(
1746            e, cache, gs, embd_gpu, max_new, qt, row_bytes, n_vocab, None, 0,
1747        )
1748    }
1749
1750    /// GraphSession over an ALREADY-PRIMED cache (round 35): keeps the chunked-prefill
1751    /// TTFT. graph_session_new's token-wise re-prime made solo long-prompt promotion a
1752    /// net ~3x END-TO-END LOSS (measured live: 871-tok prompt + 400 gen = 6.4s vs ~2.2s
1753    /// eager). Device counters sync from host state; capture recipe unchanged.
1754    /// Requires event tracking OFF (engine default; MEMRA_EVT=1 callers must not use this
1755    /// — the primed cache's buffers would carry events, illegal inside capture).
1756    pub fn graph_session_from_cache(
1757        &self,
1758        e: &Engine,
1759        cache: Cache,
1760        first_token: u32,
1761        max_new: usize,
1762    ) -> Result<(GraphSession, u32), Box<dyn std::error::Error>> {
1763        self.graph_session_from_cache_masked(e, cache, first_token, max_new, None)
1764    }
1765
1766    /// `graph_session_from_cache` + GRAMMAR MASK (constrained decoding, 2026-08-03):
1767    /// `mask_init = Some(packed bitset)` allocates the session's stable mask buffer
1768    /// (tracking is OFF here — capture-legal), seeds it with the FIRST step's mask, and
1769    /// captures mask_logits_f32 into the graphed step. The caller re-uploads contents
1770    /// per step via `GraphSession::upload_mask` — same stable-pointer discipline as the
1771    /// KV len_d counters. `None` = the unmasked session, byte-identical.
1772    pub fn graph_session_from_cache_masked(
1773        &self,
1774        e: &Engine,
1775        mut cache: Cache,
1776        first_token: u32,
1777        max_new: usize,
1778        mask_init: Option<&[u32]>,
1779    ) -> Result<(GraphSession, u32), Box<dyn std::error::Error>> {
1780        if e.ctx().is_event_tracking() {
1781            return Err(
1782                "graph_session_from_cache requires event tracking OFF (MEMRA_EVT unset)".into(),
1783            );
1784        }
1785        let n_embd = self.cfg.n_embd as usize;
1786        let (qt, row_bytes) = self.embd.qt_and_row_bytes(n_embd);
1787        let n_vocab = self.output.out_features();
1788        let embd_gpu = e.upload_u8(&self.embd.raw)?;
1789        let mut gs = GraphDecodeState::new(e)?;
1790        gs.pos_d = e.htod_i32(&[cache.pos as i32])?;
1791        gs.token_d = e.stream().clone_htod(&[first_token])?;
1792        for kvl in cache.kv.iter_mut().flatten() {
1793            e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
1794        }
1795        let mask_dev = match mask_init {
1796            Some(w) => Some(e.htod_u32_v(w)?),
1797            None => None,
1798        };
1799        let mask_words = mask_init.map(|w| w.len()).unwrap_or(0);
1800        self.graph_session_capture(
1801            e, cache, gs, embd_gpu, max_new, qt, row_bytes, n_vocab, mask_dev, mask_words,
1802        )
1803    }
1804
1805    /// Eager fa kernel-class fingerprint at a given t_kv: the fa_vec pick plus the
1806    /// intra-vec variant switches (v4 max, fa512 floor) plus the split-ladder rung.
1807    /// fa_apply handles split-count changes WITHIN a rung; anything that changes this
1808    /// tuple needs a fresh capture (bucket_max drives the capture-time kernel pick).
1809    /// Round 45; LADDER RUNG ADDED 2026-08-02 (lane/ladder-3072): the dc kernels derive
1810    /// their in-kernel partition from the CAPTURED split_keys arg (ns_eff =
1811    /// ceil(T_kv/split_keys) — the ONE-PARTITION law), and fa_apply retunes only
1812    /// n_splits/grid. A capture whose segment straddled a ladder rung therefore replayed
1813    /// the far side's partition against eager's near side — same math, different FP fold
1814    /// order, and the first near-tie flips the stream (latent at the old 3072 rung: kat
1815    /// P=3000 passed on logit margins; exposed by the 512 rung: kat P=400 flipped 97/160).
1816    /// With the rung in the fingerprint a capture never straddles it, so the captured
1817    /// split_keys equals the live ladder on every replay — bit-exact at every t_kv.
1818    pub(crate) fn fa_class_of(&self, e: &Engine, t_kv: usize) -> (bool, bool, bool, usize) {
1819        let head_dim = self.cfg.head_dim_k as usize;
1820        let nkv = self.cfg.n_head_kv as usize;
1821        let g_fp8 = Engine::kv_fp8_on();
1822        (
1823            e.fa_geom_eager(t_kv, head_dim, nkv, g_fp8).0,
1824            crate::fa_v4_at_pub(t_kv),
1825            head_dim == 512 && t_kv >= crate::fa512_min_tkv(),
1826            crate::fa_split_keys_pub(t_kv, nkv),
1827        )
1828    }
1829
1830    /// Last t_kv (clamped to `final_max`) sharing `start`'s eager kernel class.
1831    pub(crate) fn fa_segment_end(&self, e: &Engine, start: usize, final_max: usize) -> usize {
1832        let cls = self.fa_class_of(e, start);
1833        let mut end = start;
1834        while end < final_max && self.fa_class_of(e, end + 1) == cls {
1835            end += 1;
1836        }
1837        end
1838    }
1839
1840    /// Capture one kernel-class segment: snapshot/rollback the warmup runs, capture the
1841    /// dc step at bucket_max = the segment's last t_kv, fa_plan. Shared by the session
1842    /// creation, the session's recapture-on-cross, and graph_decode_loop.
1843    #[allow(clippy::too_many_arguments)]
1844    pub(crate) fn graph_capture_segment(
1845        &self,
1846        e: &Engine,
1847        cache: &mut Cache,
1848        gs: &mut GraphDecodeState,
1849        embd_gpu: &CudaSlice<u8>,
1850        qt: i32,
1851        row_bytes: usize,
1852        n_vocab: usize,
1853        final_max: usize,
1854    ) -> Result<
1855        (
1856            cudarc::driver::CudaGraph,
1857            Vec<crate::graph_update::FaMain>,
1858            usize,
1859        ),
1860        Box<dyn std::error::Error>,
1861    > {
1862        self.graph_capture_segment_masked(
1863            e, cache, gs, embd_gpu, qt, row_bytes, n_vocab, final_max, None,
1864        )
1865    }
1866
1867    /// `graph_capture_segment` + optional in-graph grammar mask (see decode_step_dc_cap_masked).
1868    #[allow(clippy::too_many_arguments)]
1869    pub(crate) fn graph_capture_segment_masked(
1870        &self,
1871        e: &Engine,
1872        cache: &mut Cache,
1873        gs: &mut GraphDecodeState,
1874        embd_gpu: &CudaSlice<u8>,
1875        qt: i32,
1876        row_bytes: usize,
1877        n_vocab: usize,
1878        final_max: usize,
1879        mask: Option<(&CudaSlice<u32>, usize)>,
1880    ) -> Result<
1881        (
1882            cudarc::driver::CudaGraph,
1883            Vec<crate::graph_update::FaMain>,
1884            usize,
1885        ),
1886        Box<dyn std::error::Error>,
1887    > {
1888        let t0 = cache.pos + 1;
1889        let seg_end = self.fa_segment_end(e, t0, final_max);
1890        let bucket_max = seg_end;
1891        let snap = cache.snapshot(e)?;
1892        let pos_save = e.dtoh_i32_one(&gs.pos_d)?;
1893        let len_save: Vec<Option<i32>> = cache
1894            .kv
1895            .iter()
1896            .map(|k| k.as_ref().map(|kvl| e.dtoh_i32_one(&kvl.len_d).unwrap()))
1897            .collect();
1898        let tok_save = e.dtoh_u32_one(&gs.token_d)?;
1899        let graph = {
1900            let GraphDecodeState { token_d, pos_d, .. } = gs;
1901            let token_d: &mut CudaSlice<u32> = token_d;
1902            let pos_d: &mut CudaSlice<i32> = pos_d;
1903            let cache_ref = &mut *cache;
1904            e.capture_graph(|e| {
1905                self.decode_step_dc_cap_masked(
1906                    e, token_d, pos_d, embd_gpu, qt, row_bytes, cache_ref, n_vocab, bucket_max,
1907                    mask,
1908                )
1909            })?
1910        };
1911        gs.captures += 1;
1912        cache.rollback(e, &snap, 0)?;
1913        e.set_i32_one(&mut gs.pos_d, pos_save)?;
1914        for (il, ls) in len_save.iter().enumerate() {
1915            if let (Some(kvl), Some(v)) = (cache.kv[il].as_mut(), ls) {
1916                e.set_i32_one(&mut kvl.len_d, *v)?;
1917            }
1918        }
1919        e.set_u32_one(&mut gs.token_d, tok_save)?;
1920        let plan = crate::graph_update::fa_plan(&graph)?;
1921        if std::env::var("MEMRA_GRAPH_CENSUS").as_deref() == Ok("1") {
1922            eprintln!(
1923                "[graph-census] segment t_kv {t0}..={seg_end} fa_plan mains: {}",
1924                plan.len()
1925            );
1926            if let Ok(c) = crate::graph_update::node_census(&graph) {
1927                eprintln!("[graph-census] {c:?}");
1928            }
1929        }
1930        Ok((graph, plan, seg_end))
1931    }
1932
1933    /// Measurement door for `graph_session_recapture` (graph-allocfree-probe): the capture
1934    /// path timed WITHOUT the prompt prime. Same call the live step() makes at a
1935    /// kernel-class crossing.
1936    pub fn graph_session_recapture_pub(
1937        &self,
1938        e: &Engine,
1939        sess: &mut GraphSession,
1940    ) -> Result<(), Box<dyn std::error::Error>> {
1941        self.graph_session_recapture(e, sess)
1942    }
1943
1944    /// Session recapture at a kernel-class boundary (called by GraphSession::step).
1945    /// The mask node (when present) re-bakes the SAME stable buffer — contents carry over.
1946    pub(crate) fn graph_session_recapture(
1947        &self,
1948        e: &Engine,
1949        sess: &mut GraphSession,
1950    ) -> Result<(), Box<dyn std::error::Error>> {
1951        let mask = sess.mask_dev.take();
1952        let (graph, plan, seg_end) = self.graph_capture_segment_masked(
1953            e,
1954            &mut sess.cache,
1955            &mut sess.gs,
1956            &sess.embd_gpu,
1957            sess.qt,
1958            sess.row_bytes,
1959            sess.n_vocab,
1960            sess.bucket_max,
1961            mask.as_ref().map(|d| (d, sess.mask_words)),
1962        )?;
1963        sess.mask_dev = mask;
1964        sess.graph = graph;
1965        sess.plan = plan;
1966        sess.seg_end = seg_end;
1967        Ok(())
1968    }
1969
1970    /// Shared capture tail: capture the FIRST kernel-class segment, build the session.
1971    #[allow(clippy::too_many_arguments)]
1972    fn graph_session_capture(
1973        &self,
1974        e: &Engine,
1975        mut cache: Cache,
1976        mut gs: GraphDecodeState,
1977        embd_gpu_owned: CudaSlice<u8>,
1978        max_new: usize,
1979        qt: i32,
1980        row_bytes: usize,
1981        n_vocab: usize,
1982        mask_dev: Option<CudaSlice<u32>>,
1983        mask_words: usize,
1984    ) -> Result<(GraphSession, u32), Box<dyn std::error::Error>> {
1985        let embd_gpu = embd_gpu_owned;
1986        let bucket_max = cache.pos + max_new + 1;
1987        let (graph, plan, seg_end) = self.graph_capture_segment_masked(
1988            e,
1989            &mut cache,
1990            &mut gs,
1991            &embd_gpu,
1992            qt,
1993            row_bytes,
1994            n_vocab,
1995            bucket_max,
1996            mask_dev.as_ref().map(|d| (d, mask_words)),
1997        )?;
1998        let first = e.dtoh_u32_one(&gs.token_d)?;
1999        Ok((
2000            GraphSession {
2001                gs,
2002                cache,
2003                embd_gpu,
2004                graph,
2005                plan,
2006                bucket_max,
2007                seg_end,
2008                qt,
2009                row_bytes,
2010                n_vocab,
2011                mask_dev,
2012                mask_words,
2013            },
2014            first,
2015        ))
2016    }
2017
2018    /// Device-counter full-attention decode (CUDA-GRAPH-PLAN Phase 2): clone of `full_attn_decode`
2019    /// using the `_dc` KV-append (write slot from `kvl.len_d`) + `_dc` fa_decode (t_kv from `kvl.len_d`
2020    /// after inc), and the resident device rope `pos_d`. Bit-identical to `full_attn_decode` (the
2021    /// `_dc` kernels reproduce the same math; fa_decode_dc with bucket_max==t_kv reproduces the same
2022    /// n_splits/per/combine). Advances `kvl.len`/`kvl.len_d`.
2023    pub(crate) fn full_attn_decode_dc(
2024        &self,
2025        e: &Engine,
2026        fa: &FullAttnLayer,
2027        h: &CudaSlice<f32>,
2028        pos_d: &CudaSlice<i32>,
2029        cache: &mut Cache,
2030        il: usize,
2031    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2032        // eager-mirror path: advance host counters and size n_splits from the live t_kv (bit-identical
2033        // to fa_decode). The capture path uses full_attn_decode_dc_cap (fixed bucket_max, no host
2034        // advance, full-buffer K/V view).
2035        self.full_attn_decode_dc_inner(e, fa, h, None, pos_d, cache, il, None)
2036    }
2037
2038    /// PRE-QUANTIZED-INPUT dc full-attn (device-counter path). See full_attn_decode_pre. BIT-IDENTICAL.
2039    pub(crate) fn full_attn_decode_dc_pre(
2040        &self,
2041        e: &Engine,
2042        fa: &FullAttnLayer,
2043        h: &CudaSlice<f32>,
2044        hq: &CudaSlice<i8>,
2045        hd: &CudaSlice<f32>,
2046        pos_d: &CudaSlice<i32>,
2047        cache: &mut Cache,
2048        il: usize,
2049    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2050        self.full_attn_decode_dc_inner(e, fa, h, Some((hq, hd)), pos_d, cache, il, None)
2051    }
2052
2053    /// PRE-QUANTIZED-INPUT CAPTURE dc full-attn (graph path, fixed bucket_max). BIT-IDENTICAL.
2054    pub(crate) fn full_attn_decode_dc_cap_pre(
2055        &self,
2056        e: &Engine,
2057        fa: &FullAttnLayer,
2058        h: &CudaSlice<f32>,
2059        hq: &CudaSlice<i8>,
2060        hd: &CudaSlice<f32>,
2061        pos_d: &CudaSlice<i32>,
2062        cache: &mut Cache,
2063        il: usize,
2064        bucket_max: usize,
2065    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2066        self.full_attn_decode_dc_inner(e, fa, h, Some((hq, hd)), pos_d, cache, il, Some(bucket_max))
2067    }
2068
2069    /// CAPTURE variant of `full_attn_decode_dc` (CUDA-GRAPH-PLAN Phase 3). `bucket_max` sizes the
2070    /// fa_decode_dc grid (n_splits) at capture time; the kernel reads the ACTUAL t_kv from the device
2071    /// counter `kvl.len_d`. Does NOT advance the host `kvl.len` (only the DEVICE counter via inc_seqlen,
2072    /// which is captured and replays each launch). Views the FULL K/V cache buffer so the kernel may
2073    /// safely read up to any t_kv within the bucket on replay. Bit-identical to eager when
2074    /// `bucket_max` yields the same n_splits as eager for the replayed t_kv (the bucket-key contract).
2075    pub(crate) fn full_attn_decode_dc_cap(
2076        &self,
2077        e: &Engine,
2078        fa: &FullAttnLayer,
2079        h: &CudaSlice<f32>,
2080        pos_d: &CudaSlice<i32>,
2081        cache: &mut Cache,
2082        il: usize,
2083        bucket_max: usize,
2084    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2085        self.full_attn_decode_dc_inner(e, fa, h, None, pos_d, cache, il, Some(bucket_max))
2086    }
2087
2088    fn full_attn_decode_dc_inner(
2089        &self,
2090        e: &Engine,
2091        fa: &FullAttnLayer,
2092        h: &CudaSlice<f32>,
2093        pre_q: Option<(&CudaSlice<i8>, &CudaSlice<f32>)>,
2094        pos_d: &CudaSlice<i32>,
2095        cache: &mut Cache,
2096        il: usize,
2097        cap_bucket_max: Option<usize>,
2098    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2099        // step35 has no device-counter twin yet: the `_dc` family needs a windowed dc fa_decode
2100        // (SWA layers read a token-OFFSET view, which the dc kernels' len_d-derived t_kv cannot
2101        // express) plus a per-layer-n_head capture. Refuse loudly instead of silently running
2102        // the generic geometry. The eager arm (`step35_decode_attn`) is the supported decode.
2103        if self.cfg.step35.is_some() {
2104            return Err(
2105                "step35 has no device-counter/graph decode arm (SWA needs an offset KV \
2106                        view the dc kernels cannot express) — use the eager decode"
2107                    .into(),
2108            );
2109        }
2110        let cfg = &self.cfg;
2111        let geometry = cfg.full_attention_geometry_at(il as u32);
2112        let n_head = geometry.n_head as usize;
2113        let n_head_kv = geometry.n_head_kv as usize;
2114        let head_dim = geometry.head_dim_k as usize;
2115        let eps = cfg.rms_eps;
2116        let scale = geometry.attention_scale();
2117
2118        let n_embd = cfg.n_embd as usize;
2119        // Q8 TRUNK-FUSION (2026-07-05): wq+wk+wv share input h — on the 35B every full-attn
2120        // projection is Q8_0, so ONE fused3 launch (block-offset split, out_f 8192/512/512)
2121        // replaces three launch-latency-class m=1 launches. BIT-IDENTICAL per (tensor,row) to
2122        // the three matmul_pre MMVQ dispatches (same kernel body). MEMRA_Q8_DUAL=0 rollback.
2123        let qkv_fused = |e: &Engine,
2124                         hq: &CudaSlice<i8>,
2125                         hd: &CudaSlice<f32>|
2126         -> Result<
2127            (CudaSlice<f32>, CudaSlice<f32>, CudaSlice<f32>),
2128            Box<dyn std::error::Error>,
2129        > {
2130            if let Some((qf, k, v)) = e.matmul_q8_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hd)? {
2131                return Ok((qf, k, v));
2132            }
2133            Ok((
2134                e.matmul_pre(&fa.wq, hq, hd, h, 1)?,
2135                e.matmul_pre(&fa.wk, hq, hd, h, 1)?,
2136                e.matmul_pre(&fa.wv, hq, hd, h, 1)?,
2137            ))
2138        };
2139        let (qf, mut k, v) =
2140            if e.uses_q8_1_fast(&fa.wq) && e.uses_q8_1_fast(&fa.wk) && e.uses_q8_1_fast(&fa.wv) {
2141                match pre_q {
2142                    Some((hq, hd)) => qkv_fused(e, hq, hd)?,
2143                    None => {
2144                        let (hq, hd) = e.quantize_q8_1(h, 1, n_embd)?;
2145                        qkv_fused(e, &hq, &hd)?
2146                    }
2147                }
2148            } else {
2149                (
2150                    e.matmul(&fa.wq, h, 1)?,
2151                    e.matmul(&fa.wk, h, 1)?,
2152                    e.matmul(&fa.wv, h, 1)?,
2153                )
2154            };
2155        // M3/Hy3 have no attention output gate — wq out is exactly q; skip the split.
2156        let gated = geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ;
2157        let (mut q, gate) = if gated {
2158            let mut q = e.uninit(n_head * head_dim)?;
2159            let mut gate = e.uninit(n_head * head_dim)?;
2160            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, 1)?;
2161            (q, Some(gate))
2162        } else {
2163            (qf, None)
2164        };
2165
2166        let mut qn = e.uninit(n_head * head_dim)?;
2167        e.rms_norm(&q, fa.q_norm.float_data(), &mut qn, head_dim, n_head, eps)?;
2168        q = qn;
2169        let mut kn = e.uninit(n_head_kv * head_dim)?;
2170        e.rms_norm(
2171            &k,
2172            fa.k_norm.float_data(),
2173            &mut kn,
2174            head_dim,
2175            n_head_kv,
2176            eps,
2177        )?;
2178        k = kn;
2179        let rope_dims = geometry.n_rot as usize;
2180        // rope pos from the resident device counter (no per-step host upload).
2181        e.rope_neox(
2182            &mut q,
2183            pos_d,
2184            head_dim,
2185            rope_dims,
2186            n_head,
2187            1,
2188            geometry.rope_base,
2189            1.0,
2190        )?;
2191        e.rope_neox(
2192            &mut k,
2193            pos_d,
2194            head_dim,
2195            rope_dims,
2196            n_head_kv,
2197            1,
2198            geometry.rope_base,
2199            1.0,
2200        )?;
2201
2202        let kvl = cache.kv[il].as_mut().unwrap();
2203        // (1) append at the device write slot kvl.len_d (== old len).
2204        e.append_kv_quantized_dc(
2205            &k,
2206            &v,
2207            &mut kvl.k,
2208            &mut kvl.v,
2209            &kvl.len_d,
2210            kvl.kv_dim_k,
2211            kvl.kv_dim_v,
2212            kvl.k_tok_bytes,
2213            kvl.v_tok_bytes,
2214            crate::Engine::kv_fp8_on(),
2215        )?;
2216        // (2) advance the device counter: kvl.len_d now holds new len == t_kv.
2217        e.inc_seqlen(&mut kvl.len_d)?;
2218        // n_splits sizing + K/V view extent:
2219        //  - eager path (cap_bucket_max==None): advance host len; size from live t_kv == bit-identical
2220        //    to fa_decode; view exactly t_kv*tok_bytes.
2221        //  - capture path (Some(bucket_max)): DO NOT touch host len (replay advances only the device
2222        //    counter); size n_splits from bucket_max; view the FULL cache buffer so any in-bucket t_kv
2223        //    is in range on replay.
2224        let (bucket_max, k_view, v_view) = match cap_bucket_max {
2225            None => {
2226                kvl.len += 1;
2227                let t_kv = kvl.len;
2228                (
2229                    t_kv,
2230                    e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes),
2231                    e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes),
2232                )
2233            }
2234            Some(bm) => (
2235                bm,
2236                e.view_u8(&kvl.k, kvl.k.len()),
2237                e.view_u8(&kvl.v, kvl.v.len()),
2238            ),
2239        };
2240        let (ktb, vtb) = (kvl.k_tok_bytes, kvl.v_tok_bytes);
2241        let mut attn = e.uninit(n_head * head_dim)?;
2242        if std::env::var("MEMRA_NOFA").is_ok() {
2243            return Err(
2244                "MEMRA_NOFA (naive f32 SDPA) is incompatible with the quantized KV cache; \
2245                        unset MEMRA_NOFA to use fa_decode_dc"
2246                    .into(),
2247            );
2248        }
2249        // (3) fa_decode reads t_kv from kvl.len_d; bucket_max yields the eager n_splits -> bit-identical.
2250        e.fa_decode_dc(
2251            &q,
2252            &k_view,
2253            &v_view,
2254            &mut attn,
2255            head_dim,
2256            n_head,
2257            n_head_kv,
2258            &kvl.len_d,
2259            bucket_max,
2260            scale,
2261            ktb,
2262            vtb,
2263            crate::Engine::kv_fp8_on(),
2264        )?;
2265
2266        let attn_g = match &gate {
2267            Some(gate) => {
2268                let mut gsig = e.uninit(n_head * head_dim)?;
2269                e.sigmoid(gate, &mut gsig, n_head * head_dim)?;
2270                let mut ag = e.uninit(n_head * head_dim)?;
2271                e.mul(&attn, &gsig, &mut ag, n_head * head_dim)?;
2272                ag
2273            }
2274            None => attn,
2275        };
2276        Ok(e.matmul(&fa.wo, &attn_g, 1)?)
2277    }
2278
2279    /// Greedy generation: prime with prompt tokens (decode them in sequence to build state),
2280    /// then generate `max_new` tokens. Returns the generated token ids. (Back-compat: greedy,
2281    /// no EOS/stop — used by the decode==prefill validation gate. New code uses `generate_with`.)
2282    pub fn generate(
2283        &self,
2284        e: &Engine,
2285        prompt: &[u32],
2286        max_new: usize,
2287    ) -> Result<Vec<u32>, Box<dyn std::error::Error>> {
2288        let max_ctx = prompt.len() + max_new + 8;
2289        let mut cache = Cache::new(e, &self.cfg, max_ctx)?;
2290        let mut last_logits = Vec::new();
2291        // prime: BATCHED cache prime (prime_cache — the prefill-throughput path, the measured #1
2292        // e2e gap: tokenwise primed at ~102/38 tok/s vs ~2000-5900 tok/s batched). Prompts below
2293        // PRIME_MIN_T, MEMRA_PRIME_TOKENWISE=1, and frozen Hy3 CPU/GPU expert splits take the
2294        // tokenwise loop. Frozen mixed residency would otherwise transiently stage the missing
2295        // expert bank through the GPU on every prompt replay.
2296        let t_prime = std::time::Instant::now();
2297        let batched_prime = prompt.len() >= crate::hybrid_forward::PRIME_MIN_T
2298            && std::env::var("MEMRA_PRIME_TOKENWISE").is_err()
2299            && !e.frozen_cpu_experts_prefer_tokenwise_prime();
2300        if batched_prime {
2301            let (l, _h_seed, _hiddens) = self.prime_cache(e, prompt, &mut cache, 0)?;
2302            last_logits = l;
2303        } else {
2304            for &tok in prompt {
2305                last_logits = self.decode_step(e, tok, &mut cache)?;
2306            }
2307        }
2308        e.stream().synchronize()?;
2309        // Harness timing contract: prime wall time published for gen-only throughput math
2310        // (bench binaries read this right after the call; subtraction-from-total breaks down
2311        // when prime >> gen — measured ±80% error at 6k-token prompts).
2312        crate::PRIME_NANOS.store(
2313            t_prime.elapsed().as_nanos() as u64,
2314            std::sync::atomic::Ordering::Relaxed,
2315        );
2316        let mut out = Vec::with_capacity(max_new);
2317        if self.cfg.gemma4.is_some()
2318            && let Some(embd_gpu) = self.embd_gpu_try(e)
2319        {
2320            // Graph serving probed FLAT vs this dc loop (2026-07-12, 1.7k N=2: 174.6/174.2 vs
2321            // 174.5/174.3) — the GRAPH-GATE's +2.5% is over the plain-eager loop, and the dc
2322            // arc already banked that; the gate (IDENTICAL at every ctx since the wkv
2323            // capture-arm fix) stays as the correctness harness.
2324            // DEVICE-COUNTER greedy loop (the dc arc): stream-identical to eager (DC-GATE).
2325            // E4B rides its own dc step (same trunk fns as its eager chain).
2326            let n_vocab = self.output.out_features();
2327            let (qt, rb) = self.embd.qt_and_row_bytes(self.cfg.n_embd as usize);
2328            for kvl in cache.kv.iter_mut().flatten() {
2329                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
2330            }
2331            let e4b = self.is_gemma4_e4b();
2332            // 26B/31B WHOLE-TOKEN GRAPH SERVING door (MEMRA_GEMMA_GRAPH=1): measured FLAT on
2333            // the 26B (jsonl 2026-07-12) but the 31B carries ~4% launch-gap share (HANDOVER
2334            // graph-arc note) and was never measured — the plain-short 1.00x cell probe.
2335            if !e4b && std::env::var("MEMRA_GEMMA_GRAPH").as_deref() == Ok("1") {
2336                let first = argmax(&last_logits) as u32;
2337                let (toks, _reason) = self.gemma4_generate_graph(
2338                    e,
2339                    cache.pos,
2340                    first,
2341                    &mut cache,
2342                    max_new,
2343                    &[],
2344                    |_| true,
2345                )?;
2346                out.extend(toks);
2347                return Ok(out);
2348            }
2349            let mut token_d = e.stream().clone_htod(&[argmax(&last_logits) as u32])?;
2350            let mut pos_d = e.htod_i32(&[cache.pos as i32])?;
2351            // E4B GRAPH-EXEC-UPDATE SERVING: one capture at bucket=win, per-token fa
2352            // geometry retune, replay. The 2026-07-12 park ("flat 173.5, stream 64/64") did
2353            // NOT reproduce — the capture warmups are real self-feeding steps and the old
2354            // door dropped their 2 tokens (E4B-GRAPH-GATE 3/64). Snapshot/rollback (the 26B
2355            // graph-loop pattern) fixes the stream; the exec-update kills the bucket-split
2356            // tax (42 fa launches at 64 splits vs eager's ~ceil(t_kv/8)).
2357            // DEFAULT: budget-gated ON (2026-07-13 valid-window A/B: steady-state replay
2358            // beats eager but the one-time capture ~30ms crosses over near 200 tokens —
2359            // 128tok −1.3%, 400tok +0.9%). MEMRA_E4B_GRAPH=1 forces, =0 kills.
2360            let win = self
2361                .cfg
2362                .gemma4
2363                .as_ref()
2364                .map(|g| g.sliding_window as usize)
2365                .unwrap_or(0);
2366            let e4b_graph = match std::env::var("MEMRA_E4B_GRAPH").as_deref() {
2367                Ok("1") => true,
2368                Ok("0") => false,
2369                _ => max_new >= 256,
2370            };
2371            if e4b && cache.pos + max_new + 2 < win && e4b_graph {
2372                self.gemma4_e4b_graph_exec_loop(
2373                    e,
2374                    &mut cache,
2375                    &mut token_d,
2376                    &mut pos_d,
2377                    embd_gpu,
2378                    qt,
2379                    rb,
2380                    n_vocab,
2381                    win,
2382                    max_new,
2383                    usize::MAX,
2384                    |tok| {
2385                        out.push(tok);
2386                        None
2387                    },
2388                )?;
2389                return Ok(out);
2390            }
2391            for _ in 0..max_new {
2392                out.push(e.dtoh_u32(&token_d)?[0]);
2393                token_d = if e4b {
2394                    self.gemma4_e4b_decode_step_dc(
2395                        e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab,
2396                    )?
2397                } else {
2398                    self.gemma4_decode_step_dc(
2399                        e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab, None,
2400                    )?
2401                };
2402            }
2403            return Ok(out);
2404        }
2405        // QWEN DC-EAGER route (2026-07-15, MEMRA_QWEN_DC=0 seam — mirror of generate_with's
2406        // serving loop; see the note there. The graph route probed −11% first.)
2407        // step35 is EXCLUDED: this route calls `decode_step_dc`, whose full-attn arm refuses
2408        // step35 by design (SWA layers need a token-OFFSET KV view the dc kernels' len_d-derived
2409        // t_kv cannot express). Without this gate the door opens for any greedy model and the
2410        // refusal surfaces as a user-visible generate() error — the first PP-2 boot of
2411        // Step-3.7-Flash died exactly there, AFTER a clean load and an argmax MATCH.
2412        static QWEN_DC2: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2413        let qwen_dc =
2414            *QWEN_DC2.get_or_init(|| std::env::var("MEMRA_QWEN_DC").as_deref() != Ok("0"));
2415        if qwen_dc
2416            && max_new > 0
2417            && self.cfg.step35.is_none()
2418            && let Some(embd_gpu) = self.embd_gpu_try(e)
2419        {
2420            let n_vocab = self.output.out_features();
2421            let (qt, rb) = self.embd.qt_and_row_bytes(self.cfg.n_embd as usize);
2422            for kvl in cache.kv.iter_mut().flatten() {
2423                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
2424            }
2425            let mut pos_d = e.htod_i32(&[cache.pos as i32])?;
2426            let mut token_d = e.stream().clone_htod(&[argmax(&last_logits) as u32])?;
2427            for _ in 0..max_new {
2428                out.push(e.dtoh_u32(&token_d)?[0]);
2429                token_d = self.decode_step_dc(
2430                    e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab,
2431                )?;
2432            }
2433            return Ok(out);
2434        }
2435        for _ in 0..max_new {
2436            let next = argmax(&last_logits) as u32;
2437            out.push(next);
2438            last_logits = self.decode_step(e, next, &mut cache)?;
2439        }
2440        Ok(out)
2441    }
2442
2443    /// E4B whole-token GRAPH-EXEC-UPDATE serving loop (shared by `generate` and
2444    /// `generate_with`): capture ONE self-feeding dcg step at bucket=`win`, then per token
2445    /// retune the fa nodes' split geometry to the live eager counts
2446    /// (`graph_update::fa_apply`) before replaying the instantiated exec.
2447    ///
2448    /// The capture's two warmup runs are REAL executions (self-feeding: they consume two
2449    /// tokens and advance KV/counters) — snapshot/rollback around the capture (the 26B
2450    /// graph-loop pattern) restores device+host state, or the stream drops those tokens
2451    /// (E4B-GRAPH-GATE 3/64 break, 2026-07-12). `emit` sees each token BEFORE its
2452    /// successor's replay; returning `Some(reason)` stops the loop. Caller owns the
2453    /// under-window gate (`cache.pos + budget + 2 < win`).
2454    #[allow(clippy::too_many_arguments)]
2455    fn gemma4_e4b_graph_exec_loop(
2456        &self,
2457        e: &Engine,
2458        cache: &mut Cache,
2459        token_d: &mut CudaSlice<u32>,
2460        pos_d: &mut CudaSlice<i32>,
2461        embd_gpu: &CudaSlice<u8>,
2462        qt: i32,
2463        rb: usize,
2464        n_vocab: usize,
2465        win: usize,
2466        budget: usize,
2467        ctx_cap: usize,
2468        mut emit: impl FnMut(u32) -> Option<StopReason>,
2469    ) -> Result<StopReason, Box<dyn std::error::Error>> {
2470        // BISECT ARM (MEMRA_E4B_DCG_EAGER=1): run the dcg step EAGERLY per token at the
2471        // exact live bucket — no capture/replay/exec-update. Separates "the dc-bucket path
2472        // diverges from dc-eager numerically" from "the replay/update mechanism is wrong".
2473        if let Ok(m) = std::env::var("MEMRA_E4B_DCG_EAGER") {
2474            // =1: exact live bucket per token; =2: the capture's fixed win bucket.
2475            let mut reason = StopReason::MaxNew;
2476            for _ in 0..budget {
2477                let tok = e.dtoh_u32_one(token_d)?;
2478                if let Some(r) = emit(tok) {
2479                    reason = r;
2480                    break;
2481                }
2482                if cache.pos >= ctx_cap {
2483                    reason = StopReason::ContextFull;
2484                    break;
2485                }
2486                let b = if m == "2" { win } else { cache.pos + 1 };
2487                self.gemma4_e4b_decode_step_dcg(
2488                    e, token_d, pos_d, embd_gpu, qt, rb, cache, n_vocab, b,
2489                )?;
2490                cache.pos += 1;
2491                for kvl in cache.kv.iter_mut().flatten() {
2492                    kvl.len += 1;
2493                }
2494            }
2495            return Ok(reason);
2496        }
2497        // snapshot device+host state (the 2 capture-warmup runs must leave no residue).
2498        let snap = cache.snapshot(e)?;
2499        let pos_save = e.dtoh_i32_one(pos_d)?;
2500        let len_save: Vec<Option<i32>> = cache
2501            .kv
2502            .iter()
2503            .map(|k| k.as_ref().map(|kvl| e.dtoh_i32_one(&kvl.len_d).unwrap()))
2504            .collect();
2505        let tok_save = e.dtoh_u32_one(token_d)?;
2506        let (graph, keeper) = e.capture_graph_retained(|e| {
2507            self.gemma4_e4b_decode_step_dcg(
2508                e, token_d, pos_d, embd_gpu, qt, rb, cache, n_vocab, win,
2509            )
2510        })?;
2511        cache.rollback(e, &snap, 0)?;
2512        e.set_i32_one(pos_d, pos_save)?;
2513        for (il, ls) in len_save.iter().enumerate() {
2514            if let (Some(kvl), Some(v)) = (cache.kv[il].as_mut(), ls) {
2515                e.set_i32_one(&mut kvl.len_d, *v)?;
2516            }
2517        }
2518        e.set_u32_one(token_d, tok_save)?;
2519        let mut plan = crate::graph_update::fa_plan(&graph)?;
2520        if std::env::var("MEMRA_GRAPH_NODES_DUMP").as_deref() == Ok("1") {
2521            let nodes = crate::graph_update::kernel_nodes(&graph)?;
2522            let mut counts: std::collections::BTreeMap<String, (usize, (u32, u32, u32))> =
2523                std::collections::BTreeMap::new();
2524            for n in &nodes {
2525                counts
2526                    .entry(n.name.clone())
2527                    .or_insert((0, (n.params.gridDimX, n.params.gridDimY, n.params.gridDimZ)))
2528                    .0 += 1;
2529            }
2530            eprintln!(
2531                "[graph-nodes] {} kernel nodes, {} fa update units (bucket={win})",
2532                nodes.len(),
2533                plan.len()
2534            );
2535            for (name, (c, grid)) in &counts {
2536                eprintln!("[graph-nodes]   {c:4}x {name} grid={grid:?}");
2537            }
2538        }
2539        let mut reason = StopReason::MaxNew;
2540        let timing = std::env::var("MEMRA_E4B_GRAPH_TIMING").as_deref() == Ok("1");
2541        let (mut t_dtoh, mut t_apply, mut t_launch) = (
2542            std::time::Duration::ZERO,
2543            std::time::Duration::ZERO,
2544            std::time::Duration::ZERO,
2545        );
2546        for _ in 0..budget {
2547            let t0 = std::time::Instant::now();
2548            let tok = e.dtoh_u32_one(token_d)?;
2549            let t1 = std::time::Instant::now();
2550            if let Some(r) = emit(tok) {
2551                reason = r;
2552                break;
2553            }
2554            if cache.pos >= ctx_cap {
2555                reason = StopReason::ContextFull;
2556                break;
2557            }
2558            // live t_kv AFTER this replay's in-graph append = pos + 1.
2559            crate::graph_update::fa_apply(&graph, &mut plan, cache.pos + 1, crate::fa_split_keys)?;
2560            let t2 = std::time::Instant::now();
2561            graph.launch()?;
2562            if timing {
2563                let t3 = std::time::Instant::now();
2564                t_dtoh += t1 - t0;
2565                t_apply += t2 - t1;
2566                t_launch += t3 - t2;
2567            }
2568            cache.pos += 1;
2569            for kvl in cache.kv.iter_mut().flatten() {
2570                kvl.len += 1;
2571            }
2572        }
2573        if timing {
2574            eprintln!(
2575                "[e4b-graph timing] dtoh(sync-wait) {:?} apply {:?} launch {:?}",
2576                t_dtoh, t_apply, t_launch
2577            );
2578        }
2579        drop(keeper); // capture-retained transients must outlive every replay
2580        Ok(reason)
2581    }
2582
2583    /// The reusable serving generation API (BASE-3). Primes the prompt, then samples up to
2584    /// `params.max_new` tokens, stopping on EOS, any stop-token, or the context-length guard.
2585    /// Calls `on_token(id)` after each emitted token (for streaming; return `false` to stop early).
2586    /// Returns `GenOutput { tokens, stop_reason }`. Does NOT detokenize — the caller (which owns
2587    /// the tokenizer) handles text + stop-STRING matching on the detokenized tail.
2588    pub fn generate_with<F: FnMut(u32) -> bool>(
2589        &self,
2590        e: &Engine,
2591        prompt: &[u32],
2592        params: &GenParams,
2593        sampler: &mut crate::sampler::Sampler,
2594        mut on_token: F,
2595    ) -> Result<GenOutput, Box<dyn std::error::Error>> {
2596        // Context guard: prompt + generated must fit max_ctx (caller-supplied or model default).
2597        let ctx_cap = params.max_ctx.unwrap_or(prompt.len() + params.max_new + 8);
2598        if prompt.len() >= ctx_cap {
2599            return Ok(GenOutput {
2600                tokens: Vec::new(),
2601                stop_reason: StopReason::ContextFull,
2602            });
2603        }
2604        let room = ctx_cap - prompt.len();
2605        let budget = params.max_new.min(room);
2606
2607        let mut cache = Cache::new(e, &self.cfg, ctx_cap)?;
2608        let mut last_logits = Vec::new();
2609        // BATCHED PRIME (2026-07-06 fix — generate_with was still tokenwise! run-gen's "decode"
2610        // numbers folded a ~40-100 tok/s tokenwise prime into the rate) + PRIME_NANOS contract.
2611        // Frozen Hy3 CPU/GPU expert serving is the deliberate exception: its batched MoE path
2612        // bypasses the CPU tier and rereads the spilled expert bank.
2613        let t_prime = std::time::Instant::now();
2614        let batched = prompt.len() >= crate::hybrid_forward::PRIME_MIN_T
2615            && std::env::var("MEMRA_PRIME_TOKENWISE").is_err()
2616            && !e.frozen_cpu_experts_prefer_tokenwise_prime();
2617        if batched {
2618            let (l, _h, _x) = self.prime_cache(e, prompt, &mut cache, 0)?;
2619            last_logits = l;
2620            for &tok in prompt {
2621                sampler.accept(tok);
2622            }
2623        } else {
2624            for &tok in prompt {
2625                last_logits = self.decode_step(e, tok, &mut cache)?;
2626                sampler.accept(tok);
2627            }
2628        }
2629        e.stream().synchronize()?;
2630        crate::PRIME_NANOS.store(
2631            t_prime.elapsed().as_nanos() as u64,
2632            std::sync::atomic::Ordering::Relaxed,
2633        );
2634        // MEMRA_PROFILE_GEN=2: profiler capture starts HERE — after the prime — so an
2635        // `nsys -c cudaProfilerApi` capture contains ONLY the decode loop (the run-spec
2636        // MEMRA_PROFILE_SPEC=2 pattern; =1 in run_gen brackets prime+decode).
2637        if std::env::var("MEMRA_PROFILE_GEN").as_deref() == Ok("2") {
2638            unsafe extern "C" {
2639                fn cudaProfilerStart() -> i32;
2640            }
2641            unsafe {
2642                cudaProfilerStart();
2643            }
2644        }
2645        let mut out = Vec::with_capacity(budget);
2646        let mut reason = StopReason::MaxNew;
2647        // gemma4 DEVICE-COUNTER greedy serving loop (the dc arc): token/pos/kv-lens live in
2648        // device counters, argmax on device — host sees 4B/token. Stream-identical to the
2649        // eager chain (DC-GATE). Penalties/temp fall through to the host-logits loop.
2650        if self.cfg.gemma4.is_some()
2651            && sampler.is_greedy()
2652            && sampler.penalty_last_n() == 0
2653            && let Some(embd_gpu) = self.embd_gpu_try(e)
2654        {
2655            let n_vocab = self.output.out_features();
2656            let (qt, rb) = self.embd.qt_and_row_bytes(self.cfg.n_embd as usize);
2657            for kvl in cache.kv.iter_mut().flatten() {
2658                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
2659            }
2660            let first = crate::forward::argmax(&last_logits) as u32;
2661            let e4b = self.is_gemma4_e4b();
2662            let mut token_d = e.stream().clone_htod(&[first])?;
2663            let mut pos_d = e.htod_i32(&[cache.pos as i32])?;
2664            // E4B GRAPH-EXEC-UPDATE serving door (under-window regime) — mirror of the
2665            // `generate` door incl the budget-gated default; run-gen/serving measure here.
2666            let win = self
2667                .cfg
2668                .gemma4
2669                .as_ref()
2670                .map(|g| g.sliding_window as usize)
2671                .unwrap_or(0);
2672            let e4b_graph = match std::env::var("MEMRA_E4B_GRAPH").as_deref() {
2673                Ok("1") => true,
2674                Ok("0") => false,
2675                _ => budget >= 256,
2676            };
2677            if e4b && cache.pos + budget + 2 < win && e4b_graph {
2678                let (out_cell, sampler_cell) = (&mut out, &mut *sampler);
2679                let reason = self.gemma4_e4b_graph_exec_loop(
2680                    e,
2681                    &mut cache,
2682                    &mut token_d,
2683                    &mut pos_d,
2684                    embd_gpu,
2685                    qt,
2686                    rb,
2687                    n_vocab,
2688                    win,
2689                    budget,
2690                    ctx_cap,
2691                    |tok| {
2692                        sampler_cell.accept(tok);
2693                        out_cell.push(tok);
2694                        if params.eos.contains(&tok) {
2695                            return Some(StopReason::Eos);
2696                        }
2697                        if !on_token(tok) {
2698                            return Some(StopReason::Callback);
2699                        }
2700                        None
2701                    },
2702                )?;
2703                return Ok(GenOutput {
2704                    tokens: out,
2705                    stop_reason: reason,
2706                });
2707            }
2708            // 12B/31B WHOLE-TOKEN GRAPH door (MEMRA_GEMMA_GRAPH=1), mirrored from `generate`:
2709            // run-gen/serving measure THIS path, and the `generate` door never covered it —
2710            // the 2026-07-22 graph A/B read flat because the env engaged nothing here.
2711            if !e4b && std::env::var("MEMRA_GEMMA_GRAPH").as_deref() == Ok("1") {
2712                let (out_cell, sampler_cell) = (&mut out, &mut *sampler);
2713                let eos = params.eos.clone();
2714                let (toks, greason) = self.gemma4_generate_graph(
2715                    e,
2716                    cache.pos,
2717                    first,
2718                    &mut cache,
2719                    budget,
2720                    &eos,
2721                    |tok| {
2722                        sampler_cell.accept(tok);
2723                        out_cell.push(tok);
2724                        on_token(tok)
2725                    },
2726                )?;
2727                let _ = toks;
2728                return Ok(GenOutput {
2729                    tokens: out,
2730                    stop_reason: greason,
2731                });
2732            }
2733            let mut next = first;
2734            for _ in 0..budget {
2735                sampler.accept(next);
2736                out.push(next);
2737                if params.eos.contains(&next) {
2738                    reason = StopReason::Eos;
2739                    break;
2740                }
2741                if !on_token(next) {
2742                    reason = StopReason::Callback;
2743                    break;
2744                }
2745                if cache.pos >= ctx_cap {
2746                    reason = StopReason::ContextFull;
2747                    break;
2748                }
2749                token_d = if e4b {
2750                    self.gemma4_e4b_decode_step_dc(
2751                        e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab,
2752                    )?
2753                } else {
2754                    self.gemma4_decode_step_dc(
2755                        e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab, None,
2756                    )?
2757                };
2758                next = e.dtoh_u32(&token_d)?[0];
2759            }
2760            return Ok(GenOutput {
2761                tokens: out,
2762                stop_reason: reason,
2763            });
2764        }
2765        // QWEN DC-EAGER serving loop (2026-07-15, MEMRA_QWEN_DC=0 seam — the gemma dc-arc
2766        // pattern): the eager tail dtoh'd the FULL VOCAB logits + host-argmax'd every
2767        // token (the duty map's 10.3%-of-wall gap at 13% DRAM duty). decode_step_dc keeps
2768        // the token id + argmax device-resident — 4B/token host traffic, same tuned eager
2769        // kernels. Greedy + no-penalty only (sampling needs host logits).
2770        // (The CUDA-graph route was probed first and read −11%: the replay's dc-fa family
2771        // + capture rungs lag the tuned eager lanes; jsonl 2026-07-15.)
2772        // step35 is EXCLUDED here for the same reason as the `generate` mirror above: every route
2773        // inside this door (`decode_step_dc` and the `graph_decode_loop` capture) reaches
2774        // `full_attn_decode_dc_inner`, which refuses step35 because its SWA layers read a
2775        // token-OFFSET KV view the dc kernels cannot express. step35 takes the host-logits eager
2776        // loop at the bottom of this function (`decode_step` -> `step35_decode_attn`), which is
2777        // the supported decode for this arch. Removing this gate requires a windowed dc fa_decode
2778        // plus a per-layer-n_head capture, not a flag.
2779        static QWEN_DC: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2780        let qwen_dc = *QWEN_DC.get_or_init(|| std::env::var("MEMRA_QWEN_DC").as_deref() != Ok("0"));
2781        if qwen_dc
2782            && sampler.is_greedy()
2783            && sampler.penalty_last_n() == 0
2784            && budget > 0
2785            && self.cfg.step35.is_none()
2786            && let Some(embd_gpu) = self.embd_gpu_try(e)
2787        {
2788            let n_vocab = self.output.out_features();
2789            let (qt, rb) = self.embd.qt_and_row_bytes(self.cfg.n_embd as usize);
2790            for kvl in cache.kv.iter_mut().flatten() {
2791                e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
2792            }
2793            let mut pos_d = e.htod_i32(&[cache.pos as i32])?;
2794            let mut token_d = e
2795                .stream()
2796                .clone_htod(&[crate::forward::argmax(&last_logits) as u32])?;
2797            // HYBRID GRAPH DOOR (round 35): graph_decode_loop over the batched-prime
2798            // cache — the E4B graph-exec door's hybrid mirror. Counters (pos_d/token_d/
2799            // len_d) synced above; event tracking is engine-default-OFF so capture over
2800            // these buffers is legal. PROMOTED default-ON at budget >= 256 (the E4B
2801            // door's amortization rule): official-shape A/B interleaved x5 = eager 190.3
2802            // -> graph 220.7 tok/s (+16.0%, 5/5, spread ±0.1); 128-tok stream IDENTICAL;
2803            // graph-decode-gate 256 steps x 16 buckets BIT-IDENTICAL. This REFUTES the
2804            // 2026-07-15 "-11%" qwen-graph verdict — it predated the exec-update rework
2805            // and the 07-26 FA family (stale-verdict law, round 35). =0 reverts.
2806            // Default ON at budget >= 256 on BOTH arches (unified-merge resolution,
2807            // 2026-07-30): main shipped this door budget-keyed on sm_120a (52222ddd,
2808            // E4B graph door) and every 5090 board row since measured with it; the H100
2809            // lane measured +16% x5. The branch-era arch-gate (79395a3e) cited the
2810            // stale 2026-07-15 "-11%" verdict, which predates main's promotion — the
2811            // rig-divergence law protects main's SHIPPED default, so the gate came off.
2812            // MEMRA_GEN_GRAPH=1 opts in anywhere; =0 reverts anywhere.
2813            //
2814            // KEY LOWERED 256 -> 48 (q27 deep dive, 2026-08-05, pro6000wk-runpod-community).
2815            // The 256 key was set by the E4B amortization rule, never by a measured crossover,
2816            // so every <=128-token generation — including the whole published board, which runs
2817            // --max-tokens 128 — was silently EAGER. Swept the actual crossover on TWO models
2818            // (the key is a cross-model default, so one artifact is not enough), interleaved
2819            // arms with the order alternated per rep, N=3, all runs argmax MATCH:
2820            //   Qwen3.6-27B-Q8_0     : n=16 -7.47% | n=32 -1.35% | n=48 +0.90% | n=64 +1.93%
2821            //                          n=128 +3.80% | n=512 +5.50%
2822            //   Qwen3.6-27B-NVFP4-MTP: n=16 -15.27% | n=32 +0.22% | n=48 +3.45%
2823            //                          n=64 +5.09% | n=128 +7.72%
2824            // Both models: clearly negative at 16, no reliable gain at 32, positive from 48 up,
2825            // monotone in budget from 48 on. 48 is the first budget where BOTH are positive, so
2826            // it is the key — the capture cost needs ~32 steps to amortize, not ~256. The n=32
2827            // nvfp4 cell is NOISY, not flat (graph arm 79.02/78.91/77.09, spread 1.93 vs an
2828            // eager spread of 0.04): it is not evidence of a win, and it is why the key sits at
2829            // 48 rather than 32. Exactness at the new key:
2830            // graph-decode-gate 256 steps BIT-IDENTICAL (buckets=16, captures=2),
2831            // graph-session-gate 96 tokens PASS, kernel-check ALL GREEN, run-spec K=1..8
2832            // self-consistency PASS. Board caveat: community board, RELATIVE deltas only.
2833            //
2834            // SM-GATED (5090-arbiter gate, 2026-08-05, research/q27-deepdive-20260805/local5090/):
2835            // the 48 key does NOT transfer to the 82-SM local rig. Same A/B protocol there
2836            // (tg128 d512, N=3 interleaved, order alternated, warmup discarded): q27-NVFP4-MTP
2837            // graph arm at n=128 = -1.61% (eager 45.86 / graph 45.12 median, 3/3 pairs lose),
2838            // and the crossover sweep stays negative through n=256 (-1.07%) and n=512 (-0.59%)
2839            // — on few-SM silicon the replay's fixed kernel forms lag the tuned eager lanes and
2840            // the launch-gap tax the graph amortizes is proportionally smaller. Key on SM count
2841            // (the fa_split_keys big_rig pattern, lib.rs fa_sm_count), threshold 180: the 48
2842            // crossover is MEASURED only at 188 SM (PRO 6000) and refuted at 82 SM; the 132-SM
2843            // H100 board and the 170-SM desktop 5090 are UNMEASURED at sub-256 budgets, so they
2844            // keep the shipped 256 key their board rows were measured with (rig-divergence +
2845            // stale-verdict laws). Widening the gate below 180 requires an on-box crossover
2846            // sweep on that silicon, not an inference from this comment.
2847            let big_rig = e.sm_count() >= 180;
2848            let gen_graph = match std::env::var("MEMRA_GEN_GRAPH").as_deref() {
2849                Ok("1") => true,
2850                Ok("0") => false,
2851                _ => budget >= if big_rig { 48 } else { 256 },
2852            };
2853            // SLRU expert cache is capture-ILLEGAL: a cache miss drains/H2Ds on the compute
2854            // stream mid-decode, which CUDA forbids while capturing (Ornith-35B Q4_K_M on the
2855            // 24GB rig died with CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED, 2026-08-01 — any MoE
2856            // model whose experts overflow the residency budget hit this at budget >= 256).
2857            // The door only opens with every MoE layer's experts device-resident; =1 cannot
2858            // legalize a capture, so this closes the forced door too.
2859            let moe_resident = self.layers.iter().all(|l| match &l.ffn {
2860                crate::hybrid::Ffn::Moe(m) => m.dev_exps.is_some(),
2861                _ => true,
2862            });
2863            if gen_graph && !moe_resident {
2864                static NOTICE: std::sync::Once = std::sync::Once::new();
2865                NOTICE.call_once(|| {
2866                    eprintln!(
2867                        "[gen-graph] door CLOSED: MoE experts on the SLRU cache path \
2868                     (capture-illegal) — eager decode"
2869                    )
2870                });
2871            }
2872            if gen_graph && moe_resident && budget > 0 {
2873                let head_dim = self.cfg.head_dim_k as usize;
2874                let mut gs = GraphDecodeState::new(e)?;
2875                gs.pos_d = pos_d;
2876                gs.token_d = token_d;
2877                let (out_cell, sampler_cell) = (&mut out, &mut *sampler);
2878                let reason = self.graph_decode_loop(
2879                    e,
2880                    &mut gs,
2881                    &mut cache,
2882                    embd_gpu,
2883                    qt,
2884                    rb,
2885                    head_dim,
2886                    budget,
2887                    |tok| {
2888                        sampler_cell.accept(tok);
2889                        out_cell.push(tok);
2890                        if params.eos.contains(&tok) {
2891                            return Some(StopReason::Eos);
2892                        }
2893                        if !on_token(tok) {
2894                            return Some(StopReason::Callback);
2895                        }
2896                        None
2897                    },
2898                )?;
2899                return Ok(GenOutput {
2900                    tokens: out,
2901                    stop_reason: reason,
2902                });
2903            }
2904            let mut next = e.dtoh_u32(&token_d)?[0];
2905            for _ in 0..budget {
2906                sampler.accept(next);
2907                out.push(next);
2908                if params.eos.contains(&next) {
2909                    reason = StopReason::Eos;
2910                    break;
2911                }
2912                if !on_token(next) {
2913                    reason = StopReason::Callback;
2914                    break;
2915                }
2916                if cache.pos >= ctx_cap {
2917                    reason = StopReason::ContextFull;
2918                    break;
2919                }
2920                token_d = self.decode_step_dc(
2921                    e, &token_d, &mut pos_d, embd_gpu, qt, rb, &mut cache, n_vocab,
2922                )?;
2923                next = e.dtoh_u32(&token_d)?[0];
2924            }
2925            return Ok(GenOutput {
2926                tokens: out,
2927                stop_reason: reason,
2928            });
2929        }
2930        for _ in 0..budget {
2931            let next = sampler.sample(&last_logits);
2932            sampler.accept(next);
2933            out.push(next);
2934            if params.eos.contains(&next) {
2935                reason = StopReason::Eos;
2936                break;
2937            }
2938            if !on_token(next) {
2939                reason = StopReason::Callback;
2940                break;
2941            }
2942            if cache.pos >= ctx_cap {
2943                reason = StopReason::ContextFull;
2944                break;
2945            }
2946            last_logits = self.decode_step(e, next, &mut cache)?;
2947        }
2948        Ok(GenOutput {
2949            tokens: out,
2950            stop_reason: reason,
2951        })
2952    }
2953
2954    /// Full-attention decode: project q/gate/k/v for the new token, QK-norm, RoPE at pos,
2955    /// append k,v to the layer KV cache, attend over the full [0..=pos] context.
2956    pub(crate) fn full_attn_decode(
2957        &self,
2958        e: &Engine,
2959        fa: &FullAttnLayer,
2960        h: &CudaSlice<f32>,
2961        pos_d: &CudaSlice<i32>,
2962        pos: usize,
2963        cache: &mut Cache,
2964        il: usize,
2965    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2966        self.full_attn_decode_pre(e, fa, h, None, pos_d, pos, cache, il)
2967    }
2968
2969    /// PRE-QUANTIZED-INPUT eager full-attn (attn-input NORM-FUSION lever): caller passes the
2970    /// attn-normed activation already q8_1 `(hq,hd)` (rms_norm_q8_1) -> skips internal quantize_q8_1.
2971    /// `None` = quantize h here (the spec / non-fused path). BIT-IDENTICAL.
2972    pub(crate) fn full_attn_decode_pre(
2973        &self,
2974        e: &Engine,
2975        fa: &FullAttnLayer,
2976        h: &CudaSlice<f32>,
2977        pre_q: Option<(&CudaSlice<i8>, &CudaSlice<f32>)>,
2978        pos_d: &CudaSlice<i32>,
2979        pos: usize,
2980        cache: &mut Cache,
2981        il: usize,
2982    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2983        if self.cfg.step35.is_some() {
2984            return self.step35_decode_attn(e, fa, il, h, pre_q, pos_d, cache);
2985        }
2986        let cfg = &self.cfg;
2987        let geometry = cfg.full_attention_geometry_at(il as u32);
2988        let n_head = geometry.n_head as usize;
2989        let n_head_kv = geometry.n_head_kv as usize;
2990        let head_dim = geometry.head_dim_k as usize;
2991        let eps = cfg.rms_eps;
2992        let scale = geometry.attention_scale();
2993
2994        // LATENCY-HIDING (MEMRA_KV_PREFETCH=1): warm this layer's KV stream into L2 while the
2995        // q/k/v projections run ahead of the fa (fa is latency-bound; its lines land warm).
2996        // Value-free scheduling — no numeric config change.
2997        static KV_PF: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2998        if *KV_PF.get_or_init(|| std::env::var("MEMRA_KV_PREFETCH").as_deref() == Ok("1")) {
2999            let kvl = cache.kv[il].as_ref().unwrap();
3000            let t_kv = kvl.len + 1;
3001            e.prefetch_l2(&kvl.k, t_kv * kvl.k_tok_bytes)?;
3002            e.prefetch_l2(&kvl.v, t_kv * kvl.v_tok_bytes)?;
3003        }
3004
3005        // wq|wk|wv all take the same input `h` (in_f = n_embd) — quantize q8_1 ONCE, feed all three.
3006        // Q8 TRUNK-FUSION: on Q8_0 trunks (35B) the three fold into ONE fused3 launch (same MMVQ
3007        // body per (tensor,row) — bit-identical; see full_attn_decode_dc_inner). MEMRA_Q8_DUAL=0 off.
3008        let n_embd = cfg.n_embd as usize;
3009        let qkv_fused = |e: &Engine,
3010                         hq: &CudaSlice<i8>,
3011                         hd: &CudaSlice<f32>|
3012         -> Result<
3013            (CudaSlice<f32>, CudaSlice<f32>, CudaSlice<f32>),
3014            Box<dyn std::error::Error>,
3015        > {
3016            if let Some((qf, k, v)) = e.matmul_q8_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hd)? {
3017                return Ok((qf, k, v));
3018            }
3019            Ok((
3020                e.matmul_pre(&fa.wq, hq, hd, h, 1)?,
3021                e.matmul_pre(&fa.wk, hq, hd, h, 1)?,
3022                e.matmul_pre(&fa.wv, hq, hd, h, 1)?,
3023            ))
3024        };
3025        let (qf, mut k, v) =
3026            if e.uses_q8_1_fast(&fa.wq) && e.uses_q8_1_fast(&fa.wk) && e.uses_q8_1_fast(&fa.wv) {
3027                match pre_q {
3028                    Some((hq, hd)) => qkv_fused(e, hq, hd)?,
3029                    None => {
3030                        let (hq, hd) = e.quantize_q8_1(h, 1, n_embd)?;
3031                        qkv_fused(e, &hq, &hd)?
3032                    }
3033                }
3034            } else {
3035                (
3036                    e.matmul(&fa.wq, h, 1)?,
3037                    e.matmul(&fa.wk, h, 1)?,
3038                    e.matmul(&fa.wv, h, 1)?,
3039                )
3040            };
3041        // q|gate fused: [2*head_dim per head]. Split on-device (no dtoh/host-loop/htod).
3042        // M3/Hy3 have no attention output gate — wq out is exactly q; skip the split.
3043        let gated = geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ;
3044        let (mut q, gate) = if gated {
3045            let mut q = e.uninit(n_head * head_dim)?;
3046            let mut gate = e.uninit(n_head * head_dim)?;
3047            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, 1)?;
3048            (q, Some(gate))
3049        } else {
3050            (qf, None)
3051        };
3052
3053        // QK-norm + RoPE at position `pos`
3054        let mut qn = e.uninit(n_head * head_dim)?;
3055        e.rms_norm(&q, fa.q_norm.float_data(), &mut qn, head_dim, n_head, eps)?;
3056        q = qn;
3057        let mut kn = e.uninit(n_head_kv * head_dim)?;
3058        e.rms_norm(
3059            &k,
3060            fa.k_norm.float_data(),
3061            &mut kn,
3062            head_dim,
3063            n_head_kv,
3064            eps,
3065        )?;
3066        k = kn;
3067        let rope_dims = geometry.n_rot as usize;
3068        e.rope_neox(
3069            &mut q,
3070            pos_d,
3071            head_dim,
3072            rope_dims,
3073            n_head,
3074            1,
3075            geometry.rope_base,
3076            1.0,
3077        )?;
3078        e.rope_neox(
3079            &mut k,
3080            pos_d,
3081            head_dim,
3082            rope_dims,
3083            n_head_kv,
3084            1,
3085            geometry.rope_base,
3086            1.0,
3087        )?;
3088
3089        // append k,v into the RESIDENT GPU QUANTIZED KV cache at the current position (q8_0 K /
3090        // q5_1 V, on-device append-quantize kernel; no host round-trip). KVQUANT-PLAN §C/E2.
3091        let kvl = cache.kv[il].as_mut().unwrap();
3092        e.append_kv_quantized(
3093            &k,
3094            &v,
3095            &mut kvl.k,
3096            &mut kvl.v,
3097            kvl.len,
3098            kvl.kv_dim_k,
3099            kvl.kv_dim_v,
3100            kvl.k_tok_bytes,
3101            kvl.v_tok_bytes,
3102            crate::Engine::kv_fp8_on(),
3103        )?;
3104        kvl.len += 1;
3105        let t_kv = kvl.len;
3106
3107        // attend: q[hd,nh,1] over the resident byte K/V (view first t_kv*tok_bytes BYTES).
3108        let k_view = e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes);
3109        let v_view = e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes);
3110        let (ktb, vtb) = (kvl.k_tok_bytes, kvl.v_tok_bytes);
3111        let mut attn = e.uninit(n_head * head_dim)?;
3112        if std::env::var("MEMRA_NOFA").is_ok() {
3113            return Err(
3114                "MEMRA_NOFA (naive f32 SDPA) is incompatible with the quantized KV cache; \
3115                        unset MEMRA_NOFA to use fa_decode"
3116                    .into(),
3117            );
3118        }
3119        e.fa_decode_kvmod(
3120            &q,
3121            &k_view,
3122            &v_view,
3123            &mut attn,
3124            head_dim,
3125            n_head,
3126            n_head_kv,
3127            t_kv,
3128            scale,
3129            ktb,
3130            vtb,
3131            crate::Engine::kv_fp8_on(),
3132        )?;
3133        let _ = pos;
3134
3135        // output gate: attn * sigmoid(gate), then o-proj
3136        let attn_g = match &gate {
3137            Some(gate) => {
3138                let mut gsig = e.uninit(n_head * head_dim)?;
3139                e.sigmoid(gate, &mut gsig, n_head * head_dim)?;
3140                let mut ag = e.uninit(n_head * head_dim)?;
3141                e.mul(&attn, &gsig, &mut ag, n_head * head_dim)?;
3142                ag
3143            }
3144            None => attn,
3145        };
3146        Ok(e.matmul(&fa.wo, &attn_g, 1)?)
3147    }
3148
3149    /// BATCHED full-attention decode over `m` independent streams (one token each).
3150    ///
3151    /// Generic m-band primitive, not lockstep-specific: any caller holding `m` streams at the
3152    /// same layer (multi-stream decode, a continuous-batching serve loop) can use it. The split
3153    /// follows what the hardware cares about — WEIGHT-BOUND work runs once at `m` because all
3154    /// streams share the same projection weights (one weight read serves `m` tokens instead of
3155    /// `m` reads), while KV-BOUND work stays per stream because each stream owns its own cache.
3156    ///
3157    /// Bit-identity with the per-stream path holds by construction: `quantize_q8_1` and
3158    /// `rms_norm` are per-row, `rope_neox` takes a per-token position vector, the fused3/matmul
3159    /// m-band kernels are the same ones spec verify is gated on, and attention itself is
3160    /// untouched per stream.
3161    ///
3162    /// `xcat` is `[m, n_embd]` normed activations; `pos_cat` is the `m` rope positions;
3163    /// returns `[m, n_embd]` attention outputs.
3164    #[allow(clippy::too_many_arguments)]
3165    pub(crate) fn full_attn_decode_batched(
3166        &self,
3167        e: &Engine,
3168        fa: &FullAttnLayer,
3169        xcat: &CudaSlice<f32>,
3170        m: usize,
3171        pos_cat: &CudaSlice<i32>,
3172        caches: &mut [Cache],
3173        il: usize,
3174    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3175        if self.cfg.step35.is_some() {
3176            return Err(
3177                "step35 has no batched (m-stream) decode mixer — per-layer n_head, \
3178                        partial rope and the SWA offset view need a step35 twin"
3179                    .into(),
3180            );
3181        }
3182        let cfg = &self.cfg;
3183        let geometry = cfg.full_attention_geometry_at(il as u32);
3184        let n_head = geometry.n_head as usize;
3185        let n_head_kv = geometry.n_head_kv as usize;
3186        let head_dim = geometry.head_dim_k as usize;
3187        let n_embd = cfg.n_embd as usize;
3188        let eps = cfg.rms_eps;
3189        let scale = geometry.attention_scale();
3190        let q_row = n_head * head_dim;
3191        let kv_row = n_head_kv * head_dim;
3192
3193        // --- weight-bound: one quantize + one q/k/v projection for all m streams ---
3194        let (hq, hd) = e.quantize_q8_1(xcat, m, n_embd)?;
3195        let use_q8 =
3196            e.uses_q8_1_fast(&fa.wq) && e.uses_q8_1_fast(&fa.wk) && e.uses_q8_1_fast(&fa.wv);
3197        let (qf, mut k, v) = if use_q8 {
3198            match e.matmul_q8_fused3_t(&fa.wq, &fa.wk, &fa.wv, &hq, &hd, m)? {
3199                Some(trio) => trio,
3200                None => (
3201                    e.matmul_pre(&fa.wq, &hq, &hd, xcat, m)?,
3202                    e.matmul_pre(&fa.wk, &hq, &hd, xcat, m)?,
3203                    e.matmul_pre(&fa.wv, &hq, &hd, xcat, m)?,
3204                ),
3205            }
3206        } else {
3207            (
3208                e.matmul(&fa.wq, xcat, m)?,
3209                e.matmul(&fa.wk, xcat, m)?,
3210                e.matmul(&fa.wv, xcat, m)?,
3211            )
3212        };
3213
3214        // --- elementwise: batched by treating the m streams as extra rows/tokens ---
3215        let gated = geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ;
3216        let (mut q, gate) = if gated {
3217            let mut q = e.uninit(m * q_row)?;
3218            let mut gate = e.uninit(m * q_row)?;
3219            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, m)?;
3220            (q, Some(gate))
3221        } else {
3222            (qf, None)
3223        };
3224        let mut qn = e.uninit(m * q_row)?;
3225        e.rms_norm(
3226            &q,
3227            fa.q_norm.float_data(),
3228            &mut qn,
3229            head_dim,
3230            n_head * m,
3231            eps,
3232        )?;
3233        q = qn;
3234        let mut kn = e.uninit(m * kv_row)?;
3235        e.rms_norm(
3236            &k,
3237            fa.k_norm.float_data(),
3238            &mut kn,
3239            head_dim,
3240            n_head_kv * m,
3241            eps,
3242        )?;
3243        k = kn;
3244        let rope_dims = geometry.n_rot as usize;
3245        e.rope_neox(
3246            &mut q,
3247            pos_cat,
3248            head_dim,
3249            rope_dims,
3250            n_head,
3251            m,
3252            geometry.rope_base,
3253            1.0,
3254        )?;
3255        e.rope_neox(
3256            &mut k,
3257            pos_cat,
3258            head_dim,
3259            rope_dims,
3260            n_head_kv,
3261            m,
3262            geometry.rope_base,
3263            1.0,
3264        )?;
3265
3266        // --- KV-bound: each stream appends to and attends over its own cache ---
3267        let mut attn_cat = e.uninit(m * q_row)?;
3268        let mut q_s = e.uninit(q_row)?;
3269        let mut k_s = e.uninit(kv_row)?;
3270        let mut v_s = e.uninit(kv_row)?;
3271        for (s, cache) in caches.iter_mut().enumerate().take(m) {
3272            e.copy_view_into(&mut k_s, 0, &k.slice(s * kv_row..(s + 1) * kv_row), kv_row)?;
3273            e.copy_view_into(&mut v_s, 0, &v.slice(s * kv_row..(s + 1) * kv_row), kv_row)?;
3274            e.copy_view_into(&mut q_s, 0, &q.slice(s * q_row..(s + 1) * q_row), q_row)?;
3275            let kvl = cache.kv[il].as_mut().unwrap();
3276            e.append_kv_quantized(
3277                &k_s,
3278                &v_s,
3279                &mut kvl.k,
3280                &mut kvl.v,
3281                kvl.len,
3282                kvl.kv_dim_k,
3283                kvl.kv_dim_v,
3284                kvl.k_tok_bytes,
3285                kvl.v_tok_bytes,
3286                crate::Engine::kv_fp8_on(),
3287            )?;
3288            kvl.len += 1;
3289            let t_kv = kvl.len;
3290            let k_view = e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes);
3291            let v_view = e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes);
3292            let mut attn = e.uninit(q_row)?;
3293            e.fa_decode_kvmod(
3294                &q_s,
3295                &k_view,
3296                &v_view,
3297                &mut attn,
3298                head_dim,
3299                n_head,
3300                n_head_kv,
3301                t_kv,
3302                scale,
3303                kvl.k_tok_bytes,
3304                kvl.v_tok_bytes,
3305                crate::Engine::kv_fp8_on(),
3306            )?;
3307            e.copy_into(&mut attn_cat, s * q_row, &attn, q_row)?;
3308        }
3309
3310        // --- weight-bound again: gate epilogue + one output projection for all m streams ---
3311        let attn_g = match &gate {
3312            Some(gate) => {
3313                let mut gsig = e.uninit(m * q_row)?;
3314                e.sigmoid(gate, &mut gsig, m * q_row)?;
3315                let mut ag = e.uninit(m * q_row)?;
3316                e.mul(&attn_cat, &gsig, &mut ag, m * q_row)?;
3317                ag
3318            }
3319            None => attn_cat,
3320        };
3321        e.matmul(&fa.wo, &attn_g, m)
3322    }
3323
3324    /// Linear-attention decode: conv with ring-buffer state, GDN scan carrying SSM state.
3325    pub fn linear_attn_decode(
3326        &self,
3327        e: &Engine,
3328        la: &LinearAttnLayer,
3329        h: &CudaSlice<f32>,
3330        cache: &mut Cache,
3331        il: usize,
3332    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3333        self.linear_attn_decode_inner(e, la, h, None, cache, il, false)
3334    }
3335
3336    /// PRE-QUANTIZED-INPUT variant (DECODE attn-input NORM-FUSION lever): the caller passes the
3337    /// post-attn-norm activation ALREADY q8_1-quantized `(hq,hd)` (produced by rms_norm_q8_1, fusing
3338    /// the attn_norm + the mixer's internal quantize_q8_1). Skips the internal quantize. Caller
3339    /// GUARANTEES the projections are q8_1-fast. `persistent` selects the capture-safe state plumbing.
3340    /// BIT-IDENTICAL to linear_attn_decode(h) when (hq,hd)==quantize_q8_1(rms_norm(x)*w).
3341    pub fn linear_attn_decode_pre(
3342        &self,
3343        e: &Engine,
3344        la: &LinearAttnLayer,
3345        h: &CudaSlice<f32>,
3346        hq: &CudaSlice<i8>,
3347        hd: &CudaSlice<f32>,
3348        cache: &mut Cache,
3349        il: usize,
3350        persistent: bool,
3351    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3352        self.linear_attn_decode_inner(e, la, h, Some((hq, hd)), cache, il, persistent)
3353    }
3354
3355    /// CAPTURE variant of `linear_attn_decode` (CUDA-GRAPH-PLAN Phase 3). The GDN scan needs distinct
3356    /// in/out SSM-state buffers; the eager path SWAPS a fresh scratch into `rl.ssm_state` (new pointer
3357    /// each step), which is a CAPTURE HAZARD — the graph bakes capture-time pointers and never re-runs
3358    /// the host swap, so replay would read a stale state buffer. Here we instead COPY the scratch back
3359    /// into the STABLE `rl.ssm_state` buffer (memcpy_dtod, captured, same pointers every replay). Math
3360    /// is identical; only the buffer plumbing differs. `conv_state` is already mutated in place (no
3361    /// pointer change) so it is capture-safe as-is.
3362    pub(crate) fn linear_attn_decode_cap(
3363        &self,
3364        e: &Engine,
3365        la: &LinearAttnLayer,
3366        h: &CudaSlice<f32>,
3367        cache: &mut Cache,
3368        il: usize,
3369    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3370        self.linear_attn_decode_inner(e, la, h, None, cache, il, true)
3371    }
3372
3373    fn linear_attn_decode_inner(
3374        &self,
3375        e: &Engine,
3376        la: &LinearAttnLayer,
3377        h: &CudaSlice<f32>,
3378        pre_q: Option<(&CudaSlice<i8>, &CudaSlice<f32>)>,
3379        cache: &mut Cache,
3380        il: usize,
3381        persistent_state: bool,
3382    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3383        let cfg = &self.cfg;
3384        let ssm = cfg.ssm.as_ref().unwrap();
3385        let d_state = ssm.state_size as usize;
3386        let num_k = ssm.group_count as usize;
3387        let num_v = ssm.time_step_rank as usize;
3388        let d_conv = ssm.conv_kernel as usize;
3389        let head_k = d_state;
3390        let key_dim = head_k * num_k;
3391        let value_dim = d_state * num_v;
3392        let conv_dim = key_dim * 2 + value_dim;
3393        let eps = cfg.rms_eps;
3394        let scale = 1.0 / (d_state as f32).sqrt();
3395
3396        // projections (T=1): wqkv, wqkv_gate, ssm_beta, ssm_alpha ALL take input `h` (in_f = n_embd)
3397        // -> quantize q8_1 ONCE, feed all four (was 4x redundant quantize_q8_1 of the same row).
3398        let n_embd = cfg.n_embd as usize;
3399        let all_fast = e.uses_q8_1_fast(&la.wqkv)
3400            && e.uses_q8_1_fast(&la.wqkv_gate)
3401            && e.uses_q8_1_fast(&la.ssm_beta)
3402            && e.uses_q8_1_fast(&la.ssm_alpha);
3403        // beta+alpha DUAL fuse (2026-07-05): ssm_beta and ssm_alpha are the same tiny shape
3404        // ([n_embd -> num_v=32]) — out_f=32 launches are pure launch latency (15-16us each,
3405        // HANDOVER b4-headroom note). The existing dual mr2 kernel (FFN gate+up) folds them into
3406        // ONE launch. Bit-identical per row: same MMVQ warp-per-row body, blockIdx.y picks the
3407        // weight; the separable macro-scale multiply is the same single f32 mul as matmul_pre's
3408        // in-kernel scale. Falls back to two matmul_pre when ineligible (Float layers 1/2/4 etc).
3409        let beta_alpha =
3410            |e: &Engine,
3411             hq: &CudaSlice<i8>,
3412             hd: &CudaSlice<f32>|
3413             -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
3414                if let Some(((mut b, bs), (mut a, as_))) =
3415                    e.matmul_pre_dual_noscale(&la.ssm_beta, &la.ssm_alpha, hq, hd, 1)?
3416                {
3417                    if bs != 1.0 {
3418                        e.scale_inplace(&mut b, bs, la.ssm_beta.out_features())?;
3419                    }
3420                    if as_ != 1.0 {
3421                        e.scale_inplace(&mut a, as_, la.ssm_alpha.out_features())?;
3422                    }
3423                    return Ok((b, a));
3424                }
3425                // Q8_0 twin of the NVFP4 dual (9B GGUFs store ssm_beta/alpha as Q8_0 on most layers):
3426                // one fused2 launch, bit-identical per row, no macro-scale (q8_0 scale==1.0).
3427                if let Some((b, a)) = e.matmul_q8_fused2(&la.ssm_beta, &la.ssm_alpha, hq, hd)? {
3428                    return Ok((b, a));
3429                }
3430                Ok((
3431                    e.matmul_pre(&la.ssm_beta, hq, hd, h, 1)?,
3432                    e.matmul_pre(&la.ssm_alpha, hq, hd, h, 1)?,
3433                ))
3434            };
3435        // Q8 TRUNK-FUSION (2026-07-05): wqkv+wqkv_gate share (hq,hd) and in_f — on the 35B both
3436        // are Q8_0 (out_f 8192/4096), so ONE fused2 launch replaces the two biggest
3437        // launch-latency-class m=1 launches of every linear layer. BIT-IDENTICAL per (tensor,row)
3438        // (same MMVQ body, block-offset split). Falls back per-tensor when ineligible.
3439        let qkv_pair =
3440            |e: &Engine,
3441             hq: &CudaSlice<i8>,
3442             hd: &CudaSlice<f32>|
3443             -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
3444                if let Some((qkv, z)) = e.matmul_q8_fused2(&la.wqkv, &la.wqkv_gate, hq, hd)? {
3445                    return Ok((qkv, z));
3446                }
3447                Ok((
3448                    e.matmul_pre(&la.wqkv, hq, hd, h, 1)?,
3449                    e.matmul_pre(&la.wqkv_gate, hq, hd, h, 1)?,
3450                ))
3451            };
3452        let (qkv_mixed, z, beta_raw, alpha) = if all_fast {
3453            // attn-input NORM-FUSION: use the caller's pre-quantized (hq,hd) when provided (the
3454            // attn_norm already emitted q8_1 via rms_norm_q8_1), else quantize h here. Bit-identical.
3455            match pre_q {
3456                Some((hq, hd)) => {
3457                    let (b, a) = beta_alpha(e, hq, hd)?;
3458                    let (qkv, z) = qkv_pair(e, hq, hd)?;
3459                    (qkv, z, b, a)
3460                }
3461                None => {
3462                    let (hq, hd) = e.quantize_q8_1(h, 1, n_embd)?;
3463                    let (b, a) = beta_alpha(e, &hq, &hd)?;
3464                    let (qkv, z) = qkv_pair(e, &hq, &hd)?;
3465                    (qkv, z, b, a)
3466                }
3467            }
3468        } else {
3469            // 35B trunk lands HERE: wqkv/wqkv_gate are Q8_0 but ssm_beta/alpha are F32, so
3470            // all_fast is false. Still fuse the two Q8_0 projections (one quantize + ONE launch
3471            // instead of two matmuls each re-quantizing h) — matmul_q8_fused2_x is bit-identical
3472            // to the two m=1 MMVQ dispatches. beta/alpha keep the Float cuBLAS path.
3473            let (qm, zg) = match e.matmul_q8_fused2_x(&la.wqkv, &la.wqkv_gate, h)? {
3474                Some(pair) => pair,
3475                None => (e.matmul(&la.wqkv, h, 1)?, e.matmul(&la.wqkv_gate, h, 1)?),
3476            };
3477            (
3478                qm,
3479                zg,
3480                e.matmul(&la.ssm_beta, h, 1)?,
3481                e.matmul(&la.ssm_alpha, h, 1)?,
3482            )
3483        };
3484
3485        // RANK3 LEVER (conv fuse): assemble [conv_state | new col], depthwise causal conv + SiLU, and
3486        // roll the ring — ALL in ONE kernel (`ssm_conv1d_fused_decode`), never materializing conv_in
3487        // to HBM. Replaces conv_assemble_and_roll + ssm_conv1d. Bit-identical (same accumulation order).
3488        let rl = cache.recur[il].as_mut().unwrap();
3489        let mut conv_out = e.uninit(conv_dim)?; // [conv_dim, 1] channel-major, SiLU
3490        e.ssm_conv1d_fused_decode(
3491            &qkv_mixed,
3492            &mut rl.conv_state,
3493            la.ssm_conv1d.float_data(),
3494            &mut conv_out,
3495            conv_dim,
3496            d_conv,
3497        )?;
3498
3499        // GDN scan: SSM state stays RESIDENT on GPU. gdn needs DISTINCT in/out state buffers.
3500        // DECODE DETERMINISM FIX: write the new state into the PERSISTENT spare buffer
3501        // (`ssm_state_alt`) and PING-PONG the two owned buffers in place — instead of allocating a
3502        // fresh `state_scratch` via `e.uninit` each step and swapping its pointer in. The old
3503        // per-step alloc/free churned the stream-ordered async pool; the freed prior state block was
3504        // recycled by a later step's scratch while a kernel still referenced the swapped-in state,
3505        // a use-after-reuse that made decode RUN-TO-RUN nondeterministic (two identical primes
3506        // diverged). With two stable resident buffers there is no per-step alloc/free and no pool
3507        // churn; the math is byte-identical. `o` is a true per-step output (consumed immediately by
3508        // gated_rmsnorm below) so it stays a normal scratch.
3509        let mut o = e.uninit(d_state * num_v)?;
3510        let n_state = d_state * d_state * num_v;
3511        let _ = head_k; // head_k == d_state; the kernels use head_k = d_state internally.
3512        // GDN PREP, FUSED (2026-07-03): repack + q/k L2-norm + beta sigmoid + g_log in ONE
3513        // gdn_prep_decode launch (was 5 tiny serialized kernels: qkv_to_gdn_repack, 2x l2_norm,
3514        // sigmoid, gdn_glog). Same math; the L2 reduce runs a 32-lane warp tree instead of the
3515        // 256-thread two-level tree (different FP sum order) — gates: argmax + run-spec exactness.
3516        // (A prep+scan single-launch fusion — lane/gdnfuse, MEMRA_GDN_FUSE — measured NEUTRAL on
3517        // eager decode 2026-07-08 and was removed in the flag audit; rig5090.jsonl holds the record.)
3518        {
3519            let mut q_l2 = e.uninit(d_state * num_v)?;
3520            let mut k_l2 = e.uninit(d_state * num_v)?;
3521            let mut v_gd = e.uninit(d_state * num_v)?;
3522            let mut beta = e.uninit(num_v)?;
3523            let mut g_log = e.uninit(num_v)?;
3524            e.gdn_prep_decode(
3525                &conv_out,
3526                &beta_raw,
3527                &alpha,
3528                la.ssm_dt.float_data(),
3529                la.ssm_a.float_data(),
3530                &mut q_l2,
3531                &mut k_l2,
3532                &mut v_gd,
3533                &mut beta,
3534                &mut g_log,
3535                d_state,
3536                num_v,
3537                num_k,
3538                key_dim,
3539                eps,
3540            )?;
3541            // gdn reads ssm_state, writes the spare ssm_state_alt (disjoint resident fields).
3542            let RecurLayer {
3543                ssm_state,
3544                ssm_state_alt,
3545                ..
3546            } = rl;
3547            e.gdn_scan_s128(
3548                &q_l2,
3549                &k_l2,
3550                &v_gd,
3551                &g_log,
3552                &beta,
3553                ssm_state,
3554                ssm_state_alt,
3555                &mut o,
3556                num_v,
3557                1,
3558                scale,
3559            )?;
3560        }
3561        if persistent_state {
3562            // CAPTURE-safe (graph replay): the canonical state every replay reads must stay at a
3563            // FIXED pointer (baked into the captured graph). Copy the freshly-written spare BACK
3564            // into ssm_state (captured, replays each launch). No host pointer swap.
3565            let alt = std::mem::replace(&mut rl.ssm_state_alt, e.zeros(0)?);
3566            e.copy_into(&mut rl.ssm_state, 0, &alt, n_state)?;
3567            rl.ssm_state_alt = alt;
3568        } else {
3569            // EAGER: swap the two OWNED resident buffers in place (stable pointers, no alloc/free).
3570            std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
3571        }
3572
3573        // gated RMSNorm + ssm_out. FUSED-QUANTIZE ARM (launch-arc): when ssm_out rides the
3574        // q8_1 fast path, emit q8_1 straight from the gated norm (bit-identical bytes to
3575        // gated_rmsnorm + quantize_q8_1) and feed matmul_pre — one launch instead of three
3576        // (norm, quantize, scale all fold away). Fallback = the original f32 chain.
3577        if e.uses_q8_1_fast(&la.ssm_out) {
3578            // norm is PER d_state-ROW (num_v rows), exactly like the f32 twin's grid; the q8_1
3579            // block stream is row-major so the flat bytes feed the matvec unchanged.
3580            let (gq, gd) =
3581                e.gated_rmsnorm_q8_1(&o, la.ssm_norm.float_data(), &z, d_state, num_v, eps)?;
3582            let g0 = e.zeros(0)?;
3583            return Ok(e.matmul_pre(&la.ssm_out, &gq, &gd, &g0, 1)?);
3584        }
3585        let mut gn = e.uninit(d_state * num_v)?;
3586        e.gated_rmsnorm(
3587            &o,
3588            la.ssm_norm.float_data(),
3589            &z,
3590            &mut gn,
3591            d_state,
3592            num_v,
3593            eps,
3594        )?;
3595        Ok(e.matmul(&la.ssm_out, &gn, 1)?)
3596    }
3597}