Skip to main content

memra_engine/
hybrid_forward.rs

1//! Hybrid forward pass (Stage-1, f32, prefill, single sequence). Per layer dispatches to a
2//! linear-attention (Gated DeltaNet) or full-attention mixer, then SwiGLU FFN. Matches
3//! llama.cpp src/models/qwen35.cpp node-for-node.
4
5use cudarc::driver::CudaSlice;
6use memra_gguf::config::ModelConfig;
7use crate::Engine;
8use crate::cache::Cache;
9
10/// Resident trunk transients for the eager prime (piecewise-graph foundation; see
11/// HybridModel::prime_slabs). Every buffer is fully overwritten before use per prime.
12pub struct PrimeSlabs {
13    pub t_cap: usize,
14    pub h: CudaSlice<f32>,
15    pub x1: CudaSlice<f32>,
16    pub z: CudaSlice<f32>,
17    pub act: CudaSlice<f32>,
18    pub xa: CudaSlice<f32>,
19    pub xb: CudaSlice<f32>,
20    pub h16: CudaSlice<u8>,
21    pub z16: CudaSlice<u8>,
22    /// piecewise boundary slabs (increment 2): GEMM outputs land here so the
23    /// downstream captured segments see fixed addresses.
24    pub gate: CudaSlice<f32>,     // t * n_ff_max
25    pub up: CudaSlice<f32>,       // t * n_ff_max
26    pub ffn_out: CudaSlice<f32>,  // t * n_embd
27    /// piecewise increment 3: per-layer S-glue segment graphs (down-add + next
28    /// attn-norm, ALL-slab IO, zero in-graph allocations -> keeperless capture is
29    /// clean). Baked at this t_cap; replay only when t == t_cap. seg_glue[il] fires
30    /// between layer il and il+1 (ping-pong parity is deterministic per il).
31    pub seg_glue: Vec<Option<cudarc::driver::CudaGraph>>,
32    /// increment 5 (core-split edition): the mixer out-GEMM writes _into_ `mixed`
33    /// directly (no staging copy — the increment-4 copy route was refuted), making
34    /// S-mid [add + post-norm] all-slab and capturable.
35    pub mixed: CudaSlice<f32>,
36    pub seg_mid: Vec<Option<cudarc::driver::CudaGraph>>,
37    pub seg_t: usize,
38}
39
40
41/// task #18 (attn side): one sequence's pre-attention outputs (post-rope q/k, v, out-gate).
42pub(crate) struct AttnPre {
43    pub q: cudarc::driver::CudaSlice<f32>,
44    pub k: cudarc::driver::CudaSlice<f32>,
45    pub v: cudarc::driver::CudaSlice<f32>,
46    pub gate: Option<cudarc::driver::CudaSlice<f32>>,
47}
48
49/// task #18: one sequence's GDN prep outputs (the scan inputs).
50pub(crate) struct GdnPrep {
51    pub hk: usize,
52    pub q_l2: cudarc::driver::CudaSlice<f32>,
53    pub k_l2: cudarc::driver::CudaSlice<f32>,
54    pub v_g: cudarc::driver::CudaSlice<f32>,
55    pub beta: cudarc::driver::CudaSlice<f32>,
56    pub g_log: cudarc::driver::CudaSlice<f32>,
57    pub kb16: Option<cudarc::driver::CudaSlice<u8>>,
58    pub qb16: Option<cudarc::driver::CudaSlice<u8>>,
59}
60
61/// Device scratch for the burst verify stream (see `verify_stream_scratch`).
62pub(crate) struct VerifyStreamScratch {
63    pub pos_d: CudaSlice<i32>,
64    pub row_ctrs: Vec<CudaSlice<i32>>,
65}
66use crate::hybrid::{HybridModel, Mixer, FullAttnLayer, LinearAttnLayer, MoeWeights};
67
68struct MoeInputTraceWriter {
69    dir: std::path::PathBuf,
70    index: std::fs::File,
71    payloads: std::collections::HashMap<u16, (std::fs::File, u64)>,
72}
73
74static MOE_INPUT_TRACE_WRITER: std::sync::OnceLock<
75    std::sync::Mutex<Option<MoeInputTraceWriter>>,
76> = std::sync::OnceLock::new();
77
78/// STAGE-2 GROUPED DECODE gate (MEMRA_MOE_GDEC, default ON; `=0` restores the sequential
79/// per-expert launch chain). See `moe_gdec_token`.
80fn gdec_enabled() -> bool {
81    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
82    *E.get_or_init(|| std::env::var("MEMRA_MOE_GDEC").map(|v| v != "0").unwrap_or(true))
83}
84
85/// Deterministic in-token expert prefetch. `MEMRA_MOE_PREFETCH=1` overlaps memory-source H2D on the
86/// copy stream; selecting the opt-in worker spill backend enables the same known-next hook for disk.
87fn moe_prefetch_enabled() -> bool {
88    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
89    *E.get_or_init(|| std::env::var("MEMRA_MOE_PREFETCH").as_deref() == Ok("1")
90        || crate::spill_pread::worker_enabled())
91}
92
93/// Best-effort OS page-cache prefetch distance for mmap-backed expert ranges. Independent of the
94/// H2D copy-stream experiment so storage->RAM and RAM->HBM overlap can be measured separately.
95/// The opt-in default stays one expert to preserve the original experiment; spill rigs can widen
96/// it with `MEMRA_MOE_PAGE_PREFETCH_WINDOW` to cover NVMe latency.
97fn moe_page_prefetch_window() -> usize {
98    static W: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
99    *W.get_or_init(|| page_prefetch_window_from_values(
100        std::env::var("MEMRA_MOE_PAGE_PREFETCH").as_deref() == Ok("1"),
101        std::env::var("MEMRA_MOE_PAGE_PREFETCH_WINDOW").ok().as_deref(),
102    ))
103}
104
105fn page_prefetch_window_from_values(enabled: bool, raw_window: Option<&str>) -> usize {
106    if !enabled {
107        return 0;
108    }
109    raw_window
110        .and_then(|value| value.parse().ok())
111        .unwrap_or(1)
112}
113
114/// Return only the newly exposed positions in a rolling lookahead window. Position zero seeds the
115/// full window; each later position adds one expert at the far edge. Thus widening the window does
116/// not repeatedly issue `MADV_WILLNEED` for the same range.
117fn page_prefetch_positions(
118    position: usize,
119    len: usize,
120    window: usize,
121) -> std::ops::Range<usize> {
122    if window == 0 || position >= len {
123        return len..len;
124    }
125    let (start, count) = if position == 0 {
126        (1, window)
127    } else {
128        (position.saturating_add(window), 1)
129    };
130    let start = start.min(len);
131    start..start.saturating_add(count).min(len)
132}
133
134/// Grouped worker-I/O schedule: prime the first active expert before the loop, then queue exactly
135/// one known-next expert at each iteration. Returning positions keeps expert ordering authoritative.
136fn grouped_worker_prefetch_position(order_len: usize, current: Option<usize>) -> Option<usize> {
137    let position = current.map_or(0, |position| position.saturating_add(1));
138    (position < order_len).then_some(position)
139}
140
141/// Fill the worker ring with complete experts, retaining one pinned buffer for an unexpected
142/// demand miss. Each expert has gate/up/down extents, so depth 16 admits a rolling five-expert
143/// window. Position zero primes the current expert too: its three independent reads can run in
144/// parallel instead of demand-serializing gate, up, and down before any useful GPU work exists.
145fn worker_prefetch_window() -> usize {
146    static WINDOW: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
147    *WINDOW.get_or_init(|| {
148        let automatic = crate::spill_pread::configured_depth().saturating_sub(1) / 3;
149        std::env::var("MEMRA_SPILL_WORKER_EXPERT_WINDOW")
150            .ok()
151            .and_then(|value| value.parse::<usize>().ok())
152            .unwrap_or(automatic.max(1))
153    })
154}
155
156/// Return only positions newly exposed by a rolling worker-I/O window. Unlike mmap page advice,
157/// this includes the current expert when the window is seeded so all three current projections
158/// enter the CPU pool together.
159fn worker_prefetch_positions(position: usize, len: usize, window: usize) -> std::ops::Range<usize> {
160    if window == 0 || position >= len {
161        return len..len;
162    }
163    let (start, count) = if position == 0 {
164        (0, window)
165    } else {
166        (position.saturating_add(window).saturating_sub(1), 1)
167    };
168    let start = start.min(len);
169    start..start.saturating_add(count).min(len)
170}
171
172/// LAUNCH-STRUCTURE STAGE 3 gate (MEMRA_MOE_DEV, default ON; `=0` restores host routing). The
173/// zero-DtoH device-dispatch path for fully-resident layers: router top-k output stays on device,
174/// expert weight pointers come from the per-layer device table. Requires the fused router (the
175/// dev path consumes the device sel/w directly), so MEMRA_FUSED_ROUTER=0 also disables it.
176fn moe_dev_enabled() -> bool {
177    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
178    *E.get_or_init(|| std::env::var("MEMRA_MOE_DEV").map(|v| v != "0").unwrap_or(true)
179        && !matches!(std::env::var("MEMRA_FUSED_ROUTER").as_deref(), Ok("0")))
180}
181
182/// MoE EXPERT dp4a gate (MEMRA_MOE_Q8, default ON; `=0` restores the Stage-A f32-dequant expert
183/// kernels). Applies when gate/up/down expert qtypes are all in the dp4a body set (IQ3_S/IQ4_XS).
184/// FP-order differs from Stage-A (int dp4a + warp tree) — argmax/run-gen/stream-identity gates
185/// arbitrate; the sequential and fused q8 paths ship as a matched pair (MEMRA_MOE_GATE contract).
186fn moe_q8_enabled() -> bool {
187    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
188    *E.get_or_init(|| std::env::var("MEMRA_MOE_Q8").map(|v| v != "0").unwrap_or(true))
189}
190
191/// gemma4 fast-arm gate: qtypes with an `expert_dot_g` dp4a body (superset used by the gelu
192/// dev arm; the qwen q8 arms keep their own battery-gated q8_expert_supported policy).
193fn expert_dp4a_supported(qt: i32) -> bool {
194    qt == crate::QT_Q4_0 || qt == crate::QT_IQ3_S || qt == crate::QT_IQ4_XS
195        || qt == crate::QT_Q3_K || qt == crate::QT_Q4_K || qt == crate::QT_Q6_K
196}
197
198fn q8_expert_supported(qt: i32) -> bool {
199    // k-quant arms added 2026-07-06 (Q3_K/Q4_K/Q6_K bodies for the UD tail layers). Briefly
200    // default-excluded the same day when they appeared to break 35B real-prompt spec — the
201    // ACTUAL culprit was the MoE router's cuBLASLt n-dependence (d994271); with the router
202    // decode-exact at verify t, the k-quant arms pass the full spec battery (p1/p2/p3 + raw
203    // K=1..8) and are DEFAULT ON again (+9 tok/s: 148.9 -> 157.9). MEMRA_MOE_Q8_KQ=0 excludes.
204    static KQ: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
205    let kq = *KQ.get_or_init(|| {
206        std::env::var("MEMRA_MOE_Q8_KQ").map(|v| v != "0").unwrap_or(true)
207    });
208    // NVFP4 experts: DEFAULT ON (2026-07-17). The M3-era "decode-vs-verify MISMATCH 3.4e1"
209    // that had this excluded was the missing per-expert macro-scale fold, fixed in the
210    // dev-kernel epilogues + moe_w_scale_by_expert; the 35B ct-NVFP4 artifact now runs the
211    // q8 arm at parity with the IQ4_XS daily (174-178 tok/s, spec K=1..8 exact). M3/Hy3
212    // never reach the q8 arms regardless (sigmoid-router cfg gates on pairs/dev/gdec).
213    // MEMRA_MOE_Q8_NVFP4=0 restores the f32 arm.
214    let nvfp4_q8 = std::env::var("MEMRA_MOE_Q8_NVFP4").map(|v| v != "0").unwrap_or(true);
215    qt == crate::QT_IQ3_S || qt == crate::QT_IQ4_XS || (nvfp4_q8 && qt == crate::QT_NVFP4)
216        || (kq && (qt == crate::QT_Q3_K || qt == crate::QT_Q4_K || qt == crate::QT_Q6_K))
217}
218
219/// The decode-once (_dec) and IQ-MMA expert kernels dequant via IQ-specific extractors —
220/// k-quant tensors must fall to the _em dot path instead.
221fn q8_expert_dec_supported(qt: i32) -> bool {
222    qt == crate::QT_IQ3_S || qt == crate::QT_IQ4_XS || qt == crate::QT_Q4_0
223}
224
225/// Grouped-f16 door (MEMRA_MOE_F16G) per-projection admission: the qtype has a dequant-to-f16
226/// kernel in cu/moe_f16_grouped.cu AND the projection's k dimension tiles its block size.
227/// Round 49 widened coverage to q35's UD mix (gate/up IQ3_S x39 + Q3_K x1 + IQ4_XS x1; down
228/// IQ4_XS x37 + Q6_K x3 + Q4_K x1) — the round-47 IQ4_XS/Q4_0-only table admitted ~1 of 41
229/// q35 layers, which is why that cell measured FLAT.
230fn f16g_proj_ok(qt: i32, in_f: usize) -> bool {
231    match qt {
232        crate::QT_Q4_0 => in_f % 32 == 0,
233        crate::QT_IQ4_XS | crate::QT_IQ3_S | crate::QT_Q3_K | crate::QT_Q4_K
234        | crate::QT_Q6_K => in_f % 256 == 0,
235        _ => false,
236    }
237}
238
239/// STAGE 3 prewarm gate (MEMRA_MOE_PREWARM, default ON; `=0` leaves residency organic). One-shot
240/// per layer: force-admit every block while FREE slots cover the whole layer (never evicts).
241fn moe_prewarm_enabled() -> bool {
242    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
243    *E.get_or_init(|| std::env::var("MEMRA_MOE_PREWARM").map(|v| v != "0").unwrap_or(true))
244}
245
246/// During a discarded fixed-residency profile, admit CPU-routed misses after their current-token
247/// CPU result is complete. The current result and numeric path are unchanged; later warmup tokens
248/// can then vote for and exercise those experts on GPU before the cache is frozen.
249fn cpu_expert_profile_admit_enabled() -> bool {
250    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
251    *E.get_or_init(|| std::env::var("MEMRA_CPU_EXPERT_FREEZE_PROFILE_ADMIT").as_deref() == Ok("1"))
252}
253
254/// Minimum prompt length for the BATCHED cache prime (`prime_cache`). Below this the tokenwise
255/// decode loop wins anyway (the batched path's GEMM dispatch needs m>=16, and the stateful conv
256/// kernel needs T >= d_conv-1). Callers: generate / generate_spec.
257pub const PRIME_MIN_T: usize = 16;
258
259impl HybridModel {
260    /// CHUNK-INVARIANCE BISECT SEAM (lane/chunk-invariance): `MEMRA_PRIME_TRACE=<path>`
261    /// appends one JSONL row per (chunk, layer) with a hash of that layer's last-row
262    /// post-residual hidden. Diagnostic only — never on in a measured or gated run
263    /// (it forces a dtoh + host hash per layer).
264    fn prime_trace_path() -> Option<&'static str> {
265        static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
266        P.get_or_init(|| std::env::var("MEMRA_PRIME_TRACE").ok())
267            .as_deref()
268    }
269
270    /// CHUNK-ORDER INVARIANCE door (`MEMRA_PRIME_INVARIANT=1`, default OFF).
271    /// ON: chunked-prefill split points are pinned to `prime_grain()` and STOP tracking
272    /// `MEMRA_PRIME_CHUNK`, so the same prompt primes through the same boundary set — and
273    /// therefore the same arithmetic — on every rig regardless of that rig's chunk config.
274    /// The cost is that `MEMRA_PRIME_CHUNK` no longer bounds the prime's transient
275    /// footprint; `MEMRA_PRIME_GRAIN` does. Gated by
276    /// `tools/chunk-invariance-gate.sh` (fast-gate tier 1).
277    pub fn prime_invariant() -> bool {
278        static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
279        *E.get_or_init(|| std::env::var("MEMRA_PRIME_INVARIANT").as_deref() == Ok("1"))
280    }
281
282    /// The fixed prefill segmentation grain (`MEMRA_PRIME_GRAIN`, default 4096 = the
283    /// historical `MEMRA_PRIME_CHUNK` default, so the invariant door's boundary set matches
284    /// today's default-config output). Clamped to >= PRIME_MIN_T: a grain below the stateful
285    /// conv's minimum would make the tail-merge rule the real segmenter.
286    /// This is a NUMERIC-CONFIG knob under the invariant door — changing it changes bits,
287    /// exactly like `MEMRA_KV_K` or `MEMRA_GDN_CHUNK`.
288    pub fn prime_grain() -> usize {
289        static G: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
290        *G.get_or_init(|| {
291            std::env::var("MEMRA_PRIME_GRAIN").ok()
292                .and_then(|v| v.parse().ok()).unwrap_or(4096)
293                .max(PRIME_MIN_T)
294        })
295    }
296
297    /// Prefill forward over `tokens`; returns logits [T, n_vocab] (host f32).
298    pub fn forward(&self, e: &Engine, tokens: &[u32]) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
299        if self.is_gemma4_e4b() { return self.gemma4_e4b_forward(e, tokens, false); }
300        if self.cfg.gemma4.is_some() { return self.gemma4_forward(e, tokens, false); }
301        let cfg = &self.cfg;
302        let n_embd = cfg.n_embd as usize;
303        let t = tokens.len();
304        let eps = cfg.rms_eps;
305        let pos: Vec<i32> = (0..t as i32).collect();
306        let pos_d = e.htod_i32(&pos)?;
307
308        let mut x = self.embed(e, tokens)?;   // [T, n_embd]
309
310        for (il, layer) in self.layers.iter().enumerate() {
311            // attn_norm
312            let mut h = e.uninit(t * n_embd)?;
313            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
314
315            let mixed = match &layer.mixer {
316                Mixer::Full(fa) => self.full_attn(e, fa, &h, &pos_d, t)?,
317                Mixer::Linear(la) => self.linear_attn(e, la, &h, t)?,
318                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
319            };
320
321            // residual 1
322            let mut x1 = e.uninit(t * n_embd)?;
323            e.add(&x, &mixed, &mut x1, t * n_embd)?;
324
325            // pre-FFN norm (post_attention_norm), FFN (Dense or MoE), residual 2
326            let mut z = e.uninit(t * n_embd)?;
327            e.rms_norm(&x1, layer.post_attn_norm.float_data(), &mut z, n_embd, t, eps)?;
328            let ffn_out = match &layer.ffn {
329                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
330                    let n_ff = ffn_gate.out_features();
331                    let mut g2 = e.matmul_group(&[ffn_gate, ffn_up], &z, t)?;
332                    let up = g2.pop().unwrap();
333                    let gate = g2.pop().unwrap();
334                    let mut act = e.uninit(t * n_ff)?;
335                    Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, t * n_ff)?;
336                    e.matmul(ffn_down, &act, t)?
337                }
338                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il(e, m, &z, t, il as u16)?,
339            };
340            let mut x2 = e.uninit(t * n_embd)?;
341            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
342            x = x2;
343        }
344
345        let mut hn = e.uninit(t * n_embd)?;
346        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
347        let logits = e.matmul(&self.output, &hn, t)?;
348        Ok(e.dtoh(&logits)?)
349    }
350
351    /// Prefill that returns ONLY the last token's logits — the common case (greedy/sample needs
352    /// just the final position to start decode). Runs the trunk over all T, then the lm_head
353    /// (output.weight, the largest matrix — 248320 rows) on the LAST hidden row ONLY, not all T.
354    /// On a 512-token prompt this turns a [512,248320] GEMM into [1,248320] — the dominant prefill
355    /// cost (nsys: ~99ms when done for all T). Bit-identical last-row logits to forward()[last].
356    pub fn forward_last(&self, e: &Engine, tokens: &[u32]) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
357        if self.cfg.gemma4.is_some() { return self.gemma4_forward(e, tokens, true); }
358        let cfg = &self.cfg;
359        let n_embd = cfg.n_embd as usize;
360        let t = tokens.len();
361        let eps = cfg.rms_eps;
362        let pos: Vec<i32> = (0..t as i32).collect();
363        let pos_d = e.htod_i32(&pos)?;
364
365        let mut x = self.embed(e, tokens)?;   // [T, n_embd]
366        // MEMRA_LAYER_PROBE=1: synchronize + print after every stage — bisects an in-graph
367        // ILLEGAL_ADDRESS to (layer, stage) at ~1 line of output per layer (M3 bring-up tool).
368        let probe = std::env::var("MEMRA_LAYER_PROBE").is_ok();
369        for (il, layer) in self.layers.iter().enumerate() {
370            let mut h = e.uninit(t * n_embd)?;
371            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
372            if probe { e.stream().synchronize()?; eprintln!("[probe] L{il} norm ok"); }
373            let mixed = match &layer.mixer {
374                Mixer::Full(fa) => self.full_attn(e, fa, &h, &pos_d, t)?,
375                Mixer::Linear(la) => self.linear_attn(e, la, &h, t)?,
376                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
377            };
378            if probe { e.stream().synchronize()?; eprintln!("[probe] L{il} mixer ok"); }
379            let mut x1 = e.uninit(t * n_embd)?;
380            e.add(&x, &mixed, &mut x1, t * n_embd)?;
381            let mut z = e.uninit(t * n_embd)?;
382            e.rms_norm(&x1, layer.post_attn_norm.float_data(), &mut z, n_embd, t, eps)?;
383            let ffn_out = match &layer.ffn {
384                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
385                    let n_ff = ffn_gate.out_features();
386                    let mut g2 = e.matmul_group(&[ffn_gate, ffn_up], &z, t)?;
387                    let up = g2.pop().unwrap();
388                    let gate = g2.pop().unwrap();
389                    let mut act = e.uninit(t * n_ff)?;
390                    Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, t * n_ff)?;
391                    e.matmul(ffn_down, &act, t)?
392                }
393                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il(e, m, &z, t, il as u16)?,
394            };
395            if probe { e.stream().synchronize()?; eprintln!("[probe] L{il} ffn ok"); }
396            let mut x2 = e.uninit(t * n_embd)?;
397            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
398            x = x2;
399        }
400        // norm over all T, then slice the LAST row and run lm_head on that single row.
401        let mut hn = e.uninit(t * n_embd)?;
402        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
403        let last = e.view(&hn, t * n_embd);            // [T, n_embd]
404        let last_row = last.slice((t - 1) * n_embd..t * n_embd);  // [1, n_embd]
405        let mut hlast = e.uninit(n_embd)?;
406        e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
407        let logits = e.matmul(&self.output, &hlast, 1)?;   // [1, n_vocab] — lm_head on ONE row
408        Ok(e.dtoh(&logits)?)
409    }
410
411    /// BATCHED PROMPT PRIME (the measured #1 e2e gap, e2e-image-1): `forward_last`'s batched
412    /// prefill body EXTENDED to leave a DECODE-READY cache behind — vs the tokenwise prime's
413    /// ~102/38 tok/s (9B/27B) decode_step loop, this runs the whole prompt at prefill throughput.
414    ///   (a) full-attn layers append their T post-RoPE K/V rows into `cache.kv[il]` via the SAME
415    ///       per-row quantize kernel as the decode append (bit-identical cache bytes per row);
416    ///   (b) linear layers run STATEFULLY from the cache's current recurrent state (zero at a
417    ///       fresh prime): carried-ring conv (ssm_conv1d_tm_state) + ONE gdn_scan(state_in,
418    ///       state_out) whose internal sequential t-loop equals T chained T=1 steps — but with
419    ///       the NORMAL prefill matmul dispatch (GEMM at m>=16), NOT the decode-exact MMVQ the
420    ///       spec verify uses (prime is a prefill-regime pass; the run-gen prefill==decode
421    ///       argmax gate is the accuracy authority, exactly as for forward_last);
422    ///   (c) `cache.pos`/KV len/len_d advance by T.
423    /// Returns (last-row logits host, h_seed = last-row PRE-output_norm hidden [n_embd],
424    /// hiddens = the full pre-output_norm hidden stack [T, n_embd] — generate_spec's prompt_h).
425    /// FRESH-PROMPT ONLY (cache.pos == 0): the fa_prefill tiles attend within `tokens` alone.
426    /// forward_last itself stays untouched (kernel-check / run-gen gate on it).
427    pub fn prime_cache(&self, e: &Engine, tokens: &[u32], cache: &mut Cache)
428                       -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
429        let n_embd = self.cfg.n_embd as usize;
430        let t = tokens.len();
431        // SESSION CONTINUATION (2026-07-05): cache.pos > 0 = priming a NEW SUFFIX onto a live
432        // session cache — every chunk (including the first) takes the continuation arm
433        // (fa_prefill_view over the quantized past + this chunk). Fresh prime (pos==0) unchanged.
434        assert!(t >= PRIME_MIN_T, "prime_cache needs T >= {PRIME_MIN_T} (caller gates)");
435        assert!(cache.pos + t <= cache.max_ctx, "prime_cache: prompt exceeds cache max_ctx");
436
437        // CHUNKED PRIME (2026-07-05, the long-ctx OOM fix): the monolithic prime allocates
438        // per-layer transients proportional to T (gate/up/act = T*n_ff*4B EACH — 1.5GB apiece at
439        // 16k on the 27B), which OOMs a 24GB card around 16k prompt tokens. Chunk the prompt:
440        // each chunk runs the full layer stack with transients sized to the chunk, appending its
441        // K/V to the resident quantized cache and carrying the GDN conv-ring + recurrent state
442        // through `cache.recur` (linear_attn_prime is already stateful — a chunk boundary is
443        // exactly the state carry it was built for). Full-attn chunks after the first attend to
444        // the QUANTIZED past KV via fa_prefill_view (the spec-verify pattern) — same numeric
445        // class as decode reading the cache. Prompts <= one chunk take the ORIGINAL monolithic
446        // body byte-for-byte (chunk 0 short-circuits to the f32 fa_prefill path).
447        // MEMRA_PRIME_CHUNK sets the chunk size (tokens); 0 disables chunking (monolithic).
448        if self.is_gemma4_e4b() {
449            return self.gemma4_e4b_prime(e, tokens, cache);
450        }
451        if self.cfg.gemma4.is_some() {
452            // gemma4 v0: monolithic fresh-prompt prime (chunked/continuation arms later).
453            return self.gemma4_prime(e, tokens, cache);
454        }
455        let mut chunk: usize = std::env::var("MEMRA_PRIME_CHUNK").ok()
456            .and_then(|v| v.parse().ok()).unwrap_or(4096);
457        // CHUNK-ORDER INVARIANCE (lane/chunk-invariance, 2026-08-05; vLLM #38561 shape).
458        // MEMRA_PRIME_CHUNK is documented as a memory-transient knob, but it also decides
459        // the prefill's ARITHMETIC, so two rigs with different values produced different
460        // greedy text for the same prompt (research/session-affinity-20260805: 97- and
461        // 149-token prompts). ROOT CAUSE, measured in research/chunk-invariance-20260805
462        // (VERDICT.md) — and it is NOT what docs originally said:
463        //   * NOT trunk GEMM m / reduction order. REFUTED: the prefill GEMM is m-INVARIANT
464        //     (rows [0,32) bit-identical at m=32 vs m=33..80, both quantized wq and the
465        //     output head), so growing a chunk cannot move an existing row's value.
466        //   * NOT the GDN scan segmentation. REFUTED: MEMRA_GDN_CHUNKED=0 (sequential scan,
467        //     no WY segmentation at all) still diverges, so vLLM's mamba-boundary fix does
468        //     not describe our leak.
469        //   * IT IS the attention numeric CLASS edge in full_attn_prime_fa_dispatch, which
470        //     selects on `base_len == 0`: chunk 0 attends over this batch's f32 K/V
471        //     (fa_prefill) while every later chunk attends over the q8_0/q5_1 quantized KV
472        //     cache (fa_prefill_view_ws). The chunk size therefore decides WHERE in the
473        //     prompt that precision edge falls. Signature: per-row maxdiff is exactly 0.0
474        //     before the first boundary and O(1) right after, first_div_pos == chunk size.
475        // Pinning split points to a fixed grain makes that edge land at the same position on
476        // every rig, which is sufficient for bit-identity here (measured: 4/4 arms exact).
477        // Under the door MEMRA_PRIME_CHUNK no longer steers arithmetic — MEMRA_PRIME_GRAIN
478        // becomes both the (explicitly numeric) knob and the transient bound. The stronger
479        // fix that needs no grain knob at all is to drop the `base_len == 0` f32 special case
480        // so every row is in one class; that trades the unchunked fast path and owns its own
481        // arm (see VERDICT.md "a cheaper stronger fix").
482        if Self::prime_invariant() {
483            chunk = Self::prime_grain();
484        }
485        if chunk == 0 || t <= chunk {
486            return self.prime_chunk(e, tokens, cache);
487        }
488        let mut hiddens = e.uninit(t * n_embd)?;
489        let mut last: Option<(Vec<f32>, CudaSlice<f32>)> = None;
490        let mut start = 0usize;
491        while start < t {
492            // keep the tail chunk >= PRIME_MIN_T (the stateful conv needs T >= d_conv-1).
493            let mut end = (start + chunk).min(t);
494            if t - end > 0 && t - end < PRIME_MIN_T { end = t; }
495            let (l, hs, x) = self.prime_chunk(e, &tokens[start..end], cache)?;
496            e.copy_into(&mut hiddens, start * n_embd, &x, (end - start) * n_embd)?;
497            last = Some((l, hs));
498            start = end;
499        }
500        let (logits, h_seed) = last.unwrap();
501        Ok((logits, h_seed, hiddens))
502    }
503
504    /// One prime chunk: the full layer stack over `tokens`, continuing from the cache's current
505    /// state (`cache.pos` = tokens already primed; 0 = fresh). Positions/RoPE are absolute
506    /// task #21 de-broadcast: the q/k head count PREP must emit so the scan's consumers
507    /// agree. Compact (num_k) ONLY when the scan will take the chunked+mma route AND
508    /// num_k == num_v/2 (the engine-side hint mirrors this exact formula); everything
509    /// else (s128 verify tier, chunked-off, mma-off) keeps the broadcast layout.
510    fn gdn_hk(e: &Engine, t: usize, num_v: usize, num_k: usize) -> usize {
511        if Engine::gdn_db_on()
512            && Engine::gdn_chunked_enabled() && t >= 16
513            && e.gdn_mma_enabled(Engine::gdn_chunk_size())
514            && num_k * 2 == num_v
515        {
516            num_k
517        } else {
518            num_v
519        }
520    }
521
522    /// task #17: gate for the fused fp16-operand epilogues (silu_mul/gated_rmsnorm/sig_mul
523    /// `_f16out` twins). Bit-identical class (the twins emit the cvt kernel's exact halves),
524    /// but seam-gated (MEMRA_F16OUT=0) so a bit-check can arbitrate, and OFF under verify-exact
525    /// (matmul_group skips the f16 lane there; the twins must not resurrect it).
526    fn f16out_on(e: &Engine, t: usize) -> bool {
527        crate::f16_ffi::pp_f16_enabled() && t >= 16 && !e.verify_exact_on()
528            && std::env::var("MEMRA_F16OUT").as_deref() != Ok("0")
529    }
530
531    /// (cache.pos + i). Returns (last-row logits, h_seed, this chunk's hidden stack [T, n_embd]).
532    /// See HybridModel::prime_slabs — the eager prime's resident trunk transients.
533    pub fn prime_slabs_get(&self, e: &Engine, t: usize, n_embd: usize, n_ff_max: usize)
534                           -> Result<std::sync::MutexGuard<'_, Option<PrimeSlabs>>, Box<dyn std::error::Error>> {
535        let mut g = self.prime_slabs.lock().unwrap();
536        let need_new = match g.as_ref() { None => true, Some(sl) => sl.t_cap < t };
537        if need_new {
538            *g = Some(PrimeSlabs {
539                t_cap: t,
540                h: e.uninit(t * n_embd)?,
541                x1: e.uninit(t * n_embd)?,
542                z: e.uninit(t * n_embd)?,
543                act: e.uninit(t * n_ff_max)?,
544                xa: e.uninit(t * n_embd)?,
545                xb: e.uninit(t * n_embd)?,
546                h16: e.alloc_u8_uninit(t * n_embd * 2)?,
547                z16: e.alloc_u8_uninit(t * n_embd * 2)?,
548                gate: e.uninit(t * n_ff_max)?,
549                up: e.uninit(t * n_ff_max)?,
550                ffn_out: e.uninit(t * n_embd)?,
551                seg_glue: Vec::new(),
552                mixed: e.uninit(t * n_embd)?,
553                seg_mid: Vec::new(),
554                seg_t: 0,
555            });
556        }
557        Ok(g)
558    }
559
560    fn prime_chunk(&self, e: &Engine, tokens: &[u32], cache: &mut Cache)
561                       -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
562        let cfg = &self.cfg;
563        let n_embd = cfg.n_embd as usize;
564        let t = tokens.len();
565        let eps = cfg.rms_eps;
566        let base = cache.pos;
567        let pos: Vec<i32> = (base as i32..(base + t) as i32).collect();
568        let pos_d = e.htod_i32(&pos)?;
569
570        let x_embed = self.embed(e, tokens)?;   // [T, n_embd]
571        // task #14: fuse the fp16 GEMM-operand emission into the trunk norms (kills the
572        // standalone convert launches). Only when the f16 lane serves and T reaches the
573        // GEMM tier; bit-identical either way.
574        let f16fuse = crate::f16_ffi::pp_f16_enabled() && t >= 16;
575        // PRIME SLABS (piecewise foundation): trunk transients in resident buffers —
576        // ~224 fewer alloc/free calls per prime and FROZEN Lt operand addresses
577        // (nvjet's alignment-variant selection becomes run-to-run stable). Every slab is
578        // fully overwritten before use; x ping-pongs xa<->xb; the hidden-stack return
579        // clones the final x (the slab cannot leave). Non-slab fallback: MEMRA_PRIME_SLABS=0.
580        let n_ff_max = self.layers.iter().map(|l| match &l.ffn {
581            crate::hybrid::Ffn::Dense { ffn_gate, .. } => ffn_gate.out_features(),
582            _ => n_embd,
583        }).max().unwrap_or(n_embd).max(n_embd);
584        let use_slabs = std::env::var("MEMRA_PRIME_SLABS").as_deref() != Ok("0");
585        let mut slab_guard = if use_slabs {
586            Some(self.prime_slabs_get(e, t, n_embd, n_ff_max)?)
587        } else {
588            None
589        };
590        let mut x_own;   // fallback storage when slabs are off
591        type SlabRefs<'a> = (&'a mut CudaSlice<f32>, &'a mut CudaSlice<f32>, &'a mut CudaSlice<f32>, &'a mut CudaSlice<f32>, &'a mut CudaSlice<u8>, &'a mut CudaSlice<u8>, &'a mut CudaSlice<f32>, &'a mut CudaSlice<f32>, &'a mut CudaSlice<f32>);
592        let (mut x_cur, mut x_nxt, sl): (&mut CudaSlice<f32>, &mut CudaSlice<f32>, Option<SlabRefs>);
593        let mut seg: Option<(&mut Vec<Option<cudarc::driver::CudaGraph>>, &mut Vec<Option<cudarc::driver::CudaGraph>>, &mut CudaSlice<f32>, &mut usize)> = None;
594        let mut x_own2;
595        match slab_guard.as_mut() {
596            Some(g) => {
597                let slabs = g.as_mut().unwrap();
598                e.copy_into(&mut slabs.xa, 0, &x_embed, t * n_embd)?;
599                let PrimeSlabs { xa, xb, h, x1, z, act, h16, z16, gate, up, ffn_out, seg_glue, mixed, seg_mid, seg_t, .. } = slabs;
600                x_cur = xa;
601                x_nxt = xb;
602                seg = Some((seg_glue, seg_mid, mixed, seg_t));
603                sl = Some((h, x1, z, act, h16, z16, gate, up, ffn_out));
604            }
605            None => {
606                x_own = x_embed;
607                x_own2 = e.uninit(t * n_embd)?;
608                x_cur = &mut x_own;
609                x_nxt = &mut x_own2;
610                sl = None;
611            }
612        }
613        let mut alloc_h; let mut alloc_x1; let mut alloc_z; let mut alloc_act;
614        let mut alloc_h16; let mut alloc_z16;
615        let mut alloc_gate; let mut alloc_up; let mut alloc_fo;
616        let (h, x1, z, act): (&mut CudaSlice<f32>, &mut CudaSlice<f32>, &mut CudaSlice<f32>, &mut CudaSlice<f32>);
617        let (h16, z16): (&mut CudaSlice<u8>, &mut CudaSlice<u8>);
618        let (sl_gate, sl_up, sl_fo): (&mut CudaSlice<f32>, &mut CudaSlice<f32>, &mut CudaSlice<f32>);
619        match sl {
620            Some((a, b, c, d, e16, f16b, g, u, fo)) => {
621                h = a; x1 = b; z = c; act = d; h16 = e16; z16 = f16b;
622                sl_gate = g; sl_up = u; sl_fo = fo;
623            }
624            None => {
625                alloc_h = e.uninit(t * n_embd)?;
626                alloc_x1 = e.uninit(t * n_embd)?;
627                alloc_z = e.uninit(t * n_embd)?;
628                alloc_act = e.uninit(t * n_ff_max)?;
629                alloc_h16 = e.alloc_u8_uninit(t * n_embd * 2)?;
630                alloc_z16 = e.alloc_u8_uninit(t * n_embd * 2)?;
631                alloc_gate = e.uninit(t * n_ff_max)?;
632                alloc_up = e.uninit(t * n_ff_max)?;
633                alloc_fo = e.uninit(t * n_embd)?;
634                h = &mut alloc_h; x1 = &mut alloc_x1; z = &mut alloc_z; act = &mut alloc_act;
635                h16 = &mut alloc_h16; z16 = &mut alloc_z16;
636                sl_gate = &mut alloc_gate; sl_up = &mut alloc_up; sl_fo = &mut alloc_fo;
637            }
638        }
639        // piecewise increment 3: layer-0 norm hoisted; the per-layer tail fuses
640        // [down-add + NEXT layer's attn-norm] into one captured S-glue segment
641        // (all-slab IO, zero in-graph allocations). Capture happens lazily on the
642        // first prime at this t (capture does not execute -> launch right after).
643        let n_layers = self.layers.len();
644        // OPT-IN (2026-07-26 interleaved verdict): 2-kernel segments measured NET
645        // -0.7%-to-neutral under the interleaved A/B protocol — one cuGraphLaunch costs
646        // about what two kernel submissions do. The earlier "+0.9%" was cross-run clock
647        // drift (the repo's interleaved-A/B law exists for exactly this). Larger segments
648        // (S-prep/S-attn, 7-9 kernels) remain the open hypothesis; the core-split
649        // machinery stays (byte-identical) as their foundation.
650        let use_seg = f16fuse && seg.is_some()
651            && std::env::var("MEMRA_PRIME_SEG").as_deref() == Ok("1");
652        if let Some((sg, sm, _, st)) = seg.as_mut() {
653            if **st != t {
654                sg.clear();
655                sg.extend((0..n_layers).map(|_| None));
656                sm.clear();
657                sm.extend((0..n_layers).map(|_| None));
658                **st = t;
659            }
660        }
661        {
662            let layer0 = &self.layers[0];
663            if f16fuse {
664                e.rms_norm_f16out(x_cur, layer0.attn_norm.float_data(), h, h16, n_embd, t, eps)?;
665            } else {
666                e.rms_norm(x_cur, layer0.attn_norm.float_data(), h, n_embd, t, eps)?;
667            }
668        }
669        for (il, layer) in self.layers.iter().enumerate() {
670            let hx16 = if f16fuse { Some(&*h16) } else { None };
671            if use_seg {
672                // core-split path: projections -> _inner core -> out-GEMM INTO the mixed
673                // slab (no copies) -> S-mid segment [add + post-norm] as one graph launch.
674                let (pre, pre16, w_out) = match &layer.mixer {
675                    Mixer::Full(fa) => {
676                        let g3 = match hx16 {
677                            Some(xh) => e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], h, xh, t)?,
678                            None => e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?,
679                        };
680                        let (pre, pre16) = self.full_attn_prime_core_inner(e, fa, g3, &pos_d, t, cache, il)?;
681                        (pre, pre16, &fa.wo)
682                    }
683                    Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
684                    Mixer::Linear(la) => {
685                        let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
686                        let g4 = match hx16 {
687                            Some(xh) => e.matmul_group_xh(&ws, h, xh, t)?,
688                            None => e.matmul_group(&ws, h, t)?,
689                        };
690                        let (pre, pre16) = self.linear_attn_prime_core_pad_inner(e, la, g4, t, cache, il, None)?;
691                        (pre, pre16, &la.ssm_out)
692                    }
693                };
694                {
695                    let (_, sm, mslab, _) = seg.as_mut().unwrap();
696                    let pre_n = pre.len() / t;
697                    let xh_pre = match pre16 {
698                        Some(x) => x,
699                        None => e.f16_act(&pre, t * pre_n, pre_n)?,
700                    };
701                    if !e.try_f16_gemm_pre_into(w_out, &xh_pre, t, mslab)? {
702                        let y = e.matmul(w_out, &pre, t)?;
703                        e.copy_into(mslab, 0, &y, t * n_embd)?;
704                    }
705                    if sm[il].is_none() {
706                        use cudarc::driver::sys::{CUgraphInstantiate_flags, CUstreamCaptureMode};
707                        let w_post = layer.post_attn_norm.float_data();
708                        e.stream().synchronize()?;
709                        e.stream().begin_capture(CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED)?;
710                        let r = (|| -> Result<(), Box<dyn std::error::Error>> {
711                            e.add(x_cur, mslab, x1, t * n_embd)?;
712                            e.rms_norm_f16out(x1, w_post, z, z16, n_embd, t, eps)?;
713                            Ok(())
714                        })();
715                        let g = e.stream().end_capture(
716                            CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH);
717                        r?;
718                        sm[il] = Some(g?.ok_or("S-mid capture produced no graph")?);
719                    }
720                    sm[il].as_ref().unwrap().launch()?;
721                }
722            } else {
723                let mixed = match &layer.mixer {
724                    Mixer::Full(fa) => self.full_attn_prime(e, fa, h, hx16, &pos_d, t, cache, il)?,
725                    Mixer::Linear(la) => self.linear_attn_prime(e, la, h, hx16, t, cache, il)?,
726                    Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
727                };
728                if f16fuse {
729                    // round 28: residual+norm in ONE kernel (add_rms_norm precedent,
730                    // bit-identical) — the standalone add pass disappears.
731                    e.add_rms_norm_f16out(x_cur, &mixed, layer.post_attn_norm.float_data(),
732                                          x1, z, z16, n_embd, t, eps)?;
733                } else {
734                    e.add(x_cur, &mixed, x1, t * n_embd)?;
735                    e.rms_norm(x1, layer.post_attn_norm.float_data(), z, n_embd, t, eps)?;
736                }
737            }
738            let zx16 = if f16fuse { Some(&*z16) } else { None };
739            match &layer.ffn {
740                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
741                    let n_ff = ffn_gate.out_features();
742                    // gate/up INTO boundary slabs (piecewise increment 2); fall back to
743                    // the allocating group + copy when a mirror is missing.
744                    let mut into_ok = false;
745                    if let Some(xh) = zx16 {
746                        into_ok = e.try_f16_gemm_pre_into(ffn_gate, xh, t, sl_gate)?
747                            && e.try_f16_gemm_pre_into(ffn_up, xh, t, sl_up)?;
748                    }
749                    if !into_ok {
750                        let mut g2 = match zx16 {
751                            Some(xh) => e.matmul_group_xh(&[ffn_gate, ffn_up], z, xh, t)?,
752                            None => e.matmul_group(&[ffn_gate, ffn_up], z, t)?,
753                        };
754                        let up_y = g2.pop().unwrap();
755                        let gate_y = g2.pop().unwrap();
756                        e.copy_into(sl_gate, 0, &gate_y, t * n_ff)?;
757                        e.copy_into(sl_up, 0, &up_y, t * n_ff)?;
758                    }
759                    // task #17: the silu arm's f16out twin emits the down GEMM's fp16
760                    // operand in-epilogue; non-silu activations keep the standalone convert.
761                    let act16 = if Self::f16out_on(e, t) && self.cfg.m3.is_none() {
762                        let mut a16 = e.alloc_u8_uninit(t * n_ff * 2)?;
763                        e.silu_mul_f16out(sl_gate, sl_up, act, &mut a16, t * n_ff)?;
764                        Some(a16)
765                    } else {
766                        Self::ffn_act(e, &self.cfg, sl_gate, sl_up, act, t * n_ff)?;
767                        None
768                    };
769                    // down GEMM into the ffn_out slab (f16 arm; fallback copies)
770                    let xh_act = match act16 {
771                        Some(x) => x,
772                        None => e.f16_act(act, t * n_ff, n_ff)?,
773                    };
774                    if !e.try_f16_gemm_pre_into(ffn_down, &xh_act, t, sl_fo)? {
775                        let y = e.matmul(ffn_down, &*act, t)?;
776                        e.copy_into(sl_fo, 0, &y, t * n_embd)?;
777                    }
778                }
779                crate::hybrid::Ffn::Moe(m) => {
780                    let y = self.moe_ffn_il(e, m, z, t, il as u16)?;
781                    e.copy_into(sl_fo, 0, &y, t * n_embd)?;
782                }
783            }
784            if use_seg && il + 1 < n_layers {
785                // S-glue segment: [add + next attn-norm(+f16out)] — one cuGraphLaunch
786                let w_next = self.layers[il + 1].attn_norm.float_data();
787                let (sg, _, _, _) = seg.as_mut().unwrap();
788                if sg[il].is_none() {
789                    use cudarc::driver::sys::{CUgraphInstantiate_flags, CUstreamCaptureMode};
790                    e.stream().synchronize()?;
791                    e.stream().begin_capture(CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED)?;
792                    let r = (|| -> Result<(), Box<dyn std::error::Error>> {
793                        e.add(x1, sl_fo, x_nxt, t * n_embd)?;
794                        e.rms_norm_f16out(x_nxt, w_next, h, h16, n_embd, t, eps)?;
795                        Ok(())
796                    })();
797                    let g = e.stream().end_capture(
798                        CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH);
799                    r?;
800                    sg[il] = Some(g?.ok_or("S-glue capture produced no graph")?);
801                }
802                sg[il].as_ref().unwrap().launch()?;
803            } else {
804                if il + 1 < n_layers {
805                    let w_next = self.layers[il + 1].attn_norm.float_data();
806                    if f16fuse {
807                        e.add_rms_norm_f16out(x1, sl_fo, w_next, x_nxt, h, h16, n_embd, t, eps)?;
808                    } else {
809                        e.add(x1, sl_fo, x_nxt, t * n_embd)?;
810                        e.rms_norm(x_nxt, w_next, h, n_embd, t, eps)?;
811                    }
812                } else {
813                    e.add(x1, sl_fo, x_nxt, t * n_embd)?;
814                }
815            }
816            // CHUNK-INVARIANCE BISECT SEAM (lane/chunk-invariance, 2026-08-05): dump the
817            // per-layer post-residual hidden for the LAST row of this chunk, keyed by
818            // absolute position, so two runs at different MEMRA_PRIME_CHUNK can be diffed
819            // layer-by-layer to find the FIRST diverging layer. Diagnostic only —
820            // unset (the default) costs one OnceLock read per layer.
821            if let Some(path) = Self::prime_trace_path() {
822                let row = (base + t - 1) as usize;
823                let host = e.dtoh(x_nxt)?;
824                let last = &host[(t - 1) * n_embd..t * n_embd];
825                use std::io::Write as _;
826                let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
827                let mut h64: u64 = 0xcbf29ce484222325;
828                for v in last {
829                    h64 ^= v.to_bits() as u64;
830                    h64 = h64.wrapping_mul(0x100000001b3);
831                }
832                writeln!(f, "{{\"pos\":{row},\"layer\":{il},\"t\":{t},\"base\":{base},\
833                             \"hash\":\"{h64:016x}\",\"v0\":{:.9e},\"v1\":{:.9e},\"v2\":{:.9e}}}",
834                         last[0], last[1], last[2])?;
835            }
836            std::mem::swap(&mut x_cur, &mut x_nxt);
837        }
838        // hidden-stack return: clone the final x out of the slab
839        let mut x = e.uninit(t * n_embd)?;
840        e.copy_into(&mut x, 0, x_cur, t * n_embd)?;
841        drop(slab_guard);
842
843        // h_seed = LAST row of x BEFORE output_norm (MTP-PLAN §A default) or AFTER it
844        // (MEMRA_SPEC_HPOST — reference convention; hn is computed just below either way, so
845        // the post-norm copy happens after hn exists).
846        let mut h_seed = e.uninit(n_embd)?;
847        if !crate::spec::spec_hpost() {
848            e.copy_view_into(&mut h_seed, 0, &x.slice((t - 1) * n_embd..t * n_embd), n_embd)?;
849        }
850        // last-row logits, exactly like forward_last (norm all T — per-row op — then lm_head on 1 row).
851        let mut hn = e.uninit(t * n_embd)?;
852        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
853        if crate::spec::spec_hpost() {
854            e.copy_view_into(&mut h_seed, 0, &hn.slice((t - 1) * n_embd..t * n_embd), n_embd)?;
855        }
856        let last = e.view(&hn, t * n_embd);
857        let last_row = last.slice((t - 1) * n_embd..t * n_embd);
858        let mut hlast = e.uninit(n_embd)?;
859        e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
860        let logits = e.matmul(&self.output, &hlast, 1)?;
861        cache.pos += t;
862        // Hidden stack handed to generate_spec as prompt_h: pre-norm x (default) or the full
863        // post-norm stack hn (MEMRA_SPEC_HPOST).
864        Ok((e.dtoh(&logits)?, h_seed, if crate::spec::spec_hpost() { hn } else { x }))
865    }
866
867    /// CAPTURE-SAFE prime trunk (task #14 increment 1, ARCHITECTURE-H100.md design v2):
868    /// prime_chunk's layer stack with every capture hazard hoisted — `x` is the
869    /// PRE-EMBEDDED input in a STABLE graph-input buffer (embed + its 8MB htod stay
870    /// eager, one launch), `pos_d` is a baked device param (fresh prime = 0..T, constant
871    /// per bucket), logits/h_seed/hidden stay DEVICE-resident (no dtoh), and cache.pos is
872    /// NOT advanced (host state — the replay wrapper owns it). Body mirrors prime_chunk
873    /// (the prime-graph-gate pins them together). KNOWN smoke-scope gap: append host-len
874    /// bookkeeping still runs on the host per call — the real replay path moves the write
875    /// slot to the len_d device counter (increment 3).
876    /// GRAPH-OUTPUT CONTRACT: results are COPIED into caller-provided stable buffers
877    /// (`logits_out` [n_vocab], `h_seed_out` [n_embd]) — every internal allocation drops
878    /// INSIDE the capture region (alloc+free node pairs). Retaining an in-capture
879    /// allocation across end_capture makes instantiate throw INVALID_VALUE (smoke finding
880    /// 2026-07-26), and under AUTO_FREE_ON_LAUNCH its address wouldn't survive a launch
881    /// anyway — the decode GraphSession's pre-allocated-output pattern is the law here.
882    pub fn prime_chunk_captured(&self, e: &Engine, x_in: &CudaSlice<f32>, pos_d: &CudaSlice<i32>,
883                                t: usize, cache: &mut Cache,
884                                len_d: &CudaSlice<i32>,
885                                logits_out: &mut CudaSlice<f32>, h_seed_out: &mut CudaSlice<f32>)
886                                -> Result<(), Box<dyn std::error::Error>> {
887        let cfg = &self.cfg;
888        let n_embd = cfg.n_embd as usize;
889        let eps = cfg.rms_eps;
890        let f16fuse = crate::f16_ffi::pp_f16_enabled() && t >= 16;
891        let mut x = e.uninit(t * n_embd)?;
892        e.copy_into(&mut x, 0, x_in, t * n_embd)?;
893        for (il, layer) in self.layers.iter().enumerate() {
894            let mut h = e.uninit(t * n_embd)?;
895            let mut hx16: Option<CudaSlice<u8>> = None;
896            if f16fuse {
897                let mut b16 = e.alloc_u8_uninit(t * n_embd * 2)?;
898                e.rms_norm_f16out(&x, layer.attn_norm.float_data(), &mut h, &mut b16, n_embd, t, eps)?;
899                hx16 = Some(b16);
900            } else {
901                e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
902            }
903            let mixed = match &layer.mixer {
904                Mixer::Full(fa) => self.full_attn_prime(e, fa, &h, hx16.as_ref(), pos_d, t, cache, il)?,
905                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
906                Mixer::Linear(la) => {
907                    let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
908                    let g4 = match hx16.as_ref() {
909                        Some(xh) => e.matmul_group_xh(&ws, &h, xh, t)?,
910                        None => e.matmul_group(&ws, &h, t)?,
911                    };
912                    self.linear_attn_prime_core_pad(e, la, g4, t, cache, il, Some(len_d))?
913                }
914            };
915            let mut x1 = e.uninit(t * n_embd)?;
916            e.add(&x, &mixed, &mut x1, t * n_embd)?;
917            let mut z = e.uninit(t * n_embd)?;
918            let mut zx16: Option<CudaSlice<u8>> = None;
919            if f16fuse {
920                let mut b16 = e.alloc_u8_uninit(t * n_embd * 2)?;
921                e.rms_norm_f16out(&x1, layer.post_attn_norm.float_data(), &mut z, &mut b16, n_embd, t, eps)?;
922                zx16 = Some(b16);
923            } else {
924                e.rms_norm(&x1, layer.post_attn_norm.float_data(), &mut z, n_embd, t, eps)?;
925            }
926            let ffn_out = match &layer.ffn {
927                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
928                    let n_ff = ffn_gate.out_features();
929                    let mut g2 = match &zx16 {
930                        Some(xh) => e.matmul_group_xh(&[ffn_gate, ffn_up], &z, xh, t)?,
931                        None => e.matmul_group(&[ffn_gate, ffn_up], &z, t)?,
932                    };
933                    let up = g2.pop().unwrap();
934                    let gate = g2.pop().unwrap();
935                    let mut act = e.uninit(t * n_ff)?;
936                    Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, t * n_ff)?;
937                    e.matmul(ffn_down, &act, t)?
938                }
939                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il(e, m, &z, t, il as u16)?,
940            };
941            let mut x2 = e.uninit(t * n_embd)?;
942            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
943            x = x2;
944        }
945        // device-indexed TRUE last row (pads sit past it in a bucketed graph)
946        if !crate::spec::spec_hpost() {
947            e.row_gather_dev(&x, h_seed_out, len_d, n_embd)?;
948        }
949        let mut hn = e.uninit(t * n_embd)?;
950        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
951        if crate::spec::spec_hpost() {
952            e.row_gather_dev(&hn, h_seed_out, len_d, n_embd)?;
953        }
954        let mut hlast = e.uninit(n_embd)?;
955        e.row_gather_dev(&hn, &mut hlast, len_d, n_embd)?;
956        let logits = e.matmul(&self.output, &hlast, 1)?;
957        let nv = logits.len();
958        e.copy_into(logits_out, 0, &logits, nv)?;
959        Ok(())
960    }
961
962    /// Cross-request BATCHED fresh prime (task #13, design in ARCHITECTURE-H100.md): the
963    /// trunk's token-parallel ops (embed, norms, adds, ffn, projection GROUPS) run once on
964    /// the CONCATENATION of B sequences — GEMMs at m = sum_T, the continuous-batching win
965    /// the serving-lane bench attributed the remaining vLLM gap to. The stateful mixer
966    /// CORES (QK-norm/RoPE/FA/append, conv/GDN scans) run per sequence on split projection
967    /// buffers (D2D row copies; the mixers' own out-projections stay per-seq this
968    /// increment). CONTINUATION primes (increment (b), 2026-07-30): cache.pos > 0 seqs
969    /// batch the projections/FFN/lm_head exactly like fresh; the mixer cores take the
970    /// per-seq CONTINUATION arms (Full: core_inner with carried pos_d + fa_prefill_view
971    /// over the quantized past; Linear: the stateful pad_view twin — the same state
972    /// carry the chunked single-seq prime rides). The fresh-only favl/gdn-vl fast paths
973    /// stay byte-identical (gated on !carried). gemma4 models have no continuation
974    /// prime (v0 monolithic fresh) — carried gemma4 batches return Err (caller falls
975    /// back to single-chunk serving).
976    /// NUMERIC CONFIG: a concat GEMM tiles K differently than per-seq GEMMs — same class
977    /// as every prefill GEMM change; prime_batch_gate arbitrates (argmax + stream battery).
978    pub fn prime_cache_batch(&self, e: &Engine, prompts: &[&[u32]], caches: &mut [&mut Cache])
979                             -> Result<Vec<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>)>, Box<dyn std::error::Error>> {
980        let cfg = &self.cfg;
981        let n_embd = cfg.n_embd as usize;
982        let eps = cfg.rms_eps;
983        let b = prompts.len();
984        assert!(b >= 1 && b == caches.len());
985        let pos0s: Vec<usize> = caches.iter().map(|c| c.pos).collect();
986        let carried = pos0s.iter().any(|&p| p > 0);
987        if carried && cfg.gemma4.is_some() {
988            return Err("prime_cache_batch: gemma4 has no continuation prime (v0 fresh-only)".into());
989        }
990        let ts: Vec<usize> = prompts.iter().map(|p| p.len()).collect();
991        for &t in &ts { assert!(t >= PRIME_MIN_T, "prime_cache_batch needs T >= {PRIME_MIN_T}"); }
992        for (s, c) in caches.iter().enumerate() {
993            assert!(c.pos + ts[s] <= c.max_ctx, "prime_cache_batch: prompt exceeds cache max_ctx");
994        }
995        let total: usize = ts.iter().sum();
996        let offs: Vec<usize> = ts.iter().scan(0usize, |a, &t| { let o = *a; *a += t; Some(o) }).collect();
997        // per-seq positions (fresh: 0..T_s; continuation: pos0..pos0+T_s)
998        let pos_ds: Vec<CudaSlice<i32>> = ts.iter().zip(&pos0s)
999            .map(|(&t, &p0)| e.htod_i32(&(p0 as i32..(p0 + t) as i32).collect::<Vec<_>>()))
1000            .collect::<Result<_, _>>()?;
1001        // split a concat [total, dim] buffer into per-seq copies
1002        let split = |e: &Engine, y: &CudaSlice<f32>, dim: usize|
1003                     -> Result<Vec<CudaSlice<f32>>, Box<dyn std::error::Error>> {
1004            let mut out = Vec::with_capacity(b);
1005            for s in 0..b {
1006                let mut ys = e.uninit(ts[s] * dim)?;
1007                e.copy_view_into(&mut ys, 0, &y.slice(offs[s] * dim..(offs[s] + ts[s]) * dim), ts[s] * dim)?;
1008                out.push(ys);
1009            }
1010            Ok(out)
1011        };
1012
1013        let cat_tokens: Vec<u32> = prompts.iter().flat_map(|p| p.iter().copied()).collect();
1014        let mut x = self.embed(e, &cat_tokens)?;   // [total, n_embd]
1015        for (il, layer) in self.layers.iter().enumerate() {
1016            let mut h = e.uninit(total * n_embd)?;
1017            let mut hx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
1018            e.rms_norm_f16out(&x, layer.attn_norm.float_data(), &mut h, &mut hx16, n_embd, total, eps)?;
1019            // mixer: projection GROUP on the concat (m = total), stateful core per seq
1020            let mut mixed = e.uninit(total * n_embd)?;
1021            match &layer.mixer {
1022                Mixer::Full(fa) => {
1023                    let g3 = e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], &h, &hx16, total)?;
1024                    // task #18 (attn side): the WHOLE attn core is varlen for fresh gated
1025                    // batches — split/QK-norm/RoPE/append (attn_pre_vl8, view inputs: the
1026                    // q/k/v split copies vanish) + ONE varlen FA. Per-block math identical
1027                    // everywhere (bit-gateable); MEMRA_FA_VL=0 or a non-bf16kv config falls
1028                    // back to the per-seq dispatch.
1029                    let (n_head, n_head_kv, head_dim) =
1030                        (self.cfg.n_head as usize, self.cfg.n_head_kv as usize, self.cfg.head_dim_k as usize);
1031                    let fa_scale = 1.0 / (head_dim as f32).sqrt();
1032                    let use_favl = !carried
1033                        && (2..=8).contains(&b)
1034                        && (head_dim == 256 || head_dim == 128)
1035                        && self.cfg.attn_out_gate()
1036                        && std::env::var("MEMRA_NOFA").is_err()
1037                        && std::env::var("MEMRA_FA_FLOOR").is_err()
1038                        && std::env::var("MEMRA_FA_PP_W2").as_deref() != Ok("1")
1039                        && std::env::var("MEMRA_FA_BF16KV").as_deref() != Ok("0")
1040                        && std::env::var("MEMRA_FA_VL").as_deref() != Ok("0");
1041                    if use_favl {
1042                        let (qf_w, kf_w, vf_w) =
1043                            (fa.wq.out_features(), fa.wk.out_features(), fa.wv.out_features());
1044                        struct APre {
1045                            q: CudaSlice<f32>, gate: Option<CudaSlice<f32>>,
1046                            qn: CudaSlice<f32>, kn: CudaSlice<f32>,
1047                        }
1048                        let mut aps = Vec::with_capacity(b);
1049                        for &t in ts.iter().take(b) {
1050                            aps.push(APre {
1051                                q: e.uninit(t * n_head * head_dim)?,
1052                                gate: Some(e.uninit(t * n_head * head_dim)?),
1053                                qn: e.uninit(t * n_head * head_dim)?,
1054                                kn: e.uninit(t * n_head_kv * head_dim)?,
1055                            });
1056                        }
1057                        let (kv_dim_k, kv_dim_v, ktb, vtb) = {
1058                            let kvl = caches[0].kv[il].as_ref().unwrap();
1059                            (kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes)
1060                        };
1061                        let pargs: Vec<crate::AttnPreVl> = (0..b).map(|s| {
1062                            let (o, t) = (offs[s], ts[s]);
1063                            let kvl = caches[s].kv[il].as_ref().unwrap();
1064                            assert!(kvl.len == 0 && kvl.len + t <= caches[s].max_ctx,
1065                                    "prime_cache_batch attn vl: fresh + capacity");
1066                            crate::AttnPreVl {
1067                                qf: e.addr_f32v(&g3[0].slice(o * qf_w..(o + t) * qf_w)),
1068                                kf: e.addr_f32v(&g3[1].slice(o * kf_w..(o + t) * kf_w)),
1069                                vf: e.addr_f32v(&g3[2].slice(o * vf_w..(o + t) * vf_w)),
1070                                q: e.addr_f32(&aps[s].q),
1071                                gate: e.addr_f32(aps[s].gate.as_ref().unwrap()),
1072                                qn: e.addr_f32(&aps[s].qn), kn: e.addr_f32(&aps[s].kn),
1073                                kc: e.addr_u8(&kvl.k), vc: e.addr_u8(&kvl.v),
1074                                t: t as i32, pad: 0,
1075                            }
1076                        }).collect();
1077                        e.attn_pre_vl8(&pargs, fa.q_norm.float_data(), fa.k_norm.float_data(),
1078                                       head_dim, self.cfg.rope_dim_count as usize, n_head, n_head_kv,
1079                                       self.cfg.rms_eps, self.cfg.rope_freq_base, 1.0,
1080                                       kv_dim_k, kv_dim_v, ktb, vtb)?;
1081                        for s in 0..b {
1082                            let kvl = caches[s].kv[il].as_mut().unwrap();
1083                            kvl.len += ts[s];
1084                            let new_len = kvl.len as i32;
1085                            e.set_i32_one(&mut kvl.len_d, new_len)?;
1086                        }
1087                        let mut attns = Vec::with_capacity(b);
1088                        let mut mirrors = Vec::with_capacity(b);
1089                        for &t in ts.iter().take(b) {
1090                            attns.push(e.uninit(t * n_head * head_dim)?);
1091                            let n = t * n_head_kv * head_dim;
1092                            mirrors.push((e.alloc_u8_uninit(n * 2)?, e.alloc_u8_uninit(n * 2)?));
1093                        }
1094                        // FA3 batched twin (round 31): TMA-swizzled wgmma vl when the
1095                        // promoted single-seq config is on; else the mma favl.
1096                        let fa3_on = match std::env::var("MEMRA_FA3").as_deref() {
1097                            Ok("0") => false,
1098                            Ok("1") => true,
1099                            _ => cfg!(memra_hopper_mma),
1100                        };
1101                        if fa3_on {
1102                            let mut q16s = Vec::with_capacity(b);
1103                            let mut v16s = Vec::with_capacity(b);
1104                            for s in 0..b {
1105                                let t = ts[s];
1106                                let mut q16 = e.alloc_u8_uninit(t * n_head * head_dim * 2)?;
1107                                e.f32_to_bf16_into(&aps[s].qn, &mut q16, t * n_head * head_dim)?;
1108                                let mut k16 = e.alloc_u8_uninit(t * n_head_kv * head_dim * 2)?;
1109                                e.f32_to_bf16_into(&aps[s].kn, &mut k16, t * n_head_kv * head_dim)?;
1110                                let mut v16 = e.alloc_u8_uninit(t * n_head_kv * head_dim * 2)?;
1111                                e.f32_to_bf16_v(&g3[2].slice(offs[s] * vf_w..(offs[s] + t) * vf_w),
1112                                                &mut v16, t * n_head_kv * head_dim)?;
1113                                q16s.push(q16);
1114                                v16s.push((k16, v16));
1115                            }
1116                            let mut qp = [core::ptr::null::<core::ffi::c_void>(); 8];
1117                            let mut kp = qp;
1118                            let mut vp = qp;
1119                            let mut op = [core::ptr::null_mut::<f32>(); 8];
1120                            let mut tsv = [0i32; 8];
1121                            for s in 0..b {
1122                                qp[s] = e.addr_u8(&q16s[s]) as *const core::ffi::c_void;
1123                                kp[s] = e.addr_u8(&v16s[s].0) as *const core::ffi::c_void;
1124                                vp[s] = e.addr_u8(&v16s[s].1) as *const core::ffi::c_void;
1125                                op[s] = e.addr_f32(&attns[s]) as *mut f32;
1126                                tsv[s] = ts[s] as i32;
1127                            }
1128                            let rc = unsafe {
1129                                crate::fa3_vl_raw(qp.as_ptr(), kp.as_ptr(), vp.as_ptr(), op.as_ptr(),
1130                                                  tsv.as_ptr(), b as i32, n_head as i32,
1131                                                  n_head_kv as i32, head_dim as i32, fa_scale,
1132                                                  e.stream().cu_stream() as *mut core::ffi::c_void)
1133                            };
1134                            if rc != 0 {
1135                                return Err(format!("memra_fa3_vl rc={rc}").into());
1136                            }
1137                        } else {
1138                            let fargs: Vec<crate::FaSeqVl> = (0..b).map(|s| crate::FaSeqVl {
1139                                q: e.addr_f32(&aps[s].qn), k16: e.addr_u8(&mirrors[s].0),
1140                                v16: e.addr_u8(&mirrors[s].1), o: e.addr_f32(&attns[s]),
1141                                kf: e.addr_f32(&aps[s].kn),
1142                                vf: e.addr_f32v(&g3[2].slice(offs[s] * vf_w..(offs[s] + ts[s]) * vf_w)),
1143                                t: ts[s] as i32, pad: 0,
1144                            }).collect();
1145                            e.fa_prefill_vl8(&fargs, head_dim, n_head, n_head_kv, fa_scale)?;
1146                        }
1147                        for (s, attn) in attns.into_iter().enumerate() {
1148                            let (attn_g, ag16) = self.full_attn_prime_post_fa(
1149                                e, attn, &aps[s].gate, ts[s], n_head, head_dim)?;
1150                            let mut done = false;
1151                            if let Some(xh) = &ag16 {
1152                                done = e.try_f16_gemm_pre_into_off(&fa.wo, xh, ts[s], &mut mixed, offs[s] * n_embd)?;
1153                            }
1154                            if !done {
1155                                let m = e.matmul(&fa.wo, &attn_g, ts[s])?;
1156                                e.copy_into(&mut mixed, offs[s] * n_embd, &m, ts[s] * n_embd)?;
1157                            }
1158                        }
1159                    } else {
1160                        let mut parts: Vec<Vec<CudaSlice<f32>>> = (0..b).map(|_| Vec::new()).collect();
1161                        for (w, y) in [&fa.wq, &fa.wk, &fa.wv].iter().zip(g3) {
1162                            for (s, ys) in split(e, &y, w.out_features())?.into_iter().enumerate() {
1163                                parts[s].push(ys);
1164                            }
1165                        }
1166                        for (s, g3s) in parts.into_iter().enumerate() {
1167                            // task #16 gather removal: wo writes into `mixed` at offs[s] directly.
1168                            let (attn_g, ag16) = self.full_attn_prime_core_inner(
1169                                e, fa, g3s, &pos_ds[s], ts[s], caches[s], il)?;
1170                            let mut done = false;
1171                            if let Some(xh) = &ag16 {
1172                                done = e.try_f16_gemm_pre_into_off(&fa.wo, xh, ts[s], &mut mixed, offs[s] * n_embd)?;
1173                            }
1174                            if !done {
1175                                let m = e.matmul(&fa.wo, &attn_g, ts[s])?;
1176                                e.copy_into(&mut mixed, offs[s] * n_embd, &m, ts[s] * n_embd)?;
1177                            }
1178                        }
1179                    }
1180                }
1181                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1182                Mixer::Linear(la) => {
1183                    // task #16: NO split copies (cores read row-offset views of the concat
1184                    // outputs; out-GEMMs write into `mixed` at offs[s]). task #18: the core
1185                    // itself is BATCHED — per-seq prep/K1-K3, then ONE varlen K4 + ONE
1186                    // varlen K5 launch for all sequences.
1187                    let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
1188                    let g4 = e.matmul_group_xh(&ws, &h, &hx16, total)?;
1189                    let outs = self.linear_attn_prime_core_batch(e, la, &g4, &offs, &ts, caches, il)?;
1190                    for (s, (gn, gn16)) in outs.into_iter().enumerate() {
1191                        let (o, t) = (offs[s], ts[s]);
1192                        let mut done = false;
1193                        if let Some(xh) = &gn16 {
1194                            done = e.try_f16_gemm_pre_into_off(&la.ssm_out, xh, t, &mut mixed, o * n_embd)?;
1195                        }
1196                        if !done {
1197                            let m = e.matmul(&la.ssm_out, &gn, t)?;
1198                            e.copy_into(&mut mixed, o * n_embd, &m, t * n_embd)?;
1199                        }
1200                    }
1201                }
1202            }
1203            let mut x1 = e.uninit(total * n_embd)?;
1204            let mut z = e.uninit(total * n_embd)?;
1205            let mut zx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
1206            e.add_rms_norm_f16out(&x, &mixed, layer.post_attn_norm.float_data(),
1207                                  &mut x1, &mut z, &mut zx16, n_embd, total, eps)?;
1208            let ffn_out = match &layer.ffn {
1209                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
1210                    let n_ff = ffn_gate.out_features();
1211                    let mut g2 = e.matmul_group_xh(&[ffn_gate, ffn_up], &z, &zx16, total)?;
1212                    let up = g2.pop().unwrap();
1213                    let gate = g2.pop().unwrap();
1214                    let mut act = e.uninit(total * n_ff)?;
1215                    // task #17 (batch trunk): silu twin emits the down GEMM's fp16 operand
1216                    // in-epilogue (nsys round-26: this trunk still paid 32 cvt passes).
1217                    if Self::f16out_on(e, total) && self.cfg.m3.is_none() {
1218                        let mut a16 = e.alloc_u8_uninit(total * n_ff * 2)?;
1219                        e.silu_mul_f16out(&gate, &up, &mut act, &mut a16, total * n_ff)?;
1220                        match e.try_f16_gemm_pre(ffn_down, &a16, total)? {
1221                            Some(y) => y,
1222                            None => e.matmul(ffn_down, &act, total)?,
1223                        }
1224                    } else {
1225                        Self::ffn_act(e, &self.cfg, &gate, &up, &mut act, total * n_ff)?;
1226                        e.matmul(ffn_down, &act, total)?
1227                    }
1228                }
1229                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il(e, m, &z, total, il as u16)?,
1230            };
1231            let mut x2 = e.uninit(total * n_embd)?;
1232            e.add(&x1, &ffn_out, &mut x2, total * n_embd)?;
1233            x = x2;
1234        }
1235        // epilogue per seq (identical math to prime_chunk: norm all rows, lm_head on last row)
1236        let mut hn = e.uninit(total * n_embd)?;
1237        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, total, eps)?;
1238        // batched lm_head (nsys round-26: B sequential m=1 matvecs re-read the 600MB
1239        // lm_head weight B times — 2.2ms at B=6): gather the B last rows and run ONE
1240        // m=B GEMM (f16 lane; falls back to the per-seq matvec when no mirror). The
1241        // f16-vs-mmvq logits delta is the prefill GEMM numeric class — prime_batch_gate's
1242        // argmax battery arbitrates, same as every other prefill GEMM change.
1243        let mut hcat = e.uninit(b * n_embd)?;
1244        for s in 0..b {
1245            let last0 = (offs[s] + ts[s] - 1) * n_embd;
1246            e.copy_view_into(&mut hcat, s * n_embd, &hn.slice(last0..last0 + n_embd), n_embd)?;
1247        }
1248        let logits_cat = if b >= 2 { e.try_f16_gemm(&self.output, &hcat, b)? } else { None };
1249        let logits_host: Option<Vec<f32>> = match &logits_cat {
1250            Some(lc) => Some(e.dtoh(lc)?),
1251            None => None,
1252        };
1253        let n_vocab = self.output.out_features();
1254        let mut hidden_all = if crate::spec::spec_hpost() {
1255            split(e, &hn, n_embd)?
1256        } else {
1257            split(e, &x, n_embd)?
1258        };
1259        let mut out = Vec::with_capacity(b);
1260        for s in 0..b {
1261            let last0 = (offs[s] + ts[s] - 1) * n_embd;
1262            let mut h_seed = e.uninit(n_embd)?;
1263            if !crate::spec::spec_hpost() {
1264                e.copy_view_into(&mut h_seed, 0, &x.slice(last0..last0 + n_embd), n_embd)?;
1265            } else {
1266                e.copy_view_into(&mut h_seed, 0, &hn.slice(last0..last0 + n_embd), n_embd)?;
1267            }
1268            let logits = match &logits_host {
1269                Some(lh) => lh[s * n_vocab..(s + 1) * n_vocab].to_vec(),
1270                None => {
1271                    let mut hlast = e.uninit(n_embd)?;
1272                    e.copy_view_into(&mut hlast, 0, &hn.slice(last0..last0 + n_embd), n_embd)?;
1273                    e.dtoh(&e.matmul(&self.output, &hlast, 1)?)?
1274                }
1275            };
1276            caches[s].pos += ts[s];
1277            out.push((logits, h_seed, hidden_all.remove(0)));
1278        }
1279        Ok(out)
1280    }
1281
1282    /// `full_attn` (batched prefill mixer) + the cache side-effect: append the T post-RoPE K/V
1283    /// rows into the resident quantized KV cache (q8_0 K / q5_1 V) and advance len/len_d. Row
1284    /// bytes are BIT-IDENTICAL to the decode append (same per-warp quant kernel per row; the
1285    /// batched `append_kv_quantized_rows` runs that exact warp math on a (block, token) grid).
1286    /// The attention itself is unchanged prefill math (fa_prefill over the f32 K/V).
1287    fn full_attn_prime(&self, e: &Engine, fa: &FullAttnLayer, h: &CudaSlice<f32>,
1288                       hx: Option<&CudaSlice<u8>>,
1289                       pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache, il: usize)
1290                       -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1291        // PROJ/CORE SPLIT (task #13, 2026-07-26): the q/k/v group projection is hoisted so
1292        // the cross-request batch driver can run it at m = sum_T over concatenated tokens;
1293        // this single-seq path composes proj+core identically (byte-for-byte the old body).
1294        // task #14: `hx` = the norm-fused fp16 twin of `h` (skips the convert launch).
1295        let g3 = match hx {
1296            Some(xh) => e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], h, xh, t)?,
1297            None => e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?,
1298        };
1299        self.full_attn_prime_core(e, fa, g3, pos_d, t, cache, il)
1300    }
1301
1302    /// Everything after the q/k/v projections (split/QK-norm/RoPE/FA/gate/append + wo).
1303    /// `g3` = matmul_group([wq, wk, wv]) output rows for THIS sequence.
1304    /// Composes inner + wo (== the pre-split core; every existing caller unchanged).
1305    fn full_attn_prime_core(&self, e: &Engine, fa: &FullAttnLayer, g3: Vec<CudaSlice<f32>>,
1306                            pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache, il: usize)
1307                            -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1308        let (attn_g, ag16) = self.full_attn_prime_core_inner(e, fa, g3, pos_d, t, cache, il)?;
1309        if let Some(xh) = &ag16 {
1310            if let Some(y) = e.try_f16_gemm_pre(&fa.wo, xh, t)? {
1311                return Ok(y);
1312            }
1313        }
1314        Ok(e.matmul(&fa.wo, &attn_g, t)?)
1315    }
1316
1317    fn full_attn_prime_core_inner(&self, e: &Engine, fa: &FullAttnLayer, g3: Vec<CudaSlice<f32>>,
1318                            pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache, il: usize)
1319                            -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
1320        let cfg = &self.cfg;
1321        let n_head = cfg.n_head as usize;
1322        let n_head_kv = cfg.n_head_kv as usize;
1323        let head_dim = cfg.head_dim_k as usize;
1324        let scale = 1.0 / (head_dim as f32).sqrt();
1325        let (pre, base_len) = self.full_attn_prime_pre_fa(e, fa, g3, pos_d, t, cache, il)?;
1326        let AttnPre { q, k, v, gate } = pre;
1327        let mut attn = e.uninit(t * n_head * head_dim)?;
1328        self.full_attn_prime_fa_dispatch(e, &q, &k, &v, &mut attn, base_len, t, cache, il,
1329                                         head_dim, n_head, n_head_kv, scale)?;
1330        self.full_attn_prime_post_fa(e, attn, &gate, t, n_head, head_dim)
1331    }
1332
1333    /// task #18 (attn side): projections tail through KV append — everything before the
1334    /// attention kernel. Returns the post-rope q/k, v, optional out-gate, and the KV rows
1335    /// present BEFORE this chunk's append (base_len; 0 == fresh).
1336    #[allow(clippy::type_complexity)]
1337    fn full_attn_prime_pre_fa(&self, e: &Engine, fa: &FullAttnLayer, mut g3: Vec<CudaSlice<f32>>,
1338                            pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache, il: usize)
1339                            -> Result<(AttnPre, usize), Box<dyn std::error::Error>> {
1340        let cfg = &self.cfg;
1341        let n_head = cfg.n_head as usize;
1342        let n_head_kv = cfg.n_head_kv as usize;
1343        let head_dim = cfg.head_dim_k as usize;
1344        let eps = cfg.rms_eps;
1345
1346        // qwen35 fuses [q|gate] per head in wq (2*head_dim stride); M3/Hy3 have NO output gate
1347        // (attention_output_gate=false) — wq out = n_head*head_dim exactly, and q_gate_split
1348        // would read 2x out of bounds. `gated` keys both the split and the sigmoid epilogue.
1349        let gated = cfg.attn_out_gate();
1350        let v = g3.pop().unwrap();
1351        let mut k = g3.pop().unwrap();
1352        let qf = g3.pop().unwrap();
1353        let (mut q, gate) = if gated {
1354            let mut q = e.uninit(t * n_head * head_dim)?;
1355            let mut gate = e.uninit(t * n_head * head_dim)?;
1356            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, t)?;
1357            (q, Some(gate))
1358        } else {
1359            (qf, None)
1360        };
1361
1362        let mut qn = e.uninit(t * n_head * head_dim)?;
1363        e.rms_norm(&q, fa.q_norm.float_data(), &mut qn, head_dim, n_head * t, eps)?;
1364        q = qn;
1365        let mut kn = e.uninit(t * n_head_kv * head_dim)?;
1366        e.rms_norm(&k, fa.k_norm.float_data(), &mut kn, head_dim, n_head_kv * t, eps)?;
1367        k = kn;
1368        let rope_dims = cfg.rope_dim_count as usize;
1369        e.rope_neox(&mut q, pos_d, head_dim, rope_dims, n_head, t, cfg.rope_freq_base, 1.0)?;
1370        e.rope_neox(&mut k, pos_d, head_dim, rope_dims, n_head_kv, t, cfg.rope_freq_base, 1.0)?;
1371
1372        // CACHE SIDE-EFFECT: append the T post-rope K/V token rows (token-major [T, kv_dim] ==
1373        // the cache row layout) quantized into cache.kv[il], then advance len + device len_d.
1374        {
1375            let kvl = cache.kv[il].as_mut().unwrap();
1376            assert!(kvl.len + t <= cache.max_ctx, "prime_cache: KV overflow");
1377            e.append_kv_quantized_rows(&k, &v, &mut kvl.k, &mut kvl.v, kvl.len, t,
1378                                       kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes,
1379                                       crate::Engine::kv_fp8_on())?;
1380            kvl.len += t;
1381            let new_len = kvl.len as i32;
1382            e.set_i32_one(&mut kvl.len_d, new_len)?;
1383        }
1384
1385        let base_len = {
1386            let kvl = cache.kv[il].as_ref().unwrap();
1387            kvl.len - t   // KV rows present BEFORE this chunk's append above
1388        };
1389        Ok((AttnPre { q, k, v, gate }, base_len))
1390    }
1391
1392    /// batched prefill attention. FRESH prime (no past KV): unchanged forward_last math over
1393    /// the f32 K/V of this batch. CONTINUATION chunk (past KV present): the chunk's queries
1394    /// must attend to [0 .. base+t) — run fa_prefill_view over the resident QUANTIZED cache
1395    /// (the spec-verify pattern; kernel's causal mask offsets by T_kv-T). Numerically this
1396    /// reads q8_0/q5_1-dequantized K/V for the past AND the current chunk — the same class as
1397    /// decode reading the cache; the run-gen/first-16 battery is the accuracy authority.
1398    #[allow(clippy::too_many_arguments)]
1399    fn full_attn_prime_fa_dispatch(&self, e: &Engine, q: &CudaSlice<f32>, k: &CudaSlice<f32>,
1400                            v: &CudaSlice<f32>, attn: &mut CudaSlice<f32>, base_len: usize,
1401                            t: usize, cache: &mut Cache, il: usize,
1402                            head_dim: usize, n_head: usize, n_head_kv: usize, scale: f32)
1403                            -> Result<(), Box<dyn std::error::Error>> {
1404        if base_len == 0 {
1405            // fa_prefill's smem layout is compile-time HEAD_DIM: stamped twins exist for 256
1406            // (qwen35) and 128 (M3, `_hd128` — 2026-07-07). Other dims would overrun the
1407            // runtime-sized allocation -> ILLEGAL_ADDRESS; fall to naive SDPA there.
1408            if std::env::var("MEMRA_NOFA").is_ok() || !(head_dim == 256 || head_dim == 128) {
1409                e.sdpa_naive(q, k, v, attn, head_dim, n_head, n_head_kv, t, t, scale, true)?;
1410            } else {
1411                e.fa_prefill(q, k, v, attn, head_dim, n_head, n_head_kv, t, t, scale, true)?;
1412            }
1413        } else {
1414            let kvl = cache.kv[il].as_ref().unwrap();
1415            let t_kv = base_len + t;
1416            let k_view = e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes);
1417            let v_view = e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes);
1418            // ARC B (2026-07-05): dequant-once workspace, DEFAULT ON. fa_prefill_q's inline
1419            // dequant re-reads+re-dequants the whole quantized KV stream from every one of the
1420            // T/64 x n_head CTAs (64x+ redundant at chunk=4096; 30.5% of the 32k prime wall).
1421            // fa_prefill_view_ws dequants K/V ONCE into a resident bf16 workspace then runs the
1422            // bit-identical bf16 twin (fa_prefill_qw) — same staged values, same FP order, token-
1423            // identical output (gate: MEMRA_PRIME_CHUNK=4096 ws-on vs ws-off vs monolithic).
1424            // MEMRA_PRIME_DEQW=0 reverts to the inline-dequant kernel.
1425            let deqw = std::env::var("MEMRA_PRIME_DEQW").map(|v| v != "0").unwrap_or(true);
1426            if deqw {
1427                e.fa_prefill_view_ws(q, &k_view, &v_view, attn, head_dim, n_head, n_head_kv,
1428                                     t, t_kv, scale, true, kvl.k_tok_bytes, kvl.v_tok_bytes,
1429                                     crate::Engine::kv_fp8_on())?;
1430            } else {
1431                e.fa_prefill_view(q, &k_view, &v_view, attn, head_dim, n_head, n_head_kv,
1432                                  t, t_kv, scale, true, kvl.k_tok_bytes, kvl.v_tok_bytes,
1433                                  crate::Engine::kv_fp8_on())?;
1434            }
1435        }
1436        Ok(())
1437    }
1438
1439    /// task #17: sig_mul_f16out fuses [sigmoid + mul + f16 convert] into one launch
1440    /// (bit-identical composition) and hands wo its fp16 operand directly.
1441    fn full_attn_prime_post_fa(&self, e: &Engine, attn: CudaSlice<f32>,
1442                            gate: &Option<CudaSlice<f32>>, t: usize,
1443                            n_head: usize, head_dim: usize)
1444                            -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
1445        let (attn_g, ag16) = match gate {
1446            Some(gate) => {
1447                let n = t * n_head * head_dim;
1448                let mut ag = e.uninit(n)?;
1449                if Self::f16out_on(e, t) {
1450                    let mut a16 = e.alloc_u8_uninit(n * 2)?;
1451                    e.sig_mul_f16out(&attn, gate, &mut ag, &mut a16, n)?;
1452                    (ag, Some(a16))
1453                } else {
1454                    let mut gsig = e.uninit(n)?;
1455                    e.sigmoid(gate, &mut gsig, n)?;
1456                    e.mul(&attn, &gsig, &mut ag, n)?;
1457                    (ag, None)
1458                }
1459            }
1460            None => (attn, None),
1461        };
1462        Ok((attn_g, ag16))
1463    }
1464
1465    /// STATEFUL batched linear-attention prime: `linear_attn`'s prefill-dispatch pass (normal
1466    /// `e.matmul` — GEMM at m>=16 — plus the prefill repack/L2/glog kernels) but with the state
1467    /// carried THROUGH the cache like the spec verify does: carried-ring conv
1468    /// (ssm_conv1d_tm_state writes the final ring back) + ONE gdn_scan from cache.recur[il]'s
1469    /// current state (zero at a fresh prime) whose final state ping-pongs back into the cache.
1470    /// Wiring mirrors `linear_attn_verify_t` (spec.rs); dispatch mirrors `linear_attn` (prefill).
1471    fn linear_attn_prime(&self, e: &Engine, la: &LinearAttnLayer, h: &CudaSlice<f32>,
1472                         hx: Option<&CudaSlice<u8>>, t: usize,
1473                         cache: &mut Cache, il: usize)
1474                         -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1475        // PROJ/CORE SPLIT (task #13): see full_attn_prime — same hoist for the GDN 4-tuple.
1476        let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
1477        let g4 = match hx {
1478            Some(xh) => e.matmul_group_xh(&ws, h, xh, t)?,
1479            None => e.matmul_group(&ws, h, t)?,
1480        };
1481        self.linear_attn_prime_core(e, la, g4, t, cache, il)
1482    }
1483
1484    /// Everything after the GDN 4-tuple projections (conv/chunk stack/gated norm + ssm_out).
1485    fn linear_attn_prime_core(&self, e: &Engine, la: &LinearAttnLayer, mut g4: Vec<CudaSlice<f32>>,
1486                              t: usize, cache: &mut Cache, il: usize)
1487                              -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1488        self.linear_attn_prime_core_pad(e, la, g4.drain(..).collect(), t, cache, il, None)
1489    }
1490
1491    /// task #14: `pad_len` = device true length for PADDED prime graphs. Pads become
1492    /// identity GDN steps (gdn_pad_mask zeroes beta/g_log past the true length) and the
1493    /// conv ring writes back from the true tail. None = classic path, byte-identical.
1494    #[allow(clippy::too_many_arguments)]
1495    fn linear_attn_prime_core_pad_inner(&self, e: &Engine, la: &LinearAttnLayer, mut g4: Vec<CudaSlice<f32>>,
1496                              t: usize, cache: &mut Cache, il: usize,
1497                              pad_len: Option<&CudaSlice<i32>>)
1498                              -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
1499        // shim over the view twin (task #16): full-range views of the owned buffers.
1500        let ssm = self.cfg.ssm.as_ref().unwrap();
1501        let d_state = ssm.state_size as usize;
1502        let num_k = ssm.group_count as usize;
1503        let num_v = ssm.time_step_rank as usize;
1504        let key_dim = d_state * num_k;
1505        let value_dim = d_state * num_v;
1506        let conv_dim = key_dim * 2 + value_dim;
1507        let alpha = g4.pop().unwrap();                   // [T, num_v]
1508        let beta_raw = g4.pop().unwrap();                // [T, num_v]
1509        let z = g4.pop().unwrap();                       // [T, value_dim]
1510        let qkv_mixed = g4.pop().unwrap();               // [T, conv_dim] token-major
1511        self.linear_attn_prime_core_pad_view(
1512            e, la,
1513            &qkv_mixed.slice(0..t * conv_dim), &z.slice(0..t * value_dim),
1514            &beta_raw.slice(0..t * num_v), &alpha.slice(0..t * num_v),
1515            t, cache, il, pad_len)
1516    }
1517
1518    /// task #18: the GDN prep stage (conv + repack + l2 x2 + sigmoid + glog + pad-mask) —
1519    /// shared verbatim by the per-seq scan path and the varlen batched path.
1520    #[allow(clippy::too_many_arguments)]
1521    fn linear_attn_gdn_prep(&self, e: &Engine, la: &LinearAttnLayer,
1522                            qkv_mixed: &cudarc::driver::CudaView<f32>,
1523                            beta_raw: &cudarc::driver::CudaView<f32>,
1524                            alpha: &cudarc::driver::CudaView<f32>,
1525                            t: usize, cache: &mut Cache, il: usize,
1526                            pad_len: Option<&CudaSlice<i32>>)
1527                            -> Result<GdnPrep, Box<dyn std::error::Error>> {
1528        let cfg = &self.cfg;
1529        let ssm = cfg.ssm.as_ref().unwrap();
1530        let d_state = ssm.state_size as usize;       // 128
1531        let num_k = ssm.group_count as usize;        // 16
1532        let num_v = ssm.time_step_rank as usize;     // 32
1533        let d_conv = ssm.conv_kernel as usize;       // 4
1534        let key_dim = d_state * num_k;               // 2048
1535        let value_dim = d_state * num_v;             // 4096
1536        let conv_dim = key_dim * 2 + value_dim;      // 8192
1537        let eps = cfg.rms_eps;
1538        debug_assert!(t >= d_conv - 1, "stateful conv needs T >= pad (PRIME_MIN_T gates)");
1539
1540        // conv with CARRIED ring state + ring roll (state read + final-window write-back).
1541        // task #18 conv-fuse (default ON): conv + SiLU + repack in one pass — the 67MB
1542        // conv_out intermediate and its transposed re-read disappear (values bit-identical).
1543        // MEMRA_CONV_FUSE=0 reverts to the two-kernel chain.
1544        let rl = cache.recur[il].as_mut().unwrap();
1545        let hk = Self::gdn_hk(e, t, num_v, num_k);
1546        let conv_fuse = std::env::var("MEMRA_CONV_FUSE").as_deref() != Ok("0");
1547        let hk = if conv_fuse { hk } else { num_v };   // de-broadcast rides the fused conv
1548        let mut q_g = e.uninit(d_state * hk * t)?;
1549        let mut k_g = e.uninit(d_state * hk * t)?;
1550        let mut v_g = e.uninit(d_state * num_v * t)?;
1551        if conv_fuse {
1552            e.ssm_conv1d_gdn_state_pad(qkv_mixed, &mut rl.conv_state, la.ssm_conv1d.float_data(),
1553                                  &mut q_g, &mut k_g, &mut v_g,
1554                                  conv_dim, t, d_conv, d_state, num_v, num_k, key_dim, hk, pad_len)?;
1555        } else {
1556            let mut conv_out = e.uninit(conv_dim * t)?;      // [conv_dim, T] channel-major, SiLU
1557            e.ssm_conv1d_tm_state_pad_v(qkv_mixed, &mut rl.conv_state, la.ssm_conv1d.float_data(),
1558                                  &mut conv_out, conv_dim, t, d_conv, pad_len)?;
1559            e.qkv_to_gdn_repack(&conv_out, &mut q_g, &mut k_g, &mut v_g, d_state, num_v, num_k, key_dim, t)?;
1560        }
1561        let mut q_l2 = e.uninit(d_state * hk * t)?;
1562        // mirror-fold (round 35): q's bf16 twin (wgmma K45/K2 A-operand) in-epilogue too.
1563        // Emitted only where a consumer exists (the wgmma config) — on other arches the
1564        // alloc + epilogue stores would be pure waste.
1565        let qb16 = if Engine::l2_v2_on(d_state) && e.gdn_wgmma_on(32) {
1566            let mut qb = e.alloc_u8_uninit(d_state * hk * t * 2)?;
1567            e.l2_norm_pp(&q_g, &mut q_l2, Some(&mut qb), d_state, hk * t, eps)?;
1568            Some(qb)
1569        } else {
1570            e.l2_norm_pp(&q_g, &mut q_l2, None, d_state, hk * t, eps)?;
1571            None
1572        };
1573        let mut k_l2 = e.uninit(d_state * hk * t)?;
1574        // mirror-fold: emit k's bf16 twin (the chunked-scan kb16 mirror) in-epilogue
1575        let kb16 = if Engine::l2_v2_on(d_state) {
1576            let mut kb = e.alloc_u8_uninit(d_state * hk * t * 2)?;
1577            e.l2_norm_pp(&k_g, &mut k_l2, Some(&mut kb), d_state, hk * t, eps)?;
1578            Some(kb)
1579        } else {
1580            e.l2_norm_pp(&k_g, &mut k_l2, None, d_state, hk * t, eps)?;
1581            None
1582        };
1583        let mut beta = e.uninit(t * num_v)?;
1584        e.sigmoid_v(beta_raw, &mut beta, t * num_v)?;
1585        let mut g_log = e.uninit(t * num_v)?;
1586        e.gdn_glog_v(alpha, la.ssm_dt.float_data(), la.ssm_a.float_data(), &mut g_log, num_v, t)?;
1587        if let Some(len_d) = pad_len {
1588            e.gdn_pad_mask(&mut beta, &mut g_log, len_d, num_v, t)?;
1589        }
1590        Ok(GdnPrep { hk, q_l2, k_l2, v_g, beta, g_log, kb16, qb16 })
1591    }
1592
1593    /// task #18: batched GDN mixer core — per-seq prep + K1-K3, then ONE varlen K4 and
1594    /// ONE varlen K5 launch for all B sequences (full-machine occupancy vs B underfilled
1595    /// per-seq trains). Per-block math identical to the per-seq path (bit-gateable);
1596    /// MEMRA_GDN_VL=0 or a non-mma/non-C32 config falls back to the per-seq loop.
1597    #[allow(clippy::too_many_arguments)]
1598    fn linear_attn_prime_core_batch(&self, e: &Engine, la: &LinearAttnLayer,
1599                                    g4: &[CudaSlice<f32>], offs: &[usize], ts: &[usize],
1600                                    caches: &mut [&mut Cache], il: usize)
1601                                    -> Result<Vec<(CudaSlice<f32>, Option<CudaSlice<u8>>)>, Box<dyn std::error::Error>> {
1602        let ssm = self.cfg.ssm.as_ref().unwrap();
1603        let d_state = ssm.state_size as usize;
1604        let num_k = ssm.group_count as usize;
1605        let num_v = ssm.time_step_rank as usize;
1606        let key_dim = d_state * num_k;
1607        let value_dim = d_state * num_v;
1608        let conv_dim = key_dim * 2 + value_dim;
1609        let eps = self.cfg.rms_eps;
1610        let scale = 1.0 / (d_state as f32).sqrt();
1611        let b = ts.len();
1612        let c = Engine::gdn_chunk_size();
1613        // CONTINUATION batches (increment (b)) take the per-seq stateful pad_view twin
1614        // below — the varlen K4/K5 chain is fresh-only (zero initial state assumed).
1615        let carried = caches.iter().any(|c| c.pos > 0);
1616        let use_vl = !carried
1617            && (2..=8).contains(&b)
1618            && Engine::gdn_chunked_enabled() && ts.iter().all(|&t| t >= 16)
1619            && e.gdn_mma_enabled(c)
1620            && std::env::var("MEMRA_GDN_VL").as_deref() != Ok("0");
1621        if !use_vl {
1622            return (0..b).map(|s| {
1623                let (o, t) = (offs[s], ts[s]);
1624                self.linear_attn_prime_core_pad_view(
1625                    e, la,
1626                    &g4[0].slice(o * conv_dim..(o + t) * conv_dim),
1627                    &g4[1].slice(o * value_dim..(o + t) * value_dim),
1628                    &g4[2].slice(o * num_v..(o + t) * num_v),
1629                    &g4[3].slice(o * num_v..(o + t) * num_v),
1630                    t, caches[s], il, None)
1631            }).collect();
1632        }
1633        // increment 3: the ENTIRE core is varlen — allocs only per seq, then 13 launches
1634        // total for the whole batch: [conv, ring, repack, l2(q+k), gate-prep] +
1635        // [k-mirror, K1, K2, K3, w-mirror, K4, K5] + [gated-norm tail].
1636        struct SeqBufs {
1637            conv_out: CudaSlice<f32>, q_g: CudaSlice<f32>, k_g: CudaSlice<f32>, v_g: CudaSlice<f32>,
1638            q_l2: CudaSlice<f32>, k_l2: CudaSlice<f32>, beta: CudaSlice<f32>, g_log: CudaSlice<f32>,
1639            gn: CudaSlice<f32>, gn16: CudaSlice<u8>,
1640        }
1641        let d_conv = ssm.conv_kernel as usize;
1642        let f16o = Self::f16out_on(e, 16);
1643        let hk = Self::gdn_hk(e, 16, num_v, num_k);   // vl path is always chunked+mma
1644        let mut sb = Vec::with_capacity(b);
1645        let mut pres = Vec::with_capacity(b);
1646        for &t in ts.iter().take(b) {
1647            sb.push(SeqBufs {
1648                conv_out: e.uninit(conv_dim * t)?,
1649                q_g: e.uninit(d_state * hk * t)?,
1650                k_g: e.uninit(d_state * hk * t)?,
1651                v_g: e.uninit(d_state * num_v * t)?,
1652                q_l2: e.uninit(d_state * hk * t)?,
1653                k_l2: e.uninit(d_state * hk * t)?,
1654                beta: e.uninit(t * num_v)?,
1655                g_log: e.uninit(t * num_v)?,
1656                gn: e.uninit(d_state * num_v * t)?,
1657                gn16: e.alloc_u8_uninit(d_state * num_v * t * 2)?,
1658            });
1659            pres.push(e.gdn_chunk_alloc(num_v, t, c, hk)?);
1660        }
1661        let prep_args: Vec<crate::GdnPrepVl> = (0..b).map(|s| {
1662            let (o, t) = (offs[s], ts[s]);
1663            let rl = caches[s].recur[il].as_ref().unwrap();
1664            crate::GdnPrepVl {
1665                qkv: e.addr_f32v(&g4[0].slice(o * conv_dim..(o + t) * conv_dim)),
1666                conv_state: e.addr_f32(&rl.conv_state),
1667                conv_out: e.addr_f32(&sb[s].conv_out),
1668                q_g: e.addr_f32(&sb[s].q_g), k_g: e.addr_f32(&sb[s].k_g), v_g: e.addr_f32(&sb[s].v_g),
1669                q_l2: e.addr_f32(&sb[s].q_l2), k_l2: e.addr_f32(&sb[s].k_l2),
1670                beta_raw: e.addr_f32v(&g4[2].slice(o * num_v..(o + t) * num_v)),
1671                alpha: e.addr_f32v(&g4[3].slice(o * num_v..(o + t) * num_v)),
1672                beta: e.addr_f32(&sb[s].beta), g_log: e.addr_f32(&sb[s].g_log),
1673                o: e.addr_f32(&pres[s].o),
1674                z: e.addr_f32v(&g4[1].slice(o * value_dim..(o + t) * value_dim)),
1675                gn: e.addr_f32(&sb[s].gn), gn16: e.addr_u8(&sb[s].gn16),
1676                kb16: if Engine::l2_v2_on(d_state) { e.addr_u8(&pres[s].kb16) } else { 0 },
1677                qb16: if Engine::l2_v2_on(d_state) && e.gdn_wgmma_on(c) { e.addr_u8(&pres[s].qb16) } else { 0 },
1678                t: t as i32, pad: 0,
1679            }
1680        }).collect();
1681        let args: Vec<crate::GdnSeqVl> = (0..b).map(|s| {
1682            let rl = caches[s].recur[il].as_ref().unwrap();
1683            crate::GdnSeqVl {
1684                kb16: e.addr_u8(&pres[s].kb16), gcum: e.addr_f32(&pres[s].gcum),
1685                beta: e.addr_f32(&sb[s].beta), u: e.addr_f32(&pres[s].u),
1686                wb16: e.addr_u8(&pres[s].wb16), y: e.addr_u8(&pres[s].y16),
1687                ssnap: e.addr_u8(&pres[s].ssnap16),
1688                state_in: e.addr_f32(&rl.ssm_state), state_out: e.addr_f32(&rl.ssm_state_alt),
1689                q: e.addr_f32(&sb[s].q_l2), p: e.addr_f32(&pres[s].p),
1690                o: e.addr_f32(&pres[s].o),
1691                k: e.addr_f32(&sb[s].k_l2), v: e.addr_f32(&sb[s].v_g),
1692                g: e.addr_f32(&sb[s].g_log), a: e.addr_f32(&pres[s].a),
1693                w: e.addr_f32(&pres[s].w),
1694                t: ts[s] as i32, nc: pres[s].nc as i32,
1695            }
1696        }).collect();
1697        e.gdn_prep_vl8(&prep_args, la.ssm_conv1d.float_data(), la.ssm_dt.float_data(),
1698                       la.ssm_a.float_data(), conv_dim, d_conv, d_state, num_v, num_k, key_dim, hk, eps)?;
1699        // mirror-fold (round 27): l2 v2 emits kb16 in-epilogue; K3's store emits wb16 —
1700        // both standalone mirror launches vanish on the default config.
1701        if !Engine::l2_v2_on(d_state) {
1702            e.gdn_mirror_vl8(&args, num_v, 0, hk)?;
1703        }
1704        // task #22: wgmma-fused vl twins — qb16 mirrors + GdnWVl8 side-struct.
1705        let wq8: Option<crate::GdnWVl8> = if e.gdn_wgmma_on(c) {
1706            // qb16 emitted by the vl l2 mirror-fold when l2-v2 serves; bulk cvt otherwise
1707            if !Engine::l2_v2_on(d_state) {
1708                for s in 0..b {
1709                    e.f32_to_bf16_into(&sb[s].q_l2, &mut pres[s].qb16, d_state * hk * ts[s])?;
1710                }
1711            }
1712            let mut wa = [crate::GdnWVl::default(); 8];
1713            for s in 0..b {
1714                wa[s] = crate::GdnWVl { qb16: e.addr_u8(&pres[s].qb16), pb16: e.addr_u8(&pres[s].pb16) };
1715            }
1716            Some(crate::GdnWVl8(wa))
1717        } else { None };
1718        e.gdn_chunk_k123_vl8(&args, num_v, hk, wq8.as_ref())?;
1719        e.gdn_chunk_vl8(&args, num_v, scale, hk, wq8.as_ref())?;
1720        if f16o {
1721            e.gdn_tail_vl8(&prep_args, la.ssm_norm.float_data(), d_state, num_v, eps)?;
1722        }
1723        // per-seq state swap (+ non-f16out tail fallback)
1724        let mut out = Vec::with_capacity(b);
1725        for (s, bufs) in sb.into_iter().enumerate() {
1726            let rl = caches[s].recur[il].as_mut().unwrap();
1727            std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1728            let (o, t) = (offs[s], ts[s]);
1729            let SeqBufs { mut gn, gn16, .. } = bufs;
1730            if f16o {
1731                out.push((gn, Some(gn16)));
1732            } else {
1733                let z_v = g4[1].slice(o * value_dim..(o + t) * value_dim);
1734                e.gated_rmsnorm_zv(&pres[s].o, la.ssm_norm.float_data(), &z_v, &mut gn,
1735                                   d_state, num_v * t, eps)?;
1736                out.push((gn, None));
1737            }
1738        }
1739        Ok(out)
1740    }
1741
1742    /// task #16: view-consuming GDN prime core — the batched prime hands row-offset
1743    /// views of the CONCAT projection outputs directly (no per-seq split copies).
1744    /// Same kernels, same values, byte-identical to the Vec shim above.
1745    #[allow(clippy::too_many_arguments)]
1746    fn linear_attn_prime_core_pad_view(&self, e: &Engine, la: &LinearAttnLayer,
1747                              qkv_mixed: &cudarc::driver::CudaView<f32>,
1748                              z: &cudarc::driver::CudaView<f32>,
1749                              beta_raw: &cudarc::driver::CudaView<f32>,
1750                              alpha: &cudarc::driver::CudaView<f32>,
1751                              t: usize, cache: &mut Cache, il: usize,
1752                              pad_len: Option<&CudaSlice<i32>>)
1753                              -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
1754        let cfg = &self.cfg;
1755        let ssm = cfg.ssm.as_ref().unwrap();
1756        let d_state = ssm.state_size as usize;       // 128
1757        let num_v = ssm.time_step_rank as usize;     // 32
1758        let eps = cfg.rms_eps;
1759        let scale = 1.0 / (d_state as f32).sqrt();
1760
1761        let prep = self.linear_attn_gdn_prep(e, la, qkv_mixed, beta_raw, alpha, t, cache, il, pad_len)?;
1762
1763        // ONE gdn_scan over T from the cache's CURRENT state (zero at fresh prime); the final
1764        // state lands in the spare buffer and ping-pongs back (stable resident pointers, the
1765        // decode-determinism discipline from linear_attn_decode_inner). A4: `gdn_scan_prefill`
1766        // dispatches the chunked WY form under MEMRA_GDN_CHUNKED (prefill-only seam; decode +
1767        // verify keep the sequential kernel).
1768        let mut o = e.uninit(d_state * num_v * t)?;
1769        let rl = cache.recur[il].as_mut().unwrap();
1770        {
1771            let crate::cache::RecurLayer { ssm_state, ssm_state_alt, .. } = rl;
1772            e.gdn_scan_prefill(&prep.q_l2, &prep.k_l2, &prep.v_g, &prep.g_log, &prep.beta,
1773                               prep.kb16.as_ref(), prep.qb16.as_ref(), ssm_state, ssm_state_alt, &mut o, num_v, t, scale,
1774                               prep.hk)?;
1775        }
1776        std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1777
1778        // gated RMSNorm + out projection (prefill dispatch). task #17: the f16out twin also
1779        // emits the ssm_out GEMM's fp16 operand in-epilogue (kills the standalone convert).
1780        let mut gn = e.uninit(d_state * num_v * t)?;
1781        let gn16 = if Self::f16out_on(e, t) {
1782            let mut g16 = e.alloc_u8_uninit(d_state * num_v * t * 2)?;
1783            e.gated_rmsnorm_f16out_zv(&o, la.ssm_norm.float_data(), z, &mut gn, &mut g16,
1784                                      d_state, num_v * t, eps)?;
1785            Some(g16)
1786        } else {
1787            e.gated_rmsnorm_zv(&o, la.ssm_norm.float_data(), z, &mut gn, d_state, num_v * t, eps)?;
1788            None
1789        };
1790        Ok((gn, gn16))
1791    }
1792
1793    /// Task #15 core-split wrapper: composes inner + ssm_out (== the old core).
1794    #[allow(clippy::too_many_arguments)]
1795    fn linear_attn_prime_core_pad(&self, e: &Engine, la: &LinearAttnLayer, g4: Vec<CudaSlice<f32>>,
1796                              t: usize, cache: &mut Cache, il: usize,
1797                              pad_len: Option<&CudaSlice<i32>>)
1798                              -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1799        let (gn, gn16) = self.linear_attn_prime_core_pad_inner(e, la, g4, t, cache, il, pad_len)?;
1800        if let Some(xh) = &gn16 {
1801            if let Some(y) = e.try_f16_gemm_pre(&la.ssm_out, xh, t)? {
1802                return Ok(y);
1803            }
1804        }
1805        Ok(e.matmul(&la.ssm_out, &gn, t)?)
1806    }
1807
1808    /// Full-attention mixer with QK-norm, partial RoPE, sigmoid output gate (qwen35 :257-336).
1809    pub fn full_attn(&self, e: &Engine, fa: &FullAttnLayer, h: &CudaSlice<f32>, pos_d: &CudaSlice<i32>, t: usize)
1810                 -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1811        let cfg = &self.cfg;
1812        let _n_embd = cfg.n_embd as usize;
1813        let n_head = cfg.n_head as usize;
1814        let n_head_kv = cfg.n_head_kv as usize;
1815        let head_dim = cfg.head_dim_k as usize;
1816        let eps = cfg.rms_eps;
1817        let scale = 1.0 / (head_dim as f32).sqrt();
1818
1819        // qwen35: wq output = head_dim*2*n_head (fused [q|gate] per head). M3/Hy3: NO output
1820        // gate — wq out = n_head*head_dim, no split (see prime-path note).
1821        let gated = cfg.attn_out_gate();
1822        // grouped: one f16 activation convert feeds q/k/v (matmul_group)
1823        let mut g3 = e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?;
1824        let v = g3.pop().unwrap();
1825        let mut k = g3.pop().unwrap();
1826        let qf = g3.pop().unwrap();
1827        let (mut q, gate) = if gated {
1828            let mut q = e.uninit(t * n_head * head_dim)?;
1829            let mut gate = e.uninit(t * n_head * head_dim)?;
1830            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, t)?;
1831            (q, Some(gate))
1832        } else {
1833            (qf, None)
1834        };
1835
1836        // QK-norm (per head_dim row), then partial RoPE.
1837        let mut qn = e.uninit(t * n_head * head_dim)?;
1838        e.rms_norm(&q, fa.q_norm.float_data(), &mut qn, head_dim, n_head * t, eps)?;
1839        q = qn;
1840        let mut kn = e.uninit(t * n_head_kv * head_dim)?;
1841        e.rms_norm(&k, fa.k_norm.float_data(), &mut kn, head_dim, n_head_kv * t, eps)?;
1842        k = kn;
1843        let rope_dims = cfg.rope_dim_count as usize;
1844        e.rope_neox(&mut q, pos_d, head_dim, rope_dims, n_head, t, cfg.rope_freq_base, 1.0)?;
1845        e.rope_neox(&mut k, pos_d, head_dim, rope_dims, n_head_kv, t, cfg.rope_freq_base, 1.0)?;
1846
1847        // SDPA
1848        let mut attn = e.uninit(t * n_head * head_dim)?;
1849        // hand-written FlashAttention prefill (head_dim 256/128 stamped twins). MEMRA_NOFA
1850        // falls back to naive sdpa.
1851        if std::env::var("MEMRA_NOFA").is_ok() || !(head_dim == 256 || head_dim == 128) {
1852            // head_dim gate: see prime-path note (fa_prefill is stamped at 256 and 128 only).
1853            e.sdpa_naive(&q, &k, &v, &mut attn, head_dim, n_head, n_head_kv, t, t, scale, true)?;
1854        } else {
1855            e.fa_prefill(&q, &k, &v, &mut attn, head_dim, n_head, n_head_kv, t, t, scale, true)?;
1856        }
1857
1858        // output gate: attn * sigmoid(gate) — qwen35 only (M3 has no gate).
1859        let attn_g = match &gate {
1860            Some(gate) => {
1861                let mut gsig = e.uninit(t * n_head * head_dim)?;
1862                e.sigmoid(gate, &mut gsig, t * n_head * head_dim)?;
1863                let mut ag = e.uninit(t * n_head * head_dim)?;
1864                e.mul(&attn, &gsig, &mut ag, t * n_head * head_dim)?;
1865                ag
1866            }
1867            None => attn,
1868        };
1869
1870        // o projection
1871        let o = e.matmul(&fa.wo, &attn_g, t)?;
1872        Ok(o)
1873    }
1874
1875    /// Linear-attention (Gated DeltaNet) mixer (qwen35 :338-470).
1876    pub fn linear_attn(&self, e: &Engine, la: &LinearAttnLayer, h: &CudaSlice<f32>, t: usize)
1877                   -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1878        let cfg = &self.cfg;
1879        let _n_embd = cfg.n_embd as usize;
1880        let ssm = cfg.ssm.as_ref().unwrap();
1881        let d_state = ssm.state_size as usize;       // 128
1882        let num_k = ssm.group_count as usize;        // 16
1883        let num_v = ssm.time_step_rank as usize;     // 32
1884        let d_conv = ssm.conv_kernel as usize;       // 4
1885        let head_k = d_state; let head_v = d_state;
1886        let key_dim = head_k * num_k;                // 2048
1887        let value_dim = head_v * num_v;              // 4096
1888        let conv_dim = key_dim * 2 + value_dim;      // 8192
1889        let eps = cfg.rms_eps;
1890        let scale = 1.0 / (d_state as f32).sqrt();
1891
1892        // projections
1893        // grouped: one f16 activation convert feeds all four projections (matmul_group)
1894        let mut g4 = e.matmul_group(&[&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha], h, t)?;
1895        let alpha = g4.pop().unwrap();                   // [T, num_v]
1896        let beta_raw = g4.pop().unwrap();                // [T, num_v]
1897        let z = g4.pop().unwrap();                       // [T, value_dim]
1898        let qkv_mixed = g4.pop().unwrap();               // [T, conv_dim] token-major
1899
1900        // conv + GDN repack, FUSED (2026-07-03): ssm_conv1d_gdn reads qkv_mixed [T, conv_dim]
1901        // token-major DIRECTLY (causal window rows t-pad..t, rows<0 = zero prefill state), applies
1902        // the 8-tap conv + SiLU, and scatters straight into the GDN [d_state, num_v, T] q/k/v
1903        // layout with the modulo head-repeat. Replaces transpose + zeros + conv_left_pad +
1904        // ssm_conv1d + qkv_to_gdn_repack (5 launches, conv_in/conv_out scratch + a 16MB@T=512
1905        // round-trip). BIT-IDENTICAL accumulation and scatter mapping.
1906        let _ = (head_k, head_v);
1907        let mut q_g = e.uninit(d_state * num_v * t)?;
1908        let mut k_g = e.uninit(d_state * num_v * t)?;
1909        let mut v_g = e.uninit(d_state * num_v * t)?;
1910        e.ssm_conv1d_gdn(&qkv_mixed, la.ssm_conv1d.float_data(), &mut q_g, &mut k_g, &mut v_g,
1911                         conv_dim, t, d_conv, d_state, num_v, num_k, key_dim)?;
1912        // L2-norm q,k per (head_dim) row — rows are contiguous d_state in q_g.
1913        let mut q_l2 = e.uninit(d_state * num_v * t)?;
1914        e.l2_norm(&q_g, &mut q_l2, d_state, num_v * t, eps)?;
1915        let mut k_l2 = e.uninit(d_state * num_v * t)?;
1916        e.l2_norm(&k_g, &mut k_l2, d_state, num_v * t, eps)?;
1917        let v_gd = v_g;
1918
1919        // beta = sigmoid(beta_raw) ; g_log = a * softplus(alpha + dt). Both need [num_v, T] layout
1920        // (g[t*num_v + h]). beta_raw/alpha are [T, num_v] token-major == that layout already.
1921        let mut beta = e.uninit(t * num_v)?;
1922        e.sigmoid(&beta_raw, &mut beta, t * num_v)?;
1923        // gdn_glog expects alpha [H,T] with alpha[t*H+h] and dt_bias/a [H] — matches token-major [T,num_v].
1924        let mut g_log = e.uninit(t * num_v)?;
1925        e.gdn_glog(&alpha, la.ssm_dt.float_data(), la.ssm_a.float_data(), &mut g_log, num_v, t)?;
1926
1927        // GDN scan (A4: gdn_scan_prefill dispatches chunked WY under MEMRA_GDN_CHUNKED)
1928        let state_in = e.zeros(d_state * d_state * num_v)?;  // zero state (prefill)
1929        let mut state_out = e.zeros(d_state * d_state * num_v)?;
1930        let mut o = e.uninit(d_state * num_v * t)?;
1931        e.gdn_scan_prefill(&q_l2, &k_l2, &v_gd, &g_log, &beta, None, None, &state_in, &mut state_out, &mut o, num_v, t, scale, num_v)?;
1932
1933        // gated RMSNorm: dst = RMSNorm(o, ssm_norm[head_v]) * silu(z). o is [d_state, num_v, T];
1934        // rows of head_v=d_state, nrows = num_v*T. z must match row layout: z is [T, value_dim] token-major
1935        // = [T, num_v*head_v]; per (t, vh) the head_v slice is contiguous -> rows align as (t*num_v+vh).
1936        // o rows are (t*num_v+vh) too. Good.
1937        let mut gn = e.uninit(d_state * num_v * t)?;
1938        e.gated_rmsnorm(&o, la.ssm_norm.float_data(), &z, &mut gn, d_state, num_v * t, eps)?;
1939
1940        // ssm_out projection: gn is [d_state, num_v, T] = [value_dim, T] viewed token-major as [T, value_dim]?
1941        // gn layout: (t*num_v+vh)*d_state + i  == token t, then (vh,i) = channel vh*d_state+i. That's
1942        // token-major [T, value_dim]. linear wants [T, in=value_dim]. Good.
1943        let out = e.matmul(&la.ssm_out, &gn, t)?;
1944        Ok(out)
1945    }
1946}
1947
1948impl HybridModel {
1949    /// MoE FFN (EDGE-1). z: [T, n_embd] (already post-attention-normed). Returns moe_out [T, n_embd].
1950    /// Node-for-node vs llama.cpp build_moe_ffn + qwen35moe::build_layer_ffn.
1951    ///
1952    /// `il` is the trunk layer index — the residency-cache key prefix (a gate-expert of layer 3 is a
1953    /// different 860160-byte block than the same expert of layer 7).
1954    ///
1955    /// Routing: host softmax+sort (default) OR the fused router kernel (MEMRA_FUSED_ROUTER).
1956    /// Dispatch: stage-every-token into 3 scratch slots (default) OR the SLRU residency cache
1957    /// (MEMRA_MOE_CACHE). The cache-HIT weight path is bit-identical to stage-every-token (§B.3).
1958    /// Convenience wrapper used by the hybrid trunk/MTP loops: pulls dims + max-block from `self`.
1959    pub fn moe_ffn_il(&self, e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>, t: usize, il: u16)
1960               -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1961        Self::moe_ffn(e, m, z, t, &self.cfg, il, self.max_moe_block())
1962    }
1963
1964    /// Decode-path twin with a PRE-QUANTIZED z (from add_rms_norm_zq8): threads (zq, zd) into the
1965    /// t=1 dev arm so the per-layer standalone quantize_q8_1 launch folds away. Identical bytes
1966    /// (the fused kernel reproduces quantize_q8_1 exactly); every other path ignores the pair.
1967    pub fn moe_ffn_il_zq8(&self, e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>,
1968                          zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>, t: usize, il: u16)
1969               -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1970        Self::moe_ffn_inner(e, m, z, zq8, t, &self.cfg, il, self.max_moe_block())
1971    }
1972
1973    /// MoE FFN (EDGE-1), source-/model-agnostic. z: [T, n_embd] (already post-attention-normed).
1974    /// Returns moe_out [T, n_embd]. Node-for-node vs llama.cpp build_moe_ffn. Shared by the hybrid
1975    /// (qwen35moe, shared expert present) and the dense-attention MoE (OLMoE, no shared expert) paths;
1976    /// `cfg.moe` supplies the dims and the optional shexp fields decide whether step 3 runs.
1977    ///
1978    /// `il` is the layer index — the residency-cache key prefix. `max_block` is the global max expert
1979    /// stride (fixed cache-slot size); pass `self.max_moe_block()`.
1980    pub(crate) fn moe_ffn(e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>, t: usize,
1981                          cfg: &ModelConfig, il: u16, max_block: usize)
1982               -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1983        Self::moe_ffn_inner(e, m, z, None, t, cfg, il, max_block)
1984    }
1985
1986    #[allow(clippy::too_many_arguments)]
1987    pub(crate) fn moe_ffn_inner(
1988        e: &Engine,
1989        m: &MoeWeights,
1990        z: &CudaSlice<f32>,
1991        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
1992        t: usize,
1993        cfg: &ModelConfig,
1994        il: u16,
1995        max_block: usize,
1996    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1997        let worker_io = crate::spill_pread::worker_enabled();
1998        let epoch_lfu = std::env::var_os("MEMRA_MOE_LFU_DECAY").is_some();
1999        if Engine::moe_cache_enabled() && (worker_io || epoch_lfu) {
2000            e.with_moe_cache(max_block, |cache, _| {
2001                cache.begin_forward_epoch(il, t);
2002                if worker_io {
2003                    cache.begin_worker_scope();
2004                }
2005                Ok(())
2006            })?;
2007        }
2008        // A2: Expert-grouped dispatch for prefill (T>1). MEMRA_MOE_GROUPED=1 routes here.
2009        if t > 1 && std::env::var("MEMRA_MOE_GROUPED").is_ok() {
2010            let grouped_out = Self::moe_ffn_grouped(e, m, z, t, cfg, il, max_block)?;
2011            // MEMRA_MOE_GATE: byte-identity comparison vs sequential path.
2012            // KNOWN t>1 MISMATCH maxdiff ~3.4e-4 (deterministic, 5x bit-identical 2026-07-05): the
2013            // sequential arm routes resident experts through the dev_q8 dp4a path (q8_1-quantized z
2014            // and act rows) while grouped stays f32-dequant qmatvec — a quantize-path difference,
2015            // not a bug (per-stage: act q8-vs-f32 ~4-9e-3 abs on |act|<=3, down-only ~1-3e-4; the
2016            // q8_1 activation-quantize error class). MEMRA_MOE_Q8=0 restores BYTE-IDENTICAL.
2017            if std::env::var("MEMRA_MOE_GATE").is_ok() {
2018                let seq_out = Self::moe_ffn_sequential(e, m, z, t, cfg, il, max_block)?;
2019                let g_host = e.dtoh(&grouped_out)?;
2020                let s_host = e.dtoh(&seq_out)?;
2021                let g_bytes: &[u8] = unsafe { std::slice::from_raw_parts(g_host.as_ptr() as *const u8, g_host.len() * 4) };
2022                let s_bytes: &[u8] = unsafe { std::slice::from_raw_parts(s_host.as_ptr() as *const u8, s_host.len() * 4) };
2023                if g_bytes == s_bytes {
2024                    if il == 0 { println!("moe-gate il={il} t={t} BYTE-IDENTICAL (first layer only printed)"); }
2025                } else {
2026                    let diffs = g_host.iter().zip(s_host.iter()).enumerate()
2027                        .filter(|(_, (a, b))| a != b).count();
2028                    let maxdiff = g_host.iter().zip(s_host.iter())
2029                        .map(|(a, b)| (a - b).abs()).fold(0.0f32, f32::max);
2030                    panic!("moe-gate il={il} t={t} MISMATCH: {diffs}/{} elems differ, maxdiff={maxdiff:.6e}", g_host.len());
2031                }
2032            }
2033            return Ok(grouped_out);
2034        }
2035        Self::moe_ffn_sequential_zq8(e, m, z, zq8, t, cfg, il, max_block)
2036    }
2037
2038    /// Sequential (per-token) MoE FFN -- the original path. Factored out for the gate comparison.
2039    pub(crate) fn moe_ffn_sequential(e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>, t: usize,
2040                          cfg: &ModelConfig, il: u16, max_block: usize)
2041               -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2042        Self::moe_ffn_sequential_zq8(e, m, z, None, t, cfg, il, max_block)
2043    }
2044
2045    /// Append the host-visible router selection for one layer/forward when calibration tracing is
2046    /// enabled. Both sequential and expert-grouped prefill must call this after routing so the
2047    /// trace is independent of the dispatch optimization selected for the forward.
2048    fn trace_moe_routes(il: u16, t: usize, sel_all: &[u32], weights: &[f32])
2049                        -> Result<(), Box<dyn std::error::Error>> {
2050        use std::io::Write as _;
2051        if let Ok(path) = std::env::var("MEMRA_MOE_TRACE") {
2052            let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
2053            let ids: Vec<String> = sel_all.iter().map(|s| s.to_string()).collect();
2054            writeln!(f, "{} {} {}", il, t, ids.join(","))?;
2055        }
2056        if let Ok(path) = std::env::var("MEMRA_MOE_WEIGHT_TRACE") {
2057            let mut f = std::fs::OpenOptions::new().create(true).append(true).open(path)?;
2058            let pairs: Vec<String> = sel_all.iter().zip(weights)
2059                .map(|(expert, weight)| format!("{expert}:{weight:.9}"))
2060                .collect();
2061            writeln!(f, "{} {} {}", il, t, pairs.join(","))?;
2062        }
2063        Ok(())
2064    }
2065
2066    /// Append the f32 input to one MoE layer for offline, layerwise calibration. This diagnostic
2067    /// intentionally performs a DtoH copy and is therefore disabled unless an explicit fresh trace
2068    /// directory is supplied. Each layer owns one payload file; index.jsonl records byte offsets so
2069    /// a validator can prove request/layer coverage before the trace is used for pruning or healing.
2070    fn trace_moe_input(e: &Engine, il: u16, t: usize, n_embd: usize, z: &CudaSlice<f32>)
2071                       -> Result<(), Box<dyn std::error::Error>> {
2072        use std::io::Write as _;
2073        let Ok(dir) = std::env::var("MEMRA_MOE_INPUT_TRACE_DIR") else { return Ok(()) };
2074        let host = e.dtoh(z)?;
2075        if host.len() != t * n_embd {
2076            return Err(format!(
2077                "MoE input trace shape mismatch at layer {il}: got {} values, expected {}x{}",
2078                host.len(), t, n_embd
2079            ).into());
2080        }
2081        let bytes = unsafe {
2082            std::slice::from_raw_parts(
2083                host.as_ptr().cast::<u8>(), host.len() * std::mem::size_of::<f32>()
2084            )
2085        };
2086        let state = MOE_INPUT_TRACE_WRITER.get_or_init(|| std::sync::Mutex::new(None));
2087        let mut state = state.lock().map_err(|_| "MoE input trace writer lock is poisoned")?;
2088        if state.is_none() {
2089            let dir = std::path::PathBuf::from(&dir);
2090            std::fs::create_dir_all(&dir)?;
2091            let index = std::fs::OpenOptions::new().create(true).append(true)
2092                .open(dir.join("index.jsonl"))?;
2093            *state = Some(MoeInputTraceWriter {
2094                dir,
2095                index,
2096                payloads: std::collections::HashMap::new(),
2097            });
2098        }
2099        let writer = state.as_mut().unwrap();
2100        if writer.dir != std::path::Path::new(&dir) {
2101            return Err("MEMRA_MOE_INPUT_TRACE_DIR changed after capture started".into());
2102        }
2103        let file_name = format!("layer-{il:03}.f32");
2104        if !writer.payloads.contains_key(&il) {
2105            let payload = std::fs::OpenOptions::new().create(true).append(true)
2106                .open(writer.dir.join(&file_name))?;
2107            let offset = payload.metadata()?.len();
2108            writer.payloads.insert(il, (payload, offset));
2109        }
2110        let (payload, offset) = writer.payloads.get_mut(&il).unwrap();
2111        let row_offset = *offset;
2112        payload.write_all(bytes)?;
2113        *offset += bytes.len() as u64;
2114        writeln!(
2115            writer.index,
2116            "{{\"format\":\"memra-moe-input-trace-v1\",\"layer\":{il},\"tokens\":{t},\
2117             \"hidden_size\":{n_embd},\"file\":\"{file_name}\",\"offset\":{row_offset},\
2118             \"payload_bytes\":{}}}",
2119            bytes.len()
2120        )?;
2121        Ok(())
2122    }
2123
2124    #[allow(clippy::too_many_arguments)]
2125    pub(crate) fn moe_ffn_sequential_zq8(
2126        e: &Engine,
2127        m: &MoeWeights,
2128        z: &CudaSlice<f32>,
2129        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
2130        t: usize,
2131        cfg: &ModelConfig,
2132        il: u16,
2133        max_block: usize,
2134    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2135        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
2136        let moe = cfg.moe.as_ref().unwrap();
2137        let n_embd = cfg.n_embd as usize;          // 2048 (gate/up in_f, down out_f)
2138        let n_expert = moe.expert_count as usize;  // 256
2139        let n_used = moe.expert_used_count as usize; // 8
2140        let n_ff_exp = moe.expert_ff_length as usize; // 512 (gate/up out_f, down in_f)
2141
2142        // verify the HostExps dims match cfg (catches a wrong-file / transpose mixup)
2143        debug_assert_eq!(m.gate_exps.in_f, n_embd);
2144        debug_assert_eq!(m.gate_exps.out_f, n_ff_exp);
2145        debug_assert_eq!(m.down_exps.in_f, n_ff_exp);  // down is TRANSPOSED: in=512
2146        debug_assert_eq!(m.down_exps.out_f, n_embd);   //                     out=2048
2147        debug_assert_eq!(m.gate_exps.n_expert, n_expert);
2148
2149        let use_cache = Engine::moe_cache_enabled();
2150        let uniform_experts = m.has_uniform_expert_layout();
2151        let moe_q8 = uniform_experts && moe_q8_enabled()
2152            && q8_expert_supported(m.gate_exps.qtype) && q8_expert_supported(m.up_exps.qtype)
2153            && q8_expert_supported(m.down_exps.qtype);
2154        // Experimental secondary backend: complete experts already resident in the SLRU stay on
2155        // CUDA; any expert missing one or more projections runs from the original host GGUF bytes
2156        // through llama.cpp CPU quant dots. Small-t only keeps decode and speculative verification
2157        // in the same numeric/dispatch class; real prefill remains on the established GPU path and
2158        // seeds the residency cache. An explicit library path is the build/runtime gate, so naked
2159        // commands and CI have no llama.cpp or OpenMP dependency.
2160        let cpu_expert_requested = crate::cpu_experts::configured();
2161        if cpu_expert_requested && (cfg.hy3.is_none() || cfg.m3.is_some()) {
2162            return Err(std::io::Error::other(
2163                "MEMRA_CPU_EXPERT_LIB is experimental and currently gated to Hy3",
2164            )
2165            .into());
2166        }
2167        let cpu_hybrid = cpu_expert_requested && t < PRIME_MIN_T && m.dev_exps.is_none();
2168        // A dynamic SLRU changes which complete experts run on CUDA versus llama.cpp CPU dots.
2169        // Those backends are each deterministic but are different numeric configurations, so a
2170        // later prefill eviction can change greedy output. Freeze after the first real prefill;
2171        // decode/spec reads the fixed resident set, while later prefill misses use transient GPU
2172        // staging below and cannot change backend assignment.
2173        let freeze_cpu_residency = cpu_expert_requested
2174            && std::env::var("MEMRA_CPU_EXPERT_FREEZE_CACHE").as_deref() == Ok("1");
2175        let caller_warms_before_freeze = std::env::var("MEMRA_CPU_EXPERT_FREEZE_WARMUP_TOKENS")
2176            .ok()
2177            .and_then(|value| value.parse::<usize>().ok())
2178            .is_some_and(|tokens| tokens > 0);
2179        if cpu_hybrid && freeze_cpu_residency && !caller_warms_before_freeze {
2180            e.freeze_moe_cache();
2181        }
2182        let cache_frozen = use_cache && e.moe_cache_frozen();
2183        let cache_dispatch = use_cache && (!cache_frozen || cpu_hybrid);
2184
2185        // 1. ROUTER: logits = ffn_gate_inp @ z  -> [T, 256]. gate_inp is F32 -> cuBLASLt, whose
2186        // reductions are n-DEPENDENT (lt_ndep probe: m=1 vs m=2 col0 differs every bit). At
2187        // small t (spec verify, 2..15) that shifts router logits vs the T=1 decode chain ->
2188        // top-k WEIGHTS (and at tie margins the SELECTION) differ -> verify != decode. Route
2189        // small-t through per-column m=1 calls (decode-exact contract); real prefill keeps the
2190        // batched GEMM.
2191        let logits = if t < PRIME_MIN_T {
2192            // t == 1 included since 2026-07-10 (was cuBLAS gemvx, 3.1% + adjacent of the depth
2193            // decode map): decode and verify now route through the SAME kernel — the
2194            // verify==decode router parity holds by construction instead of by FP-order luck.
2195            if crate::router_kernel_on() {
2196                // MEMRA_ROUTER_KERNEL=1: in-house router GEMV (battery-gated numeric config —
2197                // top-k discontinuity means FP-order changes can flip routing; oracle arbitrates).
2198                e.router_gemv(m.gate_inp.float_data(), z, cfg.n_embd as usize,
2199                              m.gate_exps.n_expert, t)?
2200            } else {
2201                e.matmul_decode_exact(&m.gate_inp, z, t)?
2202            }
2203        } else if crate::router_prefill_exact_on() && crate::router_kernel_on() {
2204            // SERVE ISOLATION (lane/concat-prime-exact, 2026-08-02): the cuBLASLt router GEMM
2205            // is m-DEPENDENT — probed on the Ornith-35B router weight, rows [0,19) of an m=65
2206            // call differ from the m=19 call by 3.9e-3 while the same probe on the lm_head /
2207            // wq MMQ+f16 weights is BIT-IDENTICAL across m (research/concat-prime-exact-20260802,
2208            // gemm-razor-router-o35b.log vs gemm-razor-o35b.log). Because the router feeds a
2209            // top-k DISCONTINUITY, that perturbation reorders ties and at ~16% of (layer,token)
2210            // pairs changes the selected expert SET — so a request's own prefill routing depended
2211            // on how many OTHER requests' tokens shared its concat batch (cross-request prime
2212            // batching, worker.rs task #13). The in-house router GEMV computes one row per
2213            // (expert, token) block with a fixed per-row reduction order and is m-INVARIANT
2214            // (same probe: BIT-IDENTICAL, gemm-razor-router-gemv-o35b.log), so routing prefill
2215            // through it makes a session's routing a function of its OWN tokens alone — the
2216            // serving isolation contract at the prime level. MEMRA_ROUTER_PREFILL_EXACT=0 reverts
2217            // to the batched cuBLASLt GEMM (numeric-config rollback seam).
2218            e.router_gemv(m.gate_inp.float_data(), z, cfg.n_embd as usize,
2219                          m.gate_exps.n_expert, t)?
2220        } else {
2221            e.matmul(&m.gate_inp, z, t)?
2222        };
2223
2224        // LAUNCH-STRUCTURE STAGE 3 (2026-07-05, MEMRA_MOE_DEV default ON, =0 rollback): ZERO-DtoH
2225        // device-dispatch when this layer's expert blocks are ALL cache-resident. The fused
2226        // router's sel/w stay ON DEVICE; the expert weight pointers come from the per-layer
2227        // device table of fixed slot addresses; gate/up/silu + down/fma run as the same TWO
2228        // launches per token as gdec. Removes the per-layer router DtoH + stream sync — the
2229        // per-token host stall that dominated the 35B decode wall after stages 1+2.
2230        // BIT-IDENTITY: the router kernel is selection-exact vs the host oracle (kernel-check
2231        // tie gate) and the _dev matvec twins reproduce the gdec kernels' exact FP chains; the
2232        // only difference is where sel/w/pointers are READ from (device instead of params).
2233        // Residency: one-shot PREWARM force-admits the layer while free slots cover it
2234        // (MEMRA_MOE_PREWARM=0 -> organic residency, dev path fires when the SLRU fills).
2235        // Any non-resident layer falls through to host routing + the gdec/sequential path.
2236        // FITS-VRAM RESIDENT EXPERTS (2026-07-06): the layer's expert slabs are device-resident
2237        // (load-time decision) — fire the zero-DtoH dev path unconditionally with the prebuilt
2238        // pointer row. No cache, no dispatch, no residency check: the llama full-offload regime
2239        // (it measured 169.55 vs the cache path's 28.5 on the local 35B — the residency-gate
2240        // all-or-nothing fallback was the 6x). BIT-IDENTITY: same _dev kernels, same math; only
2241        // the pointer table's provenance differs (slab base+stride vs SLRU slot addresses).
2242        // MoE PREFILL PAIR-BATCH (2026-07-06, the 16x pp hole): t>1 on resident experts — ONE
2243        // launch per proj covers ALL (token,expert) pairs (grid.y=pair, warp-per-row), replacing
2244        // the per-expert loop (256 experts x 3-4 launches x tiny m_e). Scatter is slot-ordered
2245        // per token (the sequential-axpy bit-identity class). Requires q8-supported qtypes +
2246        // resident slabs. MEMRA_MOE_PAIRS=0 rollback.
2247        // t >= PRIME_MIN_T only (2026-07-06 exactness fix, verify-probe proof): spec VERIFY
2248        // batches (t = 2..K+2) previously rode these pairs kernels while T=1 decode rode the
2249        // dev_q8 loop — different FP chains, verify-T2 logit maxdiff 2.6e-1 vs eager -> greedy
2250        // flips at tight margins -> 35B real-prompt spec self-consistency FAIL (the 27B
2251        // "verify must be kernel-DISPATCH-identical to decode" lesson, MoE edition). Small-t
2252        // now rides the dev loop below (same kernels per token as decode); pairs serves real
2253        // prefill (t >= 16, where spec never verifies).
2254        // sigmoid-router archs (M3, Hy3) must NOT enter the pairs/dev arms: those route via the
2255        // fused SOFTMAX device router (moe_router_topk) — silently wrong experts (the M3
2256        // gate-MISMATCH 74602-vs-92 lesson, 2026-07-07). Host sigmoid routing below is correct.
2257        // Per-expert macro-scales (compressed-tensors NVFP4 ST class, e.g. unsloth 35B-A3B):
2258        // the device-dispatch kernels (pairs/dev) do NOT fold them — those checkpoints must
2259        // ride the macro-aware sequential/staged paths below or every expert output is off by
2260        // its global scale (~3e4x, measured garbage 2026-07-16). GGUF experts: macros None.
2261        let no_exp_macros = m.gate_exps.macros.is_none() && m.up_exps.macros.is_none()
2262            && m.down_exps.macros.is_none();
2263        if cfg.sigmoid_router().is_none() && cfg.m3.is_none() && cfg.hy3.is_none()
2264            && no_exp_macros
2265            && t >= PRIME_MIN_T && m.dev_exps.is_some() && moe_q8_enabled()
2266            && q8_expert_supported(m.gate_exps.qtype) && q8_expert_supported(m.up_exps.qtype)
2267            && q8_expert_supported(m.down_exps.qtype)
2268            && std::env::var("MEMRA_MOE_PAIRS").map(|v| v != "0").unwrap_or(true)
2269            && std::env::var("MEMRA_MOE_STATS").is_err() {
2270            return Self::moe_ffn_pairs(e, m, z, &logits, t, cfg);
2271        }
2272
2273        // t < PRIME_MIN_T: moe_ffn_dev loops tokens serially (1 launch-pair per token) — the
2274        // decode path AND the spec-verify path (dispatch parity = exactness; see pairs gate
2275        // above). Serial launches are fine at t<=10 (K+2); real prefill never lands here.
2276        // moe_ffn_dev routes via the FUSED SOFTMAX device router (moe_router_topk) — sigmoid
2277        // routing (M3, Hy3: +expert bias) has no device kernel yet, so those arches must NOT
2278        // enter the dev arms: with MOE_CACHE=1 M3 silently routed softmax = wrong experts
2279        // (gate MISMATCH 74602 vs 92, caught 2026-07-07). Host sigmoid path below is correct.
2280        // macro-carrying experts are handled inside the dev path now (epilogue fold + w-scale);
2281        // pairs/gdec/csr keep their macro gates until their kernels grow the fold.
2282        let dev_ok = uniform_experts && cfg.m3.is_none() && cfg.hy3.is_none();
2283        // Observation modes must route through the host-visible selection below. Otherwise a fully
2284        // resident layer returns through device dispatch before its trace/stats row is recorded,
2285        // silently biasing calibration toward only non-resident layers on large-VRAM machines.
2286        let observe_routes = std::env::var("MEMRA_MOE_STATS").is_ok()
2287            || std::env::var("MEMRA_MOE_TRACE").is_ok()
2288            || std::env::var("MEMRA_MOE_WEIGHT_TRACE").is_ok()
2289            || std::env::var("MEMRA_MOE_INPUT_TRACE_DIR").is_ok();
2290        if dev_ok && t < PRIME_MIN_T && m.dev_exps.is_some() && n_used <= 8 && moe_dev_enabled()
2291            && !observe_routes {
2292            return Self::moe_ffn_dev(e, m, z, zq8, &logits, t, cfg, il, max_block);
2293        }
2294        if dev_ok && use_cache && n_used <= 8 && moe_dev_enabled()
2295            && !observe_routes {
2296            let row_ok = e.with_moe_cache(max_block, |c, eng| {
2297                if moe_prewarm_enabled() { c.prewarm_layer(il, m, eng)?; }
2298                Ok(c.layer_dev_row(il, n_expert, eng)?.is_some())
2299            })?;
2300            if row_ok {
2301                return Self::moe_ffn_dev(e, m, z, zq8, &logits, t, cfg, il, max_block);
2302            }
2303        }
2304
2305        // Per-token (sel[8], w[8]) — sigmoid host oracle (M3/Hy3), else fused-router/softmax.
2306        let (sel_all, w_all, routed_cpu_input) = if let Some(sig) = cfg.sigmoid_router() {
2307            if cpu_hybrid {
2308                let (sel, w, input) = Self::moe_route_sigmoid_with_input(
2309                    e,
2310                    &logits,
2311                    z,
2312                    t,
2313                    n_expert,
2314                    n_used,
2315                    m.exp_probs_b.as_deref(),
2316                    sig,
2317                    m.active_experts.as_deref(),
2318                )?;
2319                (sel, w, Some(input))
2320            } else {
2321                let (sel, w) = Self::moe_route_cfg(
2322                    e,
2323                    &logits,
2324                    t,
2325                    n_expert,
2326                    n_used,
2327                    m.exp_probs_b.as_deref(),
2328                    Some(sig),
2329                    m.active_experts.as_deref(),
2330                )?;
2331                (sel, w, None)
2332            }
2333        } else {
2334            let (sel, w) = Self::moe_route_cfg(
2335                e,
2336                &logits,
2337                t,
2338                n_expert,
2339                n_used,
2340                None,
2341                None,
2342                m.active_experts.as_deref(),
2343            )?;
2344            (sel, w, None)
2345        };
2346
2347        // MEMRA_MOE_TRACE=<path>: append one line per (layer, step) with the selected expert ids —
2348        // offline analysis derives the decode working set + step-to-step reuse (the go/no-go
2349        // measurement for resident-expert tiering; see rig5090.jsonl 2026-07-07 pinned-tier row).
2350        Self::trace_moe_routes(il, t, &sel_all, &w_all)?;
2351        Self::trace_moe_input(e, il, t, n_embd, z)?;
2352
2353        // Worker reads ordinarily overlap NVMe with the current expert but leave their H2D copies
2354        // demand-serialized on the compute stream. Hy3 decode already has a safe owner-thread
2355        // boundary here: sigmoid routing read `logits` back to the host, which synchronized all
2356        // earlier-layer compute. Submit the complete selected set first, then reserve only victims
2357        // outside that set and queue its H2D copies on the copy stream. Dispatch inserts an event
2358        // wait for each pending block, so later copies can overlap the earlier expert kernels while
2359        // the selected weights and numerical kernels remain unchanged. Restrict this experiment to
2360        // T=1; batched forwards can have token-local consumers still in flight between selections.
2361        // The CPU backend reads mmap-backed model bytes directly. Positioned-read worker buffers
2362        // are CUDA write-combined staging allocations and are intentionally not CPU-compute inputs;
2363        // do not spend NVMe bandwidth filling them for a layer whose misses go to CPU.
2364        let worker_disk_prefetch =
2365            cache_dispatch && crate::spill_pread::worker_enabled() && !cpu_hybrid;
2366        let promote_worker_h2d =
2367            t == 1 && worker_disk_prefetch && crate::spill_pread::copy_h2d_enabled();
2368        if promote_worker_h2d {
2369            let mut selected_blocks = Vec::with_capacity(n_used * 3);
2370            for &ex in sel_all.iter().take(n_used) {
2371                let ex = ex as u16;
2372                selected_blocks.extend([
2373                    BlockId::new(il, PROJ_GATE, ex),
2374                    BlockId::new(il, PROJ_UP, ex),
2375                    BlockId::new(il, PROJ_DOWN, ex),
2376                ]);
2377            }
2378            for &ex in sel_all.iter().take(n_used) {
2379                Self::moe_prefetch_disk_expert(e, il, ex as usize, m, max_block, &selected_blocks)?;
2380            }
2381            e.with_moe_cache(max_block, |cache, eng| {
2382                cache.promote_worker_reads_at_safe_boundary(
2383                    &selected_blocks,
2384                    &selected_blocks,
2385                    eng,
2386                )?;
2387                Ok(())
2388            })?;
2389        }
2390
2391        // MEMRA_MOE_STATS: per-layer routing stats for the A2 (expert-grouped prefill) baseline —
2392        // per-token expert-id entropy, active-expert coverage, tokens-per-expert group sizes.
2393        if t > 1 && std::env::var("MEMRA_MOE_STATS").is_ok() {
2394            let mut cnt = vec![0u32; n_expert];
2395            for &s in sel_all.iter() { cnt[s as usize] += 1; }
2396            let total = sel_all.len() as f64;
2397            let mut h = 0.0f64;
2398            let mut active = 0usize;
2399            for &c in &cnt { if c > 0 { active += 1; let p = c as f64 / total; h -= p * p.log2(); } }
2400            let maxc = cnt.iter().copied().max().unwrap_or(0);
2401            println!("moe-stats il={} t={} assignments={} active={}/{} entropy={:.3}b (max {:.3}b) mean_tok_per_active={:.2} max_tok_per_expert={}",
2402                     il, t, sel_all.len(), active, n_expert, h, (n_expert as f64).log2(), total / active.max(1) as f64, maxc);
2403        }
2404
2405        // LAUNCH-STRUCTURE STAGE 2 (2026-07-05): moe_out memset elision on the gdec path.
2406        // moe_down8_fma_f32 FULLY overwrites its token row (dst[o] = the in-kernel FMA chain that
2407        // starts at 0.0f — numerically the axpy-into-zeroed-row chain), so when the grouped-decode
2408        // path fires the upfront `e.zeros(t*n_embd)` memset is pure launch churn. Allocate uninit
2409        // when gdec CAN fire (any t — decode t=1 AND the spec verify t=K+1 route here per token)
2410        // and lazily zero ONLY the row of a token that falls through to the sequential axpy loop.
2411        // BIT-IDENTITY: unchanged — every row is either fully overwritten (gdec) or
2412        // zeroed-then-accumulated exactly as before (fallback).
2413        let gdec_may_fire = uniform_experts && use_cache && n_used <= 8 && gdec_enabled();
2414        let mut moe_out = if gdec_may_fire {
2415            e.uninit(t * n_embd)?
2416        } else {
2417            e.zeros(t * n_embd)?
2418        };
2419        // The router readback above already established a host boundary. Copy each small-t hidden
2420        // row once so CPU miss experts can start while the owner thread queues resident GPU work.
2421        let cpu_input = if cpu_hybrid {
2422            Some(routed_cpu_input.ok_or("CPU expert routing did not return the MoE input")?)
2423        } else {
2424            None
2425        };
2426
2427        // GPU scratch: one slot per proj, big enough for ONE expert (default stage-every-token path).
2428        // STAGE 2: LAZY — allocated only if the no-cache staging path actually runs (under
2429        // MEMRA_MOE_CACHE they were 3 dead ~1MB alloc_zeros + memset + free per layer per token,
2430        // measured ~123 memsets/token of the decode wall).
2431        let g_len = m.gate_exps.max_expert_bytes();  // 860160 for the uniform 35B gate
2432        let u_len = m.up_exps.max_expert_bytes();    // 860160 for the uniform 35B up
2433        let d_len = m.down_exps.max_expert_bytes();  // 1114112 for the uniform 35B down
2434        let mut scratch_g: Option<CudaSlice<u8>> = None;
2435        let mut scratch_u: Option<CudaSlice<u8>> = None;
2436        let mut scratch_d: Option<CudaSlice<u8>> = None;
2437        // `max_block` (the GLOBAL max expert stride across all layers) is passed in — the cache slots
2438        // are FIXED-ADDRESS and must fit any layer's block (UD/dynamic GGUFs vary quant per layer).
2439
2440        // EDGE-1 §C.2/C.3: the optional pipeline queues the next selected expert's MISS blocks on
2441        // the copy stream before launching the current expert's compute. Pending slots stay invisible
2442        // to cache hits until dispatch inserts the completion-event wait; current gate/up/down ids are
2443        // protected from eviction. This changes scheduling only, never the GGUF bytes or GEMM path.
2444        let page_window = moe_page_prefetch_window();
2445
2446        // 2. PER TOKEN: routed-expert loop. The ONE dispatch change vs Stage-1: a resident slot
2447        //    (cache HIT, no H2D) OR a staged slot (MISS) feeds the SAME unchanged qmatvec_view.
2448        for tok in 0..t {
2449            let sel = &sel_all[tok * n_used..(tok + 1) * n_used];
2450            let w = &w_all[tok * n_used..(tok + 1) * n_used];
2451            let zt = z.slice(tok * n_embd..(tok + 1) * n_embd);  // CudaView<f32>
2452            let mut tok_q8: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
2453
2454            // STAGE-2 GROUPED DECODE (2026-07-04, MEMRA_MOE_GDEC default ON, =0 rollback): fold
2455            // this token's whole routed-expert FFN (8x gate/up/silu + 8x down/axpy = 40 launches)
2456            // into TWO launches via expert-pointer indirection over the fixed-address cache slots.
2457            // Fires only when ALL 3*n_used blocks are ALREADY cache-resident (pure-HIT: zero
2458            // memcpy, zero admission, so no slot can move under the collected pointers) — any
2459            // miss falls through to the sequential loop below, which admits as before. In steady
2460            // state on a fully-resident rig every token-layer takes the grouped path.
2461            // BIT-IDENTITY: each in-kernel dot reproduces qmatvec_f32's exact reduction; SiLU is
2462            // silu_mul_f32's exact expression; the down accumulation is a slot-ordered
2463            // __fmaf_rn chain == the sequential axpy_f32 chain (MEMRA_MOE_GDEC_GATE compares).
2464            // cfg.m3: the grouped kernels' fused epilogues are plain SiLU — M3's swigluoai must
2465            // NOT take them until the kernels grow the clamped variant. NVFP4 experts carry
2466            // per-expert macro-scales the fused kernels don't fold — those fall through too.
2467            let no_macros = m.gate_exps.macros.is_none() && m.up_exps.macros.is_none()
2468                && m.down_exps.macros.is_none();
2469            if gdec_may_fire && moe_q8 && cfg.m3.is_none() && no_macros {
2470                if tok_q8.is_none() {
2471                    tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
2472                }
2473                let (zq, zd) = tok_q8.as_ref().unwrap();
2474                if Self::moe_gdec_token_q8(e, m, il, max_block, zq, zd, sel, w,
2475                                           &mut moe_out, tok, n_embd, n_ff_exp, n_used)? {
2476                    continue;
2477                }
2478            } else if gdec_may_fire && cfg.m3.is_none() && no_macros
2479                && Self::moe_gdec_token(e, m, il, max_block, &zt, sel, w,
2480                                        &mut moe_out, tok, n_embd, n_ff_exp, n_used)? {
2481                continue;
2482            }
2483
2484            // STAGE 2 memset-elision invariant: moe_out was allocated UNINIT when gdec could fire.
2485            // This token fell through to the sequential axpy loop, which ACCUMULATES — zero its row
2486            // first (row-sized memset, replaces the old full-buffer zeros; other rows are gdec-owned).
2487            if gdec_may_fire {
2488                let mut row = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2489                e.memset_zeros_view(&mut row)?;
2490            }
2491
2492            // Fiddler-style split at whole-expert granularity. Partial residency is deliberately a
2493            // CPU assignment: transferring the missing projection would reintroduce the PCIe/NVMe
2494            // stall this path exists to remove, while mixing projections would require another
2495            // activation round-trip. Weight addresses remain valid until this worker is joined at
2496            // the bottom of the token scope.
2497            let mut cpu_mask = vec![false; sel.len()];
2498            let cpu_worker = if let Some(host_input) = cpu_input.as_ref() {
2499                let gpu_resident = if use_cache {
2500                    e.with_moe_cache(max_block, |cache, _| {
2501                        Ok(sel
2502                            .iter()
2503                            .map(|&expert| {
2504                                let expert = expert as u16;
2505                                [PROJ_GATE, PROJ_UP, PROJ_DOWN]
2506                                    .into_iter()
2507                                    .filter(|&projection| {
2508                                        cache
2509                                            .resident(BlockId::new(il, projection, expert))
2510                                            .is_some()
2511                                    })
2512                                    .count()
2513                            })
2514                            .collect::<Vec<_>>())
2515                    })?
2516                } else {
2517                    vec![0; sel.len()]
2518                };
2519                let mut cpu_selected = Vec::new();
2520                for (index, (&expert, &route_weight)) in sel.iter().zip(w).enumerate() {
2521                    if gpu_resident[index] != 3 {
2522                        cpu_mask[index] = true;
2523                        crate::cpu_experts::record_incomplete_gpu_residency(gpu_resident[index]);
2524                        let expert = expert as usize;
2525                        cpu_selected.push((expert, route_weight));
2526                    }
2527                }
2528                if crate::cpu_experts::predictor_enabled() {
2529                    // Fire-and-forget lookahead: the predictor worker scores layers il+1..
2530                    // from this layer's MoE input and prefetches predicted-and-missing
2531                    // experts into the companion RAM cache. Never blocks this thread.
2532                    let row = &host_input[tok * n_embd..(tok + 1) * n_embd];
2533                    crate::cpu_experts::predictor_submit(il, row);
2534                }
2535                if cpu_selected.is_empty() {
2536                    None
2537                } else {
2538                    let row = &host_input[tok * n_embd..(tok + 1) * n_embd];
2539                    let job = crate::cpu_experts::prepare_job(m, il, &cpu_selected, row)
2540                        .map_err(std::io::Error::other)?;
2541                    Some(crate::cpu_experts::submit(job).map_err(std::io::Error::other)?)
2542                }
2543            } else {
2544                None
2545            };
2546
2547            let worker_window = worker_disk_prefetch
2548                .then(worker_prefetch_window)
2549                .unwrap_or(0);
2550            for (j, &ex) in sel.iter().enumerate() {
2551                if cpu_mask[j] {
2552                    continue;
2553                }
2554                let ex = ex as usize;
2555                for next in page_prefetch_positions(j, sel.len(), page_window) {
2556                    Self::moe_prefetch_host_expert(sel[next] as usize, m);
2557                }
2558                let keep = [
2559                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_GATE, ex as u16),
2560                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_UP, ex as u16),
2561                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_DOWN, ex as u16),
2562                ];
2563                if worker_disk_prefetch && worker_window > 0 {
2564                    for next in worker_prefetch_positions(j, sel.len(), worker_window) {
2565                        Self::moe_prefetch_disk_expert(
2566                            e,
2567                            il,
2568                            sel[next] as usize,
2569                            m,
2570                            max_block,
2571                            &keep,
2572                        )?;
2573                    }
2574                } else if cache_dispatch
2575                    && !cpu_hybrid
2576                    && moe_prefetch_enabled()
2577                    && j + 1 < sel.len()
2578                {
2579                    let next = sel[j + 1] as usize;
2580                    Self::moe_prefetch_expert(e, il, next, m, max_block, &keep)?;
2581                }
2582                let [gate_q8, up_q8, down_q8] = [moe_q8; 3];
2583                if cache_dispatch && (gate_q8 || up_q8 || down_q8) {
2584                    // dp4a EXPERT PATH (MEMRA_MOE_Q8): quantize z-row once per token. Mixed expert
2585                    // layouts stay on the metadata-aware f32 path.
2586                    if (gate_q8 || up_q8) && tok_q8.is_none() {
2587                        tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
2588                    }
2589                    let gate = if gate_q8 {
2590                        let (zq, zd) = tok_q8.as_ref().unwrap();
2591                        Self::moe_cached_gemm_q8(e, il, PROJ_GATE, ex, m, max_block, zq, zd)?
2592                    } else {
2593                        Self::moe_cached_gemm(e, il, PROJ_GATE, ex, m, max_block, &zt)?
2594                    };
2595                    let up = if up_q8 {
2596                        let (zq, zd) = tok_q8.as_ref().unwrap();
2597                        Self::moe_cached_gemm_q8(e, il, PROJ_UP, ex, m, max_block, zq, zd)?
2598                    } else {
2599                        Self::moe_cached_gemm(e, il, PROJ_UP, ex, m, max_block, &zt)?
2600                    };
2601                    let mut act = e.uninit(n_ff_exp)?;
2602                    Self::ffn_act_scaled(
2603                        e,
2604                        cfg,
2605                        &gate,
2606                        &up,
2607                        m.gate_exps.macro_scale(ex),
2608                        m.up_exps.macro_scale(ex),
2609                        &mut act,
2610                        n_ff_exp,
2611                    )?;
2612                    let y = if down_q8 {
2613                        let (aq2, ad2) = e.quantize_q8_1(&act, 1, n_ff_exp)?;
2614                        Self::moe_cached_gemm_q8(e, il, PROJ_DOWN, ex, m, max_block, &aq2, &ad2)?
2615                    } else {
2616                        let actv = act.slice(0..n_ff_exp);
2617                        Self::moe_cached_gemm(e, il, PROJ_DOWN, ex, m, max_block, &actv)?
2618                    };
2619                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2620                    // down-proj macro folds into the accumulate weight (1.0 for non-macro archs).
2621                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
2622                } else if cache_dispatch {
2623                    // SLRU residency cache: per-projection, dispatch the block (HIT => resident slot,
2624                    // MISS => staged slot) then run the SAME unchanged qmatvec_view from that slot.
2625                    // The bytes the kernel reads are byte-for-byte the same GGUF block (§B.3); the
2626                    // only difference between HIT and MISS is whether the memcpy_htod ran.
2627                    let gate = Self::moe_cached_gemm(e, il, PROJ_GATE, ex, m, max_block, &zt)?;
2628                    let up   = Self::moe_cached_gemm(e, il, PROJ_UP,   ex, m, max_block, &zt)?;
2629                    let mut act = e.uninit(n_ff_exp)?;  // activation fully overwrites
2630                    Self::ffn_act_scaled(e, cfg, &gate, &up,
2631                        m.gate_exps.macro_scale(ex), m.up_exps.macro_scale(ex), &mut act, n_ff_exp)?;
2632                    let actv = act.slice(0..n_ff_exp);
2633                    let y = Self::moe_cached_gemm(e, il, PROJ_DOWN, ex, m, max_block, &actv)?;
2634                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2635                    // down-proj macro folds into the accumulate weight (post-matmul linear scale).
2636                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
2637                } else if cache_frozen {
2638                    // A later prompt prime must not change the CPU/GPU assignment frozen after the
2639                    // first prime. Reuse every fixed resident projection directly and stage only a
2640                    // true miss through the ordinary scratch slot. This preserves the established
2641                    // f32-dequant numeric path while avoiding a full-bank reread on every prime.
2642                    let gate = Self::moe_frozen_gemm(
2643                        e,
2644                        il,
2645                        PROJ_GATE,
2646                        ex,
2647                        m,
2648                        max_block,
2649                        &zt,
2650                        &mut scratch_g,
2651                        g_len,
2652                    )?;
2653                    let up = Self::moe_frozen_gemm(
2654                        e,
2655                        il,
2656                        PROJ_UP,
2657                        ex,
2658                        m,
2659                        max_block,
2660                        &zt,
2661                        &mut scratch_u,
2662                        u_len,
2663                    )?;
2664                    let mut act = e.uninit(n_ff_exp)?;
2665                    Self::ffn_act_scaled(
2666                        e,
2667                        cfg,
2668                        &gate,
2669                        &up,
2670                        m.gate_exps.macro_scale(ex),
2671                        m.up_exps.macro_scale(ex),
2672                        &mut act,
2673                        n_ff_exp,
2674                    )?;
2675                    let actv = act.slice(0..n_ff_exp);
2676                    let y = Self::moe_frozen_gemm(
2677                        e,
2678                        il,
2679                        PROJ_DOWN,
2680                        ex,
2681                        m,
2682                        max_block,
2683                        &actv,
2684                        &mut scratch_d,
2685                        d_len,
2686                    )?;
2687                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2688                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
2689                } else {
2690                    // Stage-1: stage gate/up/down for expert `ex` into the scratch slots, then GEMM.
2691                    // Lazy scratch: first no-cache expert allocates the 3 slots (uninit — stage_expert
2692                    // fully overwrites the byte range the GEMM reads).
2693                    if scratch_g.is_none() {
2694                        scratch_g = Some(e.alloc_u8_uninit(g_len)?);
2695                        scratch_u = Some(e.alloc_u8_uninit(u_len)?);
2696                        scratch_d = Some(e.alloc_u8_uninit(d_len)?);
2697                    }
2698                    let (sg, su, sd) = (scratch_g.as_mut().unwrap(), scratch_u.as_mut().unwrap(),
2699                                        scratch_d.as_mut().unwrap());
2700                    let gl = m.gate_exps.expert_layout(ex);
2701                    let ul = m.up_exps.expert_layout(ex);
2702                    let dl = m.down_exps.expert_layout(ex);
2703                    e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
2704                    let gate = e.qmatvec_view(sg, 0..gl.len, &zt, 1,
2705                        m.gate_exps.in_f, m.gate_exps.out_f, gl.qtype, gl.row_bytes)?;
2706
2707                    e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
2708                    let up = e.qmatvec_view(su, 0..ul.len, &zt, 1,
2709                        m.up_exps.in_f, m.up_exps.out_f, ul.qtype, ul.row_bytes)?;
2710
2711                    let mut act = e.uninit(n_ff_exp)?;  // activation fully overwrites
2712                    Self::ffn_act_scaled(e, cfg, &gate, &up,
2713                        m.gate_exps.macro_scale(ex), m.up_exps.macro_scale(ex), &mut act, n_ff_exp)?;
2714
2715                    e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
2716                    let actv = act.slice(0..n_ff_exp);
2717                    let y = e.qmatvec_view(sd, 0..dl.len, &actv, 1,
2718                        m.down_exps.in_f, m.down_exps.out_f, dl.qtype, dl.row_bytes)?;
2719
2720                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2721                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
2722                }
2723            }
2724            if let Some(worker) = cpu_worker {
2725                let cpu_output = worker.wait().map_err(std::io::Error::other)?;
2726                let cpu_output = e.htod(&cpu_output)?;
2727                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
2728                e.axpy_into(&cpu_output, 1.0, &mut dst, n_embd)?;
2729            }
2730            if cpu_hybrid && !cache_frozen && cpu_expert_profile_admit_enabled() {
2731                for (j, &ex) in sel.iter().enumerate() {
2732                    if cpu_mask[j] {
2733                        Self::moe_profile_admit_expert(e, il, ex as usize, m, max_block)?;
2734                    }
2735                }
2736            }
2737        }
2738
2739        // 3. SHARED EXPERT (ALWAYS-ON, no routing) on the SAME z — qwen35moe only. OLMoE and most
2740        //    vanilla MoE have NO shared expert (the shexp tensors are absent / `None`); skip it then.
2741        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
2742        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
2743        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
2744            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
2745        {
2746            let n_ff_sh = gate_shexp.out_features();  // 512
2747            // Q8 TRUNK-FUSION (decode t=1): gate_shexp+up_shexp are Q8_0 same-shape on the 35B —
2748            // ONE fused2 launch (also folds the two per-matmul re-quantizes of z into one).
2749            // Bit-identical per (tensor,row); falls back to the two matmul calls when ineligible.
2750            // Small-t (spec verify 2..15) rides matmul_decode_exact so shexp FP chains match the
2751            // t==1 decode chain per column (cuBLASLt n-dependence + dp4a-vs-mmvq class); real
2752            // prefill keeps the batched matmul. Activation routes through ffn_act (SiLU for
2753            // softmax archs, clamped swigluoai for M3 — identical to silu_mul when cfg.m3 is None).
2754            let verify_t = t > 1 && t < PRIME_MIN_T;
2755            let (sg_gate, sg_up) = if t == 1 {
2756                match e.matmul_q8_fused2_x(gate_shexp, up_shexp, z)? {
2757                    Some(pair) => pair,
2758                    None => (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?),
2759                }
2760            } else if verify_t {
2761                (e.matmul_decode_exact(gate_shexp, z, t)?, e.matmul_decode_exact(up_shexp, z, t)?)
2762            } else {
2763                (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?)   // [T, 512] each
2764            };
2765            let mut sa = e.uninit(t * n_ff_sh)?;  // activation fully overwrites
2766            Self::ffn_act(e, cfg, &sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
2767            let sh = if verify_t { e.matmul_decode_exact(down_shexp, &sa, t)? }
2768                     else { e.matmul(down_shexp, &sa, t)? };     // [T, n_embd]
2769
2770            // shexp gate: qwen35moe sigmoid-gates via ffn_gate_inp_shexp (1-D ne=[n_embd] ->
2771            // out_f=1); M3 has no gate tensor -> weight 1.0. Decode + verify ride the fused
2772            // sigmoid-dot kernel (one fold order for both chains; kills the per-layer
2773            // cuBLASLt m=1 splitK GEMM — 40x/step, ~10% of the H100 q35 decode step).
2774            // SERVE ISOLATION (lane/concat-prime-exact, 2026-08-02): PREFILL rides it too.
2775            // This out_f=1 cuBLASLt GEMV is the SECOND m-dependent op in the trunk (probed:
2776            // rows [0,19) move by 1.07e-4 between m=74 and m=75 while sigmoid_dot_rows is
2777            // BIT-IDENTICAL — allw-shexpgate-o35b.log). The gate multiplies the shared
2778            // expert's contribution into every token's residual, so under cross-request
2779            // concat prefill a session's hidden state depended on its co-arrivals' token
2780            // count. MEMRA_ROUTER_PREFILL_EXACT=0 reverts to the batched cuBLASLt linear.
2781            let g = match &m.gate_inp_shexp {
2782                Some(gate_inp_shexp) => {
2783                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
2784                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
2785                    } else {
2786                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
2787                        let mut g = e.uninit(t)?;  // sigmoid fully overwrites
2788                        e.sigmoid(&gs, &mut g, t)?;
2789                        g
2790                    }
2791                }
2792                None => e.htod(&vec![1.0f32; t])?,
2793            };
2794            // moe_out[r, :] += sh[r, :] * g[r]   (per-token scalar gate; g=1 ungated)
2795            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
2796        }
2797
2798        Ok(moe_out)
2799    }
2800
2801    /// Stage-1 (no-cache) per-DECODE-TOKEN H2D bytes: every routed block re-staged every layer every
2802    /// token = sum over MoE layers of n_used * (gate+up+down expert_stride). The §D.4 PCIe baseline.
2803    pub fn stage1_h2d_per_token(&self) -> u64 {
2804        use crate::hybrid::Ffn;
2805        let n_used = self.cfg.moe.as_ref().map(|m| m.expert_used_count as u64).unwrap_or(0);
2806        let mut bytes = 0u64;
2807        for l in self.layers.iter() {
2808            if let Ffn::Moe(m) = &l.ffn {
2809                bytes += n_used * (m.gate_exps.max_expert_bytes() + m.up_exps.max_expert_bytes()
2810                                   + m.down_exps.max_expert_bytes()) as u64;
2811            }
2812        }
2813        bytes
2814    }
2815
2816    /// Largest expert block (bytes) across ALL MoE layers + the MTP head — the fixed cache slot size.
2817    /// UD/dynamic GGUFs quant different layers differently, so `expert_stride` varies per layer; the
2818    /// residency cache slots are fixed-address and must fit any block, so size to this global max.
2819    pub(crate) fn max_moe_block(&self) -> usize {
2820        use crate::hybrid::Ffn;
2821        let mut mx = 0usize;
2822        let mut scan = |ffn: &Ffn| {
2823            if let Ffn::Moe(m) = ffn {
2824                mx = mx.max(m.gate_exps.max_expert_bytes())
2825                       .max(m.up_exps.max_expert_bytes())
2826                       .max(m.down_exps.max_expert_bytes());
2827            }
2828        };
2829        for l in self.layers.iter() { scan(&l.ffn); }
2830        if let Some(mtp) = self.mtp.as_ref() { scan(&mtp.ffn); }
2831        mx
2832    }
2833
2834    /// Exact retained projection lengths for cache class sizing. Pruned ids keep router positions
2835    /// but have no bytes and therefore consume no residency slot.
2836    pub(crate) fn moe_cache_block_sizes(&self) -> Vec<usize> {
2837        use crate::hybrid::Ffn;
2838        let mut sizes = Vec::new();
2839        let mut scan = |ffn: &Ffn| {
2840            let Ffn::Moe(m) = ffn else { return };
2841            for ex in 0..m.gate_exps.n_expert {
2842                if m.active_experts.as_ref().is_some_and(|active| !active[ex]) {
2843                    continue;
2844                }
2845                for exps in [&m.gate_exps, &m.up_exps, &m.down_exps] {
2846                    let len = exps.expert_layout(ex).len;
2847                    if len > 0 {
2848                        sizes.push(len);
2849                    }
2850                }
2851            }
2852        };
2853        for layer in &self.layers {
2854            scan(&layer.ffn);
2855        }
2856        if let Some(mtp) = &self.mtp {
2857            scan(&mtp.ffn);
2858        }
2859        sizes
2860    }
2861
2862    /// Persist the frozen residency set so a later process can restage it directly and skip
2863    /// the profiling warmup. Plain text: a versioned header binding slot geometry, then one
2864    /// `layer proj ex` triple per line. A mismatched or stale profile is rejected at load
2865    /// (header check) or degrades to fewer restaged blocks (per-id checks); either way the
2866    /// post-freeze argmax gate still validates the serving assignment.
2867    pub fn save_cpu_expert_residency_profile(
2868        &self,
2869        e: &Engine,
2870        path: &std::path::Path,
2871    ) -> Result<(), Box<dyn std::error::Error>> {
2872        let Some(ids) = e.export_moe_residency() else {
2873            return Err("no MoE residency cache to persist".into());
2874        };
2875        let mut body = format!(
2876            "memra-freeze-profile v1 max_block={} blocks={}\n",
2877            self.max_moe_block(),
2878            ids.len()
2879        );
2880        for (layer, proj, ex) in &ids {
2881            body.push_str(&format!("{layer} {proj} {ex}\n"));
2882        }
2883        let tmp = path.with_extension("tmp");
2884        std::fs::write(&tmp, body)?;
2885        std::fs::rename(&tmp, path)?;
2886        println!(
2887            "[moe-cache] freeze profile saved: {} blocks -> {}",
2888            ids.len(),
2889            path.display()
2890        );
2891        Ok(())
2892    }
2893
2894    /// Restage a saved freeze profile and freeze immediately, skipping the profiling warmup.
2895    /// Returns false (leaving the cache untouched for a normal warmup) when the profile is
2896    /// missing or its header does not match this model's slot geometry.
2897    pub fn restore_cpu_expert_residency_profile(
2898        &self,
2899        e: &Engine,
2900        path: &std::path::Path,
2901    ) -> Result<bool, Box<dyn std::error::Error>> {
2902        use crate::hybrid::Ffn;
2903        use crate::moe_cache::BlockId;
2904        let Ok(content) = std::fs::read_to_string(path) else {
2905            return Ok(false);
2906        };
2907        let mut lines = content.lines();
2908        let Some(header) = lines.next() else { return Ok(false) };
2909        let expected = format!("memra-freeze-profile v1 max_block={}", self.max_moe_block());
2910        if !header.starts_with(&expected) {
2911            println!(
2912                "[moe-cache] freeze profile ignored (geometry mismatch): {}",
2913                path.display()
2914            );
2915            return Ok(false);
2916        }
2917        let mut by_layer: std::collections::HashMap<u16, Vec<BlockId>> =
2918            std::collections::HashMap::new();
2919        for line in lines {
2920            let mut fields = line.split_whitespace();
2921            let (Some(layer), Some(proj), Some(ex)) =
2922                (fields.next(), fields.next(), fields.next())
2923            else {
2924                continue;
2925            };
2926            let (Ok(layer), Ok(proj), Ok(ex)) =
2927                (layer.parse::<u16>(), proj.parse::<u8>(), ex.parse::<u16>())
2928            else {
2929                continue;
2930            };
2931            by_layer
2932                .entry(layer)
2933                .or_default()
2934                .push(BlockId::new(layer, proj, ex));
2935        }
2936        let requested: usize = by_layer.values().map(Vec::len).sum();
2937        if requested == 0 {
2938            return Ok(false);
2939        }
2940        let max_block = self.max_moe_block();
2941        let mut restaged = 0usize;
2942        let mut stage_layer = |layer_index: u16,
2943                               ffn: &Ffn|
2944         -> Result<(), Box<dyn std::error::Error>> {
2945            let Ffn::Moe(m) = ffn else { return Ok(()) };
2946            let Some(ids) = by_layer.get(&layer_index) else {
2947                return Ok(());
2948            };
2949            e.with_moe_cache(max_block, |cache, eng| {
2950                for id in ids {
2951                    if cache.restage_block(*id, m, eng)? {
2952                        restaged += 1;
2953                    }
2954                }
2955                Ok(())
2956            })
2957        };
2958        for (index, layer) in self.layers.iter().enumerate() {
2959            stage_layer(index as u16, &layer.ffn)?;
2960        }
2961        if let Some(mtp) = self.mtp.as_ref() {
2962            stage_layer(u16::MAX, &mtp.ffn)?;
2963        }
2964        e.freeze_moe_cache();
2965        println!(
2966            "[moe-cache] freeze profile restored: {restaged}/{requested} blocks restaged from {}",
2967            path.display()
2968        );
2969        Ok(true)
2970    }
2971
2972    /// Freeze the heterogeneous CPU/GPU split after the caller's discarded profile warmup.
2973    pub fn freeze_cpu_expert_residency(
2974        &self,
2975        e: &Engine,
2976    ) -> Result<(), Box<dyn std::error::Error>> {
2977        e.freeze_moe_cache();
2978        Ok(())
2979    }
2980
2981    /// FFN activation dispatch: swigluoai (clamped, alpha/limit) when cfg.m3 says so, else the
2982    /// standard SiLU*up. One seam so every FFN site (dense, routed expert, shared expert) follows
2983    /// the model's activation exactly.
2984    pub fn ffn_act(e: &Engine, cfg: &ModelConfig, gate: &CudaSlice<f32>, up: &CudaSlice<f32>,
2985               act: &mut CudaSlice<f32>, n: usize) -> Result<(), Box<dyn std::error::Error>> {
2986        Self::ffn_act_scaled(e, cfg, gate, up, 1.0, 1.0, act, n)
2987    }
2988
2989    /// ffn_act with per-tensor post-matmul macro-scales folded in (gs/us == 1.0 -> identical
2990    /// float ops to ffn_act; used by the ModelOpt NVFP4 expert path where each expert tensor
2991    /// carries a `weight_scale_2`).
2992    #[allow(clippy::too_many_arguments)]
2993    pub(crate) fn ffn_act_scaled(e: &Engine, cfg: &ModelConfig, gate: &CudaSlice<f32>, up: &CudaSlice<f32>,
2994               gs: f32, us: f32, act: &mut CudaSlice<f32>, n: usize)
2995               -> Result<(), Box<dyn std::error::Error>> {
2996        if let Some(m3) = cfg.m3.as_ref() {
2997            return e.swigluoai_mul_scaled(gate, up, gs, us, m3.swiglu_alpha, m3.swiglu_limit, act, n);
2998        }
2999        if gs == 1.0 && us == 1.0 { return e.silu_mul(gate, up, act, n); }
3000        e.silu_mul_scaled(gate, up, gs, us, act, n)
3001    }
3002
3003    /// Routing for the whole batch: returns (sel [T*n_used] expert ids, w [T*n_used] renorm weights),
3004    /// token-major. Default = the Stage-1 host path (dtoh logits, softmax-256, stable DESC top-k,
3005    /// renorm). MEMRA_FUSED_ROUTER = the device kernel (§A) which reproduces the same numerics; we
3006    /// still dtoh the tiny [T,n_used] sel/w buffers (64 B/token vs 1 KB/token) — the host loop
3007    /// indexes HostExps.bytes on the CPU to choose the DMA source (§A.2 output staging).
3008    fn moe_route(e: &Engine, logits: &CudaSlice<f32>, t: usize, n_expert: usize, n_used: usize)
3009                 -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
3010        Self::moe_route_cfg(e, logits, t, n_expert, n_used, None, None, None)
3011    }
3012
3013    /// DeepSeek-V3-class sigmoid routing (MiniMax-M3, Hy3), host oracle. Reference:
3014    /// M3/Hy3 modeling code — scores = sigmoid(logits); selection over scores + expert bias
3015    /// (M3 `e_score_correction_bias` / Hy3 `expert_bias`, both surfaced as `exp_probs_b`);
3016    /// weights = un-biased scores of the selected experts, sum-normalized when `route_norm`,
3017    /// x scaling factor (M3 routed_scaling_factor 2.0 / Hy3 router_scaling_factor 2.826).
3018    /// `sig` = (scaling_factor, route_norm) from `cfg.sigmoid_router()`; softmax archs pass
3019    /// None -> the qwen35moe/OLMoE path below.
3020    fn moe_route_cfg(e: &Engine, logits: &CudaSlice<f32>, t: usize, n_expert: usize, n_used: usize,
3021                     bias: Option<&[f32]>, sig: Option<(f32, bool)>, active: Option<&[bool]>)
3022                 -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
3023        if let Some((sf, route_norm)) = sig {
3024            // sigmoid routing. Host path only for now (fused-router kernel is softmax-top-k).
3025            let lg = e.dtoh(logits)?;
3026            return Self::moe_route_sigmoid_host(
3027                &lg, t, n_expert, n_used, bias, sf, route_norm, active,
3028            );
3029        }
3030        // LAUNCH-STRUCTURE STAGE 1 (2026-07-05): fused router DEFAULT ON (MEMRA_FUSED_ROUTER=0
3031        // rollback) via the single-sync pinned readback — softmax arch only; the M3 sigmoid arm
3032        // above returns before this (host path until a sigmoid fused-router kernel exists).
3033        if active.is_none() && !matches!(std::env::var("MEMRA_FUSED_ROUTER").as_deref(), Ok("0")) {
3034            return e.moe_router_topk_host(logits, t, n_expert, n_used);
3035        }
3036        // Host oracle (the §D bit-identity reference).
3037        let lg = e.dtoh(logits)?;   // [T*n_expert] host
3038        let mut sel = vec![0u32; t * n_used];
3039        let mut w_out = vec![0f32; t * n_used];
3040        for tok in 0..t {
3041            let row = &lg[tok * n_expert..(tok + 1) * n_expert];
3042            // softmax over ALL n_expert (stable: subtract max)
3043            let maxl = row.iter().enumerate()
3044                .filter(|(i, _)| active.is_none_or(|mask| mask[*i]))
3045                .map(|(_, &x)| x).fold(f32::NEG_INFINITY, f32::max);
3046            let mut probs = vec![0f32; n_expert];
3047            let mut den = 0f32;
3048            for i in 0..n_expert {
3049                if active.is_some_and(|mask| !mask[i]) { continue; }
3050                let x = (row[i] - maxl).exp(); probs[i] = x; den += x;
3051            }
3052            for p in probs.iter_mut() { *p /= den; }
3053            // stable DESC sort: prob DESC, ascending-index tiebreak.
3054            let mut idx: Vec<usize> = (0..n_expert)
3055                .filter(|&i| active.is_none_or(|mask| mask[i])).collect();
3056            idx.sort_by(|&a, &b| probs[b].total_cmp(&probs[a]).then(a.cmp(&b)));
3057            let sl = &idx[..n_used];
3058            let mut wv: Vec<f32> = sl.iter().map(|&i| probs[i]).collect();
3059            let mut ws: f32 = wv.iter().sum();
3060            ws = ws.max(6.103515625e-5_f32);  // F16 smallest normal, clamp BEFORE divide
3061            for x in wv.iter_mut() { *x /= ws; }
3062            for j in 0..n_used {
3063                sel[tok * n_used + j] = sl[j] as u32;
3064                w_out[tok * n_used + j] = wv[j];
3065            }
3066        }
3067        Ok((sel, w_out))
3068    }
3069
3070    #[allow(clippy::too_many_arguments)]
3071    fn moe_route_sigmoid_with_input(
3072        e: &Engine,
3073        logits: &CudaSlice<f32>,
3074        input: &CudaSlice<f32>,
3075        t: usize,
3076        n_expert: usize,
3077        n_used: usize,
3078        bias: Option<&[f32]>,
3079        (sf, route_norm): (f32, bool),
3080        active: Option<&[bool]>,
3081    ) -> Result<(Vec<u32>, Vec<f32>, Vec<f32>), Box<dyn std::error::Error>> {
3082        let (lg, input) = e.dtoh_pair(logits, input)?;
3083        let (sel, w) =
3084            Self::moe_route_sigmoid_host(&lg, t, n_expert, n_used, bias, sf, route_norm, active)?;
3085        Ok((sel, w, input))
3086    }
3087
3088    /// Start the prediction-guided prefetch worker (MEMRA_MOE_PREFETCH=depth). Call after
3089    /// residency freeze: the worker filters against a static snapshot of the frozen HBM set.
3090    /// Builds a fully-owned per-layer table (host router copies via one-time DtoH, bias,
3091    /// active mask, prebuilt projection descriptors) so no model reference escapes.
3092    pub fn start_moe_prefetch_predictor(
3093        &self,
3094        e: &Engine,
3095        cfg: &ModelConfig,
3096    ) -> Result<(), Box<dyn std::error::Error>> {
3097        use crate::hybrid::Ffn;
3098        let Some(sig) = cfg.sigmoid_router() else {
3099            return Err("prefetch predictor requires a sigmoid-router arch".into());
3100        };
3101        let resident: std::collections::HashSet<(u16, u8, u16)> = e
3102            .export_moe_residency()
3103            .ok_or("prefetch predictor needs the frozen MoE residency cache")?
3104            .into_iter()
3105            .collect();
3106        let mut layers = Vec::new();
3107        for (index, layer) in self.layers.iter().enumerate() {
3108            let Ffn::Moe(m) = &layer.ffn else { continue };
3109            let crate::model::GpuTensor::Float { data, .. } = &m.gate_inp else { continue };
3110            let router = e.dtoh(data)?;
3111            let n_expert = m.gate_exps.n_expert;
3112            let n_embd = m.gate_exps.in_f;
3113            if router.len() != n_embd * n_expert {
3114                continue;
3115            }
3116            let build = |exps: &crate::model::HostExps| {
3117                (0..n_expert)
3118                    .map(|expert| crate::cpu_experts::predictor_projection(exps, expert))
3119                    .collect::<Vec<_>>()
3120            };
3121            layers.push((index as u16, crate::cpu_experts::PredictLayerInit {
3122                router,
3123                bias: m.exp_probs_b.clone(),
3124                active: m.active_experts.clone(),
3125                n_embd,
3126                n_used: cfg
3127                    .moe
3128                    .as_ref()
3129                    .map(|moe| moe.expert_used_count as usize)
3130                    .ok_or("prefetch predictor requires MoE config")?,
3131                sig,
3132                weights_n_expert: n_expert,
3133                gate: build(&m.gate_exps),
3134                up: build(&m.up_exps),
3135                down: build(&m.down_exps),
3136            }));
3137        }
3138        crate::cpu_experts::start_prefetch_predictor(layers, resident)
3139            .map_err(|error| error.into())
3140    }
3141
3142    /// Crate-visible sigmoid-routing oracle for the prefetch predictor: identical selection
3143    /// math to the runtime router, applied to host-computed lookahead logits.
3144    #[allow(clippy::too_many_arguments)]
3145    pub(crate) fn moe_route_sigmoid_host_public(
3146        logits: &[f32],
3147        t: usize,
3148        n_expert: usize,
3149        n_used: usize,
3150        bias: Option<&[f32]>,
3151        sf: f32,
3152        route_norm: bool,
3153        active: Option<&[bool]>,
3154    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
3155        Self::moe_route_sigmoid_host(logits, t, n_expert, n_used, bias, sf, route_norm, active)
3156    }
3157
3158    #[allow(clippy::too_many_arguments)]
3159    fn moe_route_sigmoid_host(
3160        lg: &[f32],
3161        t: usize,
3162        n_expert: usize,
3163        n_used: usize,
3164        bias: Option<&[f32]>,
3165        sf: f32,
3166        route_norm: bool,
3167        active: Option<&[bool]>,
3168    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
3169        if lg.len() != t * n_expert {
3170            return Err(format!(
3171                "sigmoid router logits length mismatch: got {}, expected {}",
3172                lg.len(),
3173                t * n_expert,
3174            )
3175            .into());
3176        }
3177        let mut sel = vec![0u32; t * n_used];
3178        let mut w_out = vec![0f32; t * n_used];
3179        for tok in 0..t {
3180            let row = &lg[tok * n_expert..(tok + 1) * n_expert];
3181            let scores: Vec<f32> = row.iter().map(|&x| 1.0 / (1.0 + (-x).exp())).collect();
3182            // selection score = sigmoid + bias; weight = plain sigmoid.
3183            let selsc: Vec<f32> = match bias {
3184                Some(b) => scores.iter().zip(b).map(|(s, bb)| s + bb).collect(),
3185                None => scores.clone(),
3186            };
3187            let mut idx: Vec<usize> = (0..n_expert)
3188                .filter(|&i| active.is_none_or(|mask| mask[i]))
3189                .collect();
3190            idx.sort_by(|&a, &b| selsc[b].total_cmp(&selsc[a]).then(a.cmp(&b)));
3191            let sl = &idx[..n_used];
3192            let mut wv: Vec<f32> = sl.iter().map(|&i| scores[i]).collect();
3193            if route_norm {
3194                let ws: f32 = wv.iter().sum::<f32>().max(1e-20);
3195                for x in wv.iter_mut() {
3196                    *x = *x / ws * sf;
3197                }
3198            } else {
3199                for x in wv.iter_mut() {
3200                    *x *= sf;
3201                }
3202            }
3203            for j in 0..n_used {
3204                sel[tok * n_used + j] = sl[j] as u32;
3205                w_out[tok * n_used + j] = wv[j];
3206            }
3207        }
3208        Ok((sel, w_out))
3209    }
3210
3211    /// LAUNCH-STRUCTURE STAGE 3: the ZERO-DtoH fully-resident MoE FFN. Caller guarantees the
3212    /// layer's device pointer row exists (checked under the cache lock). Router top-k runs on
3213    /// device; sel/w are consumed by the `_dev` matvec twins directly; NOTHING crosses PCIe.
3214    /// Same numerics as the fused-router + gdec chain (kernel-level bit-identity, see the
3215    /// MoE PREFILL PAIR-BATCH: host routing (sel/w like the sequential path), then 5 launches
3216    /// TOTAL per layer (quantize z, gate-pairs, up-pairs, silu, act-quantize, down-pairs,
3217    /// scatter) regardless of T or expert count. Bit-identity class: per (pair,row) dot =
3218    /// qmatvec_expert_q8 order; per-token accumulation slot-ordered (scatter kernel).
3219    fn moe_ffn_pairs(e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>, logits: &CudaSlice<f32>,
3220                     t: usize, cfg: &ModelConfig)
3221                     -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3222        let moe = cfg.moe.as_ref().unwrap();
3223        let n_embd = cfg.n_embd as usize;
3224        let n_expert = moe.expert_count as usize;
3225        let n_used = moe.expert_used_count as usize;
3226        let n_ff_exp = moe.expert_ff_length as usize;
3227        let dev = m.dev_exps.as_ref().unwrap();
3228        // WALL-GAP ARC: interleaved gate/up slab strides (see moe_ffn_dev).
3229        let (rbg_d, rbu_d) = if dev.gu_il {
3230            let sxx = m.gate_exps.row_bytes + m.up_exps.row_bytes; (sxx, sxx)
3231        } else { (m.gate_exps.row_bytes, m.up_exps.row_bytes) };
3232
3233        let (sel_all, w_all) = Self::moe_route(e, logits, t, n_expert, n_used)?;
3234        let n_pairs = t * n_used;
3235        // pair arrays: pair p = (token p/n_used, slot p%n_used) — ALREADY slot-ordered per token,
3236        // so the CSR is trivial: tok_pair_off[tok] = tok*n_used, ids identity.
3237        let pair_tok: Vec<i32> = (0..n_pairs).map(|p| (p / n_used) as i32).collect();
3238        let pair_ex:  Vec<i32> = sel_all.iter().map(|&x| x as i32).collect();
3239        let pair_w:   Vec<f32> = w_all.clone();
3240        let tok_off:  Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
3241        let tok_ids:  Vec<i32> = (0..n_pairs as i32).collect();
3242        let pt = e.htod_i32(&pair_tok)?;
3243        let px = e.htod_i32(&pair_ex)?;
3244        let pw = e.htod(&pair_w)?;
3245        let toff = e.htod_i32(&tok_off)?;
3246        let tids = e.htod_i32(&tok_ids)?;
3247
3248        // z quantized ONCE for all tokens; gate/up pair matvecs; silu; act quantize; down; scatter.
3249        // EXPERT-MAJOR CSR (rung 2): pairs grouped by expert -> the kernel reuses each weight
3250        // row across the expert's token group (llama-MMQ's core win). Host grouping is O(pairs).
3251        let mut by_ex: Vec<Vec<i32>> = vec![Vec::new(); n_expert];
3252        for p in 0..n_pairs { by_ex[pair_ex[p] as usize].push(p as i32); }
3253        let mut ex_ids: Vec<i32> = Vec::new();
3254        let mut ex_off: Vec<i32> = vec![0];
3255        let mut ex_pairs: Vec<i32> = Vec::with_capacity(n_pairs);
3256        for (ex, list) in by_ex.iter().enumerate() {
3257            if list.is_empty() { continue; }
3258            ex_ids.push(ex as i32);
3259            ex_pairs.extend_from_slice(list);
3260            ex_off.push(ex_pairs.len() as i32);
3261        }
3262        let n_active = ex_ids.len();
3263        let exi = e.htod_i32(&ex_ids)?;
3264        let exo = e.htod_i32(&ex_off)?;
3265        let exp_d = e.htod_i32(&ex_pairs)?;
3266        let _ = &px;   // pair-major twin keeps it; em path uses CSR
3267
3268        // INT8-MMA EXPERT MMQ (MEMRA_MOE_MMA=1, opt-in): the m16n8k16.s8 tensor-core analog of the
3269        // _dec dp4a kernel (cu/mmq_iq_experts.cu). Same CSR grouping; per-expert matvec runs as a
3270        // 128x128-tile int8 MMA GEMM over the expert's token group. Weight IQ nibbles decode to int8
3271        // at tile-load + per-32 float scale; activation is q8_1_mmq (D4, same quant class as dp4a).
3272        // FP-ORDER differs from dp4a (MMA reduction) — logits SHIFT, gated on argmax/spec/closeness,
3273        // NOT byte-identity (like the W4A8 path). Requires IQ3_S/IQ4_XS + in_f % 256 == 0.
3274        // t >= 16 (GEMM_M-class rule): the MMA tile needs token volume (crossover ~200 tok/expert;
3275        // microbench: dp4a wins at tiny groups). ALSO an exactness requirement — spec verify
3276        // batches (t=2..K+2) must ride the dp4a path whose FP order matches the T=1 decode chain,
3277        // else K=1 self-consistency FAILs (caught 2026-07-06: MMA at T=2 flipped a verify argmax).
3278        // DEFAULT ON (2026-07-06, third flip — this time with the real culprit fixed): the
3279        // "MMA prime breaks spec" failure was the ROUTER's cuBLASLt n-dependence (d994271),
3280        // not MMA's own FP order — both this and the k-quant arms were innocent suspects whose
3281        // margin shifts surfaced the router bug. With the router decode-exact at verify t, the
3282        // full battery is green with MMA on (spec p1/p2/p3 PASS, raw K=1..8 PASS, argmax MATCH,
3283        // pp6257 2862 = 2.1x dec). t>=16 floor still required: verify batches must ride dp4a
3284        // (dispatch parity with the T=1 decode chain). MEMRA_MOE_MMA=0 rollback;
3285        // MEMRA_MOE_MMA_T overrides the floor (bisect seam).
3286        static MMA_T: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
3287        let mma_t = *MMA_T.get_or_init(|| {
3288            std::env::var("MEMRA_MOE_MMA_T").ok().and_then(|v| v.parse().ok()).unwrap_or(16)
3289        });
3290        let use_mma = std::env::var("MEMRA_MOE_MMA").map(|v| v != "0").unwrap_or(true)
3291            && t >= mma_t
3292            && q8_expert_dec_supported(m.gate_exps.qtype) && q8_expert_dec_supported(m.up_exps.qtype)
3293            && q8_expert_dec_supported(m.down_exps.qtype)
3294            && n_embd % 256 == 0 && n_ff_exp % 256 == 0;
3295        // GROUPED f16 LANE admission (MEMRA_MOE_F16G, rounds 46-49): its own door, no longer a
3296        // subset of the MMQ arm — q35's k-quant stragglers (Q6_K/Q4_K down, one Q3_K gate/up
3297        // layer) fail q8_expert_dec_supported but dequant fine to f16, so f16g must be able to
3298        // take a layer the MMQ arm would reject. Same t >= mma_t floor as MMA: decode and
3299        // spec-verify batches must ride the dp4a path whose FP order matches the T=1 chain.
3300        // MODE 2 (sm_120a naked default, lane/f16g-default-rearb 2026-08-02): every layer
3301        // whose three projections pass f16g_proj_ok rides the sk visitor with direct tile
3302        // loaders — with IQ4_XS/IQ3_S direct coverage the sk arm beats the int8-MMA MMQ
3303        // tiles on the IQ-bank models too (q35 board-2048 +33.9%, KAT pp512 +46.7%).
3304        // AUTO-KQUANT (mode 3, MEMRA_MOE_F16G=3, lane/q4k-expert-prefill): admit f16g ONLY
3305        // where the MMA arm can't take the layer's QTYPES — the k-quant expert class whose
3306        // baseline is the per-pair _em fallback (Ornith-35B Q4_K: board-2048 3.14x). Its
3307        // MMA-capable carve-out (IQ3_S/IQ4_XS/Q4_0 banks on MMQ) was priced pre-IQ-direct;
3308        // it survives as the rollback seam. Keyed on qtype capability, NOT use_mma, so
3309        // MEMRA_MOE_MMA=0 stays a pure dp4a rollback seam.
3310        let mma_capable = q8_expert_dec_supported(m.gate_exps.qtype)
3311            && q8_expert_dec_supported(m.up_exps.qtype)
3312            && q8_expert_dec_supported(m.down_exps.qtype)
3313            && n_embd % 256 == 0 && n_ff_exp % 256 == 0;
3314        let f16g_mode = crate::moe_f16g_mode();
3315        let f16g = f16g_mode != 0 && t >= mma_t
3316            && (f16g_mode != 3 || !mma_capable)
3317            && f16g_proj_ok(m.gate_exps.qtype, n_embd)
3318            && f16g_proj_ok(m.up_exps.qtype, n_embd)
3319            && f16g_proj_ok(m.down_exps.qtype, n_ff_exp);
3320        if use_mma || f16g {
3321            // GROUPED f16 LANE (MEMRA_MOE_F16G, rounds 46-49, experimental door): dequant
3322            // the active experts ONCE per projection to f16 and run one grouped f16 GEMM over
3323            // the CSR groups. f16-mirror numeric class — argmax/spec gated before promotion.
3324            // NOTE: the pair-gather kernel needs pair p ordered EXPERT-MAJOR (ex_pairs order);
3325            // the y rows come back in that same CSR order, so gate/up/down all stay pair-major
3326            // in ex_pairs order — but moe_pairs_silu_mul and the scatter consume PAIR-ID order.
3327            // We therefore gather activations per ex_pairs and scatter y back through ex_pairs.
3328            let y_down = if f16g {
3329                // CSR order end-to-end: gather z rows by the pair's TOKEN (pair p's token is
3330                // p / n_used — the trivial CSR above), silu in CSR order (elementwise), one
3331                // permute at the very end back to pair-id order for the scatter.
3332                let csr_tok: Vec<i32> = ex_pairs.iter().map(|&p| p / n_used as i32).collect();
3333                let csr_tok_d = e.htod_i32(&csr_tok)?;
3334                let (z_f16, z_s) = e.moe_f16g_act(z, Some(&csr_tok_d), n_embd, n_pairs)?;
3335                let g_csr = e.moe_f16_grouped(&dev.ptr_row, 0, n_expert, &exi, &ex_off, &exo,
3336                                              &z_f16, &z_s, n_embd, n_ff_exp, n_active, n_pairs,
3337                                              m.gate_exps.qtype, rbg_d)?;
3338                let u_csr = e.moe_f16_grouped(&dev.ptr_row, 1, n_expert, &exi, &ex_off, &exo,
3339                                              &z_f16, &z_s, n_embd, n_ff_exp, n_active, n_pairs,
3340                                              m.up_exps.qtype, rbu_d)?;
3341                let act_csr = e.moe_pairs_silu_mul(&g_csr, &u_csr, n_pairs * n_ff_exp)?;
3342                let (a_f16, a_s) = e.moe_f16g_act(&act_csr, None, n_ff_exp, n_pairs)?;
3343                let d_csr = e.moe_f16_grouped(&dev.ptr_row, 2, n_expert, &exi, &ex_off, &exo,
3344                                              &a_f16, &a_s, n_ff_exp, n_embd, n_active, n_pairs,
3345                                              m.down_exps.qtype, m.down_exps.row_bytes)?;
3346                e.rows_permute(&d_csr, &exp_d, n_pairs, n_embd)?
3347            } else {
3348            // gate/up: activation = z, token-major over t tokens; pair_tok gathers the routed row.
3349            let z_scr = e.mmq_iq_quantize_act(z, n_embd, t)?;
3350            let gate = e.mmq_iq_experts(&dev.ptr_row, 0, n_expert, &exi, &exo, &exp_d, &pt, &z_scr,
3351                                        n_embd, n_ff_exp, n_active, n_pairs, t,
3352                                        m.gate_exps.qtype, rbg_d)?;
3353            let up = e.mmq_iq_experts(&dev.ptr_row, 1, n_expert, &exi, &exo, &exp_d, &pt, &z_scr,
3354                                      n_embd, n_ff_exp, n_active, n_pairs, t,
3355                                      m.up_exps.qtype, rbu_d)?;
3356            // down: activation = silu(gate)*up, pair-major [n_pairs, n_ff_exp]; pair_tok =
3357            // identity. FUSED ACT-EPILOGUE (default on): one launch computes the activation in
3358            // registers and writes ONLY the quantized scratch — the two-pass chain
3359            // (moe_pairs_silu_mul writes act f32, mmq_iq_quantize_act re-reads it) is the
3360            // MEMRA_MOE_FUSE_ACTQ=0 rollback. Scratch bytes are BYTE-IDENTICAL (kernel-check).
3361            let a_scr = if crate::moe_fuse_actq_on() {
3362                e.mmq_iq_fused_act_quant(&gate, &up, n_ff_exp, n_pairs, 0)?
3363            } else {
3364                let act = e.moe_pairs_silu_mul(&gate, &up, n_pairs * n_ff_exp)?;
3365                e.mmq_iq_quantize_act(&act, n_ff_exp, n_pairs)?
3366            };
3367            let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
3368            let pself = e.htod_i32(&pair_self)?;
3369            e.mmq_iq_experts(&dev.ptr_row, 2, n_expert, &exi, &exo, &exp_d, &pself, &a_scr,
3370                             n_ff_exp, n_embd, n_active, n_pairs, n_pairs,
3371                             m.down_exps.qtype, m.down_exps.row_bytes)?
3372            };
3373            let mut moe_out = e.uninit(t * n_embd)?;
3374            e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
3375            if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
3376                (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
3377            {
3378                let n_ff_sh = gate_shexp.out_features();
3379                let sg_gate = e.matmul(gate_shexp, z, t)?;
3380                let sg_up = e.matmul(up_shexp, z, t)?;
3381                let mut sa = e.uninit(t * n_ff_sh)?;
3382                Self::ffn_act(e, cfg, &sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
3383                let sh = e.matmul(down_shexp, &sa, t)?;
3384                // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
3385                // SERVE ISOLATION (lane/concat-prime-exact): the fused sigmoid-dot is the
3386                // m-INVARIANT form — see the sequential arm's note. This is the PAIRS arm,
3387                // i.e. the one real prefill actually takes on a resident-expert MoE model,
3388                // so the concat-prime isolation fix has to land here as well.
3389                let g = match &m.gate_inp_shexp {
3390                    Some(gate_inp_shexp) if crate::router_prefill_exact_on() => {
3391                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
3392                    }
3393                    Some(gate_inp_shexp) => {
3394                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
3395                        let mut g = e.uninit(t)?;
3396                        e.sigmoid(&gs, &mut g, t)?;
3397                        g
3398                    }
3399                    None => e.htod(&vec![1.0f32; t])?,
3400                };
3401                e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
3402            }
3403            return Ok(moe_out);
3404        }
3405
3406        // DECODE-ONCE MMQ (rung 3, MEMRA_MOE_DEC=1 default-on): dequant each weight group once per
3407        // (row,group) then dp4a across the expert's tokens. _em re-decoded per token (NEUTRAL).
3408        let dec = std::env::var("MEMRA_MOE_DEC").map(|v| v != "0").unwrap_or(true);
3409        let matvec = |proj, exi: &_, exo: &_, exp_d: &_, pt: &_, aq: &_, ad: &_,
3410                      inf, outf, qtype, rb| -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3411            // _dec's decode-once extractors are IQ-only; k-quant expert layers take the _em dot path.
3412            let dec = dec && q8_expert_dec_supported(qtype);
3413            if dec { e.moe_pairs_matvec_q8_dec(&dev.ptr_row, proj, exi, exo, exp_d, pt, aq, ad,
3414                                               inf, outf, n_expert, n_active, n_pairs, qtype, rb) }
3415            else   { e.moe_pairs_matvec_q8_em (&dev.ptr_row, proj, exi, exo, exp_d, pt, aq, ad,
3416                                               inf, outf, n_expert, n_active, n_pairs, qtype, rb) }
3417        };
3418        let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
3419        let gate = matvec(0, &exi, &exo, &exp_d, &pt, &zq, &zd,
3420                          n_embd, n_ff_exp, m.gate_exps.qtype, rbg_d)?;
3421        let up = matvec(1, &exi, &exo, &exp_d, &pt, &zq, &zd,
3422                        n_embd, n_ff_exp, m.up_exps.qtype, rbu_d)?;
3423        let act = e.moe_pairs_silu_mul(&gate, &up, n_pairs * n_ff_exp)?;
3424        let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
3425        // down consumes PAIR-major activation rows: pair_tok = identity.
3426        let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
3427        let pself = e.htod_i32(&pair_self)?;
3428        let y_down = matvec(2, &exi, &exo, &exp_d, &pself, &aq2, &ad2,
3429                            n_ff_exp, n_embd, m.down_exps.qtype, m.down_exps.row_bytes)?;
3430        let mut moe_out = e.uninit(t * n_embd)?;   // scatter fully overwrites per (token,col)
3431        e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
3432
3433        // SHARED EXPERT epilogue — same as the other paths.
3434        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
3435        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
3436        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
3437            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
3438        {
3439            let n_ff_sh = gate_shexp.out_features();
3440            let sg_gate = e.matmul(gate_shexp, z, t)?;
3441            let sg_up = e.matmul(up_shexp, z, t)?;
3442            let mut sa = e.uninit(t * n_ff_sh)?;
3443            e.silu_mul(&sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
3444            let sh = e.matmul(down_shexp, &sa, t)?;
3445            // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
3446            // m-INVARIANT fused sigmoid-dot under router_prefill_exact (serve isolation,
3447            // lane/concat-prime-exact) — every shexp-gate arm shares the same form so a
3448            // dispatch choice cannot change bits.
3449            let g = match &m.gate_inp_shexp {
3450                Some(gate_inp_shexp) if crate::router_prefill_exact_on() => {
3451                    e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
3452                }
3453                Some(gate_inp_shexp) => {
3454                    let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
3455                    let mut g = e.uninit(t)?;
3456                    e.sigmoid(&gs, &mut g, t)?;
3457                    g
3458                }
3459                None => e.htod(&vec![1.0f32; t])?,
3460            };
3461            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
3462        }
3463        Ok(moe_out)
3464    }
3465
3466    /// kernel headers); the shared-expert epilogue is byte-identical to moe_ffn_sequential's.
3467    #[allow(clippy::too_many_arguments)]
3468    #[allow(clippy::too_many_arguments)]
3469    fn moe_ffn_dev(e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>,
3470                   zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>, logits: &CudaSlice<f32>,
3471                   t: usize, cfg: &ModelConfig, il: u16, max_block: usize)
3472                   -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3473        let moe = cfg.moe.as_ref().unwrap();
3474        let n_embd = cfg.n_embd as usize;
3475        let n_expert = moe.expert_count as usize;
3476        let n_used = moe.expert_used_count as usize;
3477        let n_ff_exp = moe.expert_ff_length as usize;
3478
3479        // device top-k: sel [t, n_used] i32, w [t, n_used] f32 — stays on device.
3480        let (sel_d, mut w_d) = e.moe_router_topk(logits, t, n_expert, n_used)?;
3481        // Down-projection macro fold (compressed-tensors NVFP4 artifacts): one tiny launch,
3482        // skipped entirely for macro-free experts (every k-quant GGUF).
3483        if m.has_macros {
3484            e.moe_w_scale_by_expert(&mut w_d, &sel_d, &m.dev_macros, n_expert, t * n_used)?;
3485        }
3486
3487        // moe_out rows are FULLY overwritten by moe_down8_fma_dev — uninit (stage-2 rule).
3488        let mut moe_out = e.uninit(t * n_embd)?;
3489
3490        // RESIDENT-EXPERTS arm: the pointer row comes from the load-time slab (no cache, no
3491        // lock). Same kernels/loop as the SLRU arm below — only the row's provenance differs.
3492        if let Some(dev) = m.dev_exps.as_ref() {
3493            // WALL-GAP ARC: interleaved gate/up slab (MEMRA_MOE_GU_IL) -> both projections use
3494            // the combined stride; up's base is offset in the ptr table. Down unchanged.
3495            let (rbg_d, rbu_d) = if dev.gu_il {
3496                let sxx = m.gate_exps.row_bytes + m.up_exps.row_bytes; (sxx, sxx)
3497            } else { (m.gate_exps.row_bytes, m.up_exps.row_bytes) };
3498            let q8 = moe_q8_enabled()
3499                && q8_expert_supported(m.gate_exps.qtype) && q8_expert_supported(m.up_exps.qtype)
3500                && q8_expert_supported(m.down_exps.qtype);
3501            // SMALL-M ROWS ARM (MEMRA_SPEC_M2, lane/spec-m2): batch the verify token loop —
3502            // ONE batched z-quantize + ONE gate_up rows launch + ONE act quantize + ONE down
3503            // rows launch (4 launches/layer, was 4t). BIT-IDENTICAL per token to the serial
3504            // loop below (rows twins = the _v/w8h2v per-token programs on a grid.z token axis;
3505            // quantize_q8_1 is per-32-block row-independent). Gated to the AUTO kernel modes —
3506            // a custom MEMRA_MOE_DEVQ8_GU/DOWN diagnostic run keeps the serial loop so the
3507            // dispatched kernel stays exactly the env-selected one — and to the w8h2v shape
3508            // (n_ff_exp==512, n_used<=8), the same contract the AUTO down dispatch keys on.
3509            let rows_arm = q8 && t > 1 && crate::spec::spec_m2()
3510                && n_ff_exp == 512 && n_used <= 8
3511                && std::env::var("MEMRA_MOE_DEVQ8_GU").map(|v| v.is_empty() || v == "v").unwrap_or(true)
3512                && std::env::var("MEMRA_MOE_DEVQ8_DOWN").map(|v| v.is_empty() || v == "w8h2v").unwrap_or(true);
3513            // CSR EXPERT-DEDUP gate_up (verify-cost target #1, DEFAULT ON 2026-07-10): the
3514            // owner-scan kernel serves every (token, slot) pair of an expert from ONE block,
3515            // deduping the 38-40% duplicated weight-stream+decode the overlap probe measured
3516            // (gate_up 55.0 -> 39.7us/launch; +1.3-2.1% spec e2e p2, +0.6-1.7% p3, all K).
3517            // Bit-identical to the _rows twins (explicit-intrinsic accumulate — the ULP/fmad
3518            // lesson; =2 byte-compare verified zero diffs). down stays on _rows (CSR down
3519            // measured 23.5 -> 37.5us: 16-group rows can't amortize the serial pair loop).
3520            // MEMRA_MOE_CSR=0 rollback; =2 runs BOTH paths and byte-compares (debug).
3521            let csr_mode = std::env::var("MEMRA_MOE_CSR").ok()
3522                .and_then(|v| v.parse::<i32>().ok()).unwrap_or(1);
3523            let csr_qt = |qt: i32| qt == crate::QT_IQ4_XS || qt == crate::QT_IQ3_S;
3524            let csr_arm = rows_arm && csr_mode > 0 && t <= 10
3525                && csr_qt(m.gate_exps.qtype) && csr_qt(m.up_exps.qtype)
3526                && csr_qt(m.down_exps.qtype);
3527            if csr_arm {
3528                if csr_mode == 2 {
3529                    static ENGAGED: std::sync::Once = std::sync::Once::new();
3530                    ENGAGED.call_once(|| eprintln!("[memra] moe CSR byte-compare mode ON (t={t})"));
3531                }
3532                let n_pairs = t * n_used;
3533                let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
3534                let act = e.moe_gate_up_silu8_dev_q8_csr(&dev.ptr_row, &sel_d, &zq, &zd, n_pairs,
3535                                                         n_embd, n_ff_exp, n_used, n_expert,
3536                                                         m.gate_exps.qtype, m.up_exps.qtype,
3537                                                         rbg_d, rbu_d)?;
3538                let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
3539                // down stays on the _rows twin — BOTH CSR down variants measured negative
3540                // (v1 serial pairs 23.5->37.5us; v2 warp-parallel+SMEM -14% e2e, K=8 -37%):
3541                // 16-group rows have too little decode to amortize any dedup structure.
3542                e.moe_down8_fma_dev_q8_rows(&dev.ptr_row, &sel_d, &w_d, &aq2, &ad2, &mut moe_out,
3543                                            t, n_ff_exp, n_embd, n_used, n_expert,
3544                                            m.down_exps.qtype, m.down_exps.row_bytes)?;
3545                if csr_mode == 2 {
3546                    // DEBUG BYTE-COMPARE: run the _rows twins on the same inputs, diff bits.
3547                    let act_r = e.moe_gate_up_silu8_dev_q8_rows(&dev.ptr_row, &sel_d, &zq, &zd, t,
3548                                                                n_embd, n_ff_exp, n_used, n_expert,
3549                                                                m.gate_exps.qtype, m.up_exps.qtype,
3550                                                                rbg_d, rbu_d, &m.dev_macros)?;
3551                    let mut out_r = e.uninit(t * n_embd)?;
3552                    let (aq2r, ad2r) = e.quantize_q8_1(&act_r, n_pairs, n_ff_exp)?;
3553                    e.moe_down8_fma_dev_q8_rows(&dev.ptr_row, &sel_d, &w_d, &aq2r, &ad2r, &mut out_r,
3554                                                t, n_ff_exp, n_embd, n_used, n_expert,
3555                                                m.down_exps.qtype, m.down_exps.row_bytes)?;
3556                    let (a1, a2) = (e.dtoh(&act)?, e.dtoh(&act_r)?);
3557                    let (o1, o2) = (e.dtoh(&moe_out)?, e.dtoh(&out_r)?);
3558                    let ba = a1.iter().zip(&a2).filter(|(x, y)| x.to_bits() != y.to_bits()).count();
3559                    let bo = o1.iter().zip(&o2).filter(|(x, y)| x.to_bits() != y.to_bits()).count();
3560                    if ba + bo > 0 {
3561                        eprintln!("[csr-check] il={il} t={t} ACT diffs={ba}/{} OUT diffs={bo}/{}",
3562                                  a1.len(), o1.len());
3563                        // First 4 differing ACT elements: (pair, o, csr, rows) + that pair's expert
3564                        let sel_h = e.dtoh_i32(&sel_d)?;
3565                        let mut shown = 0;
3566                        for (i, (x, y)) in a1.iter().zip(&a2).enumerate() {
3567                            if x.to_bits() != y.to_bits() && shown < 4 {
3568                                let (p, o) = (i / n_ff_exp, i % n_ff_exp);
3569                                let ex = sel_h[p];
3570                                let npx = sel_h.iter().filter(|&&v| v == ex).count();
3571                                eprintln!("  ACT p={p} ex={ex} np={npx} o={o} csr={x:e} rows={y:e}");
3572                                shown += 1;
3573                            }
3574                        }
3575                        std::process::exit(3);
3576                    }
3577                }
3578            } else if rows_arm {
3579                // TEMP PROBE (MEMRA_MOE_OVERLAP=1): cross-token expert-activation overlap at
3580                // verify — sizes the CSR dedup win (unique experts vs t*n_used pairs).
3581                if std::env::var("MEMRA_MOE_OVERLAP").as_deref() == Ok("1") {
3582                    use std::sync::atomic::{AtomicU64, Ordering};
3583                    static PAIRS: AtomicU64 = AtomicU64::new(0);
3584                    static UNIQ: AtomicU64 = AtomicU64::new(0);
3585                    static CALLS: AtomicU64 = AtomicU64::new(0);
3586                    let sel_h = e.dtoh_i32(&sel_d)?;
3587                    let mut u: Vec<i32> = sel_h.clone(); u.sort_unstable(); u.dedup();
3588                    PAIRS.fetch_add(sel_h.len() as u64, Ordering::Relaxed);
3589                    UNIQ.fetch_add(u.len() as u64, Ordering::Relaxed);
3590                    let c = CALLS.fetch_add(1, Ordering::Relaxed) + 1;
3591                    if c % 480 == 0 {
3592                        let p = PAIRS.load(Ordering::Relaxed); let q = UNIQ.load(Ordering::Relaxed);
3593                        eprintln!("[overlap] calls={c} pairs={p} unique={q} ratio={:.3} (t={t})",
3594                                  q as f64 / p as f64);
3595                    }
3596                }
3597                let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
3598                let act = e.moe_gate_up_silu8_dev_q8_rows(&dev.ptr_row, &sel_d, &zq, &zd, t,
3599                                                          n_embd, n_ff_exp, n_used, n_expert,
3600                                                          m.gate_exps.qtype, m.up_exps.qtype,
3601                                                          rbg_d, rbu_d, &m.dev_macros)?;
3602                let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
3603                e.moe_down8_fma_dev_q8_rows(&dev.ptr_row, &sel_d, &w_d, &aq2, &ad2, &mut moe_out,
3604                                            t, n_ff_exp, n_embd, n_used, n_expert,
3605                                            m.down_exps.qtype, m.down_exps.row_bytes)?;
3606            } else {
3607            for tok in 0..t {
3608                let zt = z.slice(tok * n_embd..(tok + 1) * n_embd);
3609                let selt = sel_d.slice(tok * n_used..(tok + 1) * n_used);
3610                let wt = w_d.slice(tok * n_used..(tok + 1) * n_used);
3611                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
3612                if q8 {
3613                    let (zq, zd) = match (t, zq8) {
3614                        (1, Some((q, d))) => (q.clone(), d.clone()),
3615                        _ => e.quantize_q8_1_view(&zt, 1, n_embd)?,
3616                    };
3617                    let act = e.moe_gate_up_silu8_dev_q8(&dev.ptr_row, &selt, &zq, &zd,
3618                                                         n_embd, n_ff_exp, n_used, n_expert,
3619                                                         m.gate_exps.qtype, m.up_exps.qtype,
3620                                                         rbg_d, rbu_d, &m.dev_macros)?;
3621                    let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
3622                    e.moe_down8_fma_dev_q8(&dev.ptr_row, &selt, &wt, &aq2, &ad2, &mut dst,
3623                                           n_ff_exp, n_embd, n_used, n_expert,
3624                                           m.down_exps.qtype, m.down_exps.row_bytes)?;
3625                } else {
3626                    let act = e.moe_gate_up_silu8_dev(&dev.ptr_row, &selt, &zt, n_embd, n_ff_exp,
3627                                                      n_used, n_expert,
3628                                                      m.gate_exps.qtype, m.up_exps.qtype,
3629                                                      rbg_d, rbu_d, &m.dev_macros)?;
3630                    e.moe_down8_fma_dev(&dev.ptr_row, &selt, &wt, &act, &mut dst,
3631                                        n_ff_exp, n_embd, n_used, n_expert,
3632                                        m.down_exps.qtype, m.down_exps.row_bytes)?;
3633                }
3634            }
3635            }
3636        } else {
3637        // Launch under the cache lock: the row borrow lives as long as the closure, and the
3638        // lock covers only launch ISSUE (µs), same policy as moe_cached_gemm.
3639        // Q8 ARM PARITY (2026-07-06): the SLRU arm ran the f32-dequant _dev kernels only —
3640        // 80us/launch vs the q8 twins' 15us on the SAME shapes (fixed-build profile: 228
3641        // f32 launches = 36ms of the 64-tok window). Same q8 gate + kernels as the resident
3642        // arm above; MEMRA_MOE_Q8=0 restores the byte-identical f32 path.
3643        let q8 = moe_q8_enabled()
3644            && q8_expert_supported(m.gate_exps.qtype) && q8_expert_supported(m.up_exps.qtype)
3645            && q8_expert_supported(m.down_exps.qtype);
3646        e.with_moe_cache(max_block, |c, eng| {
3647            let row = c.layer_dev_row(il, n_expert, eng)?
3648                .ok_or("moe_ffn_dev: layer row vanished under the lock")?;
3649            for tok in 0..t {
3650                let zt = z.slice(tok * n_embd..(tok + 1) * n_embd);
3651                let selt = sel_d.slice(tok * n_used..(tok + 1) * n_used);
3652                let wt = w_d.slice(tok * n_used..(tok + 1) * n_used);
3653                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
3654                if q8 {
3655                    let (zq, zd) = match (t, zq8) {
3656                        (1, Some((q, d))) => (q.clone(), d.clone()),
3657                        _ => eng.quantize_q8_1_view(&zt, 1, n_embd)?,
3658                    };
3659                    let act = eng.moe_gate_up_silu8_dev_q8(row, &selt, &zq, &zd,
3660                                                           n_embd, n_ff_exp, n_used, n_expert,
3661                                                           m.gate_exps.qtype, m.up_exps.qtype,
3662                                                           m.gate_exps.row_bytes, m.up_exps.row_bytes,
3663                                                           &m.dev_macros)?;
3664                    let (aq2, ad2) = eng.quantize_q8_1(&act, n_used, n_ff_exp)?;
3665                    eng.moe_down8_fma_dev_q8(row, &selt, &wt, &aq2, &ad2, &mut dst,
3666                                             n_ff_exp, n_embd, n_used, n_expert,
3667                                             m.down_exps.qtype, m.down_exps.row_bytes)?;
3668                } else {
3669                    let act = eng.moe_gate_up_silu8_dev(row, &selt, &zt, n_embd, n_ff_exp,
3670                                                        n_used, n_expert,
3671                                                        m.gate_exps.qtype, m.up_exps.qtype,
3672                                                        m.gate_exps.row_bytes, m.up_exps.row_bytes,
3673                                                        &m.dev_macros)?;
3674                    eng.moe_down8_fma_dev(row, &selt, &wt, &act, &mut dst,
3675                                          n_ff_exp, n_embd, n_used, n_expert,
3676                                          m.down_exps.qtype, m.down_exps.row_bytes)?;
3677                }
3678            }
3679            // instrumentation parity with the host paths (3 blocks/expert-slot, all hits).
3680            c.hits += (t * 3 * n_used) as u64;
3681            Ok(())
3682        })?;
3683        }
3684
3685        // SHARED EXPERT epilogue — byte-identical to moe_ffn_sequential step 3 (incl. its Q8
3686        // TRUNK-FUSION arm: fused2 is bit-identical to the two matmul calls per (tensor,row)).
3687        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
3688        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
3689        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
3690            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
3691        {
3692            let n_ff_sh = gate_shexp.out_features();
3693            // verify-t (2..15) decode-exact arm: this fn now serves the spec verify batches
3694            // (pairs gate moved to t>=PRIME_MIN_T), so the shexp chain must match t==1 per col.
3695            let verify_t = t > 1 && t < PRIME_MIN_T;
3696            let (sg_gate, sg_up) = if t == 1 {
3697                match e.matmul_q8_fused2_x(gate_shexp, up_shexp, z)? {
3698                    Some(pair) => pair,
3699                    None => (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?),
3700                }
3701            } else if verify_t {
3702                // VERIFY-TIER TRUNK FUSION (MEMRA_SPEC_FUSED_T, t=2-4): the shexp gate+up pair
3703                // rides one shared quantize + one fused2 batched launch instead of two
3704                // decode-exact calls. Bit-identical per (tensor,token,row) — see spec_fused_t.
3705                let mut fused = None;
3706                if crate::spec::spec_fused_t() && (2..=4).contains(&t)
3707                    && e.uses_q8_1_fast(gate_shexp) && e.uses_q8_1_fast(up_shexp) {
3708                    let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
3709                    fused = e.matmul_q8_fused2_t(gate_shexp, up_shexp, &zq, &zd, t)?;
3710                }
3711                match fused {
3712                    Some(pair) => pair,
3713                    None => (e.matmul_decode_exact(gate_shexp, z, t)?,
3714                             e.matmul_decode_exact(up_shexp, z, t)?),
3715                }
3716            } else {
3717                (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?)
3718            };
3719            let mut sa = e.uninit(t * n_ff_sh)?;  // silu_mul fully overwrites
3720            e.silu_mul(&sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
3721            let sh = if verify_t { e.matmul_decode_exact(down_shexp, &sa, t)? }
3722                     else { e.matmul(down_shexp, &sa, t)? };
3723            // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
3724            // Same fused sigmoid-dot as moe_ffn_sequential step 3 (byte-identity contract
3725            // between the two arms; prefill keeps the batched cuBLASLt linear).
3726            let g = match &m.gate_inp_shexp {
3727                Some(gate_inp_shexp) => {
3728                    // router_prefill_exact_on(): the fused sigmoid-dot is m-INVARIANT and
3729                    // serves EVERY t (serve isolation, lane/concat-prime-exact).
3730                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
3731                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
3732                    } else {
3733                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
3734                        let mut g = e.uninit(t)?;
3735                        e.sigmoid(&gs, &mut g, t)?;
3736                        g
3737                    }
3738                }
3739                None => e.htod(&vec![1.0f32; t])?,
3740            };
3741            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
3742        }
3743
3744        Ok(moe_out)
3745    }
3746
3747    /// STAGE-2 GROUPED DECODE (2026-07-04): run ONE token's whole routed-expert FFN in TWO
3748    /// launches when every one of its 3*n_used blocks is ALREADY cache-resident. Returns
3749    /// Ok(true) if the grouped path ran (caller skips the sequential loop for this token);
3750    /// Ok(false) on ANY miss (caller falls through — the sequential loop stages/admits as
3751    /// before, so the NEXT occurrence takes the grouped path). Pointer safety: cache slots are
3752    /// fixed-address for the engine's lifetime and the pure-HIT path performs no admission, so
3753    /// the collected raw pointers cannot move between collection and launch (single-threaded
3754    /// decode; the lock is held only for collection, launches are stream-ordered after any
3755    /// prior same-stream staging writes).
3756    #[allow(clippy::too_many_arguments)]
3757    /// q8 twin of moe_gdec_token (dp4a arc): same residency check + 2-launch shape; the fused
3758    /// kernels consume the pre-quantized z-row and re-quantize act per slot batch.
3759    #[allow(clippy::too_many_arguments)]
3760    fn moe_gdec_token_q8(e: &Engine, m: &MoeWeights, il: u16, max_block: usize,
3761                      zq: &CudaSlice<i8>, zd: &CudaSlice<f32>, sel: &[u32], w: &[f32],
3762                      moe_out: &mut CudaSlice<f32>, tok: usize,
3763                      n_embd: usize, n_ff_exp: usize, n_used: usize)
3764                      -> Result<bool, Box<dyn std::error::Error>> {
3765        use crate::moe_cache::{BlockId, PROJ_GATE, PROJ_UP, PROJ_DOWN};
3766        use cudarc::driver::DevicePtr;
3767        let ptrs = e.with_moe_cache(max_block, |c, eng| {
3768            let mut g = [0u64; 8];
3769            let mut u = [0u64; 8];
3770            let mut d = [0u64; 8];
3771            for (j, &ex) in sel.iter().enumerate() {
3772                let ex = ex as u16;
3773                let (Some(sg), Some(su), Some(sd)) = (c.resident(BlockId::new(il, PROJ_GATE, ex)),
3774                                                      c.resident(BlockId::new(il, PROJ_UP,   ex)),
3775                                                      c.resident(BlockId::new(il, PROJ_DOWN, ex)))
3776                else { return Ok(None); };
3777                let __s = eng.stream();
3778                let (pg, _e0) = c.slot(sg).device_ptr(&__s);
3779                let (pu, _e1) = c.slot(su).device_ptr(&__s);
3780                let (pd, _e2) = c.slot(sd).device_ptr(&__s);
3781                g[j] = pg as u64; u[j] = pu as u64; d[j] = pd as u64;
3782            }
3783            if cpu_expert_profile_admit_enabled() && !c.is_frozen() {
3784                for &ex in sel {
3785                    let ex = ex as u16;
3786                    for proj in [PROJ_GATE, PROJ_UP, PROJ_DOWN] {
3787                        c.note_profile_hit(BlockId::new(il, proj, ex));
3788                    }
3789                }
3790            }
3791            c.hits += (3 * n_used) as u64;
3792            Ok(Some((g, u, d)))
3793        })?;
3794        let Some((g, u, d)) = ptrs else { return Ok(false) };
3795        let mut wv = [0f32; 8];
3796        wv[..n_used].copy_from_slice(w);
3797        let act = e.moe_gate_up_silu8_q8(crate::WPtr8(g), crate::WPtr8(u), zq, zd,
3798                                         n_embd, n_ff_exp, n_used,
3799                                         m.gate_exps.qtype, m.up_exps.qtype,
3800                                         m.gate_exps.row_bytes, m.up_exps.row_bytes)?;
3801        // per-slot act quantize: [n_used, n_ff] rows in one quantize launch.
3802        let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
3803        let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
3804        e.moe_down8_fma_q8(crate::WPtr8(d), crate::F32x8(wv), &aq2, &ad2, &mut dst,
3805                           n_ff_exp, n_embd, n_used,
3806                           m.down_exps.qtype, m.down_exps.row_bytes)?;
3807        Ok(true)
3808    }
3809
3810    fn moe_gdec_token(e: &Engine, m: &MoeWeights, il: u16, max_block: usize,
3811                      zt: &cudarc::driver::CudaView<f32>, sel: &[u32], w: &[f32],
3812                      moe_out: &mut CudaSlice<f32>, tok: usize,
3813                      n_embd: usize, n_ff_exp: usize, n_used: usize)
3814                      -> Result<bool, Box<dyn std::error::Error>> {
3815        use crate::moe_cache::{BlockId, PROJ_GATE, PROJ_UP, PROJ_DOWN};
3816        use cudarc::driver::DevicePtr;
3817        // One lock hold: residency-check all 3*n_used blocks, collect raw slot pointers.
3818        let ptrs = e.with_moe_cache(max_block, |c, eng| {
3819            let mut g = [0u64; 8];
3820            let mut u = [0u64; 8];
3821            let mut d = [0u64; 8];
3822            for (j, &ex) in sel.iter().enumerate() {
3823                let ex = ex as u16;
3824                let (Some(sg), Some(su), Some(sd)) = (c.resident(BlockId::new(il, PROJ_GATE, ex)),
3825                                                      c.resident(BlockId::new(il, PROJ_UP,   ex)),
3826                                                      c.resident(BlockId::new(il, PROJ_DOWN, ex)))
3827                else { return Ok(None); };
3828                let __s = eng.stream();
3829                let (pg, _e0) = c.slot(sg).device_ptr(&__s);
3830                let (pu, _e1) = c.slot(su).device_ptr(&__s);
3831                let (pd, _e2) = c.slot(sd).device_ptr(&__s);
3832                g[j] = pg as u64; u[j] = pu as u64; d[j] = pd as u64;
3833            }
3834            if cpu_expert_profile_admit_enabled() && !c.is_frozen() {
3835                for &ex in sel {
3836                    let ex = ex as u16;
3837                    for proj in [PROJ_GATE, PROJ_UP, PROJ_DOWN] {
3838                        c.note_profile_hit(BlockId::new(il, proj, ex));
3839                    }
3840                }
3841            }
3842            c.hits += (3 * n_used) as u64; // instrumentation parity with dispatch()
3843            Ok(Some((g, u, d)))
3844        })?;
3845        let Some((g, u, d)) = ptrs else { return Ok(false) };
3846        let mut wv = [0f32; 8];
3847        wv[..n_used].copy_from_slice(w);
3848        // 2 launches: (gate+up+silu) x8, then (down + slot-ordered FMA accumulate) x8.
3849        let act = e.moe_gate_up_silu8(crate::WPtr8(g), crate::WPtr8(u), zt,
3850                                      n_embd, n_ff_exp, n_used,
3851                                      m.gate_exps.qtype, m.up_exps.qtype,
3852                                      m.gate_exps.row_bytes, m.up_exps.row_bytes)?;
3853        let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
3854        e.moe_down8_fma_into(crate::WPtr8(d), crate::F32x8(wv), &act, &mut dst,
3855                             n_ff_exp, n_embd, n_used,
3856                             m.down_exps.qtype, m.down_exps.row_bytes)?;
3857        Ok(true)
3858    }
3859
3860    /// EDGE-1 §B.3: dispatch one expert projection through the SLRU cache, then run the SAME
3861    /// `qmatvec_view` from whichever slot it landed in (resident HIT or staged MISS). `x` is the
3862    /// sliced activation row. `proj` selects the gate/up/down HostExps tensor. Returns y = W_expert @ x.
3863    /// q8 twin of moe_cached_gemm: same dispatch/slot mechanics, dp4a expert kernel.
3864    fn moe_cached_gemm_q8(e: &Engine, il: u16, proj: u8, ex: usize, m: &MoeWeights,
3865                          max_block: usize, aq: &CudaSlice<i8>, ad: &CudaSlice<f32>)
3866                          -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3867        use crate::moe_cache::{BlockId, DispatchSlot, PROJ_GATE, PROJ_UP};
3868        let exps = match proj { PROJ_GATE => &m.gate_exps, PROJ_UP => &m.up_exps, _ => &m.down_exps };
3869        let layout = exps.expert_layout(ex);
3870        let id = BlockId::new(il, proj, ex as u16);
3871        let source = exps.expert_source(ex);
3872        e.with_moe_cache(max_block, |c, eng| {
3873            let slot = c.dispatch_source(id, source, eng)?;
3874            let DispatchSlot::Resident(sl) = slot;
3875            let buf = c.slot(sl);
3876            eng.qmatvec_expert_q8(buf, 0..layout.len, aq, ad, 1, exps.in_f, exps.out_f,
3877                                  layout.qtype, layout.row_bytes)
3878        })
3879    }
3880
3881    fn moe_cached_gemm(e: &Engine, il: u16, proj: u8, ex: usize, m: &MoeWeights,
3882                       max_block: usize, x: &cudarc::driver::CudaView<f32>)
3883                       -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3884        use crate::moe_cache::{BlockId, DispatchSlot, PROJ_GATE, PROJ_UP};
3885        let exps = match proj { PROJ_GATE => &m.gate_exps, PROJ_UP => &m.up_exps, _ => &m.down_exps };
3886        let layout = exps.expert_layout(ex);
3887        let id = BlockId::new(il, proj, ex as u16);
3888        let source = exps.expert_source(ex);
3889        // dispatch under the lock (lookup/admit/memcpy-issue), then resolve the slot and GEMM.
3890        e.with_moe_cache(max_block, |c, eng| {
3891            let slot = c.dispatch_source(id, source, eng)?;
3892            // resolve the device buffer for this slot; the GEMM is enqueued on the compute stream
3893            // (the same stream the memcpy was issued on, so ordering holds without extra sync).
3894            let DispatchSlot::Resident(sl) = slot;
3895            let buf = c.slot(sl);
3896            eng.qmatvec_view(buf, 0..layout.len, x, 1, exps.in_f, exps.out_f,
3897                             layout.qtype, layout.row_bytes)
3898        })
3899    }
3900
3901    /// Populate the warmup cache for an expert whose current-token output intentionally ran on
3902    /// CPU. No GEMM is launched and callers invoke this only after the CPU result has completed,
3903    /// so the current forward's backend assignment and output remain unchanged.
3904    fn moe_profile_admit_expert(
3905        e: &Engine,
3906        il: u16,
3907        ex: usize,
3908        m: &MoeWeights,
3909        max_block: usize,
3910    ) -> Result<(), Box<dyn std::error::Error>> {
3911        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
3912        e.with_moe_cache(max_block, |cache, eng| {
3913            for (proj, exps) in [
3914                (PROJ_GATE, &m.gate_exps),
3915                (PROJ_UP, &m.up_exps),
3916                (PROJ_DOWN, &m.down_exps),
3917            ] {
3918                let id = BlockId::new(il, proj, ex as u16);
3919                let _ = cache.dispatch_source(id, exps.expert_source(ex), eng)?;
3920            }
3921            Ok(())
3922        })
3923    }
3924
3925    /// Read a projection from the immutable residency set when present; otherwise use one
3926    /// transient slot without admitting or evicting anything. Used only by post-freeze prefill.
3927    #[allow(clippy::too_many_arguments)]
3928    fn moe_frozen_gemm(
3929        e: &Engine,
3930        il: u16,
3931        proj: u8,
3932        ex: usize,
3933        m: &MoeWeights,
3934        max_block: usize,
3935        x: &cudarc::driver::CudaView<f32>,
3936        scratch: &mut Option<CudaSlice<u8>>,
3937        scratch_len: usize,
3938    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3939        use crate::moe_cache::{BlockId, PROJ_GATE, PROJ_UP};
3940        let exps = match proj {
3941            PROJ_GATE => &m.gate_exps,
3942            PROJ_UP => &m.up_exps,
3943            _ => &m.down_exps,
3944        };
3945        let layout = exps.expert_layout(ex);
3946        let id = BlockId::new(il, proj, ex as u16);
3947        if let Some(output) = e.with_moe_cache(max_block, |cache, eng| {
3948            let Some(slot) = cache.resident(id) else {
3949                return Ok(None);
3950            };
3951            let buf = cache.slot(slot);
3952            Ok(Some(eng.qmatvec_view(
3953                buf,
3954                0..layout.len,
3955                x,
3956                1,
3957                exps.in_f,
3958                exps.out_f,
3959                layout.qtype,
3960                layout.row_bytes,
3961            )?))
3962        })? {
3963            return Ok(output);
3964        }
3965        if scratch.is_none() {
3966            *scratch = Some(e.alloc_u8_uninit(scratch_len)?);
3967        }
3968        let scratch = scratch.as_mut().unwrap();
3969        e.stage_expert(exps.expert_bytes(ex), scratch, 0)?;
3970        e.qmatvec_view(
3971            scratch,
3972            0..layout.len,
3973            x,
3974            1,
3975            exps.in_f,
3976            exps.out_f,
3977            layout.qtype,
3978            layout.row_bytes,
3979        )
3980    }
3981
3982    fn moe_prefetch_expert(
3983        e: &Engine,
3984        il: u16,
3985        ex: usize,
3986        m: &MoeWeights,
3987        max_block: usize,
3988        keep: &[crate::moe_cache::BlockId],
3989    ) -> Result<(), Box<dyn std::error::Error>> {
3990        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
3991        e.with_moe_cache(max_block, |c, eng| {
3992            for (proj, exps) in [(PROJ_GATE, &m.gate_exps), (PROJ_UP, &m.up_exps),
3993                                 (PROJ_DOWN, &m.down_exps)] {
3994                let id = BlockId::new(il, proj, ex as u16);
3995                let _ = c.prefetch_source(id, exps.expert_source(ex), keep, eng)?;
3996            }
3997            Ok(())
3998        })
3999    }
4000
4001    /// Worker-mode disk lookahead for grouped prefill. Memory sources are deliberately skipped so
4002    /// selecting `worker` changes only storage scheduling here; all CUDA work stays in dispatch.
4003    fn moe_prefetch_disk_expert(e: &Engine, il: u16, ex: usize, m: &MoeWeights,
4004                                max_block: usize, keep: &[crate::moe_cache::BlockId])
4005                                -> Result<(), Box<dyn std::error::Error>> {
4006        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
4007        e.with_moe_cache(max_block, |c, eng| {
4008            for (proj, exps) in [(PROJ_GATE, &m.gate_exps), (PROJ_UP, &m.up_exps),
4009                                 (PROJ_DOWN, &m.down_exps)] {
4010                let source = exps.expert_source(ex);
4011                if let crate::model::ExpertSource::Disk { .. } = &source {
4012                    let id = BlockId::new(il, proj, ex as u16);
4013                    let _ = c.prefetch_source(id, source, keep, eng)?;
4014                }
4015            }
4016            Ok(())
4017        })
4018    }
4019
4020    #[inline]
4021    fn moe_prefetch_host_expert(ex: usize, m: &MoeWeights) {
4022        let _ = m.gate_exps.prefetch_expert_pages(ex);
4023        let _ = m.up_exps.prefetch_expert_pages(ex);
4024        let _ = m.down_exps.prefetch_expert_pages(ex);
4025    }
4026}
4027
4028// ================================================================================================
4029// A2: EXPERT-GROUPED MoE PREFILL (MEMRA_MOE_GROUPED=1). Resident-case prototype.
4030//
4031// Instead of the per-token loop (T * 8 experts * 3 projections = 12024 individual m=1 matvecs),
4032// this groups tokens by expert and runs ONE matmul per active expert per projection at m=m_e.
4033// On a 501-token prefill with ~170 active experts, that's ~510 matmuls (vs 12024).
4034//
4035// EXACTNESS: per-token accumulation across its 8 experts is reordered (grouped processes experts
4036// in expert-id order, not the router's top-k order). To preserve bit-identity with the sequential
4037// loop, we use an 8-SLOT scheme: expert outputs are scattered into slots keyed by the token's
4038// top-k position (0..7), then reduced in that fixed order. This makes the f32 addition order
4039// identical to the per-token loop regardless of expert processing order.
4040//
4041// Memory: T * 8 * n_embd * 4 = 501 * 8 * 2048 * 4 = ~32 MB (slot buffer). Fine on 96GB.
4042// ================================================================================================
4043
4044impl HybridModel {
4045    /// A2 expert-grouped MoE FFN (prefill path, MEMRA_MOE_GROUPED=1). Same semantics as moe_ffn:
4046    /// z [T, n_embd] -> moe_out [T, n_embd]. BIT-IDENTICAL to moe_ffn when using the slot scheme.
4047    pub(crate) fn moe_ffn_grouped(e: &Engine, m: &MoeWeights, z: &CudaSlice<f32>, t: usize,
4048                                  cfg: &ModelConfig, il: u16, _max_block: usize)
4049                   -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4050        let moe = cfg.moe.as_ref().unwrap();
4051        let n_embd = cfg.n_embd as usize;
4052        let n_expert = moe.expert_count as usize;
4053        let n_used = moe.expert_used_count as usize;
4054        let n_ff_exp = moe.expert_ff_length as usize;
4055
4056        // 1. ROUTER (identical to moe_ffn).
4057        let logits = e.matmul(&m.gate_inp, z, t)?;
4058        let (sel_all, w_all) = if let Some(sig) = cfg.sigmoid_router() {
4059            Self::moe_route_cfg(e, &logits, t, n_expert, n_used,
4060                                m.exp_probs_b.as_deref(), Some(sig), m.active_experts.as_deref())?
4061        } else {
4062            Self::moe_route_cfg(e, &logits, t, n_expert, n_used,
4063                                None, None, m.active_experts.as_deref())?
4064        };
4065        Self::trace_moe_routes(il, t, &sel_all, &w_all)?;
4066
4067        // 2. BUILD PER-EXPERT TOKEN LISTS (host-side grouping).
4068        // For each expert e, we need: which tokens use it, their positions in z, their top-k
4069        // slot index (for bit-identical accumulation), and their weights.
4070        struct ExpertGroup {
4071            tok_indices: Vec<i32>,   // indices into z rows (0..T-1)
4072            slot_indices: Vec<i32>,  // top-k slot (0..n_used-1) for that token-expert pair
4073            weights: Vec<f32>,       // renormalized weight for that token-expert pair
4074        }
4075        let mut groups: Vec<ExpertGroup> = (0..n_expert).map(|_| ExpertGroup {
4076            tok_indices: Vec::new(), slot_indices: Vec::new(), weights: Vec::new(),
4077        }).collect();
4078
4079        for tok in 0..t {
4080            for j in 0..n_used {
4081                let ex = sel_all[tok * n_used + j] as usize;
4082                let w = w_all[tok * n_used + j];
4083                groups[ex].tok_indices.push(tok as i32);
4084                groups[ex].slot_indices.push(j as i32);
4085                groups[ex].weights.push(w);
4086            }
4087        }
4088
4089        // 3. ALLOCATE SLOT BUFFER: [T, n_used, n_embd] f32, zero-initialized.
4090        // Each token's 8 expert contributions land in their respective slots.
4091        let mut slot_buf = e.zeros(t * n_used * n_embd)?;
4092        let mut wbuf = e.zeros(t * n_used)?;  // [T, n_used] weight buffer for FMA reduce
4093
4094        // Expert weight dimensions (used in both cache and staging paths).
4095        let g_len = m.gate_exps.max_expert_bytes();
4096        let u_len = m.up_exps.max_expert_bytes();
4097        let d_len = m.down_exps.max_expert_bytes();
4098        let use_cache = Engine::moe_cache_enabled();
4099        let max_block = _max_block;
4100
4101        // GPU scratch for staging (only allocated when NOT using cache).
4102        let (mut scratch_g, mut scratch_u, mut scratch_d) = if !use_cache {
4103            (Some(e.alloc_u8(g_len)?), Some(e.alloc_u8(u_len)?), Some(e.alloc_u8(d_len)?))
4104        } else {
4105            (None, None, None)
4106        };
4107
4108        // 4. PER ACTIVE EXPERT: gather, compute, scatter.
4109        // Processing ORDER: DESCENDING m_e (biggest token batches first) — the concluded winner
4110        // (rig5090 2026-07-04, the ascending-id arm and its MEMRA_MOE_ORDER seam removed): desc is
4111        // a first-forward win at partial cache capacity — the hot (big-m_e) experts are admitted
4112        // to the SLRU before the small-m_e tail can pollute it, so residency converges in ONE
4113        // forward instead of several: auto-cache T=501 126.9 -> 169.9 tok/s (1.34x), cap512
4114        // 119.6 -> 160.8 (and kills the rep-to-rep bimodal); wash (<2%) at cap64 pure-spill and
4115        // at long prompts where every expert stages regardless. Order is FREE to change without
4116        // breaking the byte-identity gate: the slot scheme pins each token's accumulation order
4117        // regardless of expert processing order (the whole point of the slots).
4118        let mut order: Vec<usize> =
4119            (0..n_expert).filter(|&ex| !groups[ex].tok_indices.is_empty()).collect();
4120        order.sort_by(|&a, &b| groups[b].tok_indices.len()
4121            .cmp(&groups[a].tok_indices.len()).then(a.cmp(&b)));
4122        let mut m_dist: Vec<usize> = Vec::new();  // for stats
4123        let page_window = moe_page_prefetch_window();
4124        let worker_disk_prefetch = use_cache && crate::spill_pread::worker_enabled();
4125        if worker_disk_prefetch {
4126            if let Some(first) = grouped_worker_prefetch_position(order.len(), None) {
4127                Self::moe_prefetch_disk_expert(e, il, order[first], m, max_block, &[])?;
4128            }
4129        }
4130        for (order_pos, &ex) in order.iter().enumerate() {
4131            for next in page_prefetch_positions(order_pos, order.len(), page_window) {
4132                Self::moe_prefetch_host_expert(order[next], m);
4133            }
4134            if worker_disk_prefetch {
4135                if let Some(next) = grouped_worker_prefetch_position(order.len(), Some(order_pos)) {
4136                    use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
4137                    let keep = [
4138                        BlockId::new(il, PROJ_GATE, ex as u16),
4139                        BlockId::new(il, PROJ_UP, ex as u16),
4140                        BlockId::new(il, PROJ_DOWN, ex as u16),
4141                    ];
4142                    Self::moe_prefetch_disk_expert(e, il, order[next], m, max_block, &keep)?;
4143                }
4144            }
4145            let grp = &groups[ex];
4146            let m_e = grp.tok_indices.len();
4147            m_dist.push(m_e);
4148            let gl = m.gate_exps.expert_layout(ex);
4149            let ul = m.up_exps.expert_layout(ex);
4150            let dl = m.down_exps.expert_layout(ex);
4151
4152            // Upload index/weight arrays to device. The down-proj per-expert macro-scale
4153            // (ModelOpt weight_scale_2) folds into the scatter weights — post-matmul linear,
4154            // same fold as the sequential loop's `w[j] * macro_scale(ex)`. 1.0 for GGUF experts.
4155            let tok_idx_d = e.htod_i32(&grp.tok_indices)?;
4156            let slot_idx_d = e.htod_i32(&grp.slot_indices)?;
4157            let dmac = m.down_exps.macro_scale(ex);
4158            let weight_d = if dmac == 1.0 { e.htod(&grp.weights)? } else {
4159                let scaled: Vec<f32> = grp.weights.iter().map(|&w| w * dmac).collect();
4160                e.htod(&scaled)?
4161            };
4162
4163            // GATHER: collect m_e activation rows from z into a contiguous buffer.
4164            let mut gathered = e.zeros(m_e * n_embd)?;
4165            e.gather_rows(z, &tok_idx_d, &mut gathered, n_embd, m_e)?;
4166            let gv = gathered.slice(0..m_e * n_embd);
4167
4168            // Compute gate/up/down matmuls -- two paths: cache-resident or host-staged.
4169            let y = if use_cache {
4170                use crate::moe_cache::{BlockId, PROJ_GATE, PROJ_UP, PROJ_DOWN};
4171                // CACHE PATH: dispatch through MOE cache, get device-resident buffer, GEMM at m=m_e.
4172                let gate = e.with_moe_cache(max_block, |c, eng| {
4173                    let id = BlockId::new(il, PROJ_GATE, ex as u16);
4174                    let slot = c.dispatch_source(id, m.gate_exps.expert_source(ex), eng)?;
4175                    let buf = c.buf(slot);
4176                    eng.qmatvec_view(buf, 0..gl.len, &gv, m_e,
4177                        m.gate_exps.in_f, m.gate_exps.out_f, gl.qtype, gl.row_bytes)
4178                })?;
4179                let up = e.with_moe_cache(max_block, |c, eng| {
4180                    let id = BlockId::new(il, PROJ_UP, ex as u16);
4181                    let slot = c.dispatch_source(id, m.up_exps.expert_source(ex), eng)?;
4182                    let buf = c.buf(slot);
4183                    eng.qmatvec_view(buf, 0..ul.len, &gv, m_e,
4184                        m.up_exps.in_f, m.up_exps.out_f, ul.qtype, ul.row_bytes)
4185                })?;
4186                // SiLU-MUL activation (per-expert macro-scales folded).
4187                let mut act = e.zeros(m_e * n_ff_exp)?;
4188                Self::ffn_act_scaled(e, cfg, &gate, &up,
4189                    m.gate_exps.macro_scale(ex), m.up_exps.macro_scale(ex), &mut act, m_e * n_ff_exp)?;
4190                let actv = act.slice(0..m_e * n_ff_exp);
4191                e.with_moe_cache(max_block, |c, eng| {
4192                    let id = BlockId::new(il, PROJ_DOWN, ex as u16);
4193                    let slot = c.dispatch_source(id, m.down_exps.expert_source(ex), eng)?;
4194                    let buf = c.buf(slot);
4195                    eng.qmatvec_view(buf, 0..dl.len, &actv, m_e,
4196                        m.down_exps.in_f, m.down_exps.out_f, dl.qtype, dl.row_bytes)
4197                })?
4198            } else {
4199                // STAGING PATH: H2D the expert blocks into scratch buffers, then GEMM.
4200                let sg = scratch_g.as_mut().unwrap();
4201                let su = scratch_u.as_mut().unwrap();
4202                let sd = scratch_d.as_mut().unwrap();
4203                e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
4204                e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
4205                e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
4206                let gate = e.qmatvec_view(sg, 0..gl.len, &gv, m_e,
4207                    m.gate_exps.in_f, m.gate_exps.out_f, gl.qtype, gl.row_bytes)?;
4208                let up = e.qmatvec_view(su, 0..ul.len, &gv, m_e,
4209                    m.up_exps.in_f, m.up_exps.out_f, ul.qtype, ul.row_bytes)?;
4210                // SiLU-MUL activation (per-expert macro-scales folded).
4211                let mut act = e.zeros(m_e * n_ff_exp)?;
4212                Self::ffn_act_scaled(e, cfg, &gate, &up,
4213                    m.gate_exps.macro_scale(ex), m.up_exps.macro_scale(ex), &mut act, m_e * n_ff_exp)?;
4214                let actv = act.slice(0..m_e * n_ff_exp);
4215                e.qmatvec_view(sd, 0..dl.len, &actv, m_e,
4216                    m.down_exps.in_f, m.down_exps.out_f, dl.qtype, dl.row_bytes)?
4217            };
4218
4219            // SCATTER into slot buffer: each row goes to slot_buf[tok, slot, :].
4220            e.scatter_slot(&y, &tok_idx_d, &slot_idx_d, &weight_d,
4221                           &mut slot_buf, &mut wbuf, n_embd, n_used, m_e)?;
4222        }
4223
4224        // 5. REDUCE SLOTS: sum the 8 slots per token into the final moe_out.
4225        let mut moe_out = e.zeros(t * n_embd)?;
4226        e.reduce_slots(&slot_buf, &wbuf, &mut moe_out, n_embd, n_used, t)?;
4227
4228        // STATS: print m-distribution when MEMRA_MOE_STATS is set.
4229        if std::env::var("MEMRA_MOE_STATS").is_ok() && !m_dist.is_empty() {
4230            m_dist.sort_unstable();
4231            let active = m_dist.len();
4232            let mean = m_dist.iter().sum::<usize>() as f64 / active as f64;
4233            let median = m_dist[active / 2];
4234            let max_m = *m_dist.last().unwrap();
4235            let min_m = m_dist[0];
4236            let above16 = m_dist.iter().filter(|&&x| x >= 16).count();
4237            println!("moe-grouped il={il} t={t} active={active}/{n_expert} \
4238                      m_e: min={min_m} median={median} mean={mean:.1} max={max_m} \
4239                      above_gemm_threshold(>=16)={above16}/{active}");
4240        }
4241
4242        // 6. SHARED EXPERT (same as moe_ffn — untouched).
4243        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
4244        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
4245        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
4246            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
4247        {
4248            let n_ff_sh = gate_shexp.out_features();
4249            let sg_gate = e.matmul(gate_shexp, z, t)?;
4250            let sg_up = e.matmul(up_shexp, z, t)?;
4251            let mut sa = e.zeros(t * n_ff_sh)?;
4252            Self::ffn_act(e, cfg, &sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
4253            let sh = e.matmul(down_shexp, &sa, t)?;
4254            // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
4255            // Fused sigmoid-dot below PRIME_MIN_T — one fold order with the sequential and
4256            // dev decode arms (dispatch choice must not change bits).
4257            let g = match &m.gate_inp_shexp {
4258                Some(gate_inp_shexp) => {
4259                    // router_prefill_exact_on(): the fused sigmoid-dot is m-INVARIANT and
4260                    // serves EVERY t (serve isolation, lane/concat-prime-exact).
4261                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
4262                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
4263                    } else {
4264                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
4265                        let mut g = e.uninit(t)?;
4266                        e.sigmoid(&gs, &mut g, t)?;
4267                        g
4268                    }
4269                }
4270                None => e.htod(&vec![1.0f32; t])?,
4271            };
4272            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
4273        }
4274
4275        Ok(moe_out)
4276    }
4277
4278    /// Lane-3 M2: cross-stream MoE for lockstep decode. Routes all m stream rows in one
4279    /// batch, executes fully-HBM-resident experts through the grouped gather/GEMM/scatter
4280    /// machinery at m_e>1 (weight reads amortized across streams), and assigns any expert
4281    /// with a missing projection to that row's CPU companion call (whole-expert granularity,
4282    /// same rule as the sequential frozen path). Slot-pinned accumulation keeps each row's
4283    /// expert-sum order identical to the sequential path.
4284    pub(crate) fn moe_ffn_lockstep(
4285        &self,
4286        e: &Engine,
4287        m: &MoeWeights,
4288        zbatch: &CudaSlice<f32>,
4289        mrows: usize,
4290        il: u16,
4291        max_block: usize,
4292    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4293        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
4294        let cfg = &self.cfg;
4295        let moe = cfg.moe.as_ref().unwrap();
4296        let n_embd = cfg.n_embd as usize;
4297        let n_expert = moe.expert_count as usize;
4298        let n_used = moe.expert_used_count as usize;
4299        let n_ff_exp = moe.expert_ff_length as usize;
4300
4301        let logits = e.matmul(&m.gate_inp, zbatch, mrows)?;
4302        let (sel_all, w_all) = if let Some(sig) = cfg.sigmoid_router() {
4303            Self::moe_route_cfg(e, &logits, mrows, n_expert, n_used,
4304                                m.exp_probs_b.as_deref(), Some(sig), m.active_experts.as_deref())?
4305        } else {
4306            Self::moe_route_cfg(e, &logits, mrows, n_expert, n_used,
4307                                None, None, m.active_experts.as_deref())?
4308        };
4309        Self::trace_moe_routes(il, mrows, &sel_all, &w_all)?;
4310
4311        // Residency split at whole-expert granularity against the (frozen) cache.
4312        let resident_expert: Vec<bool> = e.with_moe_cache(max_block, |c, _| {
4313            Ok((0..n_expert)
4314                .map(|ex| {
4315                    [PROJ_GATE, PROJ_UP, PROJ_DOWN].into_iter().all(|p| {
4316                        c.resident(BlockId::new(il, p, ex as u16)).is_some()
4317                    })
4318                })
4319                .collect())
4320        })?;
4321
4322        struct Group {
4323            rows: Vec<i32>,
4324            slots: Vec<i32>,
4325            weights: Vec<f32>,
4326        }
4327        let mut groups: std::collections::HashMap<usize, Group> = Default::default();
4328        let mut cpu_rows: Vec<Vec<(usize, f32)>> = vec![Vec::new(); mrows];
4329        let mut cpu_by_expert: std::collections::HashMap<usize, Vec<(usize, f32)>> =
4330            Default::default();
4331        for row in 0..mrows {
4332            for j in 0..n_used {
4333                let ex = sel_all[row * n_used + j] as usize;
4334                let w = w_all[row * n_used + j];
4335                if resident_expert[ex] {
4336                    let group = groups.entry(ex).or_insert_with(|| Group {
4337                        rows: Vec::new(),
4338                        slots: Vec::new(),
4339                        weights: Vec::new(),
4340                    });
4341                    group.rows.push(row as i32);
4342                    group.slots.push(j as i32);
4343                    group.weights.push(w);
4344                } else {
4345                    crate::cpu_experts::record_incomplete_gpu_residency(0);
4346                    cpu_rows[row].push((ex, w));
4347                    cpu_by_expert.entry(ex).or_default().push((row, w));
4348                }
4349            }
4350        }
4351
4352        // CPU tickets first: reads/compute overlap the GPU grouped work below. Experts routed
4353        // by >=2 streams go through the multi-row ABI (weight decode amortized across rows);
4354        // each row's remaining experts stay one ordinary per-row call. Contribution FP-sum
4355        // order per row differs from the sequential single-call chunk — part of the
4356        // documented lockstep numeric class.
4357        let host_rows = e.dtoh(zbatch)?;
4358        let rows_ok = crate::cpu_experts::rows_supported();
4359        enum CpuPart {
4360            Single { row: usize },
4361            Rows { rows: Vec<usize> },
4362        }
4363        let mut tickets: Vec<(CpuPart, crate::cpu_experts::CpuExpertTicket)> = Vec::new();
4364        let mut rows_served: std::collections::HashSet<(usize, usize)> = Default::default();
4365        if rows_ok {
4366            let mut shared: Vec<(usize, Vec<(usize, f32)>)> = cpu_by_expert
4367                .into_iter()
4368                .filter(|(_, rows)| rows.len() >= 2)
4369                .collect();
4370            shared.sort_by_key(|(ex, _)| *ex);
4371            for (ex, mut row_weights) in shared {
4372                row_weights.sort_by_key(|(row, _)| *row);
4373                let inputs: Vec<(&[f32], f32)> = row_weights
4374                    .iter()
4375                    .map(|&(row, w)| (&host_rows[row * n_embd..(row + 1) * n_embd], w))
4376                    .collect();
4377                let job = crate::cpu_experts::prepare_rows_job(m, ex, &inputs)
4378                    .map_err(std::io::Error::other)?;
4379                for &(row, _) in &row_weights {
4380                    rows_served.insert((row, ex));
4381                }
4382                tickets.push((
4383                    CpuPart::Rows {
4384                        rows: row_weights.iter().map(|&(row, _)| row).collect(),
4385                    },
4386                    crate::cpu_experts::submit_rows(job).map_err(std::io::Error::other)?,
4387                ));
4388            }
4389        }
4390        for (row, selected) in cpu_rows.iter().enumerate() {
4391            let leftover: Vec<(usize, f32)> = selected
4392                .iter()
4393                .copied()
4394                .filter(|&(ex, _)| !rows_served.contains(&(row, ex)))
4395                .collect();
4396            if leftover.is_empty() {
4397                continue;
4398            }
4399            let host_row = &host_rows[row * n_embd..(row + 1) * n_embd];
4400            let job = crate::cpu_experts::prepare_job(m, il, &leftover, host_row)
4401                .map_err(std::io::Error::other)?;
4402            tickets.push((
4403                CpuPart::Single { row },
4404                crate::cpu_experts::submit(job).map_err(std::io::Error::other)?,
4405            ));
4406        }
4407
4408        let mut slot_buf = e.zeros(mrows * n_used * n_embd)?;
4409        let mut wbuf = e.zeros(mrows * n_used)?;
4410        let mut order: Vec<usize> = groups.keys().copied().collect();
4411        order.sort_by(|&a, &b| {
4412            groups[&b].rows.len().cmp(&groups[&a].rows.len()).then(a.cmp(&b))
4413        });
4414        for &ex in &order {
4415            let group = &groups[&ex];
4416            let m_e = group.rows.len();
4417            let gl = m.gate_exps.expert_layout(ex);
4418            let ul = m.up_exps.expert_layout(ex);
4419            let dl = m.down_exps.expert_layout(ex);
4420            let row_idx_d = e.htod_i32(&group.rows)?;
4421            let slot_idx_d = e.htod_i32(&group.slots)?;
4422            let dmac = m.down_exps.macro_scale(ex);
4423            let weight_d = if dmac == 1.0 {
4424                e.htod(&group.weights)?
4425            } else {
4426                let scaled: Vec<f32> = group.weights.iter().map(|&w| w * dmac).collect();
4427                e.htod(&scaled)?
4428            };
4429            let mut gathered = e.zeros(m_e * n_embd)?;
4430            e.gather_rows(zbatch, &row_idx_d, &mut gathered, n_embd, m_e)?;
4431            let gv = gathered.slice(0..m_e * n_embd);
4432            let gate = e.with_moe_cache(max_block, |c, eng| {
4433                let slot = c
4434                    .resident(BlockId::new(il, PROJ_GATE, ex as u16))
4435                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
4436                eng.qmatvec_view(c.buf(crate::moe_cache::DispatchSlot::Resident(slot)), 0..gl.len, &gv, m_e,
4437                    m.gate_exps.in_f, m.gate_exps.out_f, gl.qtype, gl.row_bytes)
4438            })?;
4439            let up = e.with_moe_cache(max_block, |c, eng| {
4440                let slot = c
4441                    .resident(BlockId::new(il, PROJ_UP, ex as u16))
4442                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
4443                eng.qmatvec_view(c.buf(crate::moe_cache::DispatchSlot::Resident(slot)), 0..ul.len, &gv, m_e,
4444                    m.up_exps.in_f, m.up_exps.out_f, ul.qtype, ul.row_bytes)
4445            })?;
4446            let mut act = e.zeros(m_e * n_ff_exp)?;
4447            Self::ffn_act_scaled(e, cfg, &gate, &up,
4448                m.gate_exps.macro_scale(ex), m.up_exps.macro_scale(ex), &mut act, m_e * n_ff_exp)?;
4449            let actv = act.slice(0..m_e * n_ff_exp);
4450            let y = e.with_moe_cache(max_block, |c, eng| {
4451                let slot = c
4452                    .resident(BlockId::new(il, PROJ_DOWN, ex as u16))
4453                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
4454                eng.qmatvec_view(c.buf(crate::moe_cache::DispatchSlot::Resident(slot)), 0..dl.len, &actv, m_e,
4455                    m.down_exps.in_f, m.down_exps.out_f, dl.qtype, dl.row_bytes)
4456            })?;
4457            e.scatter_slot(&y, &row_idx_d, &slot_idx_d, &weight_d,
4458                           &mut slot_buf, &mut wbuf, n_embd, n_used, m_e)?;
4459        }
4460        let mut moe_out = e.zeros(mrows * n_embd)?;
4461        e.reduce_slots(&slot_buf, &wbuf, &mut moe_out, n_embd, n_used, mrows)?;
4462
4463        // CPU contributions join BEFORE the shared expert (the sequential path's placement).
4464        let mut row_sums: Vec<Option<Vec<f32>>> = vec![None; mrows];
4465        for (part, ticket) in tickets {
4466            let cpu_output = ticket.wait().map_err(std::io::Error::other)?;
4467            let mut add_row = |row: usize, chunk: &[f32]| {
4468                let sum = row_sums[row].get_or_insert_with(|| vec![0.0f32; n_embd]);
4469                for (accumulator, value) in sum.iter_mut().zip(chunk) {
4470                    *accumulator += value;
4471                }
4472            };
4473            match part {
4474                CpuPart::Single { row } => add_row(row, &cpu_output),
4475                CpuPart::Rows { rows } => {
4476                    for (slot, row) in rows.into_iter().enumerate() {
4477                        add_row(row, &cpu_output[slot * n_embd..(slot + 1) * n_embd]);
4478                    }
4479                }
4480            }
4481        }
4482        for (row, sum) in row_sums.into_iter().enumerate() {
4483            let Some(sum) = sum else { continue };
4484            let cpu_output = e.htod(&sum)?;
4485            let mut dst = moe_out.slice_mut(row * n_embd..(row + 1) * n_embd);
4486            e.axpy_into(&cpu_output, 1.0, &mut dst, n_embd)?;
4487        }
4488
4489        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
4490            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
4491        {
4492            let n_ff_sh = gate_shexp.out_features();
4493            let sg_gate = e.matmul(gate_shexp, zbatch, mrows)?;
4494            let sg_up = e.matmul(up_shexp, zbatch, mrows)?;
4495            let mut sa = e.zeros(mrows * n_ff_sh)?;
4496            Self::ffn_act(e, cfg, &sg_gate, &sg_up, &mut sa, mrows * n_ff_sh)?;
4497            let sh = e.matmul(down_shexp, &sa, mrows)?;
4498            // lockstep rows ARE decode tokens: fused sigmoid-dot per row so batched serving
4499            // decode matches the single-sequence decode chain bit-for-bit.
4500            let g = match &m.gate_inp_shexp {
4501                Some(gate_inp_shexp) => {
4502                    e.sigmoid_dot_rows(zbatch, gate_inp_shexp.float_data(), n_embd, mrows)?
4503                }
4504                None => e.htod(&vec![1.0f32; mrows])?,
4505            };
4506            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, mrows)?;
4507        }
4508
4509        Ok(moe_out)
4510    }
4511}
4512
4513// ============================ gemma4 (R8 verified wiring) ==================================
4514// Node-for-node vs llama.cpp src/models/gemma4.cpp:180-405 (HANDOVER "R8 VERIFIED WIRING").
4515// v0 bring-up: full attention everywhere (exact for prompts < sliding_window 1024 — R6 masking
4516// later), sdpa_naive (hd 512 has no FA stamp), sequential host-staged MoE (the perf arms grow
4517// gemma variants after the correctness gate).
4518impl HybridModel {
4519    /// Per-layer attention geometry (R5): (head_dim, n_kv, n_head, rope_base, scale, is_swa).
4520    pub(crate) fn gemma4_geom(&self, il: usize) -> (usize, usize, usize, f32, f32, bool) {
4521        let g = self.cfg.gemma4.as_ref().unwrap();
4522        let swa = g.swa_pattern[il];
4523        let hd = if swa { g.key_length_swa } else { g.key_length_global } as usize;
4524        // attention scale = 1.0 (llama gemma4.cpp:11 "Gemma4 uses self.scaling = 1.0" — q/k are
4525        // per-head rms-normed; NOT the 1/sqrt(hd) default. Bring-up bug: 1/sqrt(hd) left token-0
4526        // rows exact (softmax over one element) while every later position drifted).
4527        (hd, g.head_count_kv[il] as usize, self.cfg.n_head as usize,
4528         if swa { g.rope_base_swa } else { g.rope_base_global },
4529         1.0, swa)
4530    }
4531
4532    /// Suppress-token mask over t logits rows (tokenizer.ggml.suppress_tokens; no-op when the
4533    /// model ships none). NOT monotonic like softcap — must run before every argmax/sample, so
4534    /// every gemma4 logits tail (forward/prime/decode/dc/verify/e4b) calls this on device ld.
4535    fn gemma4_suppress(&self, e: &Engine, ld: &mut CudaSlice<f32>, t: usize)
4536                       -> Result<(), Box<dyn std::error::Error>> {
4537        if let Some((ids, n)) = self.gemma4_aux.as_ref().and_then(|a| a.suppress_d.as_ref()) {
4538            e.mask_ids_rows(ld, ids, *n, self.output.out_features(), t)?;
4539        }
4540        Ok(())
4541    }
4542
4543    /// gemma4 attention (R5 geometry, R7 weightless V-norm on the RAW K projection, R9 dual rope).
4544    /// `cache`: Some => PRIME mode — append the T post-rope K / normed V rows into the quantized
4545    /// KV cache (same per-row quantize math as the decode append) and advance len. Fresh-prompt
4546    /// only (v0): attends within `tokens` via the f32 sdpa.
4547    fn gemma4_attn_prime(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
4548                         h: &CudaSlice<f32>, pos_d: &CudaSlice<i32>, t: usize,
4549                         cache: Option<&mut Cache>)
4550                         -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4551        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
4552        let eps = self.cfg.rms_eps;
4553        let aux = self.gemma4_aux.as_ref().unwrap();
4554
4555        // quantize-once window: q/k/v share `h` — the MMQ D4 activation quantizes once
4556        // (h stays borrowed across the triple, so the cache key can't go stale).
4557        e.mmq_act_begin();
4558        let q0 = e.matmul(&fa.wq, h, t)?;   // [t, nh*hd]
4559        let k0 = e.matmul(&fa.wk, h, t)?;   // [t, nkv*hd]
4560        // globals ship no v_proj (wv := wk at load) — V input is the SAME projection output;
4561        // reuse k0 instead of re-running the identical matmul (K=V dedup, 5 layers).
4562        let v0 = if swa { e.matmul(&fa.wv, h, t)? } else { e.clone_dtod(&k0)? };
4563
4564        let mut q = e.uninit(t * nh * hd)?;
4565        let mut k = e.uninit(t * nkv * hd)?;
4566        // R7: V = weightless rms_norm of the raw projection; NEVER roped.
4567        let mut v = e.uninit(t * nkv * hd)?;
4568        // 31B glue lane: producers emit the bf16 FA operands (norm emits vb; rope emits qb/kb
4569        // post-rope) — kills 3 f32->bf16 converts + re-reads per layer. Bit-identical operands
4570        // (same __float2bfloat16); MEMRA_FA_EMIT=0 reverts to the convert-in-FA path.
4571        static EMIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4572        let emit = t >= 16 && crate::Engine::qkvnorm_w_on_prefill(nh * t + 2 * nkv * t, hd)
4573            && *EMIT.get_or_init(|| std::env::var("MEMRA_FA_EMIT").map(|s| s != "0").unwrap_or(true));
4574        let mut qb = e.alloc_uninit::<u8>(if emit { t * nh * hd * 2 } else { 1 })?;
4575        let mut kb = e.alloc_uninit::<u8>(if emit { t * nkv * hd * 2 } else { 1 })?;
4576        let mut vb = e.alloc_uninit::<u8>(if emit { t * nkv * hd * 2 } else { 1 })?;
4577        // f16-P/V door: emit V as f16 straight from the norm when this layer's FA consumer
4578        // reads f16 — kills the per-layer bf16->f16 re-encode (1.4GB/pass on 31B SWA).
4579        let v_f16 = emit && crate::fa_f16pv_on() && match hd {
4580            512 => true,
4581            256 => swa && crate::faw_hp_on() && nh % 2 == 0 && (nh / nkv) % 2 == 0,
4582            _ => false,
4583        };
4584        if emit {
4585            e.rms_norm_qkv_w4b(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
4586                               &aux.ones, &mut q, &mut k, &mut v, &mut vb,
4587                               hd, nh * t, nkv * t, eps, v_f16)?;
4588        } else {
4589            e.rms_norm_qkv(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
4590                           &aux.ones, &mut q, &mut k, &mut v, hd, nh * t, nkv * t, eps)?;
4591        }
4592
4593        let ff = if swa { None } else {
4594            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
4595        };
4596        if emit {
4597            e.rope_neox2_bf16e(&mut q, &mut k, &mut qb, &mut kb, pos_d, hd, hd, nh, nkv, t,
4598                               base, 1.0, ff)?;
4599        } else {
4600            e.rope_neox2(&mut q, &mut k, pos_d, hd, hd, nh, nkv, t, base, 1.0, ff)?;
4601        }
4602
4603        if let Some(cache) = cache {
4604            let kvl = cache.kv[il].as_mut().unwrap();
4605            assert_eq!(kvl.len, 0, "gemma4 prime is fresh-prompt only (v0)");
4606            e.append_kv_quantized_rows(&k, &v, &mut kvl.k, &mut kvl.v, kvl.len, t,
4607                                       kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes, (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()))?;
4608            kvl.len += t;
4609        }
4610        let mut attn = e.zeros(t * nh * hd)?;
4611        // R6: SWA layers mask keys older than sliding_window once the prompt exceeds it
4612        // (windowed naive twin; fa windowed stamps later). Under the window, full attention
4613        // is exact — SWA rides fa_prefill (hd-256 stamp), the hd-512 globals stay naive.
4614        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
4615        if swa && t > win {
4616            if hd == 256 && std::env::var("MEMRA_NOFA").is_err() {
4617                if emit { e.fa_prefill_w_pre(&qb, &kb, &vb, &mut attn, hd, nh, nkv, t, t,
4618                                             scale, true, win, v_f16)?; }
4619                else { e.fa_prefill_w(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true,
4620                                      win)?; }
4621            } else {
4622                e.sdpa_naive_w(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
4623            }
4624        } else if hd == 256 && std::env::var("MEMRA_NOFA").is_err() {
4625            e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
4626        } else if hd == 512 && std::env::var("MEMRA_NOFA").is_err() {
4627            if emit { e.fa_prefill_hd512_pre(&qb, &kb, &vb, &mut attn, hd, nh, nkv, t, t,
4628                                             scale, true, v_f16)?; }
4629            else { e.fa_prefill_hd512(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?; }
4630        } else {
4631            e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
4632        }
4633        Ok(e.matmul(&fa.wo, &attn, t)?)
4634    }
4635
4636    /// Back-compat wrapper (pure prefill, no cache).
4637    fn gemma4_attn(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
4638                   h: &CudaSlice<f32>, pos_d: &CudaSlice<i32>, t: usize)
4639                   -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4640        self.gemma4_attn_prime(e, fa, il, h, pos_d, t, None)
4641    }
4642
4643    /// gemma4 MoE with the expert input PRE-QUANTIZED (the q8z tail fusion). Caller guarantees
4644    /// the fast-arm conditions (resident dev slabs + dp4a qtypes) — decode t=1 and verify rows
4645    /// arms only, per-token kernel chains identical to the f32-input path (same quantize bytes:
4646    /// the q8z epilogue is quantize_q8_1 verbatim).
4647    fn gemma4_moe_q8(&self, e: &Engine, m: &crate::hybrid::MoeWeights,
4648                     bits: &crate::hybrid::Gemma4MoeBits,
4649                     mq: &(CudaSlice<i8>, CudaSlice<f32>),
4650                     router_in: &CudaSlice<f32>, t: usize)
4651                     -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4652        let cfg = &self.cfg;
4653        let moe = cfg.moe.as_ref().unwrap();
4654        let n_embd = cfg.n_embd as usize;
4655        let n_expert = moe.expert_count as usize;
4656        let n_used = moe.expert_used_count as usize;
4657        let n_ff_exp = moe.expert_ff_length as usize;
4658        // router stays the two-launch pair: THREE fuse variants measured worse (serial-dot
4659        // -50%, 1024-thread warp-parallel -12% on topk sync overhead — jsonl 2026-07-11);
4660        // the pair's 12us is kernel time, not launch gaps.
4661        let logits = if crate::router_kernel_on() {
4662            e.router_gemv(m.gate_inp.float_data(), router_in, n_embd, n_expert, t)?
4663        } else {
4664            e.matmul(&m.gate_inp, router_in, t)?
4665        };
4666        let dev = m.dev_exps.as_ref().unwrap();
4667        let (sel_d, w_d) = e.moe_router_topk_scaled(&logits, t, n_expert, n_used,
4668                                                    &bits.per_expert_scale_d)?;
4669        let (zq, zd) = mq;
4670        if t == 1 {
4671            let selv = sel_d.slice(0..n_used);
4672            let wv = w_d.slice(0..n_used);
4673            let act = e.moe_gate_up_gelu8_dev_q8(&dev.ptr_row, &selv, zq, zd,
4674                                                 n_embd, n_ff_exp, n_used, n_expert,
4675                                                 m.gate_exps.qtype, m.up_exps.qtype,
4676                                                 m.gate_exps.row_bytes, m.up_exps.row_bytes)?;
4677            let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
4678            let mut moe_out = e.uninit(n_embd)?;
4679            e.moe_down8_fma_dev_q8(&dev.ptr_row, &selv, &wv, &aq2, &ad2,
4680                                   &mut moe_out.slice_mut(0..n_embd), n_ff_exp, n_embd,
4681                                   n_used, n_expert, m.down_exps.qtype, m.down_exps.row_bytes)?;
4682            return Ok(moe_out);
4683        }
4684        let csr = t <= 10 && std::env::var("MEMRA_GEMMA_CSR").as_deref() != Ok("0");
4685        let act = if csr {
4686            e.moe_gate_up_gelu8_dev_q8_csr(&dev.ptr_row, &sel_d, zq, zd, t * n_used,
4687                                           n_embd, n_ff_exp, n_used, n_expert,
4688                                           m.gate_exps.qtype, m.up_exps.qtype,
4689                                           m.gate_exps.row_bytes, m.up_exps.row_bytes)?
4690        } else {
4691            e.moe_gate_up_gelu8_dev_q8_rows(&dev.ptr_row, &sel_d, zq, zd, t,
4692                                            n_embd, n_ff_exp, n_used, n_expert,
4693                                            m.gate_exps.qtype, m.up_exps.qtype,
4694                                            m.gate_exps.row_bytes, m.up_exps.row_bytes)?
4695        };
4696        let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
4697        let mut moe_out = e.uninit(t * n_embd)?;
4698        // down stays rows_g: the CSR dedup twin measured NEGATIVE at nsb=22 too (189.4 vs
4699        // 207.0 depth spec, bitwise-exact — jsonl 2026-07-10; qwen nsb=16 same verdict).
4700        e.moe_down8_fma_dev_q8_rows_g(&dev.ptr_row, &sel_d, &w_d, &aq2, &ad2, &mut moe_out, t,
4701                                      n_ff_exp, n_embd, n_used, n_expert,
4702                                      m.down_exps.qtype, m.down_exps.row_bytes)?;
4703        Ok(moe_out)
4704    }
4705
4706    /// gemma4 MoE (R2 router prologue input supplied by caller, R3 per-expert output scale).
4707    /// Sequential host-staged v0 — softmax gating + renorm (moe_route, the qwen recipe), GELU
4708    /// experts, scale folded into the accumulate weight (post-matmul linear scale, exact fold).
4709    fn gemma4_moe(&self, e: &Engine, m: &crate::hybrid::MoeWeights,
4710                  bits: &crate::hybrid::Gemma4MoeBits, moe_in: &CudaSlice<f32>,
4711                  router_in: &CudaSlice<f32>, t: usize)
4712                  -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4713        let cfg = &self.cfg;
4714        let moe = cfg.moe.as_ref().unwrap();
4715        let n_embd = cfg.n_embd as usize;
4716        let n_expert = moe.expert_count as usize;
4717        let n_used = moe.expert_used_count as usize;
4718        let n_ff_exp = moe.expert_ff_length as usize;
4719
4720        // Router: the in-house GEMV for ALL small t (decode AND verify ride the same per-column
4721        // kernel — the cuBLASLt n-dependence flipped top-k at verify t on the 27B, d994271);
4722        // batched matmul only at real prefill.
4723        let logits = if t < PRIME_MIN_T && crate::router_kernel_on() {
4724            e.router_gemv(m.gate_inp.float_data(), router_in, n_embd, n_expert, t)?
4725        } else {
4726            e.matmul(&m.gate_inp, router_in, t)?
4727        };
4728
4729        // FAST SMALL-T ARM (decode t=1 AND spec verify t=2..15): device softmax-topk router,
4730        // then PER TOKEN the same fused gate_up GELU + down8 FMA launch pair over the resident
4731        // dev slabs — verify rides the EXACT decode kernel chain per token (dispatch-parity
4732        // law; the qwen "verify must be kernel-dispatch-identical to decode" lesson).
4733        if t < PRIME_MIN_T && m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
4734            && expert_dp4a_supported(m.gate_exps.qtype) && expert_dp4a_supported(m.up_exps.qtype)
4735            && expert_dp4a_supported(m.down_exps.qtype)
4736            && std::env::var("MEMRA_GEMMA_MOE_FAST").as_deref() != Ok("0") {
4737            let dev = m.dev_exps.as_ref().unwrap();
4738            let (sel_d, w_d) = e.moe_router_topk_scaled(&logits, t, n_expert, n_used,
4739                                                        &bits.per_expert_scale_d)?;
4740            if t == 1 {
4741                let (zq, zd) = e.quantize_q8_1(moe_in, 1, n_embd)?;
4742                let selv = sel_d.slice(0..n_used);
4743                let wv = w_d.slice(0..n_used);
4744                let act = e.moe_gate_up_gelu8_dev_q8(&dev.ptr_row, &selv, &zq, &zd,
4745                                                     n_embd, n_ff_exp, n_used, n_expert,
4746                                                     m.gate_exps.qtype, m.up_exps.qtype,
4747                                                     m.gate_exps.row_bytes, m.up_exps.row_bytes)?;
4748                let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
4749                let mut moe_out = e.uninit(n_embd)?;
4750                e.moe_down8_fma_dev_q8(&dev.ptr_row, &selv, &wv, &aq2, &ad2,
4751                                       &mut moe_out.slice_mut(0..n_embd), n_ff_exp, n_embd,
4752                                       n_used, n_expert, m.down_exps.qtype, m.down_exps.row_bytes)?;
4753                return Ok(moe_out);
4754            }
4755            // VERIFY ROWS TWINS (t=2..15): ONE launch pair for all tokens; per (token,row,slot)
4756            // bodies are the t=1 kernels VERBATIM (bit-identical to the per-token loop).
4757            // gate_up rides the CSR owner-scan dedup when t <= 10 (each duplicated expert's
4758            // weight stream decoded once; the qwen CSR class). MEMRA_GEMMA_CSR=0 -> rows.
4759            let (zq, zd) = e.quantize_q8_1(moe_in, t, n_embd)?;
4760            let csr = t <= 10 && std::env::var("MEMRA_GEMMA_CSR").as_deref() != Ok("0");
4761            let act = if csr {
4762                e.moe_gate_up_gelu8_dev_q8_csr(&dev.ptr_row, &sel_d, &zq, &zd, t * n_used,
4763                                               n_embd, n_ff_exp, n_used, n_expert,
4764                                               m.gate_exps.qtype, m.up_exps.qtype,
4765                                               m.gate_exps.row_bytes, m.up_exps.row_bytes)?
4766            } else {
4767                e.moe_gate_up_gelu8_dev_q8_rows(&dev.ptr_row, &sel_d, &zq, &zd, t,
4768                                                n_embd, n_ff_exp, n_used, n_expert,
4769                                                m.gate_exps.qtype, m.up_exps.qtype,
4770                                                m.gate_exps.row_bytes, m.up_exps.row_bytes)?
4771            };
4772            let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
4773            let mut moe_out = e.uninit(t * n_embd)?;
4774            e.moe_down8_fma_dev_q8_rows_g(&dev.ptr_row, &sel_d, &w_d, &aq2, &ad2, &mut moe_out, t,
4775                                          n_ff_exp, n_embd, n_used, n_expert,
4776                                          m.down_exps.qtype, m.down_exps.row_bytes)?;
4777            return Ok(moe_out);
4778        }
4779
4780        let (sel_all, mut w_all) = Self::moe_route(e, &logits, t, n_expert, n_used)?;
4781        for (i, &sx) in sel_all.iter().enumerate() {
4782            w_all[i] *= bits.per_expert_scale[sx as usize];
4783        }
4784
4785        // PREFILL PAIRS ARM (t >= 16): expert-major CSR over the resident slabs — ONE launch per
4786        // projection covers ALL (token,expert) pairs (the qwen pairs recipe, _em dot = expert_dot_g
4787        // which has the Q4_0 body; GELU pairs epilogue; R3 scale folded into pair_w).
4788        if t >= PRIME_MIN_T && m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
4789            && expert_dp4a_supported(m.gate_exps.qtype) && expert_dp4a_supported(m.up_exps.qtype)
4790            && expert_dp4a_supported(m.down_exps.qtype)
4791            && std::env::var("MEMRA_GEMMA_MOE_PAIRS").as_deref() != Ok("0") {
4792            let dev = m.dev_exps.as_ref().unwrap();
4793            let n_pairs = t * n_used;
4794            let pair_ex: Vec<i32> = sel_all.iter().map(|&x| x as i32).collect();
4795            let pair_tok: Vec<i32> = (0..n_pairs).map(|p| (p / n_used) as i32).collect();
4796            let tok_off: Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
4797            let tok_ids: Vec<i32> = (0..n_pairs as i32).collect();
4798            let pt = e.htod_i32(&pair_tok)?;
4799            let pw = e.htod(&w_all)?;
4800            let toff = e.htod_i32(&tok_off)?;
4801            let tids = e.htod_i32(&tok_ids)?;
4802            let mut by_ex: Vec<Vec<i32>> = vec![Vec::new(); n_expert];
4803            for p in 0..n_pairs { by_ex[pair_ex[p] as usize].push(p as i32); }
4804            let mut ex_ids: Vec<i32> = Vec::new();
4805            let mut ex_off: Vec<i32> = vec![0];
4806            let mut ex_pairs: Vec<i32> = Vec::with_capacity(n_pairs);
4807            for (ex, list) in by_ex.iter().enumerate() {
4808                if list.is_empty() { continue; }
4809                ex_ids.push(ex as i32);
4810                ex_pairs.extend_from_slice(list);
4811                ex_off.push(ex_pairs.len() as i32);
4812            }
4813            let n_active = ex_ids.len();
4814            let exi = e.htod_i32(&ex_ids)?;
4815            let exo = e.htod_i32(&ex_off)?;
4816            let exp_d = e.htod_i32(&ex_pairs)?;
4817            // GROUPED f16 LANE (MEMRA_MOE_F16G=1, round 46 arc 2): dequant active experts to
4818            // f16 once per projection + one grouped f16 GEMM over the CSR groups; CSR order
4819            // end-to-end (gelu is elementwise), one row permute before the scatter. The
4820            // ragged down k (704) needs no padding here — cublas takes any k.
4821            // f16-mirror numeric class, argmax/spec gated. PER-MODEL default OFF (round 50):
4822            // the gelu class regressed g26 board-2048 prefill -8.3% under the round-49
4823            // Hopper default — see moe_f16g_gemma_on.
4824            if crate::moe_f16g_gemma_on()
4825                && f16g_proj_ok(m.gate_exps.qtype, n_embd)
4826                && f16g_proj_ok(m.up_exps.qtype, n_embd)
4827                && f16g_proj_ok(m.down_exps.qtype, n_ff_exp) {
4828                let csr_tok: Vec<i32> = ex_pairs.iter().map(|&p| p / n_used as i32).collect();
4829                let csr_tok_d = e.htod_i32(&csr_tok)?;
4830                let (z_f16, z_s) = e.moe_f16g_act(moe_in, Some(&csr_tok_d), n_embd, n_pairs)?;
4831                let g_csr = e.moe_f16_grouped(&dev.ptr_row, 0, n_expert, &exi, &ex_off, &exo,
4832                                              &z_f16, &z_s, n_embd, n_ff_exp, n_active, n_pairs,
4833                                              m.gate_exps.qtype, m.gate_exps.row_bytes)?;
4834                let u_csr = e.moe_f16_grouped(&dev.ptr_row, 1, n_expert, &exi, &ex_off, &exo,
4835                                              &z_f16, &z_s, n_embd, n_ff_exp, n_active, n_pairs,
4836                                              m.up_exps.qtype, m.up_exps.row_bytes)?;
4837                let act_csr = e.moe_pairs_gelu_mul(&g_csr, &u_csr, n_pairs * n_ff_exp)?;
4838                let (a_f16, a_s) = e.moe_f16g_act(&act_csr, None, n_ff_exp, n_pairs)?;
4839                let d_csr = e.moe_f16_grouped(&dev.ptr_row, 2, n_expert, &exi, &ex_off, &exo,
4840                                              &a_f16, &a_s, n_ff_exp, n_embd, n_active, n_pairs,
4841                                              m.down_exps.qtype, m.down_exps.row_bytes)?;
4842                let y_down = e.rows_permute(&d_csr, &exp_d, n_pairs, n_embd)?;
4843                let mut moe_out = e.uninit(t * n_embd)?;
4844                e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
4845                if std::env::var("MEMRA_F16G_DEBUG").is_ok() {
4846                    let scan = |v: &[f32]| v.iter().filter(|x| !x.is_finite()).count();
4847                    let (yd, mo) = (e.dtoh(&y_down)?, e.dtoh(&moe_out)?);
4848                    eprintln!("[f16g-debug] post-permute bad={} post-scatter bad={}",
4849                              scan(&yd), scan(&mo));
4850                }
4851                return Ok(moe_out);
4852            }
4853            // gate/up: int8-MMA expert GEMM (in_f = n_embd 2816 = 11x256 tiles ok); down keeps
4854            // the decode-once dp4a (in_f 704 fails the 256-superblock tiling).
4855            let mma = n_embd % 256 == 0
4856                && std::env::var("MEMRA_GEMMA_MOE_MMA").as_deref() != Ok("0");
4857            let (gate, up) = if mma {
4858                let z_scr = e.mmq_iq_quantize_act(moe_in, n_embd, t)?;
4859                (e.mmq_iq_experts(&dev.ptr_row, 0, n_expert, &exi, &exo, &exp_d, &pt, &z_scr,
4860                                  n_embd, n_ff_exp, n_active, n_pairs, t,
4861                                  m.gate_exps.qtype, m.gate_exps.row_bytes)?,
4862                 e.mmq_iq_experts(&dev.ptr_row, 1, n_expert, &exi, &exo, &exp_d, &pt, &z_scr,
4863                                  n_embd, n_ff_exp, n_active, n_pairs, t,
4864                                  m.up_exps.qtype, m.up_exps.row_bytes)?)
4865            } else {
4866                let (zq, zd) = e.quantize_q8_1(moe_in, t, n_embd)?;
4867                (e.moe_pairs_matvec_q8_dec(&dev.ptr_row, 0, &exi, &exo, &exp_d, &pt, &zq, &zd,
4868                                           n_embd, n_ff_exp, n_expert, n_active, n_pairs,
4869                                           m.gate_exps.qtype, m.gate_exps.row_bytes)?,
4870                 e.moe_pairs_matvec_q8_dec(&dev.ptr_row, 1, &exi, &exo, &exp_d, &pt, &zq, &zd,
4871                                           n_embd, n_ff_exp, n_expert, n_active, n_pairs,
4872                                           m.up_exps.qtype, m.up_exps.row_bytes)?)
4873            };
4874            let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
4875            let pself = e.htod_i32(&pair_self)?;
4876            // DOWN through the int8-MMA expert GEMM (2026-07-31, g26 prefill lever): the
4877            // ragged k (n_ff_exp=704 on the 26B) rides a PADDED k-walk — in_f rounds up
4878            // to the 256-val superblock (768) while the act quantizer's zero padding
4879            // makes every padded-k product exactly zero (weight overread bytes multiply
4880            // zero int8 act values; the dev slab carries 144B tail slack for the OOB).
4881            // The old dp4a matvec was 11.3ms/call at m=T (the 0.07x prefill wall).
4882            // MEMRA_GEMMA_MOE_MMA=0 reverts down together with gate/up.
4883            // FUSED ACT-EPILOGUE (default on): gelu_tanh(gate)*up + D4 quantize in one
4884            // launch — no f32 act buffer (the fused kernel zero-pads the ragged tail
4885            // exactly like the two-pass quantizer). MEMRA_MOE_FUSE_ACTQ=0 rollback.
4886            // Scratch bytes are BYTE-IDENTICAL (kernel-check gated).
4887            let y_down = if mma {
4888                let in_pad = n_ff_exp.div_ceil(256) * 256;
4889                let a_scr = if crate::moe_fuse_actq_on() {
4890                    e.mmq_iq_fused_act_quant(&gate, &up, n_ff_exp, n_pairs, 1)?
4891                } else {
4892                    let act = e.moe_pairs_gelu_mul(&gate, &up, n_pairs * n_ff_exp)?;
4893                    e.mmq_iq_quantize_act(&act, n_ff_exp, n_pairs)?
4894                };
4895                e.mmq_iq_experts(&dev.ptr_row, 2, n_expert, &exi, &exo, &exp_d, &pself, &a_scr,
4896                                 in_pad, n_embd, n_active, n_pairs, n_pairs,
4897                                 m.down_exps.qtype, m.down_exps.row_bytes)?
4898            } else {
4899                let act = e.moe_pairs_gelu_mul(&gate, &up, n_pairs * n_ff_exp)?;
4900                let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
4901                e.moe_pairs_matvec_q8_dec(&dev.ptr_row, 2, &exi, &exo, &exp_d, &pself, &aq2, &ad2,
4902                                          n_ff_exp, n_embd, n_expert, n_active, n_pairs,
4903                                          m.down_exps.qtype, m.down_exps.row_bytes)?
4904            };
4905            let mut moe_out = e.uninit(t * n_embd)?;
4906            e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
4907            return Ok(moe_out);
4908        }
4909
4910        let g_len = m.gate_exps.expert_stride;
4911        let u_len = m.up_exps.expert_stride;
4912        let d_len = m.down_exps.expert_stride;
4913        // Resident dev slabs (fits-VRAM regime): read each expert straight from the device slab
4914        // at ex*stride — zero H2D, SAME qmatvec_view kernel/bytes as the staged path. Staging is
4915        // the spill fallback.
4916        let dev = m.dev_exps.as_ref().filter(|d| !d.gu_il);
4917        let (mut sg, mut su, mut sd) = if dev.is_some() { (None, None, None) } else {
4918            (Some(e.alloc_u8_uninit(g_len)?), Some(e.alloc_u8_uninit(u_len)?), Some(e.alloc_u8_uninit(d_len)?))
4919        };
4920        let mut moe_out = e.zeros(t * n_embd)?;
4921        for tok in 0..t {
4922            let sel = &sel_all[tok * n_used..(tok + 1) * n_used];
4923            let w = &w_all[tok * n_used..(tok + 1) * n_used];
4924            let zt = moe_in.slice(tok * n_embd..(tok + 1) * n_embd);
4925            for (j, &ex) in sel.iter().enumerate() {
4926                let ex = ex as usize;
4927                let gate = match dev {
4928                    Some(d) => e.qmatvec_view(&d.gate, ex * g_len..(ex + 1) * g_len, &zt, 1,
4929                        m.gate_exps.in_f, m.gate_exps.out_f, m.gate_exps.qtype, m.gate_exps.row_bytes)?,
4930                    None => {
4931                        let sg = sg.as_mut().unwrap();
4932                        e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
4933                        e.qmatvec_view(sg, 0..g_len, &zt, 1,
4934                            m.gate_exps.in_f, m.gate_exps.out_f, m.gate_exps.qtype, m.gate_exps.row_bytes)?
4935                    }
4936                };
4937                let up = match dev {
4938                    Some(d) => e.qmatvec_view(&d.up, ex * u_len..(ex + 1) * u_len, &zt, 1,
4939                        m.up_exps.in_f, m.up_exps.out_f, m.up_exps.qtype, m.up_exps.row_bytes)?,
4940                    None => {
4941                        let su = su.as_mut().unwrap();
4942                        e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
4943                        e.qmatvec_view(su, 0..u_len, &zt, 1,
4944                            m.up_exps.in_f, m.up_exps.out_f, m.up_exps.qtype, m.up_exps.row_bytes)?
4945                    }
4946                };
4947                let mut act = e.uninit(n_ff_exp)?;
4948                e.gelu_tanh_mul(&gate, &up, &mut act, n_ff_exp)?;
4949                let actv = act.slice(0..n_ff_exp);
4950                let y = match dev {
4951                    Some(d) => e.qmatvec_view(&d.down, ex * d_len..(ex + 1) * d_len, &actv, 1,
4952                        m.down_exps.in_f, m.down_exps.out_f, m.down_exps.qtype, m.down_exps.row_bytes)?,
4953                    None => {
4954                        let sd = sd.as_mut().unwrap();
4955                        e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
4956                        e.qmatvec_view(sd, 0..d_len, &actv, 1,
4957                            m.down_exps.in_f, m.down_exps.out_f, m.down_exps.qtype, m.down_exps.row_bytes)?
4958                    }
4959                };
4960                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
4961                e.axpy_into(&y, w[j], &mut dst, n_embd)?;
4962            }
4963        }
4964        Ok(moe_out)
4965    }
4966
4967    /// One gemma4 trunk layer (R8): x -> x_next.
4968    fn gemma4_layer(&self, e: &Engine, il: usize, layer: &crate::hybrid::HybridLayer,
4969                    x: &CudaSlice<f32>, pos_d: &CudaSlice<i32>, t: usize)
4970                    -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4971        let n_embd = self.cfg.n_embd as usize;
4972        let eps = self.cfg.rms_eps;
4973
4974        let mut h = e.zeros(t * n_embd)?;
4975        e.rms_norm(x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
4976        let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
4977        let o = self.gemma4_attn(e, fa, il, &h, pos_d, t)?;
4978        // gemma order: post_attention_norm applies to the ATTENTION OUTPUT, then the residual.
4979        let mut cur = e.zeros(t * n_embd)?;
4980        e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, t, eps)?;
4981        self.gemma4_layer_tail_add(e, layer, &cur, x, t)
4982    }
4983
4984    /// Everything after the attention output in a gemma4 layer: the residual add (cur + x ->
4985    /// attn_out) FUSED with the three attn_out norms, then shared FFN + router + MoE + combine +
4986    /// layer scale — shared verbatim by the prefill, decode and verify paths.
4987    fn gemma4_layer_tail_add(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
4988                             cur: &CudaSlice<f32>, x: &CudaSlice<f32>, t: usize)
4989                             -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4990        Ok(self.gemma4_layer_tail_add_n(e, layer, cur, x, t, None)?.0)
4991    }
4992
4993    /// tail_add with the NEXT layer's attn_norm fused into the closing add+scale (one launch
4994    /// produces both x_next and h_next — the cross-layer fusion). `next_norm` None = last layer.
4995    fn gemma4_layer_tail_add_n(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
4996                               cur: &CudaSlice<f32>, x: &CudaSlice<f32>, t: usize,
4997                               next_norm: Option<&CudaSlice<f32>>)
4998                               -> Result<(CudaSlice<f32>, Option<CudaSlice<f32>>), Box<dyn std::error::Error>> {
4999        let n_embd = self.cfg.n_embd as usize;
5000        let bits = layer.gemma4.as_ref().unwrap();
5001        let (sn, attn_out) = self.gemma4_layer_tail_core(e, layer, cur, x, t)?;
5002        let mut xn = e.uninit(t * n_embd)?;
5003        match next_norm {
5004            Some(w) => {
5005                let mut hn = e.uninit(t * n_embd)?;
5006                e.add_scale_rms_norm(&sn, &attn_out, bits.layer_scale, w, &mut xn, &mut hn,
5007                                     n_embd, t, self.cfg.rms_eps)?;
5008                Ok((xn, Some(hn)))
5009            }
5010            None => {
5011                e.add_scale(&sn, &attn_out, bits.layer_scale, &mut xn, t * n_embd)?;
5012                Ok((xn, None))
5013            }
5014        }
5015    }
5016
5017    /// Tail core: attn_out = cur+x (fused with the 3 norms), both FFN branches, the combine
5018    /// norm — returns (sn, attn_out) for the closing add+scale variants.
5019    fn gemma4_layer_tail_core(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
5020                              cur: &CudaSlice<f32>, x: &CudaSlice<f32>, t: usize)
5021                              -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
5022        self.gemma4_layer_tail_core_pn(e, layer, cur, x, t, None, false)
5023    }
5024
5025    /// tail_core with an optional PRE-NORM fold (glue-fusion lane): `pre_norm = Some(wa)`
5026    /// means `cur` is the RAW attention output and the dense entry runs
5027    /// rms(cur, wa) + residual-add + ffn_norm as ONE launch (E4B's post-attn norm fold).
5028    /// `defer_post_norm`: dense-arm exit returns RAW f0 (ffn_down output) instead of
5029    /// sn = rms(f0, post_ffw) — the caller fuses the post-norm into its residual emit
5030    /// (rms_pre_add_q8_1, E4B glue wave 5). MoE arm ignores it.
5031    fn gemma4_layer_tail_core_pn(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
5032                                 cur: &CudaSlice<f32>, x: &CudaSlice<f32>, t: usize,
5033                                 pre_norm: Option<&CudaSlice<f32>>, defer_post_norm: bool)
5034                                 -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
5035        let n_embd = self.cfg.n_embd as usize;
5036        let eps = self.cfg.rms_eps;
5037        let bits = layer.gemma4.as_ref().unwrap();
5038
5039        // DENSE gemma4 variants (31B/E4B — no MoE, no parallel branch): attn_out = cur + x
5040        // fused with the single ffn_norm; GELU_PAR ffn; post_ffw_norm.
5041        let Some(mbits) = bits.moe_bits.as_ref() else {
5042            let crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } = &layer.ffn
5043            else { panic!("gemma4 dense layer without Dense ffn") };
5044            let mut attn_out = e.uninit(t * n_embd)?;
5045            let mut zsh = e.uninit(t * n_embd)?;
5046            // wave-2: with the pre-norm fold active the entry ALSO emits zsh q8_1 — the
5047            // t=1 fused2 gate/up consume the pair with no standalone quantize launch.
5048            let mut zpair: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
5049            match pre_norm {
5050                Some(wa) if t == 1 => {
5051                    zpair = Some(e.rms_pre_add_rms_norm_q8z(cur, wa, x,
5052                                                            bits.ffn_norm.float_data(),
5053                                                            &mut attn_out, &mut zsh,
5054                                                            n_embd, t, eps)?);
5055                }
5056                Some(wa) => e.rms_pre_add_rms_norm(cur, wa, x, bits.ffn_norm.float_data(),
5057                                                   &mut attn_out, &mut zsh, n_embd, t, eps)?,
5058                None => e.add_rms_norm(cur, x, bits.ffn_norm.float_data(), &mut attn_out,
5059                                       &mut zsh, n_embd, t, eps)?,
5060            }
5061            let n_ff = ffn_gate.out_features();
5062            // FFN persistent slab (counter-barrier form) FALSIFIED here 2026-07-14
5063            // (falsification #7, jsonl row): PDL glue already hides the launch boundaries
5064            // it fused; barrier + worst-segment occupancy net −0.3% (31B depth) / −2.3%
5065            // (E4B spec). Down's act dependency is all-to-all, so sentinel sync cannot
5066            // rescue segment C — the megakernel front is closed for the dense tail.
5067            let (gate, up) = if t == 1 {
5068                let (zq, zd) = match zpair {
5069                    Some(p) => p,
5070                    None => e.quantize_q8_1(&zsh, 1, n_embd)?,
5071                };
5072                match e.matmul_q4_fused2(ffn_gate, ffn_up, &zq, &zd)? {
5073                    Some(p) => p,
5074                    None => (e.matmul_pre(ffn_gate, &zq, &zd, &zsh, 1)?,
5075                             e.matmul_pre(ffn_up, &zq, &zd, &zsh, 1)?),
5076                }
5077            } else {
5078                // BATCHED FUSED2 (DEFAULT ON 2026-07-13, MEMRA_F2B=0 seam): one segmented
5079                // launch for the verify's gate+up — the up segment's blocks fill SMs as
5080                // the gate segment drains (the launch-tail mechanism behind the b-tier
5081                // plateau; first positive after six falsified in-kernel variants).
5082                static F2B: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
5083                let f2b = *F2B.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
5084                let fused = if f2b {
5085                    let (zq, zd) = e.quantize_q8_1(&zsh, t, n_embd)?;
5086                    e.matmul_q4_fused2_batched(ffn_gate, ffn_up, &zq, &zd, t)?
5087                } else { None };
5088                match fused {
5089                    Some(p) => p,
5090                    None => {
5091                        // quantize-once window: gate/up share `zsh` (borrowed across the pair).
5092                        e.mmq_act_begin();
5093                        (e.matmul(ffn_gate, &zsh, t)?, e.matmul(ffn_up, &zsh, t)?)
5094                    }
5095                }
5096            };
5097            let mut act = e.uninit(t * n_ff)?;
5098            // act quantize folds into the GELU epilogue (bit-identical q8_1 rounding);
5099            // ffn_down rides matmul_pre — one quantize launch fewer per layer.
5100            let f0 = if e.uses_q8_1_fast(ffn_down) {
5101                let upv = e.view(&up, t * n_ff);
5102                let up_all = upv.slice(0..t * n_ff);
5103                let (aq, ad) = e.gelu_tanh_mul_q8_1(&gate, &up_all, &mut act, n_ff, t)?;
5104                e.matmul_pre(ffn_down, &aq, &ad, &act, t)?
5105            } else {
5106                e.gelu_tanh_mul(&gate, &up, &mut act, t * n_ff)?;
5107                e.matmul(ffn_down, &act, t)?
5108            };
5109            if defer_post_norm { return Ok((f0, attn_out)); }
5110            let mut sn = e.uninit(t * n_embd)?;
5111            e.rms_norm(&f0, bits.post_ffw_norm.float_data(), &mut sn, n_embd, t, eps)?;
5112            return Ok((sn, attn_out));
5113        };
5114
5115        assert!(pre_norm.is_none(), "pre-norm fold is dense-entry only");
5116        // MoE variant (26B): attn_out = cur + x fused with the three attn_out norms
5117        // (ffn_norm + router-scale + pre_ffw_norm_2): ONE launch, chains verbatim. At small t
5118        // (decode + verify) the zsh and moe_in outputs are EMITTED q8_1 (both consumers are
5119        // quantized matmuls — two quantize launches + two f32 round-trips fold away).
5120        let mut attn_out = e.uninit(t * n_embd)?;
5121        let mut router_in = e.uninit(t * n_embd)?;
5122        let fast_moe = match &layer.ffn {
5123            crate::hybrid::Ffn::Moe(m) => m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
5124                && expert_dp4a_supported(m.gate_exps.qtype)
5125                && expert_dp4a_supported(m.up_exps.qtype)
5126                && expert_dp4a_supported(m.down_exps.qtype)
5127                && std::env::var("MEMRA_GEMMA_MOE_FAST").as_deref() != Ok("0"),
5128            _ => false,
5129        };
5130        let q8z = t < PRIME_MIN_T && fast_moe;
5131        let (zsh_f32, zsh_q8, moe_q8) = if q8z {
5132            let (z0, m2) = e.add_rms_norm3_q8z(cur, x, bits.ffn_norm.float_data(),
5133                                               &mbits.router_scale_pre,
5134                                               mbits.pre_ffw_norm_2.float_data(),
5135                                               &mut attn_out, &mut router_in, n_embd, t, eps)?;
5136            (None, Some(z0), Some(m2))
5137        } else {
5138            let mut zsh = e.uninit(t * n_embd)?;
5139            let mut moe_in = e.uninit(t * n_embd)?;
5140            e.add_rms_norm3(cur, x, bits.ffn_norm.float_data(), &mbits.router_scale_pre,
5141                            mbits.pre_ffw_norm_2.float_data(), &mut attn_out, &mut zsh,
5142                            &mut router_in, &mut moe_in, n_embd, t, eps)?;
5143            (Some((zsh, moe_in)), None, None)
5144        };
5145        let attn_out2 = attn_out;
5146        #[allow(unused_variables)]
5147        let attn_out = &attn_out2;
5148        let n_ff = mbits.shared_gate.out_features();
5149        let (gate, up) = if let Some((zq, zd)) = zsh_q8.as_ref() {
5150            if t == 1 {
5151                match e.matmul_q4_fused2(&mbits.shared_gate, &mbits.shared_up, zq, zd)? {
5152                    Some(p) => p,
5153                    None => {
5154                        let h0 = e.zeros(0)?;
5155                        (e.matmul_pre(&mbits.shared_gate, zq, zd, &h0, 1)?,
5156                         e.matmul_pre(&mbits.shared_up, zq, zd, &h0, 1)?)
5157                    }
5158                }
5159            } else {
5160                // verify t 2..15: the batched mmvq twins consume the pre-quantized pair.
5161                let h0 = e.zeros(0)?;
5162                (e.matmul_pre(&mbits.shared_gate, zq, zd, &h0, t)?,
5163                 e.matmul_pre(&mbits.shared_up, zq, zd, &h0, t)?)
5164            }
5165        } else {
5166            let (zsh, _) = zsh_f32.as_ref().unwrap();
5167            (e.matmul(&mbits.shared_gate, zsh, t)?, e.matmul(&mbits.shared_up, zsh, t)?)
5168        };
5169        let mut act = e.uninit(t * n_ff)?;
5170        e.gelu_tanh_mul(&gate, &up, &mut act, t * n_ff)?;
5171        let mlp0 = e.matmul(&mbits.shared_down, &act, t)?;
5172        let crate::hybrid::Ffn::Moe(m) = &layer.ffn else { panic!("gemma4 layer not MoE") };
5173        let moe0 = match (&moe_q8, &zsh_f32) {
5174            (Some(mq), _) => self.gemma4_moe_q8(e, m, mbits, mq, &router_in, t)?,
5175            (None, Some((_, moe_in))) => self.gemma4_moe(e, m, mbits, moe_in, &router_in, t)?,
5176            _ => unreachable!(),
5177        };
5178        // post_ffw_norm_1(mlp0) + post_ffw_norm_2(moe0): one fused launch, per-row verbatim.
5179        let mut mlp = e.uninit(t * n_embd)?;
5180        let mut moe = e.uninit(t * n_embd)?;
5181        e.rms_norm2x(&mlp0, &moe0, mbits.post_ffw_norm_1.float_data(),
5182                     mbits.post_ffw_norm_2.float_data(), &mut mlp, &mut moe, n_embd, t, eps)?;
5183
5184        // combine: rms_norm(mlp + moe, post_ffw_norm) + attn_out, then the layer output scalar.
5185        // add+norm fused (add_rms_norm == add then rms_norm, kernel-check-pinned identity).
5186        let mut sum = e.uninit(t * n_embd)?;
5187        let mut sn = e.uninit(t * n_embd)?;
5188        e.add_rms_norm(&mlp, &moe, bits.post_ffw_norm.float_data(), &mut sum, &mut sn,
5189                       n_embd, t, eps)?;
5190        Ok((sn, attn_out2))
5191    }
5192
5193    /// tail_add with the next attn_norm emitted PRE-QUANTIZED q8_1 (decode/verify loops).
5194    fn gemma4_layer_tail_add_nq(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
5195                                cur: &CudaSlice<f32>, x: &CudaSlice<f32>, t: usize,
5196                                next_norm: Option<&CudaSlice<f32>>)
5197                                -> Result<(CudaSlice<f32>, Option<(CudaSlice<i8>, CudaSlice<f32>)>), Box<dyn std::error::Error>> {
5198        let n_embd = self.cfg.n_embd as usize;
5199        let bits = layer.gemma4.as_ref().unwrap();
5200        let (sn, attn_out) = self.gemma4_layer_tail_core(e, layer, cur, x, t)?;
5201        let mut xn = e.uninit(t * n_embd)?;
5202        match next_norm {
5203            Some(w) => {
5204                let pair = e.add_scale_rms_norm_q8_1(&sn, &attn_out, bits.layer_scale, w, &mut xn,
5205                                                     n_embd, t, self.cfg.rms_eps)?;
5206                Ok((xn, Some(pair)))
5207            }
5208            None => {
5209                e.add_scale(&sn, &attn_out, bits.layer_scale, &mut xn, t * n_embd)?;
5210                Ok((xn, None))
5211            }
5212        }
5213    }
5214
5215    /// gemma4 prefill: `last_only` = forward_last semantics (lm_head on the final row only).
5216    /// R4: final logits softcapped 30*tanh(l/30) on host (monotonic — argmax unaffected).
5217    fn gemma4_forward(&self, e: &Engine, tokens: &[u32], last_only: bool)
5218                      -> Result<Vec<f32>, Box<dyn std::error::Error>> {
5219        // E4B routes to its own forward regardless of the caller's entry point (forward /
5220        // forward_last / prime paths all funnel here for gemma4).
5221        if self.is_gemma4_e4b() { return self.gemma4_e4b_forward(e, tokens, last_only); }
5222        let n_embd = self.cfg.n_embd as usize;
5223        let t = tokens.len();
5224        let pos: Vec<i32> = (0..t as i32).collect();
5225        let pos_d = e.htod_i32(&pos)?;
5226
5227        let mut x = self.embed(e, tokens)?;
5228        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
5229        // MEMRA_GEMMA_PROBE=1: per-layer trunk stats (host rms + max of the LAST token row) —
5230        // the bring-up bisect vs llama-eval-callback node stats.
5231        let probe = std::env::var("MEMRA_GEMMA_PROBE").is_ok();
5232        let stat = |e: &Engine, x: &CudaSlice<f32>, tag: &str| -> Result<(), Box<dyn std::error::Error>> {
5233            let h = e.dtoh(x)?;
5234            let bad = h.iter().filter(|v| !v.is_finite()).count();
5235            let mx = h.iter().filter(|v| v.is_finite()).fold(0.0f32, |m, v| m.max(v.abs()));
5236            eprintln!("[gemma-probe] {tag}: tok0_first3={:?} bad={bad} max={mx:.3e}", &h[..3]);
5237            Ok(())
5238        };
5239        if probe { stat(e, &x, "embed")?; }
5240        for (il, layer) in self.layers.iter().enumerate() {
5241            x = self.gemma4_layer(e, il, layer, &x, &pos_d, t)?;
5242            if probe { stat(e, &x, &format!("L{il}"))?; }
5243        }
5244        let mut hn = e.zeros(t * n_embd)?;
5245        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, self.cfg.rms_eps)?;
5246        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
5247        let n_vocab = self.output.out_features();
5248        let logits = if last_only {
5249            let hv = e.view(&hn, t * n_embd);
5250            let last_row = hv.slice((t - 1) * n_embd..t * n_embd);
5251            let mut hlast = e.zeros(n_embd)?;
5252            e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
5253            let mut ld = e.matmul(&self.output, &hlast, 1)?;
5254            e.softcap(&mut ld, cap, n_vocab)?;
5255            self.gemma4_suppress(e, &mut ld, 1)?;
5256            e.dtoh(&ld)?
5257        } else {
5258            let mut ld = e.matmul(&self.output, &hn, t)?;
5259            e.softcap(&mut ld, cap, t * n_vocab)?;
5260            self.gemma4_suppress(e, &mut ld, t)?;
5261            e.dtoh(&ld)?
5262        };
5263        Ok(logits)
5264    }
5265
5266    /// gemma4 BATCHED PROMPT PRIME v0 (fresh cache only): the prefill graph over the whole
5267    /// prompt with each layer's post-rope K / weightless-normed V appended into the quantized
5268    /// KV cache (decode-append row math). Returns (last-row softcapped logits, h_seed = last
5269    /// pre-output_norm hidden, hiddens = full pre-output_norm stack [T, n_embd]).
5270    pub(crate) fn gemma4_prime(&self, e: &Engine, tokens: &[u32], cache: &mut Cache)
5271                               -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
5272        assert_eq!(cache.pos, 0, "gemma4 prime v0 is fresh-prompt only");
5273        let n_embd = self.cfg.n_embd as usize;
5274        let eps = self.cfg.rms_eps;
5275        let t = tokens.len();
5276        let pos: Vec<i32> = (0..t as i32).collect();
5277        let pos_d = e.htod_i32(&pos)?;
5278        let mut x = self.embed(e, tokens)?;
5279        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
5280        for (il, layer) in self.layers.iter().enumerate() {
5281            let mut h = e.zeros(t * n_embd)?;
5282            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
5283            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer not full-attn") };
5284            let o = self.gemma4_attn_prime(e, fa, il, &h, &pos_d, t, Some(cache))?;
5285            let mut cur = e.zeros(t * n_embd)?;
5286            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, t, eps)?;
5287            x = self.gemma4_layer_tail_add(e, layer, &cur, &x, t)?;
5288            self.dflash_tap(e, cache, il, &x, t)?;
5289        }
5290        cache.pos += t;
5291        let hiddens = e.clone_dtod(&x)?;
5292        let xv = e.view(&x, t * n_embd);
5293        let last_row = xv.slice((t - 1) * n_embd..t * n_embd);
5294        let mut h_seed = e.zeros(n_embd)?;
5295        e.copy_view_into(&mut h_seed, 0, &last_row, n_embd)?;
5296        let mut hn = e.uninit(n_embd)?;
5297        e.rms_norm(&h_seed, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
5298        let mut ld = e.matmul(&self.output, &hn, 1)?;
5299        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
5300        e.softcap(&mut ld, cap, self.output.out_features())?;
5301        self.gemma4_suppress(e, &mut ld, 1)?;
5302        let logits = e.dtoh(&ld)?;
5303        Ok((logits, h_seed, hiddens))
5304    }
5305
5306    /// gemma4 T=1 decode attention: per-layer geometry, quantized-KV append + fa_decode
5307    /// (vec kernels at hd 256, generic scalar at the globals' hd 512), weightless V-norm,
5308    /// dual rope, scale 1.0. Takes the attn-normed input PRE-QUANTIZED (the cross-layer
5309    /// fused norm emits q8 directly — the f32 h never materializes).
5310    fn gemma4_decode_attn(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
5311                          hq: &CudaSlice<i8>, hdq: &CudaSlice<f32>,
5312                          pos_d: &CudaSlice<i32>, cache: &mut Cache)
5313                          -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
5314        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
5315        let eps = self.cfg.rms_eps;
5316        let aux = self.gemma4_aux.as_ref().unwrap();
5317        let (hq, hdq) = (hq, hdq);
5318        let h0 = e.zeros(0)?;
5319        let h = &h0;
5320        let (q0, k0, v0) = if swa {
5321            match e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, &hq, &hdq)? {
5322                Some(t3) => t3,
5323                None => (e.matmul_pre(&fa.wq, &hq, &hdq, h, 1)?,
5324                         e.matmul_pre(&fa.wk, &hq, &hdq, h, 1)?,
5325                         e.matmul_pre(&fa.wv, &hq, &hdq, h, 1)?),
5326            }
5327        } else {
5328            let (q0, k0) = match e.matmul_q4_fused2(&fa.wq, &fa.wk, &hq, &hdq)? {
5329                Some(p) => p,
5330                None => (e.matmul_pre(&fa.wq, &hq, &hdq, h, 1)?,
5331                         e.matmul_pre(&fa.wk, &hq, &hdq, h, 1)?),
5332            };
5333            let v0 = e.clone_dtod(&k0)?;
5334            (q0, k0, v0)
5335        };
5336        let mut q = e.uninit(nh * hd)?;
5337        let mut k = e.uninit(nkv * hd)?;
5338        let mut v = e.uninit(nkv * hd)?;
5339        // E4B wave-3 fold, m=1 completion (2026-07-23): the rows arms took this fold at
5340        // 550fcfa5; the decode-step trio kept 2 launches/layer it doesn't need.
5341        let ff = if swa { None } else {
5342            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
5343        };
5344        e.rms_norm_qkv_rope(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
5345                            &aux.ones, &mut q, &mut k, &mut v, hd, nh, nkv,
5346                            pos_d, nh, nkv, base, 1.0, ff, eps)?;
5347        let kvl = cache.kv[il].as_mut().unwrap();
5348        e.append_kv_quantized(&k, &v, &mut kvl.k, &mut kvl.v, kvl.len,
5349                              kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes, (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()))?;
5350        kvl.len += 1;
5351        // R6 decode: SWA layers attend only the last `sliding_window` keys — a token-aligned
5352        // VIEW OFFSET into the quantized cache (keys carry absolute rope; the mask is purely
5353        // positional). Globals attend the full history.
5354        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
5355        let mut attn = e.uninit(nh * hd)?;
5356        // global layers: SAME hd512 rows twin as verify with t=1 (parity law).
5357        if !swa && hd == 512 && kvl.len >= crate::fa512_min_tkv()
5358            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
5359            let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
5360            let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
5361            // device-len: eager syncs the counter (async arg-store), dc keeps it live.
5362            let base = kvl.len as i32;
5363            e.i32_set_k(&mut kvl.len_d, base)?;
5364            e.fa_decode_rows(&q, &kp, &vp, &mut attn, hd, nh, nkv, kvl.len - 1, 1, scale,
5365                             kvl.k_tok_bytes, kvl.v_tok_bytes, Some((&kvl.len_d, -1)), false,
5366                             false, None)?;
5367            return Ok(e.matmul(&fa.wo, &attn, 1)?);
5368        }
5369        // windowed regime: SAME rows_w kernel as verify with t=1 (parity law — see verify_attn).
5370        if swa && kvl.len > win && hd == 256
5371            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
5372            let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
5373            let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
5374            let base = kvl.len as i32;
5375            e.i32_set_k(&mut kvl.len_d, base)?;
5376            e.fa_decode_rows_w(&q, &kp, &vp, &mut attn, hd, nh, nkv, &kvl.len_d, -1, 1, scale,
5377                               win, kvl.k_tok_bytes, kvl.v_tok_bytes, None)?;
5378            return Ok(e.matmul(&fa.wo, &attn, 1)?);
5379        }
5380        let (off_tok, t_kv) = if swa && kvl.len > win { (kvl.len - win, win) } else { (0, kvl.len) };
5381        let k_view = e.view_u8_range(&kvl.k, off_tok * kvl.k_tok_bytes,
5382                                     (off_tok + t_kv) * kvl.k_tok_bytes);
5383        let v_view = e.view_u8_range(&kvl.v, off_tok * kvl.v_tok_bytes,
5384                                     (off_tok + t_kv) * kvl.v_tok_bytes);
5385        e.fa_decode_kvmod(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, t_kv, scale,
5386                    kvl.k_tok_bytes, kvl.v_tok_bytes, swa && crate::Engine::wkv_on())?;
5387        Ok(e.matmul(&fa.wo, &attn, 1)?)
5388    }
5389
5390    /// gemma4 DEVICE-COUNTER decode step (graph arc): token id + rope pos + KV lengths live in
5391    /// device counters; ZERO varying host kernel args. `cap_bucket_max` = Some(bucket) for graph
5392    /// capture (host mirrors untouched, n_splits from bucket, full-buffer KV views) / None for
5393    /// the eager-dc gate path (host mirrors advanced, live geometry — bit-identical target =
5394    /// gemma4_decode_step_h's token stream). V1 scope: t_kv <= sliding_window (no window views
5395    /// in-graph; the driver gates).
5396    #[allow(clippy::too_many_arguments)]
5397    pub fn gemma4_decode_step_dc(&self, e: &Engine, token_d: &CudaSlice<u32>,
5398                                 pos_d: &mut CudaSlice<i32>, embd_gpu: &CudaSlice<u8>,
5399                                 embd_qt: i32, embd_rb: usize, cache: &mut Cache,
5400                                 n_vocab: usize, cap_bucket_max: Option<(usize, usize)>)
5401                                 -> Result<CudaSlice<u32>, Box<dyn std::error::Error>> {
5402        let mut tok_out = e.stream().alloc_zeros::<u32>(1)?;
5403        self.gemma4_decode_step_dc_into(e, token_d, pos_d, embd_gpu, embd_qt, embd_rb, cache,
5404                                        n_vocab, cap_bucket_max, &mut tok_out)?;
5405        Ok(tok_out)
5406    }
5407
5408    /// CAPTURE body: argmax lands in the PERSISTENT `tok_out` (same buffer = same address on
5409    /// every replay; pass `token_d` itself for the self-feeding graph loop).
5410    #[allow(clippy::too_many_arguments)]
5411    pub fn gemma4_decode_step_dc_into(&self, e: &Engine, token_d: &CudaSlice<u32>,
5412                                      pos_d: &mut CudaSlice<i32>, embd_gpu: &CudaSlice<u8>,
5413                                      embd_qt: i32, embd_rb: usize, cache: &mut Cache,
5414                                      n_vocab: usize, cap_bucket_max: Option<(usize, usize)>,
5415                                      tok_out: &mut CudaSlice<u32>)
5416                                      -> Result<(), Box<dyn std::error::Error>> {
5417        let n_embd = self.cfg.n_embd as usize;
5418        let eps = self.cfg.rms_eps;
5419        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
5420        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
5421        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
5422        let n_layers = self.layers.len();
5423        for (il, layer) in self.layers.iter().enumerate() {
5424            let (hq, hdq) = match h_carry.take() {
5425                Some(p) => p,
5426                None => e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, 1, eps)?,
5427            };
5428            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
5429            let o = self.gemma4_decode_attn_dc(e, fa, il, &hq, &hdq, pos_d, cache, cap_bucket_max)?;
5430            let mut cur = e.uninit(n_embd)?;
5431            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, 1, eps)?;
5432            let next_norm = if il + 1 < n_layers {
5433                Some(self.layers[il + 1].attn_norm.float_data())
5434            } else { None };
5435            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
5436            x = xn;
5437            h_carry = hn;
5438        }
5439        let mut hn = e.uninit(n_embd)?;
5440        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
5441        let mut logits = e.matmul(&self.output, &hn, 1)?;
5442        self.gemma4_suppress(e, &mut logits, 1)?;   // cap skipped (monotonic); the mask is not
5443        e.argmax_token_device_into(&logits, tok_out, n_vocab)?;
5444        e.inc_seqlen(pos_d)?;
5445        if cap_bucket_max.is_none() { cache.pos += 1; }
5446        Ok(())
5447    }
5448
5449    /// Persistent transient slots for the ALLOC-FREE captured dc step (the graph door):
5450    /// every buffer the step produces per token lives here, allocated ONCE pre-capture, so
5451    /// the captured graph carries zero cuMemAllocAsync/Free nodes (the 226us/launch tax,
5452    /// osrt 2026-07-23). Sized for the model's max per-layer shapes.
5453
5454    /// Build the slot set (call OUTSIDE any capture).
5455    pub fn g4_dc_slots(&self, e: &Engine) -> Result<G4DcSlots, Box<dyn std::error::Error>> {
5456        let n_embd = self.cfg.n_embd as usize;
5457        let n_vocab = self.output.out_features();
5458        let n_layers = self.layers.len();
5459        let (mut qmax, mut kvmax, mut ffmax) = (0usize, 0usize, 0usize);
5460        for il in 0..n_layers {
5461            let (hd, nkv, nh, _b, _s, _w) = self.gemma4_geom(il);
5462            qmax = qmax.max(nh * hd);
5463            kvmax = kvmax.max(nkv * hd);
5464            if let crate::hybrid::Ffn::Dense { ffn_gate, .. } = &self.layers[il].ffn {
5465                ffmax = ffmax.max(ffn_gate.out_features());
5466            }
5467        }
5468        Ok(G4DcSlots {
5469            x: e.uninit(n_embd)?, xn: e.uninit(n_embd)?, cur: e.uninit(n_embd)?,
5470            hq: e.alloc_i8_uninit(n_embd)?, hd_: e.uninit(n_embd / 32)?,
5471            q0: e.uninit(qmax)?, k0: e.uninit(kvmax)?, v0: e.uninit(kvmax)?,
5472            q: e.uninit(qmax)?, k: e.uninit(kvmax)?, v: e.uninit(kvmax)?,
5473            attn: e.uninit(qmax)?, o: e.uninit(n_embd)?,
5474            attn_out: e.uninit(n_embd)?, zsh: e.uninit(n_embd)?,
5475            // zq/zd feed the wo matvec (nh*hd rows — 4096 on hd512 globals > n_embd),
5476            // the ffn entry (n_embd) and the lm_head (n_embd): size for the max.
5477            zq: e.alloc_i8_uninit(n_embd.max(qmax))?, zd: e.uninit(n_embd.max(qmax) / 32)?,
5478            gate: e.uninit(ffmax)?, up: e.uninit(ffmax)?,
5479            act: e.uninit(ffmax)?, actq: e.alloc_i8_uninit(ffmax)?, actd: e.uninit(ffmax / 32)?,
5480            f0: e.uninit(n_embd)?, sn: e.uninit(n_embd)?,
5481            hn: e.uninit(n_embd)?, logits: e.uninit(n_vocab)?,
5482        })
5483    }
5484
5485    /// m=1 pre-quantized matvec into a slot — mirrors matmul_pre's m=1 mmvq route exactly
5486    /// (rp4-mirror bytes; mmvq_supports guaranteed for gemma4 q4_0/q6_K).
5487    fn g4_matvec_m1_into(&self, e: &Engine, w: &crate::model::GpuTensor,
5488                         aq: &CudaSlice<i8>, ad: &CudaSlice<f32>, y: &mut CudaSlice<f32>)
5489                         -> Result<(), Box<dyn std::error::Error>> {
5490        use crate::model::GpuTensor;
5491        let (bytes, qtype, row_bytes, scale, rp) = match w {
5492            GpuTensor::Quant { bytes, qtype, row_bytes, scale, rp, .. } =>
5493                (bytes, *qtype, *row_bytes, *scale, *rp),
5494            _ => return Err("g4_matvec_m1_into: non-quant tensor".into()),
5495        };
5496        let (mbytes, mrp) = match w {
5497            GpuTensor::Quant { rp4: Some(m4), .. } => (m4, true),
5498            _ => (bytes, rp),
5499        };
5500        e.qmatvec_mmvq_into(mbytes, aq, ad, 1, w.in_features(), w.out_features(),
5501                            qtype, row_bytes, scale, mrp, y)
5502    }
5503
5504    /// ALLOC-FREE dc step (capture body): kernel-for-kernel mirror of
5505    /// `gemma4_decode_step_dc_into` at t=1 with every transient slot-fed. Dense gemma4 only
5506    /// (12B/31B; uniform q4_0 trunk guarantees the fused2/3 arms).
5507    #[allow(clippy::too_many_arguments)]
5508    pub fn gemma4_decode_step_dc_slotted(&self, e: &Engine, token_d: &CudaSlice<u32>,
5509                                         pos_d: &mut CudaSlice<i32>, embd_gpu: &CudaSlice<u8>,
5510                                         embd_qt: i32, embd_rb: usize, cache: &mut Cache,
5511                                         n_vocab: usize, cap_bucket_max: Option<(usize, usize)>,
5512                                         sl: &mut G4DcSlots, tok_out: &mut CudaSlice<u32>,
5513                                         ring: Option<(&mut CudaSlice<u32>, usize)>)
5514                                         -> Result<(), Box<dyn std::error::Error>> {
5515        let n_embd = self.cfg.n_embd as usize;
5516        let eps = self.cfg.rms_eps;
5517        e.embed_gather_device_into(embd_gpu, token_d, &mut sl.x, n_embd, embd_qt, embd_rb)?;
5518        e.scale_inplace(&mut sl.x, (n_embd as f32).sqrt(), n_embd)?;
5519        let n_layers = self.layers.len();
5520        let mut has_carry = false;
5521        for il in 0..n_layers {
5522            if !has_carry {
5523                e.rms_norm_q8_1_into(&sl.x, self.layers[il].attn_norm.float_data(), n_embd, 1,
5524                                     eps, &mut sl.hq, &mut sl.hd_)?;
5525            }
5526            has_carry = true;
5527            let layer = &self.layers[il];
5528            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
5529            self.gemma4_decode_attn_dc_slotted(e, fa, il, pos_d, cache, cap_bucket_max, sl)?;
5530            e.rms_norm(&sl.o, layer.post_attn_norm.float_data(), &mut sl.cur, n_embd, 1, eps)?;
5531            let next_norm = if il + 1 < n_layers {
5532                Some(self.layers[il + 1].attn_norm.float_data())
5533            } else { None };
5534            self.gemma4_layer_tail_slotted(e, layer, next_norm, sl)?;
5535            std::mem::swap(&mut sl.x, &mut sl.xn);
5536        }
5537        e.rms_norm(&sl.x, self.output_norm.float_data(), &mut sl.hn, n_embd, 1, eps)?;
5538        e.quantize_q8_1_into(&sl.hn, 1, n_embd, &mut sl.zq, &mut sl.zd)?;
5539        // lm_head via the same m=1 mmvq route as matmul (q6_K on the gemma4 family).
5540        {
5541            let (zq, zd) = (&sl.zq, &sl.zd);
5542            let zq = unsafe { &*(zq as *const CudaSlice<i8>) };
5543            let zd = unsafe { &*(zd as *const CudaSlice<f32>) };
5544            self.g4_matvec_m1_into(e, &self.output, zq, zd, &mut sl.logits)?;
5545        }
5546        self.gemma4_suppress(e, &mut sl.logits, 1)?;
5547        e.argmax_token_device_into(&sl.logits, tok_out, n_vocab)?;
5548        if let Some((ring, base)) = ring {
5549            // in-graph token parking: slot = (pos - base) % ring.len(); the door drains
5550            // with ONE sync per chunk instead of a per-token dtoh (the launch-serialization
5551            // fix — llama's 885us/launch overlaps GPU work because nothing waits per token).
5552            e.plain_tok_ring(tok_out, pos_d, base, ring)?;
5553        }
5554        e.inc_seqlen(pos_d)?;
5555        if cap_bucket_max.is_none() { cache.pos += 1; }
5556        Ok(())
5557    }
5558
5559    /// Slot-fed dc attention: mirrors gemma4_decode_attn_dc's CAPTURE arm kernel-for-kernel.
5560    #[allow(clippy::too_many_arguments)]
5561    fn gemma4_decode_attn_dc_slotted(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer,
5562                                     il: usize, pos_d: &CudaSlice<i32>, cache: &mut Cache,
5563                                     cap_bucket_max: Option<(usize, usize)>, sl: &mut G4DcSlots)
5564                                     -> Result<(), Box<dyn std::error::Error>> {
5565        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
5566        let eps = self.cfg.rms_eps;
5567        let aux = self.gemma4_aux.as_ref().unwrap();
5568        {
5569            let hq = unsafe { &*(&sl.hq as *const CudaSlice<i8>) };
5570            let hdq = unsafe { &*(&sl.hd_ as *const CudaSlice<f32>) };
5571            if swa {
5572                if !e.matmul_q4_fused3_into(&fa.wq, &fa.wk, &fa.wv, hq, hdq,
5573                                            &mut sl.q0, &mut sl.k0, &mut sl.v0)? {
5574                    return Err("slotted step: fused3 unavailable (non-uniform trunk)".into());
5575                }
5576            } else {
5577                if !e.matmul_q4_fused2_into(&fa.wq, &fa.wk, hq, hdq, &mut sl.q0, &mut sl.k0)? {
5578                    return Err("slotted step: fused2 unavailable".into());
5579                }
5580                let k0r = unsafe { &*(&sl.k0 as *const CudaSlice<f32>) };
5581                e.copy_into(&mut sl.v0, 0, k0r, nkv * hd)?;
5582            }
5583        }
5584        // E4B wave-3 fold, m=1 completion (2026-07-23) — MUST mirror the dc_into arm
5585        // kernel-for-kernel (graph stream-identity gate).
5586        let ff = if swa { None } else {
5587            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
5588        };
5589        let kvl = cache.kv[il].as_mut().unwrap();
5590        let kv_fp8 = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
5591        if crate::Engine::qkv_append_on() {
5592            // append fold (2026-07-23): mirrors dc_into.
5593            e.rms_norm_qkv_rope_append_dc(&sl.q0, &sl.k0, &sl.v0, fa.q_norm.float_data(),
5594                fa.k_norm.float_data(), &aux.ones, &mut sl.q, &mut sl.k, &mut sl.v, hd, nh, nkv,
5595                pos_d, nh, nkv, base, 1.0, ff, eps,
5596                &mut kvl.k, &mut kvl.v, &kvl.len_d, kvl.k_tok_bytes, kvl.v_tok_bytes, kv_fp8)?;
5597        } else {
5598            e.rms_norm_qkv_rope(&sl.q0, &sl.k0, &sl.v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
5599                                &aux.ones, &mut sl.q, &mut sl.k, &mut sl.v, hd, nh, nkv,
5600                                pos_d, nh, nkv, base, 1.0, ff, eps)?;
5601            e.append_kv_quantized_dc(&sl.k, &sl.v, &mut kvl.k, &mut kvl.v, &kvl.len_d,
5602                                     kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes,
5603                                     kv_fp8)?;
5604        }
5605        e.inc_seqlen(&mut kvl.len_d)?;
5606        let (b_swa, b_glob) = cap_bucket_max.expect("slotted step is capture-only");
5607        let k_view = e.view_u8(&kvl.k, kvl.k.len());
5608        let v_view = e.view_u8(&kvl.v, kvl.v.len());
5609        let rows_on = std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0");
5610        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
5611        // combine-q8 emit (wave-5b m=1 port): the rows arms quantize inside the combine —
5612        // the standalone quantize launch runs only on the non-rows fallback. MUST mirror
5613        // the dc_into arm branch-for-branch (stream gate).
5614        let mut fa_q8 = false;
5615        if !swa && hd == 512 && b_glob >= crate::fa512_min_tkv() && rows_on {
5616            e.fa_decode_rows(&sl.q, &k_view, &v_view, &mut sl.attn, hd, nh, nkv, b_glob - 1,
5617                             1, scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
5618                             Some((&kvl.len_d, -1)), false, false,
5619                             Some((&mut sl.zq, &mut sl.zd)))?;
5620            fa_q8 = true;
5621        } else if swa && b_swa > win && hd == 256 && rows_on {
5622            e.fa_decode_rows_w(&sl.q, &k_view, &v_view, &mut sl.attn, hd, nh, nkv,
5623                               &kvl.len_d, -1, 1, scale, win,
5624                               kvl.k_tok_bytes, kvl.v_tok_bytes,
5625                               Some((&mut sl.zq, &mut sl.zd)))?;
5626            fa_q8 = true;
5627        } else {
5628            let b = if swa { b_swa } else { b_glob };
5629            e.fa_decode_dc(&sl.q, &k_view, &v_view, &mut sl.attn, hd, nh, nkv, &kvl.len_d, b,
5630                           scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
5631                           swa && crate::Engine::wkv_on())?;
5632        }
5633        if !fa_q8 {
5634            let aq = unsafe { &*(&sl.attn as *const CudaSlice<f32>) };
5635            e.quantize_q8_1_into(aq, 1, nh * hd, &mut sl.zq, &mut sl.zd)?;
5636        }
5637        {
5638            let zq = unsafe { &*(&sl.zq as *const CudaSlice<i8>) };
5639            let zd = unsafe { &*(&sl.zd as *const CudaSlice<f32>) };
5640            self.g4_matvec_m1_into(e, &fa.wo, zq, zd, &mut sl.o)?;
5641        }
5642        Ok(())
5643    }
5644
5645    /// Slot-fed dense layer tail: mirrors gemma4_layer_tail_core (t=1, no pre-norm fold) +
5646    /// tail_add_nq kernel-for-kernel; the next layer's (hq, hd_) carry lands in the slots.
5647    fn gemma4_layer_tail_slotted(&self, e: &Engine, layer: &crate::hybrid::HybridLayer,
5648                                 next_norm: Option<&CudaSlice<f32>>, sl: &mut G4DcSlots)
5649                                 -> Result<(), Box<dyn std::error::Error>> {
5650        let n_embd = self.cfg.n_embd as usize;
5651        let eps = self.cfg.rms_eps;
5652        let bits = layer.gemma4.as_ref().unwrap();
5653        let crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } = &layer.ffn
5654        else { return Err("slotted tail: dense ffn only".into()) };
5655        e.add_rms_norm(&sl.cur, &sl.x, bits.ffn_norm.float_data(), &mut sl.attn_out,
5656                       &mut sl.zsh, n_embd, 1, eps)?;
5657        let n_ff = ffn_gate.out_features();
5658        {
5659            let zshr = unsafe { &*(&sl.zsh as *const CudaSlice<f32>) };
5660            e.quantize_q8_1_into(zshr, 1, n_embd, &mut sl.zq, &mut sl.zd)?;
5661        }
5662        {
5663            let zq = unsafe { &*(&sl.zq as *const CudaSlice<i8>) };
5664            let zd = unsafe { &*(&sl.zd as *const CudaSlice<f32>) };
5665            if !e.matmul_q4_fused2_into(ffn_gate, ffn_up, zq, zd, &mut sl.gate, &mut sl.up)? {
5666                return Err("slotted tail: ffn fused2 unavailable".into());
5667            }
5668        }
5669        debug_assert!(e.uses_q8_1_fast(ffn_down));
5670        {
5671            let upr = unsafe { &*(&sl.up as *const CudaSlice<f32>) };
5672            let upv = e.view(upr, n_ff);
5673            let up_all = upv.slice(0..n_ff);
5674            let gr = unsafe { &*(&sl.gate as *const CudaSlice<f32>) };
5675            e.gelu_tanh_mul_q8_1_into(gr, &up_all, &mut sl.act, n_ff, 1,
5676                                      &mut sl.actq, &mut sl.actd)?;
5677        }
5678        {
5679            let aq = unsafe { &*(&sl.actq as *const CudaSlice<i8>) };
5680            let ad = unsafe { &*(&sl.actd as *const CudaSlice<f32>) };
5681            self.g4_matvec_m1_into(e, ffn_down, aq, ad, &mut sl.f0)?;
5682        }
5683        e.rms_norm(&sl.f0, bits.post_ffw_norm.float_data(), &mut sl.sn, n_embd, 1, eps)?;
5684        match next_norm {
5685            Some(w) => {
5686                e.add_scale_rms_norm_q8_1_into(&sl.sn, &sl.attn_out, bits.layer_scale, w,
5687                                               &mut sl.xn, n_embd, 1, eps,
5688                                               &mut sl.hq, &mut sl.hd_)?;
5689            }
5690            None => {
5691                e.add_scale(&sl.sn, &sl.attn_out, bits.layer_scale, &mut sl.xn, n_embd)?;
5692            }
5693        }
5694        Ok(())
5695    }
5696
5697    /// dc attention: same math as gemma4_decode_attn, KV slot/lengths from device counters.
5698    #[allow(clippy::too_many_arguments)]
5699    fn gemma4_decode_attn_dc(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
5700                             hq: &CudaSlice<i8>, hdq: &CudaSlice<f32>,
5701                             pos_d: &CudaSlice<i32>, cache: &mut Cache,
5702                             cap_bucket_max: Option<(usize, usize)>)
5703                             -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
5704        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
5705        let eps = self.cfg.rms_eps;
5706        let aux = self.gemma4_aux.as_ref().unwrap();
5707        let (q0, k0, v0) = if swa {
5708            match e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hdq)? {
5709                Some(t3) => t3,
5710                None => {
5711                    let h0 = e.zeros(0)?;
5712                    (e.matmul_pre(&fa.wq, hq, hdq, &h0, 1)?,
5713                     e.matmul_pre(&fa.wk, hq, hdq, &h0, 1)?,
5714                     e.matmul_pre(&fa.wv, hq, hdq, &h0, 1)?)
5715                }
5716            }
5717        } else {
5718            let (q0, k0) = match e.matmul_q4_fused2(&fa.wq, &fa.wk, hq, hdq)? {
5719                Some(p) => p,
5720                None => {
5721                    let h0 = e.zeros(0)?;
5722                    (e.matmul_pre(&fa.wq, hq, hdq, &h0, 1)?,
5723                     e.matmul_pre(&fa.wk, hq, hdq, &h0, 1)?)
5724                }
5725            };
5726            let v0 = e.clone_dtod(&k0)?;
5727            (q0, k0, v0)
5728        };
5729        let mut q = e.uninit(nh * hd)?;
5730        let mut k = e.uninit(nkv * hd)?;
5731        let mut v = e.uninit(nkv * hd)?;
5732        // E4B wave-3 fold, m=1 completion (2026-07-23): mirrored by the slotted arm.
5733        let ff = if swa { None } else {
5734            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
5735        };
5736        let kvl = cache.kv[il].as_mut().unwrap();
5737        let kv_fp8 = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
5738        if crate::Engine::qkv_append_on() {
5739            // append fold (2026-07-23): norm+rope+cache-append in ONE launch.
5740            e.rms_norm_qkv_rope_append_dc(&q0, &k0, &v0, fa.q_norm.float_data(),
5741                fa.k_norm.float_data(), &aux.ones, &mut q, &mut k, &mut v, hd, nh, nkv,
5742                pos_d, nh, nkv, base, 1.0, ff, eps,
5743                &mut kvl.k, &mut kvl.v, &kvl.len_d, kvl.k_tok_bytes, kvl.v_tok_bytes, kv_fp8)?;
5744        } else {
5745            e.rms_norm_qkv_rope(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
5746                                &aux.ones, &mut q, &mut k, &mut v, hd, nh, nkv,
5747                                pos_d, nh, nkv, base, 1.0, ff, eps)?;
5748            e.append_kv_quantized_dc(&k, &v, &mut kvl.k, &mut kvl.v, &kvl.len_d,
5749                                     kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes, kv_fp8)?;
5750        }
5751        e.inc_seqlen(&mut kvl.len_d)?;
5752        let mut attn = e.uninit(nh * hd)?;
5753        // combine-q8 emit carrier (wave-5b m=1 port): rows arms fill this; the tail then
5754        // rides g4_matvec_m1_into instead of matmul's internal quantize.
5755        let mut fa_q8: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
5756        // Weight prefetch NOT wired here (26B/31B/qwen probes 2026-07-13: 26B flat
5757        // 196.6/196.0 vs 196.4/196.1 — MoE ffn dilutes wo; 31B −0.2% — dense decode sits
5758        // at the DRAM wall, no idle window to front-load into). E4B keeps the arm
5759        // (gemma4_e4b_attn, +0.65% valid window).
5760        match cap_bucket_max {
5761            None => {
5762                // dc-EAGER: host knows the length — R6 window views EXACTLY like the eager
5763                // decode (SWA layers attend the last `sliding_window` keys); the device
5764                // counters carry only the append slot + the graph seam.
5765                kvl.len += 1;
5766                let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
5767                if !swa && hd == 512 && kvl.len >= crate::fa512_min_tkv()
5768                    && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
5769                    // global layers: SAME hd512 rows twin as verify, t=1 (parity law).
5770                    // dc: len_d is live (inc_seqlen) — device-len rides it, plus=-1.
5771                    let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
5772                    let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
5773                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
5774                    e.fa_decode_rows(&q, &kp, &vp, &mut attn, hd, nh, nkv, kvl.len - 1, 1,
5775                                     scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
5776                                     Some((&kvl.len_d, -1)), false, false,
5777                                     Some((&mut aq8, &mut ad8)))?;
5778                    fa_q8 = Some((aq8, ad8));
5779                } else if swa && kvl.len > win && hd == 256
5780                    && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
5781                    // windowed regime: SAME rows_w kernel as verify, t=1 (parity law).
5782                    let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
5783                    let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
5784                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
5785                    e.fa_decode_rows_w(&q, &kp, &vp, &mut attn, hd, nh, nkv, &kvl.len_d, -1,
5786                                       1, scale, win, kvl.k_tok_bytes, kvl.v_tok_bytes,
5787                                       Some((&mut aq8, &mut ad8)))?;
5788                    fa_q8 = Some((aq8, ad8));
5789                } else {
5790                    let (off_tok, t_kv) = if swa && kvl.len > win { (kvl.len - win, win) }
5791                                          else { (0, kvl.len) };
5792                    let k_view = e.view_u8_range(&kvl.k, off_tok * kvl.k_tok_bytes,
5793                                                 (off_tok + t_kv) * kvl.k_tok_bytes);
5794                    let v_view = e.view_u8_range(&kvl.v, off_tok * kvl.v_tok_bytes,
5795                                                 (off_tok + t_kv) * kvl.v_tok_bytes);
5796                    e.fa_decode_kvmod(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, t_kv, scale,
5797                                kvl.k_tok_bytes, kvl.v_tok_bytes, swa && crate::Engine::wkv_on())?;
5798                }
5799            }
5800            Some((b_swa, b_glob)) => {
5801                // capture: full-buffer views + device length. PARITY LAW port (graph arc step
5802                // 3): the SAME regime branches as dc-eager, pinned per arm — b_swa is the
5803                // exact-key t_kv for the dc family (n_splits constant per bucket), b_glob is
5804                // the RUNG max for the rows family (kernels derive per-replay splits from
5805                // kvl.len_d; grid/partials sized for the rung end, excess splits exit).
5806                let k_view = e.view_u8(&kvl.k, kvl.k.len());
5807                let v_view = e.view_u8(&kvl.v, kvl.v.len());
5808                let rows_on = std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0");
5809                let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
5810                if !swa && hd == 512 && b_glob >= crate::fa512_min_tkv() && rows_on {
5811                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
5812                    e.fa_decode_rows(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, b_glob - 1,
5813                                     1, scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
5814                                     Some((&kvl.len_d, -1)), false, false,
5815                                     Some((&mut aq8, &mut ad8)))?;
5816                    fa_q8 = Some((aq8, ad8));
5817                } else if swa && b_swa > win && hd == 256 && rows_on {
5818                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
5819                    e.fa_decode_rows_w(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
5820                                       &kvl.len_d, -1, 1, scale, win,
5821                                       kvl.k_tok_bytes, kvl.v_tok_bytes,
5822                                       Some((&mut aq8, &mut ad8)))?;
5823                    fa_q8 = Some((aq8, ad8));
5824                } else {
5825                    let b = if swa { b_swa } else { b_glob };
5826                    e.fa_decode_dc(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, &kvl.len_d, b,
5827                                   scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
5828                                   swa && crate::Engine::wkv_on())?;
5829                }
5830            }
5831        }
5832        // combine-q8 emit (wave-5b m=1 port): the rows arms produced the wo activation pair
5833        // in-combine — ride the slotted arm's exact matvec route (parity by construction).
5834        if let Some((aq8, ad8)) = fa_q8 {
5835            let mut y = e.uninit(fa.wo.out_features())?;
5836            self.g4_matvec_m1_into(e, &fa.wo, &aq8, &ad8, &mut y)?;
5837            return Ok(y);
5838        }
5839        Ok(e.matmul(&fa.wo, &attn, 1)?)
5840    }
5841
5842    /// gemma4 GRAPH-REPLAY greedy loop: per (swa-key, global-key) fa bucket, capture ONE full
5843    /// dc step (self-feeding: argmax writes token_d in-graph) and replay it — one graph launch
5844    /// per token, one 4B dtoh. V1 scope: whole generation under the sliding window (no window
5845    /// views in-graph); caller gates and falls back to the dc-eager loop.
5846    pub fn gemma4_generate_graph(&self, e: &Engine, prompt_pos: usize, first_token: u32,
5847                                 cache: &mut Cache, max_new: usize, eos: &[u32],
5848                                 mut on_token: impl FnMut(u32) -> bool)
5849                                 -> Result<(Vec<u32>, crate::decode::StopReason), Box<dyn std::error::Error>> {
5850        if self.is_gemma4_e4b() {
5851            return Err("E4B graph serving is unwired (HANDOVER-E4B.md) — dc-eager is the serving arm".into());
5852        }
5853        use crate::decode::StopReason;
5854        let n_vocab = self.output.out_features();
5855        let n_embd = self.cfg.n_embd as usize;
5856        let embd_gpu = self.embd_gpu.get_or_init(|| {
5857            e.upload_u8(&self.embd.raw).expect("embed table upload")
5858        });
5859        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
5860        for kvl in cache.kv.iter_mut().flatten() {
5861            e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
5862        }
5863        let mut token_d = e.stream().clone_htod(&[first_token])?;
5864        let mut pos_d = e.htod_i32(&[prompt_pos as i32])?;
5865        let g4 = self.cfg.gemma4.as_ref().unwrap();
5866        let (hd_s, hd_g) = (g4.key_length_swa as usize, g4.key_length_global as usize);
5867        // per-layer nkv: swa vs global counts from the pattern (uniform within class).
5868        let nkv_s = g4.head_count_kv.iter().zip(g4.swa_pattern.iter())
5869            .find(|p| *p.1).map(|p| *p.0 as usize).unwrap_or(8);
5870        let nkv_g = g4.head_count_kv.iter().zip(g4.swa_pattern.iter())
5871            .find(|p| !*p.1).map(|p| *p.0 as usize).unwrap_or(2);
5872        let mut graphs: std::collections::HashMap<((bool, usize), (bool, usize), bool, bool),
5873                                                  (cudarc::driver::CudaGraph,
5874                                                   Vec<Box<dyn std::any::Any + Send>>)> = Default::default();
5875        // ALLOC-FREE capture: persistent transient slots — the captured graph carries zero
5876        // mem nodes (the 226us/launch tax). Slots must outlive every cached graph.
5877        let mut slots = self.g4_dc_slots(e)?;
5878        // Chunked replay ring: tokens park on-device; ONE drain sync per chunk. ring_base is
5879        // baked at the door entry (the modulo keeps every capture valid indefinitely).
5880        const RING: usize = 64;
5881        // DRAIN pinned at 1 (2026-07-23): relaunching the SAME graph exec before its prior
5882        // launch completes is ILLEGAL (chunk=4 -> ILLEGAL_ADDRESS; chunk=1 clean). Pipelining
5883        // needs alternating exec instances — and the measured payoff was ~0 (the ~200us
5884        // cuGraphLaunch host cost already overlaps its own launch's GPU work; llama pays
5885        // 885us/launch the same way). MEMRA_GRAPH_DRAIN raises it only for experiments.
5886        const DRAIN: usize = 1;
5887        let mut ring = e.stream().alloc_zeros::<u32>(RING)?;
5888        let ring_base = prompt_pos;
5889        let mut out = Vec::with_capacity(max_new);
5890        let mut reason = StopReason::MaxNew;
5891        let mut next = first_token;
5892        let mut captures = 0usize;
5893        for _ in 0..max_new {
5894            out.push(next);
5895            if eos.contains(&next) { reason = StopReason::Eos; break; }
5896            if !on_token(next) { reason = StopReason::Callback; break; }
5897            let t_kv = cache.pos + 1;
5898            // Bucket key per ARM (graph arc step 3):
5899            //  - swa component: dc family under the window (exact (fa_vec, n_splits) key —
5900            //    n_splits must be constant per bucket); rows_w above it (window-constant, so
5901            //    the component collapses to a single marker).
5902            //  - global component: dc family under the fa512 floor (exact key); rows_dpl16
5903            //    at/above it — the kernel derives splits from len_d per replay, so buckets
5904            //    are power-of-2 RUNGS (one capture per doubling; grid sized for the rung end).
5905            let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
5906            let f512 = crate::fa512_min_tkv();
5907            let key_s = if t_kv > win { (true, usize::MAX) }
5908                        else { e.fa_bucket_key(t_kv, hd_s, nkv_s, crate::Engine::wkv_on()) };
5909            let (key_g, rung_end) = if t_kv >= f512 {
5910                // strict upper bound: the rung must ROLL at exact powers (t_kv==1024 starts
5911                // the [1024,2048) bucket) — sizing covers every replayed T_kv < end.
5912                let end = (t_kv + 1).next_power_of_two().max(f512 * 2);
5913                ((true, end), end)
5914            } else { (e.fa_bucket_key(t_kv, hd_g, nkv_g, false), t_kv) };
5915            let key = (key_s, key_g, t_kv >= f512, t_kv > win);
5916            if !graphs.contains_key(&key) {
5917                let bucket_max = (t_kv, rung_end);
5918                // snapshot device+host state (the 3 capture-warmup runs leave no residue).
5919                let snap = cache.snapshot(e)?;
5920                let pos_save = e.dtoh_i32_one(&pos_d)?;
5921                let len_save: Vec<Option<i32>> = cache.kv.iter()
5922                    .map(|k| k.as_ref().map(|kvl| e.dtoh_i32_one(&kvl.len_d).unwrap())).collect();
5923                let tok_save = e.dtoh_u32_one(&token_d)?;
5924                // RETAINED capture (2026-07-23): the plain capture_graph left pool-transient
5925                // clones as dead COPY NODES replayed every launch — the E4B 0.74ms/token
5926                // regression class, and this door's measured -8.8%. The keeper pins warmup
5927                // transients so the captured graph holds kernel nodes only.
5928                let graph = {
5929                    let tok_ref = &mut token_d;
5930                    let pos_ref = &mut pos_d;
5931                    let cache_ref = &mut *cache;
5932                    let slots_ref = &mut slots;
5933                    let ring_ref = &mut ring;
5934                    e.capture_graph_retained_flags(
5935                        cudarc::driver::sys::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_USE_NODE_PRIORITY,
5936                        |e| {
5937                        // self-feeding: the argmax writes token_d itself.
5938                        let tok_in = unsafe { &*(tok_ref as *const CudaSlice<u32>) };
5939                        let sl = unsafe { &mut *(slots_ref as *mut G4DcSlots) };
5940                        let rg = unsafe { &mut *(ring_ref as *mut CudaSlice<u32>) };
5941                        self.gemma4_decode_step_dc_slotted(e, tok_in, pos_ref, embd_gpu, qt, rb,
5942                                                           cache_ref, n_vocab, Some(bucket_max),
5943                                                           sl, tok_ref, Some((rg, ring_base)))
5944                    })?
5945                };
5946                cache.rollback(e, &snap, 0)?;
5947                e.set_i32_one(&mut pos_d, pos_save)?;
5948                for (il, ls) in len_save.iter().enumerate() {
5949                    if let (Some(kvl), Some(v)) = (cache.kv[il].as_mut(), ls) {
5950                        e.set_i32_one(&mut kvl.len_d, *v)?;
5951                    }
5952                }
5953                e.set_u32_one(&mut token_d, tok_save)?;
5954                if std::env::var("MEMRA_GRAPH_CENSUS").as_deref() == Ok("1") {
5955                    if let Ok(c) = crate::graph_update::node_census(&graph.0) {
5956                        eprintln!("[graph-census] {c:?}");
5957                    }
5958                }
5959                graphs.insert(key, graph);
5960                captures += 1;
5961            }
5962            // CHUNKED REPLAY: enqueue up to DRAIN launches back-to-back (the ~200us host
5963            // cost of each cuGraphLaunch overlaps the PREVIOUS launch's ~11ms GPU work),
5964            // then ONE sync + ring drain. The chunk must stay inside this bucket key and
5965            // the budget; capture warmups already emitted their tokens through the ring.
5966            let mut chunk = 1usize;
5967            let drain_cap: usize = std::env::var("MEMRA_GRAPH_DRAIN").ok()
5968                .and_then(|v| v.parse().ok()).unwrap_or(DRAIN);
5969            while chunk < drain_cap && out.len() + chunk < max_new {
5970                let t_next = cache.pos + 1 + chunk;
5971                let key_s2 = if t_next > win { (true, usize::MAX) }
5972                             else { e.fa_bucket_key(t_next, hd_s, nkv_s, crate::Engine::wkv_on()) };
5973                let key_g2 = if t_next >= f512 {
5974                    (true, (t_next + 1).next_power_of_two().max(f512 * 2))
5975                } else { e.fa_bucket_key(t_next, hd_g, nkv_g, false) };
5976                if (key_s2, key_g2, t_next >= f512, t_next > win) != key { break; }
5977                chunk += 1;
5978            }
5979            let g = &graphs.get(&key).unwrap().0;
5980            for _ in 0..chunk { g.launch()?; }
5981            e.stream().synchronize()?;
5982            let ringh = e.dtoh_u32(&ring)?;
5983            for j in 0..chunk {
5984                let pos_j = cache.pos + j;
5985                let tok_j = ringh[(pos_j - ring_base) % RING];
5986                cache.pos += 0; // advanced below in one shot
5987                if j + 1 == chunk { next = tok_j; }
5988                else {
5989                    out.push(tok_j);
5990                    if eos.contains(&tok_j) || !on_token(tok_j) {
5991                        reason = if eos.contains(&tok_j) { StopReason::Eos }
5992                                 else { StopReason::Callback };
5993                        // roll device/host state back to the stop point.
5994                        let keep = cache.pos + j + 1;
5995                        e.set_i32_one(&mut pos_d, keep as i32)?;
5996                        for kvl in cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
5997                            e.set_i32_one(&mut kvl.len_d, keep as i32)?;
5998                            kvl.len = keep;
5999                        }
6000                        cache.pos = keep;
6001                        if std::env::var("MEMRA_GRAPH_STATS").is_ok() {
6002                            eprintln!("[gemma-graph] captures={captures} buckets={}", graphs.len());
6003                        }
6004                        return Ok((out, reason));
6005                    }
6006                }
6007            }
6008            cache.pos += chunk;
6009            for kvl in cache.kv.iter_mut().filter_map(|k| k.as_mut()) { kvl.len += chunk; }
6010        }
6011        if std::env::var("MEMRA_GRAPH_STATS").is_ok() {
6012            eprintln!("[gemma-graph] captures={captures} buckets={}", graphs.len());
6013        }
6014        Ok((out, reason))
6015    }
6016
6017    /// gemma4 VERIFY step (spec decode): t tokens batched through the trunk at positions
6018    /// pos0..pos0+t-1, K/V rows appended to the quantized cache (caller rolls back rejected
6019    /// rows via kvl.len), per-token causal windowed attend (fa_decode per token — the same
6020    /// kernel family as T=1 decode at each token's t_kv). Returns [t, n_vocab] softcapped
6021    /// logits (host) + advances cache.pos by t.
6022    pub(crate) fn gemma4_decode_step_t(&self, e: &Engine, tokens: &[u32], pos0: usize,
6023                                       cache: &mut Cache)
6024                                       -> Result<Vec<f32>, Box<dyn std::error::Error>> {
6025        Ok(self.gemma4_decode_step_t_h(e, tokens, pos0, cache)?.0)
6026    }
6027
6028    /// GREEDY verify: per-row DEVICE argmax (t x 4B host traffic instead of the t x 1MB logits
6029    /// stack — softcap skipped: tanh is monotonic, per-row argmax unaffected). Returns
6030    /// (argmax ids [t], post-output_norm hidden stack [t, n_embd]).
6031    pub(crate) fn gemma4_decode_step_t_am(&self, e: &Engine, tokens: &[u32], pos0: usize,
6032                                          cache: &mut Cache)
6033                                          -> Result<(Vec<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6034        let (ld, hn) = self.gemma4_verify_trunk(e, tokens, pos0, cache, None)?;
6035        let t = tokens.len();
6036        let n_vocab = self.output.out_features();
6037        let mut toks = e.stream().alloc_zeros::<u32>(t)?;
6038        for i in 0..t {
6039            e.argmax_token_device_col(&ld, i, n_vocab, &mut toks, i)?;
6040        }
6041        Ok((e.dtoh_u32(&toks)?, hn))
6042    }
6043
6044    /// Device-token verify (async spec round): tokens live in tok_d[0..t]; per-row argmax
6045    /// lands in a DEVICE buffer (no host logits). Returns (vam_d [t] u32 device, hn stack).
6046    pub(crate) fn gemma4_decode_step_t_am_dev(&self, e: &Engine, tok_d: &CudaSlice<u32>, t: usize,
6047                                              pos0: usize, cache: &mut Cache)
6048                                              -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6049        let (ld, hn) = self.gemma4_verify_trunk(e, &vec![0u32; t], pos0, cache, Some(tok_d))?;
6050        let n_vocab = self.output.out_features();
6051        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
6052        for i in 0..t {
6053            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
6054        }
6055        Ok((vam, hn))
6056    }
6057
6058    /// gemma4 verify + the POST-output_norm hidden stack [t, n_embd] (the drafter's h input —
6059    /// llama's h_nextn convention).
6060    pub(crate) fn gemma4_decode_step_t_h(&self, e: &Engine, tokens: &[u32], pos0: usize,
6061                                         cache: &mut Cache)
6062                                         -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6063        let (mut ld, hn) = self.gemma4_verify_trunk(e, tokens, pos0, cache, None)?;
6064        let t = tokens.len();
6065        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
6066        e.softcap(&mut ld, cap, t * self.output.out_features())?;
6067        Ok((e.dtoh(&ld)?, hn))
6068    }
6069
6070    /// Persistent device scratch for the BURST verify stream (one per generation): rope pos
6071    /// rows [cap] + per-row t_kv counters (device-filled from the round counter, zero H2D).
6072    pub(crate) fn verify_stream_scratch(&self, e: &Engine, cap: usize)
6073                                        -> Result<VerifyStreamScratch, Box<dyn std::error::Error>> {
6074        Ok(VerifyStreamScratch {
6075            pos_d: e.htod_i32(&vec![0i32; cap])?,
6076            row_ctrs: (0..cap).map(|_| e.htod_i32(&[0])).collect::<Result<_, _>>()?,
6077        })
6078    }
6079
6080    /// BURST verify trunk (device-slot twin): tokens from a device buffer, rope positions
6081    /// iota'd from `ctr`, appends/attention at the layers' len_d counters (verify_attn_stream),
6082    /// NO host cache.pos/len advance. Returns (per-row device argmaxes [t], post-norm hidden
6083    /// stack [t, n_embd]) — the burst's accept/seed inputs, zero host readbacks.
6084    /// `scr` = PERSISTENT per-gen scratch (pos rows + per-row t_kv counters): the first cut
6085    /// htod_i32-allocated these per call — 9 pageable H2D copies per round, each a stream
6086    /// sync, exactly the turnaround the burst exists to remove.
6087    pub(crate) fn gemma4_verify_t_am_stream(&self, e: &Engine, tok_d: &CudaSlice<u32>, t: usize,
6088                                            ctr: &CudaSlice<i32>, hint: usize,
6089                                            cache: &mut Cache,
6090                                            scr: &mut VerifyStreamScratch)
6091                                            -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6092        let n_embd = self.cfg.n_embd as usize;
6093        let eps = self.cfg.rms_eps;
6094        assert!(t <= scr.row_ctrs.len() && t <= 64);
6095        e.i32_iota_from(ctr, &mut scr.pos_d, t)?;
6096        for i in 0..t {
6097            e.i32_copy_add(ctr, &mut scr.row_ctrs[i], (i + 1) as i32)?;
6098        }
6099        let (pos_d, row_ctrs) = (&scr.pos_d, &scr.row_ctrs);
6100        let embd_gpu = self.embd_gpu.get_or_init(|| {
6101            e.upload_u8(&self.embd.raw).expect("embed table upload")
6102        });
6103        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
6104        let mut x = e.embed_gather_device_td(embd_gpu, tok_d, t, n_embd, qt, rb)?;
6105        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
6106        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
6107        let n_layers = self.layers.len();
6108        for (il, layer) in self.layers.iter().enumerate() {
6109            let (hq, hdq) = match h_carry.take() {
6110                Some(p) => p,
6111                None => e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, t, eps)?,
6112            };
6113            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
6114            let o = self.gemma4_verify_attn_stream(e, fa, il, &hq, &hdq, pos_d, t, cache,
6115                                                    hint, row_ctrs)?;
6116            let mut cur = e.uninit(t * n_embd)?;
6117            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, t, eps)?;
6118            let next_norm = if il + 1 < n_layers {
6119                Some(self.layers[il + 1].attn_norm.float_data())
6120            } else { None };
6121            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, t, next_norm)?;
6122            x = xn;
6123            h_carry = hn;
6124            self.dflash_tap(e, cache, il, &x, t)?;
6125        }
6126        let mut hn = e.uninit(t * n_embd)?;
6127        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
6128        let ld = e.matmul(&self.output, &hn, t)?;
6129        let n_vocab = self.output.out_features();
6130        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
6131        for i in 0..t {
6132            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
6133        }
6134        Ok((vam, hn))
6135    }
6136
6137    /// Verify trunk core: returns (UN-softcapped logits device [t, n_vocab], post-output_norm
6138    /// hidden stack [t, n_embd]); appends KV rows + advances cache.pos.
6139    /// DFlash tap write (dflash lane): copy the post-layer residual rows of `x` into the
6140    /// armed sink at the tap slot for `il` (row-major [t, n_taps*hidden]). Per-row D2D
6141    /// copies — t <= block_size on verify; prime pays t*n_taps once per prompt (dedicated
6142    /// kernel later if it shows in the profile).
6143    fn dflash_tap(&self, e: &Engine, cache: &mut Cache, il: usize, x: &CudaSlice<f32>, t: usize)
6144                  -> Result<(), Box<dyn std::error::Error>> {
6145        let Some(taps) = cache.dflash_taps.as_mut() else { return Ok(()) };
6146        let Some(slot) = taps.layer_ids.iter().position(|&l| l == il) else { return Ok(()) };
6147        let h = taps.hidden;
6148        let n_taps = taps.layer_ids.len();
6149        debug_assert_eq!(taps.t, t);
6150        let xv = e.view(x, t * h);
6151        for r in 0..t {
6152            let row = xv.slice(r * h..(r + 1) * h);
6153            e.copy_view_into(&mut taps.buf, r * n_taps * h + slot * h, &row, h)?;
6154        }
6155        Ok(())
6156    }
6157
6158    fn gemma4_verify_trunk(&self, e: &Engine, tokens: &[u32], pos0: usize, cache: &mut Cache,
6159                           tok_dev: Option<&CudaSlice<u32>>)
6160                           -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6161        let n_embd = self.cfg.n_embd as usize;
6162        let eps = self.cfg.rms_eps;
6163        let t = tokens.len();
6164        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
6165        let pos_d = e.htod_i32(&pos)?;
6166        let mut x = match tok_dev {
6167            Some(td) => {
6168                let embd_gpu = self.embd_gpu.get_or_init(|| {
6169                    e.upload_u8(&self.embd.raw).expect("embed table upload")
6170                });
6171                let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
6172                e.embed_gather_device_td(embd_gpu, td, t, n_embd, qt, rb)?
6173            }
6174            None => e.htod(&self.embd.gather(n_embd, tokens))?,
6175        };
6176        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
6177        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
6178        let n_layers = self.layers.len();
6179        for (il, layer) in self.layers.iter().enumerate() {
6180            let (hq, hdq) = match h_carry.take() {
6181                Some(p) => p,
6182                None => e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, t, eps)?,
6183            };
6184            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
6185            let o = self.gemma4_verify_attn(e, fa, il, &hq, &hdq, &pos_d, t, cache)?;
6186            let mut cur = e.uninit(t * n_embd)?;
6187            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, t, eps)?;
6188            let next_norm = if il + 1 < n_layers {
6189                Some(self.layers[il + 1].attn_norm.float_data())
6190            } else { None };
6191            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, t, next_norm)?;
6192            x = xn;
6193            h_carry = hn;
6194            self.dflash_tap(e, cache, il, &x, t)?;
6195        }
6196        let mut hn = e.uninit(t * n_embd)?;
6197        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
6198        let mut ld = e.matmul(&self.output, &hn, t)?;
6199        self.gemma4_suppress(e, &mut ld, t)?;   // before the per-row argmax consumers
6200        cache.pos += t;
6201        Ok((ld, hn))
6202    }
6203
6204    /// Verify attention: project/norm/rope t rows, append them to the cache, then attend each
6205    /// row causally over [win_off_i .. base+i] via the SAME fa_decode dispatch as T=1 decode.
6206    /// BURST verify attention (device-slot twin of `gemma4_verify_attn`): the append lands
6207    /// at the layer's len_d counter (rows_dc), attention bases ride the counter (rows /
6208    /// rows_w with base_dev), and NO host len is read or bumped — `hint` is a host UPPER
6209    /// bound on base_len used only for split sizing and arm gating (the burst loop passes
6210    /// pos0 + burst slack). Host len mirrors re-sync at the burst drain.
6211    #[allow(clippy::too_many_arguments)]
6212    fn gemma4_verify_attn_stream(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
6213                                 hq: &CudaSlice<i8>, hdq: &CudaSlice<f32>,
6214                                 pos_d: &CudaSlice<i32>, t: usize,
6215                                 cache: &mut Cache, hint: usize,
6216                                 row_ctrs: &[CudaSlice<i32>])
6217                                 -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6218        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
6219        let eps = self.cfg.rms_eps;
6220        let aux = self.gemma4_aux.as_ref().unwrap();
6221        let h0 = e.zeros(0)?;
6222        let h = &h0;
6223        // BATCHED FUSED qkv (MEMRA_F2B=1, megakernel microcosm): swa layers fuse all three,
6224        // globals fuse q,k (v := k clone). Bit-identical per row; segments tail-fill.
6225        static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6226        let f2b = *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
6227        let fused_qkv = if f2b {
6228            if swa {
6229                e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
6230                    .map(|(a, b, c)| (a, b, Some(c)))
6231            } else {
6232                e.matmul_q4_fused2_batched(&fa.wq, &fa.wk, hq, hdq, t)?
6233                    .map(|(a, b)| (a, b, None))
6234            }
6235        } else { None };
6236        let (q0, k0, v0) = match fused_qkv {
6237            Some((a, b, cv)) => {
6238                let v = match cv { Some(c) => c, None => e.clone_dtod(&b)? };
6239                (a, b, v)
6240            }
6241            None => {
6242                let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
6243                let k0 = e.matmul_pre(&fa.wk, hq, hdq, h, t)?;
6244                let v0 = if swa { e.matmul_pre(&fa.wv, hq, hdq, h, t)? }
6245                         else { e.clone_dtod(&k0)? };
6246                (q0, k0, v0)
6247            }
6248        };
6249        let mut q = e.uninit(t * nh * hd)?;
6250        let mut k = e.uninit(t * nkv * hd)?;
6251        let mut v = e.uninit(t * nkv * hd)?;
6252        // E4B wave-3 fold (2026-07-23 decode-dust port): q/k/v norms + q/k rope in ONE
6253        // launch — rope math verbatim on the normed rows; V ones-rms, never roped.
6254        let ff = if swa { None } else {
6255            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
6256        };
6257        e.rms_norm_qkv_rope(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
6258                            &aux.ones, &mut q, &mut k, &mut v, hd, nh * t, nkv * t,
6259                            pos_d, nh, nkv, base, 1.0, ff, eps)?;
6260        let kvl = cache.kv[il].as_mut().unwrap();
6261        // append at the DEVICE slot; the counter advances by t on-device.
6262        e.append_kv_quantized_rows_dc(&k, &v, &mut kvl.k, &mut kvl.v, &kvl.len_d, t,
6263                                      kvl.kv_dim_k, kvl.kv_dim_v,
6264                                      kvl.k_tok_bytes, kvl.v_tok_bytes,
6265                                      (!swa && crate::Engine::gkv_on())
6266                                          || (swa && crate::Engine::wkv_on()))?;
6267        // len_d is NOT advanced here: the burst's device rollback (spec_rollback_stream) is
6268        // the sole len writer after this round's attention (base stays = old len, plus = 0).
6269        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
6270        let mut attn = e.uninit(t * nh * hd)?;
6271        let k_view = e.view_u8(&kvl.k, kvl.k.len());
6272        let v_view = e.view_u8(&kvl.v, kvl.v.len());
6273        // arm gating on the host HINT (upper bound; burst entry guards hint >= the vec floor
6274        // and a stable window regime — the same rung/regime keys as the draft graph).
6275        if swa && hint + 1 >= win {
6276            // fully-windowed rows: per-row window geometry from the counter (plus = 0: len_d
6277            // still holds the pre-append len; row r's T_kv = ctr + r + 1).
6278            e.fa_decode_rows_w(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
6279                               &kvl.len_d, 0, t, scale, win,
6280                               kvl.k_tok_bytes, kvl.v_tok_bytes, None)?;
6281        } else if hd == 512 && hint + t < crate::fa512_min_tkv() {
6282            // globals UNDER the fa512 crossover: eager runs the per-row fa_decode_kvmod
6283            // fallback there (parity with t=1 decode) — mirror it with per-row fa_decode_dc
6284            // (bit-correct for any t_kv <= bucket; the drafter dc arc proved the pairing).
6285            // Burst entry gates the horizon onto one side of the crossover, so hint decides
6286            // for every row.
6287            // NO .max(512): fa_decode_dc gates its hd512 dpl16-vs-scalar pick on bucket_max
6288            // (mirroring eager's fa512 floor) — forcing 512 here flipped the arm to dpl16
6289            // while eager ran scalar (il=5 KV drift, the burst's 4/128). SAME LAW capped
6290            // from above (2026-07-13): a regime-pinned hint near the floor (round-graph
6291            // captures use f512-1) pow2-rounds PAST it — clamp under the floor, or the arm
6292            // re-flips to dpl16 (the round-graph 4/64). The scalar unified self-splits, so
6293            // any bucket >= the live length is exact.
6294            let bucket = (hint + t + 2).next_power_of_two()
6295                .min(crate::fa512_min_tkv().saturating_sub(1));
6296            let qv = e.view(&q, t * nh * hd);
6297            for i in 0..t {
6298                let q_row = qv.slice(i * nh * hd..(i + 1) * nh * hd);
6299                let mut q_one = e.uninit(nh * hd)?;
6300                e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
6301                let mut a_one = e.uninit(nh * hd)?;
6302                e.fa_decode_dc(&q_one, &k_view, &v_view, &mut a_one, hd, nh, nkv,
6303                               &row_ctrs[i], bucket, scale,
6304                               kvl.k_tok_bytes, kvl.v_tok_bytes, false)?;
6305                e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
6306            }
6307        } else if hd == 512 {
6308            // globals past the crossover: dpl16 dc twin via the shared rows wrapper (hint
6309            // sizes splits — upper bound; splits beyond the device len exit in-kernel).
6310            e.fa_decode_rows(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, hint, t, scale,
6311                             kvl.k_tok_bytes, kvl.v_tok_bytes,
6312                             Some((&kvl.len_d, 0)), false, false, None)?;
6313        } else {
6314            // hd256 under-window: v4 device-len rows twin.
6315            e.fa_decode_rows_dc(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
6316                                &kvl.len_d, hint + t, t, scale,
6317                                kvl.k_tok_bytes, kvl.v_tok_bytes, 0,
6318                                swa && crate::Engine::wkv_on())?;
6319        }
6320        Ok(e.matmul(&fa.wo, &attn, t)?)
6321    }
6322
6323    fn gemma4_verify_attn(&self, e: &Engine, fa: &crate::hybrid::FullAttnLayer, il: usize,
6324                          hq: &CudaSlice<i8>, hdq: &CudaSlice<f32>,
6325                          pos_d: &CudaSlice<i32>, t: usize,
6326                          cache: &mut Cache)
6327                          -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6328        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
6329        let eps = self.cfg.rms_eps;
6330        let aux = self.gemma4_aux.as_ref().unwrap();
6331        let n_embd = self.cfg.n_embd as usize;
6332        let _ = n_embd;
6333
6334        let h0 = e.zeros(0)?;
6335        let h = &h0;
6336        // BATCHED FUSED qkv (MEMRA_F2B=1, megakernel microcosm): swa layers fuse all three,
6337        // globals fuse q,k (v := k clone). Bit-identical per row; segments tail-fill.
6338        static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6339        let f2b = *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
6340        let fused_qkv = if f2b {
6341            if swa {
6342                e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
6343                    .map(|(a, b, c)| (a, b, Some(c)))
6344            } else {
6345                e.matmul_q4_fused2_batched(&fa.wq, &fa.wk, hq, hdq, t)?
6346                    .map(|(a, b)| (a, b, None))
6347            }
6348        } else { None };
6349        let (q0, k0, v0) = match fused_qkv {
6350            Some((a, b, cv)) => {
6351                let v = match cv { Some(c) => c, None => e.clone_dtod(&b)? };
6352                (a, b, v)
6353            }
6354            None => {
6355                let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
6356                let k0 = e.matmul_pre(&fa.wk, hq, hdq, h, t)?;
6357                let v0 = if swa { e.matmul_pre(&fa.wv, hq, hdq, h, t)? }
6358                         else { e.clone_dtod(&k0)? };
6359                (q0, k0, v0)
6360            }
6361        };
6362        let mut q = e.uninit(t * nh * hd)?;
6363        let mut k = e.uninit(t * nkv * hd)?;
6364        let mut v = e.uninit(t * nkv * hd)?;
6365        // E4B wave-3 fold (2026-07-23 decode-dust port): q/k/v norms + q/k rope in ONE
6366        // launch — rope math verbatim on the normed rows; V ones-rms, never roped.
6367        let ff = if swa { None } else {
6368            Some(aux.rope_freqs.as_ref().expect("gemma4 global rope needs rope_freqs.weight"))
6369        };
6370        e.rms_norm_qkv_rope(&q0, &k0, &v0, fa.q_norm.float_data(), fa.k_norm.float_data(),
6371                            &aux.ones, &mut q, &mut k, &mut v, hd, nh * t, nkv * t,
6372                            pos_d, nh, nkv, base, 1.0, ff, eps)?;
6373        let kvl = cache.kv[il].as_mut().unwrap();
6374        let base_len = kvl.len;
6375        e.append_kv_quantized_rows(&k, &v, &mut kvl.k, &mut kvl.v, base_len, t,
6376                                   kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes, (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()))?;
6377        kvl.len += t;
6378        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
6379        let mut attn = e.uninit(t * nh * hd)?;
6380        // ROWS twin when no window offset is in play (globals are hd512-ineligible; SWA rows
6381        // under the window need no offset): ONE launch, per-row causal == the per-token loop.
6382        let rows_ok = (hd == 256 && base_len + 1 >= crate::fa_vec_min_tkv())
6383            // gemma globals: hd512 rows twin in the dpl16 vec regime (row 0 gates the batch);
6384            // decode rides the SAME symbol at t=1 (parity law).
6385            || (hd == 512 && !swa && base_len + 1 >= crate::fa512_min_tkv());
6386        if rows_ok && (!swa || base_len + t <= win) {
6387            let k_view = e.view_u8(&kvl.k, (base_len + t) * kvl.k_tok_bytes);
6388            let v_view = e.view_u8(&kvl.v, (base_len + t) * kvl.v_tok_bytes);
6389            if hd == 512 {
6390                // device-len twin: sync the counter to the verify base (async arg-store).
6391                e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
6392                e.fa_decode_rows(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, base_len, t,
6393                                 scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
6394                                 Some((&kvl.len_d, 0)), false,
6395                                 swa && crate::Engine::wkv_on(), None)?;
6396            } else {
6397                // hd256: the SAME v4_dc symbol the burst verify launches (PARITY LAW — the
6398                // host-base rows_v4 twin compiles apart from v4_dc and the two-symbol split
6399                // drifted the burst's persisted KV at il=8; one symbol, counter arg-stored).
6400                e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
6401                e.fa_decode_rows_dc(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
6402                                    &kvl.len_d, base_len + t, t, scale,
6403                                    kvl.k_tok_bytes, kvl.v_tok_bytes, 0,
6404                                    swa && crate::Engine::wkv_on())?;
6405            }
6406            return Ok(e.matmul(&fa.wo, &attn, t)?);
6407        }
6408        // WINDOWED rows twin (deep ctx, every row fully windowed): one launch, ABSOLUTE-index
6409        // per-row geometry over the prefix view. PARITY LAW (2026-07-10 root-cause): textually
6410        // identical kernels do NOT compile bit-identically (nvcc unrolls fa_decode_vec_q 2x vs
6411        // its rows_w clone — SASS-proven; the unpinned score `+=` chain then rounds apart), so
6412        // decode-vs-verify parity comes from BOTH sides launching THIS SAME rows_w kernel
6413        // (decode passes t=1) — bitwise equal per position by symbol identity, any lane.
6414        // MEMRA_GEMMA_ROWS_W=0 -> per-token loop (decode falls back to fa_decode views too).
6415        if hd == 256 && swa && base_len + 1 >= win
6416            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
6417            let k_view = e.view_u8(&kvl.k, (base_len + t) * kvl.k_tok_bytes);
6418            let v_view = e.view_u8(&kvl.v, (base_len + t) * kvl.v_tok_bytes);
6419            e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
6420            e.fa_decode_rows_w(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, &kvl.len_d, 0,
6421                               t, scale, win, kvl.k_tok_bytes, kvl.v_tok_bytes, None)?;
6422            return Ok(e.matmul(&fa.wo, &attn, t)?);
6423        }
6424        for i in 0..t {
6425            let avail = base_len + i + 1;
6426            let (off_tok, t_kv) = if swa && avail > win { (avail - win, win) } else { (0, avail) };
6427            let k_view = e.view_u8_range(&kvl.k, off_tok * kvl.k_tok_bytes,
6428                                         (off_tok + t_kv) * kvl.k_tok_bytes);
6429            let v_view = e.view_u8_range(&kvl.v, off_tok * kvl.v_tok_bytes,
6430                                         (off_tok + t_kv) * kvl.v_tok_bytes);
6431            let qi = e.view(&q, t * nh * hd);
6432            let q_row = qi.slice(i * nh * hd..(i + 1) * nh * hd);
6433            let mut q_one = e.uninit(nh * hd)?;
6434            e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
6435            let mut a_one = e.uninit(nh * hd)?;
6436            // straddle rounds: windowed rows must ride the SAME rows_w kernel decode uses
6437            // (parity law above); global hd512 rows past the fa512 floor likewise ride the
6438            // rows_dpl16 twin; remaining rows keep the gated fa_decode pair.
6439            if swa && avail > win && hd == 256
6440                && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
6441                let kp = e.view_u8(&kvl.k, avail * kvl.k_tok_bytes);
6442                let vp = e.view_u8(&kvl.v, avail * kvl.v_tok_bytes);
6443                e.i32_set_k(&mut kvl.len_d, (avail - 1) as i32)?;
6444                e.fa_decode_rows_w(&q_one, &kp, &vp, &mut a_one, hd, nh, nkv, &kvl.len_d, 0,
6445                                   1, scale, win, kvl.k_tok_bytes, kvl.v_tok_bytes, None)?;
6446            } else if !swa && hd == 512 && avail >= crate::fa512_min_tkv()
6447                && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0") {
6448                let kp = e.view_u8(&kvl.k, avail * kvl.k_tok_bytes);
6449                let vp = e.view_u8(&kvl.v, avail * kvl.v_tok_bytes);
6450                e.i32_set_k(&mut kvl.len_d, (avail - 1) as i32)?;
6451                e.fa_decode_rows(&q_one, &kp, &vp, &mut a_one, hd, nh, nkv, avail - 1, 1,
6452                                 scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
6453                                 Some((&kvl.len_d, 0)), false, false, None)?;
6454            } else {
6455                e.fa_decode_kvmod(&q_one, &k_view, &v_view, &mut a_one, hd, nh, nkv, t_kv, scale,
6456                            kvl.k_tok_bytes, kvl.v_tok_bytes, swa && crate::Engine::wkv_on())?;
6457            }
6458            e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
6459        }
6460        Ok(e.matmul(&fa.wo, &attn, t)?)
6461    }
6462
6463    /// gemma4 T=1 decode step: R8 layer graph over the cache; returns (softcapped logits host,
6464    /// h_seed = pre-output_norm hidden). Advances cache.pos.
6465    pub(crate) fn gemma4_decode_step_h(&self, e: &Engine, token: u32, cache: &mut Cache)
6466                                       -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6467        // M1-PP2 door (crate::pp): 2-stage split with an explicit activation handoff.
6468        // Default OFF — unset env means this branch is never taken. The gemma4 arm stays
6469        // 2-stage in M2 (the N-stage gate model is the generic-arm 9B); N>2 warns + runs
6470        // unsplit rather than guessing a fence.
6471        if let Some(split) = crate::pp::pp2_split(self.layers.len()) {
6472            return self.gemma4_decode_step_h_pp2(e, token, cache, split);
6473        }
6474        if crate::pp::pp_cuts(self.layers.len()).is_some() {
6475            crate::pp::warn_unwired_once("gemma4 eager decode (N>2)");
6476        }
6477        let n_embd = self.cfg.n_embd as usize;
6478        let eps = self.cfg.rms_eps;
6479        let pos_d = e.htod_i32(&[cache.pos as i32])?;
6480        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
6481        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
6482        // cross-layer fusion: each tail's closing add+scale also EMITS the next layer's
6483        // attn-normed input pre-quantized q8_1 (the mixer consumes only quantized matmuls).
6484        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
6485        let n_layers = self.layers.len();
6486        for (il, layer) in self.layers.iter().enumerate() {
6487            let (hq, hdq) = match h_carry.take() {
6488                Some(p) => p,
6489                None => e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, 1, eps)?,
6490            };
6491            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
6492            let o = self.gemma4_decode_attn(e, fa, il, &hq, &hdq, &pos_d, cache)?;
6493            let mut cur = e.uninit(n_embd)?;
6494            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, 1, eps)?;
6495            let next_norm = if il + 1 < n_layers {
6496                Some(self.layers[il + 1].attn_norm.float_data())
6497            } else { None };
6498            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
6499            x = xn;
6500            h_carry = hn;
6501        }
6502        let mut hn = e.uninit(n_embd)?;
6503        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
6504        let h_seed = e.clone_dtod(&x)?;
6505        let mut ld = e.matmul(&self.output, &hn, 1)?;
6506        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
6507        e.softcap(&mut ld, cap, self.output.out_features())?;   // R4 on device (262k host tanh ~ms/step)
6508        self.gemma4_suppress(e, &mut ld, 1)?;
6509        let logits = e.dtoh(&ld)?;
6510        cache.pos += 1;
6511        Ok((logits, h_seed))
6512    }
6513
6514    /// M1-PP2 stage subgraph (gemma4 arm): layers [lo, hi) of the gemma4 T=1 walk with the
6515    /// pre-quantized next-norm carry LOCAL to the range. Enters with a materialized residual
6516    /// `x` (the range head runs its own `rms_norm_q8_1` against ITS layer's attn_norm — for
6517    /// lo == 0 that is exactly the unsplit loop's il==0 arm); exits with the residual
6518    /// materialized (range tail passes next_norm = None, exactly the unsplit last-layer arm).
6519    /// Bit-identity of the cut relies on the kernel-check-pinned `add_scale_rms_norm_q8_1 ==
6520    /// add_scale then rms_norm_q8_1` identity (`pp2-gate` verifies end-to-end).
6521    fn gemma4_decode_layers(&self, e: &Engine, mut x: CudaSlice<f32>, lo: usize, hi: usize,
6522                            pos_d: &CudaSlice<i32>, cache: &mut Cache)
6523                            -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6524        let n_embd = self.cfg.n_embd as usize;
6525        let eps = self.cfg.rms_eps;
6526        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
6527        for il in lo..hi {
6528            let layer = &self.layers[il];
6529            let (hq, hdq) = match h_carry.take() {
6530                Some(p) => p,
6531                // range head: il == lo — norm against THIS layer's attn_norm.
6532                None => e.rms_norm_q8_1(&x, self.layers[il].attn_norm.float_data(), n_embd, 1, eps)?,
6533            };
6534            let Mixer::Full(fa) = &layer.mixer else { panic!("gemma4 layer {il} not full-attn") };
6535            let o = self.gemma4_decode_attn(e, fa, il, &hq, &hdq, pos_d, cache)?;
6536            let mut cur = e.uninit(n_embd)?;
6537            e.rms_norm(&o, layer.post_attn_norm.float_data(), &mut cur, n_embd, 1, eps)?;
6538            let next_norm = if il + 1 < hi {
6539                Some(self.layers[il + 1].attn_norm.float_data())
6540            } else { None };
6541            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
6542            x = xn;
6543            h_carry = hn;
6544        }
6545        Ok(x)
6546    }
6547
6548    /// M1-PP2 (increment 2): `gemma4_decode_step_h` as TWO stage subgraphs, each on its
6549    /// own stream (and device, under MEMRA_PP_DEVICES) with the transport-selected
6550    /// boundary handoff — same choreography as the generic arm (decode.rs), same
6551    /// ownership contract (crate::pp). Stage 0 = embed+scale + layers [0, split);
6552    /// stage 1 = layers [split, n) + output_norm + softcapped head.
6553    /// MEMRA_PP_STREAMS=0 = the increment-1 same-stream seam. Gate: `pp2-gate`.
6554    fn gemma4_decode_step_h_pp2(&self, e: &Engine, token: u32, cache: &mut Cache, split: usize)
6555                                -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6556        if crate::pp::pp2_streams_off() {
6557            return self.gemma4_decode_step_h_pp2_samestream(e, token, cache, split);
6558        }
6559        let rt = crate::pp::Pp2Rt::get(e)?;
6560        let e0 = rt.engine(0, e);
6561        let e1 = rt.engine(1, e);
6562        let n_embd = self.cfg.n_embd as usize;
6563        let eps = self.cfg.rms_eps;
6564
6565        // ---- STAGE 0 (its own stream): embed + sqrt(n_embd) scale + layers [0, split) ----
6566        let (pos_d, slot) = {
6567            let _st0 = rt.enter(0);
6568            let pos_d = e0.htod_i32(&[cache.pos as i32])?;
6569            let mut x = e0.htod(&self.embd.gather(n_embd, &[token]))?;
6570            e0.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
6571            let x = self.gemma4_decode_layers(e0, x, 0, split, &pos_d, cache)?;
6572            let slot = rt.tx(0, &x, n_embd)?;
6573            (pos_d, slot)
6574        };
6575
6576        // ---- STAGE 1 (its own stream): RX + layers [split, n) + softcapped head ----
6577        let _st1 = rt.enter(1);
6578        let x = rt.rx(0, slot, n_embd)?;
6579        let x = self.gemma4_decode_layers(e1, x, split, self.layers.len(), &pos_d, cache)?;
6580
6581        let mut hn = e1.uninit(n_embd)?;
6582        e1.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
6583        let h_seed = e1.clone_dtod(&x)?;
6584        let mut ld = e1.matmul(&self.output, &hn, 1)?;
6585        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
6586        e1.softcap(&mut ld, cap, self.output.out_features())?;
6587        self.gemma4_suppress(e1, &mut ld, 1)?;
6588        let logits = e1.dtoh(&ld)?;
6589        cache.pos += 1;
6590        Ok((logits, h_seed))
6591    }
6592
6593    /// MEMRA_PP_STREAMS=0 rollback seam: the increment-1 gemma4 pp2 body verbatim — both
6594    /// stage subgraphs on the ambient compute stream, boundary = two plain dtod copies.
6595    fn gemma4_decode_step_h_pp2_samestream(&self, e: &Engine, token: u32, cache: &mut Cache,
6596                                           split: usize)
6597                                           -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6598        let n_embd = self.cfg.n_embd as usize;
6599        let eps = self.cfg.rms_eps;
6600        let pos_d = e.htod_i32(&[cache.pos as i32])?;
6601
6602        // ---- STAGE 0: embed + sqrt(n_embd) scale + layers [0, split) ----
6603        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
6604        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
6605        let x = self.gemma4_decode_layers(e, x, 0, split, &pos_d, cache)?;
6606
6607        // ---- STAGE BOUNDARY: explicit [n_embd] activation handoff (TX copy, RX copy) ----
6608        let boundary_tx = e.clone_dtod(&x)?;
6609        let boundary_rx = e.clone_dtod(&boundary_tx)?;
6610
6611        // ---- STAGE 1: layers [split, n) + output_norm + softcapped head ----
6612        let x = self.gemma4_decode_layers(e, boundary_rx, split, self.layers.len(), &pos_d, cache)?;
6613
6614        let mut hn = e.uninit(n_embd)?;
6615        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
6616        let h_seed = e.clone_dtod(&x)?;
6617        let mut ld = e.matmul(&self.output, &hn, 1)?;
6618        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
6619        e.softcap(&mut ld, cap, self.output.out_features())?;
6620        self.gemma4_suppress(e, &mut ld, 1)?;
6621        let logits = e.dtoh(&ld)?;
6622        cache.pos += 1;
6623        Ok((logits, h_seed))
6624    }
6625}
6626
6627// ===================================================================================== //
6628//  gemma-4 E4B (per-layer embeddings + KV-sharing) — FIRST-LIGHT forward.               //
6629//  Dedicated simple path (Stage-B matmuls, per-row causal attention over the quantized  //
6630//  cache) so the tuned 26B/31B paths stay untouched. Arch: research/gemma4-bringup/     //
6631//  e4b-arch-map.md; llama reference: src/models/gemma4.cpp (E4B arms). Wired: forward   //
6632//  (prefill logits), prime (tokenwise-equivalent batched trunk), eager decode_step_h.   //
6633//  NOT wired: dc/graph serving arms, verify/spec, chunked prime (HANDOVER-E4B.md).      //
6634// ===================================================================================== //
6635impl HybridModel {
6636    pub fn is_gemma4_e4b(&self) -> bool {
6637        self.gemma4_aux.as_ref().is_some_and(|a| a.e4b.is_some())
6638    }
6639
6640    /// E4B per-layer geometry: nh/nkv derive from the layer's OWN tensor shapes (the E4B GGUF
6641    /// ships a scalar head_count_kv; swa q 8x256 / kv 2x256, global q 4x512 / kv 1x512).
6642    /// KV-shared layers report the SHARE TARGET's kv count (their wk IS the target's tensor).
6643    fn gemma4_e4b_geom(&self, il: usize) -> (usize, usize, usize, f32, f32, bool) {
6644        let g = self.cfg.gemma4.as_ref().unwrap();
6645        let swa = g.swa_pattern[il];
6646        let hd = if swa { g.key_length_swa } else { g.key_length_global } as usize;
6647        let Mixer::Full(fa) = &self.layers[il].mixer else { panic!("e4b layer {il} not full-attn") };
6648        let nh = fa.wq.out_features() / hd;
6649        let nkv = fa.wk.out_features() / hd;
6650        (hd, nkv, nh, if swa { g.rope_base_swa } else { g.rope_base_global }, 1.0, swa)
6651    }
6652
6653    /// E4B KV-share target: Some(target) for the trailing shared layers, None = own cache.
6654    fn gemma4_e4b_kv_target(&self, il: usize) -> Option<usize> {
6655        self.layers[il].gemma4.as_ref()
6656            .and_then(|b| b.e4b.as_ref())
6657            .and_then(|e4| e4.kv_share.map(|t| t as usize))
6658    }
6659
6660    /// E4B prologue: inp_pl [t][n_layer][n_epl] =
6661    ///   ( gather(per_layer_tok_embd, tok)*sqrt(n_epl)
6662    ///   + rms_norm(model_proj . x_scaled * 1/sqrt(n_embd), proj_norm) ) * 1/sqrt(2)
6663    /// (llama gemma4.cpp build_inp_per_layer + project_per_layer_inputs, exact order).
6664    fn gemma4_e4b_inp_pl(&self, e: &Engine, tokens: &[u32], x_scaled: &CudaSlice<f32>, t: usize)
6665                         -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6666        let tok_d = e.stream().clone_htod(&tokens.to_vec())?;
6667        self.gemma4_e4b_inp_pl_dev(e, &tok_d, x_scaled, t)
6668    }
6669
6670    /// Device-token prologue core (dc arm shares it: token ids never touch the host).
6671    fn gemma4_e4b_inp_pl_dev(&self, e: &Engine, tok_d: &CudaSlice<u32>,
6672                             x_scaled: &CudaSlice<f32>, t: usize)
6673                             -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6674        let aux = self.gemma4_aux.as_ref().unwrap();
6675        let m = aux.e4b.as_ref().unwrap();
6676        let n_embd = self.cfg.n_embd as usize;
6677        let n_layer = self.layers.len();
6678        let width = m.n_epl * n_layer;
6679        let tbl = m.tok_tbl_gpu.get_or_init(|| {
6680            e.upload_u8(&m.tok_embd_bytes).expect("e4b per-layer token table upload")
6681        });
6682        let mut a = e.embed_gather_device_td(tbl, tok_d, t, width, m.tok_embd_qt,
6683                                             m.tok_embd_row_bytes)?;
6684        e.scale_inplace(&mut a, (m.n_epl as f32).sqrt(), t * width)?;
6685        let mut p = e.matmul(&m.model_proj, x_scaled, t)?;
6686        e.scale_inplace(&mut p, 1.0 / (n_embd as f32).sqrt(), t * width)?;
6687        let mut pn = e.uninit(t * width)?;
6688        e.rms_norm(&p, m.proj_norm.float_data(), &mut pn, m.n_epl, t * n_layer,
6689                   self.cfg.rms_eps)?;
6690        let mut out = e.uninit(t * width)?;
6691        e.add_scale(&a, &pn, 1.0 / 2f32.sqrt(), &mut out, t * width)?;
6692        Ok(out)
6693    }
6694
6695    /// E4B attention (t-wide, causal, per-row fa over the QUANTIZED cache — first-light
6696    /// correctness path; the fa rows arms come later). Own-KV layers project+norm+rope k/v
6697    /// and append t rows; KV-shared layers are Q-only over the target layer's cache (which
6698    /// already holds this forward's rows — the target runs earlier in the stack).
6699    #[allow(clippy::too_many_arguments)]
6700    fn gemma4_e4b_attn(&self, e: &Engine, il: usize,
6701                       hq: &CudaSlice<i8>, hdq: &CudaSlice<f32>,
6702                       pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache,
6703                       dc_bucket: Option<usize>)
6704                       -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6705        let (hd, nkv, nh, base, scale, swa) = self.gemma4_e4b_geom(il);
6706        let eps = self.cfg.rms_eps;
6707        let aux = self.gemma4_aux.as_ref().unwrap();
6708        let Mixer::Full(fa) = &self.layers[il].mixer else { unreachable!() };
6709        // pre-quantized layer input (E4B fusion port, 2026-07-12): ONE norm+quant feeds
6710        // wq/wk/wv via matmul_pre — the first-light path quantized the same h three times
6711        // (the profile's 342 quantize_q8_1/token; glue = 26% of the 6.1ms token).
6712        let h0 = e.zeros(0)?;
6713        let h = &h0;
6714
6715        let ff = if swa { None } else {
6716            Some(aux.rope_freqs.as_ref().expect("e4b global rope needs rope_freqs.weight"))
6717        };
6718        let share = self.gemma4_e4b_kv_target(il);
6719        // Own-KV arms keep the f32 (k, v) alive for the prime fa arm below.
6720        let mut kv_f32: Option<(CudaSlice<f32>, CudaSlice<f32>)> = None;
6721        let mut q;
6722        if let Some(_tgt) = share {
6723            let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
6724            q = e.uninit(t * nh * hd)?;
6725            // Q-only through the same fused norm+rope kernel (rk = 0: the k/v segments are
6726            // empty; q0 stands in for the unused k/v pointers).
6727            let mut kdummy = e.uninit(1)?;
6728            let mut vdummy = e.uninit(1)?;
6729            e.rms_norm_qkv_rope(&q0, &q0, &q0, fa.q_norm.float_data(),
6730                                fa.q_norm.float_data(), &aux.ones,
6731                                &mut q, &mut kdummy, &mut vdummy, hd, nh * t, 0,
6732                                pos_d, nh, 1, base, 1.0, ff, eps)?;
6733        } else {
6734            // wave-4b: ONE concat matvec (wq|wk|wv) at t == 1 when the cat tensor exists;
6735            // else the fused3 grid launch; else per-matvec. The cat output is contiguous
6736            // q|k|v rows — the cat norm+rope twin consumes it directly.
6737            let e4bits = self.layers[il].gemma4.as_ref().and_then(|g| g.e4b.as_ref());
6738            let cat = e4bits.and_then(|e4| e4.qkv_cat.as_ref());
6739            q = e.uninit(t * nh * hd)?;
6740            let mut k = e.uninit(t * nkv * hd)?;
6741            let mut v = e.uninit(t * nkv * hd)?;
6742            if t == 1 && cat.is_some() {
6743                let qkv0 = e.matmul_pre(cat.unwrap(), hq, hdq, h, 1)?;
6744                e.rms_norm_qkv_rope_cat(&qkv0, fa.q_norm.float_data(), fa.k_norm.float_data(),
6745                                        &aux.ones, &mut q, &mut k, &mut v, hd, nh, nkv,
6746                                        pos_d, nh, nkv, base, 1.0, ff, eps)?;
6747            } else {
6748                let (q0, k0, v0) = match if t == 1 {
6749                    e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hdq)?
6750                } else {
6751                    // E4B verify f3 port (2026-07-14): the 31B segmented-grid batched qkv
6752                    // on E4B's real-V triple — same MEMRA_F2B seam, bit-identical per row.
6753                    static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
6754                    if *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0")) {
6755                        e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
6756                    } else { None }
6757                } {
6758                    Some(triple) => triple,
6759                    None => (e.matmul_pre(&fa.wq, hq, hdq, h, t)?,
6760                             e.matmul_pre(&fa.wk, hq, hdq, h, t)?,
6761                             e.matmul_pre(&fa.wv, hq, hdq, h, t)?),   // E4B: real v (K != V)
6762                };
6763                // wave-3 fold: q/k/v norms + q/k rope in ONE launch (rope math verbatim on
6764                // the normed rows; V ones-rms, never roped).
6765                e.rms_norm_qkv_rope(&q0, &k0, &v0, fa.q_norm.float_data(),
6766                                    fa.k_norm.float_data(), &aux.ones, &mut q, &mut k, &mut v,
6767                                    hd, nh * t, nkv * t, pos_d, nh, nkv, base, 1.0, ff, eps)?;
6768            }
6769            let kvl = cache.kv[il].as_mut().unwrap();
6770            // class flag must match the cache dims (the g-threading sweep hardcoded `false`
6771            // here and the wkv default corrupted E4B: q8_0 bytes into an e4m3 cache — the
6772            // degenerate tok-0 stream, 2026-07-12).
6773            let cls = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
6774            if dc_bucket.is_some() {
6775                // DC arm (graph serving): append at the len_d slot, advance the counter
6776                // in-stream — replay-correct, no host len in the launch args. Host mirrors
6777                // are NOT touched here (the replay loop owns them; a bump at capture-record
6778                // time would double-count the capture iteration).
6779                debug_assert!(t == 1);
6780                // wave 5c: append + len_d inc fused (one launch; single-block ordering).
6781                e.append_kv_quantized_row_dc_inc(&k, &v, &mut kvl.k, &mut kvl.v,
6782                                                 &mut kvl.len_d, kvl.kv_dim_k, kvl.kv_dim_v,
6783                                                 kvl.k_tok_bytes, kvl.v_tok_bytes, cls)?;
6784            } else {
6785                e.append_kv_quantized_rows(&k, &v, &mut kvl.k, &mut kvl.v, kvl.len, t,
6786                                           kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes,
6787                                           kvl.v_tok_bytes, cls)?;
6788                kvl.len += t;
6789            }
6790            kv_f32 = Some((k, v));
6791        }
6792        // attention: per-row causal fa over the (own or target) quantized cache. The cache
6793        // already contains this forward's rows in both arms; row i attends [.., base+i].
6794        let kvl_idx = share.unwrap_or(il);
6795        let kvl = cache.kv[kvl_idx].as_ref().unwrap();
6796        let base_len = kvl.len - t;   // pre-append length (target appended this forward too)
6797        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
6798        let mut attn = e.uninit(t * nh * hd)?;
6799        // PRIME FA ARMS (perf lane 3, completed 2026-07-31 — the H100 board pinned E4B
6800        // prefill at 482 tok/s: the per-row loop ran the DECODE kernel once per token per
6801        // layer). Fresh-prompt (base_len == 0) batched attention, one launch per layer:
6802        //   - own-KV hd256 under the window: f32 fa_prefill (full causal exact — 26B pattern)
6803        //   - own-KV hd256 above the window (swa): fa_prefill_w windowed twin (12B pattern)
6804        //   - own-KV hd512 globals: fa_prefill_hd512 (12B/31B globals pattern)
6805        //   - KV-shared layers (no f32 k/v): fa_prefill_view over the target's QUANTIZED
6806        //     rows (the T=K verify kernel; the target appended this forward's rows already).
6807        //     swa-shared above the window keeps the per-row loop (no windowed view twin).
6808        // Same numeric class split as the 26B prime (f32/batched prefill vs per-row
6809        // quantized decode); run-gen argmax + chat gates arbitrate. MEMRA_NOFA=1 reverts all.
6810        if t > 1 && base_len == 0 && std::env::var("MEMRA_NOFA").is_err() {
6811            if let Some((kf, vf)) = &kv_f32 {
6812                if hd == 256 && t <= win {
6813                    e.fa_prefill(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale, true)?;
6814                    return Ok(e.matmul(&fa.wo, &attn, t)?);
6815                }
6816                if hd == 256 && swa && t > win {
6817                    e.fa_prefill_w(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale, true,
6818                                   win)?;
6819                    return Ok(e.matmul(&fa.wo, &attn, t)?);
6820                }
6821                if hd == 512 && !swa {
6822                    e.fa_prefill_hd512(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale,
6823                                       true)?;
6824                    return Ok(e.matmul(&fa.wo, &attn, t)?);
6825                }
6826            } else if share.is_some() {
6827                let g = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
6828                let k_view = e.view_u8(&kvl.k, kvl.k.len());
6829                let v_view = e.view_u8(&kvl.v, kvl.v.len());
6830                if hd == 256 && (!swa || t <= win) {
6831                    // inline-dequant quantized-view prefill (fa_prefill_q stamps 256/128)
6832                    e.fa_prefill_view(&q, &k_view, &v_view, &mut attn, hd, nh, nkv, t, t,
6833                                      scale, true, kvl.k_tok_bytes, kvl.v_tok_bytes, g)?;
6834                    return Ok(e.matmul(&fa.wo, &attn, t)?);
6835                }
6836                // remaining shared classes (swa above the window; hd512 globals): dequant
6837                // the target's t rows ONCE to f32, then the same f32 twins as own-KV.
6838                let kv_dim = nkv * hd;
6839                let mut kf = e.uninit(t * kv_dim)?;
6840                let mut vf = e.uninit(t * kv_dim)?;
6841                e.fa_dequant_kv_view_f32(&k_view, &v_view, &mut kf, &mut vf, kv_dim, kv_dim,
6842                                         t, kvl.k_tok_bytes, kvl.v_tok_bytes, g)?;
6843                if hd == 512 {
6844                    e.fa_prefill_hd512(&q, &kf, &vf, &mut attn, hd, nh, nkv, t, t, scale,
6845                                       true)?;
6846                } else {
6847                    e.fa_prefill_w(&q, &kf, &vf, &mut attn, hd, nh, nkv, t, t, scale, true,
6848                                   win)?;
6849                }
6850                return Ok(e.matmul(&fa.wo, &attn, t)?);
6851            }
6852        }
6853        if let Some(bucket) = dc_bucket {
6854            // DC arm (t == 1, under-window regime — the generate gate enforces it): ONE
6855            // fa_decode_dc over the live counter. len_d already advanced past this token
6856            // (t_kv = len_d[0], the cache.rs contract). KV-shared layers read the target's
6857            // counter (advanced when the target ran earlier in the stack).
6858            assert!(t == 1);
6859            // hd512 globals: eager (kvmod) picks the SCALAR unified below the fa512 floor,
6860            // and under the window every live t_kv sits below it — cap the capture bucket
6861            // under the floor so fa_decode_dc bakes the same scalar symbol, or the graph
6862            // rides the dpl16 twin and its numeric class diverges from the dc-eager stream
6863            // (E4B-GRAPH-GATE 2/64, 2026-07-12).
6864            let bucket = if hd == 512 && win <= crate::fa512_min_tkv() {
6865                bucket.min(crate::fa512_min_tkv().saturating_sub(1))
6866            } else { bucket };
6867            let k_view = e.view_u8(&kvl.k, kvl.k.len());
6868            let v_view = e.view_u8(&kvl.v, kvl.v.len());
6869            let g = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
6870            // Weight prefetch (SOTA item 3, 2026-07-13): wo's decode plane prefetched into
6871            // L2 across the fa window — fa reads KV only, the weight DRAM lanes are idle
6872            // there (E4B valid window +0.65%: 196.8 vs 195.6). Value-free scheduling op,
6873            // captured into the dc graph like any other launch. Extending the cascade to
6874            // the ffn gate/up planes measured NEGATIVE (193.9 vs 195.8 — 29MB/layer floods
6875            // the fill path and evicts still-hot lines); wo-only is the shipped shape.
6876            // MEMRA_WPF=0 rollback seam.
6877            if crate::Engine::wpf_level() >= 1 {
6878                e.prefetch_weight_l2(&fa.wo)?;
6879            }
6880            // wave 5b: the combine emits the wo input q8 pair directly — the standalone
6881            // quantize launch + the f32 attn round-trip fold away (t=1 fast path only).
6882            if e.uses_q8_1_fast(&fa.wo) {
6883                let mut oq = e.alloc_i8_uninit(nh * hd)?;
6884                let mut od = e.zeros(nh * hd / 32)?;
6885                e.fa_decode_dc_q8(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
6886                                  &kvl.len_d, bucket, scale,
6887                                  kvl.k_tok_bytes, kvl.v_tok_bytes, g,
6888                                  Some((&mut oq, &mut od)))?;
6889                return Ok(e.matmul_pre(&fa.wo, &oq, &od, &attn, t)?);
6890            }
6891            e.fa_decode_dc(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
6892                           &kvl.len_d, bucket, scale,
6893                           kvl.k_tok_bytes, kvl.v_tok_bytes, g)?;
6894            return Ok(e.matmul(&fa.wo, &attn, t)?);
6895        }
6896        for i in 0..t {
6897            let avail = base_len + i + 1;
6898            let (off_tok, t_kv) = if swa && avail > win { (avail - win, win) } else { (0, avail) };
6899            let k_view = e.view_u8_range(&kvl.k, off_tok * kvl.k_tok_bytes,
6900                                         (off_tok + t_kv) * kvl.k_tok_bytes);
6901            let v_view = e.view_u8_range(&kvl.v, off_tok * kvl.v_tok_bytes,
6902                                         (off_tok + t_kv) * kvl.v_tok_bytes);
6903            let qv = e.view(&q, t * nh * hd);
6904            let q_row = qv.slice(i * nh * hd..(i + 1) * nh * hd);
6905            let mut q_one = e.uninit(nh * hd)?;
6906            e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
6907            let mut a_one = e.uninit(nh * hd)?;
6908            // read class MUST match the append class (globals are e4m3 under gkv): the
6909            // swa-only flag decoded global-layer e4m3 bytes as q8_0/q5_1 — attention over
6910            // garbage on EVERY E4B global layer, the cross-mode maxdiff-30 root (2026-07-12).
6911            e.fa_decode_kvmod(&q_one, &k_view, &v_view, &mut a_one, hd, nh, nkv, t_kv, scale,
6912                        kvl.k_tok_bytes, kvl.v_tok_bytes,
6913                        (!swa && crate::Engine::gkv_on())
6914                            || (swa && crate::Engine::wkv_on()))?;
6915            e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
6916        }
6917        Ok(e.matmul(&fa.wo, &attn, t)?)
6918    }
6919
6920    /// E4B trunk: embed -> prologue -> layers (attn + dense ffn via the 31B tail_core + the
6921    /// per-layer-embedding tail + layer scale) -> output_norm. Returns (softcapped logits
6922    /// device [t, n_vocab], pre-output_norm hidden [t, n_embd]). Appends t rows per own-KV
6923    /// layer; does NOT advance cache.pos (caller owns pos).
6924    fn gemma4_e4b_trunk(&self, e: &Engine, tokens: &[u32], pos0: usize, cache: &mut Cache,
6925                        head_last: bool)
6926                        -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6927        let n_embd = self.cfg.n_embd as usize;
6928        let t = tokens.len();
6929        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
6930        let pos_d = e.htod_i32(&pos)?;
6931        let mut x = e.htod(&self.embd.gather(n_embd, tokens))?;
6932        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
6933        let inp_pl = self.gemma4_e4b_inp_pl(e, tokens, &x, t)?;
6934        self.gemma4_e4b_trunk_core(e, x, inp_pl, &pos_d, t, cache, None, true, head_last)
6935    }
6936
6937    /// Layer stack + head over prebuilt (x_scaled, inp_pl, device pos) — everything below
6938    /// here is device-driven, so the dc arm shares it verbatim (stream identity with the
6939    /// eager chain by construction: SAME functions, not twins).
6940    fn gemma4_e4b_trunk_core(&self, e: &Engine, x_in: CudaSlice<f32>, inp_pl: CudaSlice<f32>,
6941                             pos_d: &CudaSlice<i32>, t: usize, cache: &mut Cache,
6942                             dc_bucket: Option<usize>, cap_logits: bool, head_last: bool)
6943                             -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
6944        let n_embd = self.cfg.n_embd as usize;
6945        let eps = self.cfg.rms_eps;
6946        let n_layer = self.layers.len();
6947        let mut x = x_in;
6948        let aux_e4b = self.gemma4_aux.as_ref().unwrap().e4b.as_ref().unwrap();
6949        let n_epl = aux_e4b.n_epl;
6950
6951        // cross-layer fusion (2026-07-12 port of the 26B/31B trunk structure): each layer's
6952        // closing add_scale also EMITS the next layer's attn-normed input pre-quantized
6953        // q8_1 (add_scale_rms_norm_q8_1); the LAST layer emits through output_norm so the
6954        // head rides matmul_pre too. First layer's pair comes from a standalone fused
6955        // norm+quant.
6956        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
6957        for il in 0..n_layer {
6958            let layer = &self.layers[il];
6959            let (hq, hdq) = match h_carry.take() {
6960                Some(p) => p,
6961                None => e.rms_norm_q8_1(&x, layer.attn_norm.float_data(), n_embd, t, eps)?,
6962            };
6963            let o = self.gemma4_e4b_attn(e, il, &hq, &hdq, pos_d, t, cache, dc_bucket)?;
6964            // dense ffn tail with the post-attn norm FOLDED into its entry (one launch for
6965            // rms(o, post_attn_norm) + residual add + ffn_norm — glue-fusion lane).
6966            let bits = layer.gemma4.as_ref().unwrap();
6967            let e4b = bits.e4b.as_ref().expect("e4b layer bits");
6968            // glue wave 5: the tail DEFERS its post_ffw norm — the FFN exit fuses
6969            // rms(f0, post_ffw) + residual add + q8 emit into ONE launch (rms_pre_add_q8_1),
6970            // killing the rms_norm + add_q8_1 pair per layer. T-GENERIC since 2026-07-13:
6971            // the fused single-phase reduction is NOT FP-order-identical to the unfused
6972            // rms_norm+add pair (the original "bit-identical chain" claim was FALSE — the
6973            // t==1 gate left the batched VERIFY on the unfused chain and split E4B verify
6974            // from decode by logit maxdiff ~0.45, the greedy tie-flip at depth: gate 135/256.
6975            // NOFUSE bisect: disabling ONLY this fusion -> verify maxdiff 0.000e0). With the
6976            // gate dropped, decode AND verify ride the same fused chain — parity by
6977            // construction, VERIFY-GATE 0.000e0.
6978            let fuse_exit = e.uses_q8_1_fast(&e4b.inp_gate);
6979            let (sn, attn_out) = self.gemma4_layer_tail_core_pn(
6980                e, layer, &o, &x, t, Some(layer.post_attn_norm.float_data()), fuse_exit)?;
6981            let mut resid = e.uninit(t * n_embd)?;
6982            // per-layer-embedding tail: resid += rms(proj . (gelu(inp_gate . resid) * inp_pl[il]))
6983            // wave-2: the residual add emits q8_1 alongside — inp_gate rides matmul_pre.
6984            // (PLE one-block mega-fusion PROBED NEGATIVE 2026-07-13: argmax-correct but
6985            // 126 vs 189 tok/s — one SM pulling 0.74MB of weights loses to the multi-block
6986            // launch chain it replaced; jsonl row. Kernel deleted per doctrine.)
6987            let g = if fuse_exit {
6988                // sn here = RAW f0 (post_ffw deferred).
6989                let (rq, rd) = e.rms_pre_add_q8_1(&sn, bits.post_ffw_norm.float_data(),
6990                                                  &attn_out, &mut resid, n_embd, t,
6991                                                  self.cfg.rms_eps)?;
6992                e.matmul_pre(&e4b.inp_gate, &rq, &rd, &resid, t)?
6993            } else {
6994                e.add(&sn, &attn_out, &mut resid, t * n_embd)?;
6995                e.matmul(&e4b.inp_gate, &resid, t)?
6996            };
6997            let mut act = e.uninit(t * n_epl)?;
6998            let y = if t == 1 && e.uses_q8_1_fast(&e4b.proj) {
6999                let ipv = e.view(&inp_pl, n_epl * n_layer);
7000                let row = ipv.slice(il * n_epl..(il + 1) * n_epl);
7001                let (aq, ad) = e.gelu_tanh_mul_q8_1(&g, &row, &mut act, n_epl, 1)?;
7002                e.matmul_pre(&e4b.proj, &aq, &ad, &act, t)?
7003            } else {
7004                let mut inp_this = e.uninit(t * n_epl)?;
7005                e.copy_rows_strided(&inp_pl, &mut inp_this, n_epl, t, n_epl * n_layer,
7006                                    il * n_epl)?;
7007                e.gelu_tanh_mul(&g, &inp_this, &mut act, t * n_epl)?;
7008                e.matmul(&e4b.proj, &act, t)?
7009            };
7010            // rms(y, post_norm) + (yn + resid)*layer_scale + next-layer norm+quant emit,
7011            // ONE launch (glue-fusion lane; last layer emits through output_norm).
7012            let next_norm = if il + 1 < n_layer {
7013                self.layers[il + 1].attn_norm.float_data()
7014            } else {
7015                self.output_norm.float_data()
7016            };
7017            let mut xn = e.uninit(t * n_embd)?;
7018            let pair = e.rms_pre_add_scale_rms_norm_q8_1(&y, e4b.post_norm.float_data(),
7019                                                         &resid, bits.layer_scale, next_norm,
7020                                                         &mut xn, n_embd, t, eps)?;
7021            h_carry = Some(pair);
7022            x = xn;
7023        }
7024        // the head consumes the last layer's fused (output_norm) emit. head_last callers
7025        // (prime, last_only forward) need only the final row's logits — the all-T head is
7026        // t*n_vocab of discarded work (E4B prime: ~134ms GEMM + a 2.26GB dtoh kept 1 row).
7027        let (oq, odq) = h_carry.take().unwrap();
7028        let h0 = e.zeros(0)?;
7029        let hm = if head_last { 1 } else { t };
7030        let (hq, hd) = if head_last && t > 1 {
7031            let mut q1 = e.uninit_i8(n_embd)?;
7032            e.dtod_copy_view_i8(&oq.slice((t - 1) * n_embd..t * n_embd), &mut q1)?;
7033            let nb = n_embd / 32;
7034            let mut d1 = e.uninit(nb)?;
7035            e.dtod_copy_view(&odq.slice((t - 1) * nb..t * nb), &mut d1)?;
7036            (q1, d1)
7037        } else {
7038            (oq, odq)
7039        };
7040        let mut ld = e.matmul_pre(&self.output, &hq, &hd, &h0, hm)?;
7041        // softcap is strictly monotonic — greedy (argmax-only) consumers skip it, matching
7042        // the 26B/31B dc precedent (their dc head goes matmul -> argmax with no cap).
7043        // Logit-returning callers (host logits / spec prime) keep the capped emit.
7044        if cap_logits {
7045            let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
7046            e.softcap(&mut ld, cap, hm * self.output.out_features())?;
7047        }
7048        self.gemma4_suppress(e, &mut ld, hm)?;  // mask both capped and argmax-only consumers
7049        Ok((ld, x))
7050    }
7051
7052    /// E4B batched VERIFY (device tokens, the spec round's t=K+1 step): t rows through the
7053    /// e4b trunk (per-row causal attention; own-KV layers append t rows host-len, KV-shared
7054    /// layers ride their targets), per-row device argmax + the POST-output_norm hidden
7055    /// stack (the drafter's h convention). Advances cache.pos/kvl.len by t — the spec
7056    /// round rolls back rejected rows (shared layers have no KvLayer, so the plain rewind
7057    /// covers exactly the layers that appended).
7058    pub fn gemma4_e4b_decode_step_t_am_dev(&self, e: &Engine, tok_d: &CudaSlice<u32>,
7059                                                  t: usize, pos0: usize, cache: &mut Cache)
7060                                                  -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
7061        let n_embd = self.cfg.n_embd as usize;
7062        let eps = self.cfg.rms_eps;
7063        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
7064        let pos_d = e.htod_i32(&pos)?;
7065        let embd_gpu = self.embd_gpu.get_or_init(|| {
7066            e.upload_u8(&self.embd.raw).expect("embed table upload")
7067        });
7068        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
7069        let mut x = e.embed_gather_device_td(embd_gpu, tok_d, t, n_embd, qt, rb)?;
7070        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
7071        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, tok_d, &x, t)?;
7072        let (ld, xp) = self.gemma4_e4b_trunk_core(e, x, inp_pl, &pos_d, t, cache, None, true,
7073                                                  false)?;
7074        // softcap is monotonic — the per-row argmax is invariant to it (the trunk's head
7075        // emit is already capped, matching the eager chain bit-for-bit).
7076        let n_vocab = self.output.out_features();
7077        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
7078        for i in 0..t {
7079            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
7080        }
7081        let mut hn = e.uninit(t * n_embd)?;
7082        e.rms_norm(&xp, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
7083        cache.pos += t;
7084        Ok((vam, hn))
7085    }
7086
7087    /// E4B verify + host logits + POST-output_norm hidden stack (the short-prompt spec
7088    /// prime path — mirror of `gemma4_decode_step_t_h`).
7089    pub(crate) fn gemma4_e4b_decode_step_t_h(&self, e: &Engine, tokens: &[u32], pos0: usize,
7090                                             cache: &mut Cache)
7091                                             -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
7092        let n_embd = self.cfg.n_embd as usize;
7093        let eps = self.cfg.rms_eps;
7094        let t = tokens.len();
7095        let (ld, xp) = self.gemma4_e4b_trunk(e, tokens, pos0, cache, false)?;
7096        let mut hn = e.uninit(t * n_embd)?;
7097        e.rms_norm(&xp, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
7098        cache.pos += t;
7099        Ok((e.dtoh(&ld)?, hn))
7100    }
7101
7102    /// E4B GRAPH-CAPTURABLE dc step: same trunk as the dc step but token_d is updated IN
7103    /// PLACE (self-feeding replay) and every launch arg is a device counter — pos from
7104    /// pos_d (inc'd in-stream), KV slots from len_d (advanced in-stream), attention from
7105    /// fa_decode_dc at `bucket`. Host mirrors (cache.pos / kvl.len) advance in the caller's
7106    /// replay loop. UNDER-WINDOW regime only (the caller gates pos + budget < window).
7107    pub fn gemma4_e4b_decode_step_dcg(&self, e: &Engine, token_d: &mut CudaSlice<u32>,
7108                                      pos_d: &mut CudaSlice<i32>, embd_gpu: &CudaSlice<u8>,
7109                                      embd_qt: i32, embd_rb: usize, cache: &mut Cache,
7110                                      n_vocab: usize, bucket: usize)
7111                                      -> Result<(), Box<dyn std::error::Error>> {
7112        let n_embd = self.cfg.n_embd as usize;
7113        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
7114        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
7115        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, token_d, &x, 1)?;
7116        let (ld, _x) = self.gemma4_e4b_trunk_core(e, x, inp_pl, pos_d, 1, cache, Some(bucket),
7117                                                  false, false)?;
7118        e.argmax_token_device_into(&ld, token_d, n_vocab)?;
7119        e.inc_seqlen(pos_d)?;
7120        Ok(())
7121    }
7122
7123    /// E4B DEVICE-COUNTER decode step (the dc serving arm): token id rides `token_d`, the
7124    /// greedy argmax lands in the returned device buffer — 4B/token host traffic. The layer
7125    /// stack is gemma4_e4b_trunk_core, i.e. the SAME functions the eager chain runs (stream
7126    /// identity by construction, not by twin-kernel parity). Host KV mirrors advance like the
7127    /// 26B dc-eager arm (window views are host math); len_d stays synced by the caller's
7128    /// entry sync + the appends here don't read it. Graph capture is NOT wired (no
7129    /// cap_bucket_max) — the E4B graph arc comes after the perf gates.
7130    #[allow(clippy::too_many_arguments)]
7131    pub fn gemma4_e4b_decode_step_dc(&self, e: &Engine, token_d: &CudaSlice<u32>,
7132                                     pos_d: &mut CudaSlice<i32>, embd_gpu: &CudaSlice<u8>,
7133                                     embd_qt: i32, embd_rb: usize, cache: &mut Cache,
7134                                     n_vocab: usize)
7135                                     -> Result<CudaSlice<u32>, Box<dyn std::error::Error>> {
7136        let n_embd = self.cfg.n_embd as usize;
7137        let eps = self.cfg.rms_eps;
7138        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
7139        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
7140        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, token_d, &x, 1)?;
7141        let (ld, _x) = self.gemma4_e4b_trunk_core(e, x, inp_pl, pos_d, 1, cache, None, false,
7142                                                  false)?;
7143        let mut tok_out = e.stream().alloc_zeros::<u32>(1)?;
7144        e.argmax_token_device_into(&ld, &mut tok_out, n_vocab)?;
7145        e.inc_seqlen(pos_d)?;
7146        cache.pos += 1;
7147        let _ = eps;
7148        Ok(tok_out)
7149    }
7150
7151    /// E4B eager T=1 decode step (decode_step_h contract): returns (softcapped logits host,
7152    /// pre-output_norm hidden). Advances cache.pos.
7153    pub(crate) fn gemma4_e4b_decode_step_h(&self, e: &Engine, token: u32, cache: &mut Cache)
7154                                           -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
7155        let (ld, x) = self.gemma4_e4b_trunk(e, &[token], cache.pos, cache, false)?;
7156        let logits = e.dtoh(&ld)?;
7157        cache.pos += 1;
7158        Ok((logits, x))
7159    }
7160
7161    /// E4B batched prime (prime_cache contract: (last-row logits host, h_seed pre-norm last
7162    /// row, hidden stack)). First-light: the t-wide trunk (per-row attention) — correct, not
7163    /// fast; the prefill fa arms come later.
7164    pub(crate) fn gemma4_e4b_prime(&self, e: &Engine, tokens: &[u32], cache: &mut Cache)
7165                                   -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
7166        assert_eq!(cache.pos, 0, "e4b prime is fresh-prompt only (v0)");
7167        let n_embd = self.cfg.n_embd as usize;
7168        let t = tokens.len();
7169        let (ld, x) = self.gemma4_e4b_trunk(e, tokens, 0, cache, true)?;
7170        cache.pos += t;
7171        let last = e.dtoh(&ld)?;   // head_last: ld is already the final row only
7172        let xv = e.view(&x, t * n_embd);
7173        let row = xv.slice((t - 1) * n_embd..t * n_embd);
7174        let mut h_seed = e.uninit(n_embd)?;
7175        e.copy_view_into(&mut h_seed, 0, &row, n_embd)?;
7176        Ok((last, h_seed, x))
7177    }
7178
7179    /// E4B prefill logits (forward contract — no persistent cache; scratch cache internally).
7180    pub(crate) fn gemma4_e4b_forward(&self, e: &Engine, tokens: &[u32], last_only: bool)
7181                                     -> Result<Vec<f32>, Box<dyn std::error::Error>> {
7182        let mut cache = Cache::new(e, &self.cfg, tokens.len() + 8)?;
7183        let (ld, _x) = self.gemma4_e4b_trunk(e, tokens, 0, &mut cache, last_only)?;
7184        Ok(e.dtoh(&ld)?)   // head_last already reduced to the final row when last_only
7185    }
7186}
7187
7188#[cfg(test)]
7189mod page_prefetch_tests {
7190    use super::{
7191        grouped_worker_prefetch_position, page_prefetch_positions,
7192        page_prefetch_window_from_values, worker_prefetch_positions,
7193    };
7194
7195    #[test]
7196    fn page_prefetch_window_keeps_existing_opt_in_default() {
7197        assert_eq!(page_prefetch_window_from_values(false, None), 0);
7198        assert_eq!(page_prefetch_window_from_values(false, Some("8")), 0);
7199        assert_eq!(page_prefetch_window_from_values(true, None), 1);
7200        assert_eq!(page_prefetch_window_from_values(true, Some("bad")), 1);
7201        assert_eq!(page_prefetch_window_from_values(true, Some("0")), 0);
7202        assert_eq!(page_prefetch_window_from_values(true, Some("8")), 8);
7203    }
7204
7205    #[test]
7206    fn rolling_page_prefetch_advises_each_future_expert_once() {
7207        let advised: Vec<_> = (0..7)
7208            .flat_map(|position| page_prefetch_positions(position, 7, 3))
7209            .collect();
7210        assert_eq!(advised, vec![1, 2, 3, 4, 5, 6]);
7211
7212        let one_ahead: Vec<_> = (0..4)
7213            .flat_map(|position| page_prefetch_positions(position, 4, 1))
7214            .collect();
7215        assert_eq!(one_ahead, vec![1, 2, 3]);
7216        assert!(page_prefetch_positions(0, 4, 0).is_empty());
7217    }
7218
7219    #[test]
7220    fn grouped_worker_prefetch_primes_first_then_each_known_next_once() {
7221        assert_eq!(grouped_worker_prefetch_position(0, None), None);
7222        let positions: Vec<_> = std::iter::once(grouped_worker_prefetch_position(4, None).unwrap())
7223            .chain((0..4).filter_map(|position| {
7224                grouped_worker_prefetch_position(4, Some(position))
7225            }))
7226            .collect();
7227        assert_eq!(positions, vec![0, 1, 2, 3]);
7228        assert_eq!(grouped_worker_prefetch_position(1, Some(0)), None);
7229    }
7230
7231    #[test]
7232    fn rolling_worker_prefetch_primes_current_and_each_future_expert_once() {
7233        let queued: Vec<_> = (0..8)
7234            .flat_map(|position| worker_prefetch_positions(position, 8, 5))
7235            .collect();
7236        assert_eq!(queued, (0..8).collect::<Vec<_>>());
7237
7238        let one_at_a_time: Vec<_> = (0..4)
7239            .flat_map(|position| worker_prefetch_positions(position, 4, 1))
7240            .collect();
7241        assert_eq!(one_at_a_time, vec![0, 1, 2, 3]);
7242        assert!(worker_prefetch_positions(0, 4, 0).is_empty());
7243    }
7244}
7245
7246pub struct G4DcSlots {
7247    x: CudaSlice<f32>, xn: CudaSlice<f32>, cur: CudaSlice<f32>,
7248    hq: CudaSlice<i8>, hd_: CudaSlice<f32>,
7249    q0: CudaSlice<f32>, k0: CudaSlice<f32>, v0: CudaSlice<f32>,
7250    q: CudaSlice<f32>, k: CudaSlice<f32>, v: CudaSlice<f32>,
7251    attn: CudaSlice<f32>, o: CudaSlice<f32>,
7252    attn_out: CudaSlice<f32>, zsh: CudaSlice<f32>,
7253    zq: CudaSlice<i8>, zd: CudaSlice<f32>,
7254    gate: CudaSlice<f32>, up: CudaSlice<f32>,
7255    act: CudaSlice<f32>, actq: CudaSlice<i8>, actd: CudaSlice<f32>,
7256    f0: CudaSlice<f32>, sn: CudaSlice<f32>,
7257    hn: CudaSlice<f32>, logits: CudaSlice<f32>,
7258}
7259