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