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