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