Skip to main content

memra_engine/
decode.rs

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