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