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 crate::Engine;
6use crate::cache::Cache;
7use cudarc::driver::CudaSlice;
8use memra_gguf::config::ModelConfig;
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// Split prime ranges cannot enter the full-range segment-graph arm, and every slab access
41// is serialized by its device mutex after binding that device's CUDA context on the thread.
42unsafe impl Send for PrimeSlabs {}
43
44fn empty_cache_layers<T>(n: usize) -> Vec<Option<T>> {
45    std::iter::repeat_with(|| None).take(n).collect()
46}
47
48/// Temporarily move a PP-2 cache's layer state into two independently-owned cache shells.
49/// The stage walkers then receive disjoint `&mut Cache` values and can run on separate host
50/// threads without aliasing. GPU buffers are moved, not copied; Drop restores every layer
51/// and publishes the last position completed by both stages.
52struct PrimeCacheStages<'a> {
53    parent: &'a mut Cache,
54    cut: usize,
55    stage0: Cache,
56    stage1: Cache,
57}
58
59impl<'a> PrimeCacheStages<'a> {
60    fn new(parent: &'a mut Cache, cut: usize) -> Self {
61        let n = parent.kv.len();
62        assert_eq!(parent.recur.len(), n, "cache layer vectors disagree");
63        assert!(cut <= n, "PP-2 cache cut {cut} exceeds {n} layers");
64        let mut kv0 = empty_cache_layers(n);
65        let mut kv1 = empty_cache_layers(n);
66        let mut recur0 = empty_cache_layers(n);
67        let mut recur1 = empty_cache_layers(n);
68        for i in 0..cut {
69            kv0[i] = parent.kv[i].take();
70            recur0[i] = parent.recur[i].take();
71        }
72        for i in cut..n {
73            kv1[i] = parent.kv[i].take();
74            recur1[i] = parent.recur[i].take();
75        }
76        let pos = parent.pos;
77        let max_ctx = parent.max_ctx;
78        Self {
79            parent,
80            cut,
81            stage0: Cache {
82                kv: kv0,
83                recur: recur0,
84                pos,
85                max_ctx,
86                last_logits_dev: None,
87                dflash_taps: None,
88            },
89            stage1: Cache {
90                kv: kv1,
91                recur: recur1,
92                pos,
93                max_ctx,
94                last_logits_dev: None,
95                dflash_taps: None,
96            },
97        }
98    }
99
100    fn parts(&mut self) -> (&mut Cache, &mut Cache) {
101        (&mut self.stage0, &mut self.stage1)
102    }
103}
104
105impl Drop for PrimeCacheStages<'_> {
106    fn drop(&mut self) {
107        let n = self.parent.kv.len();
108        for i in 0..n {
109            let source = if i < self.cut {
110                &mut self.stage0
111            } else {
112                &mut self.stage1
113            };
114            debug_assert!(self.parent.kv[i].is_none());
115            debug_assert!(self.parent.recur[i].is_none());
116            self.parent.kv[i] = source.kv[i].take();
117            self.parent.recur[i] = source.recur[i].take();
118        }
119        self.parent.pos = self.stage0.pos.min(self.stage1.pos);
120    }
121}
122
123/// task #18 (attn side): one sequence's pre-attention outputs (post-rope q/k, v, out-gate).
124pub(crate) struct AttnPre {
125    pub q: cudarc::driver::CudaSlice<f32>,
126    pub k: cudarc::driver::CudaSlice<f32>,
127    pub v: cudarc::driver::CudaSlice<f32>,
128    pub gate: Option<cudarc::driver::CudaSlice<f32>>,
129}
130
131/// task #18: one sequence's GDN prep outputs (the scan inputs).
132pub(crate) struct GdnPrep {
133    pub hk: usize,
134    pub q_l2: cudarc::driver::CudaSlice<f32>,
135    pub k_l2: cudarc::driver::CudaSlice<f32>,
136    pub v_g: cudarc::driver::CudaSlice<f32>,
137    pub beta: cudarc::driver::CudaSlice<f32>,
138    pub g_log: cudarc::driver::CudaSlice<f32>,
139    pub kb16: Option<cudarc::driver::CudaSlice<u8>>,
140    pub qb16: Option<cudarc::driver::CudaSlice<u8>>,
141}
142
143/// Device scratch for the burst verify stream (see `verify_stream_scratch`).
144pub(crate) struct VerifyStreamScratch {
145    pub pos_d: CudaSlice<i32>,
146    pub row_ctrs: Vec<CudaSlice<i32>>,
147}
148use crate::hybrid::{FullAttnLayer, HybridModel, LinearAttnLayer, Mixer, MoeWeights};
149
150struct MoeInputTraceWriter {
151    dir: std::path::PathBuf,
152    index: std::fs::File,
153    payloads: std::collections::HashMap<u16, (std::fs::File, u64)>,
154}
155
156static MOE_INPUT_TRACE_WRITER: std::sync::OnceLock<std::sync::Mutex<Option<MoeInputTraceWriter>>> =
157    std::sync::OnceLock::new();
158
159/// STAGE-2 GROUPED DECODE gate (MEMRA_MOE_GDEC, default ON; `=0` restores the sequential
160/// per-expert launch chain). See `moe_gdec_token`.
161fn gdec_enabled() -> bool {
162    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
163    *E.get_or_init(|| {
164        std::env::var("MEMRA_MOE_GDEC")
165            .map(|v| v != "0")
166            .unwrap_or(true)
167    })
168}
169
170/// SLAB-LOCAL RESIDENT ARM gate (lane/pp-leverb 2026-08-08, MEMRA_MOE_SLAB, default ON;
171/// `=0` restores the SLRU dispatch even when resident slabs exist). Read PER CALL, never
172/// memoized — probes A/B the two provenances in one process (the MEMRA_PRIME_PP pattern).
173/// See `moe_ffn_sequential_zq8`'s slab_local arm: the sigmoid-router archs (step35/M3/Hy3)
174/// are denied every `dev_exps` consumer (pairs/dev route softmax), so before this arm the
175/// fits-VRAM resident slabs were UPLOADED for them but never READ — the SLRU kept staging
176/// the same bytes beside a dead copy (37 GB H2D per pp4096 prime on the Step SKU, anatomy
177/// receipt). The arm reads the SAME bytes through the SAME kernels; only the pointer
178/// PROVENANCE changes (slab base + ex*stride vs SLRU slot address) — the bit-identity class
179/// `moe_ffn_dev`'s resident arm already documents against its SLRU arm.
180fn moe_slab_enabled() -> bool {
181    std::env::var("MEMRA_MOE_SLAB").as_deref() != Ok("0")
182}
183
184/// Expert-grouped dispatch remains opt-in after the local 5090 transfer gate rejected the
185/// default flip. `=0` selects the established path, while any other explicit value enables the
186/// grouped research arm for the current call.
187fn moe_grouped_enabled(_cfg: &ModelConfig, _prefill: bool) -> bool {
188    std::env::var("MEMRA_MOE_GROUPED")
189        .map(|value| value != "0")
190        .unwrap_or(false)
191}
192
193/// Deterministic in-token expert prefetch. `MEMRA_MOE_PREFETCH=1` overlaps memory-source H2D on the
194/// copy stream; selecting the opt-in worker spill backend enables the same known-next hook for disk.
195fn moe_prefetch_enabled() -> bool {
196    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
197    *E.get_or_init(|| {
198        std::env::var("MEMRA_MOE_PREFETCH").as_deref() == Ok("1")
199            || crate::spill_pread::worker_enabled()
200    })
201}
202
203/// Best-effort OS page-cache prefetch distance for mmap-backed expert ranges. Independent of the
204/// H2D copy-stream experiment so storage->RAM and RAM->HBM overlap can be measured separately.
205/// The opt-in default stays one expert to preserve the original experiment; spill rigs can widen
206/// it with `MEMRA_MOE_PAGE_PREFETCH_WINDOW` to cover NVMe latency.
207fn moe_page_prefetch_window() -> usize {
208    static W: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
209    *W.get_or_init(|| {
210        page_prefetch_window_from_values(
211            std::env::var("MEMRA_MOE_PAGE_PREFETCH").as_deref() == Ok("1"),
212            std::env::var("MEMRA_MOE_PAGE_PREFETCH_WINDOW")
213                .ok()
214                .as_deref(),
215        )
216    })
217}
218
219fn page_prefetch_window_from_values(enabled: bool, raw_window: Option<&str>) -> usize {
220    if !enabled {
221        return 0;
222    }
223    raw_window.and_then(|value| value.parse().ok()).unwrap_or(1)
224}
225
226/// Return only the newly exposed positions in a rolling lookahead window. Position zero seeds the
227/// full window; each later position adds one expert at the far edge. Thus widening the window does
228/// not repeatedly issue `MADV_WILLNEED` for the same range.
229fn page_prefetch_positions(position: usize, len: usize, window: usize) -> std::ops::Range<usize> {
230    if window == 0 || position >= len {
231        return len..len;
232    }
233    let (start, count) = if position == 0 {
234        (1, window)
235    } else {
236        (position.saturating_add(window), 1)
237    };
238    let start = start.min(len);
239    start..start.saturating_add(count).min(len)
240}
241
242/// Grouped worker-I/O schedule: prime the first active expert before the loop, then queue exactly
243/// one known-next expert at each iteration. Returning positions keeps expert ordering authoritative.
244fn grouped_worker_prefetch_position(order_len: usize, current: Option<usize>) -> Option<usize> {
245    let position = current.map_or(0, |position| position.saturating_add(1));
246    (position < order_len).then_some(position)
247}
248
249/// Fill the worker ring with complete experts, retaining one pinned buffer for an unexpected
250/// demand miss. Each expert has gate/up/down extents, so depth 16 admits a rolling five-expert
251/// window. Position zero primes the current expert too: its three independent reads can run in
252/// parallel instead of demand-serializing gate, up, and down before any useful GPU work exists.
253fn worker_prefetch_window() -> usize {
254    static WINDOW: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
255    *WINDOW.get_or_init(|| {
256        let automatic = crate::spill_pread::configured_depth().saturating_sub(1) / 3;
257        std::env::var("MEMRA_SPILL_WORKER_EXPERT_WINDOW")
258            .ok()
259            .and_then(|value| value.parse::<usize>().ok())
260            .unwrap_or(automatic.max(1))
261    })
262}
263
264/// Return only positions newly exposed by a rolling worker-I/O window. Unlike mmap page advice,
265/// this includes the current expert when the window is seeded so all three current projections
266/// enter the CPU pool together.
267fn worker_prefetch_positions(position: usize, len: usize, window: usize) -> std::ops::Range<usize> {
268    if window == 0 || position >= len {
269        return len..len;
270    }
271    let (start, count) = if position == 0 {
272        (0, window)
273    } else {
274        (position.saturating_add(window).saturating_sub(1), 1)
275    };
276    let start = start.min(len);
277    start..start.saturating_add(count).min(len)
278}
279
280/// LAUNCH-STRUCTURE STAGE 3 gate (MEMRA_MOE_DEV, default ON; `=0` restores host routing). The
281/// zero-DtoH device-dispatch path for fully-resident layers: router top-k output stays on device,
282/// expert weight pointers come from the per-layer device table. Requires the fused router (the
283/// dev path consumes the device sel/w directly), so MEMRA_FUSED_ROUTER=0 also disables it.
284fn moe_dev_enabled() -> bool {
285    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
286    *E.get_or_init(|| {
287        std::env::var("MEMRA_MOE_DEV")
288            .map(|v| v != "0")
289            .unwrap_or(true)
290            && !matches!(std::env::var("MEMRA_FUSED_ROUTER").as_deref(), Ok("0"))
291    })
292}
293
294/// Device sigmoid top-k is the default for Step-3.7 / M3 / Hy3 / GLM-DSA. `MEMRA_SIG_ROUTER=0` restores
295/// the full-logit DtoH plus `moe_route_sigmoid_host` oracle without changing expert dispatch.
296fn sigmoid_router_enabled() -> bool {
297    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
298    *E.get_or_init(|| {
299        std::env::var("MEMRA_SIG_ROUTER")
300            .map(|v| v != "0")
301            .unwrap_or(true)
302    })
303}
304
305/// MoE EXPERT dp4a gate (MEMRA_MOE_Q8, default ON; `=0` restores the Stage-A f32-dequant expert
306/// kernels). Applies when gate/up/down expert qtypes are all in the dp4a body set (IQ3_S/IQ4_XS).
307/// FP-order differs from Stage-A (int dp4a + warp tree) — argmax/run-gen/stream-identity gates
308/// arbitrate; the sequential and fused q8 paths ship as a matched pair (MEMRA_MOE_GATE contract).
309fn moe_q8_enabled() -> bool {
310    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
311    *E.get_or_init(|| {
312        std::env::var("MEMRA_MOE_Q8")
313            .map(|v| v != "0")
314            .unwrap_or(true)
315    })
316}
317
318/// gemma4 fast-arm gate: qtypes with an `expert_dot_g` dp4a body (superset used by the gelu
319/// dev arm; the qwen q8 arms keep their own battery-gated q8_expert_supported policy).
320fn expert_dp4a_supported(qt: i32) -> bool {
321    qt == crate::QT_Q4_0
322        || qt == crate::QT_IQ3_S
323        || qt == crate::QT_IQ4_XS
324        || qt == crate::QT_Q3_K
325        || qt == crate::QT_Q4_K
326        || qt == crate::QT_Q6_K
327}
328
329fn q8_expert_supported(qt: i32) -> bool {
330    // k-quant arms added 2026-07-06 (Q3_K/Q4_K/Q6_K bodies for the UD tail layers). Briefly
331    // default-excluded the same day when they appeared to break 35B real-prompt spec — the
332    // ACTUAL culprit was the MoE router's cuBLASLt n-dependence (d994271); with the router
333    // decode-exact at verify t, the k-quant arms pass the full spec battery (p1/p2/p3 + raw
334    // K=1..8) and are DEFAULT ON again (+9 tok/s: 148.9 -> 157.9). MEMRA_MOE_Q8_KQ=0 excludes.
335    static KQ: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
336    let kq = *KQ.get_or_init(|| {
337        std::env::var("MEMRA_MOE_Q8_KQ")
338            .map(|v| v != "0")
339            .unwrap_or(true)
340    });
341    // NVFP4 experts: DEFAULT ON (2026-07-17). The M3-era "decode-vs-verify MISMATCH 3.4e1"
342    // that had this excluded was the missing per-expert macro-scale fold, fixed in the
343    // dev-kernel epilogues + moe_w_scale_by_expert; the 35B ct-NVFP4 artifact now runs the
344    // q8 arm at parity with the IQ4_XS daily (174-178 tok/s, spec K=1..8 exact). M3/Hy3
345    // never reach the q8 arms regardless (sigmoid-router cfg gates on pairs/dev/gdec).
346    // MEMRA_MOE_Q8_NVFP4=0 restores the f32 arm.
347    let nvfp4_q8 = std::env::var("MEMRA_MOE_Q8_NVFP4")
348        .map(|v| v != "0")
349        .unwrap_or(true);
350    qt == crate::QT_IQ3_S
351        || qt == crate::QT_IQ4_XS
352        || (nvfp4_q8 && qt == crate::QT_NVFP4)
353        || (kq && (qt == crate::QT_Q3_K || qt == crate::QT_Q4_K || qt == crate::QT_Q6_K))
354}
355
356/// The decode-once (_dec) and IQ-MMA expert kernels dequant via IQ-specific extractors —
357/// k-quant tensors must fall to the _em dot path instead.
358fn q8_expert_dec_supported(qt: i32) -> bool {
359    qt == crate::QT_IQ3_S || qt == crate::QT_IQ4_XS || qt == crate::QT_Q4_0
360}
361
362/// Grouped-f16 door (MEMRA_MOE_F16G) per-projection admission: the qtype has a dequant-to-f16
363/// kernel in cu/moe_f16_grouped.cu AND the projection's k dimension tiles its block size.
364/// Round 49 widened coverage to q35's UD mix (gate/up IQ3_S x39 + Q3_K x1 + IQ4_XS x1; down
365/// IQ4_XS x37 + Q6_K x3 + Q4_K x1) — the round-47 IQ4_XS/Q4_0-only table admitted ~1 of 41
366/// q35 layers, which is why that cell measured FLAT.
367fn f16g_proj_ok(qt: i32, in_f: usize) -> bool {
368    match qt {
369        crate::QT_Q4_0 => in_f % 32 == 0,
370        crate::QT_IQ4_XS | crate::QT_IQ3_S | crate::QT_Q3_K | crate::QT_Q4_K | crate::QT_Q6_K => {
371            in_f % 256 == 0
372        }
373        _ => false,
374    }
375}
376
377/// STAGE 3 prewarm gate (MEMRA_MOE_PREWARM, default ON; `=0` leaves residency organic). One-shot
378/// per layer: force-admit every block while FREE slots cover the whole layer (never evicts).
379fn moe_prewarm_enabled() -> bool {
380    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
381    *E.get_or_init(|| {
382        std::env::var("MEMRA_MOE_PREWARM")
383            .map(|v| v != "0")
384            .unwrap_or(true)
385    })
386}
387
388/// During a discarded fixed-residency profile, admit CPU-routed misses after their current-token
389/// CPU result is complete. The current result and numeric path are unchanged; later warmup tokens
390/// can then vote for and exercise those experts on GPU before the cache is frozen.
391fn cpu_expert_profile_admit_enabled() -> bool {
392    static E: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
393    *E.get_or_init(|| std::env::var("MEMRA_CPU_EXPERT_FREEZE_PROFILE_ADMIT").as_deref() == Ok("1"))
394}
395
396/// Minimum prompt length for the BATCHED cache prime (`prime_cache`). Below this the tokenwise
397/// decode loop wins anyway (the batched path's GEMM dispatch needs m>=16, and the stateful conv
398/// kernel needs T >= d_conv-1). Callers: generate / generate_spec.
399pub const PRIME_MIN_T: usize = 16;
400const PRIME_PIPE_MICROBATCHES: usize = 8;
401const PRIME_PIPE_MIN_CHUNK: usize = 128;
402const PRIME_PIPE_EDGE_MIN_CHUNK: usize = 64;
403const PRIME_PIPE_LINEAR_WORK: usize = 8;
404
405fn prime_pp2_auto_geometry(n_layers: usize) -> bool {
406    crate::pp::prime_pp_on()
407        && !crate::pp::pp2_streams_off()
408        && crate::pp::pp_cuts(n_layers).is_some_and(|cuts| cuts.len() == 3)
409}
410
411/// Effective internal prime chunk. An explicit MEMRA_PRIME_CHUNK is authoritative.
412/// Naked PP-2 primes use the measured pipeline geometry: up to eight microchunks, never
413/// below 128 tokens, while the legacy 4096-token cap remains the long-context bound.
414pub fn prime_chunk_tokens(t: usize, n_layers: usize) -> usize {
415    if let Ok(value) = std::env::var("MEMRA_PRIME_CHUNK") {
416        let parsed = value
417            .parse::<usize>()
418            .unwrap_or(crate::cache::PRIME_CHUNK_MAX_TOKENS);
419        return if crate::cache::swa_ring_on() {
420            if parsed == 0 {
421                crate::cache::PRIME_CHUNK_MAX_TOKENS
422            } else {
423                parsed.min(crate::cache::PRIME_CHUNK_MAX_TOKENS)
424            }
425        } else {
426            parsed
427        };
428    }
429    let chunk = crate::cache::PRIME_CHUNK_MAX_TOKENS;
430    if prime_pp2_auto_geometry(n_layers) && t >= 2 * PRIME_PIPE_MIN_CHUNK {
431        chunk.min(
432            t.div_ceil(PRIME_PIPE_MICROBATCHES)
433                .max(PRIME_PIPE_MIN_CHUNK),
434        )
435    } else {
436        chunk
437    }
438}
439
440fn fixed_prime_chunk_ranges(t: usize, chunk: usize) -> Vec<(usize, usize)> {
441    fixed_prime_chunk_ranges_for_ring(t, chunk, crate::cache::swa_ring_on())
442}
443
444fn fixed_prime_chunk_ranges_for_ring(t: usize, chunk: usize, ring_on: bool) -> Vec<(usize, usize)> {
445    if chunk == 0 || t <= chunk {
446        return vec![(0, t)];
447    }
448    let mut ranges = Vec::with_capacity(t.div_ceil(chunk));
449    let mut start = 0usize;
450    while start < t {
451        let mut end = (start + chunk).min(t);
452        if t - end > 0 && t - end < PRIME_MIN_T {
453            if ring_on {
454                let shifted = t - PRIME_MIN_T;
455                end = if shifted > start { shifted } else { t };
456            } else {
457                end = t;
458            }
459        }
460        ranges.push((start, end));
461        start = end;
462    }
463    ranges
464}
465
466fn prime_chunk_work(prefix: usize, total: usize) -> u128 {
467    let prefix = prefix as u128;
468    prefix * (prefix + (PRIME_PIPE_LINEAR_WORK as u128) * (total as u128))
469}
470
471fn dynamic_prime_chunk_ranges(
472    t: usize,
473    fixed_chunk: usize,
474    fixed: &[(usize, usize)],
475) -> Vec<(usize, usize)> {
476    let n = fixed.len();
477    if n < 3 {
478        return fixed.to_vec();
479    }
480
481    let max_first = t - (n - 1) * PRIME_MIN_T;
482    let first = fixed_chunk
483        .div_ceil(2)
484        .max(PRIME_PIPE_EDGE_MIN_CHUNK)
485        .min(max_first);
486    let mut ranges = Vec::with_capacity(n);
487    ranges.push((0, first));
488
489    let first_work = prime_chunk_work(first, t);
490    let work_span = prime_chunk_work(t, t) - first_work;
491    let denominator = (n - 1) as u128;
492    let mut previous = first;
493    for boundary in 1..n - 1 {
494        let target = first_work * denominator + work_span * (boundary as u128);
495        let remaining = n - 1 - boundary;
496        let mut low = previous + PRIME_MIN_T;
497        let mut high = t - remaining * PRIME_MIN_T;
498        while low < high {
499            let mid = low + (high - low) / 2;
500            if prime_chunk_work(mid, t) * denominator >= target {
501                high = mid;
502            } else {
503                low = mid + 1;
504            }
505        }
506        ranges.push((previous, low));
507        previous = low;
508    }
509    ranges.push((previous, t));
510    ranges
511}
512
513/// Internal prime ranges. The naked PP-2 pipeline defaults to a short-fill,
514/// equal-modeled-time schedule; MEMRA_PRIME_CHUNK_SCHED=fixed restores the measured
515/// equal-token ranges. An explicit MEMRA_PRIME_CHUNK always retains fixed semantics.
516pub fn prime_chunk_ranges(t: usize, n_layers: usize) -> Vec<(usize, usize)> {
517    let explicit_chunk = std::env::var_os("MEMRA_PRIME_CHUNK").is_some();
518    let chunk = prime_chunk_tokens(t, n_layers);
519    let fixed = fixed_prime_chunk_ranges(t, chunk);
520    let dynamic = match std::env::var("MEMRA_PRIME_CHUNK_SCHED") {
521        Ok(value) => value == "dynamic",
522        Err(_) => true,
523    };
524    if explicit_chunk || !dynamic || !prime_pp2_auto_geometry(n_layers) {
525        fixed
526    } else {
527        dynamic_prime_chunk_ranges(t, chunk, &fixed)
528    }
529}
530
531impl HybridModel {
532    /// CHUNK-INVARIANCE BISECT SEAM (lane/chunk-invariance): `MEMRA_PRIME_TRACE=<path>`
533    /// appends one JSONL row per (chunk, layer) with a hash of that layer's last-row
534    /// post-residual hidden. Diagnostic only — never on in a measured or gated run
535    /// (it forces a dtoh + host hash per layer).
536    fn prime_trace_path() -> Option<&'static str> {
537        static P: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
538        P.get_or_init(|| std::env::var("MEMRA_PRIME_TRACE").ok())
539            .as_deref()
540    }
541
542    /// Prefill forward over `tokens`; returns logits [T, n_vocab] (host f32).
543    pub fn forward(
544        &self,
545        e: &Engine,
546        tokens: &[u32],
547    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
548        if self.is_gemma4_e4b() {
549            return self.gemma4_e4b_forward(e, tokens, false);
550        }
551        if self.cfg.gemma4.is_some() {
552            return self.gemma4_forward(e, tokens, false);
553        }
554        let cfg = &self.cfg;
555        let n_embd = cfg.n_embd as usize;
556        let t = tokens.len();
557        let eps = cfg.rms_eps;
558        let pos: Vec<i32> = (0..t as i32).collect();
559        let pos_d = e.htod_i32(&pos)?;
560
561        let mut x = self.embed(e, tokens)?; // [T, n_embd]
562
563        for (il, layer) in self.layers.iter().enumerate() {
564            // attn_norm
565            let mut h = e.uninit(t * n_embd)?;
566            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
567
568            let mixed = match &layer.mixer {
569                Mixer::Full(fa) => self.full_attn(e, fa, &h, &pos_d, t, il)?,
570                Mixer::Linear(la) => self.linear_attn(e, la, &h, t)?,
571                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
572            };
573
574            // residual 1
575            let mut x1 = e.uninit(t * n_embd)?;
576            e.add(&x, &mixed, &mut x1, t * n_embd)?;
577
578            // pre-FFN norm (post_attention_norm), FFN (Dense or MoE), residual 2
579            let mut z = e.uninit(t * n_embd)?;
580            e.rms_norm(
581                &x1,
582                layer.post_attn_norm.float_data(),
583                &mut z,
584                n_embd,
585                t,
586                eps,
587            )?;
588            let ffn_out = match &layer.ffn {
589                crate::hybrid::Ffn::Dense {
590                    ffn_gate,
591                    ffn_up,
592                    ffn_down,
593                } => {
594                    let n_ff = ffn_gate.out_features();
595                    let mut g2 = e.matmul_group(&[ffn_gate, ffn_up], &z, t)?;
596                    let up = g2.pop().unwrap();
597                    let gate = g2.pop().unwrap();
598                    let mut act = e.uninit(t * n_ff)?;
599                    // A DENSE FFN reads the SHEXP clamp array: upstream's one `build_ffn` serves
600                    // both the dense MLP and the shared expert, and its limit is
601                    // `swiglu_clamp_shexp[il]` (llama-graph.cpp:1751). step35's leading dense
602                    // blocks 0-2 therefore key off clamp_shexp, not clamp_exp.
603                    Self::ffn_act_lim(
604                        e,
605                        &self.cfg,
606                        &gate,
607                        &up,
608                        1.0,
609                        1.0,
610                        self.cfg.clamp_shexp_at(il as u32),
611                        &mut act,
612                        t * n_ff,
613                    )?;
614                    e.matmul(ffn_down, &act, t)?
615                }
616                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il_prefill(e, m, &z, t, il as u16)?,
617            };
618            let mut x2 = e.uninit(t * n_embd)?;
619            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
620            x = x2;
621        }
622
623        let mut hn = e.uninit(t * n_embd)?;
624        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
625        let logits = e.matmul(&self.output, &hn, t)?;
626        Ok(e.dtoh(&logits)?)
627    }
628
629    /// Prefill that returns ONLY the last token's logits — the common case (greedy/sample needs
630    /// just the final position to start decode). Runs the trunk over all T, then the lm_head
631    /// (output.weight, the largest matrix — 248320 rows) on the LAST hidden row ONLY, not all T.
632    /// On a 512-token prompt this turns a [512,248320] GEMM into [1,248320] — the dominant prefill
633    /// cost (nsys: ~99ms when done for all T). Bit-identical last-row logits to forward()[last].
634    pub fn forward_last(
635        &self,
636        e: &Engine,
637        tokens: &[u32],
638    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
639        if self.cfg.gemma4.is_some() {
640            return self.gemma4_forward(e, tokens, true);
641        }
642        let cfg = &self.cfg;
643        let n_embd = cfg.n_embd as usize;
644        let t = tokens.len();
645        let eps = cfg.rms_eps;
646        let pos: Vec<i32> = (0..t as i32).collect();
647        let pos_d = e.htod_i32(&pos)?;
648
649        let mut x = self.embed(e, tokens)?; // [T, n_embd]
650        // MEMRA_LAYER_PROBE=1: synchronize + print after every stage — bisects an in-graph
651        // ILLEGAL_ADDRESS to (layer, stage) at ~1 line of output per layer (M3 bring-up tool).
652        let probe = std::env::var("MEMRA_LAYER_PROBE").is_ok();
653        for (il, layer) in self.layers.iter().enumerate() {
654            let mut h = e.uninit(t * n_embd)?;
655            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
656            if probe {
657                e.stream().synchronize()?;
658                eprintln!("[probe] L{il} norm ok");
659            }
660            let mixed = match &layer.mixer {
661                Mixer::Full(fa) => self.full_attn(e, fa, &h, &pos_d, t, il)?,
662                Mixer::Linear(la) => self.linear_attn(e, la, &h, t)?,
663                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
664            };
665            if probe {
666                e.stream().synchronize()?;
667                eprintln!("[probe] L{il} mixer ok");
668            }
669            let mut x1 = e.uninit(t * n_embd)?;
670            e.add(&x, &mixed, &mut x1, t * n_embd)?;
671            let mut z = e.uninit(t * n_embd)?;
672            e.rms_norm(
673                &x1,
674                layer.post_attn_norm.float_data(),
675                &mut z,
676                n_embd,
677                t,
678                eps,
679            )?;
680            let ffn_out = match &layer.ffn {
681                crate::hybrid::Ffn::Dense {
682                    ffn_gate,
683                    ffn_up,
684                    ffn_down,
685                } => {
686                    let n_ff = ffn_gate.out_features();
687                    let mut g2 = e.matmul_group(&[ffn_gate, ffn_up], &z, t)?;
688                    let up = g2.pop().unwrap();
689                    let gate = g2.pop().unwrap();
690                    let mut act = e.uninit(t * n_ff)?;
691                    // dense FFN keys off the SHEXP clamp array — see forward()'s note.
692                    Self::ffn_act_lim(
693                        e,
694                        &self.cfg,
695                        &gate,
696                        &up,
697                        1.0,
698                        1.0,
699                        self.cfg.clamp_shexp_at(il as u32),
700                        &mut act,
701                        t * n_ff,
702                    )?;
703                    e.matmul(ffn_down, &act, t)?
704                }
705                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il_prefill(e, m, &z, t, il as u16)?,
706            };
707            if probe {
708                e.stream().synchronize()?;
709                eprintln!("[probe] L{il} ffn ok");
710            }
711            let mut x2 = e.uninit(t * n_embd)?;
712            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
713            x = x2;
714        }
715        // norm over all T, then slice the LAST row and run lm_head on that single row.
716        let mut hn = e.uninit(t * n_embd)?;
717        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
718        let last = e.view(&hn, t * n_embd); // [T, n_embd]
719        let last_row = last.slice((t - 1) * n_embd..t * n_embd); // [1, n_embd]
720        let mut hlast = e.uninit(n_embd)?;
721        e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
722        let logits = e.matmul(&self.output, &hlast, 1)?; // [1, n_vocab] — lm_head on ONE row
723        Ok(e.dtoh(&logits)?)
724    }
725
726    /// BATCHED PROMPT PRIME (the measured #1 e2e gap, e2e-image-1): `forward_last`'s batched
727    /// prefill body EXTENDED to leave a DECODE-READY cache behind — vs the tokenwise prime's
728    /// ~102/38 tok/s (9B/27B) decode_step loop, this runs the whole prompt at prefill throughput.
729    ///   (a) full-attn layers append their T post-RoPE K/V rows into `cache.kv[il]` via the SAME
730    ///       per-row quantize kernel as the decode append (bit-identical cache bytes per row);
731    ///   (b) linear layers run STATEFULLY from the cache's current recurrent state (zero at a
732    ///       fresh prime): carried-ring conv (ssm_conv1d_tm_state) + ONE gdn_scan(state_in,
733    ///       state_out) whose internal sequential t-loop equals T chained T=1 steps — but with
734    ///       the NORMAL prefill matmul dispatch (GEMM at m>=16), NOT the decode-exact MMVQ the
735    ///       spec verify uses (prime is a prefill-regime pass; the run-gen prefill==decode
736    ///       argmax gate is the accuracy authority, exactly as for forward_last);
737    ///   (c) `cache.pos`/KV len/len_d advance by T.
738    /// Returns (last-row logits host, h_seed = last-row PRE-output_norm hidden [n_embd],
739    /// hiddens = the full pre-output_norm hidden stack [T, n_embd] — generate_spec's prompt_h).
740    /// FRESH-PROMPT ONLY (cache.pos == 0): the fa_prefill tiles attend within `tokens` alone.
741    /// forward_last itself stays untouched (kernel-check / run-gen gate on it).
742    ///
743    /// `queued_after` (lane/tick-seg, 2026-08-07): the number of prompt tokens of the SAME
744    /// REQUEST that the caller will prime in LATER calls — 0 when this call is the whole
745    /// request (every single-shot caller). Serve splits a long prompt across SEVERAL
746    /// prime_cache calls (one per scheduler tick, plus the prefix-cache LCP split), and the
747    /// request's absolute end position `seq_end = cache.pos + t + queued_after` steers step35's
748    /// SWA prefill arm — computing it per CALL made the arm a function of the tick budget
749    /// (budgets 512/256/64 DIFFER 1.813e0 vs monolithic, greedy diverging at step 6; dark lanes
750    /// default to 256 AND cap by live SLO headroom, so identical judge requests primed
751    /// differently under load — research/tick-seg-20260807, receipt in
752    /// research/step35-chunkfix-20260807 §9). The parameter is what prime_cache structurally
753    /// lacked: it cannot know from `tokens` and `cache` alone whether more of the request is
754    /// coming. A SESSION CONTINUATION (a NEW user turn primed onto a live cache) is a NEW
755    /// request — its arithmetic is keyed to its own extent, so those callers pass 0; only a
756    /// caller that SPLITS one request across calls passes the remainder.
757    pub fn prime_cache(
758        &self,
759        e: &Engine,
760        tokens: &[u32],
761        cache: &mut Cache,
762        queued_after: usize,
763    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
764        self.prime_cache_overlaid(e, tokens, cache, queued_after, None)
765    }
766
767    /// `prime_cache` with a vision embedding overlay (lane/vision): image merger outputs
768    /// replace the `<|image_pad|>` token embeddings at prompt-relative positions before the
769    /// trunk walk — the mixed-embedding prime. Text-only callers use `prime_cache` (overlay
770    /// None, byte-identical path). v1 scope: the serial chunk walk only — PP prime arms and
771    /// gemma4 refuse loudly (the vision serving box is single-GPU).
772    pub fn prime_cache_overlaid(
773        &self,
774        e: &Engine,
775        tokens: &[u32],
776        cache: &mut Cache,
777        queued_after: usize,
778        overlay: Option<&crate::vision::EmbedOverlay>,
779    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
780        let n_embd = self.cfg.n_embd as usize;
781        let t = tokens.len();
782        // SESSION CONTINUATION (2026-07-05): cache.pos > 0 = priming a NEW SUFFIX onto a live
783        // session cache — every chunk (including the first) takes the continuation arm
784        // (fa_prefill_view over the quantized past + this chunk). Fresh prime (pos==0) unchanged.
785        assert!(
786            t >= PRIME_MIN_T,
787            "prime_cache needs T >= {PRIME_MIN_T} (caller gates)"
788        );
789        assert!(
790            cache.pos + t <= cache.max_ctx,
791            "prime_cache: prompt exceeds cache max_ctx"
792        );
793
794        // CHUNKED PRIME (2026-07-05, the long-ctx OOM fix): the monolithic prime allocates
795        // per-layer transients proportional to T (gate/up/act = T*n_ff*4B EACH — 1.5GB apiece at
796        // 16k on the 27B), which OOMs a 24GB card around 16k prompt tokens. Chunk the prompt:
797        // each chunk runs the full layer stack with transients sized to the chunk, appending its
798        // K/V to the resident quantized cache and carrying the GDN conv-ring + recurrent state
799        // through `cache.recur` (linear_attn_prime is already stateful — a chunk boundary is
800        // exactly the state carry it was built for). Full-attn chunks after the first attend to
801        // the QUANTIZED past KV via fa_prefill_view (the spec-verify pattern) — same numeric
802        // class as decode reading the cache. Prompts <= one chunk take the ORIGINAL monolithic
803        // body byte-for-byte (chunk 0 short-circuits to the f32 fa_prefill path).
804        // MEMRA_PRIME_CHUNK sets the chunk size (tokens); 0 disables chunking (monolithic).
805        if self.is_gemma4_e4b() || self.cfg.gemma4.is_some() {
806            if overlay.is_some() {
807                return Err(
808                    "vision embedding overlay is qwen35-only (gemma4 prime refuses)".into(),
809                );
810            }
811            if self.is_gemma4_e4b() {
812                return self.gemma4_e4b_prime(e, tokens, cache);
813            }
814            // gemma4 v0: monolithic fresh-prompt prime (chunked/continuation arms later).
815            return self.gemma4_prime(e, tokens, cache);
816        }
817        let ranges = prime_chunk_ranges(t, self.layers.len());
818        // CHUNK-ORDER INVARIANCE (lane/chunk-invariance, 2026-08-05; vLLM #38561 shape).
819        // MEMRA_PRIME_CHUNK is documented as a memory-transient knob, but it also decides
820        // the prefill's ARITHMETIC, so two rigs with different values produced different
821        // greedy text for the same prompt (research/session-affinity-20260805: 97- and
822        // 149-token prompts). ROOT CAUSE, measured in research/chunk-invariance-20260805
823        // (VERDICT.md) — and it is NOT what docs originally said:
824        //   * NOT trunk GEMM m / reduction order. REFUTED: the prefill GEMM is m-INVARIANT
825        //     (rows [0,32) bit-identical at m=32 vs m=33..80, both quantized wq and the
826        //     output head), so growing a chunk cannot move an existing row's value.
827        //   * NOT the GDN scan segmentation. REFUTED: MEMRA_GDN_CHUNKED=0 (sequential scan,
828        //     no WY segmentation at all) still diverges, so vLLM's mamba-boundary fix does
829        //     not describe our leak.
830        //   * IT IS the attention numeric CLASS edge in full_attn_prime_fa_dispatch, which
831        //     selects on `base_len == 0`: chunk 0 attends over this batch's f32 K/V
832        //     (fa_prefill) while every later chunk attends over the q8_0/q5_1 quantized KV
833        //     cache (fa_prefill_view_ws). The chunk size therefore decides WHERE in the
834        //     prompt that precision edge falls. Signature: per-row maxdiff is exactly 0.0
835        //     before the first boundary and O(1) right after, first_div_pos == chunk size.
836        // The grain-free fix (full_attn_prime_fa_dispatch below) removed that class edge at
837        // the source — every row is in one numeric class, so the chunk size no longer steers
838        // arithmetic and MEMRA_PRIME_CHUNK is a pure memory/transient knob again. The interim
839        // MEMRA_PRIME_INVARIANT/MEMRA_PRIME_GRAIN pin-the-boundary door was superseded by that
840        // fix and KILLED at v0.71 per the flags doctrine (the jsonl + VERDICT.md are the
841        // record); the chunkinv gate asserts byte-identity across chunk sizes naked.
842        // The REQUEST's absolute end position, computed ONCE before the loop: every chunk sees the
843        // same value, whatever the chunk size. step35's SWA arm selects on it so that kernel
844        // selection — and therefore the logits — cannot depend on MEMRA_PRIME_CHUNK
845        // (research/step35-chunkfix-20260807; see step35_attn_pre_wo's doc note).
846        // `+ queued_after` closes the SECOND axis (lane/tick-seg): when serve splits the request
847        // across calls, the request still ends at the same absolute position, whatever the tick
848        // budget or LCP split point. MEMRA_PRIME_CALLLOCAL=1 is the ROLLBACK SEAM to the FULL
849        // pre-fix arithmetic: the per-call value here AND the unaligned FA view offset in
850        // step35_attn_pre_wo. Both halves are required for tickinv35's canary teeth under the FA
851        // default. Read per call, not cached (the probe flips it in-process between arms). Never
852        // on in a measured default run.
853        let legacy_calllocal = std::env::var("MEMRA_PRIME_CALLLOCAL").as_deref() == Ok("1");
854        let seq_end = if legacy_calllocal {
855            cache.pos + t
856        } else {
857            cache.pos + t + queued_after
858        };
859        if ranges.len() == 1 {
860            return self.prime_chunk(e, tokens, cache, seq_end, 0, overlay);
861        }
862        // PIPELINED PP-2 PRIME (lane/cx-pipeline-prime, 2026-08-08): overlap stage 0 of
863        // chunk N+1 with stage 1 of chunk N. The serial split stays reachable through
864        // MEMRA_PRIME_PIPE=0 and is the exactness oracle. N>2 keeps the serial walker;
865        // this lane owns the balanced two-stage schedule only.
866        if crate::pp::prime_pipe_on() && crate::pp::prime_pp_on() && !crate::pp::pp2_streams_off() {
867            if let Some(fence) = crate::pp::pp_cuts(self.layers.len()).filter(|f| f.len() == 3) {
868                if overlay.is_some() {
869                    return Err(
870                        "vision embedding overlay + pipelined PP prime unsupported (v1); \
871                         run the serial prime (single device or MEMRA_PRIME_PIPE=0)"
872                            .into(),
873                    );
874                }
875                if crate::pp::pp_multi_stream_same_device() {
876                    return Err(
877                        "prime chunk pipeline refused with 2 stage streams on one device — \
878                         that concurrent-stream placement remains quarantined by the deferred \
879                         pp flake record. Use one device per stage or MEMRA_PRIME_PIPE=0 for \
880                         the serial split."
881                            .into(),
882                    );
883                }
884                return self.prime_cache_pp2_pipelined(e, tokens, cache, seq_end, &ranges, &fence);
885            }
886        }
887        let mut hiddens = e.uninit(t * n_embd)?;
888        let mut last: Option<(Vec<f32>, CudaSlice<f32>)> = None;
889        for &(start, end) in &ranges {
890            let (l, hs, x) =
891                self.prime_chunk(e, &tokens[start..end], cache, seq_end, start, overlay)?;
892            e.copy_into(&mut hiddens, start * n_embd, &x, (end - start) * n_embd)?;
893            last = Some((l, hs));
894        }
895        let (logits, h_seed) = last.unwrap();
896        Ok((logits, h_seed, hiddens))
897    }
898
899    /// PP-2 chunk scheduler: stage 1 of chunk N and stage 0 of chunk N+1 are both queued
900    /// before N's epilogue D2H drains the last-stage stream. Arithmetic is unchanged:
901    /// every chunk still runs the same two `prime_layers` ranges, boundary copy, output
902    /// norm, lm head, and caller hidden-stack copy as the serial split.
903    fn prime_cache_pp2_pipelined(
904        &self,
905        e: &Engine,
906        tokens: &[u32],
907        cache: &mut Cache,
908        seq_end: usize,
909        ranges: &[(usize, usize)],
910        fence: &[usize],
911    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
912        debug_assert_eq!(fence.len(), 3);
913        debug_assert!(ranges.len() >= 2);
914        let rt = crate::pp::PpNRt::get(e)?;
915        assert_eq!(
916            rt.n_stages(),
917            2,
918            "prime pipeline requires exactly two PP stages"
919        );
920        let n_embd = self.cfg.n_embd as usize;
921        let t = tokens.len();
922        let initial_base = cache.pos;
923        let caller_stream = e.stream();
924
925        // #87 reverse publication before any new stage allocation, then prewarm both
926        // boundary slots while the stage streams are otherwise empty. Lazy-growing slot B
927        // after stage 1(N) is queued would synchronize that stream and erase the first
928        // overlap on a two-chunk prompt.
929        rt.fence_stages_behind(&caller_stream)?;
930        let max_payload = ranges.iter().map(|(s, e)| (e - s) * n_embd).max().unwrap();
931        rt.prepare_overlap_slots(0, max_payload)?;
932
933        let mut hiddens = e.uninit(t * n_embd)?;
934        let mut last: Option<(Vec<f32>, CudaSlice<f32>)> = None;
935        let mut stage_caches = PrimeCacheStages::new(cache, fence[1]);
936        let (cache0, cache1) = stage_caches.parts();
937        let (first_start, first_end) = ranges[0];
938        let mut slot = self.prime_pp2_stage0_enqueue(
939            e,
940            rt,
941            &tokens[first_start..first_end],
942            cache0,
943            seq_end,
944            fence,
945            initial_base + first_start,
946            true,
947        )?;
948        cache0.pos = initial_base + first_end;
949
950        for (i, &(start, end)) in ranges.iter().enumerate() {
951            let base = initial_base + start;
952            debug_assert_eq!(
953                cache1.pos, base,
954                "stage 1 must drain chunks in original position order"
955            );
956            let (out, next_slot) = if let Some(&(next_start, next_end)) = ranges.get(i + 1) {
957                let next_base = initial_base + next_start;
958                debug_assert_eq!(
959                    cache0.pos, next_base,
960                    "stage 0 must issue chunks in original position order"
961                );
962                let cache0_stage = &mut *cache0;
963                // Step's MoE router readback synchronizes once per layer. Two CUDA streams
964                // on one host thread therefore serialize even if the calls are ordered as
965                // a pipeline. Drive the disjoint stage caches from two scoped host threads:
966                // stage 1 consumes slot N while stage 0 produces slot N+1.
967                std::thread::scope(|scope| -> Result<_, Box<dyn std::error::Error>> {
968                    let stage0 = scope.spawn(move || -> Result<usize, String> {
969                        let next = self
970                            .prime_pp2_stage0_enqueue(
971                                e,
972                                rt,
973                                &tokens[next_start..next_end],
974                                cache0_stage,
975                                seq_end,
976                                fence,
977                                next_base,
978                                true,
979                            )
980                            .map_err(|err| err.to_string())?;
981                        cache0_stage.pos = initial_base + next_end;
982                        Ok(next)
983                    });
984                    let x = self.prime_pp2_stage1_enqueue(
985                        e,
986                        rt,
987                        slot,
988                        end - start,
989                        cache1,
990                        seq_end,
991                        fence,
992                        base,
993                        true,
994                    )?;
995                    let out = {
996                        rt.bind_stage(1)?;
997                        let _st1 = rt.enter(1);
998                        let e1 = rt.engine(1, e);
999                        self.prime_chunk_epilogue(e1, x, end - start, cache1)?
1000                    };
1001                    let next = stage0
1002                        .join()
1003                        .map_err(|_| "pipeprime stage-0 host walker panicked")?
1004                        .map_err(|err| -> Box<dyn std::error::Error> { err.into() })?;
1005                    Ok((out, Some(next)))
1006                })?
1007            } else {
1008                let x = self.prime_pp2_stage1_enqueue(
1009                    e,
1010                    rt,
1011                    slot,
1012                    end - start,
1013                    cache1,
1014                    seq_end,
1015                    fence,
1016                    base,
1017                    true,
1018                )?;
1019                let out = {
1020                    rt.bind_stage(1)?;
1021                    let _st1 = rt.enter(1);
1022                    let e1 = rt.engine(1, e);
1023                    self.prime_chunk_epilogue(e1, x, end - start, cache1)?
1024                };
1025                (out, None)
1026            };
1027
1028            rt.publish_to(1, &caller_stream)?;
1029            e.copy_into(&mut hiddens, start * n_embd, &out.2, (end - start) * n_embd)?;
1030            last = Some((out.0, out.1));
1031            crate::pp::PRIME_SPLIT_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1032
1033            if let Some(next) = next_slot {
1034                // The caller copy above reads a stage-1 allocation. Before stage 1 of the
1035                // next chunk can allocate/reuse blocks, mirror #87's body-entry fence.
1036                // Stage 0(N+1) is already queued before this wait is appended, so its
1037                // overlap with stage 1(N) is preserved.
1038                rt.fence_stages_behind(&caller_stream)?;
1039                slot = next;
1040            }
1041        }
1042
1043        debug_assert_eq!(cache0.pos, initial_base + t);
1044        debug_assert_eq!(cache1.pos, initial_base + t);
1045        let (logits, h_seed) = last.unwrap();
1046        Ok((logits, h_seed, hiddens))
1047    }
1048
1049    /// One prime chunk: the full layer stack over `tokens`, continuing from the cache's current
1050    /// state (`cache.pos` = tokens already primed; 0 = fresh). Positions/RoPE are absolute
1051    /// task #21 de-broadcast: the q/k head count PREP must emit so the scan's consumers
1052    /// agree. Compact (num_k) ONLY when the scan will take the chunked+mma route AND
1053    /// num_k == num_v/2 (the engine-side hint mirrors this exact formula); everything
1054    /// else (s128 verify tier, chunked-off, mma-off) keeps the broadcast layout.
1055    fn gdn_hk(e: &Engine, t: usize, num_v: usize, num_k: usize) -> usize {
1056        if Engine::gdn_db_on()
1057            && Engine::gdn_chunked_enabled()
1058            && t >= 16
1059            && e.gdn_mma_enabled(Engine::gdn_chunk_size())
1060            && num_k * 2 == num_v
1061        {
1062            num_k
1063        } else {
1064            num_v
1065        }
1066    }
1067
1068    /// task #17: gate for the fused fp16-operand epilogues (silu_mul/gated_rmsnorm/sig_mul
1069    /// `_f16out` twins). Bit-identical class (the twins emit the cvt kernel's exact halves),
1070    /// but seam-gated (MEMRA_F16OUT=0) so a bit-check can arbitrate, and OFF under verify-exact
1071    /// (matmul_group skips the f16 lane there; the twins must not resurrect it).
1072    fn f16out_on(e: &Engine, t: usize) -> bool {
1073        crate::f16_ffi::pp_f16_enabled()
1074            && t >= 16
1075            && !e.verify_exact_on()
1076            && std::env::var("MEMRA_F16OUT").as_deref() != Ok("0")
1077    }
1078
1079    /// (cache.pos + i). Returns (last-row logits, h_seed, this chunk's hidden stack [T, n_embd]).
1080    /// See HybridModel::prime_slabs — the eager prime's resident trunk transients.
1081    /// PER-DEVICE since lane/pp-leverb (2026-08-08): the map is keyed by the allocating
1082    /// engine's CUDA ordinal — under the prime stage split each stage's range walks through
1083    /// its OWN slabs on its own device (a dev0 slab dereferenced by a dev1 kernel would be
1084    /// a peer read per GEMM operand, the exact class Lever B removes). Single-device rigs
1085    /// see one entry, byte-identical behavior.
1086    pub fn prime_slabs_get(
1087        &self,
1088        e: &Engine,
1089        t: usize,
1090        n_embd: usize,
1091        n_ff_max: usize,
1092    ) -> Result<std::sync::Arc<std::sync::Mutex<PrimeSlabs>>, Box<dyn std::error::Error>> {
1093        let mut slabs = self.prime_slabs.lock().unwrap();
1094        let dev = e.ctx().ordinal();
1095        let need_new = match slabs.get(&dev) {
1096            None => true,
1097            Some(sl) => sl.lock().unwrap().t_cap < t,
1098        };
1099        if need_new {
1100            slabs.insert(
1101                dev,
1102                std::sync::Arc::new(std::sync::Mutex::new(PrimeSlabs {
1103                    t_cap: t,
1104                    h: e.uninit(t * n_embd)?,
1105                    x1: e.uninit(t * n_embd)?,
1106                    z: e.uninit(t * n_embd)?,
1107                    act: e.uninit(t * n_ff_max)?,
1108                    xa: e.uninit(t * n_embd)?,
1109                    xb: e.uninit(t * n_embd)?,
1110                    h16: e.alloc_u8_uninit(t * n_embd * 2)?,
1111                    z16: e.alloc_u8_uninit(t * n_embd * 2)?,
1112                    gate: e.uninit(t * n_ff_max)?,
1113                    up: e.uninit(t * n_ff_max)?,
1114                    ffn_out: e.uninit(t * n_embd)?,
1115                    seg_glue: Vec::new(),
1116                    mixed: e.uninit(t * n_embd)?,
1117                    seg_mid: Vec::new(),
1118                    seg_t: 0,
1119                })),
1120            );
1121        }
1122        Ok(slabs.get(&dev).expect("prime slab inserted").clone())
1123    }
1124
1125    /// `seq_end` = the whole REQUEST's absolute end position (`cache.pos + prompt_len` at
1126    /// `prime_cache` entry), NOT this chunk's end. Chunk-size-invariant by construction; step35's
1127    /// SWA arm selects on it (see `step35_attn_pre_wo`). Every other arch ignores it.
1128    fn prime_chunk(
1129        &self,
1130        e: &Engine,
1131        tokens: &[u32],
1132        cache: &mut Cache,
1133        seq_end: usize,
1134        chunk_off: usize,
1135        overlay: Option<&crate::vision::EmbedOverlay>,
1136    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1137        if crate::pp::pp_host_bounce_active()
1138            && (self.cfg.gemma4.is_some() || !crate::pp::prime_pp_on())
1139        {
1140            return Err(
1141                "prime_chunk: refused with MEMRA_PP_HOST_BOUNCE=1 because this configuration \
1142                 has no active prime stage split and would peer-read remote weights; keep \
1143                 MEMRA_PRIME_PP enabled and use a PP-prime-supported model"
1144                    .into(),
1145            );
1146        }
1147        // LEVER B (lane/pp-leverb, 2026-08-08): the ppN door for the chunked prime. With the
1148        // door open + per-stage streams, each chunk walks its layer ranges on the OWNING
1149        // stage's engine/device (the anatomy receipt this kills: dev1 ran ZERO prefill
1150        // kernels; stage-1 trunk weights were peer-read = 22% of the pp4096 wall).
1151        // MEMRA_PRIME_PP=0 = the unsplit rollback (also the prime-split-gate's reference
1152        // arm — prime deliberately keeps NO refuse_unsplit_if_remote, see pp.rs). The
1153        // MEMRA_PP_STREAMS=0 seam keeps the unsplit walk too: in that regime the sharded
1154        // loader is off and there is nothing remote to split for.
1155        if self.cfg.gemma4.is_none() && !crate::pp::pp2_streams_off() && crate::pp::prime_pp_on() {
1156            if let Some(fence) = crate::pp::pp_cuts(self.layers.len()) {
1157                if overlay.is_some() {
1158                    return Err("vision embedding overlay + PP prime unsupported (v1); \
1159                         run single-device or MEMRA_PRIME_PP=0"
1160                        .into());
1161                }
1162                return self.prime_chunk_ppn(e, tokens, cache, seq_end, &fence);
1163            }
1164        }
1165        if crate::pp::pp_host_bounce_active() {
1166            return Err(
1167                "prime_chunk: MEMRA_PP_HOST_BOUNCE=1 found no valid prime stage split; \
1168                 refusing an unsplit remote-weight walk"
1169                    .into(),
1170            );
1171        }
1172        let t = tokens.len();
1173        let base = cache.pos;
1174        debug_assert!(
1175            seq_end >= base + t,
1176            "prime_chunk: seq_end must cover this chunk"
1177        );
1178        let pos: Vec<i32> = (base as i32..(base + t) as i32).collect();
1179        let pos_d = e.htod_i32(&pos)?;
1180
1181        let mut x_embed = self.embed(e, tokens)?; // [T, n_embd]
1182        if let Some(ov) = overlay {
1183            // Mixed-embedding splice: image rows overwrite the pad-token embeddings that
1184            // fall inside this chunk's prompt-relative window [chunk_off, chunk_off+t).
1185            // Images larger than one prime chunk straddle boundaries, hence the clipping.
1186            let n_embd = self.cfg.n_embd as usize;
1187            for &(pos, row_off, n_rows) in &ov.spans {
1188                let lo = pos.max(chunk_off);
1189                let hi = (pos + n_rows).min(chunk_off + t);
1190                if lo < hi {
1191                    let src_row = row_off + (lo - pos);
1192                    let view = ov
1193                        .rows
1194                        .slice(src_row * n_embd..(src_row + (hi - lo)) * n_embd);
1195                    e.copy_view_into(
1196                        &mut x_embed,
1197                        (lo - chunk_off) * n_embd,
1198                        &view,
1199                        (hi - lo) * n_embd,
1200                    )?;
1201                }
1202            }
1203        }
1204        let x = self.prime_layers(
1205            e,
1206            x_embed,
1207            0,
1208            self.layers.len(),
1209            &pos_d,
1210            t,
1211            base,
1212            cache,
1213            seq_end,
1214        )?;
1215        self.prime_chunk_epilogue(e, x, t, cache)
1216    }
1217
1218    /// PRIME RANGE SUBGRAPH (lane/pp-leverb, 2026-08-08): layers `[lo, hi)` of the chunked
1219    /// prime walk — `prime_chunk`'s trunk loop extracted verbatim to the
1220    /// `decode_layers_eager(lo, hi)` / `verify_layers(lo, hi)` contract: enters with a
1221    /// MATERIALIZED `[T, n_embd]` residual, exits with the range's final residual
1222    /// materialized (cloned out of the slab). At `lo=0, hi=n_layers` — the unsplit call —
1223    /// the launch sequence is byte-identical to the pre-extraction body. Range semantics:
1224    ///   - the cross-layer [down-add + NEXT attn-norm] fusion is range-LOCAL (`il + 1 < hi`):
1225    ///     layer `hi`'s attn_norm belongs to the NEXT stage's device, so the range ends with
1226    ///     the plain add (materialize) and the next stage hoists its own first norm — the
1227    ///     kernel-check-pinned `add_rms_norm == add then rms_norm` identity, the same law
1228    ///     the decode split rests on (`prime-split-gate` arbitrates end-to-end);
1229    ///   - prime slabs are PER-DEVICE (`prime_slabs_get` keys on the engine ordinal), so
1230    ///     each stage walks through its own resident transients;
1231    ///   - the S-glue/S-mid capture path requires the FULL range (its lookahead fuses
1232    ///     `self.layers[il+1]` unconditionally) — `use_seg` gains `lo == 0 && hi == n_layers`.
1233    #[allow(clippy::too_many_arguments)]
1234    fn prime_layers(
1235        &self,
1236        e: &Engine,
1237        x_in: CudaSlice<f32>,
1238        lo: usize,
1239        hi: usize,
1240        pos_d: &CudaSlice<i32>,
1241        t: usize,
1242        base: usize,
1243        cache: &mut Cache,
1244        seq_end: usize,
1245    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1246        let cfg = &self.cfg;
1247        let n_embd = cfg.n_embd as usize;
1248        let eps = cfg.rms_eps;
1249        // task #14: fuse the fp16 GEMM-operand emission into the trunk norms (kills the
1250        // standalone convert launches). Only when the f16 lane serves and T reaches the
1251        // GEMM tier; bit-identical either way.
1252        let f16fuse = crate::f16_ffi::pp_f16_enabled() && t >= 16;
1253        // PRIME SLABS (piecewise foundation): trunk transients in resident buffers —
1254        // ~224 fewer alloc/free calls per prime and FROZEN Lt operand addresses
1255        // (nvjet's alignment-variant selection becomes run-to-run stable). Every slab is
1256        // fully overwritten before use; x ping-pongs xa<->xb; the hidden-stack return
1257        // clones the final x (the slab cannot leave). Non-slab fallback: MEMRA_PRIME_SLABS=0.
1258        let n_ff_max = self
1259            .layers
1260            .iter()
1261            .map(|l| match &l.ffn {
1262                crate::hybrid::Ffn::Dense { ffn_gate, .. } => ffn_gate.out_features(),
1263                _ => n_embd,
1264            })
1265            .max()
1266            .unwrap_or(n_embd)
1267            .max(n_embd);
1268        let use_slabs = std::env::var("MEMRA_PRIME_SLABS").as_deref() != Ok("0");
1269        let slab = if use_slabs {
1270            Some(self.prime_slabs_get(e, t, n_embd, n_ff_max)?)
1271        } else {
1272            None
1273        };
1274        let mut slab_guard = slab.as_ref().map(|sl| sl.lock().unwrap());
1275        let mut x_own; // fallback storage when slabs are off
1276        type SlabRefs<'a> = (
1277            &'a mut CudaSlice<f32>,
1278            &'a mut CudaSlice<f32>,
1279            &'a mut CudaSlice<f32>,
1280            &'a mut CudaSlice<f32>,
1281            &'a mut CudaSlice<u8>,
1282            &'a mut CudaSlice<u8>,
1283            &'a mut CudaSlice<f32>,
1284            &'a mut CudaSlice<f32>,
1285            &'a mut CudaSlice<f32>,
1286        );
1287        let (mut x_cur, mut x_nxt, sl): (
1288            &mut CudaSlice<f32>,
1289            &mut CudaSlice<f32>,
1290            Option<SlabRefs>,
1291        );
1292        let mut seg: Option<(
1293            &mut Vec<Option<cudarc::driver::CudaGraph>>,
1294            &mut Vec<Option<cudarc::driver::CudaGraph>>,
1295            &mut CudaSlice<f32>,
1296            &mut usize,
1297        )> = None;
1298        let mut x_own2;
1299        match slab_guard.as_mut() {
1300            Some(g) => {
1301                let slabs = &mut **g;
1302                e.copy_into(&mut slabs.xa, 0, &x_in, t * n_embd)?;
1303                let PrimeSlabs {
1304                    xa,
1305                    xb,
1306                    h,
1307                    x1,
1308                    z,
1309                    act,
1310                    h16,
1311                    z16,
1312                    gate,
1313                    up,
1314                    ffn_out,
1315                    seg_glue,
1316                    mixed,
1317                    seg_mid,
1318                    seg_t,
1319                    ..
1320                } = slabs;
1321                x_cur = xa;
1322                x_nxt = xb;
1323                seg = Some((seg_glue, seg_mid, mixed, seg_t));
1324                sl = Some((h, x1, z, act, h16, z16, gate, up, ffn_out));
1325            }
1326            None => {
1327                x_own = x_in;
1328                x_own2 = e.uninit(t * n_embd)?;
1329                x_cur = &mut x_own;
1330                x_nxt = &mut x_own2;
1331                sl = None;
1332            }
1333        }
1334        let mut alloc_h;
1335        let mut alloc_x1;
1336        let mut alloc_z;
1337        let mut alloc_act;
1338        let mut alloc_h16;
1339        let mut alloc_z16;
1340        let mut alloc_gate;
1341        let mut alloc_up;
1342        let mut alloc_fo;
1343        let (h, x1, z, act): (
1344            &mut CudaSlice<f32>,
1345            &mut CudaSlice<f32>,
1346            &mut CudaSlice<f32>,
1347            &mut CudaSlice<f32>,
1348        );
1349        let (h16, z16): (&mut CudaSlice<u8>, &mut CudaSlice<u8>);
1350        let (sl_gate, sl_up, sl_fo): (
1351            &mut CudaSlice<f32>,
1352            &mut CudaSlice<f32>,
1353            &mut CudaSlice<f32>,
1354        );
1355        match sl {
1356            Some((a, b, c, d, e16, f16b, g, u, fo)) => {
1357                h = a;
1358                x1 = b;
1359                z = c;
1360                act = d;
1361                h16 = e16;
1362                z16 = f16b;
1363                sl_gate = g;
1364                sl_up = u;
1365                sl_fo = fo;
1366            }
1367            None => {
1368                alloc_h = e.uninit(t * n_embd)?;
1369                alloc_x1 = e.uninit(t * n_embd)?;
1370                alloc_z = e.uninit(t * n_embd)?;
1371                alloc_act = e.uninit(t * n_ff_max)?;
1372                alloc_h16 = e.alloc_u8_uninit(t * n_embd * 2)?;
1373                alloc_z16 = e.alloc_u8_uninit(t * n_embd * 2)?;
1374                alloc_gate = e.uninit(t * n_ff_max)?;
1375                alloc_up = e.uninit(t * n_ff_max)?;
1376                alloc_fo = e.uninit(t * n_embd)?;
1377                h = &mut alloc_h;
1378                x1 = &mut alloc_x1;
1379                z = &mut alloc_z;
1380                act = &mut alloc_act;
1381                h16 = &mut alloc_h16;
1382                z16 = &mut alloc_z16;
1383                sl_gate = &mut alloc_gate;
1384                sl_up = &mut alloc_up;
1385                sl_fo = &mut alloc_fo;
1386            }
1387        }
1388        // piecewise increment 3: layer-0 norm hoisted; the per-layer tail fuses
1389        // [down-add + NEXT layer's attn-norm] into one captured S-glue segment
1390        // (all-slab IO, zero in-graph allocations). Capture happens lazily on the
1391        // first prime at this t (capture does not execute -> launch right after).
1392        let n_layers = self.layers.len();
1393        // OPT-IN (2026-07-26 interleaved verdict): 2-kernel segments measured NET
1394        // -0.7%-to-neutral under the interleaved A/B protocol — one cuGraphLaunch costs
1395        // about what two kernel submissions do. The earlier "+0.9%" was cross-run clock
1396        // drift (the repo's interleaved-A/B law exists for exactly this). Larger segments
1397        // (S-prep/S-attn, 7-9 kernels) remain the open hypothesis; the core-split
1398        // machinery stays (byte-identical) as their foundation.
1399        // step35 is excluded: the core-split path calls `full_attn_prime_core_inner`, which is
1400        // the GENERIC attn core (uniform n_head, rope_dim_count, no window, no head-wise gate).
1401        // step35 rides its own mixer through the normal per-layer arm below.
1402        let use_seg = f16fuse
1403            && seg.is_some()
1404            && self.cfg.step35.is_none()
1405            && lo == 0
1406            && hi == n_layers
1407            && std::env::var("MEMRA_PRIME_SEG").as_deref() == Ok("1");
1408        if let Some((sg, sm, _, st)) = seg.as_mut() {
1409            if **st != t {
1410                sg.clear();
1411                sg.extend((0..n_layers).map(|_| None));
1412                sm.clear();
1413                sm.extend((0..n_layers).map(|_| None));
1414                **st = t;
1415            }
1416        }
1417        {
1418            let layer_lo = &self.layers[lo];
1419            if f16fuse {
1420                e.rms_norm_f16out(
1421                    x_cur,
1422                    layer_lo.attn_norm.float_data(),
1423                    h,
1424                    h16,
1425                    n_embd,
1426                    t,
1427                    eps,
1428                )?;
1429            } else {
1430                e.rms_norm(x_cur, layer_lo.attn_norm.float_data(), h, n_embd, t, eps)?;
1431            }
1432        }
1433        for il in lo..hi {
1434            let layer = &self.layers[il];
1435            let hx16 = if f16fuse { Some(&*h16) } else { None };
1436            if use_seg {
1437                // core-split path: projections -> _inner core -> out-GEMM INTO the mixed
1438                // slab (no copies) -> S-mid segment [add + post-norm] as one graph launch.
1439                let (pre, pre16, w_out) = match &layer.mixer {
1440                    Mixer::Full(fa) => {
1441                        let g3 = match hx16 {
1442                            Some(xh) => e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], h, xh, t)?,
1443                            None => e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?,
1444                        };
1445                        let (pre, pre16) =
1446                            self.full_attn_prime_core_inner(e, fa, g3, &pos_d, t, cache, il)?;
1447                        (pre, pre16, &fa.wo)
1448                    }
1449                    Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1450                    Mixer::Linear(la) => {
1451                        let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
1452                        let g4 = match hx16 {
1453                            Some(xh) => e.matmul_group_xh(&ws, h, xh, t)?,
1454                            None => e.matmul_group(&ws, h, t)?,
1455                        };
1456                        let (pre, pre16) =
1457                            self.linear_attn_prime_core_pad_inner(e, la, g4, t, cache, il, None)?;
1458                        (pre, pre16, &la.ssm_out)
1459                    }
1460                };
1461                {
1462                    let (_, sm, mslab, _) = seg.as_mut().unwrap();
1463                    let pre_n = pre.len() / t;
1464                    let xh_pre = match pre16 {
1465                        Some(x) => x,
1466                        None => e.f16_act(&pre, t * pre_n, pre_n)?,
1467                    };
1468                    if !e.try_f16_gemm_pre_into(w_out, &xh_pre, t, mslab)? {
1469                        let y = e.matmul(w_out, &pre, t)?;
1470                        e.copy_into(mslab, 0, &y, t * n_embd)?;
1471                    }
1472                    if sm[il].is_none() {
1473                        use cudarc::driver::sys::{CUgraphInstantiate_flags, CUstreamCaptureMode};
1474                        let w_post = layer.post_attn_norm.float_data();
1475                        e.stream().synchronize()?;
1476                        e.stream()
1477                            .begin_capture(CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED)?;
1478                        let r = (|| -> Result<(), Box<dyn std::error::Error>> {
1479                            e.add(x_cur, mslab, x1, t * n_embd)?;
1480                            e.rms_norm_f16out(x1, w_post, z, z16, n_embd, t, eps)?;
1481                            Ok(())
1482                        })();
1483                        let g = e.stream().end_capture(
1484                            CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH);
1485                        r?;
1486                        sm[il] = Some(g?.ok_or("S-mid capture produced no graph")?);
1487                    }
1488                    sm[il].as_ref().unwrap().launch()?;
1489                }
1490            } else {
1491                let mixed = match &layer.mixer {
1492                    Mixer::Full(fa) => {
1493                        self.full_attn_prime(e, fa, h, hx16, &pos_d, t, cache, il, seq_end)?
1494                    }
1495                    Mixer::Linear(la) => self.linear_attn_prime(e, la, h, hx16, t, cache, il)?,
1496                    Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1497                };
1498                if f16fuse {
1499                    // round 28: residual+norm in ONE kernel (add_rms_norm precedent,
1500                    // bit-identical) — the standalone add pass disappears.
1501                    e.add_rms_norm_f16out(
1502                        x_cur,
1503                        &mixed,
1504                        layer.post_attn_norm.float_data(),
1505                        x1,
1506                        z,
1507                        z16,
1508                        n_embd,
1509                        t,
1510                        eps,
1511                    )?;
1512                } else {
1513                    e.add(x_cur, &mixed, x1, t * n_embd)?;
1514                    e.rms_norm(x1, layer.post_attn_norm.float_data(), z, n_embd, t, eps)?;
1515                }
1516            }
1517            let zx16 = if f16fuse { Some(&*z16) } else { None };
1518            match &layer.ffn {
1519                crate::hybrid::Ffn::Dense {
1520                    ffn_gate,
1521                    ffn_up,
1522                    ffn_down,
1523                } => {
1524                    let n_ff = ffn_gate.out_features();
1525                    // gate/up INTO boundary slabs (piecewise increment 2); fall back to
1526                    // the allocating group + copy when a mirror is missing.
1527                    let mut into_ok = false;
1528                    if let Some(xh) = zx16 {
1529                        into_ok = e.try_f16_gemm_pre_into(ffn_gate, xh, t, sl_gate)?
1530                            && e.try_f16_gemm_pre_into(ffn_up, xh, t, sl_up)?;
1531                    }
1532                    if !into_ok {
1533                        let mut g2 = match zx16 {
1534                            Some(xh) => e.matmul_group_xh(&[ffn_gate, ffn_up], z, xh, t)?,
1535                            None => e.matmul_group(&[ffn_gate, ffn_up], z, t)?,
1536                        };
1537                        let up_y = g2.pop().unwrap();
1538                        let gate_y = g2.pop().unwrap();
1539                        e.copy_into(sl_gate, 0, &gate_y, t * n_ff)?;
1540                        e.copy_into(sl_up, 0, &up_y, t * n_ff)?;
1541                    }
1542                    // task #17: the silu arm's f16out twin emits the down GEMM's fp16
1543                    // operand in-epilogue; non-silu activations keep the standalone convert.
1544                    // silu_mul_f16out is PLAIN silu(gate)*up — a clamped layer (step35 dense
1545                    // blocks under a live swiglu_clamp_shexp) must take the ffn_act_lim arm.
1546                    let d_lim = self.cfg.clamp_shexp_at(il as u32);
1547                    let act16 = if Self::f16out_on(e, t) && self.cfg.m3.is_none() && d_lim.is_none()
1548                    {
1549                        let mut a16 = e.alloc_u8_uninit(t * n_ff * 2)?;
1550                        e.silu_mul_f16out(sl_gate, sl_up, act, &mut a16, t * n_ff)?;
1551                        Some(a16)
1552                    } else {
1553                        Self::ffn_act_lim(
1554                            e,
1555                            &self.cfg,
1556                            sl_gate,
1557                            sl_up,
1558                            1.0,
1559                            1.0,
1560                            d_lim,
1561                            act,
1562                            t * n_ff,
1563                        )?;
1564                        None
1565                    };
1566                    // down GEMM into the ffn_out slab (f16 arm; fallback copies)
1567                    let xh_act = match act16 {
1568                        Some(x) => x,
1569                        None => e.f16_act(act, t * n_ff, n_ff)?,
1570                    };
1571                    if !e.try_f16_gemm_pre_into(ffn_down, &xh_act, t, sl_fo)? {
1572                        let y = e.matmul(ffn_down, &*act, t)?;
1573                        e.copy_into(sl_fo, 0, &y, t * n_embd)?;
1574                    }
1575                }
1576                crate::hybrid::Ffn::Moe(m) => {
1577                    let y = self.moe_ffn_il_prefill(e, m, z, t, il as u16)?;
1578                    e.copy_into(sl_fo, 0, &y, t * n_embd)?;
1579                }
1580            }
1581            if use_seg && il + 1 < hi {
1582                // S-glue segment: [add + next attn-norm(+f16out)] — one cuGraphLaunch
1583                let w_next = self.layers[il + 1].attn_norm.float_data();
1584                let (sg, _, _, _) = seg.as_mut().unwrap();
1585                if sg[il].is_none() {
1586                    use cudarc::driver::sys::{CUgraphInstantiate_flags, CUstreamCaptureMode};
1587                    e.stream().synchronize()?;
1588                    e.stream()
1589                        .begin_capture(CUstreamCaptureMode::CU_STREAM_CAPTURE_MODE_RELAXED)?;
1590                    let r = (|| -> Result<(), Box<dyn std::error::Error>> {
1591                        e.add(x1, sl_fo, x_nxt, t * n_embd)?;
1592                        e.rms_norm_f16out(x_nxt, w_next, h, h16, n_embd, t, eps)?;
1593                        Ok(())
1594                    })();
1595                    let g = e.stream().end_capture(
1596                        CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_AUTO_FREE_ON_LAUNCH,
1597                    );
1598                    r?;
1599                    sg[il] = Some(g?.ok_or("S-glue capture produced no graph")?);
1600                }
1601                sg[il].as_ref().unwrap().launch()?;
1602            } else {
1603                if il + 1 < hi {
1604                    let w_next = self.layers[il + 1].attn_norm.float_data();
1605                    if f16fuse {
1606                        e.add_rms_norm_f16out(x1, sl_fo, w_next, x_nxt, h, h16, n_embd, t, eps)?;
1607                    } else {
1608                        e.add(x1, sl_fo, x_nxt, t * n_embd)?;
1609                        e.rms_norm(x_nxt, w_next, h, n_embd, t, eps)?;
1610                    }
1611                } else {
1612                    e.add(x1, sl_fo, x_nxt, t * n_embd)?;
1613                }
1614            }
1615            // CHUNK-INVARIANCE BISECT SEAM (lane/chunk-invariance, 2026-08-05): dump the
1616            // per-layer post-residual hidden for the LAST row of this chunk, keyed by
1617            // absolute position, so two runs at different MEMRA_PRIME_CHUNK can be diffed
1618            // layer-by-layer to find the FIRST diverging layer. Diagnostic only —
1619            // unset (the default) costs one OnceLock read per layer.
1620            if let Some(path) = Self::prime_trace_path() {
1621                let row = (base + t - 1) as usize;
1622                let host = e.dtoh(x_nxt)?;
1623                let last = &host[(t - 1) * n_embd..t * n_embd];
1624                use std::io::Write as _;
1625                let mut f = std::fs::OpenOptions::new()
1626                    .create(true)
1627                    .append(true)
1628                    .open(path)?;
1629                let mut h64: u64 = 0xcbf29ce484222325;
1630                for v in last {
1631                    h64 ^= v.to_bits() as u64;
1632                    h64 = h64.wrapping_mul(0x100000001b3);
1633                }
1634                writeln!(
1635                    f,
1636                    "{{\"pos\":{row},\"layer\":{il},\"t\":{t},\"base\":{base},\
1637                             \"hash\":\"{h64:016x}\",\"v0\":{:.9e},\"v1\":{:.9e},\"v2\":{:.9e}}}",
1638                    last[0], last[1], last[2]
1639                )?;
1640            }
1641            std::mem::swap(&mut x_cur, &mut x_nxt);
1642        }
1643        // hidden-stack return: clone the final x out of the slab
1644        let mut x = e.uninit(t * n_embd)?;
1645        e.copy_into(&mut x, 0, x_cur, t * n_embd)?;
1646        drop(slab_guard);
1647        Ok(x)
1648    }
1649
1650    /// The prime chunk's tail — h_seed + output_norm + one-row lm head + cache.pos advance —
1651    /// shared verbatim by the unsplit walk and the last stage of the ppN walk (`e` = the
1652    /// engine that produced `x`, i.e. the last stage's under the split; output_norm/output
1653    /// were loaded through that engine by the sharded loader, hybrid.rs `e_head`).
1654    fn prime_chunk_epilogue(
1655        &self,
1656        e: &Engine,
1657        x: CudaSlice<f32>,
1658        t: usize,
1659        cache: &mut Cache,
1660    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1661        let n_embd = self.cfg.n_embd as usize;
1662        let eps = self.cfg.rms_eps;
1663        // h_seed = LAST row of x BEFORE output_norm (MTP-PLAN §A default) or AFTER it
1664        // (MEMRA_SPEC_HPOST — reference convention; hn is computed just below either way, so
1665        // the post-norm copy happens after hn exists).
1666        let mut h_seed = e.uninit(n_embd)?;
1667        if !crate::spec::spec_hpost() {
1668            e.copy_view_into(
1669                &mut h_seed,
1670                0,
1671                &x.slice((t - 1) * n_embd..t * n_embd),
1672                n_embd,
1673            )?;
1674        }
1675        // last-row logits, exactly like forward_last (norm all T — per-row op — then lm_head on 1 row).
1676        let mut hn = e.uninit(t * n_embd)?;
1677        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
1678        if crate::spec::spec_hpost() {
1679            e.copy_view_into(
1680                &mut h_seed,
1681                0,
1682                &hn.slice((t - 1) * n_embd..t * n_embd),
1683                n_embd,
1684            )?;
1685        }
1686        let last = e.view(&hn, t * n_embd);
1687        let last_row = last.slice((t - 1) * n_embd..t * n_embd);
1688        let mut hlast = e.uninit(n_embd)?;
1689        e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
1690        let logits = e.matmul(&self.output, &hlast, 1)?;
1691        cache.pos += t;
1692        // Hidden stack handed to generate_spec as prompt_h: pre-norm x (default) or the full
1693        // post-norm stack hn (MEMRA_SPEC_HPOST).
1694        Ok((
1695            e.dtoh(&logits)?,
1696            h_seed,
1697            if crate::spec::spec_hpost() { hn } else { x },
1698        ))
1699    }
1700
1701    /// THE PRIME STAGE SPLIT (lane/pp-leverb, 2026-08-08): one prime chunk as N stage
1702    /// subgraphs, each range on ITS OWN engine/stream/device, with the `[T, n_embd]`
1703    /// boundary handoff at every fence cut — the prime twin of `decode_step_h_ppn` /
1704    /// `decode_step_t_core_ppn`, and the kill for the anatomy's two receipts: stage-1 trunk
1705    /// weights stop being peer-read (22% of the pp4096 wall) and dev1 stops running zero
1706    /// prefill kernels. Structure mirrors the verify split exactly:
1707    ///   stage 0        `rt.enter(0)` → per-stage pos_d + embed (the table lives with
1708    ///                  stage 0) → `prime_layers(fence[0], fence[1])` → `rt.tx`
1709    ///   middle stages  `rt.rx` → per-stage pos_d → range → `rt.tx`
1710    ///   last stage     `rt.rx` → range → the shared epilogue (output_norm + head live
1711    ///                  there via the sharded loader) → `publish_to`
1712    /// Laws inherited (not relearned): `fence_stages_behind` at entry (#87 — a previous
1713    /// round's stage-freed buffers must not be reused under the caller's queued reads);
1714    /// per-stage `pos_d` (allocated/consumed/freed on one stream); per-stage Engines from
1715    /// `PpNRt` (shared-scratch race); EXIT PUBLICATION for the device-resident returns
1716    /// (h_seed + hidden stack live on the last stage — the caller's stream must wait).
1717    /// KV/MoE locality falls out: each range's KV appends run on the owning stage
1718    /// (`pp::new_cache` placed the buffers there), each stage engine owns its own SLRU
1719    /// pool (per-Engine `moe_cache`, sized on ITS device — the SGLang #33666 law), and the
1720    /// slab-local MoE arm's `DevExps.dev` gate now matches on stage-1 layers too.
1721    /// EXACTNESS: the split adds zero deviation by construction (same kernels, same bytes,
1722    /// boundary = straight f32 copy); `prime-split-gate` (ppsplit) arbitrates bit-for-bit
1723    /// and its liveness counter is bumped here — the gate goes green with this function.
1724    fn prime_chunk_ppn(
1725        &self,
1726        e: &Engine,
1727        tokens: &[u32],
1728        cache: &mut Cache,
1729        seq_end: usize,
1730        fence: &[usize],
1731    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
1732        let rt = crate::pp::PpNRt::get(e)?;
1733        let n_st = fence.len() - 1;
1734        assert_eq!(
1735            rt.n_stages(),
1736            n_st,
1737            "PpNRt stage count {} != fence stages {n_st}",
1738            rt.n_stages()
1739        );
1740        let n_embd = self.cfg.n_embd as usize;
1741        let t = tokens.len();
1742        let base = cache.pos;
1743        debug_assert!(
1744            seq_end >= base + t,
1745            "prime_chunk_ppn: seq_end must cover this chunk"
1746        );
1747        let payload = t * n_embd;
1748        // The caller's ambient stream, captured BEFORE any enter() pushes a stage stream
1749        // (inside a stage scope e.stream() IS the stage stream and the exit wait would
1750        // self-order into a no-op) — the decode_step_t_core_ppn pattern.
1751        let caller_stream = e.stream();
1752        rt.fence_stages_behind(&caller_stream)?;
1753
1754        if n_st == 2 {
1755            let slot =
1756                self.prime_pp2_stage0_enqueue(e, rt, tokens, cache, seq_end, fence, base, false)?;
1757            let x =
1758                self.prime_pp2_stage1_enqueue(e, rt, slot, t, cache, seq_end, fence, base, false)?;
1759            let out = {
1760                rt.bind_stage(1)?;
1761                let _st1 = rt.enter(1);
1762                let e1 = rt.engine(1, e);
1763                self.prime_chunk_epilogue(e1, x, t, cache)?
1764            };
1765            rt.publish_to(1, &caller_stream)?;
1766            crate::pp::PRIME_SPLIT_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1767            return Ok(out);
1768        }
1769
1770        let pos: Vec<i32> = (base as i32..(base + t) as i32).collect();
1771
1772        // ---- STAGE 0: embed + layers [0, fence[1]) + boundary-0 TX ----
1773        let mut slot = {
1774            let _st0 = rt.enter(0);
1775            let e0 = rt.engine(0, e);
1776            let pos_d = e0.htod_i32(&pos)?;
1777            let x = self.embed(e0, tokens)?;
1778            let x =
1779                self.prime_layers(e0, x, fence[0], fence[1], &pos_d, t, base, cache, seq_end)?;
1780            rt.tx(0, &x, payload)?
1781            // x + pos_d drop here: freed stream-ordered on stage-0's stream after use.
1782        };
1783
1784        // ---- MIDDLE STAGES: RX boundary s-1 -> range -> TX boundary s ----
1785        for s in 1..n_st - 1 {
1786            let _st = rt.enter(s);
1787            let es = rt.engine(s, e);
1788            let pos_d = es.htod_i32(&pos)?;
1789            let x = rt.rx(s - 1, slot, payload)?;
1790            let x = self.prime_layers(
1791                es,
1792                x,
1793                fence[s],
1794                fence[s + 1],
1795                &pos_d,
1796                t,
1797                base,
1798                cache,
1799                seq_end,
1800            )?;
1801            slot = rt.tx(s, &x, payload)?;
1802        }
1803
1804        // ---- LAST STAGE: RX + final range + the shared epilogue ----
1805        let _stl = rt.enter(n_st - 1);
1806        let el = rt.engine(n_st - 1, e);
1807        let pos_d = el.htod_i32(&pos)?;
1808        let x = rt.rx(n_st - 2, slot, payload)?;
1809        let x = self.prime_layers(
1810            el,
1811            x,
1812            fence[n_st - 1],
1813            fence[n_st],
1814            &pos_d,
1815            t,
1816            base,
1817            cache,
1818            seq_end,
1819        )?;
1820        let out = self.prime_chunk_epilogue(el, x, t, cache)?;
1821        // EXIT PUBLICATION: h_seed + the hidden stack are device-resident on the last
1822        // stage's stream; the caller resumes on its own stream (chunk-loop copy_into /
1823        // generate_spec's prompt_h consumer). The logits dtoh above already drained the
1824        // stage stream host-side, but the law is stated in events, not in a dtoh side
1825        // effect a later deferred form would remove.
1826        rt.publish_to(n_st - 1, &caller_stream)?;
1827        crate::pp::PRIME_SPLIT_CHUNKS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1828        Ok(out)
1829    }
1830
1831    fn prime_pp2_stage0_enqueue(
1832        &self,
1833        e: &Engine,
1834        rt: &crate::pp::PpNRt,
1835        tokens: &[u32],
1836        cache: &mut Cache,
1837        seq_end: usize,
1838        fence: &[usize],
1839        base: usize,
1840        pipelined: bool,
1841    ) -> Result<usize, Box<dyn std::error::Error>> {
1842        let t = tokens.len();
1843        let n_embd = self.cfg.n_embd as usize;
1844        let pos: Vec<i32> = (base as i32..(base + t) as i32).collect();
1845        rt.bind_stage(0)?;
1846        let _st0 = rt.enter(0);
1847        let e0 = rt.engine(0, e);
1848        let pos_d = e0.htod_i32(&pos)?;
1849        let x = self.embed(e0, tokens)?;
1850        let _overlap = pipelined.then(crate::pp::enter_prime_pipe_stage);
1851        let x = self.prime_layers(e0, x, fence[0], fence[1], &pos_d, t, base, cache, seq_end)?;
1852        if pipelined {
1853            rt.tx_pipelined(0, &x, t * n_embd)
1854        } else {
1855            rt.tx(0, &x, t * n_embd)
1856        }
1857    }
1858
1859    fn prime_pp2_stage1_enqueue(
1860        &self,
1861        e: &Engine,
1862        rt: &crate::pp::PpNRt,
1863        slot: usize,
1864        t: usize,
1865        cache: &mut Cache,
1866        seq_end: usize,
1867        fence: &[usize],
1868        base: usize,
1869        pipelined: bool,
1870    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1871        let n_embd = self.cfg.n_embd as usize;
1872        let pos: Vec<i32> = (base as i32..(base + t) as i32).collect();
1873        rt.bind_stage(1)?;
1874        let _st1 = rt.enter(1);
1875        let e1 = rt.engine(1, e);
1876        let pos_d = e1.htod_i32(&pos)?;
1877        let x = rt.rx(0, slot, t * n_embd)?;
1878        let _overlap = pipelined.then(crate::pp::enter_prime_pipe_stage);
1879        self.prime_layers(e1, x, fence[1], fence[2], &pos_d, t, base, cache, seq_end)
1880    }
1881
1882    /// CAPTURE-SAFE prime trunk (task #14 increment 1, ARCHITECTURE-H100.md design v2):
1883    /// prime_chunk's layer stack with every capture hazard hoisted — `x` is the
1884    /// PRE-EMBEDDED input in a STABLE graph-input buffer (embed + its 8MB htod stay
1885    /// eager, one launch), `pos_d` is a baked device param (fresh prime = 0..T, constant
1886    /// per bucket), logits/h_seed/hidden stay DEVICE-resident (no dtoh), and cache.pos is
1887    /// NOT advanced (host state — the replay wrapper owns it). Body mirrors prime_chunk
1888    /// (the prime-graph-gate pins them together). KNOWN smoke-scope gap: append host-len
1889    /// bookkeeping still runs on the host per call — the real replay path moves the write
1890    /// slot to the len_d device counter (increment 3).
1891    /// GRAPH-OUTPUT CONTRACT: results are COPIED into caller-provided stable buffers
1892    /// (`logits_out` [n_vocab], `h_seed_out` [n_embd]) — every internal allocation drops
1893    /// INSIDE the capture region (alloc+free node pairs). Retaining an in-capture
1894    /// allocation across end_capture makes instantiate throw INVALID_VALUE (smoke finding
1895    /// 2026-07-26), and under AUTO_FREE_ON_LAUNCH its address wouldn't survive a launch
1896    /// anyway — the decode GraphSession's pre-allocated-output pattern is the law here.
1897    pub fn prime_chunk_captured(
1898        &self,
1899        e: &Engine,
1900        x_in: &CudaSlice<f32>,
1901        pos_d: &CudaSlice<i32>,
1902        t: usize,
1903        cache: &mut Cache,
1904        len_d: &CudaSlice<i32>,
1905        logits_out: &mut CudaSlice<f32>,
1906        h_seed_out: &mut CudaSlice<f32>,
1907    ) -> Result<(), Box<dyn std::error::Error>> {
1908        let cfg = &self.cfg;
1909        let n_embd = cfg.n_embd as usize;
1910        let eps = cfg.rms_eps;
1911        let f16fuse = crate::f16_ffi::pp_f16_enabled() && t >= 16;
1912        let mut x = e.uninit(t * n_embd)?;
1913        e.copy_into(&mut x, 0, x_in, t * n_embd)?;
1914        for (il, layer) in self.layers.iter().enumerate() {
1915            let mut h = e.uninit(t * n_embd)?;
1916            let mut hx16: Option<CudaSlice<u8>> = None;
1917            if f16fuse {
1918                let mut b16 = e.alloc_u8_uninit(t * n_embd * 2)?;
1919                e.rms_norm_f16out(
1920                    &x,
1921                    layer.attn_norm.float_data(),
1922                    &mut h,
1923                    &mut b16,
1924                    n_embd,
1925                    t,
1926                    eps,
1927                )?;
1928                hx16 = Some(b16);
1929            } else {
1930                e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
1931            }
1932            let mixed = match &layer.mixer {
1933                // The captured prime is ONE unchunked bucket over a FRESH cache (pos == 0), so the
1934                // request ends at t. If bucketed capture ever composes with chunking, seq_end must
1935                // come from the caller (see step35_attn_pre_wo's doc note).
1936                Mixer::Full(fa) => {
1937                    self.full_attn_prime(e, fa, &h, hx16.as_ref(), pos_d, t, cache, il, t)?
1938                }
1939                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1940                Mixer::Linear(la) => {
1941                    let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
1942                    let g4 = match hx16.as_ref() {
1943                        Some(xh) => e.matmul_group_xh(&ws, &h, xh, t)?,
1944                        None => e.matmul_group(&ws, &h, t)?,
1945                    };
1946                    self.linear_attn_prime_core_pad(e, la, g4, t, cache, il, Some(len_d))?
1947                }
1948            };
1949            let mut x1 = e.uninit(t * n_embd)?;
1950            e.add(&x, &mixed, &mut x1, t * n_embd)?;
1951            let mut z = e.uninit(t * n_embd)?;
1952            let mut zx16: Option<CudaSlice<u8>> = None;
1953            if f16fuse {
1954                let mut b16 = e.alloc_u8_uninit(t * n_embd * 2)?;
1955                e.rms_norm_f16out(
1956                    &x1,
1957                    layer.post_attn_norm.float_data(),
1958                    &mut z,
1959                    &mut b16,
1960                    n_embd,
1961                    t,
1962                    eps,
1963                )?;
1964                zx16 = Some(b16);
1965            } else {
1966                e.rms_norm(
1967                    &x1,
1968                    layer.post_attn_norm.float_data(),
1969                    &mut z,
1970                    n_embd,
1971                    t,
1972                    eps,
1973                )?;
1974            }
1975            let ffn_out = match &layer.ffn {
1976                crate::hybrid::Ffn::Dense {
1977                    ffn_gate,
1978                    ffn_up,
1979                    ffn_down,
1980                } => {
1981                    let n_ff = ffn_gate.out_features();
1982                    let mut g2 = match &zx16 {
1983                        Some(xh) => e.matmul_group_xh(&[ffn_gate, ffn_up], &z, xh, t)?,
1984                        None => e.matmul_group(&[ffn_gate, ffn_up], &z, t)?,
1985                    };
1986                    let up = g2.pop().unwrap();
1987                    let gate = g2.pop().unwrap();
1988                    let mut act = e.uninit(t * n_ff)?;
1989                    // dense FFN keys off the SHEXP clamp array — see forward()'s note.
1990                    Self::ffn_act_lim(
1991                        e,
1992                        &self.cfg,
1993                        &gate,
1994                        &up,
1995                        1.0,
1996                        1.0,
1997                        self.cfg.clamp_shexp_at(il as u32),
1998                        &mut act,
1999                        t * n_ff,
2000                    )?;
2001                    e.matmul(ffn_down, &act, t)?
2002                }
2003                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il_prefill(e, m, &z, t, il as u16)?,
2004            };
2005            let mut x2 = e.uninit(t * n_embd)?;
2006            e.add(&x1, &ffn_out, &mut x2, t * n_embd)?;
2007            x = x2;
2008        }
2009        // device-indexed TRUE last row (pads sit past it in a bucketed graph)
2010        if !crate::spec::spec_hpost() {
2011            e.row_gather_dev(&x, h_seed_out, len_d, n_embd)?;
2012        }
2013        let mut hn = e.uninit(t * n_embd)?;
2014        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
2015        if crate::spec::spec_hpost() {
2016            e.row_gather_dev(&hn, h_seed_out, len_d, n_embd)?;
2017        }
2018        let mut hlast = e.uninit(n_embd)?;
2019        e.row_gather_dev(&hn, &mut hlast, len_d, n_embd)?;
2020        let logits = e.matmul(&self.output, &hlast, 1)?;
2021        let nv = logits.len();
2022        e.copy_into(logits_out, 0, &logits, nv)?;
2023        Ok(())
2024    }
2025
2026    fn step35_prime_batch_on() -> bool {
2027        std::env::var("MEMRA_STEP35_PRIME_BATCH").as_deref() != Ok("0")
2028    }
2029
2030    /// Step35 cross-request prime range: weight-streaming work runs once at `m=sum(T)`;
2031    /// sequence-scoped attention/KV work stays on each request's own cache and positions.
2032    #[allow(clippy::too_many_arguments)]
2033    fn step35_prime_batch_layers(
2034        &self,
2035        e: &Engine,
2036        mut x: CudaSlice<f32>,
2037        lo: usize,
2038        hi: usize,
2039        ts: &[usize],
2040        offs: &[usize],
2041        pos_ds: &[CudaSlice<i32>],
2042        caches: &mut [&mut Cache],
2043    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2044        let cfg = &self.cfg;
2045        let n_embd = cfg.n_embd as usize;
2046        let eps = cfg.rms_eps;
2047        let b = ts.len();
2048        let total: usize = ts.iter().sum();
2049        let f16fuse = crate::f16_ffi::pp_f16_enabled() && total >= 16;
2050
2051        let split = |e: &Engine,
2052                     y: &CudaSlice<f32>,
2053                     dim: usize|
2054         -> Result<Vec<CudaSlice<f32>>, Box<dyn std::error::Error>> {
2055            let mut out = Vec::with_capacity(b);
2056            for s in 0..b {
2057                let mut ys = e.uninit(ts[s] * dim)?;
2058                e.copy_view_into(
2059                    &mut ys,
2060                    0,
2061                    &y.slice(offs[s] * dim..(offs[s] + ts[s]) * dim),
2062                    ts[s] * dim,
2063                )?;
2064                out.push(ys);
2065            }
2066            Ok(out)
2067        };
2068
2069        for il in lo..hi {
2070            let layer = &self.layers[il];
2071            let Mixer::Full(fa) = &layer.mixer else {
2072                return Err(format!("step35 layer {il} is not full-attn — corrupt config").into());
2073            };
2074
2075            let mut h = e.uninit(total * n_embd)?;
2076            let mut hx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
2077            if f16fuse {
2078                e.rms_norm_f16out(
2079                    &x,
2080                    layer.attn_norm.float_data(),
2081                    &mut h,
2082                    &mut hx16,
2083                    n_embd,
2084                    total,
2085                    eps,
2086                )?;
2087            } else {
2088                e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, total, eps)?;
2089            }
2090
2091            // Q/K/V + gate projections and wo are batched weight streams. The step35 core
2092            // remains per sequence, so its partial RoPE, SWA view, KV append, and gate
2093            // application stay verbatim.
2094            let gate_w = fa
2095                .attn_gate
2096                .as_ref()
2097                .ok_or("step35 layer is missing attn_gate.weight (head-wise attention gate)")?;
2098            let mut g4 = if f16fuse {
2099                e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv, gate_w], &h, &hx16, total)?
2100            } else {
2101                e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv, gate_w], &h, total)?
2102            };
2103            let gate = g4.pop().unwrap();
2104            let mut parts: Vec<Vec<CudaSlice<f32>>> =
2105                (0..b).map(|_| Vec::with_capacity(3)).collect();
2106            for (w, y) in [&fa.wq, &fa.wk, &fa.wv].iter().zip(g4) {
2107                for (s, ys) in split(e, &y, w.out_features())?.into_iter().enumerate() {
2108                    parts[s].push(ys);
2109                }
2110            }
2111            let gates = split(e, &gate, gate_w.out_features())?;
2112            let geometry = self.step35_geom(il);
2113            let hd = geometry.head_dim_k as usize;
2114            let nh = geometry.n_head as usize;
2115            let mut ag_cat = e.uninit(total * nh * hd)?;
2116            for (s, (g3s, gate)) in parts.into_iter().zip(gates).enumerate() {
2117                let ag = self.step35_attn_pre_wo(
2118                    e,
2119                    fa,
2120                    g3s,
2121                    None,
2122                    Some(&gate),
2123                    &pos_ds[s],
2124                    ts[s],
2125                    Some(&mut *caches[s]),
2126                    il,
2127                    ts[s],
2128                )?;
2129                e.copy_into(&mut ag_cat, offs[s] * nh * hd, &ag, ts[s] * nh * hd)?;
2130            }
2131            let mixed = e.matmul(&fa.wo, &ag_cat, total)?;
2132
2133            let mut x1 = e.uninit(total * n_embd)?;
2134            let mut z = e.uninit(total * n_embd)?;
2135            let mut zx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
2136            if f16fuse {
2137                e.add_rms_norm_f16out(
2138                    &x,
2139                    &mixed,
2140                    layer.post_attn_norm.float_data(),
2141                    &mut x1,
2142                    &mut z,
2143                    &mut zx16,
2144                    n_embd,
2145                    total,
2146                    eps,
2147                )?;
2148            } else {
2149                e.add(&x, &mixed, &mut x1, total * n_embd)?;
2150                e.rms_norm(
2151                    &x1,
2152                    layer.post_attn_norm.float_data(),
2153                    &mut z,
2154                    n_embd,
2155                    total,
2156                    eps,
2157                )?;
2158            }
2159
2160            let ffn_out = match &layer.ffn {
2161                crate::hybrid::Ffn::Dense {
2162                    ffn_gate,
2163                    ffn_up,
2164                    ffn_down,
2165                } => {
2166                    let n_ff = ffn_gate.out_features();
2167                    let mut g2 = if f16fuse {
2168                        e.matmul_group_xh(&[ffn_gate, ffn_up], &z, &zx16, total)?
2169                    } else {
2170                        e.matmul_group(&[ffn_gate, ffn_up], &z, total)?
2171                    };
2172                    let up = g2.pop().unwrap();
2173                    let gate = g2.pop().unwrap();
2174                    let mut act = e.uninit(total * n_ff)?;
2175                    let d_lim = cfg.clamp_shexp_at(il as u32);
2176                    if Self::f16out_on(e, total) && cfg.m3.is_none() && d_lim.is_none() {
2177                        let mut a16 = e.alloc_u8_uninit(total * n_ff * 2)?;
2178                        e.silu_mul_f16out(&gate, &up, &mut act, &mut a16, total * n_ff)?;
2179                        match e.try_f16_gemm_pre(ffn_down, &a16, total)? {
2180                            Some(y) => y,
2181                            None => e.matmul(ffn_down, &act, total)?,
2182                        }
2183                    } else {
2184                        Self::ffn_act_lim(
2185                            e,
2186                            cfg,
2187                            &gate,
2188                            &up,
2189                            1.0,
2190                            1.0,
2191                            d_lim,
2192                            &mut act,
2193                            total * n_ff,
2194                        )?;
2195                        e.matmul(ffn_down, &act, total)?
2196                    }
2197                }
2198                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il(e, m, &z, total, il as u16)?,
2199            };
2200            let mut x2 = e.uninit(total * n_embd)?;
2201            e.add(&x1, &ffn_out, &mut x2, total * n_embd)?;
2202            x = x2;
2203        }
2204        Ok(x)
2205    }
2206
2207    fn step35_prime_batch_epilogue(
2208        &self,
2209        e: &Engine,
2210        x: CudaSlice<f32>,
2211        ts: &[usize],
2212        offs: &[usize],
2213        caches: &mut [&mut Cache],
2214    ) -> Result<Vec<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>)>, Box<dyn std::error::Error>> {
2215        let n_embd = self.cfg.n_embd as usize;
2216        let total: usize = ts.iter().sum();
2217        let mut hn = e.uninit(total * n_embd)?;
2218        e.rms_norm(
2219            &x,
2220            self.output_norm.float_data(),
2221            &mut hn,
2222            n_embd,
2223            total,
2224            self.cfg.rms_eps,
2225        )?;
2226
2227        let hidden_src = if crate::spec::spec_hpost() { &hn } else { &x };
2228        let mut out = Vec::with_capacity(ts.len());
2229        for s in 0..ts.len() {
2230            let mut hidden = e.uninit(ts[s] * n_embd)?;
2231            e.copy_view_into(
2232                &mut hidden,
2233                0,
2234                &hidden_src.slice(offs[s] * n_embd..(offs[s] + ts[s]) * n_embd),
2235                ts[s] * n_embd,
2236            )?;
2237            let last0 = (offs[s] + ts[s] - 1) * n_embd;
2238            let mut h_seed = e.uninit(n_embd)?;
2239            e.copy_view_into(
2240                &mut h_seed,
2241                0,
2242                &hidden_src.slice(last0..last0 + n_embd),
2243                n_embd,
2244            )?;
2245            // Exactness-first: the serial reference runs the output head at m=1.
2246            let mut hlast = e.uninit(n_embd)?;
2247            e.copy_view_into(&mut hlast, 0, &hn.slice(last0..last0 + n_embd), n_embd)?;
2248            let logits = e.dtoh(&e.matmul(&self.output, &hlast, 1)?)?;
2249            caches[s].pos += ts[s];
2250            out.push((logits, h_seed, hidden));
2251        }
2252        Ok(out)
2253    }
2254
2255    fn step35_prime_cache_batch(
2256        &self,
2257        e: &Engine,
2258        prompts: &[&[u32]],
2259        caches: &mut [&mut Cache],
2260    ) -> Result<Vec<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>)>, Box<dyn std::error::Error>> {
2261        if crate::pp::pp_host_bounce_active()
2262            && (!crate::pp::prime_pp_on() || crate::pp::pp_cuts(self.layers.len()).is_none())
2263        {
2264            return Err(
2265                "step35_prime_cache_batch: MEMRA_PP_HOST_BOUNCE=1 requires a valid prime \
2266                 stage split; refusing an unsplit remote-weight walk"
2267                    .into(),
2268            );
2269        }
2270        if !Self::step35_prime_batch_on() {
2271            return Err("step35 batched prime is disabled (MEMRA_STEP35_PRIME_BATCH=0)".into());
2272        }
2273        if caches.iter().any(|c| c.pos != 0) {
2274            return Err(
2275                "step35 batched prime currently supports complete fresh prompts only; \
2276                 continuation/tick chunks require per-request queued_after"
2277                    .into(),
2278            );
2279        }
2280
2281        let ts: Vec<usize> = prompts.iter().map(|p| p.len()).collect();
2282        for &t in &ts {
2283            assert!(
2284                t >= PRIME_MIN_T,
2285                "step35 batched prime needs T >= {PRIME_MIN_T}"
2286            );
2287        }
2288        for (s, c) in caches.iter().enumerate() {
2289            assert!(
2290                ts[s] <= c.max_ctx,
2291                "step35 batched prime exceeds cache max_ctx"
2292            );
2293        }
2294        let offs: Vec<usize> = ts
2295            .iter()
2296            .scan(0usize, |a, &t| {
2297                let o = *a;
2298                *a += t;
2299                Some(o)
2300            })
2301            .collect();
2302        let total: usize = ts.iter().sum();
2303        let payload = total * self.cfg.n_embd as usize;
2304        let cat_tokens: Vec<u32> = prompts.iter().flat_map(|p| p.iter().copied()).collect();
2305        let positions: Vec<Vec<i32>> = ts.iter().map(|&t| (0..t as i32).collect()).collect();
2306        let upload_positions =
2307            |e: &Engine| -> Result<Vec<CudaSlice<i32>>, Box<dyn std::error::Error>> {
2308                positions
2309                    .iter()
2310                    .map(|p| e.htod_i32(p))
2311                    .collect::<Result<_, _>>()
2312            };
2313
2314        static ONCE: std::sync::Once = std::sync::Once::new();
2315        ONCE.call_once(|| {
2316            eprintln!(
2317                "[step35-prime-batch] first concat prime: B={} tokens={total}",
2318                prompts.len()
2319            );
2320        });
2321
2322        let out = if !crate::pp::pp2_streams_off() && crate::pp::prime_pp_on() {
2323            if let Some(fence) = crate::pp::pp_cuts(self.layers.len()) {
2324                let rt = crate::pp::PpNRt::get(e)?;
2325                let n_st = fence.len() - 1;
2326                assert_eq!(
2327                    rt.n_stages(),
2328                    n_st,
2329                    "step35 prime batch stage count mismatch"
2330                );
2331                let caller_stream = e.stream();
2332                rt.fence_stages_behind(&caller_stream)?;
2333
2334                let mut slot = {
2335                    let _st0 = rt.enter(0);
2336                    let e0 = rt.engine(0, e);
2337                    let pos_ds = upload_positions(e0)?;
2338                    let x = self.embed(e0, &cat_tokens)?;
2339                    let x = self.step35_prime_batch_layers(
2340                        e0, x, fence[0], fence[1], &ts, &offs, &pos_ds, caches,
2341                    )?;
2342                    rt.tx(0, &x, payload)?
2343                };
2344                for s in 1..n_st - 1 {
2345                    let _st = rt.enter(s);
2346                    let es = rt.engine(s, e);
2347                    let pos_ds = upload_positions(es)?;
2348                    let x = rt.rx(s - 1, slot, payload)?;
2349                    let x = self.step35_prime_batch_layers(
2350                        es,
2351                        x,
2352                        fence[s],
2353                        fence[s + 1],
2354                        &ts,
2355                        &offs,
2356                        &pos_ds,
2357                        caches,
2358                    )?;
2359                    slot = rt.tx(s, &x, payload)?;
2360                }
2361
2362                let _stl = rt.enter(n_st - 1);
2363                let el = rt.engine(n_st - 1, e);
2364                let pos_ds = upload_positions(el)?;
2365                let x = rt.rx(n_st - 2, slot, payload)?;
2366                let x = self.step35_prime_batch_layers(
2367                    el,
2368                    x,
2369                    fence[n_st - 1],
2370                    fence[n_st],
2371                    &ts,
2372                    &offs,
2373                    &pos_ds,
2374                    caches,
2375                )?;
2376                let out = self.step35_prime_batch_epilogue(el, x, &ts, &offs, caches)?;
2377                rt.publish_to(n_st - 1, &caller_stream)?;
2378                crate::pp::STEP35_PRIME_BATCH_SPLITS
2379                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2380                out
2381            } else {
2382                let pos_ds = upload_positions(e)?;
2383                let x = self.embed(e, &cat_tokens)?;
2384                let x = self.step35_prime_batch_layers(
2385                    e,
2386                    x,
2387                    0,
2388                    self.layers.len(),
2389                    &ts,
2390                    &offs,
2391                    &pos_ds,
2392                    caches,
2393                )?;
2394                self.step35_prime_batch_epilogue(e, x, &ts, &offs, caches)?
2395            }
2396        } else {
2397            let pos_ds = upload_positions(e)?;
2398            let x = self.embed(e, &cat_tokens)?;
2399            let x = self.step35_prime_batch_layers(
2400                e,
2401                x,
2402                0,
2403                self.layers.len(),
2404                &ts,
2405                &offs,
2406                &pos_ds,
2407                caches,
2408            )?;
2409            self.step35_prime_batch_epilogue(e, x, &ts, &offs, caches)?
2410        };
2411        crate::pp::STEP35_PRIME_BATCHES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2412        Ok(out)
2413    }
2414
2415    /// Cross-request BATCHED fresh prime (task #13, design in ARCHITECTURE-H100.md): the
2416    /// trunk's token-parallel ops (embed, norms, adds, ffn, projection GROUPS) run once on
2417    /// the CONCATENATION of B sequences — GEMMs at m = sum_T, the continuous-batching win
2418    /// the serving-lane bench attributed the remaining vLLM gap to. The stateful mixer
2419    /// CORES (QK-norm/RoPE/FA/append, conv/GDN scans) run per sequence on split projection
2420    /// buffers (D2D row copies; the mixers' own out-projections stay per-seq this
2421    /// increment). CONTINUATION primes (increment (b), 2026-07-30): cache.pos > 0 seqs
2422    /// batch the projections/FFN/lm_head exactly like fresh; the mixer cores take the
2423    /// per-seq CONTINUATION arms (Full: core_inner with carried pos_d + fa_prefill_view
2424    /// over the quantized past; Linear: the stateful pad_view twin — the same state
2425    /// carry the chunked single-seq prime rides). The fresh-only favl/gdn-vl fast paths
2426    /// stay byte-identical (gated on !carried). gemma4 models have no continuation
2427    /// prime (v0 monolithic fresh) — carried gemma4 batches return Err (caller falls
2428    /// back to single-chunk serving).
2429    /// NUMERIC CONFIG: a concat GEMM tiles K differently than per-seq GEMMs — same class
2430    /// as every prefill GEMM change; prime_batch_gate arbitrates (argmax + stream battery).
2431    pub fn prime_cache_batch(
2432        &self,
2433        e: &Engine,
2434        prompts: &[&[u32]],
2435        caches: &mut [&mut Cache],
2436    ) -> Result<Vec<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>)>, Box<dyn std::error::Error>> {
2437        let cfg = &self.cfg;
2438        let n_embd = cfg.n_embd as usize;
2439        let eps = cfg.rms_eps;
2440        let b = prompts.len();
2441        assert!(b >= 1 && b == caches.len());
2442        let pos0s: Vec<usize> = caches.iter().map(|c| c.pos).collect();
2443        let carried = pos0s.iter().any(|&p| p > 0);
2444        // gemma4: refuse UNCONDITIONALLY (2026-08-07, lane/gemma4-serve-gaps). The old guard
2445        // covered only `carried` — two concurrent FRESH gemma4 prompts batched into the
2446        // generic concat attn core below (uniform geometry, no per-layer swa window, no
2447        // softcapped head): compiles, runs, wrong logits. Same silent-wrong class as the
2448        // step35 refusal beneath. The per-sequence `gemma4_prime` is the supported prefill.
2449        if cfg.gemma4.is_some() {
2450            return Err(
2451                "prime_cache_batch: gemma4 has no batched prime core (per-layer \
2452                        swa/global geometry, softcapped head) — use gemma4_prime per sequence"
2453                    .into(),
2454            );
2455        }
2456        // Step35 has a dedicated concat walk: the generic core below cannot express its
2457        // per-layer geometry/SWA/head gate, and under PP the dedicated path is stage-scoped.
2458        if cfg.step35.is_some() {
2459            return self.step35_prime_cache_batch(e, prompts, caches);
2460        }
2461        let ts: Vec<usize> = prompts.iter().map(|p| p.len()).collect();
2462        for &t in &ts {
2463            assert!(
2464                t >= PRIME_MIN_T,
2465                "prime_cache_batch needs T >= {PRIME_MIN_T}"
2466            );
2467        }
2468        for (s, c) in caches.iter().enumerate() {
2469            assert!(
2470                c.pos + ts[s] <= c.max_ctx,
2471                "prime_cache_batch: prompt exceeds cache max_ctx"
2472            );
2473        }
2474        let total: usize = ts.iter().sum();
2475        let offs: Vec<usize> = ts
2476            .iter()
2477            .scan(0usize, |a, &t| {
2478                let o = *a;
2479                *a += t;
2480                Some(o)
2481            })
2482            .collect();
2483        // per-seq positions (fresh: 0..T_s; continuation: pos0..pos0+T_s)
2484        let pos_ds: Vec<CudaSlice<i32>> = ts
2485            .iter()
2486            .zip(&pos0s)
2487            .map(|(&t, &p0)| e.htod_i32(&(p0 as i32..(p0 + t) as i32).collect::<Vec<_>>()))
2488            .collect::<Result<_, _>>()?;
2489        // split a concat [total, dim] buffer into per-seq copies
2490        let split = |e: &Engine,
2491                     y: &CudaSlice<f32>,
2492                     dim: usize|
2493         -> Result<Vec<CudaSlice<f32>>, Box<dyn std::error::Error>> {
2494            let mut out = Vec::with_capacity(b);
2495            for s in 0..b {
2496                let mut ys = e.uninit(ts[s] * dim)?;
2497                e.copy_view_into(
2498                    &mut ys,
2499                    0,
2500                    &y.slice(offs[s] * dim..(offs[s] + ts[s]) * dim),
2501                    ts[s] * dim,
2502                )?;
2503                out.push(ys);
2504            }
2505            Ok(out)
2506        };
2507
2508        let cat_tokens: Vec<u32> = prompts.iter().flat_map(|p| p.iter().copied()).collect();
2509        let mut x = self.embed(e, &cat_tokens)?; // [total, n_embd]
2510        for (il, layer) in self.layers.iter().enumerate() {
2511            let mut h = e.uninit(total * n_embd)?;
2512            let mut hx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
2513            e.rms_norm_f16out(
2514                &x,
2515                layer.attn_norm.float_data(),
2516                &mut h,
2517                &mut hx16,
2518                n_embd,
2519                total,
2520                eps,
2521            )?;
2522            // mixer: projection GROUP on the concat (m = total), stateful core per seq
2523            let mut mixed = e.uninit(total * n_embd)?;
2524            match &layer.mixer {
2525                Mixer::Full(fa) => {
2526                    let g3 = e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], &h, &hx16, total)?;
2527                    // task #18 (attn side): the WHOLE attn core is varlen for fresh gated
2528                    // batches — split/QK-norm/RoPE/append (attn_pre_vl8, view inputs: the
2529                    // q/k/v split copies vanish) + ONE varlen FA. Per-block math identical
2530                    // everywhere (bit-gateable); MEMRA_FA_VL=0 or a non-bf16kv config falls
2531                    // back to the per-seq dispatch.
2532                    let geometry = self.cfg.full_attention_geometry_at(il as u32);
2533                    let (n_head, n_head_kv, head_dim) = (
2534                        geometry.n_head as usize,
2535                        geometry.n_head_kv as usize,
2536                        geometry.head_dim_k as usize,
2537                    );
2538                    let fa_scale = geometry.attention_scale();
2539                    let use_favl = !carried
2540                        && (2..=8).contains(&b)
2541                        && (head_dim == 256 || head_dim == 128)
2542                        && geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ
2543                        && std::env::var("MEMRA_NOFA").is_err()
2544                        && std::env::var("MEMRA_FA_FLOOR").is_err()
2545                        && std::env::var("MEMRA_FA_PP_W2").as_deref() != Ok("1")
2546                        && std::env::var("MEMRA_FA_BF16KV").as_deref() != Ok("0")
2547                        && std::env::var("MEMRA_FA_VL").as_deref() != Ok("0");
2548                    if use_favl {
2549                        let (qf_w, kf_w, vf_w) = (
2550                            fa.wq.out_features(),
2551                            fa.wk.out_features(),
2552                            fa.wv.out_features(),
2553                        );
2554                        struct APre {
2555                            q: CudaSlice<f32>,
2556                            gate: Option<CudaSlice<f32>>,
2557                            qn: CudaSlice<f32>,
2558                            kn: CudaSlice<f32>,
2559                        }
2560                        let mut aps = Vec::with_capacity(b);
2561                        for &t in ts.iter().take(b) {
2562                            aps.push(APre {
2563                                q: e.uninit(t * n_head * head_dim)?,
2564                                gate: Some(e.uninit(t * n_head * head_dim)?),
2565                                qn: e.uninit(t * n_head * head_dim)?,
2566                                kn: e.uninit(t * n_head_kv * head_dim)?,
2567                            });
2568                        }
2569                        let (kv_dim_k, kv_dim_v, ktb, vtb) = {
2570                            let kvl = caches[0].kv[il].as_ref().unwrap();
2571                            (kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes)
2572                        };
2573                        let pargs: Vec<crate::AttnPreVl> = (0..b)
2574                            .map(|s| {
2575                                let (o, t) = (offs[s], ts[s]);
2576                                let kvl = caches[s].kv[il].as_ref().unwrap();
2577                                assert!(
2578                                    kvl.len == 0 && kvl.len + t <= caches[s].max_ctx,
2579                                    "prime_cache_batch attn vl: fresh + capacity"
2580                                );
2581                                crate::AttnPreVl {
2582                                    qf: e.addr_f32v(&g3[0].slice(o * qf_w..(o + t) * qf_w)),
2583                                    kf: e.addr_f32v(&g3[1].slice(o * kf_w..(o + t) * kf_w)),
2584                                    vf: e.addr_f32v(&g3[2].slice(o * vf_w..(o + t) * vf_w)),
2585                                    q: e.addr_f32(&aps[s].q),
2586                                    gate: e.addr_f32(aps[s].gate.as_ref().unwrap()),
2587                                    qn: e.addr_f32(&aps[s].qn),
2588                                    kn: e.addr_f32(&aps[s].kn),
2589                                    kc: e.addr_u8(&kvl.k),
2590                                    vc: e.addr_u8(&kvl.v),
2591                                    t: t as i32,
2592                                    pad: 0,
2593                                }
2594                            })
2595                            .collect();
2596                        e.attn_pre_vl8(
2597                            &pargs,
2598                            fa.q_norm.float_data(),
2599                            fa.k_norm.float_data(),
2600                            head_dim,
2601                            geometry.n_rot as usize,
2602                            n_head,
2603                            n_head_kv,
2604                            self.cfg.rms_eps,
2605                            geometry.rope_base,
2606                            1.0,
2607                            kv_dim_k,
2608                            kv_dim_v,
2609                            ktb,
2610                            vtb,
2611                        )?;
2612                        for s in 0..b {
2613                            let kvl = caches[s].kv[il].as_mut().unwrap();
2614                            kvl.len += ts[s];
2615                            let new_len = kvl.len as i32;
2616                            e.set_i32_one(&mut kvl.len_d, new_len)?;
2617                        }
2618                        let mut attns = Vec::with_capacity(b);
2619                        let mut mirrors = Vec::with_capacity(b);
2620                        for &t in ts.iter().take(b) {
2621                            attns.push(e.uninit(t * n_head * head_dim)?);
2622                            let n = t * n_head_kv * head_dim;
2623                            mirrors.push((e.alloc_u8_uninit(n * 2)?, e.alloc_u8_uninit(n * 2)?));
2624                        }
2625                        // FA3 batched twin (round 31): TMA-swizzled wgmma vl when the
2626                        // promoted single-seq config is on; else the mma favl.
2627                        let fa3_on = match std::env::var("MEMRA_FA3").as_deref() {
2628                            Ok("0") => false,
2629                            Ok("1") => true,
2630                            _ => cfg!(memra_hopper_mma),
2631                        };
2632                        if fa3_on {
2633                            let mut q16s = Vec::with_capacity(b);
2634                            let mut v16s = Vec::with_capacity(b);
2635                            for s in 0..b {
2636                                let t = ts[s];
2637                                let mut q16 = e.alloc_u8_uninit(t * n_head * head_dim * 2)?;
2638                                e.f32_to_bf16_into(&aps[s].qn, &mut q16, t * n_head * head_dim)?;
2639                                let mut k16 = e.alloc_u8_uninit(t * n_head_kv * head_dim * 2)?;
2640                                e.f32_to_bf16_into(&aps[s].kn, &mut k16, t * n_head_kv * head_dim)?;
2641                                let mut v16 = e.alloc_u8_uninit(t * n_head_kv * head_dim * 2)?;
2642                                e.f32_to_bf16_v(
2643                                    &g3[2].slice(offs[s] * vf_w..(offs[s] + t) * vf_w),
2644                                    &mut v16,
2645                                    t * n_head_kv * head_dim,
2646                                )?;
2647                                q16s.push(q16);
2648                                v16s.push((k16, v16));
2649                            }
2650                            let mut qp = [core::ptr::null::<core::ffi::c_void>(); 8];
2651                            let mut kp = qp;
2652                            let mut vp = qp;
2653                            let mut op = [core::ptr::null_mut::<f32>(); 8];
2654                            let mut tsv = [0i32; 8];
2655                            for s in 0..b {
2656                                qp[s] = e.addr_u8(&q16s[s]) as *const core::ffi::c_void;
2657                                kp[s] = e.addr_u8(&v16s[s].0) as *const core::ffi::c_void;
2658                                vp[s] = e.addr_u8(&v16s[s].1) as *const core::ffi::c_void;
2659                                op[s] = e.addr_f32(&attns[s]) as *mut f32;
2660                                tsv[s] = ts[s] as i32;
2661                            }
2662                            let rc = unsafe {
2663                                crate::fa3_vl_raw(
2664                                    qp.as_ptr(),
2665                                    kp.as_ptr(),
2666                                    vp.as_ptr(),
2667                                    op.as_ptr(),
2668                                    tsv.as_ptr(),
2669                                    b as i32,
2670                                    n_head as i32,
2671                                    n_head_kv as i32,
2672                                    head_dim as i32,
2673                                    fa_scale,
2674                                    e.stream().cu_stream() as *mut core::ffi::c_void,
2675                                )
2676                            };
2677                            if rc != 0 {
2678                                return Err(format!("memra_fa3_vl rc={rc}").into());
2679                            }
2680                        } else {
2681                            let fargs: Vec<crate::FaSeqVl> = (0..b)
2682                                .map(|s| crate::FaSeqVl {
2683                                    q: e.addr_f32(&aps[s].qn),
2684                                    k16: e.addr_u8(&mirrors[s].0),
2685                                    v16: e.addr_u8(&mirrors[s].1),
2686                                    o: e.addr_f32(&attns[s]),
2687                                    kf: e.addr_f32(&aps[s].kn),
2688                                    vf: e.addr_f32v(
2689                                        &g3[2].slice(offs[s] * vf_w..(offs[s] + ts[s]) * vf_w),
2690                                    ),
2691                                    t: ts[s] as i32,
2692                                    pad: 0,
2693                                })
2694                                .collect();
2695                            e.fa_prefill_vl8(&fargs, head_dim, n_head, n_head_kv, fa_scale)?;
2696                        }
2697                        for (s, attn) in attns.into_iter().enumerate() {
2698                            let (attn_g, ag16) = self.full_attn_prime_post_fa(
2699                                e,
2700                                attn,
2701                                &aps[s].gate,
2702                                ts[s],
2703                                n_head,
2704                                head_dim,
2705                            )?;
2706                            let mut done = false;
2707                            if let Some(xh) = &ag16 {
2708                                done = e.try_f16_gemm_pre_into_off(
2709                                    &fa.wo,
2710                                    xh,
2711                                    ts[s],
2712                                    &mut mixed,
2713                                    offs[s] * n_embd,
2714                                )?;
2715                            }
2716                            if !done {
2717                                let m = e.matmul(&fa.wo, &attn_g, ts[s])?;
2718                                e.copy_into(&mut mixed, offs[s] * n_embd, &m, ts[s] * n_embd)?;
2719                            }
2720                        }
2721                    } else {
2722                        let mut parts: Vec<Vec<CudaSlice<f32>>> =
2723                            (0..b).map(|_| Vec::new()).collect();
2724                        for (w, y) in [&fa.wq, &fa.wk, &fa.wv].iter().zip(g3) {
2725                            for (s, ys) in split(e, &y, w.out_features())?.into_iter().enumerate() {
2726                                parts[s].push(ys);
2727                            }
2728                        }
2729                        for (s, g3s) in parts.into_iter().enumerate() {
2730                            // task #16 gather removal: wo writes into `mixed` at offs[s] directly.
2731                            let (attn_g, ag16) = self.full_attn_prime_core_inner(
2732                                e, fa, g3s, &pos_ds[s], ts[s], caches[s], il,
2733                            )?;
2734                            let mut done = false;
2735                            if let Some(xh) = &ag16 {
2736                                done = e.try_f16_gemm_pre_into_off(
2737                                    &fa.wo,
2738                                    xh,
2739                                    ts[s],
2740                                    &mut mixed,
2741                                    offs[s] * n_embd,
2742                                )?;
2743                            }
2744                            if !done {
2745                                let m = e.matmul(&fa.wo, &attn_g, ts[s])?;
2746                                e.copy_into(&mut mixed, offs[s] * n_embd, &m, ts[s] * n_embd)?;
2747                            }
2748                        }
2749                    }
2750                }
2751                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
2752                Mixer::Linear(la) => {
2753                    // task #16: NO split copies (cores read row-offset views of the concat
2754                    // outputs; out-GEMMs write into `mixed` at offs[s]). task #18: the core
2755                    // itself is BATCHED — per-seq prep/K1-K3, then ONE varlen K4 + ONE
2756                    // varlen K5 launch for all sequences.
2757                    let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
2758                    let g4 = e.matmul_group_xh(&ws, &h, &hx16, total)?;
2759                    let outs =
2760                        self.linear_attn_prime_core_batch(e, la, &g4, &offs, &ts, caches, il)?;
2761                    for (s, (gn, gn16)) in outs.into_iter().enumerate() {
2762                        let (o, t) = (offs[s], ts[s]);
2763                        let mut done = false;
2764                        if let Some(xh) = &gn16 {
2765                            done = e.try_f16_gemm_pre_into_off(
2766                                &la.ssm_out,
2767                                xh,
2768                                t,
2769                                &mut mixed,
2770                                o * n_embd,
2771                            )?;
2772                        }
2773                        if !done {
2774                            let m = e.matmul(&la.ssm_out, &gn, t)?;
2775                            e.copy_into(&mut mixed, o * n_embd, &m, t * n_embd)?;
2776                        }
2777                    }
2778                }
2779            }
2780            let mut x1 = e.uninit(total * n_embd)?;
2781            let mut z = e.uninit(total * n_embd)?;
2782            let mut zx16 = e.alloc_u8_uninit(total * n_embd * 2)?;
2783            e.add_rms_norm_f16out(
2784                &x,
2785                &mixed,
2786                layer.post_attn_norm.float_data(),
2787                &mut x1,
2788                &mut z,
2789                &mut zx16,
2790                n_embd,
2791                total,
2792                eps,
2793            )?;
2794            let ffn_out = match &layer.ffn {
2795                crate::hybrid::Ffn::Dense {
2796                    ffn_gate,
2797                    ffn_up,
2798                    ffn_down,
2799                } => {
2800                    let n_ff = ffn_gate.out_features();
2801                    let mut g2 = e.matmul_group_xh(&[ffn_gate, ffn_up], &z, &zx16, total)?;
2802                    let up = g2.pop().unwrap();
2803                    let gate = g2.pop().unwrap();
2804                    let mut act = e.uninit(total * n_ff)?;
2805                    // task #17 (batch trunk): silu twin emits the down GEMM's fp16 operand
2806                    // in-epilogue (nsys round-26: this trunk still paid 32 cvt passes).
2807                    // A clamped layer must skip the plain-SiLU twin (see prime_chunk's note).
2808                    let d_lim = self.cfg.clamp_shexp_at(il as u32);
2809                    if Self::f16out_on(e, total) && self.cfg.m3.is_none() && d_lim.is_none() {
2810                        let mut a16 = e.alloc_u8_uninit(total * n_ff * 2)?;
2811                        e.silu_mul_f16out(&gate, &up, &mut act, &mut a16, total * n_ff)?;
2812                        match e.try_f16_gemm_pre(ffn_down, &a16, total)? {
2813                            Some(y) => y,
2814                            None => e.matmul(ffn_down, &act, total)?,
2815                        }
2816                    } else {
2817                        Self::ffn_act_lim(
2818                            e,
2819                            &self.cfg,
2820                            &gate,
2821                            &up,
2822                            1.0,
2823                            1.0,
2824                            d_lim,
2825                            &mut act,
2826                            total * n_ff,
2827                        )?;
2828                        e.matmul(ffn_down, &act, total)?
2829                    }
2830                }
2831                crate::hybrid::Ffn::Moe(m) => {
2832                    self.moe_ffn_il_prefill(e, m, &z, total, il as u16)?
2833                }
2834            };
2835            let mut x2 = e.uninit(total * n_embd)?;
2836            e.add(&x1, &ffn_out, &mut x2, total * n_embd)?;
2837            x = x2;
2838        }
2839        // epilogue per seq (identical math to prime_chunk: norm all rows, lm_head on last row)
2840        let mut hn = e.uninit(total * n_embd)?;
2841        e.rms_norm(
2842            &x,
2843            self.output_norm.float_data(),
2844            &mut hn,
2845            n_embd,
2846            total,
2847            eps,
2848        )?;
2849        // batched lm_head (nsys round-26: B sequential m=1 matvecs re-read the 600MB
2850        // lm_head weight B times — 2.2ms at B=6): gather the B last rows and run ONE
2851        // m=B GEMM (f16 lane; falls back to the per-seq matvec when no mirror). The
2852        // f16-vs-mmvq logits delta is the prefill GEMM numeric class — prime_batch_gate's
2853        // argmax battery arbitrates, same as every other prefill GEMM change.
2854        let mut hcat = e.uninit(b * n_embd)?;
2855        for s in 0..b {
2856            let last0 = (offs[s] + ts[s] - 1) * n_embd;
2857            e.copy_view_into(
2858                &mut hcat,
2859                s * n_embd,
2860                &hn.slice(last0..last0 + n_embd),
2861                n_embd,
2862            )?;
2863        }
2864        let logits_cat = if b >= 2 {
2865            e.try_f16_gemm(&self.output, &hcat, b)?
2866        } else {
2867            None
2868        };
2869        let logits_host: Option<Vec<f32>> = match &logits_cat {
2870            Some(lc) => Some(e.dtoh(lc)?),
2871            None => None,
2872        };
2873        let n_vocab = self.output.out_features();
2874        let mut hidden_all = if crate::spec::spec_hpost() {
2875            split(e, &hn, n_embd)?
2876        } else {
2877            split(e, &x, n_embd)?
2878        };
2879        let mut out = Vec::with_capacity(b);
2880        for s in 0..b {
2881            let last0 = (offs[s] + ts[s] - 1) * n_embd;
2882            let mut h_seed = e.uninit(n_embd)?;
2883            if !crate::spec::spec_hpost() {
2884                e.copy_view_into(&mut h_seed, 0, &x.slice(last0..last0 + n_embd), n_embd)?;
2885            } else {
2886                e.copy_view_into(&mut h_seed, 0, &hn.slice(last0..last0 + n_embd), n_embd)?;
2887            }
2888            let logits = match &logits_host {
2889                Some(lh) => lh[s * n_vocab..(s + 1) * n_vocab].to_vec(),
2890                None => {
2891                    let mut hlast = e.uninit(n_embd)?;
2892                    e.copy_view_into(&mut hlast, 0, &hn.slice(last0..last0 + n_embd), n_embd)?;
2893                    e.dtoh(&e.matmul(&self.output, &hlast, 1)?)?
2894                }
2895            };
2896            caches[s].pos += ts[s];
2897            out.push((logits, h_seed, hidden_all.remove(0)));
2898        }
2899        Ok(out)
2900    }
2901
2902    /// `full_attn` (batched prefill mixer) + the cache side-effect: append the T post-RoPE K/V
2903    /// rows into the resident quantized KV cache (q8_0 K / q5_1 V) and advance len/len_d. Row
2904    /// bytes are BIT-IDENTICAL to the decode append (same per-warp quant kernel per row; the
2905    /// batched `append_kv_quantized_rows` runs that exact warp math on a (block, token) grid).
2906    /// The attention itself is unchanged prefill math (fa_prefill over the f32 K/V).
2907    ///
2908    /// `seq_end` = the ABSOLUTE end position of the WHOLE prime request (`cache.pos + prompt_len`
2909    /// measured BEFORE the chunk loop starts), i.e. a chunk-size-invariant property of the request.
2910    /// Only step35's SWA arm reads it (`step35_attn_pre_wo`'s doc note explains why keying on the
2911    /// chunk's own `t_kv` made the output depend on MEMRA_PRIME_CHUNK); every other arch ignores it.
2912    #[allow(clippy::too_many_arguments)]
2913    fn full_attn_prime(
2914        &self,
2915        e: &Engine,
2916        fa: &FullAttnLayer,
2917        h: &CudaSlice<f32>,
2918        hx: Option<&CudaSlice<u8>>,
2919        pos_d: &CudaSlice<i32>,
2920        t: usize,
2921        cache: &mut Cache,
2922        il: usize,
2923        seq_end: usize,
2924    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2925        if self.cfg.step35.is_some() {
2926            return self.step35_attn_prime(e, fa, h, hx, pos_d, t, cache, il, seq_end);
2927        }
2928        // PROJ/CORE SPLIT (task #13, 2026-07-26): the q/k/v group projection is hoisted so
2929        // the cross-request batch driver can run it at m = sum_T over concatenated tokens;
2930        // this single-seq path composes proj+core identically (byte-for-byte the old body).
2931        // task #14: `hx` = the norm-fused fp16 twin of `h` (skips the convert launch).
2932        let g3 = match hx {
2933            Some(xh) => e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], h, xh, t)?,
2934            None => e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?,
2935        };
2936        self.full_attn_prime_core(e, fa, g3, pos_d, t, cache, il)
2937    }
2938
2939    /// Everything after the q/k/v projections (split/QK-norm/RoPE/FA/gate/append + wo).
2940    /// `g3` = matmul_group([wq, wk, wv]) output rows for THIS sequence.
2941    /// Composes inner + wo (== the pre-split core; every existing caller unchanged).
2942    fn full_attn_prime_core(
2943        &self,
2944        e: &Engine,
2945        fa: &FullAttnLayer,
2946        g3: Vec<CudaSlice<f32>>,
2947        pos_d: &CudaSlice<i32>,
2948        t: usize,
2949        cache: &mut Cache,
2950        il: usize,
2951    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
2952        let (attn_g, ag16) = self.full_attn_prime_core_inner(e, fa, g3, pos_d, t, cache, il)?;
2953        if let Some(xh) = &ag16 {
2954            if let Some(y) = e.try_f16_gemm_pre(&fa.wo, xh, t)? {
2955                return Ok(y);
2956            }
2957        }
2958        Ok(e.matmul(&fa.wo, &attn_g, t)?)
2959    }
2960
2961    fn full_attn_prime_core_inner(
2962        &self,
2963        e: &Engine,
2964        fa: &FullAttnLayer,
2965        g3: Vec<CudaSlice<f32>>,
2966        pos_d: &CudaSlice<i32>,
2967        t: usize,
2968        cache: &mut Cache,
2969        il: usize,
2970    ) -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
2971        let cfg = &self.cfg;
2972        let geometry = cfg.full_attention_geometry_at(il as u32);
2973        let n_head = geometry.n_head as usize;
2974        let n_head_kv = geometry.n_head_kv as usize;
2975        let head_dim = geometry.head_dim_k as usize;
2976        let scale = geometry.attention_scale();
2977        let (pre, base_len) = self.full_attn_prime_pre_fa(e, fa, g3, pos_d, t, cache, il)?;
2978        let AttnPre { q, k, v, gate } = pre;
2979        let mut attn = e.uninit(t * n_head * head_dim)?;
2980        self.full_attn_prime_fa_dispatch(
2981            e, &q, &k, &v, &mut attn, base_len, t, cache, il, head_dim, n_head, n_head_kv, scale,
2982        )?;
2983        self.full_attn_prime_post_fa(e, attn, &gate, t, n_head, head_dim)
2984    }
2985
2986    /// task #18 (attn side): projections tail through KV append — everything before the
2987    /// attention kernel. Returns the post-rope q/k, v, optional out-gate, and the KV rows
2988    /// present BEFORE this chunk's append (base_len; 0 == fresh).
2989    #[allow(clippy::type_complexity)]
2990    fn full_attn_prime_pre_fa(
2991        &self,
2992        e: &Engine,
2993        fa: &FullAttnLayer,
2994        mut g3: Vec<CudaSlice<f32>>,
2995        pos_d: &CudaSlice<i32>,
2996        t: usize,
2997        cache: &mut Cache,
2998        il: usize,
2999    ) -> Result<(AttnPre, usize), Box<dyn std::error::Error>> {
3000        let cfg = &self.cfg;
3001        let geometry = cfg.full_attention_geometry_at(il as u32);
3002        let n_head = geometry.n_head as usize;
3003        let n_head_kv = geometry.n_head_kv as usize;
3004        let head_dim = geometry.head_dim_k as usize;
3005        let eps = cfg.rms_eps;
3006
3007        // qwen35 fuses [q|gate] per head in wq (2*head_dim stride); M3/Hy3 have NO output gate
3008        // (attention_output_gate=false) — wq out = n_head*head_dim exactly, and q_gate_split
3009        // would read 2x out of bounds. `gated` keys both the split and the sigmoid epilogue.
3010        let gated = geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ;
3011        let v = g3.pop().unwrap();
3012        let mut k = g3.pop().unwrap();
3013        let qf = g3.pop().unwrap();
3014        let (mut q, gate) = if gated {
3015            let mut q = e.uninit(t * n_head * head_dim)?;
3016            let mut gate = e.uninit(t * n_head * head_dim)?;
3017            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, t)?;
3018            (q, Some(gate))
3019        } else {
3020            (qf, None)
3021        };
3022
3023        let mut qn = e.uninit(t * n_head * head_dim)?;
3024        e.rms_norm(
3025            &q,
3026            fa.q_norm.float_data(),
3027            &mut qn,
3028            head_dim,
3029            n_head * t,
3030            eps,
3031        )?;
3032        q = qn;
3033        let mut kn = e.uninit(t * n_head_kv * head_dim)?;
3034        e.rms_norm(
3035            &k,
3036            fa.k_norm.float_data(),
3037            &mut kn,
3038            head_dim,
3039            n_head_kv * t,
3040            eps,
3041        )?;
3042        k = kn;
3043        let rope_dims = geometry.n_rot as usize;
3044        e.rope_neox(
3045            &mut q,
3046            pos_d,
3047            head_dim,
3048            rope_dims,
3049            n_head,
3050            t,
3051            geometry.rope_base,
3052            1.0,
3053        )?;
3054        e.rope_neox(
3055            &mut k,
3056            pos_d,
3057            head_dim,
3058            rope_dims,
3059            n_head_kv,
3060            t,
3061            geometry.rope_base,
3062            1.0,
3063        )?;
3064
3065        // CACHE SIDE-EFFECT: append the T post-rope K/V token rows (token-major [T, kv_dim] ==
3066        // the cache row layout) quantized into cache.kv[il], then advance len + device len_d.
3067        {
3068            let kvl = cache.kv[il].as_mut().unwrap();
3069            assert!(kvl.len + t <= cache.max_ctx, "prime_cache: KV overflow");
3070            e.append_kv_quantized_rows(
3071                &k,
3072                &v,
3073                &mut kvl.k,
3074                &mut kvl.v,
3075                kvl.len,
3076                t,
3077                kvl.kv_dim_k,
3078                kvl.kv_dim_v,
3079                kvl.k_tok_bytes,
3080                kvl.v_tok_bytes,
3081                crate::Engine::kv_fp8_on(),
3082            )?;
3083            kvl.len += t;
3084            let new_len = kvl.len as i32;
3085            e.set_i32_one(&mut kvl.len_d, new_len)?;
3086        }
3087
3088        let base_len = {
3089            let kvl = cache.kv[il].as_ref().unwrap();
3090            kvl.len - t // KV rows present BEFORE this chunk's append above
3091        };
3092        Ok((AttnPre { q, k, v, gate }, base_len))
3093    }
3094
3095    /// batched prefill attention. FRESH prime (no past KV): unchanged forward_last math over
3096    /// the f32 K/V of this batch. CONTINUATION chunk (past KV present): the chunk's queries
3097    /// must attend to [0 .. base+t) — run fa_prefill_view over the resident QUANTIZED cache
3098    /// (the spec-verify pattern; kernel's causal mask offsets by T_kv-T). Numerically this
3099    /// reads q8_0/q5_1-dequantized K/V for the past AND the current chunk — the same class as
3100    /// decode reading the cache; the run-gen/first-16 battery is the accuracy authority.
3101    #[allow(clippy::too_many_arguments)]
3102    fn full_attn_prime_fa_dispatch(
3103        &self,
3104        e: &Engine,
3105        q: &CudaSlice<f32>,
3106        k: &CudaSlice<f32>,
3107        v: &CudaSlice<f32>,
3108        attn: &mut CudaSlice<f32>,
3109        base_len: usize,
3110        t: usize,
3111        cache: &mut Cache,
3112        il: usize,
3113        head_dim: usize,
3114        n_head: usize,
3115        n_head_kv: usize,
3116        scale: f32,
3117    ) -> Result<(), Box<dyn std::error::Error>> {
3118        // GRAIN-FREE CHUNK-INVARIANCE FIX (lane/chunkinv-flip, 2026-08-05): the base_len == 0
3119        // f32 special case (fa_prefill over this batch's f32 K/V) is DROPPED. pre_fa appends
3120        // the chunk's quantized rows into cache.kv[il] BEFORE this dispatch, so chunk 0 can
3121        // attend through the quantized cache exactly like every later chunk (quantize-then-
3122        // attend). One numeric class for every row => the chunk size cannot decide where a
3123        // precision edge falls, so chunked prefill is reduction-order-stable with NO grain
3124        // knob (the stronger fix VERDICT.md filed; supersedes the MEMRA_PRIME_INVARIANT door's
3125        // pin-the-boundary approach).
3126        // MEMRA_PRIME_F32CHUNK0=1 is the ROLLBACK SEAM to the old arithmetic (chunk 0 attends
3127        // f32 K/V) — flags-doctrine rollback door AND the chunkinv gate's canary injection:
3128        // with the fix unconditional, only re-introducing the class edge can prove the gate
3129        // still detects the mechanism. Never on in a measured default run.
3130        if base_len == 0 && std::env::var("MEMRA_PRIME_F32CHUNK0").as_deref() == Ok("1") {
3131            if std::env::var("MEMRA_NOFA").is_ok() || !(head_dim == 256 || head_dim == 128) {
3132                e.sdpa_naive(
3133                    q, k, v, attn, head_dim, n_head, n_head_kv, t, t, scale, true,
3134                )?;
3135            } else {
3136                e.fa_prefill(
3137                    q, k, v, attn, head_dim, n_head, n_head_kv, t, t, scale, true,
3138                )?;
3139            }
3140            return Ok(());
3141        }
3142        let kvl = cache.kv[il].as_ref().unwrap();
3143        let t_kv = base_len + t;
3144        let k_view = e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes);
3145        let v_view = e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes);
3146        // fa_prefill_q/_qw twins are stamped for head_dim 256 (qwen35) and 128 (M3) only;
3147        // other dims (and MEMRA_NOFA) take the naive quantized-view SDPA — SAME cache bytes,
3148        // same numeric class, so the uniform contract holds on the fallback too.
3149        if std::env::var("MEMRA_NOFA").is_ok() || !(head_dim == 256 || head_dim == 128) {
3150            e.sdpa_naive_quantized_view(
3151                q,
3152                &k_view,
3153                &v_view,
3154                attn,
3155                head_dim,
3156                n_head,
3157                n_head_kv,
3158                t,
3159                t_kv,
3160                scale,
3161                true,
3162                kvl.k_tok_bytes,
3163                kvl.v_tok_bytes,
3164            )?;
3165            return Ok(());
3166        }
3167        // ARC B (2026-07-05): dequant-once workspace, DEFAULT ON. fa_prefill_q's inline
3168        // dequant re-reads+re-dequants the whole quantized KV stream from every one of the
3169        // T/64 x n_head CTAs (64x+ redundant at chunk=4096; 30.5% of the 32k prime wall).
3170        // fa_prefill_view_ws dequants K/V ONCE into a resident bf16 workspace then runs the
3171        // bit-identical bf16 twin (fa_prefill_qw) — same staged values, same FP order, token-
3172        // identical output (gate: MEMRA_PRIME_CHUNK=4096 ws-on vs ws-off vs monolithic).
3173        // MEMRA_PRIME_DEQW=0 reverts to the inline-dequant kernel.
3174        let deqw = std::env::var("MEMRA_PRIME_DEQW")
3175            .map(|v| v != "0")
3176            .unwrap_or(true);
3177        if deqw {
3178            e.fa_prefill_view_ws(
3179                q,
3180                &k_view,
3181                &v_view,
3182                attn,
3183                head_dim,
3184                n_head,
3185                n_head_kv,
3186                t,
3187                t_kv,
3188                scale,
3189                true,
3190                kvl.k_tok_bytes,
3191                kvl.v_tok_bytes,
3192                crate::Engine::kv_fp8_on(),
3193            )?;
3194        } else {
3195            e.fa_prefill_view(
3196                q,
3197                &k_view,
3198                &v_view,
3199                attn,
3200                head_dim,
3201                n_head,
3202                n_head_kv,
3203                t,
3204                t_kv,
3205                scale,
3206                true,
3207                kvl.k_tok_bytes,
3208                kvl.v_tok_bytes,
3209                crate::Engine::kv_fp8_on(),
3210            )?;
3211        }
3212        Ok(())
3213    }
3214
3215    /// task #17: sig_mul_f16out fuses [sigmoid + mul + f16 convert] into one launch
3216    /// (bit-identical composition) and hands wo its fp16 operand directly.
3217    fn full_attn_prime_post_fa(
3218        &self,
3219        e: &Engine,
3220        attn: CudaSlice<f32>,
3221        gate: &Option<CudaSlice<f32>>,
3222        t: usize,
3223        n_head: usize,
3224        head_dim: usize,
3225    ) -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
3226        let (attn_g, ag16) = match gate {
3227            Some(gate) => {
3228                let n = t * n_head * head_dim;
3229                let mut ag = e.uninit(n)?;
3230                if Self::f16out_on(e, t) {
3231                    let mut a16 = e.alloc_u8_uninit(n * 2)?;
3232                    e.sig_mul_f16out(&attn, gate, &mut ag, &mut a16, n)?;
3233                    (ag, Some(a16))
3234                } else {
3235                    let mut gsig = e.uninit(n)?;
3236                    e.sigmoid(gate, &mut gsig, n)?;
3237                    e.mul(&attn, &gsig, &mut ag, n)?;
3238                    (ag, None)
3239                }
3240            }
3241            None => (attn, None),
3242        };
3243        Ok((attn_g, ag16))
3244    }
3245
3246    /// STATEFUL batched linear-attention prime: `linear_attn`'s prefill-dispatch pass (normal
3247    /// `e.matmul` — GEMM at m>=16 — plus the prefill repack/L2/glog kernels) but with the state
3248    /// carried THROUGH the cache like the spec verify does: carried-ring conv
3249    /// (ssm_conv1d_tm_state writes the final ring back) + ONE gdn_scan from cache.recur[il]'s
3250    /// current state (zero at a fresh prime) whose final state ping-pongs back into the cache.
3251    /// Wiring mirrors `linear_attn_verify_t` (spec.rs); dispatch mirrors `linear_attn` (prefill).
3252    fn linear_attn_prime(
3253        &self,
3254        e: &Engine,
3255        la: &LinearAttnLayer,
3256        h: &CudaSlice<f32>,
3257        hx: Option<&CudaSlice<u8>>,
3258        t: usize,
3259        cache: &mut Cache,
3260        il: usize,
3261    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3262        // PROJ/CORE SPLIT (task #13): see full_attn_prime — same hoist for the GDN 4-tuple.
3263        let ws = [&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha];
3264        let g4 = match hx {
3265            Some(xh) => e.matmul_group_xh(&ws, h, xh, t)?,
3266            None => e.matmul_group(&ws, h, t)?,
3267        };
3268        self.linear_attn_prime_core(e, la, g4, t, cache, il)
3269    }
3270
3271    /// Everything after the GDN 4-tuple projections (conv/chunk stack/gated norm + ssm_out).
3272    fn linear_attn_prime_core(
3273        &self,
3274        e: &Engine,
3275        la: &LinearAttnLayer,
3276        mut g4: Vec<CudaSlice<f32>>,
3277        t: usize,
3278        cache: &mut Cache,
3279        il: usize,
3280    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3281        self.linear_attn_prime_core_pad(e, la, g4.drain(..).collect(), t, cache, il, None)
3282    }
3283
3284    /// task #14: `pad_len` = device true length for PADDED prime graphs. Pads become
3285    /// identity GDN steps (gdn_pad_mask zeroes beta/g_log past the true length) and the
3286    /// conv ring writes back from the true tail. None = classic path, byte-identical.
3287    #[allow(clippy::too_many_arguments)]
3288    fn linear_attn_prime_core_pad_inner(
3289        &self,
3290        e: &Engine,
3291        la: &LinearAttnLayer,
3292        mut g4: Vec<CudaSlice<f32>>,
3293        t: usize,
3294        cache: &mut Cache,
3295        il: usize,
3296        pad_len: Option<&CudaSlice<i32>>,
3297    ) -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
3298        // shim over the view twin (task #16): full-range views of the owned buffers.
3299        let ssm = self.cfg.ssm.as_ref().unwrap();
3300        let d_state = ssm.state_size as usize;
3301        let num_k = ssm.group_count as usize;
3302        let num_v = ssm.time_step_rank as usize;
3303        let key_dim = d_state * num_k;
3304        let value_dim = d_state * num_v;
3305        let conv_dim = key_dim * 2 + value_dim;
3306        let alpha = g4.pop().unwrap(); // [T, num_v]
3307        let beta_raw = g4.pop().unwrap(); // [T, num_v]
3308        let z = g4.pop().unwrap(); // [T, value_dim]
3309        let qkv_mixed = g4.pop().unwrap(); // [T, conv_dim] token-major
3310        self.linear_attn_prime_core_pad_view(
3311            e,
3312            la,
3313            &qkv_mixed.slice(0..t * conv_dim),
3314            &z.slice(0..t * value_dim),
3315            &beta_raw.slice(0..t * num_v),
3316            &alpha.slice(0..t * num_v),
3317            t,
3318            cache,
3319            il,
3320            pad_len,
3321        )
3322    }
3323
3324    /// task #18: the GDN prep stage (conv + repack + l2 x2 + sigmoid + glog + pad-mask) —
3325    /// shared verbatim by the per-seq scan path and the varlen batched path.
3326    #[allow(clippy::too_many_arguments)]
3327    fn linear_attn_gdn_prep(
3328        &self,
3329        e: &Engine,
3330        la: &LinearAttnLayer,
3331        qkv_mixed: &cudarc::driver::CudaView<f32>,
3332        beta_raw: &cudarc::driver::CudaView<f32>,
3333        alpha: &cudarc::driver::CudaView<f32>,
3334        t: usize,
3335        cache: &mut Cache,
3336        il: usize,
3337        pad_len: Option<&CudaSlice<i32>>,
3338    ) -> Result<GdnPrep, Box<dyn std::error::Error>> {
3339        let cfg = &self.cfg;
3340        let ssm = cfg.ssm.as_ref().unwrap();
3341        let d_state = ssm.state_size as usize; // 128
3342        let num_k = ssm.group_count as usize; // 16
3343        let num_v = ssm.time_step_rank as usize; // 32
3344        let d_conv = ssm.conv_kernel as usize; // 4
3345        let key_dim = d_state * num_k; // 2048
3346        let value_dim = d_state * num_v; // 4096
3347        let conv_dim = key_dim * 2 + value_dim; // 8192
3348        let eps = cfg.rms_eps;
3349        debug_assert!(
3350            t >= d_conv - 1,
3351            "stateful conv needs T >= pad (PRIME_MIN_T gates)"
3352        );
3353
3354        // conv with CARRIED ring state + ring roll (state read + final-window write-back).
3355        // task #18 conv-fuse (default ON): conv + SiLU + repack in one pass — the 67MB
3356        // conv_out intermediate and its transposed re-read disappear (values bit-identical).
3357        // MEMRA_CONV_FUSE=0 reverts to the two-kernel chain.
3358        let rl = cache.recur[il].as_mut().unwrap();
3359        let hk = Self::gdn_hk(e, t, num_v, num_k);
3360        let conv_fuse = std::env::var("MEMRA_CONV_FUSE").as_deref() != Ok("0");
3361        let hk = if conv_fuse { hk } else { num_v }; // de-broadcast rides the fused conv
3362        let mut q_g = e.uninit(d_state * hk * t)?;
3363        let mut k_g = e.uninit(d_state * hk * t)?;
3364        let mut v_g = e.uninit(d_state * num_v * t)?;
3365        if conv_fuse {
3366            e.ssm_conv1d_gdn_state_pad(
3367                qkv_mixed,
3368                &mut rl.conv_state,
3369                la.ssm_conv1d.float_data(),
3370                &mut q_g,
3371                &mut k_g,
3372                &mut v_g,
3373                conv_dim,
3374                t,
3375                d_conv,
3376                d_state,
3377                num_v,
3378                num_k,
3379                key_dim,
3380                hk,
3381                pad_len,
3382            )?;
3383        } else {
3384            let mut conv_out = e.uninit(conv_dim * t)?; // [conv_dim, T] channel-major, SiLU
3385            e.ssm_conv1d_tm_state_pad_v(
3386                qkv_mixed,
3387                &mut rl.conv_state,
3388                la.ssm_conv1d.float_data(),
3389                &mut conv_out,
3390                conv_dim,
3391                t,
3392                d_conv,
3393                pad_len,
3394            )?;
3395            e.qkv_to_gdn_repack(
3396                &conv_out, &mut q_g, &mut k_g, &mut v_g, d_state, num_v, num_k, key_dim, t,
3397            )?;
3398        }
3399        let mut q_l2 = e.uninit(d_state * hk * t)?;
3400        // mirror-fold (round 35): q's bf16 twin (wgmma K45/K2 A-operand) in-epilogue too.
3401        // Emitted only where a consumer exists (the wgmma config) — on other arches the
3402        // alloc + epilogue stores would be pure waste.
3403        let qb16 = if Engine::l2_v2_on(d_state) && e.gdn_wgmma_on(32) {
3404            let mut qb = e.alloc_u8_uninit(d_state * hk * t * 2)?;
3405            e.l2_norm_pp(&q_g, &mut q_l2, Some(&mut qb), d_state, hk * t, eps)?;
3406            Some(qb)
3407        } else {
3408            e.l2_norm_pp(&q_g, &mut q_l2, None, d_state, hk * t, eps)?;
3409            None
3410        };
3411        let mut k_l2 = e.uninit(d_state * hk * t)?;
3412        // mirror-fold: emit k's bf16 twin (the chunked-scan kb16 mirror) in-epilogue
3413        let kb16 = if Engine::l2_v2_on(d_state) {
3414            let mut kb = e.alloc_u8_uninit(d_state * hk * t * 2)?;
3415            e.l2_norm_pp(&k_g, &mut k_l2, Some(&mut kb), d_state, hk * t, eps)?;
3416            Some(kb)
3417        } else {
3418            e.l2_norm_pp(&k_g, &mut k_l2, None, d_state, hk * t, eps)?;
3419            None
3420        };
3421        let mut beta = e.uninit(t * num_v)?;
3422        e.sigmoid_v(beta_raw, &mut beta, t * num_v)?;
3423        let mut g_log = e.uninit(t * num_v)?;
3424        e.gdn_glog_v(
3425            alpha,
3426            la.ssm_dt.float_data(),
3427            la.ssm_a.float_data(),
3428            &mut g_log,
3429            num_v,
3430            t,
3431        )?;
3432        if let Some(len_d) = pad_len {
3433            e.gdn_pad_mask(&mut beta, &mut g_log, len_d, num_v, t)?;
3434        }
3435        Ok(GdnPrep {
3436            hk,
3437            q_l2,
3438            k_l2,
3439            v_g,
3440            beta,
3441            g_log,
3442            kb16,
3443            qb16,
3444        })
3445    }
3446
3447    /// task #18: batched GDN mixer core — per-seq prep + K1-K3, then ONE varlen K4 and
3448    /// ONE varlen K5 launch for all B sequences (full-machine occupancy vs B underfilled
3449    /// per-seq trains). Per-block math identical to the per-seq path (bit-gateable);
3450    /// MEMRA_GDN_VL=0 or a non-mma/non-C32 config falls back to the per-seq loop.
3451    #[allow(clippy::too_many_arguments)]
3452    fn linear_attn_prime_core_batch(
3453        &self,
3454        e: &Engine,
3455        la: &LinearAttnLayer,
3456        g4: &[CudaSlice<f32>],
3457        offs: &[usize],
3458        ts: &[usize],
3459        caches: &mut [&mut Cache],
3460        il: usize,
3461    ) -> Result<Vec<(CudaSlice<f32>, Option<CudaSlice<u8>>)>, Box<dyn std::error::Error>> {
3462        let ssm = self.cfg.ssm.as_ref().unwrap();
3463        let d_state = ssm.state_size as usize;
3464        let num_k = ssm.group_count as usize;
3465        let num_v = ssm.time_step_rank as usize;
3466        let key_dim = d_state * num_k;
3467        let value_dim = d_state * num_v;
3468        let conv_dim = key_dim * 2 + value_dim;
3469        let eps = self.cfg.rms_eps;
3470        let scale = 1.0 / (d_state as f32).sqrt();
3471        let b = ts.len();
3472        let c = Engine::gdn_chunk_size();
3473        // CONTINUATION batches (increment (b)) take the per-seq stateful pad_view twin
3474        // below — the varlen K4/K5 chain is fresh-only (zero initial state assumed).
3475        let carried = caches.iter().any(|c| c.pos > 0);
3476        let use_vl = !carried
3477            && (2..=8).contains(&b)
3478            && Engine::gdn_chunked_enabled()
3479            && ts.iter().all(|&t| t >= 16)
3480            && e.gdn_mma_enabled(c)
3481            && std::env::var("MEMRA_GDN_VL").as_deref() != Ok("0");
3482        if !use_vl {
3483            return (0..b)
3484                .map(|s| {
3485                    let (o, t) = (offs[s], ts[s]);
3486                    self.linear_attn_prime_core_pad_view(
3487                        e,
3488                        la,
3489                        &g4[0].slice(o * conv_dim..(o + t) * conv_dim),
3490                        &g4[1].slice(o * value_dim..(o + t) * value_dim),
3491                        &g4[2].slice(o * num_v..(o + t) * num_v),
3492                        &g4[3].slice(o * num_v..(o + t) * num_v),
3493                        t,
3494                        caches[s],
3495                        il,
3496                        None,
3497                    )
3498                })
3499                .collect();
3500        }
3501        // increment 3: the ENTIRE core is varlen — allocs only per seq, then 13 launches
3502        // total for the whole batch: [conv, ring, repack, l2(q+k), gate-prep] +
3503        // [k-mirror, K1, K2, K3, w-mirror, K4, K5] + [gated-norm tail].
3504        struct SeqBufs {
3505            conv_out: CudaSlice<f32>,
3506            q_g: CudaSlice<f32>,
3507            k_g: CudaSlice<f32>,
3508            v_g: CudaSlice<f32>,
3509            q_l2: CudaSlice<f32>,
3510            k_l2: CudaSlice<f32>,
3511            beta: CudaSlice<f32>,
3512            g_log: CudaSlice<f32>,
3513            gn: CudaSlice<f32>,
3514            gn16: CudaSlice<u8>,
3515        }
3516        let d_conv = ssm.conv_kernel as usize;
3517        let f16o = Self::f16out_on(e, 16);
3518        let hk = Self::gdn_hk(e, 16, num_v, num_k); // vl path is always chunked+mma
3519        let mut sb = Vec::with_capacity(b);
3520        let mut pres = Vec::with_capacity(b);
3521        for &t in ts.iter().take(b) {
3522            sb.push(SeqBufs {
3523                conv_out: e.uninit(conv_dim * t)?,
3524                q_g: e.uninit(d_state * hk * t)?,
3525                k_g: e.uninit(d_state * hk * t)?,
3526                v_g: e.uninit(d_state * num_v * t)?,
3527                q_l2: e.uninit(d_state * hk * t)?,
3528                k_l2: e.uninit(d_state * hk * t)?,
3529                beta: e.uninit(t * num_v)?,
3530                g_log: e.uninit(t * num_v)?,
3531                gn: e.uninit(d_state * num_v * t)?,
3532                gn16: e.alloc_u8_uninit(d_state * num_v * t * 2)?,
3533            });
3534            pres.push(e.gdn_chunk_alloc(num_v, t, c, hk)?);
3535        }
3536        let prep_args: Vec<crate::GdnPrepVl> = (0..b)
3537            .map(|s| {
3538                let (o, t) = (offs[s], ts[s]);
3539                let rl = caches[s].recur[il].as_ref().unwrap();
3540                crate::GdnPrepVl {
3541                    qkv: e.addr_f32v(&g4[0].slice(o * conv_dim..(o + t) * conv_dim)),
3542                    conv_state: e.addr_f32(&rl.conv_state),
3543                    conv_out: e.addr_f32(&sb[s].conv_out),
3544                    q_g: e.addr_f32(&sb[s].q_g),
3545                    k_g: e.addr_f32(&sb[s].k_g),
3546                    v_g: e.addr_f32(&sb[s].v_g),
3547                    q_l2: e.addr_f32(&sb[s].q_l2),
3548                    k_l2: e.addr_f32(&sb[s].k_l2),
3549                    beta_raw: e.addr_f32v(&g4[2].slice(o * num_v..(o + t) * num_v)),
3550                    alpha: e.addr_f32v(&g4[3].slice(o * num_v..(o + t) * num_v)),
3551                    beta: e.addr_f32(&sb[s].beta),
3552                    g_log: e.addr_f32(&sb[s].g_log),
3553                    o: e.addr_f32(&pres[s].o),
3554                    z: e.addr_f32v(&g4[1].slice(o * value_dim..(o + t) * value_dim)),
3555                    gn: e.addr_f32(&sb[s].gn),
3556                    gn16: e.addr_u8(&sb[s].gn16),
3557                    kb16: if Engine::l2_v2_on(d_state) {
3558                        e.addr_u8(&pres[s].kb16)
3559                    } else {
3560                        0
3561                    },
3562                    qb16: if Engine::l2_v2_on(d_state) && e.gdn_wgmma_on(c) {
3563                        e.addr_u8(&pres[s].qb16)
3564                    } else {
3565                        0
3566                    },
3567                    t: t as i32,
3568                    pad: 0,
3569                }
3570            })
3571            .collect();
3572        let args: Vec<crate::GdnSeqVl> = (0..b)
3573            .map(|s| {
3574                let rl = caches[s].recur[il].as_ref().unwrap();
3575                crate::GdnSeqVl {
3576                    kb16: e.addr_u8(&pres[s].kb16),
3577                    gcum: e.addr_f32(&pres[s].gcum),
3578                    beta: e.addr_f32(&sb[s].beta),
3579                    u: e.addr_f32(&pres[s].u),
3580                    wb16: e.addr_u8(&pres[s].wb16),
3581                    y: e.addr_u8(&pres[s].y16),
3582                    ssnap: e.addr_u8(&pres[s].ssnap16),
3583                    state_in: e.addr_f32(&rl.ssm_state),
3584                    state_out: e.addr_f32(&rl.ssm_state_alt),
3585                    q: e.addr_f32(&sb[s].q_l2),
3586                    p: e.addr_f32(&pres[s].p),
3587                    o: e.addr_f32(&pres[s].o),
3588                    k: e.addr_f32(&sb[s].k_l2),
3589                    v: e.addr_f32(&sb[s].v_g),
3590                    g: e.addr_f32(&sb[s].g_log),
3591                    a: e.addr_f32(&pres[s].a),
3592                    w: e.addr_f32(&pres[s].w),
3593                    t: ts[s] as i32,
3594                    nc: pres[s].nc as i32,
3595                }
3596            })
3597            .collect();
3598        e.gdn_prep_vl8(
3599            &prep_args,
3600            la.ssm_conv1d.float_data(),
3601            la.ssm_dt.float_data(),
3602            la.ssm_a.float_data(),
3603            conv_dim,
3604            d_conv,
3605            d_state,
3606            num_v,
3607            num_k,
3608            key_dim,
3609            hk,
3610            eps,
3611        )?;
3612        // mirror-fold (round 27): l2 v2 emits kb16 in-epilogue; K3's store emits wb16 —
3613        // both standalone mirror launches vanish on the default config.
3614        if !Engine::l2_v2_on(d_state) {
3615            e.gdn_mirror_vl8(&args, num_v, 0, hk)?;
3616        }
3617        // task #22: wgmma-fused vl twins — qb16 mirrors + GdnWVl8 side-struct.
3618        let wq8: Option<crate::GdnWVl8> = if e.gdn_wgmma_on(c) {
3619            // qb16 emitted by the vl l2 mirror-fold when l2-v2 serves; bulk cvt otherwise
3620            if !Engine::l2_v2_on(d_state) {
3621                for s in 0..b {
3622                    e.f32_to_bf16_into(&sb[s].q_l2, &mut pres[s].qb16, d_state * hk * ts[s])?;
3623                }
3624            }
3625            let mut wa = [crate::GdnWVl::default(); 8];
3626            for s in 0..b {
3627                wa[s] = crate::GdnWVl {
3628                    qb16: e.addr_u8(&pres[s].qb16),
3629                    pb16: e.addr_u8(&pres[s].pb16),
3630                };
3631            }
3632            Some(crate::GdnWVl8(wa))
3633        } else {
3634            None
3635        };
3636        e.gdn_chunk_k123_vl8(&args, num_v, hk, wq8.as_ref())?;
3637        e.gdn_chunk_vl8(&args, num_v, scale, hk, wq8.as_ref())?;
3638        if f16o {
3639            e.gdn_tail_vl8(&prep_args, la.ssm_norm.float_data(), d_state, num_v, eps)?;
3640        }
3641        // per-seq state swap (+ non-f16out tail fallback)
3642        let mut out = Vec::with_capacity(b);
3643        for (s, bufs) in sb.into_iter().enumerate() {
3644            let rl = caches[s].recur[il].as_mut().unwrap();
3645            std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
3646            let (o, t) = (offs[s], ts[s]);
3647            let SeqBufs { mut gn, gn16, .. } = bufs;
3648            if f16o {
3649                out.push((gn, Some(gn16)));
3650            } else {
3651                let z_v = g4[1].slice(o * value_dim..(o + t) * value_dim);
3652                e.gated_rmsnorm_zv(
3653                    &pres[s].o,
3654                    la.ssm_norm.float_data(),
3655                    &z_v,
3656                    &mut gn,
3657                    d_state,
3658                    num_v * t,
3659                    eps,
3660                )?;
3661                out.push((gn, None));
3662            }
3663        }
3664        Ok(out)
3665    }
3666
3667    /// task #16: view-consuming GDN prime core — the batched prime hands row-offset
3668    /// views of the CONCAT projection outputs directly (no per-seq split copies).
3669    /// Same kernels, same values, byte-identical to the Vec shim above.
3670    #[allow(clippy::too_many_arguments)]
3671    fn linear_attn_prime_core_pad_view(
3672        &self,
3673        e: &Engine,
3674        la: &LinearAttnLayer,
3675        qkv_mixed: &cudarc::driver::CudaView<f32>,
3676        z: &cudarc::driver::CudaView<f32>,
3677        beta_raw: &cudarc::driver::CudaView<f32>,
3678        alpha: &cudarc::driver::CudaView<f32>,
3679        t: usize,
3680        cache: &mut Cache,
3681        il: usize,
3682        pad_len: Option<&CudaSlice<i32>>,
3683    ) -> Result<(CudaSlice<f32>, Option<CudaSlice<u8>>), Box<dyn std::error::Error>> {
3684        let cfg = &self.cfg;
3685        let ssm = cfg.ssm.as_ref().unwrap();
3686        let d_state = ssm.state_size as usize; // 128
3687        let num_v = ssm.time_step_rank as usize; // 32
3688        let eps = cfg.rms_eps;
3689        let scale = 1.0 / (d_state as f32).sqrt();
3690
3691        let prep =
3692            self.linear_attn_gdn_prep(e, la, qkv_mixed, beta_raw, alpha, t, cache, il, pad_len)?;
3693
3694        // ONE gdn_scan over T from the cache's CURRENT state (zero at fresh prime); the final
3695        // state lands in the spare buffer and ping-pongs back (stable resident pointers, the
3696        // decode-determinism discipline from linear_attn_decode_inner). A4: `gdn_scan_prefill`
3697        // dispatches the chunked WY form under MEMRA_GDN_CHUNKED (prefill-only seam; decode +
3698        // verify keep the sequential kernel).
3699        let mut o = e.uninit(d_state * num_v * t)?;
3700        let rl = cache.recur[il].as_mut().unwrap();
3701        {
3702            let crate::cache::RecurLayer {
3703                ssm_state,
3704                ssm_state_alt,
3705                ..
3706            } = rl;
3707            e.gdn_scan_prefill(
3708                &prep.q_l2,
3709                &prep.k_l2,
3710                &prep.v_g,
3711                &prep.g_log,
3712                &prep.beta,
3713                prep.kb16.as_ref(),
3714                prep.qb16.as_ref(),
3715                ssm_state,
3716                ssm_state_alt,
3717                &mut o,
3718                num_v,
3719                t,
3720                scale,
3721                prep.hk,
3722            )?;
3723        }
3724        std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
3725
3726        // gated RMSNorm + out projection (prefill dispatch). task #17: the f16out twin also
3727        // emits the ssm_out GEMM's fp16 operand in-epilogue (kills the standalone convert).
3728        let mut gn = e.uninit(d_state * num_v * t)?;
3729        let gn16 = if Self::f16out_on(e, t) {
3730            let mut g16 = e.alloc_u8_uninit(d_state * num_v * t * 2)?;
3731            e.gated_rmsnorm_f16out_zv(
3732                &o,
3733                la.ssm_norm.float_data(),
3734                z,
3735                &mut gn,
3736                &mut g16,
3737                d_state,
3738                num_v * t,
3739                eps,
3740            )?;
3741            Some(g16)
3742        } else {
3743            e.gated_rmsnorm_zv(
3744                &o,
3745                la.ssm_norm.float_data(),
3746                z,
3747                &mut gn,
3748                d_state,
3749                num_v * t,
3750                eps,
3751            )?;
3752            None
3753        };
3754        Ok((gn, gn16))
3755    }
3756
3757    /// Task #15 core-split wrapper: composes inner + ssm_out (== the old core).
3758    #[allow(clippy::too_many_arguments)]
3759    fn linear_attn_prime_core_pad(
3760        &self,
3761        e: &Engine,
3762        la: &LinearAttnLayer,
3763        g4: Vec<CudaSlice<f32>>,
3764        t: usize,
3765        cache: &mut Cache,
3766        il: usize,
3767        pad_len: Option<&CudaSlice<i32>>,
3768    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3769        let (gn, gn16) = self.linear_attn_prime_core_pad_inner(e, la, g4, t, cache, il, pad_len)?;
3770        if let Some(xh) = &gn16 {
3771            if let Some(y) = e.try_f16_gemm_pre(&la.ssm_out, xh, t)? {
3772                return Ok(y);
3773            }
3774        }
3775        Ok(e.matmul(&la.ssm_out, &gn, t)?)
3776    }
3777
3778    /// Full-attention mixer with QK-norm, partial RoPE, sigmoid output gate (qwen35 :257-336).
3779    ///
3780    /// `il` = layer index: step35 needs it (per-layer n_head / rope width / window / gate) and
3781    /// routes to its own mixer. Every other arch ignores it (uniform geometry).
3782    pub fn full_attn(
3783        &self,
3784        e: &Engine,
3785        fa: &FullAttnLayer,
3786        h: &CudaSlice<f32>,
3787        pos_d: &CudaSlice<i32>,
3788        t: usize,
3789        il: usize,
3790    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3791        if self.cfg.step35.is_some() {
3792            return self.step35_attn(e, fa, h, pos_d, t, il);
3793        }
3794        let cfg = &self.cfg;
3795        let _n_embd = cfg.n_embd as usize;
3796        let geometry = cfg.full_attention_geometry_at(il as u32);
3797        let n_head = geometry.n_head as usize;
3798        let n_head_kv = geometry.n_head_kv as usize;
3799        let head_dim = geometry.head_dim_k as usize;
3800        let eps = cfg.rms_eps;
3801        let scale = geometry.attention_scale();
3802
3803        // qwen35: wq output = head_dim*2*n_head (fused [q|gate] per head). M3/Hy3: NO output
3804        // gate — wq out = n_head*head_dim, no split (see prime-path note).
3805        let gated = geometry.attention_gate == memra_gguf::config::AttentionGateKind::FusedQ;
3806        // grouped: one f16 activation convert feeds q/k/v (matmul_group)
3807        let mut g3 = e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?;
3808        let v = g3.pop().unwrap();
3809        let mut k = g3.pop().unwrap();
3810        let qf = g3.pop().unwrap();
3811        let (mut q, gate) = if gated {
3812            let mut q = e.uninit(t * n_head * head_dim)?;
3813            let mut gate = e.uninit(t * n_head * head_dim)?;
3814            e.q_gate_split(&qf, &mut q, &mut gate, head_dim, n_head, t)?;
3815            (q, Some(gate))
3816        } else {
3817            (qf, None)
3818        };
3819
3820        // QK-norm (per head_dim row), then partial RoPE.
3821        let mut qn = e.uninit(t * n_head * head_dim)?;
3822        e.rms_norm(
3823            &q,
3824            fa.q_norm.float_data(),
3825            &mut qn,
3826            head_dim,
3827            n_head * t,
3828            eps,
3829        )?;
3830        q = qn;
3831        let mut kn = e.uninit(t * n_head_kv * head_dim)?;
3832        e.rms_norm(
3833            &k,
3834            fa.k_norm.float_data(),
3835            &mut kn,
3836            head_dim,
3837            n_head_kv * t,
3838            eps,
3839        )?;
3840        k = kn;
3841        let rope_dims = geometry.n_rot as usize;
3842        e.rope_neox(
3843            &mut q,
3844            pos_d,
3845            head_dim,
3846            rope_dims,
3847            n_head,
3848            t,
3849            geometry.rope_base,
3850            1.0,
3851        )?;
3852        e.rope_neox(
3853            &mut k,
3854            pos_d,
3855            head_dim,
3856            rope_dims,
3857            n_head_kv,
3858            t,
3859            geometry.rope_base,
3860            1.0,
3861        )?;
3862
3863        // SDPA
3864        let mut attn = e.uninit(t * n_head * head_dim)?;
3865        // hand-written FlashAttention prefill (head_dim 256/128 stamped twins). MEMRA_NOFA
3866        // falls back to naive sdpa.
3867        if std::env::var("MEMRA_NOFA").is_ok() || !(head_dim == 256 || head_dim == 128) {
3868            // head_dim gate: see prime-path note (fa_prefill is stamped at 256 and 128 only).
3869            e.sdpa_naive(
3870                &q, &k, &v, &mut attn, head_dim, n_head, n_head_kv, t, t, scale, true,
3871            )?;
3872        } else {
3873            e.fa_prefill(
3874                &q, &k, &v, &mut attn, head_dim, n_head, n_head_kv, t, t, scale, true,
3875            )?;
3876        }
3877
3878        // output gate: attn * sigmoid(gate) — qwen35 only (M3 has no gate).
3879        let attn_g = match &gate {
3880            Some(gate) => {
3881                let mut gsig = e.uninit(t * n_head * head_dim)?;
3882                e.sigmoid(gate, &mut gsig, t * n_head * head_dim)?;
3883                let mut ag = e.uninit(t * n_head * head_dim)?;
3884                e.mul(&attn, &gsig, &mut ag, t * n_head * head_dim)?;
3885                ag
3886            }
3887            None => attn,
3888        };
3889
3890        // o projection
3891        let o = e.matmul(&fa.wo, &attn_g, t)?;
3892        Ok(o)
3893    }
3894
3895    /// Linear-attention (Gated DeltaNet) mixer (qwen35 :338-470).
3896    pub fn linear_attn(
3897        &self,
3898        e: &Engine,
3899        la: &LinearAttnLayer,
3900        h: &CudaSlice<f32>,
3901        t: usize,
3902    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
3903        let cfg = &self.cfg;
3904        let _n_embd = cfg.n_embd as usize;
3905        let ssm = cfg.ssm.as_ref().unwrap();
3906        let d_state = ssm.state_size as usize; // 128
3907        let num_k = ssm.group_count as usize; // 16
3908        let num_v = ssm.time_step_rank as usize; // 32
3909        let d_conv = ssm.conv_kernel as usize; // 4
3910        let head_k = d_state;
3911        let head_v = d_state;
3912        let key_dim = head_k * num_k; // 2048
3913        let value_dim = head_v * num_v; // 4096
3914        let conv_dim = key_dim * 2 + value_dim; // 8192
3915        let eps = cfg.rms_eps;
3916        let scale = 1.0 / (d_state as f32).sqrt();
3917
3918        // projections
3919        // grouped: one f16 activation convert feeds all four projections (matmul_group)
3920        let mut g4 = e.matmul_group(
3921            &[&la.wqkv, &la.wqkv_gate, &la.ssm_beta, &la.ssm_alpha],
3922            h,
3923            t,
3924        )?;
3925        let alpha = g4.pop().unwrap(); // [T, num_v]
3926        let beta_raw = g4.pop().unwrap(); // [T, num_v]
3927        let z = g4.pop().unwrap(); // [T, value_dim]
3928        let qkv_mixed = g4.pop().unwrap(); // [T, conv_dim] token-major
3929
3930        // conv + GDN repack, FUSED (2026-07-03): ssm_conv1d_gdn reads qkv_mixed [T, conv_dim]
3931        // token-major DIRECTLY (causal window rows t-pad..t, rows<0 = zero prefill state), applies
3932        // the 8-tap conv + SiLU, and scatters straight into the GDN [d_state, num_v, T] q/k/v
3933        // layout with the modulo head-repeat. Replaces transpose + zeros + conv_left_pad +
3934        // ssm_conv1d + qkv_to_gdn_repack (5 launches, conv_in/conv_out scratch + a 16MB@T=512
3935        // round-trip). BIT-IDENTICAL accumulation and scatter mapping.
3936        let _ = (head_k, head_v);
3937        let mut q_g = e.uninit(d_state * num_v * t)?;
3938        let mut k_g = e.uninit(d_state * num_v * t)?;
3939        let mut v_g = e.uninit(d_state * num_v * t)?;
3940        e.ssm_conv1d_gdn(
3941            &qkv_mixed,
3942            la.ssm_conv1d.float_data(),
3943            &mut q_g,
3944            &mut k_g,
3945            &mut v_g,
3946            conv_dim,
3947            t,
3948            d_conv,
3949            d_state,
3950            num_v,
3951            num_k,
3952            key_dim,
3953        )?;
3954        // L2-norm q,k per (head_dim) row — rows are contiguous d_state in q_g.
3955        let mut q_l2 = e.uninit(d_state * num_v * t)?;
3956        e.l2_norm(&q_g, &mut q_l2, d_state, num_v * t, eps)?;
3957        let mut k_l2 = e.uninit(d_state * num_v * t)?;
3958        e.l2_norm(&k_g, &mut k_l2, d_state, num_v * t, eps)?;
3959        let v_gd = v_g;
3960
3961        // beta = sigmoid(beta_raw) ; g_log = a * softplus(alpha + dt). Both need [num_v, T] layout
3962        // (g[t*num_v + h]). beta_raw/alpha are [T, num_v] token-major == that layout already.
3963        let mut beta = e.uninit(t * num_v)?;
3964        e.sigmoid(&beta_raw, &mut beta, t * num_v)?;
3965        // gdn_glog expects alpha [H,T] with alpha[t*H+h] and dt_bias/a [H] — matches token-major [T,num_v].
3966        let mut g_log = e.uninit(t * num_v)?;
3967        e.gdn_glog(
3968            &alpha,
3969            la.ssm_dt.float_data(),
3970            la.ssm_a.float_data(),
3971            &mut g_log,
3972            num_v,
3973            t,
3974        )?;
3975
3976        // GDN scan (A4: gdn_scan_prefill dispatches chunked WY under MEMRA_GDN_CHUNKED)
3977        let state_in = e.zeros(d_state * d_state * num_v)?; // zero state (prefill)
3978        let mut state_out = e.zeros(d_state * d_state * num_v)?;
3979        let mut o = e.uninit(d_state * num_v * t)?;
3980        e.gdn_scan_prefill(
3981            &q_l2,
3982            &k_l2,
3983            &v_gd,
3984            &g_log,
3985            &beta,
3986            None,
3987            None,
3988            &state_in,
3989            &mut state_out,
3990            &mut o,
3991            num_v,
3992            t,
3993            scale,
3994            num_v,
3995        )?;
3996
3997        // gated RMSNorm: dst = RMSNorm(o, ssm_norm[head_v]) * silu(z). o is [d_state, num_v, T];
3998        // rows of head_v=d_state, nrows = num_v*T. z must match row layout: z is [T, value_dim] token-major
3999        // = [T, num_v*head_v]; per (t, vh) the head_v slice is contiguous -> rows align as (t*num_v+vh).
4000        // o rows are (t*num_v+vh) too. Good.
4001        let mut gn = e.uninit(d_state * num_v * t)?;
4002        e.gated_rmsnorm(
4003            &o,
4004            la.ssm_norm.float_data(),
4005            &z,
4006            &mut gn,
4007            d_state,
4008            num_v * t,
4009            eps,
4010        )?;
4011
4012        // ssm_out projection: gn is [d_state, num_v, T] = [value_dim, T] viewed token-major as [T, value_dim]?
4013        // gn layout: (t*num_v+vh)*d_state + i  == token t, then (vh,i) = channel vh*d_state+i. That's
4014        // token-major [T, value_dim]. linear wants [T, in=value_dim]. Good.
4015        let out = e.matmul(&la.ssm_out, &gn, t)?;
4016        Ok(out)
4017    }
4018}
4019
4020impl HybridModel {
4021    /// MoE FFN (EDGE-1). z: [T, n_embd] (already post-attention-normed). Returns moe_out [T, n_embd].
4022    /// Node-for-node vs llama.cpp build_moe_ffn + qwen35moe::build_layer_ffn.
4023    ///
4024    /// `il` is the trunk layer index — the residency-cache key prefix (a gate-expert of layer 3 is a
4025    /// different 860160-byte block than the same expert of layer 7).
4026    ///
4027    /// Routing: host softmax+sort (default) OR the fused router kernel (MEMRA_FUSED_ROUTER).
4028    /// Dispatch: stage-every-token into 3 scratch slots (default) OR the SLRU residency cache
4029    /// (MEMRA_MOE_CACHE). The cache-HIT weight path is bit-identical to stage-every-token (§B.3).
4030    /// Convenience wrapper used by the hybrid trunk/MTP loops: pulls dims + max-block from `self`.
4031    pub fn moe_ffn_il(
4032        &self,
4033        e: &Engine,
4034        m: &MoeWeights,
4035        z: &CudaSlice<f32>,
4036        t: usize,
4037        il: u16,
4038    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4039        Self::moe_ffn_inner(e, m, z, None, t, &self.cfg, il, self.max_moe_block(), false)
4040    }
4041
4042    /// Prefill twin: Step35 promotes expert-grouped dispatch by default while decode/spec callers
4043    /// keep `moe_ffn_il` and therefore retain their existing dispatch class.
4044    pub fn moe_ffn_il_prefill(
4045        &self,
4046        e: &Engine,
4047        m: &MoeWeights,
4048        z: &CudaSlice<f32>,
4049        t: usize,
4050        il: u16,
4051    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4052        Self::moe_ffn_inner(e, m, z, None, t, &self.cfg, il, self.max_moe_block(), true)
4053    }
4054
4055    /// Decode-path twin with a PRE-QUANTIZED z (from add_rms_norm_zq8): threads (zq, zd) into the
4056    /// t=1 dev arm so the per-layer standalone quantize_q8_1 launch folds away. Identical bytes
4057    /// (the fused kernel reproduces quantize_q8_1 exactly); every other path ignores the pair.
4058    pub fn moe_ffn_il_zq8(
4059        &self,
4060        e: &Engine,
4061        m: &MoeWeights,
4062        z: &CudaSlice<f32>,
4063        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
4064        t: usize,
4065        il: u16,
4066    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4067        Self::moe_ffn_inner(e, m, z, zq8, t, &self.cfg, il, self.max_moe_block(), false)
4068    }
4069
4070    /// MoE FFN (EDGE-1), source-/model-agnostic. z: [T, n_embd] (already post-attention-normed).
4071    /// Returns moe_out [T, n_embd]. Node-for-node vs llama.cpp build_moe_ffn. Shared by the hybrid
4072    /// (qwen35moe, shared expert present) and the dense-attention MoE (OLMoE, no shared expert) paths;
4073    /// `cfg.moe` supplies the dims and the optional shexp fields decide whether step 3 runs.
4074    ///
4075    /// `il` is the layer index — the residency-cache key prefix. `max_block` is the global max expert
4076    /// stride (fixed cache-slot size); pass `self.max_moe_block()`.
4077    pub(crate) fn moe_ffn(
4078        e: &Engine,
4079        m: &MoeWeights,
4080        z: &CudaSlice<f32>,
4081        t: usize,
4082        cfg: &ModelConfig,
4083        il: u16,
4084        max_block: usize,
4085    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4086        Self::moe_ffn_inner(e, m, z, None, t, cfg, il, max_block, false)
4087    }
4088
4089    #[allow(clippy::too_many_arguments)]
4090    pub(crate) fn moe_ffn_inner(
4091        e: &Engine,
4092        m: &MoeWeights,
4093        z: &CudaSlice<f32>,
4094        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
4095        t: usize,
4096        cfg: &ModelConfig,
4097        il: u16,
4098        max_block: usize,
4099        prefill: bool,
4100    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4101        let worker_io = crate::spill_pread::worker_enabled();
4102        let epoch_lfu = std::env::var_os("MEMRA_MOE_LFU_DECAY").is_some();
4103        if Engine::moe_cache_enabled() && (worker_io || epoch_lfu) {
4104            e.with_moe_cache(max_block, |cache, _| {
4105                cache.begin_forward_epoch(il, t);
4106                if worker_io {
4107                    cache.begin_worker_scope();
4108                }
4109                Ok(())
4110            })?;
4111        }
4112        if Self::sigmoid_resident_dev_eligible(e, m, cfg) {
4113            let moe = cfg.moe.as_ref().unwrap();
4114            let n_expert = moe.expert_count as usize;
4115            let n_used = moe.expert_used_count as usize;
4116            let sigmoid = cfg.sigmoid_router().unwrap();
4117            let logits = Self::moe_router_logits(e, m, z, t, cfg)?;
4118            Self::trace_sigmoid_router_logits(e, il, t, n_expert, n_used, &logits, m, sigmoid)?;
4119            return Self::moe_ffn_sigmoid_dev(e, m, z, zq8, &logits, t, cfg, il, sigmoid);
4120        }
4121        // Expert-grouped dispatch for prefill (T>1). MEMRA_MOE_GROUPED explicitly opts the
4122        // current caller into this research arm; the naked default stays on the established path.
4123        if t > 1 && moe_grouped_enabled(cfg, prefill) {
4124            let grouped_out = Self::moe_ffn_grouped(e, m, z, t, cfg, il, max_block)?;
4125            // MEMRA_MOE_GATE: byte-identity comparison vs sequential path.
4126            // The grouped q8 path uses the same row-wise quantize/dot/activation/FMA programs as
4127            // sequential dispatch; mixed or q8-disabled layouts fall back to the matching f32
4128            // path. A mismatch is therefore a correctness failure, not an accepted numeric class.
4129            if std::env::var("MEMRA_MOE_GATE").is_ok() {
4130                let seq_out = Self::moe_ffn_sequential(e, m, z, t, cfg, il, max_block)?;
4131                let g_host = e.dtoh(&grouped_out)?;
4132                let s_host = e.dtoh(&seq_out)?;
4133                let g_bytes: &[u8] = unsafe {
4134                    std::slice::from_raw_parts(g_host.as_ptr() as *const u8, g_host.len() * 4)
4135                };
4136                let s_bytes: &[u8] = unsafe {
4137                    std::slice::from_raw_parts(s_host.as_ptr() as *const u8, s_host.len() * 4)
4138                };
4139                if g_bytes == s_bytes {
4140                    println!("moe-gate il={il} t={t} BYTE-IDENTICAL");
4141                } else {
4142                    let diffs = g_host
4143                        .iter()
4144                        .zip(s_host.iter())
4145                        .enumerate()
4146                        .filter(|(_, (a, b))| a != b)
4147                        .count();
4148                    let maxdiff = g_host
4149                        .iter()
4150                        .zip(s_host.iter())
4151                        .map(|(a, b)| (a - b).abs())
4152                        .fold(0.0f32, f32::max);
4153                    panic!(
4154                        "moe-gate il={il} t={t} MISMATCH: {diffs}/{} elems differ, maxdiff={maxdiff:.6e}",
4155                        g_host.len()
4156                    );
4157                }
4158            }
4159            return Ok(grouped_out);
4160        }
4161        Self::moe_ffn_sequential_zq8(e, m, z, zq8, t, cfg, il, max_block)
4162    }
4163
4164    fn sigmoid_resident_dev_eligible(e: &Engine, m: &MoeWeights, cfg: &ModelConfig) -> bool {
4165        let Some(moe) = cfg.moe.as_ref() else {
4166            return false;
4167        };
4168        // Cached once per process: this predicate runs per MoE layer per decode step, and five
4169        // uncached env reads were ~300+ environ scans/step on the exact path this arm de-hosts.
4170        static OBSERVATION_MODE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
4171        let observation_mode = *OBSERVATION_MODE.get_or_init(|| {
4172            std::env::var("MEMRA_MOE_STATS").is_ok()
4173                || std::env::var("MEMRA_MOE_TRACE").is_ok()
4174                || std::env::var("MEMRA_MOE_WEIGHT_TRACE").is_ok()
4175                || std::env::var("MEMRA_MOE_INPUT_TRACE_DIR").is_ok()
4176                || std::env::var("MEMRA_MOE_GATE").is_ok()
4177        });
4178        cfg.step35.is_some()
4179            && sigmoid_router_enabled()
4180            && moe_dev_enabled()
4181            && moe_slab_enabled()
4182            && !observation_mode
4183            && moe.expert_used_count <= 8
4184            && m.has_uniform_expert_layout()
4185            && m.gate_exps.macros.is_none()
4186            && m.up_exps.macros.is_none()
4187            && m.down_exps.macros.is_none()
4188            && !m.has_macros
4189            && moe_q8_enabled()
4190            && q8_expert_supported(m.gate_exps.qtype)
4191            && q8_expert_supported(m.up_exps.qtype)
4192            && q8_expert_supported(m.down_exps.qtype)
4193            && m.dev_exps
4194                .as_ref()
4195                .is_some_and(|dev| dev.dev == e.ctx().ordinal())
4196    }
4197
4198    /// Sequential (per-token) MoE FFN -- the original path. Factored out for the gate comparison.
4199    pub(crate) fn moe_ffn_sequential(
4200        e: &Engine,
4201        m: &MoeWeights,
4202        z: &CudaSlice<f32>,
4203        t: usize,
4204        cfg: &ModelConfig,
4205        il: u16,
4206        max_block: usize,
4207    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4208        Self::moe_ffn_sequential_zq8(e, m, z, None, t, cfg, il, max_block)
4209    }
4210
4211    /// One router-logit selector shared by sequential and grouped dispatch. Real prefill must use
4212    /// the row-wise GEMV when exact routing is enabled: cuBLASLt's reduction changes with `m`,
4213    /// which makes expert selection depend on the caller's chunk or concat-batch shape.
4214    fn moe_router_logits(
4215        e: &Engine,
4216        m: &MoeWeights,
4217        z: &CudaSlice<f32>,
4218        t: usize,
4219        cfg: &ModelConfig,
4220    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4221        if t < PRIME_MIN_T {
4222            // Decode and speculative verify use one fixed per-row reduction program.
4223            if crate::router_kernel_on() {
4224                e.router_gemv(
4225                    m.gate_inp.float_data(),
4226                    z,
4227                    cfg.n_embd as usize,
4228                    m.gate_exps.n_expert,
4229                    t,
4230                )
4231            } else {
4232                e.matmul_decode_exact(&m.gate_inp, z, t)
4233            }
4234        } else if crate::router_prefill_exact_on() && crate::router_kernel_on() {
4235            e.router_gemv(
4236                m.gate_inp.float_data(),
4237                z,
4238                cfg.n_embd as usize,
4239                m.gate_exps.n_expert,
4240                t,
4241            )
4242        } else {
4243            e.matmul(&m.gate_inp, z, t)
4244        }
4245    }
4246
4247    /// Append the host-visible router selection for one layer/forward when calibration tracing is
4248    /// enabled. Both sequential and expert-grouped prefill must call this after routing so the
4249    /// trace is independent of the dispatch optimization selected for the forward.
4250    fn trace_moe_routes(
4251        il: u16,
4252        t: usize,
4253        sel_all: &[u32],
4254        weights: &[f32],
4255    ) -> Result<(), Box<dyn std::error::Error>> {
4256        use std::io::Write as _;
4257        if let Ok(path) = std::env::var("MEMRA_MOE_TRACE") {
4258            let mut f = std::fs::OpenOptions::new()
4259                .create(true)
4260                .append(true)
4261                .open(path)?;
4262            let ids: Vec<String> = sel_all.iter().map(|s| s.to_string()).collect();
4263            writeln!(f, "{} {} {}", il, t, ids.join(","))?;
4264        }
4265        if let Ok(path) = std::env::var("MEMRA_MOE_WEIGHT_TRACE") {
4266            let mut f = std::fs::OpenOptions::new()
4267                .create(true)
4268                .append(true)
4269                .open(path)?;
4270            let pairs: Vec<String> = sel_all
4271                .iter()
4272                .zip(weights)
4273                .map(|(expert, weight)| format!("{expert}:{weight:.9}"))
4274                .collect();
4275            writeln!(f, "{} {} {}", il, t, pairs.join(","))?;
4276        }
4277        Ok(())
4278    }
4279
4280    #[allow(clippy::too_many_arguments)]
4281    fn trace_sigmoid_router_logits(
4282        e: &Engine,
4283        il: u16,
4284        t: usize,
4285        n_expert: usize,
4286        n_used: usize,
4287        logits: &CudaSlice<f32>,
4288        m: &MoeWeights,
4289        (scaling_factor, route_norm): (f32, bool),
4290    ) -> Result<(), Box<dyn std::error::Error>> {
4291        if !crate::sigrouter_contract::served_logit_trace_enabled() || t != 1 {
4292            return Ok(());
4293        }
4294        let logits = e.dtoh(logits)?;
4295        let active: Vec<u8> = m
4296            .active_experts
4297            .as_ref()
4298            .map(|mask| mask.iter().map(|&enabled| u8::from(enabled)).collect())
4299            .unwrap_or_else(|| vec![1; n_expert]);
4300        let bias = m.exp_probs_b.clone().unwrap_or_else(|| vec![0.0; n_expert]);
4301        crate::sigrouter_contract::capture_served_logits(
4302            il as u32,
4303            t,
4304            n_expert,
4305            n_used,
4306            scaling_factor,
4307            route_norm,
4308            &active,
4309            &bias,
4310            &logits,
4311        )?;
4312        Ok(())
4313    }
4314
4315    /// Append the f32 input to one MoE layer for offline, layerwise calibration. This diagnostic
4316    /// intentionally performs a DtoH copy and is therefore disabled unless an explicit fresh trace
4317    /// directory is supplied. Each layer owns one payload file; index.jsonl records byte offsets so
4318    /// a validator can prove request/layer coverage before the trace is used for pruning or healing.
4319    fn trace_moe_input(
4320        e: &Engine,
4321        il: u16,
4322        t: usize,
4323        n_embd: usize,
4324        z: &CudaSlice<f32>,
4325    ) -> Result<(), Box<dyn std::error::Error>> {
4326        use std::io::Write as _;
4327        let Ok(dir) = std::env::var("MEMRA_MOE_INPUT_TRACE_DIR") else {
4328            return Ok(());
4329        };
4330        let host = e.dtoh(z)?;
4331        if host.len() != t * n_embd {
4332            return Err(format!(
4333                "MoE input trace shape mismatch at layer {il}: got {} values, expected {}x{}",
4334                host.len(),
4335                t,
4336                n_embd
4337            )
4338            .into());
4339        }
4340        let bytes = unsafe {
4341            std::slice::from_raw_parts(
4342                host.as_ptr().cast::<u8>(),
4343                host.len() * std::mem::size_of::<f32>(),
4344            )
4345        };
4346        let state = MOE_INPUT_TRACE_WRITER.get_or_init(|| std::sync::Mutex::new(None));
4347        let mut state = state
4348            .lock()
4349            .map_err(|_| "MoE input trace writer lock is poisoned")?;
4350        if state.is_none() {
4351            let dir = std::path::PathBuf::from(&dir);
4352            std::fs::create_dir_all(&dir)?;
4353            let index = std::fs::OpenOptions::new()
4354                .create(true)
4355                .append(true)
4356                .open(dir.join("index.jsonl"))?;
4357            *state = Some(MoeInputTraceWriter {
4358                dir,
4359                index,
4360                payloads: std::collections::HashMap::new(),
4361            });
4362        }
4363        let writer = state.as_mut().unwrap();
4364        if writer.dir != std::path::Path::new(&dir) {
4365            return Err("MEMRA_MOE_INPUT_TRACE_DIR changed after capture started".into());
4366        }
4367        let file_name = format!("layer-{il:03}.f32");
4368        if !writer.payloads.contains_key(&il) {
4369            let payload = std::fs::OpenOptions::new()
4370                .create(true)
4371                .append(true)
4372                .open(writer.dir.join(&file_name))?;
4373            let offset = payload.metadata()?.len();
4374            writer.payloads.insert(il, (payload, offset));
4375        }
4376        let (payload, offset) = writer.payloads.get_mut(&il).unwrap();
4377        let row_offset = *offset;
4378        payload.write_all(bytes)?;
4379        *offset += bytes.len() as u64;
4380        writeln!(
4381            writer.index,
4382            "{{\"format\":\"memra-moe-input-trace-v1\",\"layer\":{il},\"tokens\":{t},\
4383             \"hidden_size\":{n_embd},\"file\":\"{file_name}\",\"offset\":{row_offset},\
4384             \"payload_bytes\":{}}}",
4385            bytes.len()
4386        )?;
4387        Ok(())
4388    }
4389
4390    #[allow(clippy::too_many_arguments)]
4391    pub(crate) fn moe_ffn_sequential_zq8(
4392        e: &Engine,
4393        m: &MoeWeights,
4394        z: &CudaSlice<f32>,
4395        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
4396        t: usize,
4397        cfg: &ModelConfig,
4398        il: u16,
4399        max_block: usize,
4400    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
4401        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
4402        let moe = cfg.moe.as_ref().unwrap();
4403        let n_embd = cfg.n_embd as usize; // 2048 (gate/up in_f, down out_f)
4404        let n_expert = moe.expert_count as usize; // 256
4405        let n_used = moe.expert_used_count as usize; // 8
4406        let n_ff_exp = moe.expert_ff_length as usize; // 512 (gate/up out_f, down in_f)
4407
4408        // verify the HostExps dims match cfg (catches a wrong-file / transpose mixup)
4409        debug_assert_eq!(m.gate_exps.in_f, n_embd);
4410        debug_assert_eq!(m.gate_exps.out_f, n_ff_exp);
4411        debug_assert_eq!(m.down_exps.in_f, n_ff_exp); // down is TRANSPOSED: in=512
4412        debug_assert_eq!(m.down_exps.out_f, n_embd); //                     out=2048
4413        debug_assert_eq!(m.gate_exps.n_expert, n_expert);
4414
4415        // step35 PER-LAYER SwiGLU clamp (None on every other arch and on every unclamped layer).
4416        // Routed experts and the shared expert read SEPARATE arrays — never share one value.
4417        let lim_exp = cfg.clamp_exp_at(il as u32);
4418        let lim_shexp = cfg.clamp_shexp_at(il as u32);
4419        let use_cache = Engine::moe_cache_enabled();
4420        let uniform_experts = m.has_uniform_expert_layout();
4421        let moe_q8 = uniform_experts
4422            && moe_q8_enabled()
4423            && q8_expert_supported(m.gate_exps.qtype)
4424            && q8_expert_supported(m.up_exps.qtype)
4425            && q8_expert_supported(m.down_exps.qtype);
4426        // Experimental secondary backend: complete experts already resident in the SLRU stay on
4427        // CUDA; any expert missing one or more projections runs from the original host GGUF bytes
4428        // through llama.cpp CPU quant dots. Small-t only keeps decode and speculative verification
4429        // in the same numeric/dispatch class; real prefill remains on the established GPU path and
4430        // seeds the residency cache. An explicit library path is the build/runtime gate, so naked
4431        // commands and CI have no llama.cpp or OpenMP dependency.
4432        let cpu_expert_requested = crate::cpu_experts::configured();
4433        if cpu_expert_requested && (cfg.hy3.is_none() || cfg.m3.is_some()) {
4434            return Err(std::io::Error::other(
4435                "MEMRA_CPU_EXPERT_LIB is experimental and currently gated to Hy3",
4436            )
4437            .into());
4438        }
4439        let cpu_hybrid = cpu_expert_requested && t < PRIME_MIN_T && m.dev_exps.is_none();
4440        // A dynamic SLRU changes which complete experts run on CUDA versus llama.cpp CPU dots.
4441        // Those backends are each deterministic but are different numeric configurations, so a
4442        // later prefill eviction can change greedy output. Freeze after the first real prefill;
4443        // decode/spec reads the fixed resident set, while later prefill misses use transient GPU
4444        // staging below and cannot change backend assignment.
4445        let freeze_cpu_residency = cpu_expert_requested
4446            && std::env::var("MEMRA_CPU_EXPERT_FREEZE_CACHE").as_deref() == Ok("1");
4447        let caller_warms_before_freeze = std::env::var("MEMRA_CPU_EXPERT_FREEZE_WARMUP_TOKENS")
4448            .ok()
4449            .and_then(|value| value.parse::<usize>().ok())
4450            .is_some_and(|tokens| tokens > 0);
4451        if cpu_hybrid && freeze_cpu_residency && !caller_warms_before_freeze {
4452            e.freeze_moe_cache();
4453        }
4454        let cache_frozen = use_cache && e.moe_cache_frozen();
4455        let cache_dispatch = use_cache && (!cache_frozen || cpu_hybrid);
4456
4457        // 1. ROUTER: one selector is shared with expert-grouped prefill so changing dispatch
4458        // cannot change logits, selected expert ids, or routing weights.
4459        let logits = Self::moe_router_logits(e, m, z, t, cfg)?;
4460        if let Some(sig) = cfg.sigmoid_router() {
4461            Self::trace_sigmoid_router_logits(e, il, t, n_expert, n_used, &logits, m, sig)?;
4462        }
4463
4464        // LAUNCH-STRUCTURE STAGE 3 (2026-07-05, MEMRA_MOE_DEV default ON, =0 rollback): ZERO-DtoH
4465        // device-dispatch when this layer's expert blocks are ALL cache-resident. The fused
4466        // router's sel/w stay ON DEVICE; the expert weight pointers come from the per-layer
4467        // device table of fixed slot addresses; gate/up/silu + down/fma run as the same TWO
4468        // launches per token as gdec. Removes the per-layer router DtoH + stream sync — the
4469        // per-token host stall that dominated the 35B decode wall after stages 1+2.
4470        // BIT-IDENTITY: the router kernel is selection-exact vs the host oracle (kernel-check
4471        // tie gate) and the _dev matvec twins reproduce the gdec kernels' exact FP chains; the
4472        // only difference is where sel/w/pointers are READ from (device instead of params).
4473        // Residency: one-shot PREWARM force-admits the layer while free slots cover it
4474        // (MEMRA_MOE_PREWARM=0 -> organic residency, dev path fires when the SLRU fills).
4475        // Any non-resident layer falls through to host routing + the gdec/sequential path.
4476        // FITS-VRAM RESIDENT EXPERTS (2026-07-06): the layer's expert slabs are device-resident
4477        // (load-time decision) — fire the zero-DtoH dev path unconditionally with the prebuilt
4478        // pointer row. No cache, no dispatch, no residency check: the llama full-offload regime
4479        // (it measured 169.55 vs the cache path's 28.5 on the local 35B — the residency-gate
4480        // all-or-nothing fallback was the 6x). BIT-IDENTITY: same _dev kernels, same math; only
4481        // the pointer table's provenance differs (slab base+stride vs SLRU slot addresses).
4482        // MoE PREFILL PAIR-BATCH (2026-07-06, the 16x pp hole): t>1 on resident experts — ONE
4483        // launch per proj covers ALL (token,expert) pairs (grid.y=pair, warp-per-row), replacing
4484        // the per-expert loop (256 experts x 3-4 launches x tiny m_e). Scatter is slot-ordered
4485        // per token (the sequential-axpy bit-identity class). Requires q8-supported qtypes +
4486        // resident slabs. MEMRA_MOE_PAIRS=0 rollback.
4487        // t >= PRIME_MIN_T only (2026-07-06 exactness fix, verify-probe proof): spec VERIFY
4488        // batches (t = 2..K+2) previously rode these pairs kernels while T=1 decode rode the
4489        // dev_q8 loop — different FP chains, verify-T2 logit maxdiff 2.6e-1 vs eager -> greedy
4490        // flips at tight margins -> 35B real-prompt spec self-consistency FAIL (the 27B
4491        // "verify must be kernel-DISPATCH-identical to decode" lesson, MoE edition). Small-t
4492        // now rides the dev loop below (same kernels per token as decode); pairs serves real
4493        // prefill (t >= 16, where spec never verifies).
4494        // sigmoid-router archs must NOT enter the pairs/dev arms: those dispatch arms still route
4495        // via the fused SOFTMAX device router (moe_router_topk) — silently wrong experts (the
4496        // M3 gate-MISMATCH 74602-vs-92 lesson, 2026-07-07). Sigmoid routing has its own device
4497        // top-k kernel; MEMRA_SIG_ROUTER=0 selects the host sigmoid oracle instead.
4498        // Per-expert macro-scales (compressed-tensors NVFP4 ST class, e.g. unsloth 35B-A3B):
4499        // the device-dispatch kernels (pairs/dev) do NOT fold them — those checkpoints must
4500        // ride the macro-aware sequential/staged paths below or every expert output is off by
4501        // its global scale (~3e4x, measured garbage 2026-07-16). GGUF experts: macros None.
4502        let no_exp_macros = m.gate_exps.macros.is_none()
4503            && m.up_exps.macros.is_none()
4504            && m.down_exps.macros.is_none();
4505        // A clamped-SwiGLU layer (step35 43/44) also cannot ride pairs: moe_pairs_silu_mul's
4506        // epilogue is plain silu(gate)*up with no clamp form, and moe_ffn_pairs takes no `il`
4507        // so it cannot even see the per-layer limit.
4508        if cfg.sigmoid_router().is_none()
4509            && cfg.m3.is_none()
4510            && cfg.hy3.is_none()
4511            && !cfg.swiglu_clamped_at(il as u32)
4512            && no_exp_macros
4513            && t >= PRIME_MIN_T
4514            && m.dev_exps.is_some()
4515            && moe_q8_enabled()
4516            && q8_expert_supported(m.gate_exps.qtype)
4517            && q8_expert_supported(m.up_exps.qtype)
4518            && q8_expert_supported(m.down_exps.qtype)
4519            && std::env::var("MEMRA_MOE_PAIRS")
4520                .map(|v| v != "0")
4521                .unwrap_or(true)
4522            && std::env::var("MEMRA_MOE_STATS").is_err()
4523        {
4524            return Self::moe_ffn_pairs(e, m, z, &logits, t, cfg);
4525        }
4526
4527        // t < PRIME_MIN_T: moe_ffn_dev loops tokens serially (1 launch-pair per token) — the
4528        // decode path AND the spec-verify path (dispatch parity = exactness; see pairs gate
4529        // above). Serial launches are fine at t<=10 (K+2); real prefill never lands here.
4530        // moe_ffn_dev routes via the FUSED SOFTMAX device router (moe_router_topk). Sigmoid
4531        // routing has a separate device top-k kernel, but this dev arm cannot consume its sel/w
4532        // contract, so sigmoid-router arches must NOT enter it: with MOE_CACHE=1 M3 silently
4533        // routed softmax = wrong experts (gate MISMATCH 74602 vs 92, caught 2026-07-07).
4534        // MEMRA_SIG_ROUTER=0 selects the host sigmoid oracle for the supported sigmoid path.
4535        // macro-carrying experts are handled inside the dev path now (epilogue fold + w-scale);
4536        // pairs/gdec/csr keep their macro gates until their kernels grow the fold.
4537        // step35 (2026-08-06) is the third sigmoid-router arch and the deny above was written as
4538        // two arch names, not as the mechanism — so `dev_ok` let it into moe_ffn_dev's
4539        // moe_router_topk (softmax, no exp_probs_b bias, no expert_weights_scale) = silently
4540        // wrong experts, the same failure the pairs gate at :2254 already blocks by predicate.
4541        // Keyed off sigmoid_router() so arch #4 is denied by construction.
4542        // The clamped-SwiGLU layers must also fall through: every moe_ffn_dev kernel's fused
4543        // epilogue is plain silu(gate)*up (moe_gate_up_silu8_dev_*), which has no clamp form.
4544        let dev_ok = uniform_experts
4545            && cfg.sigmoid_router().is_none()
4546            && cfg.m3.is_none()
4547            && cfg.hy3.is_none()
4548            && !cfg.swiglu_clamped_at(il as u32);
4549        // Observation modes must route through the host-visible selection below. Otherwise a fully
4550        // resident layer returns through device dispatch before its trace/stats row is recorded,
4551        // silently biasing calibration toward only non-resident layers on large-VRAM machines.
4552        let observe_routes = std::env::var("MEMRA_MOE_STATS").is_ok()
4553            || std::env::var("MEMRA_MOE_TRACE").is_ok()
4554            || std::env::var("MEMRA_MOE_WEIGHT_TRACE").is_ok()
4555            || std::env::var("MEMRA_MOE_INPUT_TRACE_DIR").is_ok();
4556        if dev_ok
4557            && t < PRIME_MIN_T
4558            && m.dev_exps.is_some()
4559            && n_used <= 8
4560            && moe_dev_enabled()
4561            && !observe_routes
4562        {
4563            return Self::moe_ffn_dev(e, m, z, zq8, &logits, t, cfg, il, max_block);
4564        }
4565        if dev_ok && use_cache && n_used <= 8 && moe_dev_enabled() && !observe_routes {
4566            let row_ok = e.with_moe_cache(max_block, |c, eng| {
4567                if moe_prewarm_enabled() {
4568                    c.prewarm_layer(il, m, eng)?;
4569                }
4570                Ok(c.layer_dev_row(il, n_expert, eng)?.is_some())
4571            })?;
4572            if row_ok {
4573                return Self::moe_ffn_dev(e, m, z, zq8, &logits, t, cfg, il, max_block);
4574            }
4575        }
4576
4577        // Per-token (sel[8], w[8]) — sigmoid host oracle (M3/Hy3), else fused-router/softmax.
4578        let (sel_all, w_all, routed_cpu_input) = if let Some(sig) = cfg.sigmoid_router() {
4579            if cpu_hybrid {
4580                let (sel, w, input) = Self::moe_route_sigmoid_with_input(
4581                    e,
4582                    &logits,
4583                    z,
4584                    t,
4585                    n_expert,
4586                    n_used,
4587                    m.exp_probs_b.as_deref(),
4588                    sig,
4589                    m.active_experts.as_deref(),
4590                )?;
4591                (sel, w, Some(input))
4592            } else {
4593                let (sel, w) =
4594                    Self::moe_route_sigmoid_cfg(e, &logits, t, n_expert, n_used, m, sig)?;
4595                (sel, w, None)
4596            }
4597        } else {
4598            let (sel, w) =
4599                Self::moe_route_cfg(e, &logits, t, n_expert, n_used, m.active_experts.as_deref())?;
4600            (sel, w, None)
4601        };
4602        crate::moesd::record_host_routes(il, n_expert, n_used, &sel_all)?;
4603
4604        // MEMRA_MOE_TRACE=<path>: append one line per (layer, step) with the selected expert ids —
4605        // offline analysis derives the decode working set + step-to-step reuse (the go/no-go
4606        // measurement for resident-expert tiering; see rig5090.jsonl 2026-07-07 pinned-tier row).
4607        Self::trace_moe_routes(il, t, &sel_all, &w_all)?;
4608        Self::trace_moe_input(e, il, t, n_embd, z)?;
4609
4610        // Worker reads ordinarily overlap NVMe with the current expert but leave their H2D copies
4611        // demand-serialized on the compute stream. Hy3 decode already has a safe owner-thread
4612        // boundary here: sigmoid routing read `logits` back to the host, which synchronized all
4613        // earlier-layer compute. Submit the complete selected set first, then reserve only victims
4614        // outside that set and queue its H2D copies on the copy stream. Dispatch inserts an event
4615        // wait for each pending block, so later copies can overlap the earlier expert kernels while
4616        // the selected weights and numerical kernels remain unchanged. Restrict this experiment to
4617        // T=1; batched forwards can have token-local consumers still in flight between selections.
4618        // The CPU backend reads mmap-backed model bytes directly. Positioned-read worker buffers
4619        // are CUDA write-combined staging allocations and are intentionally not CPU-compute inputs;
4620        // do not spend NVMe bandwidth filling them for a layer whose misses go to CPU.
4621        let worker_disk_prefetch =
4622            cache_dispatch && crate::spill_pread::worker_enabled() && !cpu_hybrid;
4623        let promote_worker_h2d =
4624            t == 1 && worker_disk_prefetch && crate::spill_pread::copy_h2d_enabled();
4625        if promote_worker_h2d {
4626            let mut selected_blocks = Vec::with_capacity(n_used * 3);
4627            for &ex in sel_all.iter().take(n_used) {
4628                let ex = ex as u16;
4629                selected_blocks.extend([
4630                    BlockId::new(il, PROJ_GATE, ex),
4631                    BlockId::new(il, PROJ_UP, ex),
4632                    BlockId::new(il, PROJ_DOWN, ex),
4633                ]);
4634            }
4635            for &ex in sel_all.iter().take(n_used) {
4636                Self::moe_prefetch_disk_expert(e, il, ex as usize, m, max_block, &selected_blocks)?;
4637            }
4638            e.with_moe_cache(max_block, |cache, eng| {
4639                cache.promote_worker_reads_at_safe_boundary(
4640                    &selected_blocks,
4641                    &selected_blocks,
4642                    eng,
4643                )?;
4644                Ok(())
4645            })?;
4646        }
4647
4648        // MEMRA_MOE_STATS: per-layer routing stats for the A2 (expert-grouped prefill) baseline —
4649        // per-token expert-id entropy, active-expert coverage, tokens-per-expert group sizes.
4650        if t > 1 && std::env::var("MEMRA_MOE_STATS").is_ok() {
4651            let mut cnt = vec![0u32; n_expert];
4652            for &s in sel_all.iter() {
4653                cnt[s as usize] += 1;
4654            }
4655            let total = sel_all.len() as f64;
4656            let mut h = 0.0f64;
4657            let mut active = 0usize;
4658            for &c in &cnt {
4659                if c > 0 {
4660                    active += 1;
4661                    let p = c as f64 / total;
4662                    h -= p * p.log2();
4663                }
4664            }
4665            let maxc = cnt.iter().copied().max().unwrap_or(0);
4666            println!(
4667                "moe-stats il={} t={} assignments={} active={}/{} entropy={:.3}b (max {:.3}b) mean_tok_per_active={:.2} max_tok_per_expert={}",
4668                il,
4669                t,
4670                sel_all.len(),
4671                active,
4672                n_expert,
4673                h,
4674                (n_expert as f64).log2(),
4675                total / active.max(1) as f64,
4676                maxc
4677            );
4678        }
4679
4680        // LAUNCH-STRUCTURE STAGE 2 (2026-07-05): moe_out memset elision on the gdec path.
4681        // moe_down8_fma_f32 FULLY overwrites its token row (dst[o] = the in-kernel FMA chain that
4682        // starts at 0.0f — numerically the axpy-into-zeroed-row chain), so when the grouped-decode
4683        // path fires the upfront `e.zeros(t*n_embd)` memset is pure launch churn. Allocate uninit
4684        // when gdec CAN fire (any t — decode t=1 AND the spec verify t=K+1 route here per token)
4685        // and lazily zero ONLY the row of a token that falls through to the sequential axpy loop.
4686        // BIT-IDENTITY: unchanged — every row is either fully overwritten (gdec) or
4687        // zeroed-then-accumulated exactly as before (fallback).
4688        // `!swiglu_clamped_at`: the grouped-decode kernels' fused epilogue is plain
4689        // silu(gate)*up (see the m3 note at the call sites below) — a clamped layer must fall
4690        // through to the sequential loop's ffn_act_lim. Hoisted into the fire predicate so the
4691        // uninit/memset invariant at :2418 stays consistent with what actually dispatches.
4692        let gdec_may_fire = uniform_experts
4693            && use_cache
4694            && n_used <= 8
4695            && gdec_enabled()
4696            && !cfg.swiglu_clamped_at(il as u32);
4697        // SLAB-LOCAL RESIDENT ARM (lane/pp-leverb 2026-08-08): the fits-VRAM resident slabs
4698        // (`dev_exps`, sized per PP device by cx-503b) were consumed ONLY by the pairs/dev
4699        // arms — every one of which DENIES sigmoid-router archs (step35/M3/Hy3). So on those
4700        // archs the slabs were uploaded but never read, and every expert went through the
4701        // SLRU (37 GB H2D staging per pp4096 prime on the Step SKU; and post-cx-503b the
4702        // dead slabs additionally STARVE the SLRU, which sizes itself on free-VRAM-after-
4703        // residents). This arm reads the slabs through the SAME kernels the SLRU arm runs —
4704        // gdec's fused pair when eligible, the per-expert qmatvec twins otherwise — with the
4705        // slab base + ex*stride as the pointer provenance (the exact bit-identity class
4706        // `moe_ffn_dev` documents for its resident-vs-SLRU arms; bytes are the same HostExps
4707        // bytes either way). LOCALITY GATE: `d.dev == e.ctx().ordinal()` — a slab on another
4708        // device must NOT be dereferenced (m=1 peer reads are the measured 34-150x class,
4709        // strictly worse than staging); under PP-2 without the prime walker this admits
4710        // stage-0 layers on dev0 and leaves stage-1 layers on the SLRU, which is the honest
4711        // interim shape. MEMRA_MOE_SLAB=0 = rollback/A-B seam (read per call).
4712        let slab_local = m
4713            .dev_exps
4714            .as_ref()
4715            .filter(|d| !d.gu_il && moe_slab_enabled() && d.dev == e.ctx().ordinal());
4716        let slab_bases = slab_local.map(|d| {
4717            use cudarc::driver::DevicePtr;
4718            let s = e.stream();
4719            let (pg, _g0) = d.gate.device_ptr(&s);
4720            let (pu, _g1) = d.up.device_ptr(&s);
4721            let (pd, _g2) = d.down.device_ptr(&s);
4722            (pg as u64, pu as u64, pd as u64)
4723        });
4724        // Fused-pair eligibility mirrors the gdec call sites exactly (plain-SiLU epilogue,
4725        // no macros, m3 clamp excluded, q8-supported qtypes, <=8 experts) — INCLUDING
4726        // `gdec_enabled()`: the pair IS the gdec kernel pair, so MEMRA_MOE_GDEC=0 must
4727        // disable it here too. That seam is also the exactness localizer: with GDEC=0 both
4728        // provenances run the SAME per-expert qmatvec kernels (slab base+stride vs SLRU
4729        // slot) and must be BIT-IDENTICAL — the true provenance-only pair — while GDEC=1
4730        // compares the fused-pair class against the SLRU's hit/miss MIX (gdec for
4731        // all-resident tokens, staged loop for misses), which is a dispatch-class
4732        // comparison, not a provenance one.
4733        let slab_fused_may_fire = slab_bases.is_some()
4734            && n_used <= 8
4735            && gdec_enabled()
4736            && !cfg.swiglu_clamped_at(il as u32)
4737            && cfg.m3.is_none()
4738            && no_exp_macros
4739            && moe_q8;
4740        // moe_out memset elision: BOTH full-row-overwrite arms (gdec + slab fused) allocate
4741        // uninit; a token that falls through to any accumulating loop zeroes its own row.
4742        let mut moe_out = if gdec_may_fire || slab_fused_may_fire {
4743            e.uninit(t * n_embd)?
4744        } else {
4745            e.zeros(t * n_embd)?
4746        };
4747        // The router readback above already established a host boundary. Copy each small-t hidden
4748        // row once so CPU miss experts can start while the owner thread queues resident GPU work.
4749        let cpu_input = if cpu_hybrid {
4750            Some(routed_cpu_input.ok_or("CPU expert routing did not return the MoE input")?)
4751        } else {
4752            None
4753        };
4754
4755        // GPU scratch: one slot per proj, big enough for ONE expert (default stage-every-token path).
4756        // STAGE 2: LAZY — allocated only if the no-cache staging path actually runs (under
4757        // MEMRA_MOE_CACHE they were 3 dead ~1MB alloc_zeros + memset + free per layer per token,
4758        // measured ~123 memsets/token of the decode wall).
4759        let g_len = m.gate_exps.max_expert_bytes(); // 860160 for the uniform 35B gate
4760        let u_len = m.up_exps.max_expert_bytes(); // 860160 for the uniform 35B up
4761        let d_len = m.down_exps.max_expert_bytes(); // 1114112 for the uniform 35B down
4762        let mut scratch_g: Option<CudaSlice<u8>> = None;
4763        let mut scratch_u: Option<CudaSlice<u8>> = None;
4764        let mut scratch_d: Option<CudaSlice<u8>> = None;
4765        // `max_block` (the GLOBAL max expert stride across all layers) is passed in — the cache slots
4766        // are FIXED-ADDRESS and must fit any layer's block (UD/dynamic GGUFs vary quant per layer).
4767
4768        // EDGE-1 §C.2/C.3: the optional pipeline queues the next selected expert's MISS blocks on
4769        // the copy stream before launching the current expert's compute. Pending slots stay invisible
4770        // to cache hits until dispatch inserts the completion-event wait; current gate/up/down ids are
4771        // protected from eviction. This changes scheduling only, never the GGUF bytes or GEMM path.
4772        let page_window = moe_page_prefetch_window();
4773
4774        // 2. PER TOKEN: routed-expert loop. The ONE dispatch change vs Stage-1: a resident slot
4775        //    (cache HIT, no H2D) OR a staged slot (MISS) feeds the SAME unchanged qmatvec_view.
4776        for tok in 0..t {
4777            let sel = &sel_all[tok * n_used..(tok + 1) * n_used];
4778            let w = &w_all[tok * n_used..(tok + 1) * n_used];
4779            let zt = z.slice(tok * n_embd..(tok + 1) * n_embd); // CudaView<f32>
4780            let mut tok_q8: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
4781
4782            // STAGE-2 GROUPED DECODE (2026-07-04, MEMRA_MOE_GDEC default ON, =0 rollback): fold
4783            // this token's whole routed-expert FFN (8x gate/up/silu + 8x down/axpy = 40 launches)
4784            // into TWO launches via expert-pointer indirection over the fixed-address cache slots.
4785            // Fires only when ALL 3*n_used blocks are ALREADY cache-resident (pure-HIT: zero
4786            // memcpy, zero admission, so no slot can move under the collected pointers) — any
4787            // miss falls through to the sequential loop below, which admits as before. In steady
4788            // state on a fully-resident rig every token-layer takes the grouped path.
4789            // BIT-IDENTITY: each in-kernel dot reproduces qmatvec_f32's exact reduction; SiLU is
4790            // silu_mul_f32's exact expression; the down accumulation is a slot-ordered
4791            // __fmaf_rn chain == the sequential axpy_f32 chain (MEMRA_MOE_GDEC_GATE compares).
4792            // cfg.m3: the grouped kernels' fused epilogues are plain SiLU — M3's swigluoai must
4793            // NOT take them until the kernels grow the clamped variant. NVFP4 experts carry
4794            // per-expert macro-scales the fused kernels don't fold — those fall through too.
4795            let no_macros = m.gate_exps.macros.is_none()
4796                && m.up_exps.macros.is_none()
4797                && m.down_exps.macros.is_none();
4798            // SLAB-LOCAL FUSED PAIR (lane/pp-leverb 2026-08-08): the gdec launch pair
4799            // (moe_gate_up_silu8_q8 + moe_down8_fma_q8 — the EXACT kernels, same FP chains)
4800            // with pointers computed from the resident slab base + ex*stride instead of
4801            // collected SLRU slot addresses. No cache lock, no residency predicate — the
4802            // slab holds every expert by construction, so this arm never falls through
4803            // (gdec's P(all 24 resident) ≈ 0.37 coin-flip and the miss path's ~49-launch
4804            // staging both die). Bit-identity class: pointer provenance only, the same
4805            // slab-vs-SLRU equivalence moe_ffn_dev documents. Ordered BEFORE gdec: when a
4806            // slab exists it is strictly better (no lock, no miss).
4807            if slab_fused_may_fire {
4808                let (pg, pu, pd) = slab_bases.unwrap();
4809                let mut gp = [0u64; 8];
4810                let mut up = [0u64; 8];
4811                let mut dp = [0u64; 8];
4812                for (j, &ex) in sel.iter().enumerate() {
4813                    let ex = ex as usize;
4814                    gp[j] = pg + (ex * m.gate_exps.expert_stride) as u64;
4815                    up[j] = pu + (ex * m.up_exps.expert_stride) as u64;
4816                    dp[j] = pd + (ex * m.down_exps.expert_stride) as u64;
4817                }
4818                let mut wv = [0f32; 8];
4819                wv[..n_used].copy_from_slice(w);
4820                if tok_q8.is_none() {
4821                    tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
4822                }
4823                let (zq, zd) = tok_q8.as_ref().unwrap();
4824                let act = e.moe_gate_up_silu8_q8(
4825                    crate::WPtr8(gp),
4826                    crate::WPtr8(up),
4827                    zq,
4828                    zd,
4829                    n_embd,
4830                    n_ff_exp,
4831                    n_used,
4832                    m.gate_exps.qtype,
4833                    m.up_exps.qtype,
4834                    m.gate_exps.row_bytes,
4835                    m.up_exps.row_bytes,
4836                )?;
4837                let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
4838                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
4839                e.moe_down8_fma_q8(
4840                    crate::WPtr8(dp),
4841                    crate::F32x8(wv),
4842                    &aq2,
4843                    &ad2,
4844                    &mut dst,
4845                    n_ff_exp,
4846                    n_embd,
4847                    n_used,
4848                    m.down_exps.qtype,
4849                    m.down_exps.row_bytes,
4850                )?;
4851                continue;
4852            }
4853            if gdec_may_fire && moe_q8 && cfg.m3.is_none() && no_macros {
4854                if tok_q8.is_none() {
4855                    tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
4856                }
4857                let (zq, zd) = tok_q8.as_ref().unwrap();
4858                if Self::moe_gdec_token_q8(
4859                    e,
4860                    m,
4861                    il,
4862                    max_block,
4863                    zq,
4864                    zd,
4865                    sel,
4866                    w,
4867                    &mut moe_out,
4868                    tok,
4869                    n_embd,
4870                    n_ff_exp,
4871                    n_used,
4872                )? {
4873                    continue;
4874                }
4875            } else if gdec_may_fire
4876                && cfg.m3.is_none()
4877                && no_macros
4878                && Self::moe_gdec_token(
4879                    e,
4880                    m,
4881                    il,
4882                    max_block,
4883                    &zt,
4884                    sel,
4885                    w,
4886                    &mut moe_out,
4887                    tok,
4888                    n_embd,
4889                    n_ff_exp,
4890                    n_used,
4891                )?
4892            {
4893                continue;
4894            }
4895
4896            // STAGE 2 memset-elision invariant: moe_out was allocated UNINIT when gdec or the
4897            // slab pair could fire. This token fell through to a sequential axpy loop, which
4898            // ACCUMULATES — zero its row first (row-sized memset; other rows are owned by the
4899            // full-overwrite arms). slab_fused_may_fire never actually reaches here (its arm
4900            // has no fallible predicate), included for the allocation invariant's symmetry.
4901            if gdec_may_fire || slab_fused_may_fire {
4902                let mut row = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
4903                e.memset_zeros_view(&mut row)?;
4904            }
4905
4906            // Fiddler-style split at whole-expert granularity. Partial residency is deliberately a
4907            // CPU assignment: transferring the missing projection would reintroduce the PCIe/NVMe
4908            // stall this path exists to remove, while mixing projections would require another
4909            // activation round-trip. Weight addresses remain valid until this worker is joined at
4910            // the bottom of the token scope.
4911            let mut cpu_mask = vec![false; sel.len()];
4912            let cpu_worker = if let Some(host_input) = cpu_input.as_ref() {
4913                let gpu_resident = if use_cache {
4914                    e.with_moe_cache(max_block, |cache, _| {
4915                        Ok(sel
4916                            .iter()
4917                            .map(|&expert| {
4918                                let expert = expert as u16;
4919                                [PROJ_GATE, PROJ_UP, PROJ_DOWN]
4920                                    .into_iter()
4921                                    .filter(|&projection| {
4922                                        cache
4923                                            .resident(BlockId::new(il, projection, expert))
4924                                            .is_some()
4925                                    })
4926                                    .count()
4927                            })
4928                            .collect::<Vec<_>>())
4929                    })?
4930                } else {
4931                    vec![0; sel.len()]
4932                };
4933                let mut cpu_selected = Vec::new();
4934                for (index, (&expert, &route_weight)) in sel.iter().zip(w).enumerate() {
4935                    if gpu_resident[index] != 3 {
4936                        cpu_mask[index] = true;
4937                        crate::cpu_experts::record_incomplete_gpu_residency(gpu_resident[index]);
4938                        let expert = expert as usize;
4939                        cpu_selected.push((expert, route_weight));
4940                    }
4941                }
4942                if crate::cpu_experts::predictor_enabled() {
4943                    // Fire-and-forget lookahead: the predictor worker scores layers il+1..
4944                    // from this layer's MoE input and prefetches predicted-and-missing
4945                    // experts into the companion RAM cache. Never blocks this thread.
4946                    let row = &host_input[tok * n_embd..(tok + 1) * n_embd];
4947                    crate::cpu_experts::predictor_submit(il, row);
4948                }
4949                if cpu_selected.is_empty() {
4950                    None
4951                } else {
4952                    let row = &host_input[tok * n_embd..(tok + 1) * n_embd];
4953                    let job = crate::cpu_experts::prepare_job(m, il, &cpu_selected, row)
4954                        .map_err(std::io::Error::other)?;
4955                    Some(crate::cpu_experts::submit(job).map_err(std::io::Error::other)?)
4956                }
4957            } else {
4958                None
4959            };
4960
4961            let worker_window = worker_disk_prefetch
4962                .then(worker_prefetch_window)
4963                .unwrap_or(0);
4964            for (j, &ex) in sel.iter().enumerate() {
4965                if cpu_mask[j] {
4966                    continue;
4967                }
4968                let ex = ex as usize;
4969                // PER-EXPERT SLAB READ (lane/pp-leverb 2026-08-08): the layers the fused
4970                // pair above excludes — step35's CLAMPED layers 43/44 (ffn_act_lim has no
4971                // fused form) and macro-carrying artifacts — still have their bytes in the
4972                // local resident slab. Same kernels as the SLRU arms (`qmatvec_expert_q8` /
4973                // `qmatvec_view`), same ffn_act_lim/macro folds; provenance = slab base +
4974                // ex*stride. No dispatch lock, no admission, no prefetch — nothing to miss.
4975                if let Some(d) = slab_local {
4976                    let gl = m.gate_exps.expert_layout(ex);
4977                    let ul = m.up_exps.expert_layout(ex);
4978                    let dl = m.down_exps.expert_layout(ex);
4979                    let (g0, u0, d0) = (
4980                        ex * m.gate_exps.expert_stride,
4981                        ex * m.up_exps.expert_stride,
4982                        ex * m.down_exps.expert_stride,
4983                    );
4984                    let (gate, up) = if moe_q8 {
4985                        if tok_q8.is_none() {
4986                            tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
4987                        }
4988                        let (zq, zd) = tok_q8.as_ref().unwrap();
4989                        (
4990                            e.qmatvec_expert_q8(
4991                                &d.gate,
4992                                g0..g0 + gl.len,
4993                                zq,
4994                                zd,
4995                                1,
4996                                m.gate_exps.in_f,
4997                                m.gate_exps.out_f,
4998                                gl.qtype,
4999                                gl.row_bytes,
5000                            )?,
5001                            e.qmatvec_expert_q8(
5002                                &d.up,
5003                                u0..u0 + ul.len,
5004                                zq,
5005                                zd,
5006                                1,
5007                                m.up_exps.in_f,
5008                                m.up_exps.out_f,
5009                                ul.qtype,
5010                                ul.row_bytes,
5011                            )?,
5012                        )
5013                    } else {
5014                        (
5015                            e.qmatvec_view(
5016                                &d.gate,
5017                                g0..g0 + gl.len,
5018                                &zt,
5019                                1,
5020                                m.gate_exps.in_f,
5021                                m.gate_exps.out_f,
5022                                gl.qtype,
5023                                gl.row_bytes,
5024                            )?,
5025                            e.qmatvec_view(
5026                                &d.up,
5027                                u0..u0 + ul.len,
5028                                &zt,
5029                                1,
5030                                m.up_exps.in_f,
5031                                m.up_exps.out_f,
5032                                ul.qtype,
5033                                ul.row_bytes,
5034                            )?,
5035                        )
5036                    };
5037                    let mut act = e.uninit(n_ff_exp)?;
5038                    Self::ffn_act_lim(
5039                        e,
5040                        cfg,
5041                        &gate,
5042                        &up,
5043                        m.gate_exps.macro_scale(ex),
5044                        m.up_exps.macro_scale(ex),
5045                        lim_exp,
5046                        &mut act,
5047                        n_ff_exp,
5048                    )?;
5049                    let y = if moe_q8 {
5050                        let (aq2, ad2) = e.quantize_q8_1(&act, 1, n_ff_exp)?;
5051                        e.qmatvec_expert_q8(
5052                            &d.down,
5053                            d0..d0 + dl.len,
5054                            &aq2,
5055                            &ad2,
5056                            1,
5057                            m.down_exps.in_f,
5058                            m.down_exps.out_f,
5059                            dl.qtype,
5060                            dl.row_bytes,
5061                        )?
5062                    } else {
5063                        let actv = act.slice(0..n_ff_exp);
5064                        e.qmatvec_view(
5065                            &d.down,
5066                            d0..d0 + dl.len,
5067                            &actv,
5068                            1,
5069                            m.down_exps.in_f,
5070                            m.down_exps.out_f,
5071                            dl.qtype,
5072                            dl.row_bytes,
5073                        )?
5074                    };
5075                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5076                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
5077                    continue;
5078                }
5079                for next in page_prefetch_positions(j, sel.len(), page_window) {
5080                    Self::moe_prefetch_host_expert(sel[next] as usize, m);
5081                }
5082                let keep = [
5083                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_GATE, ex as u16),
5084                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_UP, ex as u16),
5085                    crate::moe_cache::BlockId::new(il, crate::moe_cache::PROJ_DOWN, ex as u16),
5086                ];
5087                if worker_disk_prefetch && worker_window > 0 {
5088                    for next in worker_prefetch_positions(j, sel.len(), worker_window) {
5089                        Self::moe_prefetch_disk_expert(
5090                            e,
5091                            il,
5092                            sel[next] as usize,
5093                            m,
5094                            max_block,
5095                            &keep,
5096                        )?;
5097                    }
5098                } else if cache_dispatch
5099                    && !cpu_hybrid
5100                    && moe_prefetch_enabled()
5101                    && j + 1 < sel.len()
5102                {
5103                    let next = sel[j + 1] as usize;
5104                    Self::moe_prefetch_expert(e, il, next, m, max_block, &keep)?;
5105                }
5106                let [gate_q8, up_q8, down_q8] = [moe_q8; 3];
5107                if cache_dispatch && (gate_q8 || up_q8 || down_q8) {
5108                    // dp4a EXPERT PATH (MEMRA_MOE_Q8): quantize z-row once per token. Mixed expert
5109                    // layouts stay on the metadata-aware f32 path.
5110                    if (gate_q8 || up_q8) && tok_q8.is_none() {
5111                        tok_q8 = Some(e.quantize_q8_1_view(&zt, 1, n_embd)?);
5112                    }
5113                    let gate = if gate_q8 {
5114                        let (zq, zd) = tok_q8.as_ref().unwrap();
5115                        Self::moe_cached_gemm_q8(e, il, PROJ_GATE, ex, m, max_block, zq, zd)?
5116                    } else {
5117                        Self::moe_cached_gemm(e, il, PROJ_GATE, ex, m, max_block, &zt)?
5118                    };
5119                    let up = if up_q8 {
5120                        let (zq, zd) = tok_q8.as_ref().unwrap();
5121                        Self::moe_cached_gemm_q8(e, il, PROJ_UP, ex, m, max_block, zq, zd)?
5122                    } else {
5123                        Self::moe_cached_gemm(e, il, PROJ_UP, ex, m, max_block, &zt)?
5124                    };
5125                    let mut act = e.uninit(n_ff_exp)?;
5126                    Self::ffn_act_lim(
5127                        e,
5128                        cfg,
5129                        &gate,
5130                        &up,
5131                        m.gate_exps.macro_scale(ex),
5132                        m.up_exps.macro_scale(ex),
5133                        lim_exp,
5134                        &mut act,
5135                        n_ff_exp,
5136                    )?;
5137                    let y = if down_q8 {
5138                        let (aq2, ad2) = e.quantize_q8_1(&act, 1, n_ff_exp)?;
5139                        Self::moe_cached_gemm_q8(e, il, PROJ_DOWN, ex, m, max_block, &aq2, &ad2)?
5140                    } else {
5141                        let actv = act.slice(0..n_ff_exp);
5142                        Self::moe_cached_gemm(e, il, PROJ_DOWN, ex, m, max_block, &actv)?
5143                    };
5144                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5145                    // down-proj macro folds into the accumulate weight (1.0 for non-macro archs).
5146                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
5147                } else if cache_dispatch {
5148                    // SLRU residency cache: per-projection, dispatch the block (HIT => resident slot,
5149                    // MISS => staged slot) then run the SAME unchanged qmatvec_view from that slot.
5150                    // The bytes the kernel reads are byte-for-byte the same GGUF block (§B.3); the
5151                    // only difference between HIT and MISS is whether the memcpy_htod ran.
5152                    let gate = Self::moe_cached_gemm(e, il, PROJ_GATE, ex, m, max_block, &zt)?;
5153                    let up = Self::moe_cached_gemm(e, il, PROJ_UP, ex, m, max_block, &zt)?;
5154                    let mut act = e.uninit(n_ff_exp)?; // activation fully overwrites
5155                    Self::ffn_act_lim(
5156                        e,
5157                        cfg,
5158                        &gate,
5159                        &up,
5160                        m.gate_exps.macro_scale(ex),
5161                        m.up_exps.macro_scale(ex),
5162                        lim_exp,
5163                        &mut act,
5164                        n_ff_exp,
5165                    )?;
5166                    let actv = act.slice(0..n_ff_exp);
5167                    let y = Self::moe_cached_gemm(e, il, PROJ_DOWN, ex, m, max_block, &actv)?;
5168                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5169                    // down-proj macro folds into the accumulate weight (post-matmul linear scale).
5170                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
5171                } else if cache_frozen {
5172                    // A later prompt prime must not change the CPU/GPU assignment frozen after the
5173                    // first prime. Reuse every fixed resident projection directly and stage only a
5174                    // true miss through the ordinary scratch slot. This preserves the established
5175                    // f32-dequant numeric path while avoiding a full-bank reread on every prime.
5176                    let gate = Self::moe_frozen_gemm(
5177                        e,
5178                        il,
5179                        PROJ_GATE,
5180                        ex,
5181                        m,
5182                        max_block,
5183                        &zt,
5184                        &mut scratch_g,
5185                        g_len,
5186                    )?;
5187                    let up = Self::moe_frozen_gemm(
5188                        e,
5189                        il,
5190                        PROJ_UP,
5191                        ex,
5192                        m,
5193                        max_block,
5194                        &zt,
5195                        &mut scratch_u,
5196                        u_len,
5197                    )?;
5198                    let mut act = e.uninit(n_ff_exp)?;
5199                    Self::ffn_act_lim(
5200                        e,
5201                        cfg,
5202                        &gate,
5203                        &up,
5204                        m.gate_exps.macro_scale(ex),
5205                        m.up_exps.macro_scale(ex),
5206                        lim_exp,
5207                        &mut act,
5208                        n_ff_exp,
5209                    )?;
5210                    let actv = act.slice(0..n_ff_exp);
5211                    let y = Self::moe_frozen_gemm(
5212                        e,
5213                        il,
5214                        PROJ_DOWN,
5215                        ex,
5216                        m,
5217                        max_block,
5218                        &actv,
5219                        &mut scratch_d,
5220                        d_len,
5221                    )?;
5222                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5223                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
5224                } else {
5225                    // Stage-1: stage gate/up/down for expert `ex` into the scratch slots, then GEMM.
5226                    // Lazy scratch: first no-cache expert allocates the 3 slots (uninit — stage_expert
5227                    // fully overwrites the byte range the GEMM reads).
5228                    if scratch_g.is_none() {
5229                        scratch_g = Some(e.alloc_u8_uninit(g_len)?);
5230                        scratch_u = Some(e.alloc_u8_uninit(u_len)?);
5231                        scratch_d = Some(e.alloc_u8_uninit(d_len)?);
5232                    }
5233                    let (sg, su, sd) = (
5234                        scratch_g.as_mut().unwrap(),
5235                        scratch_u.as_mut().unwrap(),
5236                        scratch_d.as_mut().unwrap(),
5237                    );
5238                    let gl = m.gate_exps.expert_layout(ex);
5239                    let ul = m.up_exps.expert_layout(ex);
5240                    let dl = m.down_exps.expert_layout(ex);
5241                    e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
5242                    let gate = e.qmatvec_view(
5243                        sg,
5244                        0..gl.len,
5245                        &zt,
5246                        1,
5247                        m.gate_exps.in_f,
5248                        m.gate_exps.out_f,
5249                        gl.qtype,
5250                        gl.row_bytes,
5251                    )?;
5252
5253                    e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
5254                    let up = e.qmatvec_view(
5255                        su,
5256                        0..ul.len,
5257                        &zt,
5258                        1,
5259                        m.up_exps.in_f,
5260                        m.up_exps.out_f,
5261                        ul.qtype,
5262                        ul.row_bytes,
5263                    )?;
5264
5265                    let mut act = e.uninit(n_ff_exp)?; // activation fully overwrites
5266                    Self::ffn_act_lim(
5267                        e,
5268                        cfg,
5269                        &gate,
5270                        &up,
5271                        m.gate_exps.macro_scale(ex),
5272                        m.up_exps.macro_scale(ex),
5273                        lim_exp,
5274                        &mut act,
5275                        n_ff_exp,
5276                    )?;
5277
5278                    e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
5279                    let actv = act.slice(0..n_ff_exp);
5280                    let y = e.qmatvec_view(
5281                        sd,
5282                        0..dl.len,
5283                        &actv,
5284                        1,
5285                        m.down_exps.in_f,
5286                        m.down_exps.out_f,
5287                        dl.qtype,
5288                        dl.row_bytes,
5289                    )?;
5290
5291                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5292                    e.axpy_into(&y, w[j] * m.down_exps.macro_scale(ex), &mut dst, n_embd)?;
5293                }
5294            }
5295            if let Some(worker) = cpu_worker {
5296                let cpu_output = worker.wait().map_err(std::io::Error::other)?;
5297                let cpu_output = e.htod(&cpu_output)?;
5298                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
5299                e.axpy_into(&cpu_output, 1.0, &mut dst, n_embd)?;
5300            }
5301            if cpu_hybrid && !cache_frozen && cpu_expert_profile_admit_enabled() {
5302                for (j, &ex) in sel.iter().enumerate() {
5303                    if cpu_mask[j] {
5304                        Self::moe_profile_admit_expert(e, il, ex as usize, m, max_block)?;
5305                    }
5306                }
5307            }
5308        }
5309
5310        // 3. SHARED EXPERT (ALWAYS-ON, no routing) on the SAME z — qwen35moe only. OLMoE and most
5311        //    vanilla MoE have NO shared expert (the shexp tensors are absent / `None`); skip it then.
5312        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
5313        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
5314        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
5315            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
5316        {
5317            let n_ff_sh = gate_shexp.out_features(); // 512
5318            // Q8 TRUNK-FUSION (decode t=1): gate_shexp+up_shexp are Q8_0 same-shape on the 35B —
5319            // ONE fused2 launch (also folds the two per-matmul re-quantizes of z into one).
5320            // Bit-identical per (tensor,row); falls back to the two matmul calls when ineligible.
5321            // Small-t (spec verify 2..15) rides matmul_decode_exact so shexp FP chains match the
5322            // t==1 decode chain per column (cuBLASLt n-dependence + dp4a-vs-mmvq class); real
5323            // prefill keeps the batched matmul. Activation routes through ffn_act (SiLU for
5324            // softmax archs, clamped swigluoai for M3 — identical to silu_mul when cfg.m3 is None).
5325            let verify_t = t > 1 && t < PRIME_MIN_T;
5326            let (sg_gate, sg_up) = if t == 1 {
5327                match e.matmul_q8_fused2_x(gate_shexp, up_shexp, z)? {
5328                    Some(pair) => pair,
5329                    None => (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?),
5330                }
5331            } else if verify_t {
5332                (
5333                    e.matmul_decode_exact(gate_shexp, z, t)?,
5334                    e.matmul_decode_exact(up_shexp, z, t)?,
5335                )
5336            } else {
5337                (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?) // [T, 512] each
5338            };
5339            let mut sa = e.uninit(t * n_ff_sh)?; // activation fully overwrites
5340            Self::ffn_act_lim(
5341                e,
5342                cfg,
5343                &sg_gate,
5344                &sg_up,
5345                1.0,
5346                1.0,
5347                lim_shexp,
5348                &mut sa,
5349                t * n_ff_sh,
5350            )?;
5351            let sh = if verify_t {
5352                e.matmul_decode_exact(down_shexp, &sa, t)?
5353            } else {
5354                e.matmul(down_shexp, &sa, t)?
5355            }; // [T, n_embd]
5356
5357            // shexp gate: qwen35moe sigmoid-gates via ffn_gate_inp_shexp (1-D ne=[n_embd] ->
5358            // out_f=1); M3 has no gate tensor -> weight 1.0. Decode + verify ride the fused
5359            // sigmoid-dot kernel (one fold order for both chains; kills the per-layer
5360            // cuBLASLt m=1 splitK GEMM — 40x/step, ~10% of the H100 q35 decode step).
5361            // SERVE ISOLATION (lane/concat-prime-exact, 2026-08-02): PREFILL rides it too.
5362            // This out_f=1 cuBLASLt GEMV is the SECOND m-dependent op in the trunk (probed:
5363            // rows [0,19) move by 1.07e-4 between m=74 and m=75 while sigmoid_dot_rows is
5364            // BIT-IDENTICAL — allw-shexpgate-o35b.log). The gate multiplies the shared
5365            // expert's contribution into every token's residual, so under cross-request
5366            // concat prefill a session's hidden state depended on its co-arrivals' token
5367            // count. MEMRA_ROUTER_PREFILL_EXACT=0 reverts to the batched cuBLASLt linear.
5368            let g = match &m.gate_inp_shexp {
5369                Some(gate_inp_shexp) => {
5370                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
5371                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
5372                    } else {
5373                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
5374                        let mut g = e.uninit(t)?; // sigmoid fully overwrites
5375                        e.sigmoid(&gs, &mut g, t)?;
5376                        g
5377                    }
5378                }
5379                None => e.htod(&vec![1.0f32; t])?,
5380            };
5381            // moe_out[r, :] += sh[r, :] * g[r]   (per-token scalar gate; g=1 ungated)
5382            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
5383        }
5384
5385        Ok(moe_out)
5386    }
5387
5388    /// Stage-1 (no-cache) per-DECODE-TOKEN H2D bytes: every routed block re-staged every layer every
5389    /// token = sum over MoE layers of n_used * (gate+up+down expert_stride). The §D.4 PCIe baseline.
5390    pub fn stage1_h2d_per_token(&self) -> u64 {
5391        use crate::hybrid::Ffn;
5392        let n_used = self
5393            .cfg
5394            .moe
5395            .as_ref()
5396            .map(|m| m.expert_used_count as u64)
5397            .unwrap_or(0);
5398        let mut bytes = 0u64;
5399        for l in self.layers.iter() {
5400            if let Ffn::Moe(m) = &l.ffn {
5401                bytes += n_used
5402                    * (m.gate_exps.max_expert_bytes()
5403                        + m.up_exps.max_expert_bytes()
5404                        + m.down_exps.max_expert_bytes()) as u64;
5405            }
5406        }
5407        bytes
5408    }
5409
5410    /// Largest expert block (bytes) across ALL MoE layers + the MTP head — the fixed cache slot size.
5411    /// UD/dynamic GGUFs quant different layers differently, so `expert_stride` varies per layer; the
5412    /// residency cache slots are fixed-address and must fit any block, so size to this global max.
5413    pub(crate) fn max_moe_block(&self) -> usize {
5414        use crate::hybrid::Ffn;
5415        let mut mx = 0usize;
5416        let mut scan = |ffn: &Ffn| {
5417            if let Ffn::Moe(m) = ffn {
5418                mx = mx
5419                    .max(m.gate_exps.max_expert_bytes())
5420                    .max(m.up_exps.max_expert_bytes())
5421                    .max(m.down_exps.max_expert_bytes());
5422            }
5423        };
5424        for l in self.layers.iter() {
5425            scan(&l.ffn);
5426        }
5427        if let Some(mtp) = self.mtp.as_ref() {
5428            scan(&mtp.ffn);
5429        }
5430        mx
5431    }
5432
5433    /// Exact retained projection lengths for cache class sizing. Pruned ids keep router positions
5434    /// but have no bytes and therefore consume no residency slot.
5435    pub(crate) fn moe_cache_block_sizes(&self) -> Vec<usize> {
5436        use crate::hybrid::Ffn;
5437        let mut sizes = Vec::new();
5438        let mut scan = |ffn: &Ffn| {
5439            let Ffn::Moe(m) = ffn else { return };
5440            for ex in 0..m.gate_exps.n_expert {
5441                if m.active_experts.as_ref().is_some_and(|active| !active[ex]) {
5442                    continue;
5443                }
5444                for exps in [&m.gate_exps, &m.up_exps, &m.down_exps] {
5445                    let len = exps.expert_layout(ex).len;
5446                    if len > 0 {
5447                        sizes.push(len);
5448                    }
5449                }
5450            }
5451        };
5452        for layer in &self.layers {
5453            scan(&layer.ffn);
5454        }
5455        if let Some(mtp) = &self.mtp {
5456            scan(&mtp.ffn);
5457        }
5458        sizes
5459    }
5460
5461    /// Persist the frozen residency set so a later process can restage it directly and skip
5462    /// the profiling warmup. Plain text: a versioned header binding slot geometry, then one
5463    /// `layer proj ex` triple per line. A mismatched or stale profile is rejected at load
5464    /// (header check) or degrades to fewer restaged blocks (per-id checks); either way the
5465    /// post-freeze argmax gate still validates the serving assignment.
5466    pub fn save_cpu_expert_residency_profile(
5467        &self,
5468        e: &Engine,
5469        path: &std::path::Path,
5470    ) -> Result<(), Box<dyn std::error::Error>> {
5471        let Some(ids) = e.export_moe_residency() else {
5472            return Err("no MoE residency cache to persist".into());
5473        };
5474        let mut body = format!(
5475            "memra-freeze-profile v1 max_block={} blocks={}\n",
5476            self.max_moe_block(),
5477            ids.len()
5478        );
5479        for (layer, proj, ex) in &ids {
5480            body.push_str(&format!("{layer} {proj} {ex}\n"));
5481        }
5482        let tmp = path.with_extension("tmp");
5483        std::fs::write(&tmp, body)?;
5484        std::fs::rename(&tmp, path)?;
5485        println!(
5486            "[moe-cache] freeze profile saved: {} blocks -> {}",
5487            ids.len(),
5488            path.display()
5489        );
5490        Ok(())
5491    }
5492
5493    /// Restage a saved freeze profile and freeze immediately, skipping the profiling warmup.
5494    /// Returns false (leaving the cache untouched for a normal warmup) when the profile is
5495    /// missing or its header does not match this model's slot geometry.
5496    pub fn restore_cpu_expert_residency_profile(
5497        &self,
5498        e: &Engine,
5499        path: &std::path::Path,
5500    ) -> Result<bool, Box<dyn std::error::Error>> {
5501        use crate::hybrid::Ffn;
5502        use crate::moe_cache::BlockId;
5503        let Ok(content) = std::fs::read_to_string(path) else {
5504            return Ok(false);
5505        };
5506        let mut lines = content.lines();
5507        let Some(header) = lines.next() else {
5508            return Ok(false);
5509        };
5510        let expected = format!("memra-freeze-profile v1 max_block={}", self.max_moe_block());
5511        if !header.starts_with(&expected) {
5512            println!(
5513                "[moe-cache] freeze profile ignored (geometry mismatch): {}",
5514                path.display()
5515            );
5516            return Ok(false);
5517        }
5518        let mut by_layer: std::collections::HashMap<u16, Vec<BlockId>> =
5519            std::collections::HashMap::new();
5520        for line in lines {
5521            let mut fields = line.split_whitespace();
5522            let (Some(layer), Some(proj), Some(ex)) = (fields.next(), fields.next(), fields.next())
5523            else {
5524                continue;
5525            };
5526            let (Ok(layer), Ok(proj), Ok(ex)) =
5527                (layer.parse::<u16>(), proj.parse::<u8>(), ex.parse::<u16>())
5528            else {
5529                continue;
5530            };
5531            by_layer
5532                .entry(layer)
5533                .or_default()
5534                .push(BlockId::new(layer, proj, ex));
5535        }
5536        let requested: usize = by_layer.values().map(Vec::len).sum();
5537        if requested == 0 {
5538            return Ok(false);
5539        }
5540        let max_block = self.max_moe_block();
5541        let mut restaged = 0usize;
5542        let mut stage_layer =
5543            |layer_index: u16, ffn: &Ffn| -> Result<(), Box<dyn std::error::Error>> {
5544                let Ffn::Moe(m) = ffn else { return Ok(()) };
5545                let Some(ids) = by_layer.get(&layer_index) else {
5546                    return Ok(());
5547                };
5548                e.with_moe_cache(max_block, |cache, eng| {
5549                    for id in ids {
5550                        if cache.restage_block(*id, m, eng)? {
5551                            restaged += 1;
5552                        }
5553                    }
5554                    Ok(())
5555                })
5556            };
5557        for (index, layer) in self.layers.iter().enumerate() {
5558            stage_layer(index as u16, &layer.ffn)?;
5559        }
5560        if let Some(mtp) = self.mtp.as_ref() {
5561            stage_layer(u16::MAX, &mtp.ffn)?;
5562        }
5563        e.freeze_moe_cache();
5564        println!(
5565            "[moe-cache] freeze profile restored: {restaged}/{requested} blocks restaged from {}",
5566            path.display()
5567        );
5568        Ok(true)
5569    }
5570
5571    /// Freeze the heterogeneous CPU/GPU split after the caller's discarded profile warmup.
5572    pub fn freeze_cpu_expert_residency(
5573        &self,
5574        e: &Engine,
5575    ) -> Result<(), Box<dyn std::error::Error>> {
5576        e.freeze_moe_cache();
5577        Ok(())
5578    }
5579
5580    /// FFN activation dispatch: swigluoai (clamped, alpha/limit) when cfg.m3 says so, else the
5581    /// standard SiLU*up. One seam so every FFN site (dense, routed expert, shared expert) follows
5582    /// the model's activation exactly.
5583    ///
5584    /// NO-`il` FORM: cannot apply step35's PER-LAYER SwiGLU clamp. Only call it from a site whose
5585    /// layer provably has no live limit (dense-FFN layers, MTP blocks) — `ffn_act_lim` is the
5586    /// form for anything that can land on a clamped layer.
5587    pub fn ffn_act(
5588        e: &Engine,
5589        cfg: &ModelConfig,
5590        gate: &CudaSlice<f32>,
5591        up: &CudaSlice<f32>,
5592        act: &mut CudaSlice<f32>,
5593        n: usize,
5594    ) -> Result<(), Box<dyn std::error::Error>> {
5595        Self::ffn_act_scaled(e, cfg, gate, up, 1.0, 1.0, act, n)
5596    }
5597
5598    /// ffn_act with per-tensor post-matmul macro-scales folded in (gs/us == 1.0 -> identical
5599    /// float ops to ffn_act; used by the ModelOpt NVFP4 expert path where each expert tensor
5600    /// carries a `weight_scale_2`). Same no-`il` contract as `ffn_act`.
5601    #[allow(clippy::too_many_arguments)]
5602    pub(crate) fn ffn_act_scaled(
5603        e: &Engine,
5604        cfg: &ModelConfig,
5605        gate: &CudaSlice<f32>,
5606        up: &CudaSlice<f32>,
5607        gs: f32,
5608        us: f32,
5609        act: &mut CudaSlice<f32>,
5610        n: usize,
5611    ) -> Result<(), Box<dyn std::error::Error>> {
5612        Self::ffn_act_lim(e, cfg, gate, up, gs, us, None, act, n)
5613    }
5614
5615    /// ffn_act_scaled + step35's PER-LAYER clamped SwiGLU. `limit`:
5616    ///   * `None`   -> the unclamped dispatch (every arch except step35's layers 43-44).
5617    ///   * `Some(l)`-> `min(silu(gate*gs), l) * clamp(up*us, +-l)` (llama-graph.cpp:2146/1751,
5618    ///                 non-DEEPSEEK4 branch). Callers source it from `cfg.clamp_exp_at(il)`
5619    ///                 (routed experts) or `cfg.clamp_shexp_at(il)` (shared expert) — the two
5620    ///                 arrays are SEPARATE and a layer can have one without the other.
5621    /// The `> 1e-6` eps gate lives in `clamp_exp_at`/`clamp_shexp_at`, so a `Some` here is
5622    /// already known live.
5623    #[allow(clippy::too_many_arguments)]
5624    pub(crate) fn ffn_act_lim(
5625        e: &Engine,
5626        cfg: &ModelConfig,
5627        gate: &CudaSlice<f32>,
5628        up: &CudaSlice<f32>,
5629        gs: f32,
5630        us: f32,
5631        limit: Option<f32>,
5632        act: &mut CudaSlice<f32>,
5633        n: usize,
5634    ) -> Result<(), Box<dyn std::error::Error>> {
5635        if let Some(m3) = cfg.m3.as_ref() {
5636            debug_assert!(
5637                limit.is_none(),
5638                "m3 swigluoai and step35 clamp are different archs"
5639            );
5640            return e.swigluoai_mul_scaled(
5641                gate,
5642                up,
5643                gs,
5644                us,
5645                m3.swiglu_alpha,
5646                m3.swiglu_limit,
5647                act,
5648                n,
5649            );
5650        }
5651        if let Some(l) = limit {
5652            return e.swiglu_clamped_mul_scaled(gate, up, gs, us, l, act, n);
5653        }
5654        if gs == 1.0 && us == 1.0 {
5655            return e.silu_mul(gate, up, act, n);
5656        }
5657        e.silu_mul_scaled(gate, up, gs, us, act, n)
5658    }
5659
5660    /// Routing for the whole batch: returns (sel [T*n_used] expert ids, w [T*n_used] renorm weights),
5661    /// token-major. Default = the Stage-1 host path (dtoh logits, softmax-256, stable DESC top-k,
5662    /// renorm). MEMRA_FUSED_ROUTER = the device kernel (§A) which reproduces the same numerics; we
5663    /// still dtoh the tiny [T,n_used] sel/w buffers (64 B/token vs 1 KB/token) — the host loop
5664    /// indexes HostExps.bytes on the CPU to choose the DMA source (§A.2 output staging).
5665    fn moe_route(
5666        e: &Engine,
5667        logits: &CudaSlice<f32>,
5668        t: usize,
5669        n_expert: usize,
5670        n_used: usize,
5671    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
5672        Self::moe_route_cfg(e, logits, t, n_expert, n_used, None)
5673    }
5674
5675    /// DeepSeek-V3-class sigmoid routing (Step-3.7, MiniMax-M3, Hy3, GLM-DSA). Reference:
5676    /// M3/Hy3 modeling code — scores = sigmoid(logits); selection over scores + expert bias
5677    /// (M3 `e_score_correction_bias` / Hy3 `expert_bias`, both surfaced as `exp_probs_b`);
5678    /// weights = un-biased scores of the selected experts, sum-normalized when `route_norm`,
5679    /// x scaling factor (M3 routed_scaling_factor 2.0 / Hy3 router_scaling_factor 2.826).
5680    /// Default uses one device kernel plus the same pinned sel/w readback contract as the fused
5681    /// softmax router. `MEMRA_SIG_ROUTER=0` restores the full-logit DtoH host oracle.
5682    #[allow(clippy::too_many_arguments)]
5683    fn moe_route_sigmoid_cfg(
5684        e: &Engine,
5685        logits: &CudaSlice<f32>,
5686        t: usize,
5687        n_expert: usize,
5688        n_used: usize,
5689        m: &MoeWeights,
5690        (sf, route_norm): (f32, bool),
5691    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
5692        if sigmoid_router_enabled() {
5693            return e.moe_router_sigmoid_topk_host(
5694                logits,
5695                t,
5696                n_expert,
5697                n_used,
5698                m.active_count(),
5699                &m.exp_probs_b_dev,
5700                &m.active_experts_dev,
5701                sf,
5702                route_norm,
5703            );
5704        }
5705        let lg = e.dtoh(logits)?;
5706        Self::moe_route_sigmoid_host(
5707            &lg,
5708            t,
5709            n_expert,
5710            n_used,
5711            m.exp_probs_b.as_deref(),
5712            sf,
5713            route_norm,
5714            m.active_experts.as_deref(),
5715        )
5716    }
5717
5718    /// Softmax router for qwen35moe/OLMoE. An active-mask overlay retains the host oracle because
5719    /// the existing softmax device kernel has no mask input.
5720    fn moe_route_cfg(
5721        e: &Engine,
5722        logits: &CudaSlice<f32>,
5723        t: usize,
5724        n_expert: usize,
5725        n_used: usize,
5726        active: Option<&[bool]>,
5727    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
5728        // LAUNCH-STRUCTURE STAGE 1 (2026-07-05): fused router DEFAULT ON (MEMRA_FUSED_ROUTER=0
5729        // rollback) via the single-sync pinned readback — softmax arch only.
5730        if active.is_none() && !matches!(std::env::var("MEMRA_FUSED_ROUTER").as_deref(), Ok("0")) {
5731            return e.moe_router_topk_host(logits, t, n_expert, n_used);
5732        }
5733        // Host oracle (the §D bit-identity reference).
5734        let lg = e.dtoh(logits)?; // [T*n_expert] host
5735        let mut sel = vec![0u32; t * n_used];
5736        let mut w_out = vec![0f32; t * n_used];
5737        for tok in 0..t {
5738            let row = &lg[tok * n_expert..(tok + 1) * n_expert];
5739            // softmax over ALL n_expert (stable: subtract max)
5740            let maxl = row
5741                .iter()
5742                .enumerate()
5743                .filter(|(i, _)| active.is_none_or(|mask| mask[*i]))
5744                .map(|(_, &x)| x)
5745                .fold(f32::NEG_INFINITY, f32::max);
5746            let mut probs = vec![0f32; n_expert];
5747            let mut den = 0f32;
5748            for i in 0..n_expert {
5749                if active.is_some_and(|mask| !mask[i]) {
5750                    continue;
5751                }
5752                let x = (row[i] - maxl).exp();
5753                probs[i] = x;
5754                den += x;
5755            }
5756            for p in probs.iter_mut() {
5757                *p /= den;
5758            }
5759            // stable DESC sort: prob DESC, ascending-index tiebreak.
5760            let mut idx: Vec<usize> = (0..n_expert)
5761                .filter(|&i| active.is_none_or(|mask| mask[i]))
5762                .collect();
5763            idx.sort_by(|&a, &b| probs[b].total_cmp(&probs[a]).then(a.cmp(&b)));
5764            let sl = &idx[..n_used];
5765            let mut wv: Vec<f32> = sl.iter().map(|&i| probs[i]).collect();
5766            let mut ws: f32 = wv.iter().sum();
5767            ws = ws.max(6.103515625e-5_f32); // F16 smallest normal, clamp BEFORE divide
5768            for x in wv.iter_mut() {
5769                *x /= ws;
5770            }
5771            for j in 0..n_used {
5772                sel[tok * n_used + j] = sl[j] as u32;
5773                w_out[tok * n_used + j] = wv[j];
5774            }
5775        }
5776        Ok((sel, w_out))
5777    }
5778
5779    #[allow(clippy::too_many_arguments)]
5780    fn moe_route_sigmoid_with_input(
5781        e: &Engine,
5782        logits: &CudaSlice<f32>,
5783        input: &CudaSlice<f32>,
5784        t: usize,
5785        n_expert: usize,
5786        n_used: usize,
5787        bias: Option<&[f32]>,
5788        (sf, route_norm): (f32, bool),
5789        active: Option<&[bool]>,
5790    ) -> Result<(Vec<u32>, Vec<f32>, Vec<f32>), Box<dyn std::error::Error>> {
5791        let (lg, input) = e.dtoh_pair(logits, input)?;
5792        let (sel, w) =
5793            Self::moe_route_sigmoid_host(&lg, t, n_expert, n_used, bias, sf, route_norm, active)?;
5794        Ok((sel, w, input))
5795    }
5796
5797    /// Start the prediction-guided prefetch worker (MEMRA_MOE_PREFETCH=depth). Call after
5798    /// residency freeze: the worker filters against a static snapshot of the frozen HBM set.
5799    /// Builds a fully-owned per-layer table (host router copies via one-time DtoH, bias,
5800    /// active mask, prebuilt projection descriptors) so no model reference escapes.
5801    pub fn start_moe_prefetch_predictor(
5802        &self,
5803        e: &Engine,
5804        cfg: &ModelConfig,
5805    ) -> Result<(), Box<dyn std::error::Error>> {
5806        use crate::hybrid::Ffn;
5807        let Some(sig) = cfg.sigmoid_router() else {
5808            return Err("prefetch predictor requires a sigmoid-router arch".into());
5809        };
5810        let resident: std::collections::HashSet<(u16, u8, u16)> = e
5811            .export_moe_residency()
5812            .ok_or("prefetch predictor needs the frozen MoE residency cache")?
5813            .into_iter()
5814            .collect();
5815        let mut layers = Vec::new();
5816        for (index, layer) in self.layers.iter().enumerate() {
5817            let Ffn::Moe(m) = &layer.ffn else { continue };
5818            let crate::model::GpuTensor::Float { data, .. } = &m.gate_inp else {
5819                continue;
5820            };
5821            let router = e.dtoh(data)?;
5822            let n_expert = m.gate_exps.n_expert;
5823            let n_embd = m.gate_exps.in_f;
5824            if router.len() != n_embd * n_expert {
5825                continue;
5826            }
5827            let build = |exps: &crate::model::HostExps| {
5828                (0..n_expert)
5829                    .map(|expert| crate::cpu_experts::predictor_projection(exps, expert))
5830                    .collect::<Vec<_>>()
5831            };
5832            layers.push((
5833                index as u16,
5834                crate::cpu_experts::PredictLayerInit {
5835                    router,
5836                    bias: m.exp_probs_b.clone(),
5837                    active: m.active_experts.clone(),
5838                    n_embd,
5839                    n_used: cfg
5840                        .moe
5841                        .as_ref()
5842                        .map(|moe| moe.expert_used_count as usize)
5843                        .ok_or("prefetch predictor requires MoE config")?,
5844                    sig,
5845                    weights_n_expert: n_expert,
5846                    gate: build(&m.gate_exps),
5847                    up: build(&m.up_exps),
5848                    down: build(&m.down_exps),
5849                },
5850            ));
5851        }
5852        crate::cpu_experts::start_prefetch_predictor(layers, resident).map_err(|error| error.into())
5853    }
5854
5855    /// Sigmoid-routing oracle shared by the prefetch predictor and `kernel-check`: identical
5856    /// selection math to the rollback runtime, applied to host-computed logits.
5857    #[allow(clippy::too_many_arguments)]
5858    pub fn moe_route_sigmoid_host_public(
5859        logits: &[f32],
5860        t: usize,
5861        n_expert: usize,
5862        n_used: usize,
5863        bias: Option<&[f32]>,
5864        sf: f32,
5865        route_norm: bool,
5866        active: Option<&[bool]>,
5867    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
5868        Self::moe_route_sigmoid_host(logits, t, n_expert, n_used, bias, sf, route_norm, active)
5869    }
5870
5871    #[allow(clippy::too_many_arguments)]
5872    fn moe_route_sigmoid_host(
5873        lg: &[f32],
5874        t: usize,
5875        n_expert: usize,
5876        n_used: usize,
5877        bias: Option<&[f32]>,
5878        sf: f32,
5879        route_norm: bool,
5880        active: Option<&[bool]>,
5881    ) -> Result<(Vec<u32>, Vec<f32>), Box<dyn std::error::Error>> {
5882        let active_count = active
5883            .map(|mask| mask.iter().filter(|&&enabled| enabled).count())
5884            .unwrap_or(n_expert);
5885        crate::sigrouter_contract::validate_active_count(n_used, active_count)?;
5886        if lg.len() != t * n_expert {
5887            return Err(format!(
5888                "sigmoid router logits length mismatch: got {}, expected {}",
5889                lg.len(),
5890                t * n_expert,
5891            )
5892            .into());
5893        }
5894        let mut sel = vec![0u32; t * n_used];
5895        let mut w_out = vec![0f32; t * n_used];
5896        for tok in 0..t {
5897            let row = &lg[tok * n_expert..(tok + 1) * n_expert];
5898            let scores: Vec<f32> = row.iter().map(|&x| 1.0 / (1.0 + (-x).exp())).collect();
5899            // selection score = sigmoid + bias; weight = plain sigmoid.
5900            let selsc: Vec<f32> = match bias {
5901                Some(b) => scores.iter().zip(b).map(|(s, bb)| s + bb).collect(),
5902                None => scores.clone(),
5903            };
5904            let mut idx: Vec<usize> = (0..n_expert)
5905                .filter(|&i| active.is_none_or(|mask| mask[i]))
5906                .collect();
5907            idx.sort_by(|&a, &b| selsc[b].total_cmp(&selsc[a]).then(a.cmp(&b)));
5908            let sl = &idx[..n_used];
5909            let mut wv: Vec<f32> = sl.iter().map(|&i| scores[i]).collect();
5910            if route_norm {
5911                let ws: f32 = wv.iter().sum::<f32>().max(1e-20);
5912                for x in wv.iter_mut() {
5913                    *x = *x / ws * sf;
5914                }
5915            } else {
5916                for x in wv.iter_mut() {
5917                    *x *= sf;
5918                }
5919            }
5920            for j in 0..n_used {
5921                sel[tok * n_used + j] = sl[j] as u32;
5922                w_out[tok * n_used + j] = wv[j];
5923            }
5924        }
5925        Ok((sel, w_out))
5926    }
5927
5928    /// Step-3.7 resident-slab dispatch: sigmoid top-k outputs remain on device and feed the
5929    /// existing slot-ordered q8 expert kernels directly. Mixed layouts, spill, remote slabs,
5930    /// macro-scaled experts, and observation modes are denied by the caller.
5931    #[allow(clippy::too_many_arguments)]
5932    fn moe_ffn_sigmoid_dev(
5933        e: &Engine,
5934        m: &MoeWeights,
5935        z: &CudaSlice<f32>,
5936        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
5937        logits: &CudaSlice<f32>,
5938        t: usize,
5939        cfg: &ModelConfig,
5940        il: u16,
5941        (scaling_factor, route_norm): (f32, bool),
5942    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
5943        let moe = cfg.moe.as_ref().unwrap();
5944        let n_embd = cfg.n_embd as usize;
5945        let n_expert = moe.expert_count as usize;
5946        let n_used = moe.expert_used_count as usize;
5947        let n_ff_exp = moe.expert_ff_length as usize;
5948        let dev = m.dev_exps.as_ref().unwrap();
5949        debug_assert!(cfg.step35.is_some());
5950        debug_assert_eq!(dev.dev, e.ctx().ordinal());
5951        debug_assert!(m.has_uniform_expert_layout());
5952        debug_assert!(!m.has_macros);
5953
5954        let (sel_d, w_d) = e.moe_router_sigmoid_topk(
5955            logits,
5956            t,
5957            n_expert,
5958            n_used,
5959            m.active_count(),
5960            &m.exp_probs_b_dev,
5961            &m.active_experts_dev,
5962            scaling_factor,
5963            route_norm,
5964        )?;
5965        crate::moesd::record_device_routes(e, il, n_expert, n_used, &sel_d)?;
5966        let (gate_row_bytes, up_row_bytes) = if dev.gu_il {
5967            let combined = m.gate_exps.row_bytes + m.up_exps.row_bytes;
5968            (combined, combined)
5969        } else {
5970            (m.gate_exps.row_bytes, m.up_exps.row_bytes)
5971        };
5972        let (zq, zd) = match (t, zq8) {
5973            (1, Some((q, d))) => (q.clone(), d.clone()),
5974            _ => e.quantize_q8_1(z, t, n_embd)?,
5975        };
5976        let n_pairs = t * n_used;
5977        let mut moe_out = if cfg.clamp_exp_at(il as u32).is_some() {
5978            // The final Step layers retain the established separate gate/up -> clamp -> down
5979            // arithmetic. Pair rows are derived from token position; selected expert ids and
5980            // routing weights remain the device router's buffers throughout.
5981            let pair_tok: Vec<i32> = (0..n_pairs).map(|pair| (pair / n_used) as i32).collect();
5982            let pair_tok_d = e.htod_i32(&pair_tok)?;
5983            let gate = e.moe_pairs_matvec_q8(
5984                &dev.ptr_row,
5985                0,
5986                &pair_tok_d,
5987                &sel_d,
5988                &zq,
5989                &zd,
5990                n_embd,
5991                n_ff_exp,
5992                n_expert,
5993                n_pairs,
5994                m.gate_exps.qtype,
5995                gate_row_bytes,
5996            )?;
5997            let up = e.moe_pairs_matvec_q8(
5998                &dev.ptr_row,
5999                1,
6000                &pair_tok_d,
6001                &sel_d,
6002                &zq,
6003                &zd,
6004                n_embd,
6005                n_ff_exp,
6006                n_expert,
6007                n_pairs,
6008                m.up_exps.qtype,
6009                up_row_bytes,
6010            )?;
6011            let mut act = e.uninit(n_pairs * n_ff_exp)?;
6012            Self::ffn_act_lim(
6013                e,
6014                cfg,
6015                &gate,
6016                &up,
6017                1.0,
6018                1.0,
6019                cfg.clamp_exp_at(il as u32),
6020                &mut act,
6021                n_pairs * n_ff_exp,
6022            )?;
6023            let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
6024            let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
6025            let pair_self_d = e.htod_i32(&pair_self)?;
6026            let down = e.moe_pairs_matvec_q8(
6027                &dev.ptr_row,
6028                2,
6029                &pair_self_d,
6030                &sel_d,
6031                &aq2,
6032                &ad2,
6033                n_ff_exp,
6034                n_embd,
6035                n_expert,
6036                n_pairs,
6037                m.down_exps.qtype,
6038                m.down_exps.row_bytes,
6039            )?;
6040            let tok_off: Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
6041            let tok_ids: Vec<i32> = (0..n_pairs as i32).collect();
6042            let tok_off_d = e.htod_i32(&tok_off)?;
6043            let tok_ids_d = e.htod_i32(&tok_ids)?;
6044            let mut output = e.uninit(t * n_embd)?;
6045            e.moe_pairs_scatter(&down, &w_d, &tok_off_d, &tok_ids_d, &mut output, t, n_embd)?;
6046            output
6047        } else {
6048            let act = e.moe_gate_up_silu8_dev_q8_rows(
6049                &dev.ptr_row,
6050                &sel_d,
6051                &zq,
6052                &zd,
6053                t,
6054                n_embd,
6055                n_ff_exp,
6056                n_used,
6057                n_expert,
6058                m.gate_exps.qtype,
6059                m.up_exps.qtype,
6060                gate_row_bytes,
6061                up_row_bytes,
6062                &m.dev_macros,
6063            )?;
6064            let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
6065            let mut output = e.uninit(t * n_embd)?;
6066            e.moe_down8_fma_dev_q8_rows_g(
6067                &dev.ptr_row,
6068                &sel_d,
6069                &w_d,
6070                &aq2,
6071                &ad2,
6072                &mut output,
6073                t,
6074                n_ff_exp,
6075                n_embd,
6076                n_used,
6077                n_expert,
6078                m.down_exps.qtype,
6079                m.down_exps.row_bytes,
6080            )?;
6081            output
6082        };
6083
6084        if std::env::var("MEMRA_SIG_ROUTER_DISPATCH_TRACE").as_deref() == Ok("1") {
6085            eprintln!(
6086                "[sigrouter-dev] layer={il} tokens={t} experts={n_expert} used={n_used} clamp={} gu_il={}",
6087                cfg.clamp_exp_at(il as u32).is_some(),
6088                dev.gu_il,
6089            );
6090        }
6091        Self::moe_ffn_grouped_add_shared(e, m, z, t, cfg, il, &mut moe_out)?;
6092        Ok(moe_out)
6093    }
6094
6095    /// LAUNCH-STRUCTURE STAGE 3: the ZERO-DtoH fully-resident MoE FFN. Caller guarantees the
6096    /// layer's device pointer row exists (checked under the cache lock). Router top-k runs on
6097    /// device; sel/w are consumed by the `_dev` matvec twins directly; NOTHING crosses PCIe.
6098    /// Same numerics as the fused-router + gdec chain (kernel-level bit-identity, see the
6099    /// MoE PREFILL PAIR-BATCH: host routing (sel/w like the sequential path), then 5 launches
6100    /// TOTAL per layer (quantize z, gate-pairs, up-pairs, silu, act-quantize, down-pairs,
6101    /// scatter) regardless of T or expert count. Bit-identity class: per (pair,row) dot =
6102    /// qmatvec_expert_q8 order; per-token accumulation slot-ordered (scatter kernel).
6103    fn moe_ffn_pairs(
6104        e: &Engine,
6105        m: &MoeWeights,
6106        z: &CudaSlice<f32>,
6107        logits: &CudaSlice<f32>,
6108        t: usize,
6109        cfg: &ModelConfig,
6110    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6111        let moe = cfg.moe.as_ref().unwrap();
6112        let n_embd = cfg.n_embd as usize;
6113        let n_expert = moe.expert_count as usize;
6114        let n_used = moe.expert_used_count as usize;
6115        let n_ff_exp = moe.expert_ff_length as usize;
6116        // This arm has no `il` and every kernel in it fuses PLAIN silu(gate)*up, so a clamped
6117        // model must never reach it. The caller's gate at `moe_ffn_sequential_zq8` denies per
6118        // layer via `swiglu_clamped_at(il)`; assert the whole-model form here so a future caller
6119        // that forgets the gate fails loudly in debug instead of returning wrong logits.
6120        debug_assert!(
6121            !cfg.swiglu_clamped_anywhere(),
6122            "moe_ffn_pairs has no per-layer clamp: fused epilogues are plain SiLU"
6123        );
6124        let dev = m.dev_exps.as_ref().unwrap();
6125        // WALL-GAP ARC: interleaved gate/up slab strides (see moe_ffn_dev).
6126        let (rbg_d, rbu_d) = if dev.gu_il {
6127            let sxx = m.gate_exps.row_bytes + m.up_exps.row_bytes;
6128            (sxx, sxx)
6129        } else {
6130            (m.gate_exps.row_bytes, m.up_exps.row_bytes)
6131        };
6132
6133        let (sel_all, w_all) = Self::moe_route(e, logits, t, n_expert, n_used)?;
6134        let n_pairs = t * n_used;
6135        // pair arrays: pair p = (token p/n_used, slot p%n_used) — ALREADY slot-ordered per token,
6136        // so the CSR is trivial: tok_pair_off[tok] = tok*n_used, ids identity.
6137        let pair_tok: Vec<i32> = (0..n_pairs).map(|p| (p / n_used) as i32).collect();
6138        let pair_ex: Vec<i32> = sel_all.iter().map(|&x| x as i32).collect();
6139        let pair_w: Vec<f32> = w_all.clone();
6140        let tok_off: Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
6141        let tok_ids: Vec<i32> = (0..n_pairs as i32).collect();
6142        let pt = e.htod_i32(&pair_tok)?;
6143        let px = e.htod_i32(&pair_ex)?;
6144        let pw = e.htod(&pair_w)?;
6145        let toff = e.htod_i32(&tok_off)?;
6146        let tids = e.htod_i32(&tok_ids)?;
6147
6148        // z quantized ONCE for all tokens; gate/up pair matvecs; silu; act quantize; down; scatter.
6149        // EXPERT-MAJOR CSR (rung 2): pairs grouped by expert -> the kernel reuses each weight
6150        // row across the expert's token group (llama-MMQ's core win). Host grouping is O(pairs).
6151        let mut by_ex: Vec<Vec<i32>> = vec![Vec::new(); n_expert];
6152        for p in 0..n_pairs {
6153            by_ex[pair_ex[p] as usize].push(p as i32);
6154        }
6155        let mut ex_ids: Vec<i32> = Vec::new();
6156        let mut ex_off: Vec<i32> = vec![0];
6157        let mut ex_pairs: Vec<i32> = Vec::with_capacity(n_pairs);
6158        for (ex, list) in by_ex.iter().enumerate() {
6159            if list.is_empty() {
6160                continue;
6161            }
6162            ex_ids.push(ex as i32);
6163            ex_pairs.extend_from_slice(list);
6164            ex_off.push(ex_pairs.len() as i32);
6165        }
6166        let n_active = ex_ids.len();
6167        let exi = e.htod_i32(&ex_ids)?;
6168        let exo = e.htod_i32(&ex_off)?;
6169        let exp_d = e.htod_i32(&ex_pairs)?;
6170        let _ = &px; // pair-major twin keeps it; em path uses CSR
6171
6172        // INT8-MMA EXPERT MMQ (MEMRA_MOE_MMA=1, opt-in): the m16n8k16.s8 tensor-core analog of the
6173        // _dec dp4a kernel (cu/mmq_iq_experts.cu). Same CSR grouping; per-expert matvec runs as a
6174        // 128x128-tile int8 MMA GEMM over the expert's token group. Weight IQ nibbles decode to int8
6175        // at tile-load + per-32 float scale; activation is q8_1_mmq (D4, same quant class as dp4a).
6176        // FP-ORDER differs from dp4a (MMA reduction) — logits SHIFT, gated on argmax/spec/closeness,
6177        // NOT byte-identity (like the W4A8 path). Requires IQ3_S/IQ4_XS + in_f % 256 == 0.
6178        // t >= 16 (GEMM_M-class rule): the MMA tile needs token volume (crossover ~200 tok/expert;
6179        // microbench: dp4a wins at tiny groups). ALSO an exactness requirement — spec verify
6180        // batches (t=2..K+2) must ride the dp4a path whose FP order matches the T=1 decode chain,
6181        // else K=1 self-consistency FAILs (caught 2026-07-06: MMA at T=2 flipped a verify argmax).
6182        // DEFAULT ON (2026-07-06, third flip — this time with the real culprit fixed): the
6183        // "MMA prime breaks spec" failure was the ROUTER's cuBLASLt n-dependence (d994271),
6184        // not MMA's own FP order — both this and the k-quant arms were innocent suspects whose
6185        // margin shifts surfaced the router bug. With the router decode-exact at verify t, the
6186        // full battery is green with MMA on (spec p1/p2/p3 PASS, raw K=1..8 PASS, argmax MATCH,
6187        // pp6257 2862 = 2.1x dec). t>=16 floor still required: verify batches must ride dp4a
6188        // (dispatch parity with the T=1 decode chain). MEMRA_MOE_MMA=0 rollback;
6189        // MEMRA_MOE_MMA_T overrides the floor (bisect seam).
6190        static MMA_T: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
6191        let mma_t = *MMA_T.get_or_init(|| {
6192            std::env::var("MEMRA_MOE_MMA_T")
6193                .ok()
6194                .and_then(|v| v.parse().ok())
6195                .unwrap_or(16)
6196        });
6197        let use_mma = std::env::var("MEMRA_MOE_MMA")
6198            .map(|v| v != "0")
6199            .unwrap_or(true)
6200            && t >= mma_t
6201            && q8_expert_dec_supported(m.gate_exps.qtype)
6202            && q8_expert_dec_supported(m.up_exps.qtype)
6203            && q8_expert_dec_supported(m.down_exps.qtype)
6204            && n_embd % 256 == 0
6205            && n_ff_exp % 256 == 0;
6206        // GROUPED f16 LANE admission (MEMRA_MOE_F16G, rounds 46-49): its own door, no longer a
6207        // subset of the MMQ arm — q35's k-quant stragglers (Q6_K/Q4_K down, one Q3_K gate/up
6208        // layer) fail q8_expert_dec_supported but dequant fine to f16, so f16g must be able to
6209        // take a layer the MMQ arm would reject. Same t >= mma_t floor as MMA: decode and
6210        // spec-verify batches must ride the dp4a path whose FP order matches the T=1 chain.
6211        // MODE 2 (sm_120a naked default, lane/f16g-default-rearb 2026-08-02): every layer
6212        // whose three projections pass f16g_proj_ok rides the sk visitor with direct tile
6213        // loaders — with IQ4_XS/IQ3_S direct coverage the sk arm beats the int8-MMA MMQ
6214        // tiles on the IQ-bank models too (q35 board-2048 +33.9%, KAT pp512 +46.7%).
6215        // AUTO-KQUANT (mode 3, MEMRA_MOE_F16G=3, lane/q4k-expert-prefill): admit f16g ONLY
6216        // where the MMA arm can't take the layer's QTYPES — the k-quant expert class whose
6217        // baseline is the per-pair _em fallback (Ornith-35B Q4_K: board-2048 3.14x). Its
6218        // MMA-capable carve-out (IQ3_S/IQ4_XS/Q4_0 banks on MMQ) was priced pre-IQ-direct;
6219        // it survives as the rollback seam. Keyed on qtype capability, NOT use_mma, so
6220        // MEMRA_MOE_MMA=0 stays a pure dp4a rollback seam.
6221        let mma_capable = q8_expert_dec_supported(m.gate_exps.qtype)
6222            && q8_expert_dec_supported(m.up_exps.qtype)
6223            && q8_expert_dec_supported(m.down_exps.qtype)
6224            && n_embd % 256 == 0
6225            && n_ff_exp % 256 == 0;
6226        let f16g_mode = crate::moe_f16g_mode();
6227        let f16g = f16g_mode != 0
6228            && t >= mma_t
6229            && (f16g_mode != 3 || !mma_capable)
6230            && f16g_proj_ok(m.gate_exps.qtype, n_embd)
6231            && f16g_proj_ok(m.up_exps.qtype, n_embd)
6232            && f16g_proj_ok(m.down_exps.qtype, n_ff_exp);
6233        if use_mma || f16g {
6234            // GROUPED f16 LANE (MEMRA_MOE_F16G, rounds 46-49, experimental door): dequant
6235            // the active experts ONCE per projection to f16 and run one grouped f16 GEMM over
6236            // the CSR groups. f16-mirror numeric class — argmax/spec gated before promotion.
6237            // NOTE: the pair-gather kernel needs pair p ordered EXPERT-MAJOR (ex_pairs order);
6238            // the y rows come back in that same CSR order, so gate/up/down all stay pair-major
6239            // in ex_pairs order — but moe_pairs_silu_mul and the scatter consume PAIR-ID order.
6240            // We therefore gather activations per ex_pairs and scatter y back through ex_pairs.
6241            let y_down = if f16g {
6242                // CSR order end-to-end: gather z rows by the pair's TOKEN (pair p's token is
6243                // p / n_used — the trivial CSR above), silu in CSR order (elementwise), one
6244                // permute at the very end back to pair-id order for the scatter.
6245                let csr_tok: Vec<i32> = ex_pairs.iter().map(|&p| p / n_used as i32).collect();
6246                let csr_tok_d = e.htod_i32(&csr_tok)?;
6247                let (z_f16, z_s) = e.moe_f16g_act(z, Some(&csr_tok_d), n_embd, n_pairs)?;
6248                let g_csr = e.moe_f16_grouped(
6249                    &dev.ptr_row,
6250                    0,
6251                    n_expert,
6252                    &exi,
6253                    &ex_off,
6254                    &exo,
6255                    &z_f16,
6256                    &z_s,
6257                    n_embd,
6258                    n_ff_exp,
6259                    n_active,
6260                    n_pairs,
6261                    m.gate_exps.qtype,
6262                    rbg_d,
6263                )?;
6264                let u_csr = e.moe_f16_grouped(
6265                    &dev.ptr_row,
6266                    1,
6267                    n_expert,
6268                    &exi,
6269                    &ex_off,
6270                    &exo,
6271                    &z_f16,
6272                    &z_s,
6273                    n_embd,
6274                    n_ff_exp,
6275                    n_active,
6276                    n_pairs,
6277                    m.up_exps.qtype,
6278                    rbu_d,
6279                )?;
6280                let act_csr = e.moe_pairs_silu_mul(&g_csr, &u_csr, n_pairs * n_ff_exp)?;
6281                let (a_f16, a_s) = e.moe_f16g_act(&act_csr, None, n_ff_exp, n_pairs)?;
6282                let d_csr = e.moe_f16_grouped(
6283                    &dev.ptr_row,
6284                    2,
6285                    n_expert,
6286                    &exi,
6287                    &ex_off,
6288                    &exo,
6289                    &a_f16,
6290                    &a_s,
6291                    n_ff_exp,
6292                    n_embd,
6293                    n_active,
6294                    n_pairs,
6295                    m.down_exps.qtype,
6296                    m.down_exps.row_bytes,
6297                )?;
6298                e.rows_permute(&d_csr, &exp_d, n_pairs, n_embd)?
6299            } else {
6300                // gate/up: activation = z, token-major over t tokens; pair_tok gathers the routed row.
6301                let z_scr = e.mmq_iq_quantize_act(z, n_embd, t)?;
6302                let gate = e.mmq_iq_experts(
6303                    &dev.ptr_row,
6304                    0,
6305                    n_expert,
6306                    &exi,
6307                    &exo,
6308                    &exp_d,
6309                    &pt,
6310                    &z_scr,
6311                    n_embd,
6312                    n_ff_exp,
6313                    n_active,
6314                    n_pairs,
6315                    t,
6316                    m.gate_exps.qtype,
6317                    rbg_d,
6318                )?;
6319                let up = e.mmq_iq_experts(
6320                    &dev.ptr_row,
6321                    1,
6322                    n_expert,
6323                    &exi,
6324                    &exo,
6325                    &exp_d,
6326                    &pt,
6327                    &z_scr,
6328                    n_embd,
6329                    n_ff_exp,
6330                    n_active,
6331                    n_pairs,
6332                    t,
6333                    m.up_exps.qtype,
6334                    rbu_d,
6335                )?;
6336                // down: activation = silu(gate)*up, pair-major [n_pairs, n_ff_exp]; pair_tok =
6337                // identity. FUSED ACT-EPILOGUE (default on): one launch computes the activation in
6338                // registers and writes ONLY the quantized scratch — the two-pass chain
6339                // (moe_pairs_silu_mul writes act f32, mmq_iq_quantize_act re-reads it) is the
6340                // MEMRA_MOE_FUSE_ACTQ=0 rollback. Scratch bytes are BYTE-IDENTICAL (kernel-check).
6341                let a_scr = if crate::moe_fuse_actq_on() {
6342                    e.mmq_iq_fused_act_quant(&gate, &up, n_ff_exp, n_pairs, 0)?
6343                } else {
6344                    let act = e.moe_pairs_silu_mul(&gate, &up, n_pairs * n_ff_exp)?;
6345                    e.mmq_iq_quantize_act(&act, n_ff_exp, n_pairs)?
6346                };
6347                let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
6348                let pself = e.htod_i32(&pair_self)?;
6349                e.mmq_iq_experts(
6350                    &dev.ptr_row,
6351                    2,
6352                    n_expert,
6353                    &exi,
6354                    &exo,
6355                    &exp_d,
6356                    &pself,
6357                    &a_scr,
6358                    n_ff_exp,
6359                    n_embd,
6360                    n_active,
6361                    n_pairs,
6362                    n_pairs,
6363                    m.down_exps.qtype,
6364                    m.down_exps.row_bytes,
6365                )?
6366            };
6367            let mut moe_out = e.uninit(t * n_embd)?;
6368            e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
6369            if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
6370                (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
6371            {
6372                let n_ff_sh = gate_shexp.out_features();
6373                let sg_gate = e.matmul(gate_shexp, z, t)?;
6374                let sg_up = e.matmul(up_shexp, z, t)?;
6375                let mut sa = e.uninit(t * n_ff_sh)?;
6376                Self::ffn_act(e, cfg, &sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
6377                let sh = e.matmul(down_shexp, &sa, t)?;
6378                // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
6379                // SERVE ISOLATION (lane/concat-prime-exact): the fused sigmoid-dot is the
6380                // m-INVARIANT form — see the sequential arm's note. This is the PAIRS arm,
6381                // i.e. the one real prefill actually takes on a resident-expert MoE model,
6382                // so the concat-prime isolation fix has to land here as well.
6383                let g = match &m.gate_inp_shexp {
6384                    Some(gate_inp_shexp) if crate::router_prefill_exact_on() => {
6385                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
6386                    }
6387                    Some(gate_inp_shexp) => {
6388                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
6389                        let mut g = e.uninit(t)?;
6390                        e.sigmoid(&gs, &mut g, t)?;
6391                        g
6392                    }
6393                    None => e.htod(&vec![1.0f32; t])?,
6394                };
6395                e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
6396            }
6397            return Ok(moe_out);
6398        }
6399
6400        // DECODE-ONCE MMQ (rung 3, MEMRA_MOE_DEC=1 default-on): dequant each weight group once per
6401        // (row,group) then dp4a across the expert's tokens. _em re-decoded per token (NEUTRAL).
6402        let dec = std::env::var("MEMRA_MOE_DEC")
6403            .map(|v| v != "0")
6404            .unwrap_or(true);
6405        let matvec = |proj,
6406                      exi: &_,
6407                      exo: &_,
6408                      exp_d: &_,
6409                      pt: &_,
6410                      aq: &_,
6411                      ad: &_,
6412                      inf,
6413                      outf,
6414                      qtype,
6415                      rb|
6416         -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6417            // _dec's decode-once extractors are IQ-only; k-quant expert layers take the _em dot path.
6418            let dec = dec && q8_expert_dec_supported(qtype);
6419            if dec {
6420                e.moe_pairs_matvec_q8_dec(
6421                    &dev.ptr_row,
6422                    proj,
6423                    exi,
6424                    exo,
6425                    exp_d,
6426                    pt,
6427                    aq,
6428                    ad,
6429                    inf,
6430                    outf,
6431                    n_expert,
6432                    n_active,
6433                    n_pairs,
6434                    qtype,
6435                    rb,
6436                )
6437            } else {
6438                e.moe_pairs_matvec_q8_em(
6439                    &dev.ptr_row,
6440                    proj,
6441                    exi,
6442                    exo,
6443                    exp_d,
6444                    pt,
6445                    aq,
6446                    ad,
6447                    inf,
6448                    outf,
6449                    n_expert,
6450                    n_active,
6451                    n_pairs,
6452                    qtype,
6453                    rb,
6454                )
6455            }
6456        };
6457        let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
6458        let gate = matvec(
6459            0,
6460            &exi,
6461            &exo,
6462            &exp_d,
6463            &pt,
6464            &zq,
6465            &zd,
6466            n_embd,
6467            n_ff_exp,
6468            m.gate_exps.qtype,
6469            rbg_d,
6470        )?;
6471        let up = matvec(
6472            1,
6473            &exi,
6474            &exo,
6475            &exp_d,
6476            &pt,
6477            &zq,
6478            &zd,
6479            n_embd,
6480            n_ff_exp,
6481            m.up_exps.qtype,
6482            rbu_d,
6483        )?;
6484        let act = e.moe_pairs_silu_mul(&gate, &up, n_pairs * n_ff_exp)?;
6485        let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
6486        // down consumes PAIR-major activation rows: pair_tok = identity.
6487        let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
6488        let pself = e.htod_i32(&pair_self)?;
6489        let y_down = matvec(
6490            2,
6491            &exi,
6492            &exo,
6493            &exp_d,
6494            &pself,
6495            &aq2,
6496            &ad2,
6497            n_ff_exp,
6498            n_embd,
6499            m.down_exps.qtype,
6500            m.down_exps.row_bytes,
6501        )?;
6502        let mut moe_out = e.uninit(t * n_embd)?; // scatter fully overwrites per (token,col)
6503        e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
6504
6505        // SHARED EXPERT epilogue — same as the other paths.
6506        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
6507        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
6508        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
6509            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
6510        {
6511            let n_ff_sh = gate_shexp.out_features();
6512            // These decode-exact forms are required by the new Step resident arm. Keep the
6513            // established grouped shared-expert program for every other architecture: widening
6514            // this to Gemma changed its speculative acceptance despite green argmax gates.
6515            let step_exact = cfg.step35.is_some();
6516            let verify_t = step_exact && t > 1 && t < PRIME_MIN_T;
6517            let (sg_gate, sg_up) = if step_exact && t == 1 {
6518                match e.matmul_q8_fused2_x(gate_shexp, up_shexp, z)? {
6519                    Some(pair) => pair,
6520                    None => (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?),
6521                }
6522            } else if verify_t {
6523                let mut fused = None;
6524                if crate::spec::spec_fused_t()
6525                    && (2..=4).contains(&t)
6526                    && e.uses_q8_1_fast(gate_shexp)
6527                    && e.uses_q8_1_fast(up_shexp)
6528                {
6529                    let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
6530                    fused = e.matmul_q8_fused2_t(gate_shexp, up_shexp, &zq, &zd, t)?;
6531                }
6532                match fused {
6533                    Some(pair) => pair,
6534                    None => (
6535                        e.matmul_decode_exact(gate_shexp, z, t)?,
6536                        e.matmul_decode_exact(up_shexp, z, t)?,
6537                    ),
6538                }
6539            } else {
6540                (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?)
6541            };
6542            let mut sa = e.uninit(t * n_ff_sh)?;
6543            e.silu_mul(&sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
6544            let sh = if verify_t {
6545                e.matmul_decode_exact(down_shexp, &sa, t)?
6546            } else {
6547                e.matmul(down_shexp, &sa, t)?
6548            };
6549            // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
6550            // m-INVARIANT fused sigmoid-dot under router_prefill_exact (serve isolation,
6551            // lane/concat-prime-exact) — every shexp-gate arm shares the same form so a
6552            // dispatch choice cannot change bits.
6553            let g = match &m.gate_inp_shexp {
6554                Some(gate_inp_shexp) if crate::router_prefill_exact_on() => {
6555                    e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
6556                }
6557                Some(gate_inp_shexp) => {
6558                    let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
6559                    let mut g = e.uninit(t)?;
6560                    e.sigmoid(&gs, &mut g, t)?;
6561                    g
6562                }
6563                None => e.htod(&vec![1.0f32; t])?,
6564            };
6565            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
6566        }
6567        Ok(moe_out)
6568    }
6569
6570    /// kernel headers); the shared-expert epilogue is byte-identical to moe_ffn_sequential's.
6571    #[allow(clippy::too_many_arguments)]
6572    #[allow(clippy::too_many_arguments)]
6573    fn moe_ffn_dev(
6574        e: &Engine,
6575        m: &MoeWeights,
6576        z: &CudaSlice<f32>,
6577        zq8: Option<&(CudaSlice<i8>, CudaSlice<f32>)>,
6578        logits: &CudaSlice<f32>,
6579        t: usize,
6580        cfg: &ModelConfig,
6581        il: u16,
6582        max_block: usize,
6583    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
6584        let moe = cfg.moe.as_ref().unwrap();
6585        let n_embd = cfg.n_embd as usize;
6586        let n_expert = moe.expert_count as usize;
6587        let n_used = moe.expert_used_count as usize;
6588        let n_ff_exp = moe.expert_ff_length as usize;
6589        // moe_router_topk is SOFTMAX-only (no exp_probs_b bias, no expert_weights_scale) and every
6590        // gate_up kernel here fuses PLAIN silu(gate)*up. `dev_ok` denies sigmoid-router archs and
6591        // clamped layers; assert both so a future caller that skips the gate fails loudly.
6592        debug_assert!(
6593            cfg.sigmoid_router().is_none(),
6594            "moe_ffn_dev routes SOFTMAX: a sigmoid-router arch would pick wrong experts"
6595        );
6596        debug_assert!(
6597            !cfg.swiglu_clamped_at(il as u32),
6598            "moe_ffn_dev's fused epilogue is plain SiLU: no clamped form"
6599        );
6600
6601        // device top-k: sel [t, n_used] i32, w [t, n_used] f32 — stays on device.
6602        let (sel_d, mut w_d) = e.moe_router_topk(logits, t, n_expert, n_used)?;
6603        // Down-projection macro fold (compressed-tensors NVFP4 artifacts): one tiny launch,
6604        // skipped entirely for macro-free experts (every k-quant GGUF).
6605        if m.has_macros {
6606            e.moe_w_scale_by_expert(&mut w_d, &sel_d, &m.dev_macros, n_expert, t * n_used)?;
6607        }
6608
6609        // moe_out rows are FULLY overwritten by moe_down8_fma_dev — uninit (stage-2 rule).
6610        let mut moe_out = e.uninit(t * n_embd)?;
6611
6612        // RESIDENT-EXPERTS arm: the pointer row comes from the load-time slab (no cache, no
6613        // lock). Same kernels/loop as the SLRU arm below — only the row's provenance differs.
6614        if let Some(dev) = m.dev_exps.as_ref() {
6615            // WALL-GAP ARC: interleaved gate/up slab (MEMRA_MOE_GU_IL) -> both projections use
6616            // the combined stride; up's base is offset in the ptr table. Down unchanged.
6617            let (rbg_d, rbu_d) = if dev.gu_il {
6618                let sxx = m.gate_exps.row_bytes + m.up_exps.row_bytes;
6619                (sxx, sxx)
6620            } else {
6621                (m.gate_exps.row_bytes, m.up_exps.row_bytes)
6622            };
6623            let q8 = moe_q8_enabled()
6624                && q8_expert_supported(m.gate_exps.qtype)
6625                && q8_expert_supported(m.up_exps.qtype)
6626                && q8_expert_supported(m.down_exps.qtype);
6627            // SMALL-M ROWS ARM (MEMRA_SPEC_M2, lane/spec-m2): batch the verify token loop —
6628            // ONE batched z-quantize + ONE gate_up rows launch + ONE act quantize + ONE down
6629            // rows launch (4 launches/layer, was 4t). BIT-IDENTICAL per token to the serial
6630            // loop below (rows twins = the _v/w8h2v per-token programs on a grid.z token axis;
6631            // quantize_q8_1 is per-32-block row-independent). Gated to the AUTO kernel modes —
6632            // a custom MEMRA_MOE_DEVQ8_GU/DOWN diagnostic run keeps the serial loop so the
6633            // dispatched kernel stays exactly the env-selected one — and to the w8h2v shape
6634            // (n_ff_exp==512, n_used<=8), the same contract the AUTO down dispatch keys on.
6635            let rows_arm = q8
6636                && t > 1
6637                && crate::spec::spec_m2()
6638                && n_ff_exp == 512
6639                && n_used <= 8
6640                && std::env::var("MEMRA_MOE_DEVQ8_GU")
6641                    .map(|v| v.is_empty() || v == "v")
6642                    .unwrap_or(true)
6643                && std::env::var("MEMRA_MOE_DEVQ8_DOWN")
6644                    .map(|v| v.is_empty() || v == "w8h2v")
6645                    .unwrap_or(true);
6646            // CSR EXPERT-DEDUP gate_up (verify-cost target #1, DEFAULT ON 2026-07-10): the
6647            // owner-scan kernel serves every (token, slot) pair of an expert from ONE block,
6648            // deduping the 38-40% duplicated weight-stream+decode the overlap probe measured
6649            // (gate_up 55.0 -> 39.7us/launch; +1.3-2.1% spec e2e p2, +0.6-1.7% p3, all K).
6650            // Bit-identical to the _rows twins (explicit-intrinsic accumulate — the ULP/fmad
6651            // lesson; =2 byte-compare verified zero diffs). down stays on _rows (CSR down
6652            // measured 23.5 -> 37.5us: 16-group rows can't amortize the serial pair loop).
6653            // MEMRA_MOE_CSR=0 rollback; =2 runs BOTH paths and byte-compares (debug).
6654            let csr_mode = std::env::var("MEMRA_MOE_CSR")
6655                .ok()
6656                .and_then(|v| v.parse::<i32>().ok())
6657                .unwrap_or(1);
6658            let csr_qt = |qt: i32| qt == crate::QT_IQ4_XS || qt == crate::QT_IQ3_S;
6659            let csr_arm = rows_arm
6660                && csr_mode > 0
6661                && t <= 10
6662                && csr_qt(m.gate_exps.qtype)
6663                && csr_qt(m.up_exps.qtype)
6664                && csr_qt(m.down_exps.qtype);
6665            if csr_arm {
6666                if csr_mode == 2 {
6667                    static ENGAGED: std::sync::Once = std::sync::Once::new();
6668                    ENGAGED.call_once(|| eprintln!("[memra] moe CSR byte-compare mode ON (t={t})"));
6669                }
6670                let n_pairs = t * n_used;
6671                let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
6672                let act = e.moe_gate_up_silu8_dev_q8_csr(
6673                    &dev.ptr_row,
6674                    &sel_d,
6675                    &zq,
6676                    &zd,
6677                    n_pairs,
6678                    n_embd,
6679                    n_ff_exp,
6680                    n_used,
6681                    n_expert,
6682                    m.gate_exps.qtype,
6683                    m.up_exps.qtype,
6684                    rbg_d,
6685                    rbu_d,
6686                )?;
6687                let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
6688                // down stays on the _rows twin — BOTH CSR down variants measured negative
6689                // (v1 serial pairs 23.5->37.5us; v2 warp-parallel+SMEM -14% e2e, K=8 -37%):
6690                // 16-group rows have too little decode to amortize any dedup structure.
6691                e.moe_down8_fma_dev_q8_rows(
6692                    &dev.ptr_row,
6693                    &sel_d,
6694                    &w_d,
6695                    &aq2,
6696                    &ad2,
6697                    &mut moe_out,
6698                    t,
6699                    n_ff_exp,
6700                    n_embd,
6701                    n_used,
6702                    n_expert,
6703                    m.down_exps.qtype,
6704                    m.down_exps.row_bytes,
6705                )?;
6706                if csr_mode == 2 {
6707                    // DEBUG BYTE-COMPARE: run the _rows twins on the same inputs, diff bits.
6708                    let act_r = e.moe_gate_up_silu8_dev_q8_rows(
6709                        &dev.ptr_row,
6710                        &sel_d,
6711                        &zq,
6712                        &zd,
6713                        t,
6714                        n_embd,
6715                        n_ff_exp,
6716                        n_used,
6717                        n_expert,
6718                        m.gate_exps.qtype,
6719                        m.up_exps.qtype,
6720                        rbg_d,
6721                        rbu_d,
6722                        &m.dev_macros,
6723                    )?;
6724                    let mut out_r = e.uninit(t * n_embd)?;
6725                    let (aq2r, ad2r) = e.quantize_q8_1(&act_r, n_pairs, n_ff_exp)?;
6726                    e.moe_down8_fma_dev_q8_rows(
6727                        &dev.ptr_row,
6728                        &sel_d,
6729                        &w_d,
6730                        &aq2r,
6731                        &ad2r,
6732                        &mut out_r,
6733                        t,
6734                        n_ff_exp,
6735                        n_embd,
6736                        n_used,
6737                        n_expert,
6738                        m.down_exps.qtype,
6739                        m.down_exps.row_bytes,
6740                    )?;
6741                    let (a1, a2) = (e.dtoh(&act)?, e.dtoh(&act_r)?);
6742                    let (o1, o2) = (e.dtoh(&moe_out)?, e.dtoh(&out_r)?);
6743                    let ba = a1
6744                        .iter()
6745                        .zip(&a2)
6746                        .filter(|(x, y)| x.to_bits() != y.to_bits())
6747                        .count();
6748                    let bo = o1
6749                        .iter()
6750                        .zip(&o2)
6751                        .filter(|(x, y)| x.to_bits() != y.to_bits())
6752                        .count();
6753                    if ba + bo > 0 {
6754                        eprintln!(
6755                            "[csr-check] il={il} t={t} ACT diffs={ba}/{} OUT diffs={bo}/{}",
6756                            a1.len(),
6757                            o1.len()
6758                        );
6759                        // First 4 differing ACT elements: (pair, o, csr, rows) + that pair's expert
6760                        let sel_h = e.dtoh_i32(&sel_d)?;
6761                        let mut shown = 0;
6762                        for (i, (x, y)) in a1.iter().zip(&a2).enumerate() {
6763                            if x.to_bits() != y.to_bits() && shown < 4 {
6764                                let (p, o) = (i / n_ff_exp, i % n_ff_exp);
6765                                let ex = sel_h[p];
6766                                let npx = sel_h.iter().filter(|&&v| v == ex).count();
6767                                eprintln!(
6768                                    "  ACT p={p} ex={ex} np={npx} o={o} csr={x:e} rows={y:e}"
6769                                );
6770                                shown += 1;
6771                            }
6772                        }
6773                        std::process::exit(3);
6774                    }
6775                }
6776            } else if rows_arm {
6777                // TEMP PROBE (MEMRA_MOE_OVERLAP=1): cross-token expert-activation overlap at
6778                // verify — sizes the CSR dedup win (unique experts vs t*n_used pairs).
6779                if std::env::var("MEMRA_MOE_OVERLAP").as_deref() == Ok("1") {
6780                    use std::sync::atomic::{AtomicU64, Ordering};
6781                    static PAIRS: AtomicU64 = AtomicU64::new(0);
6782                    static UNIQ: AtomicU64 = AtomicU64::new(0);
6783                    static CALLS: AtomicU64 = AtomicU64::new(0);
6784                    let sel_h = e.dtoh_i32(&sel_d)?;
6785                    let mut u: Vec<i32> = sel_h.clone();
6786                    u.sort_unstable();
6787                    u.dedup();
6788                    PAIRS.fetch_add(sel_h.len() as u64, Ordering::Relaxed);
6789                    UNIQ.fetch_add(u.len() as u64, Ordering::Relaxed);
6790                    let c = CALLS.fetch_add(1, Ordering::Relaxed) + 1;
6791                    if c % 480 == 0 {
6792                        let p = PAIRS.load(Ordering::Relaxed);
6793                        let q = UNIQ.load(Ordering::Relaxed);
6794                        eprintln!(
6795                            "[overlap] calls={c} pairs={p} unique={q} ratio={:.3} (t={t})",
6796                            q as f64 / p as f64
6797                        );
6798                    }
6799                }
6800                let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
6801                let act = e.moe_gate_up_silu8_dev_q8_rows(
6802                    &dev.ptr_row,
6803                    &sel_d,
6804                    &zq,
6805                    &zd,
6806                    t,
6807                    n_embd,
6808                    n_ff_exp,
6809                    n_used,
6810                    n_expert,
6811                    m.gate_exps.qtype,
6812                    m.up_exps.qtype,
6813                    rbg_d,
6814                    rbu_d,
6815                    &m.dev_macros,
6816                )?;
6817                let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
6818                e.moe_down8_fma_dev_q8_rows(
6819                    &dev.ptr_row,
6820                    &sel_d,
6821                    &w_d,
6822                    &aq2,
6823                    &ad2,
6824                    &mut moe_out,
6825                    t,
6826                    n_ff_exp,
6827                    n_embd,
6828                    n_used,
6829                    n_expert,
6830                    m.down_exps.qtype,
6831                    m.down_exps.row_bytes,
6832                )?;
6833            } else {
6834                for tok in 0..t {
6835                    let zt = z.slice(tok * n_embd..(tok + 1) * n_embd);
6836                    let selt = sel_d.slice(tok * n_used..(tok + 1) * n_used);
6837                    let wt = w_d.slice(tok * n_used..(tok + 1) * n_used);
6838                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
6839                    if q8 {
6840                        let (zq, zd) = match (t, zq8) {
6841                            (1, Some((q, d))) => (q.clone(), d.clone()),
6842                            _ => e.quantize_q8_1_view(&zt, 1, n_embd)?,
6843                        };
6844                        let act = e.moe_gate_up_silu8_dev_q8(
6845                            &dev.ptr_row,
6846                            &selt,
6847                            &zq,
6848                            &zd,
6849                            n_embd,
6850                            n_ff_exp,
6851                            n_used,
6852                            n_expert,
6853                            m.gate_exps.qtype,
6854                            m.up_exps.qtype,
6855                            rbg_d,
6856                            rbu_d,
6857                            &m.dev_macros,
6858                        )?;
6859                        let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
6860                        e.moe_down8_fma_dev_q8(
6861                            &dev.ptr_row,
6862                            &selt,
6863                            &wt,
6864                            &aq2,
6865                            &ad2,
6866                            &mut dst,
6867                            n_ff_exp,
6868                            n_embd,
6869                            n_used,
6870                            n_expert,
6871                            m.down_exps.qtype,
6872                            m.down_exps.row_bytes,
6873                        )?;
6874                    } else {
6875                        let act = e.moe_gate_up_silu8_dev(
6876                            &dev.ptr_row,
6877                            &selt,
6878                            &zt,
6879                            n_embd,
6880                            n_ff_exp,
6881                            n_used,
6882                            n_expert,
6883                            m.gate_exps.qtype,
6884                            m.up_exps.qtype,
6885                            rbg_d,
6886                            rbu_d,
6887                            &m.dev_macros,
6888                        )?;
6889                        e.moe_down8_fma_dev(
6890                            &dev.ptr_row,
6891                            &selt,
6892                            &wt,
6893                            &act,
6894                            &mut dst,
6895                            n_ff_exp,
6896                            n_embd,
6897                            n_used,
6898                            n_expert,
6899                            m.down_exps.qtype,
6900                            m.down_exps.row_bytes,
6901                        )?;
6902                    }
6903                }
6904            }
6905        } else {
6906            // Launch under the cache lock: the row borrow lives as long as the closure, and the
6907            // lock covers only launch ISSUE (µs), same policy as moe_cached_gemm.
6908            // Q8 ARM PARITY (2026-07-06): the SLRU arm ran the f32-dequant _dev kernels only —
6909            // 80us/launch vs the q8 twins' 15us on the SAME shapes (fixed-build profile: 228
6910            // f32 launches = 36ms of the 64-tok window). Same q8 gate + kernels as the resident
6911            // arm above; MEMRA_MOE_Q8=0 restores the byte-identical f32 path.
6912            let q8 = moe_q8_enabled()
6913                && q8_expert_supported(m.gate_exps.qtype)
6914                && q8_expert_supported(m.up_exps.qtype)
6915                && q8_expert_supported(m.down_exps.qtype);
6916            e.with_moe_cache(max_block, |c, eng| {
6917                let row = c
6918                    .layer_dev_row(il, n_expert, eng)?
6919                    .ok_or("moe_ffn_dev: layer row vanished under the lock")?;
6920                for tok in 0..t {
6921                    let zt = z.slice(tok * n_embd..(tok + 1) * n_embd);
6922                    let selt = sel_d.slice(tok * n_used..(tok + 1) * n_used);
6923                    let wt = w_d.slice(tok * n_used..(tok + 1) * n_used);
6924                    let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
6925                    if q8 {
6926                        let (zq, zd) = match (t, zq8) {
6927                            (1, Some((q, d))) => (q.clone(), d.clone()),
6928                            _ => eng.quantize_q8_1_view(&zt, 1, n_embd)?,
6929                        };
6930                        let act = eng.moe_gate_up_silu8_dev_q8(
6931                            row,
6932                            &selt,
6933                            &zq,
6934                            &zd,
6935                            n_embd,
6936                            n_ff_exp,
6937                            n_used,
6938                            n_expert,
6939                            m.gate_exps.qtype,
6940                            m.up_exps.qtype,
6941                            m.gate_exps.row_bytes,
6942                            m.up_exps.row_bytes,
6943                            &m.dev_macros,
6944                        )?;
6945                        let (aq2, ad2) = eng.quantize_q8_1(&act, n_used, n_ff_exp)?;
6946                        eng.moe_down8_fma_dev_q8(
6947                            row,
6948                            &selt,
6949                            &wt,
6950                            &aq2,
6951                            &ad2,
6952                            &mut dst,
6953                            n_ff_exp,
6954                            n_embd,
6955                            n_used,
6956                            n_expert,
6957                            m.down_exps.qtype,
6958                            m.down_exps.row_bytes,
6959                        )?;
6960                    } else {
6961                        let act = eng.moe_gate_up_silu8_dev(
6962                            row,
6963                            &selt,
6964                            &zt,
6965                            n_embd,
6966                            n_ff_exp,
6967                            n_used,
6968                            n_expert,
6969                            m.gate_exps.qtype,
6970                            m.up_exps.qtype,
6971                            m.gate_exps.row_bytes,
6972                            m.up_exps.row_bytes,
6973                            &m.dev_macros,
6974                        )?;
6975                        eng.moe_down8_fma_dev(
6976                            row,
6977                            &selt,
6978                            &wt,
6979                            &act,
6980                            &mut dst,
6981                            n_ff_exp,
6982                            n_embd,
6983                            n_used,
6984                            n_expert,
6985                            m.down_exps.qtype,
6986                            m.down_exps.row_bytes,
6987                        )?;
6988                    }
6989                }
6990                // instrumentation parity with the host paths (3 blocks/expert-slot, all hits).
6991                c.hits += (t * 3 * n_used) as u64;
6992                Ok(())
6993            })?;
6994        }
6995
6996        // SHARED EXPERT epilogue — byte-identical to moe_ffn_sequential step 3 (incl. its Q8
6997        // TRUNK-FUSION arm: fused2 is bit-identical to the two matmul calls per (tensor,row)).
6998        // gate_inp_shexp is OPTIONAL: qwen35moe gates the shared expert (sigmoid(gate_inp) x sh);
6999        // MiniMax-M3 (DeepSeek-V3 class) has NO shexp gate — the shared expert adds directly.
7000        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
7001            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
7002        {
7003            let n_ff_sh = gate_shexp.out_features();
7004            // verify-t (2..15) decode-exact arm: this fn now serves the spec verify batches
7005            // (pairs gate moved to t>=PRIME_MIN_T), so the shexp chain must match t==1 per col.
7006            let verify_t = t > 1 && t < PRIME_MIN_T;
7007            let (sg_gate, sg_up) = if t == 1 {
7008                match e.matmul_q8_fused2_x(gate_shexp, up_shexp, z)? {
7009                    Some(pair) => pair,
7010                    None => (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?),
7011                }
7012            } else if verify_t {
7013                // VERIFY-TIER TRUNK FUSION (MEMRA_SPEC_FUSED_T, t=2-4): the shexp gate+up pair
7014                // rides one shared quantize + one fused2 batched launch instead of two
7015                // decode-exact calls. Bit-identical per (tensor,token,row) — see spec_fused_t.
7016                let mut fused = None;
7017                if crate::spec::spec_fused_t()
7018                    && (2..=4).contains(&t)
7019                    && e.uses_q8_1_fast(gate_shexp)
7020                    && e.uses_q8_1_fast(up_shexp)
7021                {
7022                    let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
7023                    fused = e.matmul_q8_fused2_t(gate_shexp, up_shexp, &zq, &zd, t)?;
7024                }
7025                match fused {
7026                    Some(pair) => pair,
7027                    None => (
7028                        e.matmul_decode_exact(gate_shexp, z, t)?,
7029                        e.matmul_decode_exact(up_shexp, z, t)?,
7030                    ),
7031                }
7032            } else {
7033                (e.matmul(gate_shexp, z, t)?, e.matmul(up_shexp, z, t)?)
7034            };
7035            let mut sa = e.uninit(t * n_ff_sh)?; // silu_mul fully overwrites
7036            e.silu_mul(&sg_gate, &sg_up, &mut sa, t * n_ff_sh)?;
7037            let sh = if verify_t {
7038                e.matmul_decode_exact(down_shexp, &sa, t)?
7039            } else {
7040                e.matmul(down_shexp, &sa, t)?
7041            };
7042            // shexp gate: qwen35moe sigmoid-gates; M3 has no gate tensor -> weight 1.0.
7043            // Same fused sigmoid-dot as moe_ffn_sequential step 3 (byte-identity contract
7044            // between the two arms; prefill keeps the batched cuBLASLt linear).
7045            let g = match &m.gate_inp_shexp {
7046                Some(gate_inp_shexp) => {
7047                    // router_prefill_exact_on(): the fused sigmoid-dot is m-INVARIANT and
7048                    // serves EVERY t (serve isolation, lane/concat-prime-exact).
7049                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
7050                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
7051                    } else {
7052                        let gs = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
7053                        let mut g = e.uninit(t)?;
7054                        e.sigmoid(&gs, &mut g, t)?;
7055                        g
7056                    }
7057                }
7058                None => e.htod(&vec![1.0f32; t])?,
7059            };
7060            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, t)?;
7061        }
7062
7063        Ok(moe_out)
7064    }
7065
7066    /// STAGE-2 GROUPED DECODE (2026-07-04): run ONE token's whole routed-expert FFN in TWO
7067    /// launches when every one of its 3*n_used blocks is ALREADY cache-resident. Returns
7068    /// Ok(true) if the grouped path ran (caller skips the sequential loop for this token);
7069    /// Ok(false) on ANY miss (caller falls through — the sequential loop stages/admits as
7070    /// before, so the NEXT occurrence takes the grouped path). Pointer safety: cache slots are
7071    /// fixed-address for the engine's lifetime and the pure-HIT path performs no admission, so
7072    /// the collected raw pointers cannot move between collection and launch (single-threaded
7073    /// decode; the lock is held only for collection, launches are stream-ordered after any
7074    /// prior same-stream staging writes).
7075    #[allow(clippy::too_many_arguments)]
7076    /// q8 twin of moe_gdec_token (dp4a arc): same residency check + 2-launch shape; the fused
7077    /// kernels consume the pre-quantized z-row and re-quantize act per slot batch.
7078    #[allow(clippy::too_many_arguments)]
7079    fn moe_gdec_token_q8(
7080        e: &Engine,
7081        m: &MoeWeights,
7082        il: u16,
7083        max_block: usize,
7084        zq: &CudaSlice<i8>,
7085        zd: &CudaSlice<f32>,
7086        sel: &[u32],
7087        w: &[f32],
7088        moe_out: &mut CudaSlice<f32>,
7089        tok: usize,
7090        n_embd: usize,
7091        n_ff_exp: usize,
7092        n_used: usize,
7093    ) -> Result<bool, Box<dyn std::error::Error>> {
7094        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7095        use cudarc::driver::DevicePtr;
7096        let ptrs = e.with_moe_cache(max_block, |c, eng| {
7097            let mut g = [0u64; 8];
7098            let mut u = [0u64; 8];
7099            let mut d = [0u64; 8];
7100            for (j, &ex) in sel.iter().enumerate() {
7101                let ex = ex as u16;
7102                let (Some(sg), Some(su), Some(sd)) = (
7103                    c.resident(BlockId::new(il, PROJ_GATE, ex)),
7104                    c.resident(BlockId::new(il, PROJ_UP, ex)),
7105                    c.resident(BlockId::new(il, PROJ_DOWN, ex)),
7106                ) else {
7107                    return Ok(None);
7108                };
7109                let __s = eng.stream();
7110                let (pg, _e0) = c.slot(sg).device_ptr(&__s);
7111                let (pu, _e1) = c.slot(su).device_ptr(&__s);
7112                let (pd, _e2) = c.slot(sd).device_ptr(&__s);
7113                g[j] = pg as u64;
7114                u[j] = pu as u64;
7115                d[j] = pd as u64;
7116            }
7117            if cpu_expert_profile_admit_enabled() && !c.is_frozen() {
7118                for &ex in sel {
7119                    let ex = ex as u16;
7120                    for proj in [PROJ_GATE, PROJ_UP, PROJ_DOWN] {
7121                        c.note_profile_hit(BlockId::new(il, proj, ex));
7122                    }
7123                }
7124            }
7125            c.hits += (3 * n_used) as u64;
7126            Ok(Some((g, u, d)))
7127        })?;
7128        let Some((g, u, d)) = ptrs else {
7129            return Ok(false);
7130        };
7131        let mut wv = [0f32; 8];
7132        wv[..n_used].copy_from_slice(w);
7133        let act = e.moe_gate_up_silu8_q8(
7134            crate::WPtr8(g),
7135            crate::WPtr8(u),
7136            zq,
7137            zd,
7138            n_embd,
7139            n_ff_exp,
7140            n_used,
7141            m.gate_exps.qtype,
7142            m.up_exps.qtype,
7143            m.gate_exps.row_bytes,
7144            m.up_exps.row_bytes,
7145        )?;
7146        // per-slot act quantize: [n_used, n_ff] rows in one quantize launch.
7147        let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
7148        let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
7149        e.moe_down8_fma_q8(
7150            crate::WPtr8(d),
7151            crate::F32x8(wv),
7152            &aq2,
7153            &ad2,
7154            &mut dst,
7155            n_ff_exp,
7156            n_embd,
7157            n_used,
7158            m.down_exps.qtype,
7159            m.down_exps.row_bytes,
7160        )?;
7161        Ok(true)
7162    }
7163
7164    fn moe_gdec_token(
7165        e: &Engine,
7166        m: &MoeWeights,
7167        il: u16,
7168        max_block: usize,
7169        zt: &cudarc::driver::CudaView<f32>,
7170        sel: &[u32],
7171        w: &[f32],
7172        moe_out: &mut CudaSlice<f32>,
7173        tok: usize,
7174        n_embd: usize,
7175        n_ff_exp: usize,
7176        n_used: usize,
7177    ) -> Result<bool, Box<dyn std::error::Error>> {
7178        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7179        use cudarc::driver::DevicePtr;
7180        // One lock hold: residency-check all 3*n_used blocks, collect raw slot pointers.
7181        let ptrs = e.with_moe_cache(max_block, |c, eng| {
7182            let mut g = [0u64; 8];
7183            let mut u = [0u64; 8];
7184            let mut d = [0u64; 8];
7185            for (j, &ex) in sel.iter().enumerate() {
7186                let ex = ex as u16;
7187                let (Some(sg), Some(su), Some(sd)) = (
7188                    c.resident(BlockId::new(il, PROJ_GATE, ex)),
7189                    c.resident(BlockId::new(il, PROJ_UP, ex)),
7190                    c.resident(BlockId::new(il, PROJ_DOWN, ex)),
7191                ) else {
7192                    return Ok(None);
7193                };
7194                let __s = eng.stream();
7195                let (pg, _e0) = c.slot(sg).device_ptr(&__s);
7196                let (pu, _e1) = c.slot(su).device_ptr(&__s);
7197                let (pd, _e2) = c.slot(sd).device_ptr(&__s);
7198                g[j] = pg as u64;
7199                u[j] = pu as u64;
7200                d[j] = pd as u64;
7201            }
7202            if cpu_expert_profile_admit_enabled() && !c.is_frozen() {
7203                for &ex in sel {
7204                    let ex = ex as u16;
7205                    for proj in [PROJ_GATE, PROJ_UP, PROJ_DOWN] {
7206                        c.note_profile_hit(BlockId::new(il, proj, ex));
7207                    }
7208                }
7209            }
7210            c.hits += (3 * n_used) as u64; // instrumentation parity with dispatch()
7211            Ok(Some((g, u, d)))
7212        })?;
7213        let Some((g, u, d)) = ptrs else {
7214            return Ok(false);
7215        };
7216        let mut wv = [0f32; 8];
7217        wv[..n_used].copy_from_slice(w);
7218        // 2 launches: (gate+up+silu) x8, then (down + slot-ordered FMA accumulate) x8.
7219        let act = e.moe_gate_up_silu8(
7220            crate::WPtr8(g),
7221            crate::WPtr8(u),
7222            zt,
7223            n_embd,
7224            n_ff_exp,
7225            n_used,
7226            m.gate_exps.qtype,
7227            m.up_exps.qtype,
7228            m.gate_exps.row_bytes,
7229            m.up_exps.row_bytes,
7230        )?;
7231        let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
7232        e.moe_down8_fma_into(
7233            crate::WPtr8(d),
7234            crate::F32x8(wv),
7235            &act,
7236            &mut dst,
7237            n_ff_exp,
7238            n_embd,
7239            n_used,
7240            m.down_exps.qtype,
7241            m.down_exps.row_bytes,
7242        )?;
7243        Ok(true)
7244    }
7245
7246    /// EDGE-1 §B.3: dispatch one expert projection through the SLRU cache, then run the SAME
7247    /// `qmatvec_view` from whichever slot it landed in (resident HIT or staged MISS). `x` is the
7248    /// sliced activation row. `proj` selects the gate/up/down HostExps tensor. Returns y = W_expert @ x.
7249    /// q8 twin of moe_cached_gemm: same dispatch/slot mechanics, dp4a expert kernel.
7250    fn moe_cached_gemm_q8(
7251        e: &Engine,
7252        il: u16,
7253        proj: u8,
7254        ex: usize,
7255        m: &MoeWeights,
7256        max_block: usize,
7257        aq: &CudaSlice<i8>,
7258        ad: &CudaSlice<f32>,
7259    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7260        use crate::moe_cache::{BlockId, DispatchSlot, PROJ_GATE, PROJ_UP};
7261        let exps = match proj {
7262            PROJ_GATE => &m.gate_exps,
7263            PROJ_UP => &m.up_exps,
7264            _ => &m.down_exps,
7265        };
7266        let layout = exps.expert_layout(ex);
7267        let id = BlockId::new(il, proj, ex as u16);
7268        let source = exps.expert_source(ex);
7269        e.with_moe_cache(max_block, |c, eng| {
7270            let slot = c.dispatch_source(id, source, eng)?;
7271            let DispatchSlot::Resident(sl) = slot;
7272            let buf = c.slot(sl);
7273            eng.qmatvec_expert_q8(
7274                buf,
7275                0..layout.len,
7276                aq,
7277                ad,
7278                1,
7279                exps.in_f,
7280                exps.out_f,
7281                layout.qtype,
7282                layout.row_bytes,
7283            )
7284        })
7285    }
7286
7287    fn moe_cached_gemm(
7288        e: &Engine,
7289        il: u16,
7290        proj: u8,
7291        ex: usize,
7292        m: &MoeWeights,
7293        max_block: usize,
7294        x: &cudarc::driver::CudaView<f32>,
7295    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7296        use crate::moe_cache::{BlockId, DispatchSlot, PROJ_GATE, PROJ_UP};
7297        let exps = match proj {
7298            PROJ_GATE => &m.gate_exps,
7299            PROJ_UP => &m.up_exps,
7300            _ => &m.down_exps,
7301        };
7302        let layout = exps.expert_layout(ex);
7303        let id = BlockId::new(il, proj, ex as u16);
7304        let source = exps.expert_source(ex);
7305        // dispatch under the lock (lookup/admit/memcpy-issue), then resolve the slot and GEMM.
7306        e.with_moe_cache(max_block, |c, eng| {
7307            let slot = c.dispatch_source(id, source, eng)?;
7308            // resolve the device buffer for this slot; the GEMM is enqueued on the compute stream
7309            // (the same stream the memcpy was issued on, so ordering holds without extra sync).
7310            let DispatchSlot::Resident(sl) = slot;
7311            let buf = c.slot(sl);
7312            eng.qmatvec_view(
7313                buf,
7314                0..layout.len,
7315                x,
7316                1,
7317                exps.in_f,
7318                exps.out_f,
7319                layout.qtype,
7320                layout.row_bytes,
7321            )
7322        })
7323    }
7324
7325    /// Populate the warmup cache for an expert whose current-token output intentionally ran on
7326    /// CPU. No GEMM is launched and callers invoke this only after the CPU result has completed,
7327    /// so the current forward's backend assignment and output remain unchanged.
7328    fn moe_profile_admit_expert(
7329        e: &Engine,
7330        il: u16,
7331        ex: usize,
7332        m: &MoeWeights,
7333        max_block: usize,
7334    ) -> Result<(), Box<dyn std::error::Error>> {
7335        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7336        e.with_moe_cache(max_block, |cache, eng| {
7337            for (proj, exps) in [
7338                (PROJ_GATE, &m.gate_exps),
7339                (PROJ_UP, &m.up_exps),
7340                (PROJ_DOWN, &m.down_exps),
7341            ] {
7342                let id = BlockId::new(il, proj, ex as u16);
7343                let _ = cache.dispatch_source(id, exps.expert_source(ex), eng)?;
7344            }
7345            Ok(())
7346        })
7347    }
7348
7349    /// Read a projection from the immutable residency set when present; otherwise use one
7350    /// transient slot without admitting or evicting anything. Used only by post-freeze prefill.
7351    #[allow(clippy::too_many_arguments)]
7352    fn moe_frozen_gemm(
7353        e: &Engine,
7354        il: u16,
7355        proj: u8,
7356        ex: usize,
7357        m: &MoeWeights,
7358        max_block: usize,
7359        x: &cudarc::driver::CudaView<f32>,
7360        scratch: &mut Option<CudaSlice<u8>>,
7361        scratch_len: usize,
7362    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7363        use crate::moe_cache::{BlockId, PROJ_GATE, PROJ_UP};
7364        let exps = match proj {
7365            PROJ_GATE => &m.gate_exps,
7366            PROJ_UP => &m.up_exps,
7367            _ => &m.down_exps,
7368        };
7369        let layout = exps.expert_layout(ex);
7370        let id = BlockId::new(il, proj, ex as u16);
7371        if let Some(output) = e.with_moe_cache(max_block, |cache, eng| {
7372            let Some(slot) = cache.resident(id) else {
7373                return Ok(None);
7374            };
7375            let buf = cache.slot(slot);
7376            Ok(Some(eng.qmatvec_view(
7377                buf,
7378                0..layout.len,
7379                x,
7380                1,
7381                exps.in_f,
7382                exps.out_f,
7383                layout.qtype,
7384                layout.row_bytes,
7385            )?))
7386        })? {
7387            return Ok(output);
7388        }
7389        if scratch.is_none() {
7390            *scratch = Some(e.alloc_u8_uninit(scratch_len)?);
7391        }
7392        let scratch = scratch.as_mut().unwrap();
7393        e.stage_expert(exps.expert_bytes(ex), scratch, 0)?;
7394        e.qmatvec_view(
7395            scratch,
7396            0..layout.len,
7397            x,
7398            1,
7399            exps.in_f,
7400            exps.out_f,
7401            layout.qtype,
7402            layout.row_bytes,
7403        )
7404    }
7405
7406    fn moe_prefetch_expert(
7407        e: &Engine,
7408        il: u16,
7409        ex: usize,
7410        m: &MoeWeights,
7411        max_block: usize,
7412        keep: &[crate::moe_cache::BlockId],
7413    ) -> Result<(), Box<dyn std::error::Error>> {
7414        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7415        e.with_moe_cache(max_block, |c, eng| {
7416            for (proj, exps) in [
7417                (PROJ_GATE, &m.gate_exps),
7418                (PROJ_UP, &m.up_exps),
7419                (PROJ_DOWN, &m.down_exps),
7420            ] {
7421                let id = BlockId::new(il, proj, ex as u16);
7422                let _ = c.prefetch_source(id, exps.expert_source(ex), keep, eng)?;
7423            }
7424            Ok(())
7425        })
7426    }
7427
7428    /// Worker-mode disk lookahead for grouped prefill. Memory sources are deliberately skipped so
7429    /// selecting `worker` changes only storage scheduling here; all CUDA work stays in dispatch.
7430    fn moe_prefetch_disk_expert(
7431        e: &Engine,
7432        il: u16,
7433        ex: usize,
7434        m: &MoeWeights,
7435        max_block: usize,
7436        keep: &[crate::moe_cache::BlockId],
7437    ) -> Result<(), Box<dyn std::error::Error>> {
7438        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7439        e.with_moe_cache(max_block, |c, eng| {
7440            for (proj, exps) in [
7441                (PROJ_GATE, &m.gate_exps),
7442                (PROJ_UP, &m.up_exps),
7443                (PROJ_DOWN, &m.down_exps),
7444            ] {
7445                let source = exps.expert_source(ex);
7446                if let crate::model::ExpertSource::Disk { .. } = &source {
7447                    let id = BlockId::new(il, proj, ex as u16);
7448                    let _ = c.prefetch_source(id, source, keep, eng)?;
7449                }
7450            }
7451            Ok(())
7452        })
7453    }
7454
7455    #[inline]
7456    fn moe_prefetch_host_expert(ex: usize, m: &MoeWeights) {
7457        let _ = m.gate_exps.prefetch_expert_pages(ex);
7458        let _ = m.up_exps.prefetch_expert_pages(ex);
7459        let _ = m.down_exps.prefetch_expert_pages(ex);
7460    }
7461}
7462
7463// ================================================================================================
7464// A2: EXPERT-GROUPED MoE PREFILL (MEMRA_MOE_GROUPED=1). Resident-case prototype.
7465//
7466// Instead of the per-token loop (T * 8 experts * 3 projections = 12024 individual m=1 matvecs),
7467// this groups tokens by expert and runs ONE matmul per active expert per projection at m=m_e.
7468// On a 501-token prefill with ~170 active experts, that's ~510 matmuls (vs 12024).
7469//
7470// EXACTNESS: per-token accumulation across its 8 experts is reordered (grouped processes experts
7471// in expert-id order, not the router's top-k order). To preserve bit-identity with the sequential
7472// loop, we use an 8-SLOT scheme: expert outputs are scattered into slots keyed by the token's
7473// top-k position (0..7), then reduced in that fixed order. This makes the f32 addition order
7474// identical to the per-token loop regardless of expert processing order.
7475//
7476// Memory: T * 8 * n_embd * 4 = 501 * 8 * 2048 * 4 = ~32 MB (slot buffer). Fine on 96GB.
7477// ================================================================================================
7478
7479impl HybridModel {
7480    /// Resident-slab fast path for host-routed grouped prefill. Unclamped layers batch the
7481    /// sequential fused q8 program over the token axis; clamped layers use the separate
7482    /// expert-major q8 chain so `ffn_act_lim` remains authoritative.
7483    #[allow(clippy::too_many_arguments)]
7484    fn moe_ffn_grouped_resident_q8(
7485        e: &Engine,
7486        m: &MoeWeights,
7487        z: &CudaSlice<f32>,
7488        t: usize,
7489        cfg: &ModelConfig,
7490        il: u16,
7491        sel_all: &[u32],
7492        w_all: &[f32],
7493        table: &CudaSlice<u64>,
7494        gu_il: bool,
7495    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7496        let moe = cfg.moe.as_ref().unwrap();
7497        let n_embd = cfg.n_embd as usize;
7498        let n_expert = moe.expert_count as usize;
7499        let n_used = moe.expert_used_count as usize;
7500        let n_ff_exp = moe.expert_ff_length as usize;
7501        let n_pairs = t * n_used;
7502        debug_assert_eq!(sel_all.len(), n_pairs);
7503        debug_assert_eq!(w_all.len(), n_pairs);
7504        debug_assert!(
7505            m.gate_exps.macros.is_none()
7506                && m.up_exps.macros.is_none()
7507                && m.down_exps.macros.is_none(),
7508            "resident grouped q8 does not fold per-expert macro scales",
7509        );
7510
7511        // The rows twins run the resident sequential program verbatim on grid.z = token:
7512        // fused gate/up/SiLU per slot, batched activation quantization, then the original
7513        // slot-ordered down/FMA chain. Routing remains the sigmoid selector above (device by
7514        // default; MEMRA_SIG_ROUTER=0 is the host oracle); these kernels consume sel/w only and
7515        // never enter the softmax router.
7516        if !cfg.swiglu_clamped_at(il as u32) {
7517            let sel: Vec<i32> = sel_all.iter().map(|&expert| expert as i32).collect();
7518            let sel_d = e.htod_i32(&sel)?;
7519            let w_d = e.htod(w_all)?;
7520            let (gate_row_bytes, up_row_bytes) = if gu_il {
7521                let combined = m.gate_exps.row_bytes + m.up_exps.row_bytes;
7522                (combined, combined)
7523            } else {
7524                (m.gate_exps.row_bytes, m.up_exps.row_bytes)
7525            };
7526            let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
7527            let act = e.moe_gate_up_silu8_dev_q8_rows(
7528                table,
7529                &sel_d,
7530                &zq,
7531                &zd,
7532                t,
7533                n_embd,
7534                n_ff_exp,
7535                n_used,
7536                n_expert,
7537                m.gate_exps.qtype,
7538                m.up_exps.qtype,
7539                gate_row_bytes,
7540                up_row_bytes,
7541                &m.dev_macros,
7542            )?;
7543            let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
7544            let mut moe_out = e.uninit(t * n_embd)?;
7545            e.moe_down8_fma_dev_q8_rows_g(
7546                table,
7547                &sel_d,
7548                &w_d,
7549                &aq2,
7550                &ad2,
7551                &mut moe_out,
7552                t,
7553                n_ff_exp,
7554                n_embd,
7555                n_used,
7556                n_expert,
7557                m.down_exps.qtype,
7558                m.down_exps.row_bytes,
7559            )?;
7560
7561            if std::env::var("MEMRA_MOE_STATS").is_ok() {
7562                let mut counts = vec![0usize; n_expert];
7563                for &expert in sel_all {
7564                    counts[expert as usize] += 1;
7565                }
7566                let mut sizes: Vec<usize> =
7567                    counts.into_iter().filter(|&count| count != 0).collect();
7568                sizes.sort_unstable();
7569                let mean = sizes.iter().sum::<usize>() as f64 / sizes.len().max(1) as f64;
7570                println!(
7571                    "moe-grouped il={il} t={t} dispatch=resident-q8-rows active={}/{} \
7572                     m_e: min={} median={} mean={mean:.1} max={}",
7573                    sizes.len(),
7574                    n_expert,
7575                    sizes.first().copied().unwrap_or(0),
7576                    sizes.get(sizes.len() / 2).copied().unwrap_or(0),
7577                    sizes.last().copied().unwrap_or(0),
7578                );
7579            }
7580            return Ok(moe_out);
7581        }
7582
7583        // Clamped layers keep gate/up, activation, and down as separate stages. The pair-major
7584        // matvec body is qmatvec_expert_q8 verbatim, while one launch covers every routed pair.
7585        // Pair ids stay in router slot order so scatter preserves the sequential FMA chain.
7586        let pair_tok: Vec<i32> = (0..n_pairs).map(|pair| (pair / n_used) as i32).collect();
7587        let pair_ex: Vec<i32> = sel_all.iter().map(|&expert| expert as i32).collect();
7588        let tok_off: Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
7589        let tok_ids: Vec<i32> = (0..n_pairs as i32).collect();
7590
7591        let mut by_expert: Vec<Vec<i32>> = vec![Vec::new(); n_expert];
7592        for (pair, &expert) in pair_ex.iter().enumerate() {
7593            by_expert[expert as usize].push(pair as i32);
7594        }
7595
7596        let pair_tok_d = e.htod_i32(&pair_tok)?;
7597        let pair_ex_d = e.htod_i32(&pair_ex)?;
7598        let pair_w_d = e.htod(w_all)?;
7599        let tok_off_d = e.htod_i32(&tok_off)?;
7600        let tok_ids_d = e.htod_i32(&tok_ids)?;
7601
7602        let matvec = |proj: i32,
7603                      pair_rows: &CudaSlice<i32>,
7604                      aq: &CudaSlice<i8>,
7605                      ad: &CudaSlice<f32>,
7606                      in_f: usize,
7607                      out_f: usize,
7608                      qtype: i32,
7609                      row_bytes: usize|
7610         -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7611            e.moe_pairs_matvec_q8(
7612                table, proj, pair_rows, &pair_ex_d, aq, ad, in_f, out_f, n_expert, n_pairs, qtype,
7613                row_bytes,
7614            )
7615        };
7616
7617        let (gate_row_bytes, up_row_bytes) = if gu_il {
7618            let combined = m.gate_exps.row_bytes + m.up_exps.row_bytes;
7619            (combined, combined)
7620        } else {
7621            (m.gate_exps.row_bytes, m.up_exps.row_bytes)
7622        };
7623        let (zq, zd) = e.quantize_q8_1(z, t, n_embd)?;
7624        let gate = matvec(
7625            0,
7626            &pair_tok_d,
7627            &zq,
7628            &zd,
7629            n_embd,
7630            n_ff_exp,
7631            m.gate_exps.qtype,
7632            gate_row_bytes,
7633        )?;
7634        let up = matvec(
7635            1,
7636            &pair_tok_d,
7637            &zq,
7638            &zd,
7639            n_embd,
7640            n_ff_exp,
7641            m.up_exps.qtype,
7642            up_row_bytes,
7643        )?;
7644        let mut act = e.uninit(n_pairs * n_ff_exp)?;
7645        Self::ffn_act_lim(
7646            e,
7647            cfg,
7648            &gate,
7649            &up,
7650            1.0,
7651            1.0,
7652            cfg.clamp_exp_at(il as u32),
7653            &mut act,
7654            n_pairs * n_ff_exp,
7655        )?;
7656        let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
7657        let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
7658        let pair_self_d = e.htod_i32(&pair_self)?;
7659        let down = matvec(
7660            2,
7661            &pair_self_d,
7662            &aq2,
7663            &ad2,
7664            n_ff_exp,
7665            n_embd,
7666            m.down_exps.qtype,
7667            m.down_exps.row_bytes,
7668        )?;
7669        let mut moe_out = e.uninit(t * n_embd)?;
7670        e.moe_pairs_scatter(
7671            &down,
7672            &pair_w_d,
7673            &tok_off_d,
7674            &tok_ids_d,
7675            &mut moe_out,
7676            t,
7677            n_embd,
7678        )?;
7679
7680        if std::env::var("MEMRA_MOE_STATS").is_ok() {
7681            let mut sizes: Vec<usize> = by_expert
7682                .iter()
7683                .filter_map(|pairs| (!pairs.is_empty()).then_some(pairs.len()))
7684                .collect();
7685            sizes.sort_unstable();
7686            let mean = sizes.iter().sum::<usize>() as f64 / sizes.len().max(1) as f64;
7687            println!(
7688                "moe-grouped il={il} t={t} dispatch=resident-q8-clamped-pairs active={}/{} \
7689                 m_e: min={} median={} mean={mean:.1} max={}",
7690                sizes.len(),
7691                n_expert,
7692                sizes.first().copied().unwrap_or(0),
7693                sizes.get(sizes.len() / 2).copied().unwrap_or(0),
7694                sizes.last().copied().unwrap_or(0),
7695            );
7696        }
7697        Ok(moe_out)
7698    }
7699
7700    fn moe_ffn_grouped_add_shared(
7701        e: &Engine,
7702        m: &MoeWeights,
7703        z: &CudaSlice<f32>,
7704        t: usize,
7705        cfg: &ModelConfig,
7706        il: u16,
7707        moe_out: &mut CudaSlice<f32>,
7708    ) -> Result<(), Box<dyn std::error::Error>> {
7709        let n_embd = cfg.n_embd as usize;
7710        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
7711            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
7712        {
7713            let n_ff_sh = gate_shexp.out_features();
7714            let sg_gate = e.matmul(gate_shexp, z, t)?;
7715            let sg_up = e.matmul(up_shexp, z, t)?;
7716            let mut sa = e.uninit(t * n_ff_sh)?;
7717            Self::ffn_act_lim(
7718                e,
7719                cfg,
7720                &sg_gate,
7721                &sg_up,
7722                1.0,
7723                1.0,
7724                cfg.clamp_shexp_at(il as u32),
7725                &mut sa,
7726                t * n_ff_sh,
7727            )?;
7728            let sh = e.matmul(down_shexp, &sa, t)?;
7729            let gate = match &m.gate_inp_shexp {
7730                Some(gate_inp_shexp) => {
7731                    if t < PRIME_MIN_T || crate::router_prefill_exact_on() {
7732                        e.sigmoid_dot_rows(z, gate_inp_shexp.float_data(), n_embd, t)?
7733                    } else {
7734                        let raw = e.linear(z, gate_inp_shexp.float_data(), t, n_embd, 1)?;
7735                        let mut gate = e.uninit(t)?;
7736                        e.sigmoid(&raw, &mut gate, t)?;
7737                        gate
7738                    }
7739                }
7740                None => e.htod(&vec![1.0f32; t])?,
7741            };
7742            e.add_scaled_rows(&sh, &gate, moe_out, n_embd, t)?;
7743        }
7744        Ok(())
7745    }
7746
7747    /// A2 expert-grouped MoE FFN (prefill path, MEMRA_MOE_GROUPED=1). Same semantics as moe_ffn:
7748    /// z [T, n_embd] -> moe_out [T, n_embd]. BIT-IDENTICAL to moe_ffn when using the slot scheme.
7749    pub(crate) fn moe_ffn_grouped(
7750        e: &Engine,
7751        m: &MoeWeights,
7752        z: &CudaSlice<f32>,
7753        t: usize,
7754        cfg: &ModelConfig,
7755        il: u16,
7756        max_block: usize,
7757    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
7758        let moe = cfg.moe.as_ref().unwrap();
7759        let n_embd = cfg.n_embd as usize;
7760        let n_expert = moe.expert_count as usize;
7761        let n_used = moe.expert_used_count as usize;
7762        let n_ff_exp = moe.expert_ff_length as usize;
7763        // step35 per-layer SwiGLU clamp; None on every other arch / unclamped layer.
7764        let lim_exp = cfg.clamp_exp_at(il as u32);
7765
7766        // 1. ROUTER: exactly the same m-invariant sigmoid selector as the sequential path
7767        // (device by default; MEMRA_SIG_ROUTER=0 is the host oracle). The grouped dispatch never
7768        // enters the softmax-only pairs/dev router.
7769        let logits = Self::moe_router_logits(e, m, z, t, cfg)?;
7770        if let Some(sig) = cfg.sigmoid_router() {
7771            Self::trace_sigmoid_router_logits(e, il, t, n_expert, n_used, &logits, m, sig)?;
7772        }
7773        let (sel_all, w_all) = if let Some(sig) = cfg.sigmoid_router() {
7774            Self::moe_route_sigmoid_cfg(e, &logits, t, n_expert, n_used, m, sig)?
7775        } else {
7776            Self::moe_route_cfg(e, &logits, t, n_expert, n_used, m.active_experts.as_deref())?
7777        };
7778        crate::moesd::record_host_routes(il, n_expert, n_used, &sel_all)?;
7779        Self::trace_moe_routes(il, t, &sel_all, &w_all)?;
7780        Self::trace_moe_input(e, il, t, n_embd, z)?;
7781
7782        // Fits-VRAM Step35 under the PP stage split: use the expert-major q8 arithmetic directly
7783        // from the owning device's uniform resident slab. This is not `moe_ffn_pairs`: routing
7784        // already happened above and the activation is clamp-aware. Mixed layouts, remote slabs,
7785        // macro-scaled experts, q8-disabled configs, and spill fall through to metadata-aware A2.
7786        let no_exp_macros = m.gate_exps.macros.is_none()
7787            && m.up_exps.macros.is_none()
7788            && m.down_exps.macros.is_none();
7789        let resident_q8 = m.dev_exps.as_ref().filter(|dev| {
7790            m.has_uniform_expert_layout()
7791                && no_exp_macros
7792                && moe_q8_enabled()
7793                && q8_expert_supported(m.gate_exps.qtype)
7794                && q8_expert_supported(m.up_exps.qtype)
7795                && q8_expert_supported(m.down_exps.qtype)
7796                && moe_slab_enabled()
7797                && dev.dev == e.ctx().ordinal()
7798        });
7799        if let Some(dev) = resident_q8 {
7800            let mut moe_out = Self::moe_ffn_grouped_resident_q8(
7801                e,
7802                m,
7803                z,
7804                t,
7805                cfg,
7806                il,
7807                &sel_all,
7808                &w_all,
7809                &dev.ptr_row,
7810                dev.gu_il,
7811            )?;
7812            Self::moe_ffn_grouped_add_shared(e, m, z, t, cfg, il, &mut moe_out)?;
7813            return Ok(moe_out);
7814        }
7815
7816        // 2. BUILD PER-EXPERT TOKEN LISTS (host-side grouping).
7817        // For each expert e, we need: which tokens use it, their positions in z, their top-k
7818        // slot index (for bit-identical accumulation), and their weights.
7819        struct ExpertGroup {
7820            tok_indices: Vec<i32>,  // indices into z rows (0..T-1)
7821            slot_indices: Vec<i32>, // top-k slot (0..n_used-1) for that token-expert pair
7822            weights: Vec<f32>,      // renormalized weight for that token-expert pair
7823        }
7824        let mut groups: Vec<ExpertGroup> = (0..n_expert)
7825            .map(|_| ExpertGroup {
7826                tok_indices: Vec::new(),
7827                slot_indices: Vec::new(),
7828                weights: Vec::new(),
7829            })
7830            .collect();
7831
7832        for tok in 0..t {
7833            for j in 0..n_used {
7834                let ex = sel_all[tok * n_used + j] as usize;
7835                let w = w_all[tok * n_used + j];
7836                groups[ex].tok_indices.push(tok as i32);
7837                groups[ex].slot_indices.push(j as i32);
7838                groups[ex].weights.push(w);
7839            }
7840        }
7841
7842        // 3. ALLOCATE SLOT BUFFER: [T, n_used, n_embd] f32, zero-initialized.
7843        // Each token's 8 expert contributions land in their respective slots.
7844        let mut slot_buf = e.zeros(t * n_used * n_embd)?;
7845        let mut wbuf = e.zeros(t * n_used)?; // [T, n_used] weight buffer for FMA reduce
7846
7847        // Expert weight dimensions (used in both cache and staging paths).
7848        let g_len = m.gate_exps.max_expert_bytes();
7849        let u_len = m.up_exps.max_expert_bytes();
7850        let d_len = m.down_exps.max_expert_bytes();
7851        let moe_q8 = m.has_uniform_expert_layout()
7852            && moe_q8_enabled()
7853            && q8_expert_supported(m.gate_exps.qtype)
7854            && q8_expert_supported(m.up_exps.qtype)
7855            && q8_expert_supported(m.down_exps.qtype);
7856        // The per-expert slab view is legal only for the ordinary contiguous uniform layout.
7857        // Interleaved GU slabs require the pointer-table fast path above.
7858        let slab_local = m
7859            .dev_exps
7860            .as_ref()
7861            .filter(|dev| !dev.gu_il && moe_slab_enabled() && dev.dev == e.ctx().ordinal());
7862        let use_cache =
7863            slab_local.is_none() && Engine::moe_cache_enabled() && !e.moe_cache_frozen();
7864        // The sequential no-cache/frozen staging oracle is f32. Use q8 only where sequential
7865        // also does: a local resident slab or a live SLRU dispatch.
7866        let grouped_q8 = moe_q8 && (slab_local.is_some() || use_cache);
7867
7868        // GPU scratch for staging (only allocated without a local slab or cache).
7869        let (mut scratch_g, mut scratch_u, mut scratch_d) = if slab_local.is_none() && !use_cache {
7870            (
7871                Some(e.alloc_u8(g_len)?),
7872                Some(e.alloc_u8(u_len)?),
7873                Some(e.alloc_u8(d_len)?),
7874            )
7875        } else {
7876            (None, None, None)
7877        };
7878
7879        // 4. PER ACTIVE EXPERT: gather, compute, scatter.
7880        // Processing ORDER: DESCENDING m_e (biggest token batches first) — the concluded winner
7881        // (rig5090 2026-07-04, the ascending-id arm and its MEMRA_MOE_ORDER seam removed): desc is
7882        // a first-forward win at partial cache capacity — the hot (big-m_e) experts are admitted
7883        // to the SLRU before the small-m_e tail can pollute it, so residency converges in ONE
7884        // forward instead of several: auto-cache T=501 126.9 -> 169.9 tok/s (1.34x), cap512
7885        // 119.6 -> 160.8 (and kills the rep-to-rep bimodal); wash (<2%) at cap64 pure-spill and
7886        // at long prompts where every expert stages regardless. Order is FREE to change without
7887        // breaking the byte-identity gate: the slot scheme pins each token's accumulation order
7888        // regardless of expert processing order (the whole point of the slots).
7889        let mut order: Vec<usize> = (0..n_expert)
7890            .filter(|&ex| !groups[ex].tok_indices.is_empty())
7891            .collect();
7892        order.sort_by(|&a, &b| {
7893            groups[b]
7894                .tok_indices
7895                .len()
7896                .cmp(&groups[a].tok_indices.len())
7897                .then(a.cmp(&b))
7898        });
7899        let mut m_dist: Vec<usize> = Vec::new(); // for stats
7900        let page_window = moe_page_prefetch_window();
7901        let worker_disk_prefetch = use_cache && crate::spill_pread::worker_enabled();
7902        if worker_disk_prefetch {
7903            if let Some(first) = grouped_worker_prefetch_position(order.len(), None) {
7904                Self::moe_prefetch_disk_expert(e, il, order[first], m, max_block, &[])?;
7905            }
7906        }
7907        for (order_pos, &ex) in order.iter().enumerate() {
7908            for next in page_prefetch_positions(order_pos, order.len(), page_window) {
7909                Self::moe_prefetch_host_expert(order[next], m);
7910            }
7911            if worker_disk_prefetch {
7912                if let Some(next) = grouped_worker_prefetch_position(order.len(), Some(order_pos)) {
7913                    use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
7914                    let keep = [
7915                        BlockId::new(il, PROJ_GATE, ex as u16),
7916                        BlockId::new(il, PROJ_UP, ex as u16),
7917                        BlockId::new(il, PROJ_DOWN, ex as u16),
7918                    ];
7919                    Self::moe_prefetch_disk_expert(e, il, order[next], m, max_block, &keep)?;
7920                }
7921            }
7922            let grp = &groups[ex];
7923            let m_e = grp.tok_indices.len();
7924            m_dist.push(m_e);
7925            let gl = m.gate_exps.expert_layout(ex);
7926            let ul = m.up_exps.expert_layout(ex);
7927            let dl = m.down_exps.expert_layout(ex);
7928
7929            // Upload index/weight arrays to device. The down-proj per-expert macro-scale
7930            // (ModelOpt weight_scale_2) folds into the scatter weights — post-matmul linear,
7931            // same fold as the sequential loop's `w[j] * macro_scale(ex)`. 1.0 for GGUF experts.
7932            let tok_idx_d = e.htod_i32(&grp.tok_indices)?;
7933            let slot_idx_d = e.htod_i32(&grp.slot_indices)?;
7934            let dmac = m.down_exps.macro_scale(ex);
7935            let weight_d = if dmac == 1.0 {
7936                e.htod(&grp.weights)?
7937            } else {
7938                let scaled: Vec<f32> = grp.weights.iter().map(|&w| w * dmac).collect();
7939                e.htod(&scaled)?
7940            };
7941
7942            // GATHER: collect m_e activation rows from z into a contiguous buffer.
7943            let mut gathered = e.zeros(m_e * n_embd)?;
7944            e.gather_rows(z, &tok_idx_d, &mut gathered, n_embd, m_e)?;
7945            let gv = gathered.slice(0..m_e * n_embd);
7946
7947            // Compute gate/up/down from the local slab, metadata-aware cache, or staging. The q8
7948            // form keeps each gathered row in the sequential arithmetic class at `m=m_e`.
7949            let y = if let Some(dev) = slab_local {
7950                let gate_start = ex * m.gate_exps.expert_stride;
7951                let up_start = ex * m.up_exps.expert_stride;
7952                let down_start = ex * m.down_exps.expert_stride;
7953                if grouped_q8 {
7954                    let (zq, zd) = e.quantize_q8_1(&gathered, m_e, n_embd)?;
7955                    let gate = e.qmatvec_expert_q8(
7956                        &dev.gate,
7957                        gate_start..gate_start + gl.len,
7958                        &zq,
7959                        &zd,
7960                        m_e,
7961                        m.gate_exps.in_f,
7962                        m.gate_exps.out_f,
7963                        gl.qtype,
7964                        gl.row_bytes,
7965                    )?;
7966                    let up = e.qmatvec_expert_q8(
7967                        &dev.up,
7968                        up_start..up_start + ul.len,
7969                        &zq,
7970                        &zd,
7971                        m_e,
7972                        m.up_exps.in_f,
7973                        m.up_exps.out_f,
7974                        ul.qtype,
7975                        ul.row_bytes,
7976                    )?;
7977                    let mut act = e.uninit(m_e * n_ff_exp)?;
7978                    Self::ffn_act_lim(
7979                        e,
7980                        cfg,
7981                        &gate,
7982                        &up,
7983                        m.gate_exps.macro_scale(ex),
7984                        m.up_exps.macro_scale(ex),
7985                        lim_exp,
7986                        &mut act,
7987                        m_e * n_ff_exp,
7988                    )?;
7989                    let (aq2, ad2) = e.quantize_q8_1(&act, m_e, n_ff_exp)?;
7990                    e.qmatvec_expert_q8(
7991                        &dev.down,
7992                        down_start..down_start + dl.len,
7993                        &aq2,
7994                        &ad2,
7995                        m_e,
7996                        m.down_exps.in_f,
7997                        m.down_exps.out_f,
7998                        dl.qtype,
7999                        dl.row_bytes,
8000                    )?
8001                } else {
8002                    let gate = e.qmatvec_view(
8003                        &dev.gate,
8004                        gate_start..gate_start + gl.len,
8005                        &gv,
8006                        m_e,
8007                        m.gate_exps.in_f,
8008                        m.gate_exps.out_f,
8009                        gl.qtype,
8010                        gl.row_bytes,
8011                    )?;
8012                    let up = e.qmatvec_view(
8013                        &dev.up,
8014                        up_start..up_start + ul.len,
8015                        &gv,
8016                        m_e,
8017                        m.up_exps.in_f,
8018                        m.up_exps.out_f,
8019                        ul.qtype,
8020                        ul.row_bytes,
8021                    )?;
8022                    let mut act = e.uninit(m_e * n_ff_exp)?;
8023                    Self::ffn_act_lim(
8024                        e,
8025                        cfg,
8026                        &gate,
8027                        &up,
8028                        m.gate_exps.macro_scale(ex),
8029                        m.up_exps.macro_scale(ex),
8030                        lim_exp,
8031                        &mut act,
8032                        m_e * n_ff_exp,
8033                    )?;
8034                    let actv = act.slice(0..m_e * n_ff_exp);
8035                    e.qmatvec_view(
8036                        &dev.down,
8037                        down_start..down_start + dl.len,
8038                        &actv,
8039                        m_e,
8040                        m.down_exps.in_f,
8041                        m.down_exps.out_f,
8042                        dl.qtype,
8043                        dl.row_bytes,
8044                    )?
8045                }
8046            } else if use_cache {
8047                use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
8048                if grouped_q8 {
8049                    let (zq, zd) = e.quantize_q8_1(&gathered, m_e, n_embd)?;
8050                    let gate = e.with_moe_cache(max_block, |cache, eng| {
8051                        let id = BlockId::new(il, PROJ_GATE, ex as u16);
8052                        let slot = cache.dispatch_source(id, m.gate_exps.expert_source(ex), eng)?;
8053                        eng.qmatvec_expert_q8(
8054                            cache.buf(slot),
8055                            0..gl.len,
8056                            &zq,
8057                            &zd,
8058                            m_e,
8059                            m.gate_exps.in_f,
8060                            m.gate_exps.out_f,
8061                            gl.qtype,
8062                            gl.row_bytes,
8063                        )
8064                    })?;
8065                    let up = e.with_moe_cache(max_block, |cache, eng| {
8066                        let id = BlockId::new(il, PROJ_UP, ex as u16);
8067                        let slot = cache.dispatch_source(id, m.up_exps.expert_source(ex), eng)?;
8068                        eng.qmatvec_expert_q8(
8069                            cache.buf(slot),
8070                            0..ul.len,
8071                            &zq,
8072                            &zd,
8073                            m_e,
8074                            m.up_exps.in_f,
8075                            m.up_exps.out_f,
8076                            ul.qtype,
8077                            ul.row_bytes,
8078                        )
8079                    })?;
8080                    let mut act = e.uninit(m_e * n_ff_exp)?;
8081                    Self::ffn_act_lim(
8082                        e,
8083                        cfg,
8084                        &gate,
8085                        &up,
8086                        m.gate_exps.macro_scale(ex),
8087                        m.up_exps.macro_scale(ex),
8088                        lim_exp,
8089                        &mut act,
8090                        m_e * n_ff_exp,
8091                    )?;
8092                    let (aq2, ad2) = e.quantize_q8_1(&act, m_e, n_ff_exp)?;
8093                    e.with_moe_cache(max_block, |cache, eng| {
8094                        let id = BlockId::new(il, PROJ_DOWN, ex as u16);
8095                        let slot = cache.dispatch_source(id, m.down_exps.expert_source(ex), eng)?;
8096                        eng.qmatvec_expert_q8(
8097                            cache.buf(slot),
8098                            0..dl.len,
8099                            &aq2,
8100                            &ad2,
8101                            m_e,
8102                            m.down_exps.in_f,
8103                            m.down_exps.out_f,
8104                            dl.qtype,
8105                            dl.row_bytes,
8106                        )
8107                    })?
8108                } else {
8109                    let gate = e.with_moe_cache(max_block, |cache, eng| {
8110                        let id = BlockId::new(il, PROJ_GATE, ex as u16);
8111                        let slot = cache.dispatch_source(id, m.gate_exps.expert_source(ex), eng)?;
8112                        eng.qmatvec_view(
8113                            cache.buf(slot),
8114                            0..gl.len,
8115                            &gv,
8116                            m_e,
8117                            m.gate_exps.in_f,
8118                            m.gate_exps.out_f,
8119                            gl.qtype,
8120                            gl.row_bytes,
8121                        )
8122                    })?;
8123                    let up = e.with_moe_cache(max_block, |cache, eng| {
8124                        let id = BlockId::new(il, PROJ_UP, ex as u16);
8125                        let slot = cache.dispatch_source(id, m.up_exps.expert_source(ex), eng)?;
8126                        eng.qmatvec_view(
8127                            cache.buf(slot),
8128                            0..ul.len,
8129                            &gv,
8130                            m_e,
8131                            m.up_exps.in_f,
8132                            m.up_exps.out_f,
8133                            ul.qtype,
8134                            ul.row_bytes,
8135                        )
8136                    })?;
8137                    let mut act = e.uninit(m_e * n_ff_exp)?;
8138                    Self::ffn_act_lim(
8139                        e,
8140                        cfg,
8141                        &gate,
8142                        &up,
8143                        m.gate_exps.macro_scale(ex),
8144                        m.up_exps.macro_scale(ex),
8145                        lim_exp,
8146                        &mut act,
8147                        m_e * n_ff_exp,
8148                    )?;
8149                    let actv = act.slice(0..m_e * n_ff_exp);
8150                    e.with_moe_cache(max_block, |cache, eng| {
8151                        let id = BlockId::new(il, PROJ_DOWN, ex as u16);
8152                        let slot = cache.dispatch_source(id, m.down_exps.expert_source(ex), eng)?;
8153                        eng.qmatvec_view(
8154                            cache.buf(slot),
8155                            0..dl.len,
8156                            &actv,
8157                            m_e,
8158                            m.down_exps.in_f,
8159                            m.down_exps.out_f,
8160                            dl.qtype,
8161                            dl.row_bytes,
8162                        )
8163                    })?
8164                }
8165            } else {
8166                let sg = scratch_g.as_mut().unwrap();
8167                let su = scratch_u.as_mut().unwrap();
8168                let sd = scratch_d.as_mut().unwrap();
8169                e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
8170                e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
8171                e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
8172                if grouped_q8 {
8173                    let (zq, zd) = e.quantize_q8_1(&gathered, m_e, n_embd)?;
8174                    let gate = e.qmatvec_expert_q8(
8175                        sg,
8176                        0..gl.len,
8177                        &zq,
8178                        &zd,
8179                        m_e,
8180                        m.gate_exps.in_f,
8181                        m.gate_exps.out_f,
8182                        gl.qtype,
8183                        gl.row_bytes,
8184                    )?;
8185                    let up = e.qmatvec_expert_q8(
8186                        su,
8187                        0..ul.len,
8188                        &zq,
8189                        &zd,
8190                        m_e,
8191                        m.up_exps.in_f,
8192                        m.up_exps.out_f,
8193                        ul.qtype,
8194                        ul.row_bytes,
8195                    )?;
8196                    let mut act = e.uninit(m_e * n_ff_exp)?;
8197                    Self::ffn_act_lim(
8198                        e,
8199                        cfg,
8200                        &gate,
8201                        &up,
8202                        m.gate_exps.macro_scale(ex),
8203                        m.up_exps.macro_scale(ex),
8204                        lim_exp,
8205                        &mut act,
8206                        m_e * n_ff_exp,
8207                    )?;
8208                    let (aq2, ad2) = e.quantize_q8_1(&act, m_e, n_ff_exp)?;
8209                    e.qmatvec_expert_q8(
8210                        sd,
8211                        0..dl.len,
8212                        &aq2,
8213                        &ad2,
8214                        m_e,
8215                        m.down_exps.in_f,
8216                        m.down_exps.out_f,
8217                        dl.qtype,
8218                        dl.row_bytes,
8219                    )?
8220                } else {
8221                    let gate = e.qmatvec_view(
8222                        sg,
8223                        0..gl.len,
8224                        &gv,
8225                        m_e,
8226                        m.gate_exps.in_f,
8227                        m.gate_exps.out_f,
8228                        gl.qtype,
8229                        gl.row_bytes,
8230                    )?;
8231                    let up = e.qmatvec_view(
8232                        su,
8233                        0..ul.len,
8234                        &gv,
8235                        m_e,
8236                        m.up_exps.in_f,
8237                        m.up_exps.out_f,
8238                        ul.qtype,
8239                        ul.row_bytes,
8240                    )?;
8241                    let mut act = e.uninit(m_e * n_ff_exp)?;
8242                    Self::ffn_act_lim(
8243                        e,
8244                        cfg,
8245                        &gate,
8246                        &up,
8247                        m.gate_exps.macro_scale(ex),
8248                        m.up_exps.macro_scale(ex),
8249                        lim_exp,
8250                        &mut act,
8251                        m_e * n_ff_exp,
8252                    )?;
8253                    let actv = act.slice(0..m_e * n_ff_exp);
8254                    e.qmatvec_view(
8255                        sd,
8256                        0..dl.len,
8257                        &actv,
8258                        m_e,
8259                        m.down_exps.in_f,
8260                        m.down_exps.out_f,
8261                        dl.qtype,
8262                        dl.row_bytes,
8263                    )?
8264                }
8265            };
8266
8267            // SCATTER into slot buffer: each row goes to slot_buf[tok, slot, :].
8268            e.scatter_slot(
8269                &y,
8270                &tok_idx_d,
8271                &slot_idx_d,
8272                &weight_d,
8273                &mut slot_buf,
8274                &mut wbuf,
8275                n_embd,
8276                n_used,
8277                m_e,
8278            )?;
8279        }
8280
8281        // 5. REDUCE SLOTS: sum the 8 slots per token into the final moe_out.
8282        let mut moe_out = e.zeros(t * n_embd)?;
8283        e.reduce_slots(&slot_buf, &wbuf, &mut moe_out, n_embd, n_used, t)?;
8284
8285        // STATS: print m-distribution when MEMRA_MOE_STATS is set.
8286        if std::env::var("MEMRA_MOE_STATS").is_ok() && !m_dist.is_empty() {
8287            m_dist.sort_unstable();
8288            let active = m_dist.len();
8289            let mean = m_dist.iter().sum::<usize>() as f64 / active as f64;
8290            let median = m_dist[active / 2];
8291            let max_m = *m_dist.last().unwrap();
8292            let min_m = m_dist[0];
8293            let above16 = m_dist.iter().filter(|&&x| x >= 16).count();
8294            println!(
8295                "moe-grouped il={il} t={t} active={active}/{n_expert} \
8296                      m_e: min={min_m} median={median} mean={mean:.1} max={max_m} \
8297                      above_gemm_threshold(>=16)={above16}/{active}"
8298            );
8299        }
8300
8301        Self::moe_ffn_grouped_add_shared(e, m, z, t, cfg, il, &mut moe_out)?;
8302        Ok(moe_out)
8303    }
8304
8305    /// Lane-3 M2: cross-stream MoE for lockstep decode. Routes all m stream rows in one
8306    /// batch, executes fully-HBM-resident experts through the grouped gather/GEMM/scatter
8307    /// machinery at m_e>1 (weight reads amortized across streams), and assigns any expert
8308    /// with a missing projection to that row's CPU companion call (whole-expert granularity,
8309    /// same rule as the sequential frozen path). Slot-pinned accumulation keeps each row's
8310    /// expert-sum order identical to the sequential path.
8311    pub(crate) fn moe_ffn_lockstep(
8312        &self,
8313        e: &Engine,
8314        m: &MoeWeights,
8315        zbatch: &CudaSlice<f32>,
8316        mrows: usize,
8317        il: u16,
8318        max_block: usize,
8319    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
8320        use crate::moe_cache::{BlockId, PROJ_DOWN, PROJ_GATE, PROJ_UP};
8321        let cfg = &self.cfg;
8322        let moe = cfg.moe.as_ref().unwrap();
8323        let n_embd = cfg.n_embd as usize;
8324        let n_expert = moe.expert_count as usize;
8325        let n_used = moe.expert_used_count as usize;
8326        let n_ff_exp = moe.expert_ff_length as usize;
8327        // step35 per-layer SwiGLU clamp; None on every other arch / unclamped layer.
8328        let lim_exp = cfg.clamp_exp_at(il as u32);
8329        let lim_shexp = cfg.clamp_shexp_at(il as u32);
8330
8331        let logits = e.matmul(&m.gate_inp, zbatch, mrows)?;
8332        if let Some(sig) = cfg.sigmoid_router() {
8333            Self::trace_sigmoid_router_logits(e, il, mrows, n_expert, n_used, &logits, m, sig)?;
8334        }
8335        let (sel_all, w_all) = if let Some(sig) = cfg.sigmoid_router() {
8336            Self::moe_route_sigmoid_cfg(e, &logits, mrows, n_expert, n_used, m, sig)?
8337        } else {
8338            Self::moe_route_cfg(
8339                e,
8340                &logits,
8341                mrows,
8342                n_expert,
8343                n_used,
8344                m.active_experts.as_deref(),
8345            )?
8346        };
8347        Self::trace_moe_routes(il, mrows, &sel_all, &w_all)?;
8348
8349        // Residency split at whole-expert granularity against the (frozen) cache.
8350        let resident_expert: Vec<bool> = e.with_moe_cache(max_block, |c, _| {
8351            Ok((0..n_expert)
8352                .map(|ex| {
8353                    [PROJ_GATE, PROJ_UP, PROJ_DOWN]
8354                        .into_iter()
8355                        .all(|p| c.resident(BlockId::new(il, p, ex as u16)).is_some())
8356                })
8357                .collect())
8358        })?;
8359
8360        struct Group {
8361            rows: Vec<i32>,
8362            slots: Vec<i32>,
8363            weights: Vec<f32>,
8364        }
8365        let mut groups: std::collections::HashMap<usize, Group> = Default::default();
8366        let mut cpu_rows: Vec<Vec<(usize, f32)>> = vec![Vec::new(); mrows];
8367        let mut cpu_by_expert: std::collections::HashMap<usize, Vec<(usize, f32)>> =
8368            Default::default();
8369        for row in 0..mrows {
8370            for j in 0..n_used {
8371                let ex = sel_all[row * n_used + j] as usize;
8372                let w = w_all[row * n_used + j];
8373                if resident_expert[ex] {
8374                    let group = groups.entry(ex).or_insert_with(|| Group {
8375                        rows: Vec::new(),
8376                        slots: Vec::new(),
8377                        weights: Vec::new(),
8378                    });
8379                    group.rows.push(row as i32);
8380                    group.slots.push(j as i32);
8381                    group.weights.push(w);
8382                } else {
8383                    crate::cpu_experts::record_incomplete_gpu_residency(0);
8384                    cpu_rows[row].push((ex, w));
8385                    cpu_by_expert.entry(ex).or_default().push((row, w));
8386                }
8387            }
8388        }
8389
8390        // CPU tickets first: reads/compute overlap the GPU grouped work below. Experts routed
8391        // by >=2 streams go through the multi-row ABI (weight decode amortized across rows);
8392        // each row's remaining experts stay one ordinary per-row call. Contribution FP-sum
8393        // order per row differs from the sequential single-call chunk — part of the
8394        // documented lockstep numeric class.
8395        let host_rows = e.dtoh(zbatch)?;
8396        let rows_ok = crate::cpu_experts::rows_supported();
8397        enum CpuPart {
8398            Single { row: usize },
8399            Rows { rows: Vec<usize> },
8400        }
8401        let mut tickets: Vec<(CpuPart, crate::cpu_experts::CpuExpertTicket)> = Vec::new();
8402        let mut rows_served: std::collections::HashSet<(usize, usize)> = Default::default();
8403        if rows_ok {
8404            let mut shared: Vec<(usize, Vec<(usize, f32)>)> = cpu_by_expert
8405                .into_iter()
8406                .filter(|(_, rows)| rows.len() >= 2)
8407                .collect();
8408            shared.sort_by_key(|(ex, _)| *ex);
8409            for (ex, mut row_weights) in shared {
8410                row_weights.sort_by_key(|(row, _)| *row);
8411                let inputs: Vec<(&[f32], f32)> = row_weights
8412                    .iter()
8413                    .map(|&(row, w)| (&host_rows[row * n_embd..(row + 1) * n_embd], w))
8414                    .collect();
8415                let job = crate::cpu_experts::prepare_rows_job(m, ex, &inputs)
8416                    .map_err(std::io::Error::other)?;
8417                for &(row, _) in &row_weights {
8418                    rows_served.insert((row, ex));
8419                }
8420                tickets.push((
8421                    CpuPart::Rows {
8422                        rows: row_weights.iter().map(|&(row, _)| row).collect(),
8423                    },
8424                    crate::cpu_experts::submit_rows(job).map_err(std::io::Error::other)?,
8425                ));
8426            }
8427        }
8428        for (row, selected) in cpu_rows.iter().enumerate() {
8429            let leftover: Vec<(usize, f32)> = selected
8430                .iter()
8431                .copied()
8432                .filter(|&(ex, _)| !rows_served.contains(&(row, ex)))
8433                .collect();
8434            if leftover.is_empty() {
8435                continue;
8436            }
8437            let host_row = &host_rows[row * n_embd..(row + 1) * n_embd];
8438            let job = crate::cpu_experts::prepare_job(m, il, &leftover, host_row)
8439                .map_err(std::io::Error::other)?;
8440            tickets.push((
8441                CpuPart::Single { row },
8442                crate::cpu_experts::submit(job).map_err(std::io::Error::other)?,
8443            ));
8444        }
8445
8446        let mut slot_buf = e.zeros(mrows * n_used * n_embd)?;
8447        let mut wbuf = e.zeros(mrows * n_used)?;
8448        let mut order: Vec<usize> = groups.keys().copied().collect();
8449        order.sort_by(|&a, &b| {
8450            groups[&b]
8451                .rows
8452                .len()
8453                .cmp(&groups[&a].rows.len())
8454                .then(a.cmp(&b))
8455        });
8456        for &ex in &order {
8457            let group = &groups[&ex];
8458            let m_e = group.rows.len();
8459            let gl = m.gate_exps.expert_layout(ex);
8460            let ul = m.up_exps.expert_layout(ex);
8461            let dl = m.down_exps.expert_layout(ex);
8462            let row_idx_d = e.htod_i32(&group.rows)?;
8463            let slot_idx_d = e.htod_i32(&group.slots)?;
8464            let dmac = m.down_exps.macro_scale(ex);
8465            let weight_d = if dmac == 1.0 {
8466                e.htod(&group.weights)?
8467            } else {
8468                let scaled: Vec<f32> = group.weights.iter().map(|&w| w * dmac).collect();
8469                e.htod(&scaled)?
8470            };
8471            let mut gathered = e.zeros(m_e * n_embd)?;
8472            e.gather_rows(zbatch, &row_idx_d, &mut gathered, n_embd, m_e)?;
8473            let gv = gathered.slice(0..m_e * n_embd);
8474            let gate = e.with_moe_cache(max_block, |c, eng| {
8475                let slot = c
8476                    .resident(BlockId::new(il, PROJ_GATE, ex as u16))
8477                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
8478                eng.qmatvec_view(
8479                    c.buf(crate::moe_cache::DispatchSlot::Resident(slot)),
8480                    0..gl.len,
8481                    &gv,
8482                    m_e,
8483                    m.gate_exps.in_f,
8484                    m.gate_exps.out_f,
8485                    gl.qtype,
8486                    gl.row_bytes,
8487                )
8488            })?;
8489            let up = e.with_moe_cache(max_block, |c, eng| {
8490                let slot = c
8491                    .resident(BlockId::new(il, PROJ_UP, ex as u16))
8492                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
8493                eng.qmatvec_view(
8494                    c.buf(crate::moe_cache::DispatchSlot::Resident(slot)),
8495                    0..ul.len,
8496                    &gv,
8497                    m_e,
8498                    m.up_exps.in_f,
8499                    m.up_exps.out_f,
8500                    ul.qtype,
8501                    ul.row_bytes,
8502                )
8503            })?;
8504            let mut act = e.zeros(m_e * n_ff_exp)?;
8505            Self::ffn_act_lim(
8506                e,
8507                cfg,
8508                &gate,
8509                &up,
8510                m.gate_exps.macro_scale(ex),
8511                m.up_exps.macro_scale(ex),
8512                lim_exp,
8513                &mut act,
8514                m_e * n_ff_exp,
8515            )?;
8516            let actv = act.slice(0..m_e * n_ff_exp);
8517            let y = e.with_moe_cache(max_block, |c, eng| {
8518                let slot = c
8519                    .resident(BlockId::new(il, PROJ_DOWN, ex as u16))
8520                    .ok_or("lockstep resident expert vanished (cache not frozen?)")?;
8521                eng.qmatvec_view(
8522                    c.buf(crate::moe_cache::DispatchSlot::Resident(slot)),
8523                    0..dl.len,
8524                    &actv,
8525                    m_e,
8526                    m.down_exps.in_f,
8527                    m.down_exps.out_f,
8528                    dl.qtype,
8529                    dl.row_bytes,
8530                )
8531            })?;
8532            e.scatter_slot(
8533                &y,
8534                &row_idx_d,
8535                &slot_idx_d,
8536                &weight_d,
8537                &mut slot_buf,
8538                &mut wbuf,
8539                n_embd,
8540                n_used,
8541                m_e,
8542            )?;
8543        }
8544        let mut moe_out = e.zeros(mrows * n_embd)?;
8545        e.reduce_slots(&slot_buf, &wbuf, &mut moe_out, n_embd, n_used, mrows)?;
8546
8547        // CPU contributions join BEFORE the shared expert (the sequential path's placement).
8548        let mut row_sums: Vec<Option<Vec<f32>>> = vec![None; mrows];
8549        for (part, ticket) in tickets {
8550            let cpu_output = ticket.wait().map_err(std::io::Error::other)?;
8551            let mut add_row = |row: usize, chunk: &[f32]| {
8552                let sum = row_sums[row].get_or_insert_with(|| vec![0.0f32; n_embd]);
8553                for (accumulator, value) in sum.iter_mut().zip(chunk) {
8554                    *accumulator += value;
8555                }
8556            };
8557            match part {
8558                CpuPart::Single { row } => add_row(row, &cpu_output),
8559                CpuPart::Rows { rows } => {
8560                    for (slot, row) in rows.into_iter().enumerate() {
8561                        add_row(row, &cpu_output[slot * n_embd..(slot + 1) * n_embd]);
8562                    }
8563                }
8564            }
8565        }
8566        for (row, sum) in row_sums.into_iter().enumerate() {
8567            let Some(sum) = sum else { continue };
8568            let cpu_output = e.htod(&sum)?;
8569            let mut dst = moe_out.slice_mut(row * n_embd..(row + 1) * n_embd);
8570            e.axpy_into(&cpu_output, 1.0, &mut dst, n_embd)?;
8571        }
8572
8573        if let (Some(gate_shexp), Some(up_shexp), Some(down_shexp)) =
8574            (&m.gate_shexp, &m.up_shexp, &m.down_shexp)
8575        {
8576            let n_ff_sh = gate_shexp.out_features();
8577            let sg_gate = e.matmul(gate_shexp, zbatch, mrows)?;
8578            let sg_up = e.matmul(up_shexp, zbatch, mrows)?;
8579            let mut sa = e.zeros(mrows * n_ff_sh)?;
8580            Self::ffn_act_lim(
8581                e,
8582                cfg,
8583                &sg_gate,
8584                &sg_up,
8585                1.0,
8586                1.0,
8587                lim_shexp,
8588                &mut sa,
8589                mrows * n_ff_sh,
8590            )?;
8591            let sh = e.matmul(down_shexp, &sa, mrows)?;
8592            // lockstep rows ARE decode tokens: fused sigmoid-dot per row so batched serving
8593            // decode matches the single-sequence decode chain bit-for-bit.
8594            let g = match &m.gate_inp_shexp {
8595                Some(gate_inp_shexp) => {
8596                    e.sigmoid_dot_rows(zbatch, gate_inp_shexp.float_data(), n_embd, mrows)?
8597                }
8598                None => e.htod(&vec![1.0f32; mrows])?,
8599            };
8600            e.add_scaled_rows(&sh, &g, &mut moe_out, n_embd, mrows)?;
8601        }
8602
8603        Ok(moe_out)
8604    }
8605}
8606
8607// ============================ gemma4 (R8 verified wiring) ==================================
8608// Node-for-node vs llama.cpp src/models/gemma4.cpp:180-405 (HANDOVER "R8 VERIFIED WIRING").
8609// v0 bring-up: full attention everywhere (exact for prompts < sliding_window 1024 — R6 masking
8610// later), sdpa_naive (hd 512 has no FA stamp), sequential host-staged MoE (the perf arms grow
8611// gemma variants after the correctness gate).
8612impl HybridModel {
8613    /// Per-layer attention geometry (R5): (head_dim, n_kv, n_head, rope_base, scale, is_swa).
8614    pub(crate) fn gemma4_geom(&self, il: usize) -> (usize, usize, usize, f32, f32, bool) {
8615        let g = self.cfg.gemma4.as_ref().unwrap();
8616        let swa = g.swa_pattern[il];
8617        let hd = if swa {
8618            g.key_length_swa
8619        } else {
8620            g.key_length_global
8621        } as usize;
8622        // attention scale = 1.0 (llama gemma4.cpp:11 "Gemma4 uses self.scaling = 1.0" — q/k are
8623        // per-head rms-normed; NOT the 1/sqrt(hd) default. Bring-up bug: 1/sqrt(hd) left token-0
8624        // rows exact (softmax over one element) while every later position drifted).
8625        (
8626            hd,
8627            g.head_count_kv[il] as usize,
8628            self.cfg.n_head as usize,
8629            if swa {
8630                g.rope_base_swa
8631            } else {
8632                g.rope_base_global
8633            },
8634            1.0,
8635            swa,
8636        )
8637    }
8638
8639    /// Suppress-token mask over t logits rows (tokenizer.ggml.suppress_tokens; no-op when the
8640    /// model ships none). NOT monotonic like softcap — must run before every argmax/sample, so
8641    /// every gemma4 logits tail (forward/prime/decode/dc/verify/e4b) calls this on device ld.
8642    fn gemma4_suppress(
8643        &self,
8644        e: &Engine,
8645        ld: &mut CudaSlice<f32>,
8646        t: usize,
8647    ) -> Result<(), Box<dyn std::error::Error>> {
8648        if let Some((ids, n)) = self.gemma4_aux.as_ref().and_then(|a| a.suppress_d.as_ref()) {
8649            // suppress_d is primary-owned by the audited invariant (serving picks the last/head
8650            // stage as primary, and this tail runs only after the last stage). The assert turns
8651            // that argued invariant into a checked one: any topology violating primary==head
8652            // trips here in debug instead of silently peer-reading a device-0 buffer.
8653            #[cfg(debug_assertions)]
8654            crate::debug_assert_tensor_stream_device(
8655                ids,
8656                &e.stream(),
8657                "gemma4_suppress.suppress_d",
8658            );
8659            e.mask_ids_rows(ld, ids, *n, self.output.out_features(), t)?;
8660        }
8661        Ok(())
8662    }
8663
8664    /// gemma4 attention (R5 geometry, R7 weightless V-norm on the RAW K projection, R9 dual rope).
8665    /// `cache`: Some => PRIME mode — append the T post-rope K / normed V rows into the quantized
8666    /// KV cache (same per-row quantize math as the decode append) and advance len. Fresh-prompt
8667    /// only (v0): attends within `tokens` via the f32 sdpa.
8668    fn gemma4_attn_prime(
8669        &self,
8670        e: &Engine,
8671        fa: &crate::hybrid::FullAttnLayer,
8672        il: usize,
8673        h: &CudaSlice<f32>,
8674        pos_d: &CudaSlice<i32>,
8675        t: usize,
8676        cache: Option<&mut Cache>,
8677    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
8678        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
8679        let eps = self.cfg.rms_eps;
8680        let aux = self.gemma4_aux.as_ref().unwrap();
8681        let ones = aux.ones(e);
8682        #[cfg(debug_assertions)]
8683        crate::debug_assert_tensor_stream_device(ones, &e.stream(), "gemma4_attn_prime.ones");
8684
8685        // quantize-once window: q/k/v share `h` — the MMQ D4 activation quantizes once
8686        // (h stays borrowed across the triple, so the cache key can't go stale).
8687        e.mmq_act_begin();
8688        let q0 = e.matmul(&fa.wq, h, t)?; // [t, nh*hd]
8689        let k0 = e.matmul(&fa.wk, h, t)?; // [t, nkv*hd]
8690        // globals ship no v_proj (wv := wk at load) — V input is the SAME projection output;
8691        // reuse k0 instead of re-running the identical matmul (K=V dedup, 5 layers).
8692        let v0 = if swa {
8693            e.matmul(&fa.wv, h, t)?
8694        } else {
8695            e.clone_dtod(&k0)?
8696        };
8697
8698        let mut q = e.uninit(t * nh * hd)?;
8699        let mut k = e.uninit(t * nkv * hd)?;
8700        // R7: V = weightless rms_norm of the raw projection; NEVER roped.
8701        let mut v = e.uninit(t * nkv * hd)?;
8702        // 31B glue lane: producers emit the bf16 FA operands (norm emits vb; rope emits qb/kb
8703        // post-rope) — kills 3 f32->bf16 converts + re-reads per layer. Bit-identical operands
8704        // (same __float2bfloat16); MEMRA_FA_EMIT=0 reverts to the convert-in-FA path.
8705        static EMIT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
8706        let emit = t >= 16
8707            && crate::Engine::qkvnorm_w_on_prefill(nh * t + 2 * nkv * t, hd)
8708            && *EMIT.get_or_init(|| {
8709                std::env::var("MEMRA_FA_EMIT")
8710                    .map(|s| s != "0")
8711                    .unwrap_or(true)
8712            });
8713        let mut qb = e.alloc_uninit::<u8>(if emit { t * nh * hd * 2 } else { 1 })?;
8714        let mut kb = e.alloc_uninit::<u8>(if emit { t * nkv * hd * 2 } else { 1 })?;
8715        let mut vb = e.alloc_uninit::<u8>(if emit { t * nkv * hd * 2 } else { 1 })?;
8716        // f16-P/V door: emit V as f16 straight from the norm when this layer's FA consumer
8717        // reads f16 — kills the per-layer bf16->f16 re-encode (1.4GB/pass on 31B SWA).
8718        let v_f16 = emit
8719            && crate::fa_f16pv_on()
8720            && match hd {
8721                512 => true,
8722                256 => swa && crate::faw_hp_on() && nh % 2 == 0 && (nh / nkv) % 2 == 0,
8723                _ => false,
8724            };
8725        if emit {
8726            e.rms_norm_qkv_w4b(
8727                &q0,
8728                &k0,
8729                &v0,
8730                fa.q_norm.float_data(),
8731                fa.k_norm.float_data(),
8732                ones,
8733                &mut q,
8734                &mut k,
8735                &mut v,
8736                &mut vb,
8737                hd,
8738                nh * t,
8739                nkv * t,
8740                eps,
8741                v_f16,
8742            )?;
8743        } else {
8744            e.rms_norm_qkv(
8745                &q0,
8746                &k0,
8747                &v0,
8748                fa.q_norm.float_data(),
8749                fa.k_norm.float_data(),
8750                ones,
8751                &mut q,
8752                &mut k,
8753                &mut v,
8754                hd,
8755                nh * t,
8756                nkv * t,
8757                eps,
8758            )?;
8759        }
8760
8761        let ff = if swa {
8762            None
8763        } else {
8764            Some(
8765                aux.rope_freqs(e)
8766                    .expect("gemma4 global rope needs rope_freqs.weight"),
8767            )
8768        };
8769        #[cfg(debug_assertions)]
8770        if let Some(ff) = ff {
8771            crate::debug_assert_tensor_stream_device(
8772                ff,
8773                &e.stream(),
8774                "gemma4_attn_prime.rope_freqs",
8775            );
8776        }
8777        if emit {
8778            e.rope_neox2_bf16e(
8779                &mut q, &mut k, &mut qb, &mut kb, pos_d, hd, hd, nh, nkv, t, base, 1.0, ff,
8780            )?;
8781        } else {
8782            e.rope_neox2(&mut q, &mut k, pos_d, hd, hd, nh, nkv, t, base, 1.0, ff)?;
8783        }
8784
8785        if let Some(cache) = cache {
8786            let kvl = cache.kv[il].as_mut().unwrap();
8787            assert_eq!(kvl.len, 0, "gemma4 prime is fresh-prompt only (v0)");
8788            e.append_kv_quantized_rows(
8789                &k,
8790                &v,
8791                &mut kvl.k,
8792                &mut kvl.v,
8793                kvl.len,
8794                t,
8795                kvl.kv_dim_k,
8796                kvl.kv_dim_v,
8797                kvl.k_tok_bytes,
8798                kvl.v_tok_bytes,
8799                (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()),
8800            )?;
8801            kvl.len += t;
8802        }
8803        let mut attn = e.zeros(t * nh * hd)?;
8804        // R6: SWA layers mask keys older than sliding_window once the prompt exceeds it
8805        // (windowed naive twin; fa windowed stamps later). Under the window, full attention
8806        // is exact — SWA rides fa_prefill (hd-256 stamp), the hd-512 globals stay naive.
8807        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
8808        if swa && t > win {
8809            if hd == 256 && std::env::var("MEMRA_NOFA").is_err() {
8810                if emit {
8811                    e.fa_prefill_w_pre(
8812                        &qb, &kb, &vb, &mut attn, hd, nh, nkv, t, t, scale, true, win, v_f16,
8813                    )?;
8814                } else {
8815                    e.fa_prefill_w(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
8816                }
8817            } else {
8818                e.sdpa_naive_w(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
8819            }
8820        } else if hd == 256 && std::env::var("MEMRA_NOFA").is_err() {
8821            e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
8822        } else if hd == 512 && std::env::var("MEMRA_NOFA").is_err() {
8823            if emit {
8824                e.fa_prefill_hd512_pre(
8825                    &qb, &kb, &vb, &mut attn, hd, nh, nkv, t, t, scale, true, v_f16,
8826                )?;
8827            } else {
8828                e.fa_prefill_hd512(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
8829            }
8830        } else {
8831            e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
8832        }
8833        Ok(e.matmul(&fa.wo, &attn, t)?)
8834    }
8835
8836    /// Back-compat wrapper (pure prefill, no cache).
8837    fn gemma4_attn(
8838        &self,
8839        e: &Engine,
8840        fa: &crate::hybrid::FullAttnLayer,
8841        il: usize,
8842        h: &CudaSlice<f32>,
8843        pos_d: &CudaSlice<i32>,
8844        t: usize,
8845    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
8846        self.gemma4_attn_prime(e, fa, il, h, pos_d, t, None)
8847    }
8848
8849    /// gemma4 MoE with the expert input PRE-QUANTIZED (the q8z tail fusion). Caller guarantees
8850    /// the fast-arm conditions (resident dev slabs + dp4a qtypes) — decode t=1 and verify rows
8851    /// arms only, per-token kernel chains identical to the f32-input path (same quantize bytes:
8852    /// the q8z epilogue is quantize_q8_1 verbatim).
8853    fn gemma4_moe_q8(
8854        &self,
8855        e: &Engine,
8856        m: &crate::hybrid::MoeWeights,
8857        bits: &crate::hybrid::Gemma4MoeBits,
8858        mq: &(CudaSlice<i8>, CudaSlice<f32>),
8859        router_in: &CudaSlice<f32>,
8860        t: usize,
8861    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
8862        let cfg = &self.cfg;
8863        let moe = cfg.moe.as_ref().unwrap();
8864        let n_embd = cfg.n_embd as usize;
8865        let n_expert = moe.expert_count as usize;
8866        let n_used = moe.expert_used_count as usize;
8867        let n_ff_exp = moe.expert_ff_length as usize;
8868        // router stays the two-launch pair: THREE fuse variants measured worse (serial-dot
8869        // -50%, 1024-thread warp-parallel -12% on topk sync overhead — jsonl 2026-07-11);
8870        // the pair's 12us is kernel time, not launch gaps.
8871        let logits = if crate::router_kernel_on() {
8872            e.router_gemv(m.gate_inp.float_data(), router_in, n_embd, n_expert, t)?
8873        } else {
8874            e.matmul(&m.gate_inp, router_in, t)?
8875        };
8876        let dev = m.dev_exps.as_ref().unwrap();
8877        let (sel_d, w_d) =
8878            e.moe_router_topk_scaled(&logits, t, n_expert, n_used, &bits.per_expert_scale_d)?;
8879        let (zq, zd) = mq;
8880        if t == 1 {
8881            let selv = sel_d.slice(0..n_used);
8882            let wv = w_d.slice(0..n_used);
8883            let act = e.moe_gate_up_gelu8_dev_q8(
8884                &dev.ptr_row,
8885                &selv,
8886                zq,
8887                zd,
8888                n_embd,
8889                n_ff_exp,
8890                n_used,
8891                n_expert,
8892                m.gate_exps.qtype,
8893                m.up_exps.qtype,
8894                m.gate_exps.row_bytes,
8895                m.up_exps.row_bytes,
8896            )?;
8897            let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
8898            let mut moe_out = e.uninit(n_embd)?;
8899            e.moe_down8_fma_dev_q8(
8900                &dev.ptr_row,
8901                &selv,
8902                &wv,
8903                &aq2,
8904                &ad2,
8905                &mut moe_out.slice_mut(0..n_embd),
8906                n_ff_exp,
8907                n_embd,
8908                n_used,
8909                n_expert,
8910                m.down_exps.qtype,
8911                m.down_exps.row_bytes,
8912            )?;
8913            return Ok(moe_out);
8914        }
8915        let csr = t <= 10 && std::env::var("MEMRA_GEMMA_CSR").as_deref() != Ok("0");
8916        let act = if csr {
8917            e.moe_gate_up_gelu8_dev_q8_csr(
8918                &dev.ptr_row,
8919                &sel_d,
8920                zq,
8921                zd,
8922                t * n_used,
8923                n_embd,
8924                n_ff_exp,
8925                n_used,
8926                n_expert,
8927                m.gate_exps.qtype,
8928                m.up_exps.qtype,
8929                m.gate_exps.row_bytes,
8930                m.up_exps.row_bytes,
8931            )?
8932        } else {
8933            e.moe_gate_up_gelu8_dev_q8_rows(
8934                &dev.ptr_row,
8935                &sel_d,
8936                zq,
8937                zd,
8938                t,
8939                n_embd,
8940                n_ff_exp,
8941                n_used,
8942                n_expert,
8943                m.gate_exps.qtype,
8944                m.up_exps.qtype,
8945                m.gate_exps.row_bytes,
8946                m.up_exps.row_bytes,
8947            )?
8948        };
8949        let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
8950        let mut moe_out = e.uninit(t * n_embd)?;
8951        // down stays rows_g: the CSR dedup twin measured NEGATIVE at nsb=22 too (189.4 vs
8952        // 207.0 depth spec, bitwise-exact — jsonl 2026-07-10; qwen nsb=16 same verdict).
8953        e.moe_down8_fma_dev_q8_rows_g(
8954            &dev.ptr_row,
8955            &sel_d,
8956            &w_d,
8957            &aq2,
8958            &ad2,
8959            &mut moe_out,
8960            t,
8961            n_ff_exp,
8962            n_embd,
8963            n_used,
8964            n_expert,
8965            m.down_exps.qtype,
8966            m.down_exps.row_bytes,
8967        )?;
8968        Ok(moe_out)
8969    }
8970
8971    /// gemma4 MoE (R2 router prologue input supplied by caller, R3 per-expert output scale).
8972    /// Sequential host-staged v0 — softmax gating + renorm (moe_route, the qwen recipe), GELU
8973    /// experts, scale folded into the accumulate weight (post-matmul linear scale, exact fold).
8974    fn gemma4_moe(
8975        &self,
8976        e: &Engine,
8977        m: &crate::hybrid::MoeWeights,
8978        bits: &crate::hybrid::Gemma4MoeBits,
8979        moe_in: &CudaSlice<f32>,
8980        router_in: &CudaSlice<f32>,
8981        t: usize,
8982    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
8983        let cfg = &self.cfg;
8984        let moe = cfg.moe.as_ref().unwrap();
8985        let n_embd = cfg.n_embd as usize;
8986        let n_expert = moe.expert_count as usize;
8987        let n_used = moe.expert_used_count as usize;
8988        let n_ff_exp = moe.expert_ff_length as usize;
8989
8990        // Router: the in-house GEMV for ALL small t (decode AND verify ride the same per-column
8991        // kernel — the cuBLASLt n-dependence flipped top-k at verify t on the 27B, d994271);
8992        // batched matmul only at real prefill.
8993        let logits = if t < PRIME_MIN_T && crate::router_kernel_on() {
8994            e.router_gemv(m.gate_inp.float_data(), router_in, n_embd, n_expert, t)?
8995        } else {
8996            e.matmul(&m.gate_inp, router_in, t)?
8997        };
8998
8999        // FAST SMALL-T ARM (decode t=1 AND spec verify t=2..15): device softmax-topk router,
9000        // then PER TOKEN the same fused gate_up GELU + down8 FMA launch pair over the resident
9001        // dev slabs — verify rides the EXACT decode kernel chain per token (dispatch-parity
9002        // law; the qwen "verify must be kernel-dispatch-identical to decode" lesson).
9003        if t < PRIME_MIN_T
9004            && m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
9005            && expert_dp4a_supported(m.gate_exps.qtype)
9006            && expert_dp4a_supported(m.up_exps.qtype)
9007            && expert_dp4a_supported(m.down_exps.qtype)
9008            && std::env::var("MEMRA_GEMMA_MOE_FAST").as_deref() != Ok("0")
9009        {
9010            let dev = m.dev_exps.as_ref().unwrap();
9011            let (sel_d, w_d) =
9012                e.moe_router_topk_scaled(&logits, t, n_expert, n_used, &bits.per_expert_scale_d)?;
9013            if t == 1 {
9014                let (zq, zd) = e.quantize_q8_1(moe_in, 1, n_embd)?;
9015                let selv = sel_d.slice(0..n_used);
9016                let wv = w_d.slice(0..n_used);
9017                let act = e.moe_gate_up_gelu8_dev_q8(
9018                    &dev.ptr_row,
9019                    &selv,
9020                    &zq,
9021                    &zd,
9022                    n_embd,
9023                    n_ff_exp,
9024                    n_used,
9025                    n_expert,
9026                    m.gate_exps.qtype,
9027                    m.up_exps.qtype,
9028                    m.gate_exps.row_bytes,
9029                    m.up_exps.row_bytes,
9030                )?;
9031                let (aq2, ad2) = e.quantize_q8_1(&act, n_used, n_ff_exp)?;
9032                let mut moe_out = e.uninit(n_embd)?;
9033                e.moe_down8_fma_dev_q8(
9034                    &dev.ptr_row,
9035                    &selv,
9036                    &wv,
9037                    &aq2,
9038                    &ad2,
9039                    &mut moe_out.slice_mut(0..n_embd),
9040                    n_ff_exp,
9041                    n_embd,
9042                    n_used,
9043                    n_expert,
9044                    m.down_exps.qtype,
9045                    m.down_exps.row_bytes,
9046                )?;
9047                return Ok(moe_out);
9048            }
9049            // VERIFY ROWS TWINS (t=2..15): ONE launch pair for all tokens; per (token,row,slot)
9050            // bodies are the t=1 kernels VERBATIM (bit-identical to the per-token loop).
9051            // gate_up rides the CSR owner-scan dedup when t <= 10 (each duplicated expert's
9052            // weight stream decoded once; the qwen CSR class). MEMRA_GEMMA_CSR=0 -> rows.
9053            let (zq, zd) = e.quantize_q8_1(moe_in, t, n_embd)?;
9054            let csr = t <= 10 && std::env::var("MEMRA_GEMMA_CSR").as_deref() != Ok("0");
9055            let act = if csr {
9056                e.moe_gate_up_gelu8_dev_q8_csr(
9057                    &dev.ptr_row,
9058                    &sel_d,
9059                    &zq,
9060                    &zd,
9061                    t * n_used,
9062                    n_embd,
9063                    n_ff_exp,
9064                    n_used,
9065                    n_expert,
9066                    m.gate_exps.qtype,
9067                    m.up_exps.qtype,
9068                    m.gate_exps.row_bytes,
9069                    m.up_exps.row_bytes,
9070                )?
9071            } else {
9072                e.moe_gate_up_gelu8_dev_q8_rows(
9073                    &dev.ptr_row,
9074                    &sel_d,
9075                    &zq,
9076                    &zd,
9077                    t,
9078                    n_embd,
9079                    n_ff_exp,
9080                    n_used,
9081                    n_expert,
9082                    m.gate_exps.qtype,
9083                    m.up_exps.qtype,
9084                    m.gate_exps.row_bytes,
9085                    m.up_exps.row_bytes,
9086                )?
9087            };
9088            let (aq2, ad2) = e.quantize_q8_1(&act, t * n_used, n_ff_exp)?;
9089            let mut moe_out = e.uninit(t * n_embd)?;
9090            e.moe_down8_fma_dev_q8_rows_g(
9091                &dev.ptr_row,
9092                &sel_d,
9093                &w_d,
9094                &aq2,
9095                &ad2,
9096                &mut moe_out,
9097                t,
9098                n_ff_exp,
9099                n_embd,
9100                n_used,
9101                n_expert,
9102                m.down_exps.qtype,
9103                m.down_exps.row_bytes,
9104            )?;
9105            return Ok(moe_out);
9106        }
9107
9108        let (sel_all, mut w_all) = Self::moe_route(e, &logits, t, n_expert, n_used)?;
9109        for (i, &sx) in sel_all.iter().enumerate() {
9110            w_all[i] *= bits.per_expert_scale[sx as usize];
9111        }
9112
9113        // PREFILL PAIRS ARM (t >= 16): expert-major CSR over the resident slabs — ONE launch per
9114        // projection covers ALL (token,expert) pairs (the qwen pairs recipe, _em dot = expert_dot_g
9115        // which has the Q4_0 body; GELU pairs epilogue; R3 scale folded into pair_w).
9116        if t >= PRIME_MIN_T
9117            && m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
9118            && expert_dp4a_supported(m.gate_exps.qtype)
9119            && expert_dp4a_supported(m.up_exps.qtype)
9120            && expert_dp4a_supported(m.down_exps.qtype)
9121            && std::env::var("MEMRA_GEMMA_MOE_PAIRS").as_deref() != Ok("0")
9122        {
9123            let dev = m.dev_exps.as_ref().unwrap();
9124            let n_pairs = t * n_used;
9125            let pair_ex: Vec<i32> = sel_all.iter().map(|&x| x as i32).collect();
9126            let pair_tok: Vec<i32> = (0..n_pairs).map(|p| (p / n_used) as i32).collect();
9127            let tok_off: Vec<i32> = (0..=t).map(|tok| (tok * n_used) as i32).collect();
9128            let tok_ids: Vec<i32> = (0..n_pairs as i32).collect();
9129            let pt = e.htod_i32(&pair_tok)?;
9130            let pw = e.htod(&w_all)?;
9131            let toff = e.htod_i32(&tok_off)?;
9132            let tids = e.htod_i32(&tok_ids)?;
9133            let mut by_ex: Vec<Vec<i32>> = vec![Vec::new(); n_expert];
9134            for p in 0..n_pairs {
9135                by_ex[pair_ex[p] as usize].push(p as i32);
9136            }
9137            let mut ex_ids: Vec<i32> = Vec::new();
9138            let mut ex_off: Vec<i32> = vec![0];
9139            let mut ex_pairs: Vec<i32> = Vec::with_capacity(n_pairs);
9140            for (ex, list) in by_ex.iter().enumerate() {
9141                if list.is_empty() {
9142                    continue;
9143                }
9144                ex_ids.push(ex as i32);
9145                ex_pairs.extend_from_slice(list);
9146                ex_off.push(ex_pairs.len() as i32);
9147            }
9148            let n_active = ex_ids.len();
9149            let exi = e.htod_i32(&ex_ids)?;
9150            let exo = e.htod_i32(&ex_off)?;
9151            let exp_d = e.htod_i32(&ex_pairs)?;
9152            // GROUPED f16 LANE (MEMRA_MOE_F16G=1, round 46 arc 2): dequant active experts to
9153            // f16 once per projection + one grouped f16 GEMM over the CSR groups; CSR order
9154            // end-to-end (gelu is elementwise), one row permute before the scatter. The
9155            // ragged down k (704) needs no padding here — cublas takes any k.
9156            // f16-mirror numeric class, argmax/spec gated. PER-MODEL default OFF (round 50):
9157            // the gelu class regressed g26 board-2048 prefill -8.3% under the round-49
9158            // Hopper default — see moe_f16g_gemma_on.
9159            if crate::moe_f16g_gemma_on()
9160                && f16g_proj_ok(m.gate_exps.qtype, n_embd)
9161                && f16g_proj_ok(m.up_exps.qtype, n_embd)
9162                && f16g_proj_ok(m.down_exps.qtype, n_ff_exp)
9163            {
9164                let csr_tok: Vec<i32> = ex_pairs.iter().map(|&p| p / n_used as i32).collect();
9165                let csr_tok_d = e.htod_i32(&csr_tok)?;
9166                let (z_f16, z_s) = e.moe_f16g_act(moe_in, Some(&csr_tok_d), n_embd, n_pairs)?;
9167                let g_csr = e.moe_f16_grouped(
9168                    &dev.ptr_row,
9169                    0,
9170                    n_expert,
9171                    &exi,
9172                    &ex_off,
9173                    &exo,
9174                    &z_f16,
9175                    &z_s,
9176                    n_embd,
9177                    n_ff_exp,
9178                    n_active,
9179                    n_pairs,
9180                    m.gate_exps.qtype,
9181                    m.gate_exps.row_bytes,
9182                )?;
9183                let u_csr = e.moe_f16_grouped(
9184                    &dev.ptr_row,
9185                    1,
9186                    n_expert,
9187                    &exi,
9188                    &ex_off,
9189                    &exo,
9190                    &z_f16,
9191                    &z_s,
9192                    n_embd,
9193                    n_ff_exp,
9194                    n_active,
9195                    n_pairs,
9196                    m.up_exps.qtype,
9197                    m.up_exps.row_bytes,
9198                )?;
9199                let act_csr = e.moe_pairs_gelu_mul(&g_csr, &u_csr, n_pairs * n_ff_exp)?;
9200                let (a_f16, a_s) = e.moe_f16g_act(&act_csr, None, n_ff_exp, n_pairs)?;
9201                let d_csr = e.moe_f16_grouped(
9202                    &dev.ptr_row,
9203                    2,
9204                    n_expert,
9205                    &exi,
9206                    &ex_off,
9207                    &exo,
9208                    &a_f16,
9209                    &a_s,
9210                    n_ff_exp,
9211                    n_embd,
9212                    n_active,
9213                    n_pairs,
9214                    m.down_exps.qtype,
9215                    m.down_exps.row_bytes,
9216                )?;
9217                let y_down = e.rows_permute(&d_csr, &exp_d, n_pairs, n_embd)?;
9218                let mut moe_out = e.uninit(t * n_embd)?;
9219                e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
9220                if std::env::var("MEMRA_F16G_DEBUG").is_ok() {
9221                    let scan = |v: &[f32]| v.iter().filter(|x| !x.is_finite()).count();
9222                    let (yd, mo) = (e.dtoh(&y_down)?, e.dtoh(&moe_out)?);
9223                    eprintln!(
9224                        "[f16g-debug] post-permute bad={} post-scatter bad={}",
9225                        scan(&yd),
9226                        scan(&mo)
9227                    );
9228                }
9229                return Ok(moe_out);
9230            }
9231            // gate/up: int8-MMA expert GEMM (in_f = n_embd 2816 = 11x256 tiles ok); down keeps
9232            // the decode-once dp4a (in_f 704 fails the 256-superblock tiling).
9233            let mma =
9234                n_embd % 256 == 0 && std::env::var("MEMRA_GEMMA_MOE_MMA").as_deref() != Ok("0");
9235            let (gate, up) = if mma {
9236                let z_scr = e.mmq_iq_quantize_act(moe_in, n_embd, t)?;
9237                (
9238                    e.mmq_iq_experts(
9239                        &dev.ptr_row,
9240                        0,
9241                        n_expert,
9242                        &exi,
9243                        &exo,
9244                        &exp_d,
9245                        &pt,
9246                        &z_scr,
9247                        n_embd,
9248                        n_ff_exp,
9249                        n_active,
9250                        n_pairs,
9251                        t,
9252                        m.gate_exps.qtype,
9253                        m.gate_exps.row_bytes,
9254                    )?,
9255                    e.mmq_iq_experts(
9256                        &dev.ptr_row,
9257                        1,
9258                        n_expert,
9259                        &exi,
9260                        &exo,
9261                        &exp_d,
9262                        &pt,
9263                        &z_scr,
9264                        n_embd,
9265                        n_ff_exp,
9266                        n_active,
9267                        n_pairs,
9268                        t,
9269                        m.up_exps.qtype,
9270                        m.up_exps.row_bytes,
9271                    )?,
9272                )
9273            } else {
9274                let (zq, zd) = e.quantize_q8_1(moe_in, t, n_embd)?;
9275                (
9276                    e.moe_pairs_matvec_q8_dec(
9277                        &dev.ptr_row,
9278                        0,
9279                        &exi,
9280                        &exo,
9281                        &exp_d,
9282                        &pt,
9283                        &zq,
9284                        &zd,
9285                        n_embd,
9286                        n_ff_exp,
9287                        n_expert,
9288                        n_active,
9289                        n_pairs,
9290                        m.gate_exps.qtype,
9291                        m.gate_exps.row_bytes,
9292                    )?,
9293                    e.moe_pairs_matvec_q8_dec(
9294                        &dev.ptr_row,
9295                        1,
9296                        &exi,
9297                        &exo,
9298                        &exp_d,
9299                        &pt,
9300                        &zq,
9301                        &zd,
9302                        n_embd,
9303                        n_ff_exp,
9304                        n_expert,
9305                        n_active,
9306                        n_pairs,
9307                        m.up_exps.qtype,
9308                        m.up_exps.row_bytes,
9309                    )?,
9310                )
9311            };
9312            let pair_self: Vec<i32> = (0..n_pairs as i32).collect();
9313            let pself = e.htod_i32(&pair_self)?;
9314            // DOWN through the int8-MMA expert GEMM (2026-07-31, g26 prefill lever): the
9315            // ragged k (n_ff_exp=704 on the 26B) rides a PADDED k-walk — in_f rounds up
9316            // to the 256-val superblock (768) while the act quantizer's zero padding
9317            // makes every padded-k product exactly zero (weight overread bytes multiply
9318            // zero int8 act values; the dev slab carries 144B tail slack for the OOB).
9319            // The old dp4a matvec was 11.3ms/call at m=T (the 0.07x prefill wall).
9320            // MEMRA_GEMMA_MOE_MMA=0 reverts down together with gate/up.
9321            // FUSED ACT-EPILOGUE (default on): gelu_tanh(gate)*up + D4 quantize in one
9322            // launch — no f32 act buffer (the fused kernel zero-pads the ragged tail
9323            // exactly like the two-pass quantizer). MEMRA_MOE_FUSE_ACTQ=0 rollback.
9324            // Scratch bytes are BYTE-IDENTICAL (kernel-check gated).
9325            let y_down = if mma {
9326                let in_pad = n_ff_exp.div_ceil(256) * 256;
9327                let a_scr = if crate::moe_fuse_actq_on() {
9328                    e.mmq_iq_fused_act_quant(&gate, &up, n_ff_exp, n_pairs, 1)?
9329                } else {
9330                    let act = e.moe_pairs_gelu_mul(&gate, &up, n_pairs * n_ff_exp)?;
9331                    e.mmq_iq_quantize_act(&act, n_ff_exp, n_pairs)?
9332                };
9333                e.mmq_iq_experts(
9334                    &dev.ptr_row,
9335                    2,
9336                    n_expert,
9337                    &exi,
9338                    &exo,
9339                    &exp_d,
9340                    &pself,
9341                    &a_scr,
9342                    in_pad,
9343                    n_embd,
9344                    n_active,
9345                    n_pairs,
9346                    n_pairs,
9347                    m.down_exps.qtype,
9348                    m.down_exps.row_bytes,
9349                )?
9350            } else {
9351                let act = e.moe_pairs_gelu_mul(&gate, &up, n_pairs * n_ff_exp)?;
9352                let (aq2, ad2) = e.quantize_q8_1(&act, n_pairs, n_ff_exp)?;
9353                e.moe_pairs_matvec_q8_dec(
9354                    &dev.ptr_row,
9355                    2,
9356                    &exi,
9357                    &exo,
9358                    &exp_d,
9359                    &pself,
9360                    &aq2,
9361                    &ad2,
9362                    n_ff_exp,
9363                    n_embd,
9364                    n_expert,
9365                    n_active,
9366                    n_pairs,
9367                    m.down_exps.qtype,
9368                    m.down_exps.row_bytes,
9369                )?
9370            };
9371            let mut moe_out = e.uninit(t * n_embd)?;
9372            e.moe_pairs_scatter(&y_down, &pw, &toff, &tids, &mut moe_out, t, n_embd)?;
9373            return Ok(moe_out);
9374        }
9375
9376        let g_len = m.gate_exps.expert_stride;
9377        let u_len = m.up_exps.expert_stride;
9378        let d_len = m.down_exps.expert_stride;
9379        // Resident dev slabs (fits-VRAM regime): read each expert straight from the device slab
9380        // at ex*stride — zero H2D, SAME qmatvec_view kernel/bytes as the staged path. Staging is
9381        // the spill fallback.
9382        let dev = m.dev_exps.as_ref().filter(|d| !d.gu_il);
9383        let (mut sg, mut su, mut sd) = if dev.is_some() {
9384            (None, None, None)
9385        } else {
9386            (
9387                Some(e.alloc_u8_uninit(g_len)?),
9388                Some(e.alloc_u8_uninit(u_len)?),
9389                Some(e.alloc_u8_uninit(d_len)?),
9390            )
9391        };
9392        let mut moe_out = e.zeros(t * n_embd)?;
9393        for tok in 0..t {
9394            let sel = &sel_all[tok * n_used..(tok + 1) * n_used];
9395            let w = &w_all[tok * n_used..(tok + 1) * n_used];
9396            let zt = moe_in.slice(tok * n_embd..(tok + 1) * n_embd);
9397            for (j, &ex) in sel.iter().enumerate() {
9398                let ex = ex as usize;
9399                let gate = match dev {
9400                    Some(d) => e.qmatvec_view(
9401                        &d.gate,
9402                        ex * g_len..(ex + 1) * g_len,
9403                        &zt,
9404                        1,
9405                        m.gate_exps.in_f,
9406                        m.gate_exps.out_f,
9407                        m.gate_exps.qtype,
9408                        m.gate_exps.row_bytes,
9409                    )?,
9410                    None => {
9411                        let sg = sg.as_mut().unwrap();
9412                        e.stage_expert(m.gate_exps.expert_bytes(ex), sg, 0)?;
9413                        e.qmatvec_view(
9414                            sg,
9415                            0..g_len,
9416                            &zt,
9417                            1,
9418                            m.gate_exps.in_f,
9419                            m.gate_exps.out_f,
9420                            m.gate_exps.qtype,
9421                            m.gate_exps.row_bytes,
9422                        )?
9423                    }
9424                };
9425                let up = match dev {
9426                    Some(d) => e.qmatvec_view(
9427                        &d.up,
9428                        ex * u_len..(ex + 1) * u_len,
9429                        &zt,
9430                        1,
9431                        m.up_exps.in_f,
9432                        m.up_exps.out_f,
9433                        m.up_exps.qtype,
9434                        m.up_exps.row_bytes,
9435                    )?,
9436                    None => {
9437                        let su = su.as_mut().unwrap();
9438                        e.stage_expert(m.up_exps.expert_bytes(ex), su, 0)?;
9439                        e.qmatvec_view(
9440                            su,
9441                            0..u_len,
9442                            &zt,
9443                            1,
9444                            m.up_exps.in_f,
9445                            m.up_exps.out_f,
9446                            m.up_exps.qtype,
9447                            m.up_exps.row_bytes,
9448                        )?
9449                    }
9450                };
9451                let mut act = e.uninit(n_ff_exp)?;
9452                e.gelu_tanh_mul(&gate, &up, &mut act, n_ff_exp)?;
9453                let actv = act.slice(0..n_ff_exp);
9454                let y = match dev {
9455                    Some(d) => e.qmatvec_view(
9456                        &d.down,
9457                        ex * d_len..(ex + 1) * d_len,
9458                        &actv,
9459                        1,
9460                        m.down_exps.in_f,
9461                        m.down_exps.out_f,
9462                        m.down_exps.qtype,
9463                        m.down_exps.row_bytes,
9464                    )?,
9465                    None => {
9466                        let sd = sd.as_mut().unwrap();
9467                        e.stage_expert(m.down_exps.expert_bytes(ex), sd, 0)?;
9468                        e.qmatvec_view(
9469                            sd,
9470                            0..d_len,
9471                            &actv,
9472                            1,
9473                            m.down_exps.in_f,
9474                            m.down_exps.out_f,
9475                            m.down_exps.qtype,
9476                            m.down_exps.row_bytes,
9477                        )?
9478                    }
9479                };
9480                let mut dst = moe_out.slice_mut(tok * n_embd..(tok + 1) * n_embd);
9481                e.axpy_into(&y, w[j], &mut dst, n_embd)?;
9482            }
9483        }
9484        Ok(moe_out)
9485    }
9486
9487    /// One gemma4 trunk layer (R8): x -> x_next.
9488    fn gemma4_layer(
9489        &self,
9490        e: &Engine,
9491        il: usize,
9492        layer: &crate::hybrid::HybridLayer,
9493        x: &CudaSlice<f32>,
9494        pos_d: &CudaSlice<i32>,
9495        t: usize,
9496    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
9497        let n_embd = self.cfg.n_embd as usize;
9498        let eps = self.cfg.rms_eps;
9499
9500        let mut h = e.zeros(t * n_embd)?;
9501        e.rms_norm(x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
9502        let Mixer::Full(fa) = &layer.mixer else {
9503            panic!("gemma4 layer {il} not full-attn")
9504        };
9505        let o = self.gemma4_attn(e, fa, il, &h, pos_d, t)?;
9506        // gemma order: post_attention_norm applies to the ATTENTION OUTPUT, then the residual.
9507        let mut cur = e.zeros(t * n_embd)?;
9508        e.rms_norm(
9509            &o,
9510            layer.post_attn_norm.float_data(),
9511            &mut cur,
9512            n_embd,
9513            t,
9514            eps,
9515        )?;
9516        self.gemma4_layer_tail_add(e, layer, &cur, x, t)
9517    }
9518
9519    /// Everything after the attention output in a gemma4 layer: the residual add (cur + x ->
9520    /// attn_out) FUSED with the three attn_out norms, then shared FFN + router + MoE + combine +
9521    /// layer scale — shared verbatim by the prefill, decode and verify paths.
9522    fn gemma4_layer_tail_add(
9523        &self,
9524        e: &Engine,
9525        layer: &crate::hybrid::HybridLayer,
9526        cur: &CudaSlice<f32>,
9527        x: &CudaSlice<f32>,
9528        t: usize,
9529    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
9530        Ok(self.gemma4_layer_tail_add_n(e, layer, cur, x, t, None)?.0)
9531    }
9532
9533    /// tail_add with the NEXT layer's attn_norm fused into the closing add+scale (one launch
9534    /// produces both x_next and h_next — the cross-layer fusion). `next_norm` None = last layer.
9535    fn gemma4_layer_tail_add_n(
9536        &self,
9537        e: &Engine,
9538        layer: &crate::hybrid::HybridLayer,
9539        cur: &CudaSlice<f32>,
9540        x: &CudaSlice<f32>,
9541        t: usize,
9542        next_norm: Option<&CudaSlice<f32>>,
9543    ) -> Result<(CudaSlice<f32>, Option<CudaSlice<f32>>), Box<dyn std::error::Error>> {
9544        let n_embd = self.cfg.n_embd as usize;
9545        let bits = layer.gemma4.as_ref().unwrap();
9546        let (sn, attn_out) = self.gemma4_layer_tail_core(e, layer, cur, x, t)?;
9547        let mut xn = e.uninit(t * n_embd)?;
9548        match next_norm {
9549            Some(w) => {
9550                let mut hn = e.uninit(t * n_embd)?;
9551                e.add_scale_rms_norm(
9552                    &sn,
9553                    &attn_out,
9554                    bits.layer_scale,
9555                    w,
9556                    &mut xn,
9557                    &mut hn,
9558                    n_embd,
9559                    t,
9560                    self.cfg.rms_eps,
9561                )?;
9562                Ok((xn, Some(hn)))
9563            }
9564            None => {
9565                e.add_scale(&sn, &attn_out, bits.layer_scale, &mut xn, t * n_embd)?;
9566                Ok((xn, None))
9567            }
9568        }
9569    }
9570
9571    /// Tail core: attn_out = cur+x (fused with the 3 norms), both FFN branches, the combine
9572    /// norm — returns (sn, attn_out) for the closing add+scale variants.
9573    fn gemma4_layer_tail_core(
9574        &self,
9575        e: &Engine,
9576        layer: &crate::hybrid::HybridLayer,
9577        cur: &CudaSlice<f32>,
9578        x: &CudaSlice<f32>,
9579        t: usize,
9580    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
9581        self.gemma4_layer_tail_core_pn(e, layer, cur, x, t, None, false)
9582    }
9583
9584    /// tail_core with an optional PRE-NORM fold (glue-fusion lane): `pre_norm = Some(wa)`
9585    /// means `cur` is the RAW attention output and the dense entry runs
9586    /// rms(cur, wa) + residual-add + ffn_norm as ONE launch (E4B's post-attn norm fold).
9587    /// `defer_post_norm`: dense-arm exit returns RAW f0 (ffn_down output) instead of
9588    /// sn = rms(f0, post_ffw) — the caller fuses the post-norm into its residual emit
9589    /// (rms_pre_add_q8_1, E4B glue wave 5). MoE arm ignores it.
9590    fn gemma4_layer_tail_core_pn(
9591        &self,
9592        e: &Engine,
9593        layer: &crate::hybrid::HybridLayer,
9594        cur: &CudaSlice<f32>,
9595        x: &CudaSlice<f32>,
9596        t: usize,
9597        pre_norm: Option<&CudaSlice<f32>>,
9598        defer_post_norm: bool,
9599    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
9600        let n_embd = self.cfg.n_embd as usize;
9601        let eps = self.cfg.rms_eps;
9602        let bits = layer.gemma4.as_ref().unwrap();
9603
9604        // DENSE gemma4 variants (31B/E4B — no MoE, no parallel branch): attn_out = cur + x
9605        // fused with the single ffn_norm; GELU_PAR ffn; post_ffw_norm.
9606        let Some(mbits) = bits.moe_bits.as_ref() else {
9607            let crate::hybrid::Ffn::Dense {
9608                ffn_gate,
9609                ffn_up,
9610                ffn_down,
9611            } = &layer.ffn
9612            else {
9613                panic!("gemma4 dense layer without Dense ffn")
9614            };
9615            let mut attn_out = e.uninit(t * n_embd)?;
9616            let mut zsh = e.uninit(t * n_embd)?;
9617            // wave-2: with the pre-norm fold active the entry ALSO emits zsh q8_1 — the
9618            // t=1 fused2 gate/up consume the pair with no standalone quantize launch.
9619            let mut zpair: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
9620            match pre_norm {
9621                Some(wa) if t == 1 => {
9622                    zpair = Some(e.rms_pre_add_rms_norm_q8z(
9623                        cur,
9624                        wa,
9625                        x,
9626                        bits.ffn_norm.float_data(),
9627                        &mut attn_out,
9628                        &mut zsh,
9629                        n_embd,
9630                        t,
9631                        eps,
9632                    )?);
9633                }
9634                Some(wa) => e.rms_pre_add_rms_norm(
9635                    cur,
9636                    wa,
9637                    x,
9638                    bits.ffn_norm.float_data(),
9639                    &mut attn_out,
9640                    &mut zsh,
9641                    n_embd,
9642                    t,
9643                    eps,
9644                )?,
9645                None => e.add_rms_norm(
9646                    cur,
9647                    x,
9648                    bits.ffn_norm.float_data(),
9649                    &mut attn_out,
9650                    &mut zsh,
9651                    n_embd,
9652                    t,
9653                    eps,
9654                )?,
9655            }
9656            let n_ff = ffn_gate.out_features();
9657            // FFN persistent slab (counter-barrier form) FALSIFIED here 2026-07-14
9658            // (falsification #7, jsonl row): PDL glue already hides the launch boundaries
9659            // it fused; barrier + worst-segment occupancy net −0.3% (31B depth) / −2.3%
9660            // (E4B spec). Down's act dependency is all-to-all, so sentinel sync cannot
9661            // rescue segment C — the megakernel front is closed for the dense tail.
9662            let (gate, up) = if t == 1 {
9663                let (zq, zd) = match zpair {
9664                    Some(p) => p,
9665                    None => e.quantize_q8_1(&zsh, 1, n_embd)?,
9666                };
9667                match e.matmul_q4_fused2(ffn_gate, ffn_up, &zq, &zd)? {
9668                    Some(p) => p,
9669                    None => (
9670                        e.matmul_pre(ffn_gate, &zq, &zd, &zsh, 1)?,
9671                        e.matmul_pre(ffn_up, &zq, &zd, &zsh, 1)?,
9672                    ),
9673                }
9674            } else {
9675                // BATCHED FUSED2 (DEFAULT ON 2026-07-13, MEMRA_F2B=0 seam): one segmented
9676                // launch for the verify's gate+up — the up segment's blocks fill SMs as
9677                // the gate segment drains (the launch-tail mechanism behind the b-tier
9678                // plateau; first positive after six falsified in-kernel variants).
9679                static F2B: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9680                let f2b = *F2B.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
9681                let fused = if f2b {
9682                    let (zq, zd) = e.quantize_q8_1(&zsh, t, n_embd)?;
9683                    e.matmul_q4_fused2_batched(ffn_gate, ffn_up, &zq, &zd, t)?
9684                } else {
9685                    None
9686                };
9687                match fused {
9688                    Some(p) => p,
9689                    None => {
9690                        // quantize-once window: gate/up share `zsh` (borrowed across the pair).
9691                        e.mmq_act_begin();
9692                        (e.matmul(ffn_gate, &zsh, t)?, e.matmul(ffn_up, &zsh, t)?)
9693                    }
9694                }
9695            };
9696            let mut act = e.uninit(t * n_ff)?;
9697            // act quantize folds into the GELU epilogue (bit-identical q8_1 rounding);
9698            // ffn_down rides matmul_pre — one quantize launch fewer per layer.
9699            let f0 = if e.uses_q8_1_fast(ffn_down) {
9700                let upv = e.view(&up, t * n_ff);
9701                let up_all = upv.slice(0..t * n_ff);
9702                let (aq, ad) = e.gelu_tanh_mul_q8_1(&gate, &up_all, &mut act, n_ff, t)?;
9703                e.matmul_pre(ffn_down, &aq, &ad, &act, t)?
9704            } else {
9705                e.gelu_tanh_mul(&gate, &up, &mut act, t * n_ff)?;
9706                e.matmul(ffn_down, &act, t)?
9707            };
9708            if defer_post_norm {
9709                return Ok((f0, attn_out));
9710            }
9711            let mut sn = e.uninit(t * n_embd)?;
9712            e.rms_norm(
9713                &f0,
9714                bits.post_ffw_norm.float_data(),
9715                &mut sn,
9716                n_embd,
9717                t,
9718                eps,
9719            )?;
9720            return Ok((sn, attn_out));
9721        };
9722
9723        assert!(pre_norm.is_none(), "pre-norm fold is dense-entry only");
9724        // MoE variant (26B): attn_out = cur + x fused with the three attn_out norms
9725        // (ffn_norm + router-scale + pre_ffw_norm_2): ONE launch, chains verbatim. At small t
9726        // (decode + verify) the zsh and moe_in outputs are EMITTED q8_1 (both consumers are
9727        // quantized matmuls — two quantize launches + two f32 round-trips fold away).
9728        let mut attn_out = e.uninit(t * n_embd)?;
9729        let mut router_in = e.uninit(t * n_embd)?;
9730        let fast_moe = match &layer.ffn {
9731            crate::hybrid::Ffn::Moe(m) => {
9732                m.dev_exps.as_ref().is_some_and(|d| !d.gu_il)
9733                    && expert_dp4a_supported(m.gate_exps.qtype)
9734                    && expert_dp4a_supported(m.up_exps.qtype)
9735                    && expert_dp4a_supported(m.down_exps.qtype)
9736                    && std::env::var("MEMRA_GEMMA_MOE_FAST").as_deref() != Ok("0")
9737            }
9738            _ => false,
9739        };
9740        let q8z = t < PRIME_MIN_T && fast_moe;
9741        let (zsh_f32, zsh_q8, moe_q8) = if q8z {
9742            let (z0, m2) = e.add_rms_norm3_q8z(
9743                cur,
9744                x,
9745                bits.ffn_norm.float_data(),
9746                &mbits.router_scale_pre,
9747                mbits.pre_ffw_norm_2.float_data(),
9748                &mut attn_out,
9749                &mut router_in,
9750                n_embd,
9751                t,
9752                eps,
9753            )?;
9754            (None, Some(z0), Some(m2))
9755        } else {
9756            let mut zsh = e.uninit(t * n_embd)?;
9757            let mut moe_in = e.uninit(t * n_embd)?;
9758            e.add_rms_norm3(
9759                cur,
9760                x,
9761                bits.ffn_norm.float_data(),
9762                &mbits.router_scale_pre,
9763                mbits.pre_ffw_norm_2.float_data(),
9764                &mut attn_out,
9765                &mut zsh,
9766                &mut router_in,
9767                &mut moe_in,
9768                n_embd,
9769                t,
9770                eps,
9771            )?;
9772            (Some((zsh, moe_in)), None, None)
9773        };
9774        let attn_out2 = attn_out;
9775        #[allow(unused_variables)]
9776        let attn_out = &attn_out2;
9777        let n_ff = mbits.shared_gate.out_features();
9778        let (gate, up) = if let Some((zq, zd)) = zsh_q8.as_ref() {
9779            if t == 1 {
9780                match e.matmul_q4_fused2(&mbits.shared_gate, &mbits.shared_up, zq, zd)? {
9781                    Some(p) => p,
9782                    None => {
9783                        let h0 = e.zeros(0)?;
9784                        (
9785                            e.matmul_pre(&mbits.shared_gate, zq, zd, &h0, 1)?,
9786                            e.matmul_pre(&mbits.shared_up, zq, zd, &h0, 1)?,
9787                        )
9788                    }
9789                }
9790            } else {
9791                // verify t 2..15: the batched mmvq twins consume the pre-quantized pair.
9792                let h0 = e.zeros(0)?;
9793                (
9794                    e.matmul_pre(&mbits.shared_gate, zq, zd, &h0, t)?,
9795                    e.matmul_pre(&mbits.shared_up, zq, zd, &h0, t)?,
9796                )
9797            }
9798        } else {
9799            let (zsh, _) = zsh_f32.as_ref().unwrap();
9800            (
9801                e.matmul(&mbits.shared_gate, zsh, t)?,
9802                e.matmul(&mbits.shared_up, zsh, t)?,
9803            )
9804        };
9805        let mut act = e.uninit(t * n_ff)?;
9806        e.gelu_tanh_mul(&gate, &up, &mut act, t * n_ff)?;
9807        let mlp0 = e.matmul(&mbits.shared_down, &act, t)?;
9808        let crate::hybrid::Ffn::Moe(m) = &layer.ffn else {
9809            panic!("gemma4 layer not MoE")
9810        };
9811        let moe0 = match (&moe_q8, &zsh_f32) {
9812            (Some(mq), _) => self.gemma4_moe_q8(e, m, mbits, mq, &router_in, t)?,
9813            (None, Some((_, moe_in))) => self.gemma4_moe(e, m, mbits, moe_in, &router_in, t)?,
9814            _ => unreachable!(),
9815        };
9816        // post_ffw_norm_1(mlp0) + post_ffw_norm_2(moe0): one fused launch, per-row verbatim.
9817        let mut mlp = e.uninit(t * n_embd)?;
9818        let mut moe = e.uninit(t * n_embd)?;
9819        e.rms_norm2x(
9820            &mlp0,
9821            &moe0,
9822            mbits.post_ffw_norm_1.float_data(),
9823            mbits.post_ffw_norm_2.float_data(),
9824            &mut mlp,
9825            &mut moe,
9826            n_embd,
9827            t,
9828            eps,
9829        )?;
9830
9831        // combine: rms_norm(mlp + moe, post_ffw_norm) + attn_out, then the layer output scalar.
9832        // add+norm fused (add_rms_norm == add then rms_norm, kernel-check-pinned identity).
9833        let mut sum = e.uninit(t * n_embd)?;
9834        let mut sn = e.uninit(t * n_embd)?;
9835        e.add_rms_norm(
9836            &mlp,
9837            &moe,
9838            bits.post_ffw_norm.float_data(),
9839            &mut sum,
9840            &mut sn,
9841            n_embd,
9842            t,
9843            eps,
9844        )?;
9845        Ok((sn, attn_out2))
9846    }
9847
9848    /// tail_add with the next attn_norm emitted PRE-QUANTIZED q8_1 (decode/verify loops).
9849    fn gemma4_layer_tail_add_nq(
9850        &self,
9851        e: &Engine,
9852        layer: &crate::hybrid::HybridLayer,
9853        cur: &CudaSlice<f32>,
9854        x: &CudaSlice<f32>,
9855        t: usize,
9856        next_norm: Option<&CudaSlice<f32>>,
9857    ) -> Result<(CudaSlice<f32>, Option<(CudaSlice<i8>, CudaSlice<f32>)>), Box<dyn std::error::Error>>
9858    {
9859        let n_embd = self.cfg.n_embd as usize;
9860        let bits = layer.gemma4.as_ref().unwrap();
9861        let (sn, attn_out) = self.gemma4_layer_tail_core(e, layer, cur, x, t)?;
9862        let mut xn = e.uninit(t * n_embd)?;
9863        match next_norm {
9864            Some(w) => {
9865                let pair = e.add_scale_rms_norm_q8_1(
9866                    &sn,
9867                    &attn_out,
9868                    bits.layer_scale,
9869                    w,
9870                    &mut xn,
9871                    n_embd,
9872                    t,
9873                    self.cfg.rms_eps,
9874                )?;
9875                Ok((xn, Some(pair)))
9876            }
9877            None => {
9878                e.add_scale(&sn, &attn_out, bits.layer_scale, &mut xn, t * n_embd)?;
9879                Ok((xn, None))
9880            }
9881        }
9882    }
9883
9884    /// gemma4 prefill: `last_only` = forward_last semantics (lm_head on the final row only).
9885    /// R4: final logits softcapped 30*tanh(l/30) on host (monotonic — argmax unaffected).
9886    fn gemma4_forward(
9887        &self,
9888        e: &Engine,
9889        tokens: &[u32],
9890        last_only: bool,
9891    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
9892        // E4B routes to its own forward regardless of the caller's entry point (forward /
9893        // forward_last / prime paths all funnel here for gemma4).
9894        if self.is_gemma4_e4b() {
9895            return self.gemma4_e4b_forward(e, tokens, last_only);
9896        }
9897        let n_embd = self.cfg.n_embd as usize;
9898        let t = tokens.len();
9899        let pos: Vec<i32> = (0..t as i32).collect();
9900        let pos_d = e.htod_i32(&pos)?;
9901
9902        let mut x = self.embed(e, tokens)?;
9903        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
9904        // MEMRA_GEMMA_PROBE=1: per-layer trunk stats (host rms + max of the LAST token row) —
9905        // the bring-up bisect vs llama-eval-callback node stats.
9906        let probe = std::env::var("MEMRA_GEMMA_PROBE").is_ok();
9907        let stat =
9908            |e: &Engine, x: &CudaSlice<f32>, tag: &str| -> Result<(), Box<dyn std::error::Error>> {
9909                let h = e.dtoh(x)?;
9910                let bad = h.iter().filter(|v| !v.is_finite()).count();
9911                let mx = h
9912                    .iter()
9913                    .filter(|v| v.is_finite())
9914                    .fold(0.0f32, |m, v| m.max(v.abs()));
9915                eprintln!(
9916                    "[gemma-probe] {tag}: tok0_first3={:?} bad={bad} max={mx:.3e}",
9917                    &h[..3]
9918                );
9919                Ok(())
9920            };
9921        if probe {
9922            stat(e, &x, "embed")?;
9923        }
9924        for (il, layer) in self.layers.iter().enumerate() {
9925            x = self.gemma4_layer(e, il, layer, &x, &pos_d, t)?;
9926            if probe {
9927                stat(e, &x, &format!("L{il}"))?;
9928            }
9929        }
9930        let mut hn = e.zeros(t * n_embd)?;
9931        e.rms_norm(
9932            &x,
9933            self.output_norm.float_data(),
9934            &mut hn,
9935            n_embd,
9936            t,
9937            self.cfg.rms_eps,
9938        )?;
9939        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
9940        let n_vocab = self.output.out_features();
9941        let logits = if last_only {
9942            let hv = e.view(&hn, t * n_embd);
9943            let last_row = hv.slice((t - 1) * n_embd..t * n_embd);
9944            let mut hlast = e.zeros(n_embd)?;
9945            e.copy_view_into(&mut hlast, 0, &last_row, n_embd)?;
9946            let mut ld = e.matmul(&self.output, &hlast, 1)?;
9947            e.softcap(&mut ld, cap, n_vocab)?;
9948            self.gemma4_suppress(e, &mut ld, 1)?;
9949            e.dtoh(&ld)?
9950        } else {
9951            let mut ld = e.matmul(&self.output, &hn, t)?;
9952            e.softcap(&mut ld, cap, t * n_vocab)?;
9953            self.gemma4_suppress(e, &mut ld, t)?;
9954            e.dtoh(&ld)?
9955        };
9956        Ok(logits)
9957    }
9958
9959    /// gemma4 BATCHED PROMPT PRIME v0 (fresh cache only): the prefill graph over the whole
9960    /// prompt with each layer's post-rope K / weightless-normed V appended into the quantized
9961    /// KV cache (decode-append row math). Returns (last-row softcapped logits, h_seed = last
9962    /// pre-output_norm hidden, hiddens = full pre-output_norm stack [T, n_embd]).
9963    pub(crate) fn gemma4_prime(
9964        &self,
9965        e: &Engine,
9966        tokens: &[u32],
9967        cache: &mut Cache,
9968    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
9969        // Err, not assert (2026-08-07, lane/gemma4-serve-gaps): a served gemma4 prompt longer
9970        // than the worker's prefill tick used to chunk here, and chunk 2 (pos > 0) killed the
9971        // whole worker process on this line. The worker now primes gemma4 monolithically and
9972        // routes continuation suffixes tokenwise; this is the per-request backstop.
9973        if cache.pos != 0 {
9974            return Err(
9975                "gemma4 prime v0 is fresh-prompt only (no continuation/chunked prime) \
9976                        — prime the full prompt in one call or decode tokenwise"
9977                    .into(),
9978            );
9979        }
9980        let n_embd = self.cfg.n_embd as usize;
9981        let eps = self.cfg.rms_eps;
9982        let t = tokens.len();
9983        let pos: Vec<i32> = (0..t as i32).collect();
9984        let pos_d = e.htod_i32(&pos)?;
9985        let mut x = self.embed(e, tokens)?;
9986        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
9987        for (il, layer) in self.layers.iter().enumerate() {
9988            let mut h = e.zeros(t * n_embd)?;
9989            e.rms_norm(&x, layer.attn_norm.float_data(), &mut h, n_embd, t, eps)?;
9990            let Mixer::Full(fa) = &layer.mixer else {
9991                panic!("gemma4 layer not full-attn")
9992            };
9993            let o = self.gemma4_attn_prime(e, fa, il, &h, &pos_d, t, Some(cache))?;
9994            let mut cur = e.zeros(t * n_embd)?;
9995            e.rms_norm(
9996                &o,
9997                layer.post_attn_norm.float_data(),
9998                &mut cur,
9999                n_embd,
10000                t,
10001                eps,
10002            )?;
10003            x = self.gemma4_layer_tail_add(e, layer, &cur, &x, t)?;
10004            self.dflash_tap(e, cache, il, &x, t)?;
10005        }
10006        cache.pos += t;
10007        let hiddens = e.clone_dtod(&x)?;
10008        let xv = e.view(&x, t * n_embd);
10009        let last_row = xv.slice((t - 1) * n_embd..t * n_embd);
10010        let mut h_seed = e.zeros(n_embd)?;
10011        e.copy_view_into(&mut h_seed, 0, &last_row, n_embd)?;
10012        let mut hn = e.uninit(n_embd)?;
10013        e.rms_norm(
10014            &h_seed,
10015            self.output_norm.float_data(),
10016            &mut hn,
10017            n_embd,
10018            1,
10019            eps,
10020        )?;
10021        let mut ld = e.matmul(&self.output, &hn, 1)?;
10022        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
10023        e.softcap(&mut ld, cap, self.output.out_features())?;
10024        self.gemma4_suppress(e, &mut ld, 1)?;
10025        let logits = e.dtoh(&ld)?;
10026        Ok((logits, h_seed, hiddens))
10027    }
10028
10029    /// gemma4 T=1 decode attention: per-layer geometry, quantized-KV append + fa_decode
10030    /// (vec kernels at hd 256, generic scalar at the globals' hd 512), weightless V-norm,
10031    /// dual rope, scale 1.0. Takes the attn-normed input PRE-QUANTIZED (the cross-layer
10032    /// fused norm emits q8 directly — the f32 h never materializes).
10033    fn gemma4_decode_attn(
10034        &self,
10035        e: &Engine,
10036        fa: &crate::hybrid::FullAttnLayer,
10037        il: usize,
10038        hq: &CudaSlice<i8>,
10039        hdq: &CudaSlice<f32>,
10040        pos_d: &CudaSlice<i32>,
10041        cache: &mut Cache,
10042    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
10043        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
10044        let eps = self.cfg.rms_eps;
10045        let aux = self.gemma4_aux.as_ref().unwrap();
10046        let ones = aux.ones(e);
10047        #[cfg(debug_assertions)]
10048        crate::debug_assert_tensor_stream_device(ones, &e.stream(), "gemma4_decode_attn.ones");
10049        let (hq, hdq) = (hq, hdq);
10050        let h0 = e.zeros(0)?;
10051        let h = &h0;
10052        let (q0, k0, v0) = if swa {
10053            match e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, &hq, &hdq)? {
10054                Some(t3) => t3,
10055                None => (
10056                    e.matmul_pre(&fa.wq, &hq, &hdq, h, 1)?,
10057                    e.matmul_pre(&fa.wk, &hq, &hdq, h, 1)?,
10058                    e.matmul_pre(&fa.wv, &hq, &hdq, h, 1)?,
10059                ),
10060            }
10061        } else {
10062            let (q0, k0) = match e.matmul_q4_fused2(&fa.wq, &fa.wk, &hq, &hdq)? {
10063                Some(p) => p,
10064                None => (
10065                    e.matmul_pre(&fa.wq, &hq, &hdq, h, 1)?,
10066                    e.matmul_pre(&fa.wk, &hq, &hdq, h, 1)?,
10067                ),
10068            };
10069            let v0 = e.clone_dtod(&k0)?;
10070            (q0, k0, v0)
10071        };
10072        let mut q = e.uninit(nh * hd)?;
10073        let mut k = e.uninit(nkv * hd)?;
10074        let mut v = e.uninit(nkv * hd)?;
10075        // E4B wave-3 fold, m=1 completion (2026-07-23): the rows arms took this fold at
10076        // 550fcfa5; the decode-step trio kept 2 launches/layer it doesn't need.
10077        let ff = if swa {
10078            None
10079        } else {
10080            Some(
10081                aux.rope_freqs(e)
10082                    .expect("gemma4 global rope needs rope_freqs.weight"),
10083            )
10084        };
10085        #[cfg(debug_assertions)]
10086        if let Some(ff) = ff {
10087            crate::debug_assert_tensor_stream_device(
10088                ff,
10089                &e.stream(),
10090                "gemma4_decode_attn.rope_freqs",
10091            );
10092        }
10093        e.rms_norm_qkv_rope(
10094            &q0,
10095            &k0,
10096            &v0,
10097            fa.q_norm.float_data(),
10098            fa.k_norm.float_data(),
10099            ones,
10100            &mut q,
10101            &mut k,
10102            &mut v,
10103            hd,
10104            nh,
10105            nkv,
10106            pos_d,
10107            nh,
10108            nkv,
10109            base,
10110            1.0,
10111            ff,
10112            eps,
10113        )?;
10114        let kvl = cache.kv[il].as_mut().unwrap();
10115        e.append_kv_quantized(
10116            &k,
10117            &v,
10118            &mut kvl.k,
10119            &mut kvl.v,
10120            kvl.len,
10121            kvl.kv_dim_k,
10122            kvl.kv_dim_v,
10123            kvl.k_tok_bytes,
10124            kvl.v_tok_bytes,
10125            (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()),
10126        )?;
10127        kvl.len += 1;
10128        // R6 decode: SWA layers attend only the last `sliding_window` keys — a token-aligned
10129        // VIEW OFFSET into the quantized cache (keys carry absolute rope; the mask is purely
10130        // positional). Globals attend the full history.
10131        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
10132        let mut attn = e.uninit(nh * hd)?;
10133        // global layers: SAME hd512 rows twin as verify with t=1 (parity law).
10134        if !swa
10135            && hd == 512
10136            && kvl.len >= crate::fa512_min_tkv()
10137            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
10138        {
10139            let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
10140            let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
10141            // device-len: eager syncs the counter (async arg-store), dc keeps it live.
10142            let base = kvl.len as i32;
10143            e.i32_set_k(&mut kvl.len_d, base)?;
10144            e.fa_decode_rows(
10145                &q,
10146                &kp,
10147                &vp,
10148                &mut attn,
10149                hd,
10150                nh,
10151                nkv,
10152                kvl.len - 1,
10153                1,
10154                scale,
10155                kvl.k_tok_bytes,
10156                kvl.v_tok_bytes,
10157                Some((&kvl.len_d, -1)),
10158                false,
10159                false,
10160                None,
10161            )?;
10162            return Ok(e.matmul(&fa.wo, &attn, 1)?);
10163        }
10164        // windowed regime: SAME rows_w kernel as verify with t=1 (parity law — see verify_attn).
10165        if swa
10166            && kvl.len > win
10167            && hd == 256
10168            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
10169        {
10170            let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
10171            let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
10172            let base = kvl.len as i32;
10173            e.i32_set_k(&mut kvl.len_d, base)?;
10174            e.fa_decode_rows_w(
10175                &q,
10176                &kp,
10177                &vp,
10178                &mut attn,
10179                hd,
10180                nh,
10181                nkv,
10182                &kvl.len_d,
10183                -1,
10184                1,
10185                scale,
10186                win,
10187                kvl.k_tok_bytes,
10188                kvl.v_tok_bytes,
10189                None,
10190            )?;
10191            return Ok(e.matmul(&fa.wo, &attn, 1)?);
10192        }
10193        let (off_tok, t_kv) = if swa && kvl.len > win {
10194            (kvl.len - win, win)
10195        } else {
10196            (0, kvl.len)
10197        };
10198        let k_view = e.view_u8_range(
10199            &kvl.k,
10200            off_tok * kvl.k_tok_bytes,
10201            (off_tok + t_kv) * kvl.k_tok_bytes,
10202        );
10203        let v_view = e.view_u8_range(
10204            &kvl.v,
10205            off_tok * kvl.v_tok_bytes,
10206            (off_tok + t_kv) * kvl.v_tok_bytes,
10207        );
10208        e.fa_decode_kvmod(
10209            &q,
10210            &k_view,
10211            &v_view,
10212            &mut attn,
10213            hd,
10214            nh,
10215            nkv,
10216            t_kv,
10217            scale,
10218            kvl.k_tok_bytes,
10219            kvl.v_tok_bytes,
10220            swa && crate::Engine::wkv_on(),
10221        )?;
10222        Ok(e.matmul(&fa.wo, &attn, 1)?)
10223    }
10224
10225    /// gemma4 DEVICE-COUNTER decode step (graph arc): token id + rope pos + KV lengths live in
10226    /// device counters; ZERO varying host kernel args. `cap_bucket_max` = Some(bucket) for graph
10227    /// capture (host mirrors untouched, n_splits from bucket, full-buffer KV views) / None for
10228    /// the eager-dc gate path (host mirrors advanced, live geometry — bit-identical target =
10229    /// gemma4_decode_step_h's token stream). V1 scope: t_kv <= sliding_window (no window views
10230    /// in-graph; the driver gates).
10231    #[allow(clippy::too_many_arguments)]
10232    pub fn gemma4_decode_step_dc(
10233        &self,
10234        e: &Engine,
10235        token_d: &CudaSlice<u32>,
10236        pos_d: &mut CudaSlice<i32>,
10237        embd_gpu: &CudaSlice<u8>,
10238        embd_qt: i32,
10239        embd_rb: usize,
10240        cache: &mut Cache,
10241        n_vocab: usize,
10242        cap_bucket_max: Option<(usize, usize)>,
10243    ) -> Result<CudaSlice<u32>, Box<dyn std::error::Error>> {
10244        let mut tok_out = e.stream().alloc_zeros::<u32>(1)?;
10245        self.gemma4_decode_step_dc_into(
10246            e,
10247            token_d,
10248            pos_d,
10249            embd_gpu,
10250            embd_qt,
10251            embd_rb,
10252            cache,
10253            n_vocab,
10254            cap_bucket_max,
10255            &mut tok_out,
10256        )?;
10257        Ok(tok_out)
10258    }
10259
10260    /// CAPTURE body: argmax lands in the PERSISTENT `tok_out` (same buffer = same address on
10261    /// every replay; pass `token_d` itself for the self-feeding graph loop).
10262    #[allow(clippy::too_many_arguments)]
10263    pub fn gemma4_decode_step_dc_into(
10264        &self,
10265        e: &Engine,
10266        token_d: &CudaSlice<u32>,
10267        pos_d: &mut CudaSlice<i32>,
10268        embd_gpu: &CudaSlice<u8>,
10269        embd_qt: i32,
10270        embd_rb: usize,
10271        cache: &mut Cache,
10272        n_vocab: usize,
10273        cap_bucket_max: Option<(usize, usize)>,
10274        tok_out: &mut CudaSlice<u32>,
10275    ) -> Result<(), Box<dyn std::error::Error>> {
10276        let n_embd = self.cfg.n_embd as usize;
10277        let eps = self.cfg.rms_eps;
10278        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
10279        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
10280        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
10281        let n_layers = self.layers.len();
10282        for (il, layer) in self.layers.iter().enumerate() {
10283            let (hq, hdq) = match h_carry.take() {
10284                Some(p) => p,
10285                None => {
10286                    e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, 1, eps)?
10287                }
10288            };
10289            let Mixer::Full(fa) = &layer.mixer else {
10290                panic!("gemma4 layer {il} not full-attn")
10291            };
10292            let o =
10293                self.gemma4_decode_attn_dc(e, fa, il, &hq, &hdq, pos_d, cache, cap_bucket_max)?;
10294            let mut cur = e.uninit(n_embd)?;
10295            e.rms_norm(
10296                &o,
10297                layer.post_attn_norm.float_data(),
10298                &mut cur,
10299                n_embd,
10300                1,
10301                eps,
10302            )?;
10303            let next_norm = if il + 1 < n_layers {
10304                Some(self.layers[il + 1].attn_norm.float_data())
10305            } else {
10306                None
10307            };
10308            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
10309            x = xn;
10310            h_carry = hn;
10311        }
10312        let mut hn = e.uninit(n_embd)?;
10313        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
10314        let mut logits = e.matmul(&self.output, &hn, 1)?;
10315        self.gemma4_suppress(e, &mut logits, 1)?; // cap skipped (monotonic); the mask is not
10316        e.argmax_token_device_into(&logits, tok_out, n_vocab)?;
10317        e.inc_seqlen(pos_d)?;
10318        if cap_bucket_max.is_none() {
10319            cache.pos += 1;
10320        }
10321        Ok(())
10322    }
10323
10324    /// Persistent transient slots for the ALLOC-FREE captured dc step (the graph door):
10325    /// every buffer the step produces per token lives here, allocated ONCE pre-capture, so
10326    /// the captured graph carries zero cuMemAllocAsync/Free nodes (the 226us/launch tax,
10327    /// osrt 2026-07-23). Sized for the model's max per-layer shapes.
10328
10329    /// Build the slot set (call OUTSIDE any capture).
10330    pub fn g4_dc_slots(&self, e: &Engine) -> Result<G4DcSlots, Box<dyn std::error::Error>> {
10331        let n_embd = self.cfg.n_embd as usize;
10332        let n_vocab = self.output.out_features();
10333        let n_layers = self.layers.len();
10334        let (mut qmax, mut kvmax, mut ffmax) = (0usize, 0usize, 0usize);
10335        for il in 0..n_layers {
10336            let (hd, nkv, nh, _b, _s, _w) = self.gemma4_geom(il);
10337            qmax = qmax.max(nh * hd);
10338            kvmax = kvmax.max(nkv * hd);
10339            if let crate::hybrid::Ffn::Dense { ffn_gate, .. } = &self.layers[il].ffn {
10340                ffmax = ffmax.max(ffn_gate.out_features());
10341            }
10342        }
10343        Ok(G4DcSlots {
10344            x: e.uninit(n_embd)?,
10345            xn: e.uninit(n_embd)?,
10346            cur: e.uninit(n_embd)?,
10347            hq: e.alloc_i8_uninit(n_embd)?,
10348            hd_: e.uninit(n_embd / 32)?,
10349            q0: e.uninit(qmax)?,
10350            k0: e.uninit(kvmax)?,
10351            v0: e.uninit(kvmax)?,
10352            q: e.uninit(qmax)?,
10353            k: e.uninit(kvmax)?,
10354            v: e.uninit(kvmax)?,
10355            attn: e.uninit(qmax)?,
10356            o: e.uninit(n_embd)?,
10357            attn_out: e.uninit(n_embd)?,
10358            zsh: e.uninit(n_embd)?,
10359            // zq/zd feed the wo matvec (nh*hd rows — 4096 on hd512 globals > n_embd),
10360            // the ffn entry (n_embd) and the lm_head (n_embd): size for the max.
10361            zq: e.alloc_i8_uninit(n_embd.max(qmax))?,
10362            zd: e.uninit(n_embd.max(qmax) / 32)?,
10363            gate: e.uninit(ffmax)?,
10364            up: e.uninit(ffmax)?,
10365            act: e.uninit(ffmax)?,
10366            actq: e.alloc_i8_uninit(ffmax)?,
10367            actd: e.uninit(ffmax / 32)?,
10368            f0: e.uninit(n_embd)?,
10369            sn: e.uninit(n_embd)?,
10370            hn: e.uninit(n_embd)?,
10371            logits: e.uninit(n_vocab)?,
10372        })
10373    }
10374
10375    /// m=1 pre-quantized matvec into a slot — mirrors matmul_pre's m=1 mmvq route exactly
10376    /// (rp4-mirror bytes; mmvq_supports guaranteed for gemma4 q4_0/q6_K).
10377    fn g4_matvec_m1_into(
10378        &self,
10379        e: &Engine,
10380        w: &crate::model::GpuTensor,
10381        aq: &CudaSlice<i8>,
10382        ad: &CudaSlice<f32>,
10383        y: &mut CudaSlice<f32>,
10384    ) -> Result<(), Box<dyn std::error::Error>> {
10385        use crate::model::GpuTensor;
10386        let (bytes, qtype, row_bytes, scale, rp) = match w {
10387            GpuTensor::Quant {
10388                bytes,
10389                qtype,
10390                row_bytes,
10391                scale,
10392                rp,
10393                ..
10394            } => (bytes, *qtype, *row_bytes, *scale, *rp),
10395            _ => return Err("g4_matvec_m1_into: non-quant tensor".into()),
10396        };
10397        let (mbytes, mrp) = match w {
10398            GpuTensor::Quant { rp4: Some(m4), .. } => (m4, true),
10399            _ => (bytes, rp),
10400        };
10401        e.qmatvec_mmvq_into(
10402            mbytes,
10403            aq,
10404            ad,
10405            1,
10406            w.in_features(),
10407            w.out_features(),
10408            qtype,
10409            row_bytes,
10410            scale,
10411            mrp,
10412            y,
10413        )
10414    }
10415
10416    /// ALLOC-FREE dc step (capture body): kernel-for-kernel mirror of
10417    /// `gemma4_decode_step_dc_into` at t=1 with every transient slot-fed. Dense gemma4 only
10418    /// (12B/31B; uniform q4_0 trunk guarantees the fused2/3 arms).
10419    #[allow(clippy::too_many_arguments)]
10420    pub fn gemma4_decode_step_dc_slotted(
10421        &self,
10422        e: &Engine,
10423        token_d: &CudaSlice<u32>,
10424        pos_d: &mut CudaSlice<i32>,
10425        embd_gpu: &CudaSlice<u8>,
10426        embd_qt: i32,
10427        embd_rb: usize,
10428        cache: &mut Cache,
10429        n_vocab: usize,
10430        cap_bucket_max: Option<(usize, usize)>,
10431        sl: &mut G4DcSlots,
10432        tok_out: &mut CudaSlice<u32>,
10433        ring: Option<(&mut CudaSlice<u32>, usize)>,
10434    ) -> Result<(), Box<dyn std::error::Error>> {
10435        let n_embd = self.cfg.n_embd as usize;
10436        let eps = self.cfg.rms_eps;
10437        e.embed_gather_device_into(embd_gpu, token_d, &mut sl.x, n_embd, embd_qt, embd_rb)?;
10438        e.scale_inplace(&mut sl.x, (n_embd as f32).sqrt(), n_embd)?;
10439        let n_layers = self.layers.len();
10440        let mut has_carry = false;
10441        for il in 0..n_layers {
10442            if !has_carry {
10443                e.rms_norm_q8_1_into(
10444                    &sl.x,
10445                    self.layers[il].attn_norm.float_data(),
10446                    n_embd,
10447                    1,
10448                    eps,
10449                    &mut sl.hq,
10450                    &mut sl.hd_,
10451                )?;
10452            }
10453            has_carry = true;
10454            let layer = &self.layers[il];
10455            let Mixer::Full(fa) = &layer.mixer else {
10456                panic!("gemma4 layer {il} not full-attn")
10457            };
10458            self.gemma4_decode_attn_dc_slotted(e, fa, il, pos_d, cache, cap_bucket_max, sl)?;
10459            e.rms_norm(
10460                &sl.o,
10461                layer.post_attn_norm.float_data(),
10462                &mut sl.cur,
10463                n_embd,
10464                1,
10465                eps,
10466            )?;
10467            let next_norm = if il + 1 < n_layers {
10468                Some(self.layers[il + 1].attn_norm.float_data())
10469            } else {
10470                None
10471            };
10472            self.gemma4_layer_tail_slotted(e, layer, next_norm, sl)?;
10473            std::mem::swap(&mut sl.x, &mut sl.xn);
10474        }
10475        e.rms_norm(
10476            &sl.x,
10477            self.output_norm.float_data(),
10478            &mut sl.hn,
10479            n_embd,
10480            1,
10481            eps,
10482        )?;
10483        e.quantize_q8_1_into(&sl.hn, 1, n_embd, &mut sl.zq, &mut sl.zd)?;
10484        // lm_head via the same m=1 mmvq route as matmul (q6_K on the gemma4 family).
10485        {
10486            let (zq, zd) = (&sl.zq, &sl.zd);
10487            let zq = unsafe { &*(zq as *const CudaSlice<i8>) };
10488            let zd = unsafe { &*(zd as *const CudaSlice<f32>) };
10489            self.g4_matvec_m1_into(e, &self.output, zq, zd, &mut sl.logits)?;
10490        }
10491        self.gemma4_suppress(e, &mut sl.logits, 1)?;
10492        e.argmax_token_device_into(&sl.logits, tok_out, n_vocab)?;
10493        if let Some((ring, base)) = ring {
10494            // in-graph token parking: slot = (pos - base) % ring.len(); the door drains
10495            // with ONE sync per chunk instead of a per-token dtoh (the launch-serialization
10496            // fix — llama's 885us/launch overlaps GPU work because nothing waits per token).
10497            e.plain_tok_ring(tok_out, pos_d, base, ring)?;
10498        }
10499        e.inc_seqlen(pos_d)?;
10500        if cap_bucket_max.is_none() {
10501            cache.pos += 1;
10502        }
10503        Ok(())
10504    }
10505
10506    /// Slot-fed dc attention: mirrors gemma4_decode_attn_dc's CAPTURE arm kernel-for-kernel.
10507    #[allow(clippy::too_many_arguments)]
10508    fn gemma4_decode_attn_dc_slotted(
10509        &self,
10510        e: &Engine,
10511        fa: &crate::hybrid::FullAttnLayer,
10512        il: usize,
10513        pos_d: &CudaSlice<i32>,
10514        cache: &mut Cache,
10515        cap_bucket_max: Option<(usize, usize)>,
10516        sl: &mut G4DcSlots,
10517    ) -> Result<(), Box<dyn std::error::Error>> {
10518        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
10519        let eps = self.cfg.rms_eps;
10520        let aux = self.gemma4_aux.as_ref().unwrap();
10521        let ones = aux.ones(e);
10522        #[cfg(debug_assertions)]
10523        crate::debug_assert_tensor_stream_device(
10524            ones,
10525            &e.stream(),
10526            "gemma4_decode_attn_dc_slotted.ones",
10527        );
10528        {
10529            let hq = unsafe { &*(&sl.hq as *const CudaSlice<i8>) };
10530            let hdq = unsafe { &*(&sl.hd_ as *const CudaSlice<f32>) };
10531            if swa {
10532                if !e.matmul_q4_fused3_into(
10533                    &fa.wq, &fa.wk, &fa.wv, hq, hdq, &mut sl.q0, &mut sl.k0, &mut sl.v0,
10534                )? {
10535                    return Err("slotted step: fused3 unavailable (non-uniform trunk)".into());
10536                }
10537            } else {
10538                if !e.matmul_q4_fused2_into(&fa.wq, &fa.wk, hq, hdq, &mut sl.q0, &mut sl.k0)? {
10539                    return Err("slotted step: fused2 unavailable".into());
10540                }
10541                let k0r = unsafe { &*(&sl.k0 as *const CudaSlice<f32>) };
10542                e.copy_into(&mut sl.v0, 0, k0r, nkv * hd)?;
10543            }
10544        }
10545        // E4B wave-3 fold, m=1 completion (2026-07-23) — MUST mirror the dc_into arm
10546        // kernel-for-kernel (graph stream-identity gate).
10547        let ff = if swa {
10548            None
10549        } else {
10550            Some(
10551                aux.rope_freqs(e)
10552                    .expect("gemma4 global rope needs rope_freqs.weight"),
10553            )
10554        };
10555        #[cfg(debug_assertions)]
10556        if let Some(ff) = ff {
10557            crate::debug_assert_tensor_stream_device(
10558                ff,
10559                &e.stream(),
10560                "gemma4_decode_attn_dc_slotted.rope_freqs",
10561            );
10562        }
10563        let kvl = cache.kv[il].as_mut().unwrap();
10564        let kv_fp8 = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
10565        if crate::Engine::qkv_append_on() {
10566            // append fold (2026-07-23): mirrors dc_into.
10567            e.rms_norm_qkv_rope_append_dc(
10568                &sl.q0,
10569                &sl.k0,
10570                &sl.v0,
10571                fa.q_norm.float_data(),
10572                fa.k_norm.float_data(),
10573                ones,
10574                &mut sl.q,
10575                &mut sl.k,
10576                &mut sl.v,
10577                hd,
10578                nh,
10579                nkv,
10580                pos_d,
10581                nh,
10582                nkv,
10583                base,
10584                1.0,
10585                ff,
10586                eps,
10587                &mut kvl.k,
10588                &mut kvl.v,
10589                &kvl.len_d,
10590                kvl.k_tok_bytes,
10591                kvl.v_tok_bytes,
10592                kv_fp8,
10593            )?;
10594        } else {
10595            e.rms_norm_qkv_rope(
10596                &sl.q0,
10597                &sl.k0,
10598                &sl.v0,
10599                fa.q_norm.float_data(),
10600                fa.k_norm.float_data(),
10601                ones,
10602                &mut sl.q,
10603                &mut sl.k,
10604                &mut sl.v,
10605                hd,
10606                nh,
10607                nkv,
10608                pos_d,
10609                nh,
10610                nkv,
10611                base,
10612                1.0,
10613                ff,
10614                eps,
10615            )?;
10616            e.append_kv_quantized_dc(
10617                &sl.k,
10618                &sl.v,
10619                &mut kvl.k,
10620                &mut kvl.v,
10621                &kvl.len_d,
10622                kvl.kv_dim_k,
10623                kvl.kv_dim_v,
10624                kvl.k_tok_bytes,
10625                kvl.v_tok_bytes,
10626                kv_fp8,
10627            )?;
10628        }
10629        e.inc_seqlen(&mut kvl.len_d)?;
10630        let (b_swa, b_glob) = cap_bucket_max.expect("slotted step is capture-only");
10631        let k_view = e.view_u8(&kvl.k, kvl.k.len());
10632        let v_view = e.view_u8(&kvl.v, kvl.v.len());
10633        let rows_on = std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0");
10634        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
10635        // combine-q8 emit (wave-5b m=1 port): the rows arms quantize inside the combine —
10636        // the standalone quantize launch runs only on the non-rows fallback. MUST mirror
10637        // the dc_into arm branch-for-branch (stream gate).
10638        let mut fa_q8 = false;
10639        if !swa && hd == 512 && b_glob >= crate::fa512_min_tkv() && rows_on {
10640            e.fa_decode_rows(
10641                &sl.q,
10642                &k_view,
10643                &v_view,
10644                &mut sl.attn,
10645                hd,
10646                nh,
10647                nkv,
10648                b_glob - 1,
10649                1,
10650                scale,
10651                kvl.k_tok_bytes,
10652                kvl.v_tok_bytes,
10653                Some((&kvl.len_d, -1)),
10654                false,
10655                false,
10656                Some((&mut sl.zq, &mut sl.zd)),
10657            )?;
10658            fa_q8 = true;
10659        } else if swa && b_swa > win && hd == 256 && rows_on {
10660            e.fa_decode_rows_w(
10661                &sl.q,
10662                &k_view,
10663                &v_view,
10664                &mut sl.attn,
10665                hd,
10666                nh,
10667                nkv,
10668                &kvl.len_d,
10669                -1,
10670                1,
10671                scale,
10672                win,
10673                kvl.k_tok_bytes,
10674                kvl.v_tok_bytes,
10675                Some((&mut sl.zq, &mut sl.zd)),
10676            )?;
10677            fa_q8 = true;
10678        } else {
10679            let b = if swa { b_swa } else { b_glob };
10680            e.fa_decode_dc(
10681                &sl.q,
10682                &k_view,
10683                &v_view,
10684                &mut sl.attn,
10685                hd,
10686                nh,
10687                nkv,
10688                &kvl.len_d,
10689                b,
10690                scale,
10691                kvl.k_tok_bytes,
10692                kvl.v_tok_bytes,
10693                swa && crate::Engine::wkv_on(),
10694            )?;
10695        }
10696        if !fa_q8 {
10697            let aq = unsafe { &*(&sl.attn as *const CudaSlice<f32>) };
10698            e.quantize_q8_1_into(aq, 1, nh * hd, &mut sl.zq, &mut sl.zd)?;
10699        }
10700        {
10701            let zq = unsafe { &*(&sl.zq as *const CudaSlice<i8>) };
10702            let zd = unsafe { &*(&sl.zd as *const CudaSlice<f32>) };
10703            self.g4_matvec_m1_into(e, &fa.wo, zq, zd, &mut sl.o)?;
10704        }
10705        Ok(())
10706    }
10707
10708    /// Slot-fed dense layer tail: mirrors gemma4_layer_tail_core (t=1, no pre-norm fold) +
10709    /// tail_add_nq kernel-for-kernel; the next layer's (hq, hd_) carry lands in the slots.
10710    fn gemma4_layer_tail_slotted(
10711        &self,
10712        e: &Engine,
10713        layer: &crate::hybrid::HybridLayer,
10714        next_norm: Option<&CudaSlice<f32>>,
10715        sl: &mut G4DcSlots,
10716    ) -> Result<(), Box<dyn std::error::Error>> {
10717        let n_embd = self.cfg.n_embd as usize;
10718        let eps = self.cfg.rms_eps;
10719        let bits = layer.gemma4.as_ref().unwrap();
10720        let crate::hybrid::Ffn::Dense {
10721            ffn_gate,
10722            ffn_up,
10723            ffn_down,
10724        } = &layer.ffn
10725        else {
10726            return Err("slotted tail: dense ffn only".into());
10727        };
10728        e.add_rms_norm(
10729            &sl.cur,
10730            &sl.x,
10731            bits.ffn_norm.float_data(),
10732            &mut sl.attn_out,
10733            &mut sl.zsh,
10734            n_embd,
10735            1,
10736            eps,
10737        )?;
10738        let n_ff = ffn_gate.out_features();
10739        {
10740            let zshr = unsafe { &*(&sl.zsh as *const CudaSlice<f32>) };
10741            e.quantize_q8_1_into(zshr, 1, n_embd, &mut sl.zq, &mut sl.zd)?;
10742        }
10743        {
10744            let zq = unsafe { &*(&sl.zq as *const CudaSlice<i8>) };
10745            let zd = unsafe { &*(&sl.zd as *const CudaSlice<f32>) };
10746            if !e.matmul_q4_fused2_into(ffn_gate, ffn_up, zq, zd, &mut sl.gate, &mut sl.up)? {
10747                return Err("slotted tail: ffn fused2 unavailable".into());
10748            }
10749        }
10750        debug_assert!(e.uses_q8_1_fast(ffn_down));
10751        {
10752            let upr = unsafe { &*(&sl.up as *const CudaSlice<f32>) };
10753            let upv = e.view(upr, n_ff);
10754            let up_all = upv.slice(0..n_ff);
10755            let gr = unsafe { &*(&sl.gate as *const CudaSlice<f32>) };
10756            e.gelu_tanh_mul_q8_1_into(
10757                gr,
10758                &up_all,
10759                &mut sl.act,
10760                n_ff,
10761                1,
10762                &mut sl.actq,
10763                &mut sl.actd,
10764            )?;
10765        }
10766        {
10767            let aq = unsafe { &*(&sl.actq as *const CudaSlice<i8>) };
10768            let ad = unsafe { &*(&sl.actd as *const CudaSlice<f32>) };
10769            self.g4_matvec_m1_into(e, ffn_down, aq, ad, &mut sl.f0)?;
10770        }
10771        e.rms_norm(
10772            &sl.f0,
10773            bits.post_ffw_norm.float_data(),
10774            &mut sl.sn,
10775            n_embd,
10776            1,
10777            eps,
10778        )?;
10779        match next_norm {
10780            Some(w) => {
10781                e.add_scale_rms_norm_q8_1_into(
10782                    &sl.sn,
10783                    &sl.attn_out,
10784                    bits.layer_scale,
10785                    w,
10786                    &mut sl.xn,
10787                    n_embd,
10788                    1,
10789                    eps,
10790                    &mut sl.hq,
10791                    &mut sl.hd_,
10792                )?;
10793            }
10794            None => {
10795                e.add_scale(&sl.sn, &sl.attn_out, bits.layer_scale, &mut sl.xn, n_embd)?;
10796            }
10797        }
10798        Ok(())
10799    }
10800
10801    /// dc attention: same math as gemma4_decode_attn, KV slot/lengths from device counters.
10802    #[allow(clippy::too_many_arguments)]
10803    fn gemma4_decode_attn_dc(
10804        &self,
10805        e: &Engine,
10806        fa: &crate::hybrid::FullAttnLayer,
10807        il: usize,
10808        hq: &CudaSlice<i8>,
10809        hdq: &CudaSlice<f32>,
10810        pos_d: &CudaSlice<i32>,
10811        cache: &mut Cache,
10812        cap_bucket_max: Option<(usize, usize)>,
10813    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
10814        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
10815        let eps = self.cfg.rms_eps;
10816        let aux = self.gemma4_aux.as_ref().unwrap();
10817        let ones = aux.ones(e);
10818        #[cfg(debug_assertions)]
10819        crate::debug_assert_tensor_stream_device(ones, &e.stream(), "gemma4_decode_attn_dc.ones");
10820        let (q0, k0, v0) = if swa {
10821            match e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hdq)? {
10822                Some(t3) => t3,
10823                None => {
10824                    let h0 = e.zeros(0)?;
10825                    (
10826                        e.matmul_pre(&fa.wq, hq, hdq, &h0, 1)?,
10827                        e.matmul_pre(&fa.wk, hq, hdq, &h0, 1)?,
10828                        e.matmul_pre(&fa.wv, hq, hdq, &h0, 1)?,
10829                    )
10830                }
10831            }
10832        } else {
10833            let (q0, k0) = match e.matmul_q4_fused2(&fa.wq, &fa.wk, hq, hdq)? {
10834                Some(p) => p,
10835                None => {
10836                    let h0 = e.zeros(0)?;
10837                    (
10838                        e.matmul_pre(&fa.wq, hq, hdq, &h0, 1)?,
10839                        e.matmul_pre(&fa.wk, hq, hdq, &h0, 1)?,
10840                    )
10841                }
10842            };
10843            let v0 = e.clone_dtod(&k0)?;
10844            (q0, k0, v0)
10845        };
10846        let mut q = e.uninit(nh * hd)?;
10847        let mut k = e.uninit(nkv * hd)?;
10848        let mut v = e.uninit(nkv * hd)?;
10849        // E4B wave-3 fold, m=1 completion (2026-07-23): mirrored by the slotted arm.
10850        let ff = if swa {
10851            None
10852        } else {
10853            Some(
10854                aux.rope_freqs(e)
10855                    .expect("gemma4 global rope needs rope_freqs.weight"),
10856            )
10857        };
10858        #[cfg(debug_assertions)]
10859        if let Some(ff) = ff {
10860            crate::debug_assert_tensor_stream_device(
10861                ff,
10862                &e.stream(),
10863                "gemma4_decode_attn_dc.rope_freqs",
10864            );
10865        }
10866        let kvl = cache.kv[il].as_mut().unwrap();
10867        let kv_fp8 = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
10868        if crate::Engine::qkv_append_on() {
10869            // append fold (2026-07-23): norm+rope+cache-append in ONE launch.
10870            e.rms_norm_qkv_rope_append_dc(
10871                &q0,
10872                &k0,
10873                &v0,
10874                fa.q_norm.float_data(),
10875                fa.k_norm.float_data(),
10876                ones,
10877                &mut q,
10878                &mut k,
10879                &mut v,
10880                hd,
10881                nh,
10882                nkv,
10883                pos_d,
10884                nh,
10885                nkv,
10886                base,
10887                1.0,
10888                ff,
10889                eps,
10890                &mut kvl.k,
10891                &mut kvl.v,
10892                &kvl.len_d,
10893                kvl.k_tok_bytes,
10894                kvl.v_tok_bytes,
10895                kv_fp8,
10896            )?;
10897        } else {
10898            e.rms_norm_qkv_rope(
10899                &q0,
10900                &k0,
10901                &v0,
10902                fa.q_norm.float_data(),
10903                fa.k_norm.float_data(),
10904                ones,
10905                &mut q,
10906                &mut k,
10907                &mut v,
10908                hd,
10909                nh,
10910                nkv,
10911                pos_d,
10912                nh,
10913                nkv,
10914                base,
10915                1.0,
10916                ff,
10917                eps,
10918            )?;
10919            e.append_kv_quantized_dc(
10920                &k,
10921                &v,
10922                &mut kvl.k,
10923                &mut kvl.v,
10924                &kvl.len_d,
10925                kvl.kv_dim_k,
10926                kvl.kv_dim_v,
10927                kvl.k_tok_bytes,
10928                kvl.v_tok_bytes,
10929                kv_fp8,
10930            )?;
10931        }
10932        e.inc_seqlen(&mut kvl.len_d)?;
10933        let mut attn = e.uninit(nh * hd)?;
10934        // combine-q8 emit carrier (wave-5b m=1 port): rows arms fill this; the tail then
10935        // rides g4_matvec_m1_into instead of matmul's internal quantize.
10936        let mut fa_q8: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
10937        // Weight prefetch NOT wired here (26B/31B/qwen probes 2026-07-13: 26B flat
10938        // 196.6/196.0 vs 196.4/196.1 — MoE ffn dilutes wo; 31B −0.2% — dense decode sits
10939        // at the DRAM wall, no idle window to front-load into). E4B keeps the arm
10940        // (gemma4_e4b_attn, +0.65% valid window).
10941        match cap_bucket_max {
10942            None => {
10943                // dc-EAGER: host knows the length — R6 window views EXACTLY like the eager
10944                // decode (SWA layers attend the last `sliding_window` keys); the device
10945                // counters carry only the append slot + the graph seam.
10946                kvl.len += 1;
10947                let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
10948                if !swa
10949                    && hd == 512
10950                    && kvl.len >= crate::fa512_min_tkv()
10951                    && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
10952                {
10953                    // global layers: SAME hd512 rows twin as verify, t=1 (parity law).
10954                    // dc: len_d is live (inc_seqlen) — device-len rides it, plus=-1.
10955                    let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
10956                    let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
10957                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
10958                    e.fa_decode_rows(
10959                        &q,
10960                        &kp,
10961                        &vp,
10962                        &mut attn,
10963                        hd,
10964                        nh,
10965                        nkv,
10966                        kvl.len - 1,
10967                        1,
10968                        scale,
10969                        kvl.k_tok_bytes,
10970                        kvl.v_tok_bytes,
10971                        Some((&kvl.len_d, -1)),
10972                        false,
10973                        false,
10974                        Some((&mut aq8, &mut ad8)),
10975                    )?;
10976                    fa_q8 = Some((aq8, ad8));
10977                } else if swa
10978                    && kvl.len > win
10979                    && hd == 256
10980                    && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
10981                {
10982                    // windowed regime: SAME rows_w kernel as verify, t=1 (parity law).
10983                    let kp = e.view_u8(&kvl.k, kvl.len * kvl.k_tok_bytes);
10984                    let vp = e.view_u8(&kvl.v, kvl.len * kvl.v_tok_bytes);
10985                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
10986                    e.fa_decode_rows_w(
10987                        &q,
10988                        &kp,
10989                        &vp,
10990                        &mut attn,
10991                        hd,
10992                        nh,
10993                        nkv,
10994                        &kvl.len_d,
10995                        -1,
10996                        1,
10997                        scale,
10998                        win,
10999                        kvl.k_tok_bytes,
11000                        kvl.v_tok_bytes,
11001                        Some((&mut aq8, &mut ad8)),
11002                    )?;
11003                    fa_q8 = Some((aq8, ad8));
11004                } else {
11005                    let (off_tok, t_kv) = if swa && kvl.len > win {
11006                        (kvl.len - win, win)
11007                    } else {
11008                        (0, kvl.len)
11009                    };
11010                    let k_view = e.view_u8_range(
11011                        &kvl.k,
11012                        off_tok * kvl.k_tok_bytes,
11013                        (off_tok + t_kv) * kvl.k_tok_bytes,
11014                    );
11015                    let v_view = e.view_u8_range(
11016                        &kvl.v,
11017                        off_tok * kvl.v_tok_bytes,
11018                        (off_tok + t_kv) * kvl.v_tok_bytes,
11019                    );
11020                    e.fa_decode_kvmod(
11021                        &q,
11022                        &k_view,
11023                        &v_view,
11024                        &mut attn,
11025                        hd,
11026                        nh,
11027                        nkv,
11028                        t_kv,
11029                        scale,
11030                        kvl.k_tok_bytes,
11031                        kvl.v_tok_bytes,
11032                        swa && crate::Engine::wkv_on(),
11033                    )?;
11034                }
11035            }
11036            Some((b_swa, b_glob)) => {
11037                // capture: full-buffer views + device length. PARITY LAW port (graph arc step
11038                // 3): the SAME regime branches as dc-eager, pinned per arm — b_swa is the
11039                // exact-key t_kv for the dc family (n_splits constant per bucket), b_glob is
11040                // the RUNG max for the rows family (kernels derive per-replay splits from
11041                // kvl.len_d; grid/partials sized for the rung end, excess splits exit).
11042                let k_view = e.view_u8(&kvl.k, kvl.k.len());
11043                let v_view = e.view_u8(&kvl.v, kvl.v.len());
11044                let rows_on = std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0");
11045                let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
11046                if !swa && hd == 512 && b_glob >= crate::fa512_min_tkv() && rows_on {
11047                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
11048                    e.fa_decode_rows(
11049                        &q,
11050                        &k_view,
11051                        &v_view,
11052                        &mut attn,
11053                        hd,
11054                        nh,
11055                        nkv,
11056                        b_glob - 1,
11057                        1,
11058                        scale,
11059                        kvl.k_tok_bytes,
11060                        kvl.v_tok_bytes,
11061                        Some((&kvl.len_d, -1)),
11062                        false,
11063                        false,
11064                        Some((&mut aq8, &mut ad8)),
11065                    )?;
11066                    fa_q8 = Some((aq8, ad8));
11067                } else if swa && b_swa > win && hd == 256 && rows_on {
11068                    let (mut aq8, mut ad8) = e.uninit_q8_pair(nh * hd)?;
11069                    e.fa_decode_rows_w(
11070                        &q,
11071                        &k_view,
11072                        &v_view,
11073                        &mut attn,
11074                        hd,
11075                        nh,
11076                        nkv,
11077                        &kvl.len_d,
11078                        -1,
11079                        1,
11080                        scale,
11081                        win,
11082                        kvl.k_tok_bytes,
11083                        kvl.v_tok_bytes,
11084                        Some((&mut aq8, &mut ad8)),
11085                    )?;
11086                    fa_q8 = Some((aq8, ad8));
11087                } else {
11088                    let b = if swa { b_swa } else { b_glob };
11089                    e.fa_decode_dc(
11090                        &q,
11091                        &k_view,
11092                        &v_view,
11093                        &mut attn,
11094                        hd,
11095                        nh,
11096                        nkv,
11097                        &kvl.len_d,
11098                        b,
11099                        scale,
11100                        kvl.k_tok_bytes,
11101                        kvl.v_tok_bytes,
11102                        swa && crate::Engine::wkv_on(),
11103                    )?;
11104                }
11105            }
11106        }
11107        // combine-q8 emit (wave-5b m=1 port): the rows arms produced the wo activation pair
11108        // in-combine — ride the slotted arm's exact matvec route (parity by construction).
11109        if let Some((aq8, ad8)) = fa_q8 {
11110            let mut y = e.uninit(fa.wo.out_features())?;
11111            self.g4_matvec_m1_into(e, &fa.wo, &aq8, &ad8, &mut y)?;
11112            return Ok(y);
11113        }
11114        Ok(e.matmul(&fa.wo, &attn, 1)?)
11115    }
11116
11117    /// gemma4 GRAPH-REPLAY greedy loop: per (swa-key, global-key) fa bucket, capture ONE full
11118    /// dc step (self-feeding: argmax writes token_d in-graph) and replay it — one graph launch
11119    /// per token, one 4B dtoh. V1 scope: whole generation under the sliding window (no window
11120    /// views in-graph); caller gates and falls back to the dc-eager loop.
11121    pub fn gemma4_generate_graph(
11122        &self,
11123        e: &Engine,
11124        prompt_pos: usize,
11125        first_token: u32,
11126        cache: &mut Cache,
11127        max_new: usize,
11128        eos: &[u32],
11129        mut on_token: impl FnMut(u32) -> bool,
11130    ) -> Result<(Vec<u32>, crate::decode::StopReason), Box<dyn std::error::Error>> {
11131        if self.is_gemma4_e4b() {
11132            return Err(
11133                "E4B graph serving is unwired (HANDOVER-E4B.md) — dc-eager is the serving arm"
11134                    .into(),
11135            );
11136        }
11137        use crate::decode::StopReason;
11138        let n_vocab = self.output.out_features();
11139        let n_embd = self.cfg.n_embd as usize;
11140        let embd_gpu = self
11141            .embd_gpu
11142            .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload"));
11143        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
11144        for kvl in cache.kv.iter_mut().flatten() {
11145            e.set_i32_one(&mut kvl.len_d, kvl.len as i32)?;
11146        }
11147        let mut token_d = e.stream().clone_htod(&[first_token])?;
11148        let mut pos_d = e.htod_i32(&[prompt_pos as i32])?;
11149        let g4 = self.cfg.gemma4.as_ref().unwrap();
11150        let (hd_s, hd_g) = (g4.key_length_swa as usize, g4.key_length_global as usize);
11151        // per-layer nkv: swa vs global counts from the pattern (uniform within class).
11152        let nkv_s = g4
11153            .head_count_kv
11154            .iter()
11155            .zip(g4.swa_pattern.iter())
11156            .find(|p| *p.1)
11157            .map(|p| *p.0 as usize)
11158            .unwrap_or(8);
11159        let nkv_g = g4
11160            .head_count_kv
11161            .iter()
11162            .zip(g4.swa_pattern.iter())
11163            .find(|p| !*p.1)
11164            .map(|p| *p.0 as usize)
11165            .unwrap_or(2);
11166        let mut graphs: std::collections::HashMap<
11167            ((bool, usize), (bool, usize), bool, bool),
11168            (
11169                cudarc::driver::CudaGraph,
11170                Vec<Box<dyn std::any::Any + Send>>,
11171            ),
11172        > = Default::default();
11173        // ALLOC-FREE capture: persistent transient slots — the captured graph carries zero
11174        // mem nodes (the 226us/launch tax). Slots must outlive every cached graph.
11175        let mut slots = self.g4_dc_slots(e)?;
11176        // Chunked replay ring: tokens park on-device; ONE drain sync per chunk. ring_base is
11177        // baked at the door entry (the modulo keeps every capture valid indefinitely).
11178        const RING: usize = 64;
11179        // DRAIN pinned at 1 (2026-07-23): relaunching the SAME graph exec before its prior
11180        // launch completes is ILLEGAL (chunk=4 -> ILLEGAL_ADDRESS; chunk=1 clean). Pipelining
11181        // needs alternating exec instances — and the measured payoff was ~0 (the ~200us
11182        // cuGraphLaunch host cost already overlaps its own launch's GPU work; llama pays
11183        // 885us/launch the same way). MEMRA_GRAPH_DRAIN raises it only for experiments.
11184        const DRAIN: usize = 1;
11185        let mut ring = e.stream().alloc_zeros::<u32>(RING)?;
11186        let ring_base = prompt_pos;
11187        let mut out = Vec::with_capacity(max_new);
11188        let mut reason = StopReason::MaxNew;
11189        let mut next = first_token;
11190        let mut captures = 0usize;
11191        for _ in 0..max_new {
11192            out.push(next);
11193            if eos.contains(&next) {
11194                reason = StopReason::Eos;
11195                break;
11196            }
11197            if !on_token(next) {
11198                reason = StopReason::Callback;
11199                break;
11200            }
11201            let t_kv = cache.pos + 1;
11202            // Bucket key per ARM (graph arc step 3):
11203            //  - swa component: dc family under the window (exact (fa_vec, n_splits) key —
11204            //    n_splits must be constant per bucket); rows_w above it (window-constant, so
11205            //    the component collapses to a single marker).
11206            //  - global component: dc family under the fa512 floor (exact key); rows_dpl16
11207            //    at/above it — the kernel derives splits from len_d per replay, so buckets
11208            //    are power-of-2 RUNGS (one capture per doubling; grid sized for the rung end).
11209            let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
11210            let f512 = crate::fa512_min_tkv();
11211            let key_s = if t_kv > win {
11212                (true, usize::MAX)
11213            } else {
11214                e.fa_bucket_key(t_kv, hd_s, nkv_s, crate::Engine::wkv_on())
11215            };
11216            let (key_g, rung_end) = if t_kv >= f512 {
11217                // strict upper bound: the rung must ROLL at exact powers (t_kv==1024 starts
11218                // the [1024,2048) bucket) — sizing covers every replayed T_kv < end.
11219                let end = (t_kv + 1).next_power_of_two().max(f512 * 2);
11220                ((true, end), end)
11221            } else {
11222                (e.fa_bucket_key(t_kv, hd_g, nkv_g, false), t_kv)
11223            };
11224            let key = (key_s, key_g, t_kv >= f512, t_kv > win);
11225            if !graphs.contains_key(&key) {
11226                let bucket_max = (t_kv, rung_end);
11227                // snapshot device+host state (the 3 capture-warmup runs leave no residue).
11228                let snap = cache.snapshot(e)?;
11229                let pos_save = e.dtoh_i32_one(&pos_d)?;
11230                let len_save: Vec<Option<i32>> = cache
11231                    .kv
11232                    .iter()
11233                    .map(|k| k.as_ref().map(|kvl| e.dtoh_i32_one(&kvl.len_d).unwrap()))
11234                    .collect();
11235                let tok_save = e.dtoh_u32_one(&token_d)?;
11236                // RETAINED capture (2026-07-23): the plain capture_graph left pool-transient
11237                // clones as dead COPY NODES replayed every launch — the E4B 0.74ms/token
11238                // regression class, and this door's measured -8.8%. The keeper pins warmup
11239                // transients so the captured graph holds kernel nodes only.
11240                let graph = {
11241                    let tok_ref = &mut token_d;
11242                    let pos_ref = &mut pos_d;
11243                    let cache_ref = &mut *cache;
11244                    let slots_ref = &mut slots;
11245                    let ring_ref = &mut ring;
11246                    e.capture_graph_retained_flags(
11247                        cudarc::driver::sys::CUgraphInstantiate_flags::CUDA_GRAPH_INSTANTIATE_FLAG_USE_NODE_PRIORITY,
11248                        |e| {
11249                        // self-feeding: the argmax writes token_d itself.
11250                        let tok_in = unsafe { &*(tok_ref as *const CudaSlice<u32>) };
11251                        let sl = unsafe { &mut *(slots_ref as *mut G4DcSlots) };
11252                        let rg = unsafe { &mut *(ring_ref as *mut CudaSlice<u32>) };
11253                        self.gemma4_decode_step_dc_slotted(e, tok_in, pos_ref, embd_gpu, qt, rb,
11254                                                           cache_ref, n_vocab, Some(bucket_max),
11255                                                           sl, tok_ref, Some((rg, ring_base)))
11256                    })?
11257                };
11258                cache.rollback(e, &snap, 0)?;
11259                e.set_i32_one(&mut pos_d, pos_save)?;
11260                for (il, ls) in len_save.iter().enumerate() {
11261                    if let (Some(kvl), Some(v)) = (cache.kv[il].as_mut(), ls) {
11262                        e.set_i32_one(&mut kvl.len_d, *v)?;
11263                    }
11264                }
11265                e.set_u32_one(&mut token_d, tok_save)?;
11266                if std::env::var("MEMRA_GRAPH_CENSUS").as_deref() == Ok("1") {
11267                    if let Ok(c) = crate::graph_update::node_census(&graph.0) {
11268                        eprintln!("[graph-census] {c:?}");
11269                    }
11270                }
11271                graphs.insert(key, graph);
11272                captures += 1;
11273            }
11274            // CHUNKED REPLAY: enqueue up to DRAIN launches back-to-back (the ~200us host
11275            // cost of each cuGraphLaunch overlaps the PREVIOUS launch's ~11ms GPU work),
11276            // then ONE sync + ring drain. The chunk must stay inside this bucket key and
11277            // the budget; capture warmups already emitted their tokens through the ring.
11278            let mut chunk = 1usize;
11279            let drain_cap: usize = std::env::var("MEMRA_GRAPH_DRAIN")
11280                .ok()
11281                .and_then(|v| v.parse().ok())
11282                .unwrap_or(DRAIN);
11283            while chunk < drain_cap && out.len() + chunk < max_new {
11284                let t_next = cache.pos + 1 + chunk;
11285                let key_s2 = if t_next > win {
11286                    (true, usize::MAX)
11287                } else {
11288                    e.fa_bucket_key(t_next, hd_s, nkv_s, crate::Engine::wkv_on())
11289                };
11290                let key_g2 = if t_next >= f512 {
11291                    (true, (t_next + 1).next_power_of_two().max(f512 * 2))
11292                } else {
11293                    e.fa_bucket_key(t_next, hd_g, nkv_g, false)
11294                };
11295                if (key_s2, key_g2, t_next >= f512, t_next > win) != key {
11296                    break;
11297                }
11298                chunk += 1;
11299            }
11300            let g = &graphs.get(&key).unwrap().0;
11301            for _ in 0..chunk {
11302                g.launch()?;
11303            }
11304            e.stream().synchronize()?;
11305            let ringh = e.dtoh_u32(&ring)?;
11306            for j in 0..chunk {
11307                let pos_j = cache.pos + j;
11308                let tok_j = ringh[(pos_j - ring_base) % RING];
11309                cache.pos += 0; // advanced below in one shot
11310                if j + 1 == chunk {
11311                    next = tok_j;
11312                } else {
11313                    out.push(tok_j);
11314                    if eos.contains(&tok_j) || !on_token(tok_j) {
11315                        reason = if eos.contains(&tok_j) {
11316                            StopReason::Eos
11317                        } else {
11318                            StopReason::Callback
11319                        };
11320                        // roll device/host state back to the stop point.
11321                        let keep = cache.pos + j + 1;
11322                        e.set_i32_one(&mut pos_d, keep as i32)?;
11323                        for kvl in cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
11324                            e.set_i32_one(&mut kvl.len_d, keep as i32)?;
11325                            kvl.len = keep;
11326                        }
11327                        cache.pos = keep;
11328                        if std::env::var("MEMRA_GRAPH_STATS").is_ok() {
11329                            eprintln!("[gemma-graph] captures={captures} buckets={}", graphs.len());
11330                        }
11331                        return Ok((out, reason));
11332                    }
11333                }
11334            }
11335            cache.pos += chunk;
11336            for kvl in cache.kv.iter_mut().filter_map(|k| k.as_mut()) {
11337                kvl.len += chunk;
11338            }
11339        }
11340        if std::env::var("MEMRA_GRAPH_STATS").is_ok() {
11341            eprintln!("[gemma-graph] captures={captures} buckets={}", graphs.len());
11342        }
11343        Ok((out, reason))
11344    }
11345
11346    /// gemma4 VERIFY step (spec decode): t tokens batched through the trunk at positions
11347    /// pos0..pos0+t-1, K/V rows appended to the quantized cache (caller rolls back rejected
11348    /// rows via kvl.len), per-token causal windowed attend (fa_decode per token — the same
11349    /// kernel family as T=1 decode at each token's t_kv). Returns [t, n_vocab] softcapped
11350    /// logits (host) + advances cache.pos by t.
11351    pub(crate) fn gemma4_decode_step_t(
11352        &self,
11353        e: &Engine,
11354        tokens: &[u32],
11355        pos0: usize,
11356        cache: &mut Cache,
11357    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
11358        Ok(self.gemma4_decode_step_t_h(e, tokens, pos0, cache)?.0)
11359    }
11360
11361    /// GREEDY verify: per-row DEVICE argmax (t x 4B host traffic instead of the t x 1MB logits
11362    /// stack — softcap skipped: tanh is monotonic, per-row argmax unaffected). Returns
11363    /// (argmax ids [t], post-output_norm hidden stack [t, n_embd]).
11364    pub(crate) fn gemma4_decode_step_t_am(
11365        &self,
11366        e: &Engine,
11367        tokens: &[u32],
11368        pos0: usize,
11369        cache: &mut Cache,
11370    ) -> Result<(Vec<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
11371        let (ld, hn) = self.gemma4_verify_trunk(e, tokens, pos0, cache, None)?;
11372        let t = tokens.len();
11373        let n_vocab = self.output.out_features();
11374        let mut toks = e.stream().alloc_zeros::<u32>(t)?;
11375        for i in 0..t {
11376            e.argmax_token_device_col(&ld, i, n_vocab, &mut toks, i)?;
11377        }
11378        Ok((e.dtoh_u32(&toks)?, hn))
11379    }
11380
11381    /// Device-token verify (async spec round): tokens live in tok_d[0..t]; per-row argmax
11382    /// lands in a DEVICE buffer (no host logits). Returns (vam_d [t] u32 device, hn stack).
11383    pub(crate) fn gemma4_decode_step_t_am_dev(
11384        &self,
11385        e: &Engine,
11386        tok_d: &CudaSlice<u32>,
11387        t: usize,
11388        pos0: usize,
11389        cache: &mut Cache,
11390    ) -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
11391        let (ld, hn) = self.gemma4_verify_trunk(e, &vec![0u32; t], pos0, cache, Some(tok_d))?;
11392        let n_vocab = self.output.out_features();
11393        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
11394        for i in 0..t {
11395            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
11396        }
11397        Ok((vam, hn))
11398    }
11399
11400    /// gemma4 verify + the POST-output_norm hidden stack [t, n_embd] (the drafter's h input —
11401    /// llama's h_nextn convention).
11402    pub(crate) fn gemma4_decode_step_t_h(
11403        &self,
11404        e: &Engine,
11405        tokens: &[u32],
11406        pos0: usize,
11407        cache: &mut Cache,
11408    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
11409        let (mut ld, hn) = self.gemma4_verify_trunk(e, tokens, pos0, cache, None)?;
11410        let t = tokens.len();
11411        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
11412        e.softcap(&mut ld, cap, t * self.output.out_features())?;
11413        Ok((e.dtoh(&ld)?, hn))
11414    }
11415
11416    /// Persistent device scratch for the BURST verify stream (one per generation): rope pos
11417    /// rows [cap] + per-row t_kv counters (device-filled from the round counter, zero H2D).
11418    pub(crate) fn verify_stream_scratch(
11419        &self,
11420        e: &Engine,
11421        cap: usize,
11422    ) -> Result<VerifyStreamScratch, Box<dyn std::error::Error>> {
11423        Ok(VerifyStreamScratch {
11424            pos_d: e.htod_i32(&vec![0i32; cap])?,
11425            row_ctrs: (0..cap)
11426                .map(|_| e.htod_i32(&[0]))
11427                .collect::<Result<_, _>>()?,
11428        })
11429    }
11430
11431    /// BURST verify trunk (device-slot twin): tokens from a device buffer, rope positions
11432    /// iota'd from `ctr`, appends/attention at the layers' len_d counters (verify_attn_stream),
11433    /// NO host cache.pos/len advance. Returns (per-row device argmaxes [t], post-norm hidden
11434    /// stack [t, n_embd]) — the burst's accept/seed inputs, zero host readbacks.
11435    /// `scr` = PERSISTENT per-gen scratch (pos rows + per-row t_kv counters): the first cut
11436    /// htod_i32-allocated these per call — 9 pageable H2D copies per round, each a stream
11437    /// sync, exactly the turnaround the burst exists to remove.
11438    pub(crate) fn gemma4_verify_t_am_stream(
11439        &self,
11440        e: &Engine,
11441        tok_d: &CudaSlice<u32>,
11442        t: usize,
11443        ctr: &CudaSlice<i32>,
11444        hint: usize,
11445        cache: &mut Cache,
11446        scr: &mut VerifyStreamScratch,
11447    ) -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
11448        let n_embd = self.cfg.n_embd as usize;
11449        let eps = self.cfg.rms_eps;
11450        assert!(t <= scr.row_ctrs.len() && t <= 64);
11451        e.i32_iota_from(ctr, &mut scr.pos_d, t)?;
11452        for i in 0..t {
11453            e.i32_copy_add(ctr, &mut scr.row_ctrs[i], (i + 1) as i32)?;
11454        }
11455        let (pos_d, row_ctrs) = (&scr.pos_d, &scr.row_ctrs);
11456        let embd_gpu = self
11457            .embd_gpu
11458            .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload"));
11459        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
11460        let mut x = e.embed_gather_device_td(embd_gpu, tok_d, t, n_embd, qt, rb)?;
11461        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
11462        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
11463        let n_layers = self.layers.len();
11464        for (il, layer) in self.layers.iter().enumerate() {
11465            let (hq, hdq) = match h_carry.take() {
11466                Some(p) => p,
11467                None => {
11468                    e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, t, eps)?
11469                }
11470            };
11471            let Mixer::Full(fa) = &layer.mixer else {
11472                panic!("gemma4 layer {il} not full-attn")
11473            };
11474            let o = self
11475                .gemma4_verify_attn_stream(e, fa, il, &hq, &hdq, pos_d, t, cache, hint, row_ctrs)?;
11476            let mut cur = e.uninit(t * n_embd)?;
11477            e.rms_norm(
11478                &o,
11479                layer.post_attn_norm.float_data(),
11480                &mut cur,
11481                n_embd,
11482                t,
11483                eps,
11484            )?;
11485            let next_norm = if il + 1 < n_layers {
11486                Some(self.layers[il + 1].attn_norm.float_data())
11487            } else {
11488                None
11489            };
11490            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, t, next_norm)?;
11491            x = xn;
11492            h_carry = hn;
11493            self.dflash_tap(e, cache, il, &x, t)?;
11494        }
11495        let mut hn = e.uninit(t * n_embd)?;
11496        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
11497        let ld = e.matmul(&self.output, &hn, t)?;
11498        let n_vocab = self.output.out_features();
11499        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
11500        for i in 0..t {
11501            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
11502        }
11503        Ok((vam, hn))
11504    }
11505
11506    /// Verify trunk core: returns (UN-softcapped logits device [t, n_vocab], post-output_norm
11507    /// hidden stack [t, n_embd]); appends KV rows + advances cache.pos.
11508    /// DFlash tap write (dflash lane): copy the post-layer residual rows of `x` into the
11509    /// armed sink at the tap slot for `il` (row-major [t, n_taps*hidden]). Per-row D2D
11510    /// copies — t <= block_size on verify; prime pays t*n_taps once per prompt (dedicated
11511    /// kernel later if it shows in the profile).
11512    fn dflash_tap(
11513        &self,
11514        e: &Engine,
11515        cache: &mut Cache,
11516        il: usize,
11517        x: &CudaSlice<f32>,
11518        t: usize,
11519    ) -> Result<(), Box<dyn std::error::Error>> {
11520        let Some(taps) = cache.dflash_taps.as_mut() else {
11521            return Ok(());
11522        };
11523        let Some(slot) = taps.layer_ids.iter().position(|&l| l == il) else {
11524            return Ok(());
11525        };
11526        let h = taps.hidden;
11527        let n_taps = taps.layer_ids.len();
11528        debug_assert_eq!(taps.t, t);
11529        let xv = e.view(x, t * h);
11530        for r in 0..t {
11531            let row = xv.slice(r * h..(r + 1) * h);
11532            e.copy_view_into(&mut taps.buf, r * n_taps * h + slot * h, &row, h)?;
11533        }
11534        Ok(())
11535    }
11536
11537    fn gemma4_verify_trunk(
11538        &self,
11539        e: &Engine,
11540        tokens: &[u32],
11541        pos0: usize,
11542        cache: &mut Cache,
11543        tok_dev: Option<&CudaSlice<u32>>,
11544    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
11545        let n_embd = self.cfg.n_embd as usize;
11546        let eps = self.cfg.rms_eps;
11547        let t = tokens.len();
11548        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
11549        let pos_d = e.htod_i32(&pos)?;
11550        let mut x = match tok_dev {
11551            Some(td) => {
11552                let embd_gpu = self
11553                    .embd_gpu
11554                    .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload"));
11555                let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
11556                e.embed_gather_device_td(embd_gpu, td, t, n_embd, qt, rb)?
11557            }
11558            None => e.htod(&self.embd.gather(n_embd, tokens))?,
11559        };
11560        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
11561        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
11562        let n_layers = self.layers.len();
11563        for (il, layer) in self.layers.iter().enumerate() {
11564            let (hq, hdq) = match h_carry.take() {
11565                Some(p) => p,
11566                None => {
11567                    e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, t, eps)?
11568                }
11569            };
11570            let Mixer::Full(fa) = &layer.mixer else {
11571                panic!("gemma4 layer {il} not full-attn")
11572            };
11573            let o = self.gemma4_verify_attn(e, fa, il, &hq, &hdq, &pos_d, t, cache)?;
11574            let mut cur = e.uninit(t * n_embd)?;
11575            e.rms_norm(
11576                &o,
11577                layer.post_attn_norm.float_data(),
11578                &mut cur,
11579                n_embd,
11580                t,
11581                eps,
11582            )?;
11583            let next_norm = if il + 1 < n_layers {
11584                Some(self.layers[il + 1].attn_norm.float_data())
11585            } else {
11586                None
11587            };
11588            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, t, next_norm)?;
11589            x = xn;
11590            h_carry = hn;
11591            self.dflash_tap(e, cache, il, &x, t)?;
11592        }
11593        let mut hn = e.uninit(t * n_embd)?;
11594        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
11595        let mut ld = e.matmul(&self.output, &hn, t)?;
11596        self.gemma4_suppress(e, &mut ld, t)?; // before the per-row argmax consumers
11597        cache.pos += t;
11598        Ok((ld, hn))
11599    }
11600
11601    /// Verify attention: project/norm/rope t rows, append them to the cache, then attend each
11602    /// row causally over [win_off_i .. base+i] via the SAME fa_decode dispatch as T=1 decode.
11603    /// BURST verify attention (device-slot twin of `gemma4_verify_attn`): the append lands
11604    /// at the layer's len_d counter (rows_dc), attention bases ride the counter (rows /
11605    /// rows_w with base_dev), and NO host len is read or bumped — `hint` is a host UPPER
11606    /// bound on base_len used only for split sizing and arm gating (the burst loop passes
11607    /// pos0 + burst slack). Host len mirrors re-sync at the burst drain.
11608    #[allow(clippy::too_many_arguments)]
11609    fn gemma4_verify_attn_stream(
11610        &self,
11611        e: &Engine,
11612        fa: &crate::hybrid::FullAttnLayer,
11613        il: usize,
11614        hq: &CudaSlice<i8>,
11615        hdq: &CudaSlice<f32>,
11616        pos_d: &CudaSlice<i32>,
11617        t: usize,
11618        cache: &mut Cache,
11619        hint: usize,
11620        row_ctrs: &[CudaSlice<i32>],
11621    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
11622        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
11623        let eps = self.cfg.rms_eps;
11624        let aux = self.gemma4_aux.as_ref().unwrap();
11625        let ones = aux.ones(e);
11626        #[cfg(debug_assertions)]
11627        crate::debug_assert_tensor_stream_device(
11628            ones,
11629            &e.stream(),
11630            "gemma4_verify_attn_stream.ones",
11631        );
11632        let h0 = e.zeros(0)?;
11633        let h = &h0;
11634        // BATCHED FUSED qkv (MEMRA_F2B=1, megakernel microcosm): swa layers fuse all three,
11635        // globals fuse q,k (v := k clone). Bit-identical per row; segments tail-fill.
11636        static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11637        let f2b = *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
11638        let fused_qkv = if f2b {
11639            if swa {
11640                e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
11641                    .map(|(a, b, c)| (a, b, Some(c)))
11642            } else {
11643                e.matmul_q4_fused2_batched(&fa.wq, &fa.wk, hq, hdq, t)?
11644                    .map(|(a, b)| (a, b, None))
11645            }
11646        } else {
11647            None
11648        };
11649        let (q0, k0, v0) = match fused_qkv {
11650            Some((a, b, cv)) => {
11651                let v = match cv {
11652                    Some(c) => c,
11653                    None => e.clone_dtod(&b)?,
11654                };
11655                (a, b, v)
11656            }
11657            None => {
11658                let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
11659                let k0 = e.matmul_pre(&fa.wk, hq, hdq, h, t)?;
11660                let v0 = if swa {
11661                    e.matmul_pre(&fa.wv, hq, hdq, h, t)?
11662                } else {
11663                    e.clone_dtod(&k0)?
11664                };
11665                (q0, k0, v0)
11666            }
11667        };
11668        let mut q = e.uninit(t * nh * hd)?;
11669        let mut k = e.uninit(t * nkv * hd)?;
11670        let mut v = e.uninit(t * nkv * hd)?;
11671        // E4B wave-3 fold (2026-07-23 decode-dust port): q/k/v norms + q/k rope in ONE
11672        // launch — rope math verbatim on the normed rows; V ones-rms, never roped.
11673        let ff = if swa {
11674            None
11675        } else {
11676            Some(
11677                aux.rope_freqs(e)
11678                    .expect("gemma4 global rope needs rope_freqs.weight"),
11679            )
11680        };
11681        #[cfg(debug_assertions)]
11682        if let Some(ff) = ff {
11683            crate::debug_assert_tensor_stream_device(
11684                ff,
11685                &e.stream(),
11686                "gemma4_verify_attn_stream.rope_freqs",
11687            );
11688        }
11689        e.rms_norm_qkv_rope(
11690            &q0,
11691            &k0,
11692            &v0,
11693            fa.q_norm.float_data(),
11694            fa.k_norm.float_data(),
11695            ones,
11696            &mut q,
11697            &mut k,
11698            &mut v,
11699            hd,
11700            nh * t,
11701            nkv * t,
11702            pos_d,
11703            nh,
11704            nkv,
11705            base,
11706            1.0,
11707            ff,
11708            eps,
11709        )?;
11710        let kvl = cache.kv[il].as_mut().unwrap();
11711        // append at the DEVICE slot; the counter advances by t on-device.
11712        e.append_kv_quantized_rows_dc(
11713            &k,
11714            &v,
11715            &mut kvl.k,
11716            &mut kvl.v,
11717            &kvl.len_d,
11718            t,
11719            kvl.kv_dim_k,
11720            kvl.kv_dim_v,
11721            kvl.k_tok_bytes,
11722            kvl.v_tok_bytes,
11723            (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()),
11724        )?;
11725        // len_d is NOT advanced here: the burst's device rollback (spec_rollback_stream) is
11726        // the sole len writer after this round's attention (base stays = old len, plus = 0).
11727        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
11728        let mut attn = e.uninit(t * nh * hd)?;
11729        let k_view = e.view_u8(&kvl.k, kvl.k.len());
11730        let v_view = e.view_u8(&kvl.v, kvl.v.len());
11731        // arm gating on the host HINT (upper bound; burst entry guards hint >= the vec floor
11732        // and a stable window regime — the same rung/regime keys as the draft graph).
11733        if swa && hint + 1 >= win {
11734            // fully-windowed rows: per-row window geometry from the counter (plus = 0: len_d
11735            // still holds the pre-append len; row r's T_kv = ctr + r + 1).
11736            e.fa_decode_rows_w(
11737                &q,
11738                &k_view,
11739                &v_view,
11740                &mut attn,
11741                hd,
11742                nh,
11743                nkv,
11744                &kvl.len_d,
11745                0,
11746                t,
11747                scale,
11748                win,
11749                kvl.k_tok_bytes,
11750                kvl.v_tok_bytes,
11751                None,
11752            )?;
11753        } else if hd == 512 && hint + t < crate::fa512_min_tkv() {
11754            // globals UNDER the fa512 crossover: eager runs the per-row fa_decode_kvmod
11755            // fallback there (parity with t=1 decode) — mirror it with per-row fa_decode_dc
11756            // (bit-correct for any t_kv <= bucket; the drafter dc arc proved the pairing).
11757            // Burst entry gates the horizon onto one side of the crossover, so hint decides
11758            // for every row.
11759            // NO .max(512): fa_decode_dc gates its hd512 dpl16-vs-scalar pick on bucket_max
11760            // (mirroring eager's fa512 floor) — forcing 512 here flipped the arm to dpl16
11761            // while eager ran scalar (il=5 KV drift, the burst's 4/128). SAME LAW capped
11762            // from above (2026-07-13): a regime-pinned hint near the floor (round-graph
11763            // captures use f512-1) pow2-rounds PAST it — clamp under the floor, or the arm
11764            // re-flips to dpl16 (the round-graph 4/64). The scalar unified self-splits, so
11765            // any bucket >= the live length is exact.
11766            let bucket = (hint + t + 2)
11767                .next_power_of_two()
11768                .min(crate::fa512_min_tkv().saturating_sub(1));
11769            let qv = e.view(&q, t * nh * hd);
11770            for i in 0..t {
11771                let q_row = qv.slice(i * nh * hd..(i + 1) * nh * hd);
11772                let mut q_one = e.uninit(nh * hd)?;
11773                e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
11774                let mut a_one = e.uninit(nh * hd)?;
11775                e.fa_decode_dc(
11776                    &q_one,
11777                    &k_view,
11778                    &v_view,
11779                    &mut a_one,
11780                    hd,
11781                    nh,
11782                    nkv,
11783                    &row_ctrs[i],
11784                    bucket,
11785                    scale,
11786                    kvl.k_tok_bytes,
11787                    kvl.v_tok_bytes,
11788                    false,
11789                )?;
11790                e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
11791            }
11792        } else if hd == 512 {
11793            // globals past the crossover: dpl16 dc twin via the shared rows wrapper (hint
11794            // sizes splits — upper bound; splits beyond the device len exit in-kernel).
11795            e.fa_decode_rows(
11796                &q,
11797                &k_view,
11798                &v_view,
11799                &mut attn,
11800                hd,
11801                nh,
11802                nkv,
11803                hint,
11804                t,
11805                scale,
11806                kvl.k_tok_bytes,
11807                kvl.v_tok_bytes,
11808                Some((&kvl.len_d, 0)),
11809                false,
11810                false,
11811                None,
11812            )?;
11813        } else {
11814            // hd256 under-window: v4 device-len rows twin.
11815            e.fa_decode_rows_dc(
11816                &q,
11817                &k_view,
11818                &v_view,
11819                &mut attn,
11820                hd,
11821                nh,
11822                nkv,
11823                &kvl.len_d,
11824                hint + t,
11825                t,
11826                scale,
11827                kvl.k_tok_bytes,
11828                kvl.v_tok_bytes,
11829                0,
11830                swa && crate::Engine::wkv_on(),
11831            )?;
11832        }
11833        Ok(e.matmul(&fa.wo, &attn, t)?)
11834    }
11835
11836    fn gemma4_verify_attn(
11837        &self,
11838        e: &Engine,
11839        fa: &crate::hybrid::FullAttnLayer,
11840        il: usize,
11841        hq: &CudaSlice<i8>,
11842        hdq: &CudaSlice<f32>,
11843        pos_d: &CudaSlice<i32>,
11844        t: usize,
11845        cache: &mut Cache,
11846    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
11847        let (hd, nkv, nh, base, scale, swa) = self.gemma4_geom(il);
11848        let eps = self.cfg.rms_eps;
11849        let aux = self.gemma4_aux.as_ref().unwrap();
11850        let ones = aux.ones(e);
11851        #[cfg(debug_assertions)]
11852        crate::debug_assert_tensor_stream_device(ones, &e.stream(), "gemma4_verify_attn.ones");
11853        let n_embd = self.cfg.n_embd as usize;
11854        let _ = n_embd;
11855
11856        let h0 = e.zeros(0)?;
11857        let h = &h0;
11858        // BATCHED FUSED qkv (MEMRA_F2B=1, megakernel microcosm): swa layers fuse all three,
11859        // globals fuse q,k (v := k clone). Bit-identical per row; segments tail-fill.
11860        static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
11861        let f2b = *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0"));
11862        let fused_qkv = if f2b {
11863            if swa {
11864                e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
11865                    .map(|(a, b, c)| (a, b, Some(c)))
11866            } else {
11867                e.matmul_q4_fused2_batched(&fa.wq, &fa.wk, hq, hdq, t)?
11868                    .map(|(a, b)| (a, b, None))
11869            }
11870        } else {
11871            None
11872        };
11873        let (q0, k0, v0) = match fused_qkv {
11874            Some((a, b, cv)) => {
11875                let v = match cv {
11876                    Some(c) => c,
11877                    None => e.clone_dtod(&b)?,
11878                };
11879                (a, b, v)
11880            }
11881            None => {
11882                let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
11883                let k0 = e.matmul_pre(&fa.wk, hq, hdq, h, t)?;
11884                let v0 = if swa {
11885                    e.matmul_pre(&fa.wv, hq, hdq, h, t)?
11886                } else {
11887                    e.clone_dtod(&k0)?
11888                };
11889                (q0, k0, v0)
11890            }
11891        };
11892        let mut q = e.uninit(t * nh * hd)?;
11893        let mut k = e.uninit(t * nkv * hd)?;
11894        let mut v = e.uninit(t * nkv * hd)?;
11895        // E4B wave-3 fold (2026-07-23 decode-dust port): q/k/v norms + q/k rope in ONE
11896        // launch — rope math verbatim on the normed rows; V ones-rms, never roped.
11897        let ff = if swa {
11898            None
11899        } else {
11900            Some(
11901                aux.rope_freqs(e)
11902                    .expect("gemma4 global rope needs rope_freqs.weight"),
11903            )
11904        };
11905        #[cfg(debug_assertions)]
11906        if let Some(ff) = ff {
11907            crate::debug_assert_tensor_stream_device(
11908                ff,
11909                &e.stream(),
11910                "gemma4_verify_attn.rope_freqs",
11911            );
11912        }
11913        e.rms_norm_qkv_rope(
11914            &q0,
11915            &k0,
11916            &v0,
11917            fa.q_norm.float_data(),
11918            fa.k_norm.float_data(),
11919            ones,
11920            &mut q,
11921            &mut k,
11922            &mut v,
11923            hd,
11924            nh * t,
11925            nkv * t,
11926            pos_d,
11927            nh,
11928            nkv,
11929            base,
11930            1.0,
11931            ff,
11932            eps,
11933        )?;
11934        let kvl = cache.kv[il].as_mut().unwrap();
11935        let base_len = kvl.len;
11936        e.append_kv_quantized_rows(
11937            &k,
11938            &v,
11939            &mut kvl.k,
11940            &mut kvl.v,
11941            base_len,
11942            t,
11943            kvl.kv_dim_k,
11944            kvl.kv_dim_v,
11945            kvl.k_tok_bytes,
11946            kvl.v_tok_bytes,
11947            (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()),
11948        )?;
11949        kvl.len += t;
11950        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
11951        let mut attn = e.uninit(t * nh * hd)?;
11952        // ROWS twin when no window offset is in play (globals are hd512-ineligible; SWA rows
11953        // under the window need no offset): ONE launch, per-row causal == the per-token loop.
11954        let rows_ok = (hd == 256 && base_len + 1 >= crate::fa_vec_min_tkv())
11955            // gemma globals: hd512 rows twin in the dpl16 vec regime (row 0 gates the batch);
11956            // decode rides the SAME symbol at t=1 (parity law).
11957            || (hd == 512 && !swa && base_len + 1 >= crate::fa512_min_tkv());
11958        if rows_ok && (!swa || base_len + t <= win) {
11959            let k_view = e.view_u8(&kvl.k, (base_len + t) * kvl.k_tok_bytes);
11960            let v_view = e.view_u8(&kvl.v, (base_len + t) * kvl.v_tok_bytes);
11961            if hd == 512 {
11962                // device-len twin: sync the counter to the verify base (async arg-store).
11963                e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
11964                e.fa_decode_rows(
11965                    &q,
11966                    &k_view,
11967                    &v_view,
11968                    &mut attn,
11969                    hd,
11970                    nh,
11971                    nkv,
11972                    base_len,
11973                    t,
11974                    scale,
11975                    kvl.k_tok_bytes,
11976                    kvl.v_tok_bytes,
11977                    Some((&kvl.len_d, 0)),
11978                    false,
11979                    swa && crate::Engine::wkv_on(),
11980                    None,
11981                )?;
11982            } else {
11983                // hd256: the SAME v4_dc symbol the burst verify launches (PARITY LAW — the
11984                // host-base rows_v4 twin compiles apart from v4_dc and the two-symbol split
11985                // drifted the burst's persisted KV at il=8; one symbol, counter arg-stored).
11986                e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
11987                e.fa_decode_rows_dc(
11988                    &q,
11989                    &k_view,
11990                    &v_view,
11991                    &mut attn,
11992                    hd,
11993                    nh,
11994                    nkv,
11995                    &kvl.len_d,
11996                    base_len + t,
11997                    t,
11998                    scale,
11999                    kvl.k_tok_bytes,
12000                    kvl.v_tok_bytes,
12001                    0,
12002                    swa && crate::Engine::wkv_on(),
12003                )?;
12004            }
12005            return Ok(e.matmul(&fa.wo, &attn, t)?);
12006        }
12007        // WINDOWED rows twin (deep ctx, every row fully windowed): one launch, ABSOLUTE-index
12008        // per-row geometry over the prefix view. PARITY LAW (2026-07-10 root-cause): textually
12009        // identical kernels do NOT compile bit-identically (nvcc unrolls fa_decode_vec_q 2x vs
12010        // its rows_w clone — SASS-proven; the unpinned score `+=` chain then rounds apart), so
12011        // decode-vs-verify parity comes from BOTH sides launching THIS SAME rows_w kernel
12012        // (decode passes t=1) — bitwise equal per position by symbol identity, any lane.
12013        // MEMRA_GEMMA_ROWS_W=0 -> per-token loop (decode falls back to fa_decode views too).
12014        if hd == 256
12015            && swa
12016            && base_len + 1 >= win
12017            && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
12018        {
12019            let k_view = e.view_u8(&kvl.k, (base_len + t) * kvl.k_tok_bytes);
12020            let v_view = e.view_u8(&kvl.v, (base_len + t) * kvl.v_tok_bytes);
12021            e.i32_set_k(&mut kvl.len_d, base_len as i32)?;
12022            e.fa_decode_rows_w(
12023                &q,
12024                &k_view,
12025                &v_view,
12026                &mut attn,
12027                hd,
12028                nh,
12029                nkv,
12030                &kvl.len_d,
12031                0,
12032                t,
12033                scale,
12034                win,
12035                kvl.k_tok_bytes,
12036                kvl.v_tok_bytes,
12037                None,
12038            )?;
12039            return Ok(e.matmul(&fa.wo, &attn, t)?);
12040        }
12041        for i in 0..t {
12042            let avail = base_len + i + 1;
12043            let (off_tok, t_kv) = if swa && avail > win {
12044                (avail - win, win)
12045            } else {
12046                (0, avail)
12047            };
12048            let k_view = e.view_u8_range(
12049                &kvl.k,
12050                off_tok * kvl.k_tok_bytes,
12051                (off_tok + t_kv) * kvl.k_tok_bytes,
12052            );
12053            let v_view = e.view_u8_range(
12054                &kvl.v,
12055                off_tok * kvl.v_tok_bytes,
12056                (off_tok + t_kv) * kvl.v_tok_bytes,
12057            );
12058            let qi = e.view(&q, t * nh * hd);
12059            let q_row = qi.slice(i * nh * hd..(i + 1) * nh * hd);
12060            let mut q_one = e.uninit(nh * hd)?;
12061            e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
12062            let mut a_one = e.uninit(nh * hd)?;
12063            // straddle rounds: windowed rows must ride the SAME rows_w kernel decode uses
12064            // (parity law above); global hd512 rows past the fa512 floor likewise ride the
12065            // rows_dpl16 twin; remaining rows keep the gated fa_decode pair.
12066            if swa
12067                && avail > win
12068                && hd == 256
12069                && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
12070            {
12071                let kp = e.view_u8(&kvl.k, avail * kvl.k_tok_bytes);
12072                let vp = e.view_u8(&kvl.v, avail * kvl.v_tok_bytes);
12073                e.i32_set_k(&mut kvl.len_d, (avail - 1) as i32)?;
12074                e.fa_decode_rows_w(
12075                    &q_one,
12076                    &kp,
12077                    &vp,
12078                    &mut a_one,
12079                    hd,
12080                    nh,
12081                    nkv,
12082                    &kvl.len_d,
12083                    0,
12084                    1,
12085                    scale,
12086                    win,
12087                    kvl.k_tok_bytes,
12088                    kvl.v_tok_bytes,
12089                    None,
12090                )?;
12091            } else if !swa
12092                && hd == 512
12093                && avail >= crate::fa512_min_tkv()
12094                && std::env::var("MEMRA_GEMMA_ROWS_W").as_deref() != Ok("0")
12095            {
12096                let kp = e.view_u8(&kvl.k, avail * kvl.k_tok_bytes);
12097                let vp = e.view_u8(&kvl.v, avail * kvl.v_tok_bytes);
12098                e.i32_set_k(&mut kvl.len_d, (avail - 1) as i32)?;
12099                e.fa_decode_rows(
12100                    &q_one,
12101                    &kp,
12102                    &vp,
12103                    &mut a_one,
12104                    hd,
12105                    nh,
12106                    nkv,
12107                    avail - 1,
12108                    1,
12109                    scale,
12110                    kvl.k_tok_bytes,
12111                    kvl.v_tok_bytes,
12112                    Some((&kvl.len_d, 0)),
12113                    false,
12114                    false,
12115                    None,
12116                )?;
12117            } else {
12118                e.fa_decode_kvmod(
12119                    &q_one,
12120                    &k_view,
12121                    &v_view,
12122                    &mut a_one,
12123                    hd,
12124                    nh,
12125                    nkv,
12126                    t_kv,
12127                    scale,
12128                    kvl.k_tok_bytes,
12129                    kvl.v_tok_bytes,
12130                    swa && crate::Engine::wkv_on(),
12131                )?;
12132            }
12133            e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
12134        }
12135        Ok(e.matmul(&fa.wo, &attn, t)?)
12136    }
12137
12138    /// gemma4 T=1 decode step: R8 layer graph over the cache; returns (softcapped logits host,
12139    /// h_seed = pre-output_norm hidden). Advances cache.pos.
12140    pub(crate) fn gemma4_decode_step_h(
12141        &self,
12142        e: &Engine,
12143        token: u32,
12144        cache: &mut Cache,
12145    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
12146        // M1-PP2 door (crate::pp): 2-stage split with an explicit activation handoff.
12147        // Default OFF — unset env means this branch is never taken. The gemma4 arm stays
12148        // 2-stage in M2 (the N-stage gate model is the generic-arm 9B); N>2 warns + runs
12149        // unsplit rather than guessing a fence.
12150        if let Some(split) = crate::pp::pp2_split(self.layers.len()) {
12151            return self.gemma4_decode_step_h_pp2(e, token, cache, split);
12152        }
12153        if crate::pp::pp_cuts(self.layers.len()).is_some() {
12154            crate::pp::warn_unwired_once("gemma4 eager decode (N>2)");
12155        }
12156        let n_embd = self.cfg.n_embd as usize;
12157        let eps = self.cfg.rms_eps;
12158        let pos_d = e.htod_i32(&[cache.pos as i32])?;
12159        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
12160        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
12161        // cross-layer fusion: each tail's closing add+scale also EMITS the next layer's
12162        // attn-normed input pre-quantized q8_1 (the mixer consumes only quantized matmuls).
12163        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
12164        let n_layers = self.layers.len();
12165        for (il, layer) in self.layers.iter().enumerate() {
12166            let (hq, hdq) = match h_carry.take() {
12167                Some(p) => p,
12168                None => {
12169                    e.rms_norm_q8_1(&x, self.layers[0].attn_norm.float_data(), n_embd, 1, eps)?
12170                }
12171            };
12172            let Mixer::Full(fa) = &layer.mixer else {
12173                panic!("gemma4 layer {il} not full-attn")
12174            };
12175            let o = self.gemma4_decode_attn(e, fa, il, &hq, &hdq, &pos_d, cache)?;
12176            let mut cur = e.uninit(n_embd)?;
12177            e.rms_norm(
12178                &o,
12179                layer.post_attn_norm.float_data(),
12180                &mut cur,
12181                n_embd,
12182                1,
12183                eps,
12184            )?;
12185            let next_norm = if il + 1 < n_layers {
12186                Some(self.layers[il + 1].attn_norm.float_data())
12187            } else {
12188                None
12189            };
12190            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
12191            x = xn;
12192            h_carry = hn;
12193        }
12194        let mut hn = e.uninit(n_embd)?;
12195        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
12196        let h_seed = e.clone_dtod(&x)?;
12197        let mut ld = e.matmul(&self.output, &hn, 1)?;
12198        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
12199        e.softcap(&mut ld, cap, self.output.out_features())?; // R4 on device (262k host tanh ~ms/step)
12200        self.gemma4_suppress(e, &mut ld, 1)?;
12201        let logits = e.dtoh(&ld)?;
12202        cache.pos += 1;
12203        Ok((logits, h_seed))
12204    }
12205
12206    /// M1-PP2 stage subgraph (gemma4 arm): layers [lo, hi) of the gemma4 T=1 walk with the
12207    /// pre-quantized next-norm carry LOCAL to the range. Enters with a materialized residual
12208    /// `x` (the range head runs its own `rms_norm_q8_1` against ITS layer's attn_norm — for
12209    /// lo == 0 that is exactly the unsplit loop's il==0 arm); exits with the residual
12210    /// materialized (range tail passes next_norm = None, exactly the unsplit last-layer arm).
12211    /// Bit-identity of the cut relies on the kernel-check-pinned `add_scale_rms_norm_q8_1 ==
12212    /// add_scale then rms_norm_q8_1` identity (`pp2-gate` verifies end-to-end).
12213    fn gemma4_decode_layers(
12214        &self,
12215        e: &Engine,
12216        mut x: CudaSlice<f32>,
12217        lo: usize,
12218        hi: usize,
12219        pos_d: &CudaSlice<i32>,
12220        cache: &mut Cache,
12221    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12222        let n_embd = self.cfg.n_embd as usize;
12223        let eps = self.cfg.rms_eps;
12224        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
12225        for il in lo..hi {
12226            let layer = &self.layers[il];
12227            let (hq, hdq) = match h_carry.take() {
12228                Some(p) => p,
12229                // range head: il == lo — norm against THIS layer's attn_norm.
12230                None => {
12231                    e.rms_norm_q8_1(&x, self.layers[il].attn_norm.float_data(), n_embd, 1, eps)?
12232                }
12233            };
12234            let Mixer::Full(fa) = &layer.mixer else {
12235                panic!("gemma4 layer {il} not full-attn")
12236            };
12237            let o = self.gemma4_decode_attn(e, fa, il, &hq, &hdq, pos_d, cache)?;
12238            let mut cur = e.uninit(n_embd)?;
12239            e.rms_norm(
12240                &o,
12241                layer.post_attn_norm.float_data(),
12242                &mut cur,
12243                n_embd,
12244                1,
12245                eps,
12246            )?;
12247            let next_norm = if il + 1 < hi {
12248                Some(self.layers[il + 1].attn_norm.float_data())
12249            } else {
12250                None
12251            };
12252            let (xn, hn) = self.gemma4_layer_tail_add_nq(e, layer, &cur, &x, 1, next_norm)?;
12253            x = xn;
12254            h_carry = hn;
12255        }
12256        Ok(x)
12257    }
12258
12259    /// M1-PP2 (increment 2): `gemma4_decode_step_h` as TWO stage subgraphs, each on its
12260    /// own stream (and device, under MEMRA_PP_DEVICES) with the transport-selected
12261    /// boundary handoff — same choreography as the generic arm (decode.rs), same
12262    /// ownership contract (crate::pp). Stage 0 = embed+scale + layers [0, split);
12263    /// stage 1 = layers [split, n) + output_norm + softcapped head.
12264    /// Each stage uploads its own copy of the step's position scalar on its own stream.
12265    /// MEMRA_PP_STREAMS=0 = the increment-1 same-stream seam. Gate: `pp2-gate`.
12266    fn gemma4_decode_step_h_pp2(
12267        &self,
12268        e: &Engine,
12269        token: u32,
12270        cache: &mut Cache,
12271        split: usize,
12272    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
12273        if crate::pp::pp2_streams_off() {
12274            return self.gemma4_decode_step_h_pp2_samestream(e, token, cache, split);
12275        }
12276        let rt = crate::pp::Pp2Rt::get(e)?;
12277        let e0 = rt.engine(0, e);
12278        let e1 = rt.engine(1, e);
12279        let n_embd = self.cfg.n_embd as usize;
12280        let eps = self.cfg.rms_eps;
12281        let pos = cache.pos as i32;
12282
12283        // ---- STAGE 0 (its own stream): embed + sqrt(n_embd) scale + layers [0, split) ----
12284        let slot = {
12285            let _st0 = rt.enter(0);
12286            let pos_d = e0.htod_i32(&[pos])?;
12287            #[cfg(debug_assertions)]
12288            crate::debug_assert_tensor_stream_device(
12289                &pos_d,
12290                &e0.stream(),
12291                "gemma4_decode_step_h_pp2.stage0.pos_d",
12292            );
12293            let mut x = e0.htod(&self.embd.gather(n_embd, &[token]))?;
12294            e0.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
12295            let x = self.gemma4_decode_layers(e0, x, 0, split, &pos_d, cache)?;
12296            rt.tx(0, &x, n_embd)?
12297        };
12298
12299        // ---- STAGE 1 (its own stream): RX + layers [split, n) + softcapped head ----
12300        let _st1 = rt.enter(1);
12301        let pos_d = e1.htod_i32(&[pos])?;
12302        #[cfg(debug_assertions)]
12303        crate::debug_assert_tensor_stream_device(
12304            &pos_d,
12305            &e1.stream(),
12306            "gemma4_decode_step_h_pp2.stage1.pos_d",
12307        );
12308        let x = rt.rx(0, slot, n_embd)?;
12309        let x = self.gemma4_decode_layers(e1, x, split, self.layers.len(), &pos_d, cache)?;
12310
12311        let mut hn = e1.uninit(n_embd)?;
12312        e1.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
12313        let h_seed = e1.clone_dtod(&x)?;
12314        let mut ld = e1.matmul(&self.output, &hn, 1)?;
12315        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
12316        e1.softcap(&mut ld, cap, self.output.out_features())?;
12317        self.gemma4_suppress(e1, &mut ld, 1)?;
12318        let logits = e1.dtoh(&ld)?;
12319        cache.pos += 1;
12320        Ok((logits, h_seed))
12321    }
12322
12323    /// MEMRA_PP_STREAMS=0 rollback seam: the increment-1 gemma4 pp2 body verbatim — both
12324    /// stage subgraphs on the ambient compute stream, boundary = two plain dtod copies.
12325    fn gemma4_decode_step_h_pp2_samestream(
12326        &self,
12327        e: &Engine,
12328        token: u32,
12329        cache: &mut Cache,
12330        split: usize,
12331    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
12332        let n_embd = self.cfg.n_embd as usize;
12333        let eps = self.cfg.rms_eps;
12334        let pos_d = e.htod_i32(&[cache.pos as i32])?;
12335
12336        // ---- STAGE 0: embed + sqrt(n_embd) scale + layers [0, split) ----
12337        let mut x = e.htod(&self.embd.gather(n_embd, &[token]))?;
12338        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
12339        let x = self.gemma4_decode_layers(e, x, 0, split, &pos_d, cache)?;
12340
12341        // ---- STAGE BOUNDARY: explicit [n_embd] activation handoff (TX copy, RX copy) ----
12342        let boundary_tx = e.clone_dtod(&x)?;
12343        let boundary_rx = e.clone_dtod(&boundary_tx)?;
12344
12345        // ---- STAGE 1: layers [split, n) + output_norm + softcapped head ----
12346        let x =
12347            self.gemma4_decode_layers(e, boundary_rx, split, self.layers.len(), &pos_d, cache)?;
12348
12349        let mut hn = e.uninit(n_embd)?;
12350        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
12351        let h_seed = e.clone_dtod(&x)?;
12352        let mut ld = e.matmul(&self.output, &hn, 1)?;
12353        let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
12354        e.softcap(&mut ld, cap, self.output.out_features())?;
12355        self.gemma4_suppress(e, &mut ld, 1)?;
12356        let logits = e.dtoh(&ld)?;
12357        cache.pos += 1;
12358        Ok((logits, h_seed))
12359    }
12360}
12361
12362// ============================ step35 (Step-3.7-Flash) ==================================
12363// Node-for-node vs llama.cpp src/models/step35.cpp:216-300. WHY THIS IS A DEDICATED MIXER
12364// FAMILY and not a few branches inside the generic `full_attn*` chain:
12365//
12366//   1. `n_head` IS PER LAYER (`step35.attention.head_count` is an ARRAY: 64 on full-attn
12367//      layers, 96 on SWA). Every generic site reads the `cfg.n_head` scalar, so wq/wo/attn_gate
12368//      shapes and the FA head counts would be wrong on 33 of 45 layers.
12369//   2. `cfg.rope_dim_count` is 128 for this arch, but FULL-attn layers rotate only 64 dims
12370//      (upstream halves `n_rot_full` after the generic loader defaults it). A generic mixer
12371//      would rotate 128 dims on the full layers — silently wrong, plausible-looking logits.
12372//   3. Dual rope base (5e6 full / 1e4 SWA) + `rope_freqs.weight` on FULL layers ONLY.
12373//   4. SWA 3:1 (`sliding_window` 512, pattern [false,true,true,true]).
12374//   5. The head-wise gate is a SEPARATE `attn_gate.weight [n_embd, n_head_l]` (one pre-sigmoid
12375//      scalar per head, broadcast over head_dim) applied to the attention output BEFORE wo —
12376//      not the qwen35 fused-in-wq per-DIM gate that `attn_out_gate()`/`q_gate_split` handle.
12377//      Its input is the POST-attn_norm hidden (`cur`), not the residual.
12378//
12379// Attention scale is the DEFAULT 1/sqrt(n_embd_head_k) (step35.cpp:255) — NOT gemma4's 1.0.
12380impl HybridModel {
12381    /// Step35's declarative geometry row. Missing rows are artifact/config errors; never
12382    /// synthesize a drafter or trunk layer from a neighboring class.
12383    pub(crate) fn step35_geom(&self, il: usize) -> memra_gguf::config::LayerGeometry {
12384        let geometry = self
12385            .cfg
12386            .layer_geometry(il as u32)
12387            .unwrap_or_else(|| panic!("step35 layer {il} has no geometry-table row"));
12388        debug_assert_eq!(
12389            geometry.attention_gate,
12390            memra_gguf::config::AttentionGateKind::SeparateHead
12391        );
12392        geometry
12393    }
12394
12395    /// step35 attention core: everything from the q/k/v projection outputs through the head-wise
12396    /// gate, returning the GATED attention output PRE-`wo` (the caller runs wo, so this composes
12397    /// with both the plain `matmul(&fa.wo, ..)` sites and the f16/into-slab GEMM sites).
12398    ///
12399    /// `hg` = the POST-attn_norm hidden state (upstream's `cur`) — the gate projection's input
12400    /// on single-sequence paths. `gt_pre` supplies the already-projected per-head gate for a
12401    /// cross-request batch; exactly one of `hg` or `gt_pre` must be present.
12402    /// `cache`:
12403    ///   * `Some` => PRIME mode: append this chunk's post-rope K / raw V rows into the resident
12404    ///     quantized cache and attend THROUGH the cache view, exactly like the generic
12405    ///     `full_attn_prime_fa_dispatch` (quantize-then-attend on chunk 0 too, so chunk size
12406    ///     cannot decide where a precision edge falls — the grain-free chunk-invariance
12407    ///     contract, lane/chunkinv-flip).
12408    ///   * `None` => pure prefill (forward/forward_last/t2probe): attend over this batch's f32
12409    ///     q/k/v, no cache side effect.
12410    ///
12411    /// SWA WINDOW, and why prefill needs `sdpa_naive_w_quantized_view`: the view is trimmed to
12412    /// the oldest key ANY query in this chunk can reach (`off = base_len - (win-1)`), which
12413    /// bounds the view at `win-1+t` rows, but the mask is still required — inside the chunk,
12414    /// query `qt` may only see view keys `[qt, qt+win-1]`, so the earlier keys the trimmed view
12415    /// still contains must be masked per query. memra's window convention
12416    /// (`sdpa_naive_w_f32`: mask `t < q_pos-(window-1)`, `q_pos = (T_kv-T)+qt`) is bit-for-bit
12417    /// upstream's `LLAMA_SWA_TYPE_STANDARD` (`llama-hparams.h:359`: mask `p1-p0 >= n_swa`), and
12418    /// step35.cpp:6 sets exactly that swa_type — verified verbatim on both sides.
12419    ///
12420    /// Note the f32 `sdpa_naive_w` floor's shared memory is `t_kv*4` bytes, so the SWA arm needs
12421    /// `t_kv = win-1+t <= 12287` — i.e. it relies on chunked prefill (MEMRA_PRIME_CHUNK, default
12422    /// 4096). A monolithic 32k prime would exceed the 48 KiB dynamic-smem default.
12423    ///
12424    /// SWA ARM SELECTION IS KEYED ON `seq_end`, NOT ON THE CHUNK'S OWN `t_kv` (lane/step35-chunkfix,
12425    /// 2026-08-07). `seq_end` = the ABSOLUTE end position of the whole prime call
12426    /// (`cache.pos + prompt_len` at entry; `t` on the cacheless prefill path) — a property of the
12427    /// REQUEST, identical at every MEMRA_PRIME_CHUNK value. The old predicate `swa && t_kv > win`
12428    /// read the chunk's own extent, which made kernel selection — and therefore the logits, the
12429    /// hidden rows, and the generated text — a function of the chunk size:
12430    ///   a chunk [b,e) has off = max(0, b-(win-1)), t_kv = e-off, so b <= win-1 => off=0 => FA iff
12431    ///   e <= win, while every later chunk has t_kv = t+(win-1) > win. The FA rows were therefore a
12432    ///   contiguous PREFIX [0,P) with P = c*floor(win/c) for c <= win (else 0), and the output
12433    ///   depended only on P. Measured (research/step37-p2-20260806, closed-form receipt
12434    ///   `raw/chunkinv-step35-GAP2-CONFIRMED-20260807.txt`): at T=4883, c=512 and c=64 (P=512) are
12435    ///   byte-identical to each other but DIFFER from the c=4096 default (P=0) by maxdiff 1.813e0
12436    ///   with greedy text diverging at step 6; c=513 (P=0) is bit-identical to the default. A
12437    ///   one-token change in a documented machine-config knob changed the answer.
12438    /// So the pre-fix comment "same cache bytes, same numeric class" was false as written: same
12439    /// bytes, DIFFERENT numeric class — swapping `fa_prefill_view_ws` for the f32 windowed floor on
12440    /// the same rows moves the logits by ~1.8.
12441    ///
12442    /// Keying on `seq_end` makes P identically 0 for every chunk size, so MEMRA_PRIME_CHUNK is a
12443    /// pure memory/transient knob again (research/step35-chunkfix-20260807). It is
12444    /// correct-by-construction rather than a tolerance argument — the windowed kernel computes the
12445    /// same masked attention the FA arm computed (for e <= win the window mask is a no-op under
12446    /// causal, which is exactly why FA was legal there), it is now simply the ONLY arm used once
12447    /// the request passes the window. It also cannot move the shipped default: at chunk=4096 every
12448    /// chunk of a `seq_end > win` prime ALREADY had t_kv > win (chunk 0 has t_kv = min(chunk,
12449    /// seq_end) > win; every later chunk has t_kv >= t+win-1 > win since `PRIME_MIN_T` keeps
12450    /// t >= 16), and a `seq_end <= win` prime keeps every chunk on FA exactly as before — so this
12451    /// is a no-op on both default regimes and only removes the FA prefix at small chunk values.
12452    /// The `t_kv <= 12287` smem ceiling is respected: the chunks whose arm changes are precisely
12453    /// those with t_kv <= win = 512.
12454    #[allow(clippy::too_many_arguments)]
12455    fn step35_attn_pre_wo(
12456        &self,
12457        e: &Engine,
12458        fa: &FullAttnLayer,
12459        mut g3: Vec<CudaSlice<f32>>,
12460        hg: Option<&CudaSlice<f32>>,
12461        gt_pre: Option<&CudaSlice<f32>>,
12462        pos_d: &CudaSlice<i32>,
12463        t: usize,
12464        cache: Option<&mut Cache>,
12465        il: usize,
12466        seq_end: usize,
12467    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12468        let geometry = self.step35_geom(il);
12469        let hd = geometry.head_dim_k as usize;
12470        let nkv = geometry.n_head_kv as usize;
12471        let nh = geometry.n_head as usize;
12472        let rbase = geometry.rope_base;
12473        let scale = geometry.attention_scale();
12474        let swa = geometry.window.is_some();
12475        let eps = self.cfg.rms_eps;
12476        let win = geometry.window.unwrap_or(0) as usize;
12477        let n_rot = geometry.n_rot as usize;
12478
12479        let v = g3.pop().unwrap();
12480        let k0 = g3.pop().unwrap();
12481        let q0 = g3.pop().unwrap();
12482
12483        // q/k RMSNorm over head_dim rows (attn_q_norm/attn_k_norm [128] F32), then the
12484        // per-layer PARTIAL NEOX rope: n_rot = 64 on full layers (dims 64..127 pass through
12485        // unrotated), 128 on SWA. rope_freqs (llama3-style per-dim factors) on FULL only.
12486        let mut q = e.uninit(t * nh * hd)?;
12487        e.rms_norm(&q0, fa.q_norm.float_data(), &mut q, hd, nh * t, eps)?;
12488        let mut k = e.uninit(t * nkv * hd)?;
12489        e.rms_norm(&k0, fa.k_norm.float_data(), &mut k, hd, nkv * t, eps)?;
12490        let ff = if geometry.rope_factors {
12491            self.step35_aux.as_ref().and_then(|a| a.rope_freqs(e))
12492        } else {
12493            None
12494        };
12495        #[cfg(debug_assertions)]
12496        if let Some(ff) = ff {
12497            crate::debug_assert_tensor_stream_device(
12498                ff,
12499                &e.stream(),
12500                "step35_attn_pre_wo.rope_freqs",
12501            );
12502        }
12503        e.rope_neox2(&mut q, &mut k, pos_d, hd, n_rot, nh, nkv, t, rbase, 1.0, ff)?;
12504
12505        let mut attn = e.uninit(t * nh * hd)?;
12506        match cache {
12507            Some(cache) => {
12508                let base_len = cache.kv[il].as_ref().unwrap().len;
12509                // Read per layer call, never in a measured default.
12510                let legacy_tkv = std::env::var("MEMRA_STEP35_SWA_TKV").as_deref() == Ok("1");
12511                let legacy_calllocal = std::env::var("MEMRA_PRIME_CALLLOCAL").as_deref() == Ok("1");
12512                let off = if swa {
12513                    let raw = base_len.saturating_sub(win - 1);
12514                    if legacy_tkv || legacy_calllocal {
12515                        raw
12516                    } else {
12517                        raw & !31usize
12518                    }
12519                } else {
12520                    0
12521                };
12522                {
12523                    let kvl = cache.kv[il].as_mut().unwrap();
12524                    assert!(kvl.len + t <= cache.max_ctx, "step35 prime: KV overflow");
12525                    let write_row = e.prepare_kv_append(kvl, off, t)?;
12526                    e.append_kv_quantized_rows(
12527                        &k,
12528                        &v,
12529                        &mut kvl.k,
12530                        &mut kvl.v,
12531                        write_row,
12532                        t,
12533                        kvl.kv_dim_k,
12534                        kvl.kv_dim_v,
12535                        kvl.k_tok_bytes,
12536                        kvl.v_tok_bytes,
12537                        crate::Engine::kv_fp8_on(),
12538                    )?;
12539                    kvl.len += t;
12540                    let new_len = kvl.len as i32;
12541                    e.set_i32_one(&mut kvl.len_d, new_len)?;
12542                }
12543                let kvl = cache.kv[il].as_ref().unwrap();
12544                // Both segmentation seams restore the FULL pre-fix arithmetic: their legacy
12545                // arm predicate (`t_kv` below, per-call `seq_end` in prime_cache) AND the
12546                // unaligned view offset here. Both halves are load-bearing for the canaries:
12547                // on the FA default the predicate arms agree bitwise wherever they can differ
12548                // (a t_kv<=win view has no maskable key, so windowed==unwindowed FA bit-for-bit).
12549                // That made predicate-only seams INERT under Lever A — first chunkinv35c, then
12550                // tickinv35c. The raw offset restores the live segmentation-variant mechanism
12551                // on the current FA path: its tile grid starts at the chunk/call boundary.
12552                // SWA: trim the view to the oldest key any query in this chunk can reach —
12553                // ALIGNED DOWN to the FA tile size (BK=32). The raw trim is chunk-DEPENDENT
12554                // (off = base_len-(win-1), and base_len is a chunk boundary), and the FA
12555                // kernel's online-softmax recurrence groups keys into BK tiles relative to
12556                // the VIEW START — so an unaligned off regroups the same absolute keys into
12557                // different tiles at different chunk sizes = different (m,l) rounding =
12558                // chunk-dependent bits. chunkinv35 caught exactly this on the first FA-arm
12559                // battery (first_div == chunk size; raw/leverA-gates-20260807T135541Z.log).
12560                // Aligning off to 32 pins tiles to ABSOLUTE key positions for every chunk
12561                // size; the <=31 extra leading keys are older than EVERY query's window
12562                // (all queries sit at >= base_len, so keys < base_len-(win-1) are masked
12563                // for all of them) and a fully-masked key is an exact-0.0 no-op in both
12564                // kernels (NEG_INF -> p=0.0; l+=0.0 and O+=0.0 are bitwise identity), so
12565                // the floor arm's bits do not move either (gated: G2f, battery 2).
12566                let t_kv = base_len + t - off;
12567                let physical = kvl.physical_rows(off, off + t_kv)?;
12568                let k_view = e.view_u8_range(
12569                    &kvl.k,
12570                    physical.start * kvl.k_tok_bytes,
12571                    physical.end * kvl.k_tok_bytes,
12572                );
12573                let v_view = e.view_u8_range(
12574                    &kvl.v,
12575                    physical.start * kvl.v_tok_bytes,
12576                    physical.end * kvl.v_tok_bytes,
12577                );
12578                // CHUNK-INVARIANT PREDICATE: `seq_end` (the request's absolute end position), not
12579                // this chunk's `t_kv`. See the doc note — keying on t_kv made P = c*floor(win/c)
12580                // rows take FA and the output a function of MEMRA_PRIME_CHUNK.
12581                // MEMRA_STEP35_SWA_TKV=1 is the ROLLBACK SEAM to the pre-fix arithmetic — BOTH
12582                // halves: this predicate AND the unaligned view offset above (`legacy_tkv`).
12583                // It is what gives the step35 chunkinv gate its canary teeth: chunk-VARIANT by
12584                // construction, so the invariance assertion MUST break under it (the seam whose
12585                // absence made the original canary inert — GAP 1 in research/step37-p2-20260806;
12586                // and a predicate-only seam went inert AGAIN under the FA default, battery 2's
12587                // CANARY UNEXPECTEDLY MATCHED — see the offset comment). Read per call, not
12588                // cached (probes flip it in-process). Never on in a measured default run.
12589                let swa_naive = if legacy_tkv {
12590                    t_kv > win
12591                } else {
12592                    seq_end > win
12593                };
12594                if swa && swa_naive {
12595                    // Windowed mask needed (see the doc note). DEFAULT since lane/pp-prefill
12596                    // 2026-08-07: the windowed hd128 FA stamp (`fa_prefill_view_ws_w_hd128`) —
12597                    // the anatomy profile measured the f32 floor at 565 ms/layer on a pp4096
12598                    // (41% of the whole prime) while the unwindowed hd128 FA family did the
12599                    // strictly harder causal-4096 in 3.3 ms. NOTE t_kv can be <= win here
12600                    // (a chunk-0 view on a small chunk); the windowed kernel handles that
12601                    // identically to the unwindowed one modulo the mask, which is the point.
12602                    // NEW NUMERIC CLASS vs the floor (bf16-MMA online softmax vs f32 serial),
12603                    // selected on `seq_end` like every arm here, so the class is uniform for
12604                    // the whole request at every MEMRA_PRIME_CHUNK — chunkinv holds by the
12605                    // same construction as the chunkfix. MEMRA_STEP35_SWA_FA=0 = rollback to
12606                    // the f32 floor (the previous numeric config, kept as the A/B seam).
12607                    if std::env::var("MEMRA_STEP35_SWA_FA").as_deref() == Ok("0") {
12608                        e.sdpa_naive_w_quantized_view(
12609                            &q,
12610                            &k_view,
12611                            &v_view,
12612                            &mut attn,
12613                            hd,
12614                            nh,
12615                            nkv,
12616                            t,
12617                            t_kv,
12618                            scale,
12619                            true,
12620                            win,
12621                            kvl.k_tok_bytes,
12622                            kvl.v_tok_bytes,
12623                        )?;
12624                    } else {
12625                        e.fa_prefill_view_ws_w_hd128(
12626                            &q,
12627                            &k_view,
12628                            &v_view,
12629                            &mut attn,
12630                            hd,
12631                            nh,
12632                            nkv,
12633                            t,
12634                            t_kv,
12635                            scale,
12636                            true,
12637                            win,
12638                            kvl.k_tok_bytes,
12639                            kvl.v_tok_bytes,
12640                        )?;
12641                    }
12642                } else if std::env::var("MEMRA_NOFA").is_ok() {
12643                    e.sdpa_naive_quantized_view(
12644                        &q,
12645                        &k_view,
12646                        &v_view,
12647                        &mut attn,
12648                        hd,
12649                        nh,
12650                        nkv,
12651                        t,
12652                        t_kv,
12653                        scale,
12654                        true,
12655                        kvl.k_tok_bytes,
12656                        kvl.v_tok_bytes,
12657                    )?;
12658                } else {
12659                    // seq_end <= win (or a full-attn layer): no query in the WHOLE request can
12660                    // reach past the window, so the window mask is a no-op under causal and every
12661                    // chunk rides the hd128 dequant-once FA prefill — one arm for the whole
12662                    // request either way, which is what makes the chunk size arithmetic-free.
12663                    e.fa_prefill_view_ws(
12664                        &q,
12665                        &k_view,
12666                        &v_view,
12667                        &mut attn,
12668                        hd,
12669                        nh,
12670                        nkv,
12671                        t,
12672                        t_kv,
12673                        scale,
12674                        true,
12675                        kvl.k_tok_bytes,
12676                        kvl.v_tok_bytes,
12677                        crate::Engine::kv_fp8_on(),
12678                    )?;
12679                }
12680            }
12681            None => {
12682                // Cacheless prefill (forward/forward_last/t2probe) is MONOLITHIC — there is no
12683                // chunk loop on this path, so seq_end == t and the predicate is unchanged. The
12684                // assert pins that: if a chunked cacheless prefill is ever added, it must thread
12685                // seq_end here too or it re-opens the same door.
12686                debug_assert_eq!(
12687                    seq_end, t,
12688                    "step35 cacheless prefill is monolithic (seq_end == t)"
12689                );
12690                if swa && seq_end > win {
12691                    e.sdpa_naive_w(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
12692                } else if std::env::var("MEMRA_NOFA").is_ok() {
12693                    e.sdpa_naive(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
12694                } else {
12695                    e.fa_prefill(&q, &k, &v, &mut attn, hd, nh, nkv, t, t, scale, true)?;
12696                }
12697            }
12698        }
12699
12700        // HEAD-WISE GATE (step35.cpp:267-285): gate = attn_gate(cur) -> [t, n_head_l];
12701        // attn *= sigmoid(gate) broadcast over head_dim. BEFORE wo.
12702        let gw = fa
12703            .attn_gate
12704            .as_ref()
12705            .ok_or("step35 layer is missing attn_gate.weight (head-wise attention gate)")?;
12706        let gt_owned = if gt_pre.is_none() {
12707            Some(e.matmul(
12708                gw,
12709                hg.ok_or("step35 attention needs hg when gt_pre is absent")?,
12710                t,
12711            )?)
12712        } else {
12713            None
12714        };
12715        let gt = gt_pre.or(gt_owned.as_ref()).unwrap();
12716        let mut ag = e.uninit(t * nh * hd)?;
12717        e.attn_head_gate(&attn, gt, &mut ag, None, hd, nh, t)?;
12718        Ok(ag)
12719    }
12720
12721    /// step35 PREFILL mixer, no cache side effect (the `full_attn` contract: `forward`,
12722    /// `forward_last`, t2probe). Post-`wo`.
12723    pub(crate) fn step35_attn(
12724        &self,
12725        e: &Engine,
12726        fa: &FullAttnLayer,
12727        h: &CudaSlice<f32>,
12728        pos_d: &CudaSlice<i32>,
12729        t: usize,
12730        il: usize,
12731    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12732        let g3 = e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?;
12733        // Monolithic path: the request ends at t (no chunk loop). See step35_attn_pre_wo's note.
12734        let ag = self.step35_attn_pre_wo(e, fa, g3, Some(h), None, pos_d, t, None, il, t)?;
12735        Ok(e.matmul(&fa.wo, &ag, t)?)
12736    }
12737
12738    /// step35 PRIME mixer (the `full_attn_prime` contract: append this chunk's K/V into the
12739    /// resident quantized cache, attend through the cache view). Post-`wo`.
12740    ///
12741    /// `seq_end` = the ABSOLUTE end position of the whole prime request (chunk-invariant); it
12742    /// selects the SWA arm. See `step35_attn_pre_wo`'s doc note for why it cannot be this chunk's
12743    /// own extent.
12744    #[allow(clippy::too_many_arguments)]
12745    pub(crate) fn step35_attn_prime(
12746        &self,
12747        e: &Engine,
12748        fa: &FullAttnLayer,
12749        h: &CudaSlice<f32>,
12750        hx: Option<&CudaSlice<u8>>,
12751        pos_d: &CudaSlice<i32>,
12752        t: usize,
12753        cache: &mut Cache,
12754        il: usize,
12755        seq_end: usize,
12756    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12757        let g3 = match hx {
12758            Some(xh) => e.matmul_group_xh(&[&fa.wq, &fa.wk, &fa.wv], h, xh, t)?,
12759            None => e.matmul_group(&[&fa.wq, &fa.wk, &fa.wv], h, t)?,
12760        };
12761        let ag =
12762            self.step35_attn_pre_wo(e, fa, g3, Some(h), None, pos_d, t, Some(cache), il, seq_end)?;
12763        Ok(e.matmul(&fa.wo, &ag, t)?)
12764    }
12765
12766    /// step35 T=1 decode attention (post-`wo`, matching `full_attn_decode_pre`'s contract).
12767    /// `pre_q` = the attn-input norm's q8_1 pair when the caller took the norm-fusion lever
12768    /// (then `h` is a zero-length placeholder and EVERY projection here — including the gate —
12769    /// must be on the q8_1 fast path; `mixer_in_q8_1_fast` enforces that for step35 by also
12770    /// requiring `attn_gate`).
12771    ///
12772    /// SWA decode is a token-aligned VIEW OFFSET into the quantized cache (the gemma4 R6
12773    /// pattern): keys carry absolute rope and the mask is purely positional, so the single
12774    /// query at `len-1` attending the last `win` rows IS the windowed result — no mask kernel.
12775    #[allow(clippy::too_many_arguments)]
12776    pub(crate) fn step35_decode_attn(
12777        &self,
12778        e: &Engine,
12779        fa: &FullAttnLayer,
12780        il: usize,
12781        h: &CudaSlice<f32>,
12782        pre_q: Option<(&CudaSlice<i8>, &CudaSlice<f32>)>,
12783        pos_d: &CudaSlice<i32>,
12784        cache: &mut Cache,
12785    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12786        let geometry = self.step35_geom(il);
12787        let hd = geometry.head_dim_k as usize;
12788        let nkv = geometry.n_head_kv as usize;
12789        let nh = geometry.n_head as usize;
12790        let rbase = geometry.rope_base;
12791        let scale = geometry.attention_scale();
12792        let swa = geometry.window.is_some();
12793        let eps = self.cfg.rms_eps;
12794        let win = geometry.window.unwrap_or(0) as usize;
12795        let n_rot = geometry.n_rot as usize;
12796        let n_embd = self.cfg.n_embd as usize;
12797        let gw = fa
12798            .attn_gate
12799            .as_ref()
12800            .ok_or("step35 layer is missing attn_gate.weight (head-wise attention gate)")?;
12801
12802        let (q0, k0, v0, gt) = match pre_q {
12803            Some((hq, hdq)) => {
12804                debug_assert!(
12805                    e.uses_q8_1_fast(gw),
12806                    "step35 pre-quantized decode requires attn_gate on the q8_1 fast path \
12807                     (h is a zero-length placeholder here) — see mixer_in_q8_1_fast"
12808                );
12809                let (a, b, c) = match e.matmul_q8_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hdq)? {
12810                    Some(t3) => t3,
12811                    None => (
12812                        e.matmul_pre(&fa.wq, hq, hdq, h, 1)?,
12813                        e.matmul_pre(&fa.wk, hq, hdq, h, 1)?,
12814                        e.matmul_pre(&fa.wv, hq, hdq, h, 1)?,
12815                    ),
12816                };
12817                let gt = e.matmul_pre(gw, hq, hdq, h, 1)?;
12818                (a, b, c, gt)
12819            }
12820            None => {
12821                if e.uses_q8_1_fast(&fa.wq)
12822                    && e.uses_q8_1_fast(&fa.wk)
12823                    && e.uses_q8_1_fast(&fa.wv)
12824                    && e.uses_q8_1_fast(gw)
12825                {
12826                    let (hq, hdq) = e.quantize_q8_1(h, 1, n_embd)?;
12827                    let (a, b, c) = match e.matmul_q8_fused3(&fa.wq, &fa.wk, &fa.wv, &hq, &hdq)? {
12828                        Some(t3) => t3,
12829                        None => (
12830                            e.matmul_pre(&fa.wq, &hq, &hdq, h, 1)?,
12831                            e.matmul_pre(&fa.wk, &hq, &hdq, h, 1)?,
12832                            e.matmul_pre(&fa.wv, &hq, &hdq, h, 1)?,
12833                        ),
12834                    };
12835                    let gt = e.matmul_pre(gw, &hq, &hdq, h, 1)?;
12836                    (a, b, c, gt)
12837                } else {
12838                    (
12839                        e.matmul(&fa.wq, h, 1)?,
12840                        e.matmul(&fa.wk, h, 1)?,
12841                        e.matmul(&fa.wv, h, 1)?,
12842                        e.matmul(gw, h, 1)?,
12843                    )
12844                }
12845            }
12846        };
12847
12848        let mut q = e.uninit(nh * hd)?;
12849        e.rms_norm(&q0, fa.q_norm.float_data(), &mut q, hd, nh, eps)?;
12850        let mut k = e.uninit(nkv * hd)?;
12851        e.rms_norm(&k0, fa.k_norm.float_data(), &mut k, hd, nkv, eps)?;
12852        let ff = if swa {
12853            None
12854        } else {
12855            self.step35_aux.as_ref().and_then(|a| a.rope_freqs(e))
12856        };
12857        #[cfg(debug_assertions)]
12858        if let Some(ff) = ff {
12859            crate::debug_assert_tensor_stream_device(
12860                ff,
12861                &e.stream(),
12862                "step35_decode_attn.rope_freqs",
12863            );
12864        }
12865        e.rope_neox2(&mut q, &mut k, pos_d, hd, n_rot, nh, nkv, 1, rbase, 1.0, ff)?;
12866
12867        if std::env::var("MEMRA_NOFA").is_ok() {
12868            return Err(
12869                "MEMRA_NOFA (naive f32 SDPA) is incompatible with the quantized KV \
12870                        cache; unset MEMRA_NOFA to use fa_decode"
12871                    .into(),
12872            );
12873        }
12874        let kvl = cache.kv[il].as_mut().unwrap();
12875        let next_len = kvl.len + 1;
12876        let (off, t_kv) = if swa && next_len > win {
12877            (next_len - win, win)
12878        } else {
12879            (0, next_len)
12880        };
12881        let write_row = e.prepare_kv_append(kvl, off & !31usize, 1)?;
12882        e.append_kv_quantized(
12883            &k,
12884            &v0,
12885            &mut kvl.k,
12886            &mut kvl.v,
12887            write_row,
12888            kvl.kv_dim_k,
12889            kvl.kv_dim_v,
12890            kvl.k_tok_bytes,
12891            kvl.v_tok_bytes,
12892            crate::Engine::kv_fp8_on(),
12893        )?;
12894        kvl.len = next_len;
12895        let physical = kvl.physical_rows(off, off + t_kv)?;
12896        let k_view = e.view_u8_range(
12897            &kvl.k,
12898            physical.start * kvl.k_tok_bytes,
12899            physical.end * kvl.k_tok_bytes,
12900        );
12901        let v_view = e.view_u8_range(
12902            &kvl.v,
12903            physical.start * kvl.v_tok_bytes,
12904            physical.end * kvl.v_tok_bytes,
12905        );
12906        let mut attn = e.uninit(nh * hd)?;
12907        e.fa_decode_kvmod(
12908            &q,
12909            &k_view,
12910            &v_view,
12911            &mut attn,
12912            hd,
12913            nh,
12914            nkv,
12915            t_kv,
12916            scale,
12917            kvl.k_tok_bytes,
12918            kvl.v_tok_bytes,
12919            crate::Engine::kv_fp8_on(),
12920        )?;
12921
12922        let mut ag = e.uninit(nh * hd)?;
12923        e.attn_head_gate(&attn, &gt, &mut ag, None, hd, nh, 1)?;
12924        Ok(e.matmul(&fa.wo, &ag, 1)?)
12925    }
12926}
12927
12928// ===================================================================================== //
12929//  gemma-4 E4B (per-layer embeddings + KV-sharing) — FIRST-LIGHT forward.               //
12930//  Dedicated simple path (Stage-B matmuls, per-row causal attention over the quantized  //
12931//  cache) so the tuned 26B/31B paths stay untouched. Arch: research/gemma4-bringup/     //
12932//  e4b-arch-map.md; llama reference: src/models/gemma4.cpp (E4B arms). Wired: forward   //
12933//  (prefill logits), prime (tokenwise-equivalent batched trunk), eager decode_step_h.   //
12934//  NOT wired: dc/graph serving arms, verify/spec, chunked prime (HANDOVER-E4B.md).      //
12935// ===================================================================================== //
12936impl HybridModel {
12937    pub fn is_gemma4_e4b(&self) -> bool {
12938        self.gemma4_aux.as_ref().is_some_and(|a| a.e4b.is_some())
12939    }
12940
12941    /// E4B per-layer geometry: nh/nkv derive from the layer's OWN tensor shapes (the E4B GGUF
12942    /// ships a scalar head_count_kv; swa q 8x256 / kv 2x256, global q 4x512 / kv 1x512).
12943    /// KV-shared layers report the SHARE TARGET's kv count (their wk IS the target's tensor).
12944    fn gemma4_e4b_geom(&self, il: usize) -> (usize, usize, usize, f32, f32, bool) {
12945        let g = self.cfg.gemma4.as_ref().unwrap();
12946        let swa = g.swa_pattern[il];
12947        let hd = if swa {
12948            g.key_length_swa
12949        } else {
12950            g.key_length_global
12951        } as usize;
12952        let Mixer::Full(fa) = &self.layers[il].mixer else {
12953            panic!("e4b layer {il} not full-attn")
12954        };
12955        let nh = fa.wq.out_features() / hd;
12956        let nkv = fa.wk.out_features() / hd;
12957        (
12958            hd,
12959            nkv,
12960            nh,
12961            if swa {
12962                g.rope_base_swa
12963            } else {
12964                g.rope_base_global
12965            },
12966            1.0,
12967            swa,
12968        )
12969    }
12970
12971    /// E4B KV-share target: Some(target) for the trailing shared layers, None = own cache.
12972    fn gemma4_e4b_kv_target(&self, il: usize) -> Option<usize> {
12973        self.layers[il]
12974            .gemma4
12975            .as_ref()
12976            .and_then(|b| b.e4b.as_ref())
12977            .and_then(|e4| e4.kv_share.map(|t| t as usize))
12978    }
12979
12980    /// E4B prologue: inp_pl [t][n_layer][n_epl] =
12981    ///   ( gather(per_layer_tok_embd, tok)*sqrt(n_epl)
12982    ///   + rms_norm(model_proj . x_scaled * 1/sqrt(n_embd), proj_norm) ) * 1/sqrt(2)
12983    /// (llama gemma4.cpp build_inp_per_layer + project_per_layer_inputs, exact order).
12984    fn gemma4_e4b_inp_pl(
12985        &self,
12986        e: &Engine,
12987        tokens: &[u32],
12988        x_scaled: &CudaSlice<f32>,
12989        t: usize,
12990    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
12991        let tok_d = e.stream().clone_htod(&tokens.to_vec())?;
12992        self.gemma4_e4b_inp_pl_dev(e, &tok_d, x_scaled, t)
12993    }
12994
12995    /// Device-token prologue core (dc arm shares it: token ids never touch the host).
12996    fn gemma4_e4b_inp_pl_dev(
12997        &self,
12998        e: &Engine,
12999        tok_d: &CudaSlice<u32>,
13000        x_scaled: &CudaSlice<f32>,
13001        t: usize,
13002    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
13003        let aux = self.gemma4_aux.as_ref().unwrap();
13004        let m = aux.e4b.as_ref().unwrap();
13005        let n_embd = self.cfg.n_embd as usize;
13006        let n_layer = self.layers.len();
13007        let width = m.n_epl * n_layer;
13008        let tbl = m.tok_tbl_gpu.get_or_init(|| {
13009            e.upload_u8(&m.tok_embd_bytes)
13010                .expect("e4b per-layer token table upload")
13011        });
13012        let mut a =
13013            e.embed_gather_device_td(tbl, tok_d, t, width, m.tok_embd_qt, m.tok_embd_row_bytes)?;
13014        e.scale_inplace(&mut a, (m.n_epl as f32).sqrt(), t * width)?;
13015        let mut p = e.matmul(&m.model_proj, x_scaled, t)?;
13016        e.scale_inplace(&mut p, 1.0 / (n_embd as f32).sqrt(), t * width)?;
13017        let mut pn = e.uninit(t * width)?;
13018        e.rms_norm(
13019            &p,
13020            m.proj_norm.float_data(),
13021            &mut pn,
13022            m.n_epl,
13023            t * n_layer,
13024            self.cfg.rms_eps,
13025        )?;
13026        let mut out = e.uninit(t * width)?;
13027        e.add_scale(&a, &pn, 1.0 / 2f32.sqrt(), &mut out, t * width)?;
13028        Ok(out)
13029    }
13030
13031    /// E4B attention (t-wide, causal, per-row fa over the QUANTIZED cache — first-light
13032    /// correctness path; the fa rows arms come later). Own-KV layers project+norm+rope k/v
13033    /// and append t rows; KV-shared layers are Q-only over the target layer's cache (which
13034    /// already holds this forward's rows — the target runs earlier in the stack).
13035    #[allow(clippy::too_many_arguments)]
13036    fn gemma4_e4b_attn(
13037        &self,
13038        e: &Engine,
13039        il: usize,
13040        hq: &CudaSlice<i8>,
13041        hdq: &CudaSlice<f32>,
13042        pos_d: &CudaSlice<i32>,
13043        t: usize,
13044        cache: &mut Cache,
13045        dc_bucket: Option<usize>,
13046    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
13047        let (hd, nkv, nh, base, scale, swa) = self.gemma4_e4b_geom(il);
13048        let eps = self.cfg.rms_eps;
13049        let aux = self.gemma4_aux.as_ref().unwrap();
13050        let ones = aux.ones(e);
13051        #[cfg(debug_assertions)]
13052        crate::debug_assert_tensor_stream_device(ones, &e.stream(), "gemma4_e4b_attn.ones");
13053        let Mixer::Full(fa) = &self.layers[il].mixer else {
13054            unreachable!()
13055        };
13056        // pre-quantized layer input (E4B fusion port, 2026-07-12): ONE norm+quant feeds
13057        // wq/wk/wv via matmul_pre — the first-light path quantized the same h three times
13058        // (the profile's 342 quantize_q8_1/token; glue = 26% of the 6.1ms token).
13059        let h0 = e.zeros(0)?;
13060        let h = &h0;
13061
13062        let ff = if swa {
13063            None
13064        } else {
13065            Some(
13066                aux.rope_freqs(e)
13067                    .expect("e4b global rope needs rope_freqs.weight"),
13068            )
13069        };
13070        #[cfg(debug_assertions)]
13071        if let Some(ff) = ff {
13072            crate::debug_assert_tensor_stream_device(ff, &e.stream(), "gemma4_e4b_attn.rope_freqs");
13073        }
13074        let share = self.gemma4_e4b_kv_target(il);
13075        // Own-KV arms keep the f32 (k, v) alive for the prime fa arm below.
13076        let mut kv_f32: Option<(CudaSlice<f32>, CudaSlice<f32>)> = None;
13077        let mut q;
13078        if let Some(_tgt) = share {
13079            let q0 = e.matmul_pre(&fa.wq, hq, hdq, h, t)?;
13080            q = e.uninit(t * nh * hd)?;
13081            // Q-only through the same fused norm+rope kernel (rk = 0: the k/v segments are
13082            // empty; q0 stands in for the unused k/v pointers).
13083            let mut kdummy = e.uninit(1)?;
13084            let mut vdummy = e.uninit(1)?;
13085            e.rms_norm_qkv_rope(
13086                &q0,
13087                &q0,
13088                &q0,
13089                fa.q_norm.float_data(),
13090                fa.q_norm.float_data(),
13091                ones,
13092                &mut q,
13093                &mut kdummy,
13094                &mut vdummy,
13095                hd,
13096                nh * t,
13097                0,
13098                pos_d,
13099                nh,
13100                1,
13101                base,
13102                1.0,
13103                ff,
13104                eps,
13105            )?;
13106        } else {
13107            // wave-4b: ONE concat matvec (wq|wk|wv) at t == 1 when the cat tensor exists;
13108            // else the fused3 grid launch; else per-matvec. The cat output is contiguous
13109            // q|k|v rows — the cat norm+rope twin consumes it directly.
13110            let e4bits = self.layers[il].gemma4.as_ref().and_then(|g| g.e4b.as_ref());
13111            let cat = e4bits.and_then(|e4| e4.qkv_cat.as_ref());
13112            q = e.uninit(t * nh * hd)?;
13113            let mut k = e.uninit(t * nkv * hd)?;
13114            let mut v = e.uninit(t * nkv * hd)?;
13115            if t == 1 && cat.is_some() {
13116                let qkv0 = e.matmul_pre(cat.unwrap(), hq, hdq, h, 1)?;
13117                e.rms_norm_qkv_rope_cat(
13118                    &qkv0,
13119                    fa.q_norm.float_data(),
13120                    fa.k_norm.float_data(),
13121                    ones,
13122                    &mut q,
13123                    &mut k,
13124                    &mut v,
13125                    hd,
13126                    nh,
13127                    nkv,
13128                    pos_d,
13129                    nh,
13130                    nkv,
13131                    base,
13132                    1.0,
13133                    ff,
13134                    eps,
13135                )?;
13136            } else {
13137                let (q0, k0, v0) = match if t == 1 {
13138                    e.matmul_q4_fused3(&fa.wq, &fa.wk, &fa.wv, hq, hdq)?
13139                } else {
13140                    // E4B verify f3 port (2026-07-14): the 31B segmented-grid batched qkv
13141                    // on E4B's real-V triple — same MEMRA_F2B seam, bit-identical per row.
13142                    static F2B_QKV: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
13143                    if *F2B_QKV.get_or_init(|| std::env::var("MEMRA_F2B").as_deref() != Ok("0")) {
13144                        e.matmul_q4_fused3_batched(&fa.wq, &fa.wk, &fa.wv, hq, hdq, t)?
13145                    } else {
13146                        None
13147                    }
13148                } {
13149                    Some(triple) => triple,
13150                    None => (
13151                        e.matmul_pre(&fa.wq, hq, hdq, h, t)?,
13152                        e.matmul_pre(&fa.wk, hq, hdq, h, t)?,
13153                        e.matmul_pre(&fa.wv, hq, hdq, h, t)?,
13154                    ), // E4B: real v (K != V)
13155                };
13156                // wave-3 fold: q/k/v norms + q/k rope in ONE launch (rope math verbatim on
13157                // the normed rows; V ones-rms, never roped).
13158                e.rms_norm_qkv_rope(
13159                    &q0,
13160                    &k0,
13161                    &v0,
13162                    fa.q_norm.float_data(),
13163                    fa.k_norm.float_data(),
13164                    ones,
13165                    &mut q,
13166                    &mut k,
13167                    &mut v,
13168                    hd,
13169                    nh * t,
13170                    nkv * t,
13171                    pos_d,
13172                    nh,
13173                    nkv,
13174                    base,
13175                    1.0,
13176                    ff,
13177                    eps,
13178                )?;
13179            }
13180            let kvl = cache.kv[il].as_mut().unwrap();
13181            // class flag must match the cache dims (the g-threading sweep hardcoded `false`
13182            // here and the wkv default corrupted E4B: q8_0 bytes into an e4m3 cache — the
13183            // degenerate tok-0 stream, 2026-07-12).
13184            let cls = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
13185            if dc_bucket.is_some() {
13186                // DC arm (graph serving): append at the len_d slot, advance the counter
13187                // in-stream — replay-correct, no host len in the launch args. Host mirrors
13188                // are NOT touched here (the replay loop owns them; a bump at capture-record
13189                // time would double-count the capture iteration).
13190                debug_assert!(t == 1);
13191                // wave 5c: append + len_d inc fused (one launch; single-block ordering).
13192                e.append_kv_quantized_row_dc_inc(
13193                    &k,
13194                    &v,
13195                    &mut kvl.k,
13196                    &mut kvl.v,
13197                    &mut kvl.len_d,
13198                    kvl.kv_dim_k,
13199                    kvl.kv_dim_v,
13200                    kvl.k_tok_bytes,
13201                    kvl.v_tok_bytes,
13202                    cls,
13203                )?;
13204            } else {
13205                e.append_kv_quantized_rows(
13206                    &k,
13207                    &v,
13208                    &mut kvl.k,
13209                    &mut kvl.v,
13210                    kvl.len,
13211                    t,
13212                    kvl.kv_dim_k,
13213                    kvl.kv_dim_v,
13214                    kvl.k_tok_bytes,
13215                    kvl.v_tok_bytes,
13216                    cls,
13217                )?;
13218                kvl.len += t;
13219            }
13220            kv_f32 = Some((k, v));
13221        }
13222        // attention: per-row causal fa over the (own or target) quantized cache. The cache
13223        // already contains this forward's rows in both arms; row i attends [.., base+i].
13224        let kvl_idx = share.unwrap_or(il);
13225        let kvl = cache.kv[kvl_idx].as_ref().unwrap();
13226        let base_len = kvl.len - t; // pre-append length (target appended this forward too)
13227        let win = self.cfg.gemma4.as_ref().unwrap().sliding_window as usize;
13228        let mut attn = e.uninit(t * nh * hd)?;
13229        // PRIME FA ARMS (perf lane 3, completed 2026-07-31 — the H100 board pinned E4B
13230        // prefill at 482 tok/s: the per-row loop ran the DECODE kernel once per token per
13231        // layer). Fresh-prompt (base_len == 0) batched attention, one launch per layer:
13232        //   - own-KV hd256 under the window: f32 fa_prefill (full causal exact — 26B pattern)
13233        //   - own-KV hd256 above the window (swa): fa_prefill_w windowed twin (12B pattern)
13234        //   - own-KV hd512 globals: fa_prefill_hd512 (12B/31B globals pattern)
13235        //   - KV-shared layers (no f32 k/v): fa_prefill_view over the target's QUANTIZED
13236        //     rows (the T=K verify kernel; the target appended this forward's rows already).
13237        //     swa-shared above the window keeps the per-row loop (no windowed view twin).
13238        // Same numeric class split as the 26B prime (f32/batched prefill vs per-row
13239        // quantized decode); run-gen argmax + chat gates arbitrate. MEMRA_NOFA=1 reverts all.
13240        if t > 1 && base_len == 0 && std::env::var("MEMRA_NOFA").is_err() {
13241            if let Some((kf, vf)) = &kv_f32 {
13242                if hd == 256 && t <= win {
13243                    e.fa_prefill(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale, true)?;
13244                    return Ok(e.matmul(&fa.wo, &attn, t)?);
13245                }
13246                if hd == 256 && swa && t > win {
13247                    e.fa_prefill_w(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
13248                    return Ok(e.matmul(&fa.wo, &attn, t)?);
13249                }
13250                if hd == 512 && !swa {
13251                    e.fa_prefill_hd512(&q, kf, vf, &mut attn, hd, nh, nkv, t, t, scale, true)?;
13252                    return Ok(e.matmul(&fa.wo, &attn, t)?);
13253                }
13254            } else if share.is_some() {
13255                let g = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
13256                let k_view = e.view_u8(&kvl.k, kvl.k.len());
13257                let v_view = e.view_u8(&kvl.v, kvl.v.len());
13258                if hd == 256 && (!swa || t <= win) {
13259                    // inline-dequant quantized-view prefill (fa_prefill_q stamps 256/128)
13260                    e.fa_prefill_view(
13261                        &q,
13262                        &k_view,
13263                        &v_view,
13264                        &mut attn,
13265                        hd,
13266                        nh,
13267                        nkv,
13268                        t,
13269                        t,
13270                        scale,
13271                        true,
13272                        kvl.k_tok_bytes,
13273                        kvl.v_tok_bytes,
13274                        g,
13275                    )?;
13276                    return Ok(e.matmul(&fa.wo, &attn, t)?);
13277                }
13278                // remaining shared classes (swa above the window; hd512 globals): dequant
13279                // the target's t rows ONCE to f32, then the same f32 twins as own-KV.
13280                let kv_dim = nkv * hd;
13281                let mut kf = e.uninit(t * kv_dim)?;
13282                let mut vf = e.uninit(t * kv_dim)?;
13283                e.fa_dequant_kv_view_f32(
13284                    &k_view,
13285                    &v_view,
13286                    &mut kf,
13287                    &mut vf,
13288                    kv_dim,
13289                    kv_dim,
13290                    t,
13291                    kvl.k_tok_bytes,
13292                    kvl.v_tok_bytes,
13293                    g,
13294                )?;
13295                if hd == 512 {
13296                    e.fa_prefill_hd512(&q, &kf, &vf, &mut attn, hd, nh, nkv, t, t, scale, true)?;
13297                } else {
13298                    e.fa_prefill_w(&q, &kf, &vf, &mut attn, hd, nh, nkv, t, t, scale, true, win)?;
13299                }
13300                return Ok(e.matmul(&fa.wo, &attn, t)?);
13301            }
13302        }
13303        if let Some(bucket) = dc_bucket {
13304            // DC arm (t == 1, under-window regime — the generate gate enforces it): ONE
13305            // fa_decode_dc over the live counter. len_d already advanced past this token
13306            // (t_kv = len_d[0], the cache.rs contract). KV-shared layers read the target's
13307            // counter (advanced when the target ran earlier in the stack).
13308            assert!(t == 1);
13309            // hd512 globals: eager (kvmod) picks the SCALAR unified below the fa512 floor,
13310            // and under the window every live t_kv sits below it — cap the capture bucket
13311            // under the floor so fa_decode_dc bakes the same scalar symbol, or the graph
13312            // rides the dpl16 twin and its numeric class diverges from the dc-eager stream
13313            // (E4B-GRAPH-GATE 2/64, 2026-07-12).
13314            let bucket = if hd == 512 && win <= crate::fa512_min_tkv() {
13315                bucket.min(crate::fa512_min_tkv().saturating_sub(1))
13316            } else {
13317                bucket
13318            };
13319            let k_view = e.view_u8(&kvl.k, kvl.k.len());
13320            let v_view = e.view_u8(&kvl.v, kvl.v.len());
13321            let g = (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on());
13322            // Weight prefetch (SOTA item 3, 2026-07-13): wo's decode plane prefetched into
13323            // L2 across the fa window — fa reads KV only, the weight DRAM lanes are idle
13324            // there (E4B valid window +0.65%: 196.8 vs 195.6). Value-free scheduling op,
13325            // captured into the dc graph like any other launch. Extending the cascade to
13326            // the ffn gate/up planes measured NEGATIVE (193.9 vs 195.8 — 29MB/layer floods
13327            // the fill path and evicts still-hot lines); wo-only is the shipped shape.
13328            // MEMRA_WPF=0 rollback seam.
13329            if crate::Engine::wpf_level() >= 1 {
13330                e.prefetch_weight_l2(&fa.wo)?;
13331            }
13332            // wave 5b: the combine emits the wo input q8 pair directly — the standalone
13333            // quantize launch + the f32 attn round-trip fold away (t=1 fast path only).
13334            if e.uses_q8_1_fast(&fa.wo) {
13335                let mut oq = e.alloc_i8_uninit(nh * hd)?;
13336                let mut od = e.zeros(nh * hd / 32)?;
13337                e.fa_decode_dc_q8(
13338                    &q,
13339                    &k_view,
13340                    &v_view,
13341                    &mut attn,
13342                    hd,
13343                    nh,
13344                    nkv,
13345                    &kvl.len_d,
13346                    bucket,
13347                    scale,
13348                    kvl.k_tok_bytes,
13349                    kvl.v_tok_bytes,
13350                    g,
13351                    Some((&mut oq, &mut od)),
13352                )?;
13353                return Ok(e.matmul_pre(&fa.wo, &oq, &od, &attn, t)?);
13354            }
13355            e.fa_decode_dc(
13356                &q,
13357                &k_view,
13358                &v_view,
13359                &mut attn,
13360                hd,
13361                nh,
13362                nkv,
13363                &kvl.len_d,
13364                bucket,
13365                scale,
13366                kvl.k_tok_bytes,
13367                kvl.v_tok_bytes,
13368                g,
13369            )?;
13370            return Ok(e.matmul(&fa.wo, &attn, t)?);
13371        }
13372        for i in 0..t {
13373            let avail = base_len + i + 1;
13374            let (off_tok, t_kv) = if swa && avail > win {
13375                (avail - win, win)
13376            } else {
13377                (0, avail)
13378            };
13379            let k_view = e.view_u8_range(
13380                &kvl.k,
13381                off_tok * kvl.k_tok_bytes,
13382                (off_tok + t_kv) * kvl.k_tok_bytes,
13383            );
13384            let v_view = e.view_u8_range(
13385                &kvl.v,
13386                off_tok * kvl.v_tok_bytes,
13387                (off_tok + t_kv) * kvl.v_tok_bytes,
13388            );
13389            let qv = e.view(&q, t * nh * hd);
13390            let q_row = qv.slice(i * nh * hd..(i + 1) * nh * hd);
13391            let mut q_one = e.uninit(nh * hd)?;
13392            e.copy_view_into(&mut q_one, 0, &q_row, nh * hd)?;
13393            let mut a_one = e.uninit(nh * hd)?;
13394            // read class MUST match the append class (globals are e4m3 under gkv): the
13395            // swa-only flag decoded global-layer e4m3 bytes as q8_0/q5_1 — attention over
13396            // garbage on EVERY E4B global layer, the cross-mode maxdiff-30 root (2026-07-12).
13397            e.fa_decode_kvmod(
13398                &q_one,
13399                &k_view,
13400                &v_view,
13401                &mut a_one,
13402                hd,
13403                nh,
13404                nkv,
13405                t_kv,
13406                scale,
13407                kvl.k_tok_bytes,
13408                kvl.v_tok_bytes,
13409                (!swa && crate::Engine::gkv_on()) || (swa && crate::Engine::wkv_on()),
13410            )?;
13411            e.copy_into(&mut attn, i * nh * hd, &a_one, nh * hd)?;
13412        }
13413        Ok(e.matmul(&fa.wo, &attn, t)?)
13414    }
13415
13416    /// E4B trunk: embed -> prologue -> layers (attn + dense ffn via the 31B tail_core + the
13417    /// per-layer-embedding tail + layer scale) -> output_norm. Returns (softcapped logits
13418    /// device [t, n_vocab], pre-output_norm hidden [t, n_embd]). Appends t rows per own-KV
13419    /// layer; does NOT advance cache.pos (caller owns pos).
13420    fn gemma4_e4b_trunk(
13421        &self,
13422        e: &Engine,
13423        tokens: &[u32],
13424        pos0: usize,
13425        cache: &mut Cache,
13426        head_last: bool,
13427    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13428        let n_embd = self.cfg.n_embd as usize;
13429        let t = tokens.len();
13430        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
13431        let pos_d = e.htod_i32(&pos)?;
13432        let mut x = e.htod(&self.embd.gather(n_embd, tokens))?;
13433        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
13434        let inp_pl = self.gemma4_e4b_inp_pl(e, tokens, &x, t)?;
13435        self.gemma4_e4b_trunk_core(e, x, inp_pl, &pos_d, t, cache, None, true, head_last)
13436    }
13437
13438    /// Layer stack + head over prebuilt (x_scaled, inp_pl, device pos) — everything below
13439    /// here is device-driven, so the dc arm shares it verbatim (stream identity with the
13440    /// eager chain by construction: SAME functions, not twins).
13441    fn gemma4_e4b_trunk_core(
13442        &self,
13443        e: &Engine,
13444        x_in: CudaSlice<f32>,
13445        inp_pl: CudaSlice<f32>,
13446        pos_d: &CudaSlice<i32>,
13447        t: usize,
13448        cache: &mut Cache,
13449        dc_bucket: Option<usize>,
13450        cap_logits: bool,
13451        head_last: bool,
13452    ) -> Result<(CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13453        let n_embd = self.cfg.n_embd as usize;
13454        let eps = self.cfg.rms_eps;
13455        let n_layer = self.layers.len();
13456        let mut x = x_in;
13457        let aux_e4b = self.gemma4_aux.as_ref().unwrap().e4b.as_ref().unwrap();
13458        let n_epl = aux_e4b.n_epl;
13459
13460        // cross-layer fusion (2026-07-12 port of the 26B/31B trunk structure): each layer's
13461        // closing add_scale also EMITS the next layer's attn-normed input pre-quantized
13462        // q8_1 (add_scale_rms_norm_q8_1); the LAST layer emits through output_norm so the
13463        // head rides matmul_pre too. First layer's pair comes from a standalone fused
13464        // norm+quant.
13465        let mut h_carry: Option<(CudaSlice<i8>, CudaSlice<f32>)> = None;
13466        for il in 0..n_layer {
13467            let layer = &self.layers[il];
13468            let (hq, hdq) = match h_carry.take() {
13469                Some(p) => p,
13470                None => e.rms_norm_q8_1(&x, layer.attn_norm.float_data(), n_embd, t, eps)?,
13471            };
13472            let o = self.gemma4_e4b_attn(e, il, &hq, &hdq, pos_d, t, cache, dc_bucket)?;
13473            // dense ffn tail with the post-attn norm FOLDED into its entry (one launch for
13474            // rms(o, post_attn_norm) + residual add + ffn_norm — glue-fusion lane).
13475            let bits = layer.gemma4.as_ref().unwrap();
13476            let e4b = bits.e4b.as_ref().expect("e4b layer bits");
13477            // glue wave 5: the tail DEFERS its post_ffw norm — the FFN exit fuses
13478            // rms(f0, post_ffw) + residual add + q8 emit into ONE launch (rms_pre_add_q8_1),
13479            // killing the rms_norm + add_q8_1 pair per layer. T-GENERIC since 2026-07-13:
13480            // the fused single-phase reduction is NOT FP-order-identical to the unfused
13481            // rms_norm+add pair (the original "bit-identical chain" claim was FALSE — the
13482            // t==1 gate left the batched VERIFY on the unfused chain and split E4B verify
13483            // from decode by logit maxdiff ~0.45, the greedy tie-flip at depth: gate 135/256.
13484            // NOFUSE bisect: disabling ONLY this fusion -> verify maxdiff 0.000e0). With the
13485            // gate dropped, decode AND verify ride the same fused chain — parity by
13486            // construction, VERIFY-GATE 0.000e0.
13487            let fuse_exit = e.uses_q8_1_fast(&e4b.inp_gate);
13488            let (sn, attn_out) = self.gemma4_layer_tail_core_pn(
13489                e,
13490                layer,
13491                &o,
13492                &x,
13493                t,
13494                Some(layer.post_attn_norm.float_data()),
13495                fuse_exit,
13496            )?;
13497            let mut resid = e.uninit(t * n_embd)?;
13498            // per-layer-embedding tail: resid += rms(proj . (gelu(inp_gate . resid) * inp_pl[il]))
13499            // wave-2: the residual add emits q8_1 alongside — inp_gate rides matmul_pre.
13500            // (PLE one-block mega-fusion PROBED NEGATIVE 2026-07-13: argmax-correct but
13501            // 126 vs 189 tok/s — one SM pulling 0.74MB of weights loses to the multi-block
13502            // launch chain it replaced; jsonl row. Kernel deleted per doctrine.)
13503            let g = if fuse_exit {
13504                // sn here = RAW f0 (post_ffw deferred).
13505                let (rq, rd) = e.rms_pre_add_q8_1(
13506                    &sn,
13507                    bits.post_ffw_norm.float_data(),
13508                    &attn_out,
13509                    &mut resid,
13510                    n_embd,
13511                    t,
13512                    self.cfg.rms_eps,
13513                )?;
13514                e.matmul_pre(&e4b.inp_gate, &rq, &rd, &resid, t)?
13515            } else {
13516                e.add(&sn, &attn_out, &mut resid, t * n_embd)?;
13517                e.matmul(&e4b.inp_gate, &resid, t)?
13518            };
13519            let mut act = e.uninit(t * n_epl)?;
13520            let y = if t == 1 && e.uses_q8_1_fast(&e4b.proj) {
13521                let ipv = e.view(&inp_pl, n_epl * n_layer);
13522                let row = ipv.slice(il * n_epl..(il + 1) * n_epl);
13523                let (aq, ad) = e.gelu_tanh_mul_q8_1(&g, &row, &mut act, n_epl, 1)?;
13524                e.matmul_pre(&e4b.proj, &aq, &ad, &act, t)?
13525            } else {
13526                let mut inp_this = e.uninit(t * n_epl)?;
13527                e.copy_rows_strided(
13528                    &inp_pl,
13529                    &mut inp_this,
13530                    n_epl,
13531                    t,
13532                    n_epl * n_layer,
13533                    il * n_epl,
13534                )?;
13535                e.gelu_tanh_mul(&g, &inp_this, &mut act, t * n_epl)?;
13536                e.matmul(&e4b.proj, &act, t)?
13537            };
13538            // rms(y, post_norm) + (yn + resid)*layer_scale + next-layer norm+quant emit,
13539            // ONE launch (glue-fusion lane; last layer emits through output_norm).
13540            let next_norm = if il + 1 < n_layer {
13541                self.layers[il + 1].attn_norm.float_data()
13542            } else {
13543                self.output_norm.float_data()
13544            };
13545            let mut xn = e.uninit(t * n_embd)?;
13546            let pair = e.rms_pre_add_scale_rms_norm_q8_1(
13547                &y,
13548                e4b.post_norm.float_data(),
13549                &resid,
13550                bits.layer_scale,
13551                next_norm,
13552                &mut xn,
13553                n_embd,
13554                t,
13555                eps,
13556            )?;
13557            h_carry = Some(pair);
13558            x = xn;
13559        }
13560        // the head consumes the last layer's fused (output_norm) emit. head_last callers
13561        // (prime, last_only forward) need only the final row's logits — the all-T head is
13562        // t*n_vocab of discarded work (E4B prime: ~134ms GEMM + a 2.26GB dtoh kept 1 row).
13563        let (oq, odq) = h_carry.take().unwrap();
13564        let h0 = e.zeros(0)?;
13565        let hm = if head_last { 1 } else { t };
13566        let (hq, hd) = if head_last && t > 1 {
13567            let mut q1 = e.uninit_i8(n_embd)?;
13568            e.dtod_copy_view_i8(&oq.slice((t - 1) * n_embd..t * n_embd), &mut q1)?;
13569            let nb = n_embd / 32;
13570            let mut d1 = e.uninit(nb)?;
13571            e.dtod_copy_view(&odq.slice((t - 1) * nb..t * nb), &mut d1)?;
13572            (q1, d1)
13573        } else {
13574            (oq, odq)
13575        };
13576        let mut ld = e.matmul_pre(&self.output, &hq, &hd, &h0, hm)?;
13577        // softcap is strictly monotonic — greedy (argmax-only) consumers skip it, matching
13578        // the 26B/31B dc precedent (their dc head goes matmul -> argmax with no cap).
13579        // Logit-returning callers (host logits / spec prime) keep the capped emit.
13580        if cap_logits {
13581            let cap = self.cfg.gemma4.as_ref().unwrap().final_logit_softcapping;
13582            e.softcap(&mut ld, cap, hm * self.output.out_features())?;
13583        }
13584        self.gemma4_suppress(e, &mut ld, hm)?; // mask both capped and argmax-only consumers
13585        Ok((ld, x))
13586    }
13587
13588    /// E4B batched VERIFY (device tokens, the spec round's t=K+1 step): t rows through the
13589    /// e4b trunk (per-row causal attention; own-KV layers append t rows host-len, KV-shared
13590    /// layers ride their targets), per-row device argmax + the POST-output_norm hidden
13591    /// stack (the drafter's h convention). Advances cache.pos/kvl.len by t — the spec
13592    /// round rolls back rejected rows (shared layers have no KvLayer, so the plain rewind
13593    /// covers exactly the layers that appended).
13594    pub fn gemma4_e4b_decode_step_t_am_dev(
13595        &self,
13596        e: &Engine,
13597        tok_d: &CudaSlice<u32>,
13598        t: usize,
13599        pos0: usize,
13600        cache: &mut Cache,
13601    ) -> Result<(CudaSlice<u32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13602        let n_embd = self.cfg.n_embd as usize;
13603        let eps = self.cfg.rms_eps;
13604        let pos: Vec<i32> = (0..t).map(|i| (pos0 + i) as i32).collect();
13605        let pos_d = e.htod_i32(&pos)?;
13606        let embd_gpu = self
13607            .embd_gpu
13608            .get_or_init(|| e.upload_u8(&self.embd.raw).expect("embed table upload"));
13609        let (qt, rb) = self.embd.qt_and_row_bytes(n_embd);
13610        let mut x = e.embed_gather_device_td(embd_gpu, tok_d, t, n_embd, qt, rb)?;
13611        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), t * n_embd)?;
13612        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, tok_d, &x, t)?;
13613        let (ld, xp) =
13614            self.gemma4_e4b_trunk_core(e, x, inp_pl, &pos_d, t, cache, None, true, false)?;
13615        // softcap is monotonic — the per-row argmax is invariant to it (the trunk's head
13616        // emit is already capped, matching the eager chain bit-for-bit).
13617        let n_vocab = self.output.out_features();
13618        let mut vam = e.stream().alloc_zeros::<u32>(t)?;
13619        for i in 0..t {
13620            e.argmax_token_device_col(&ld, i, n_vocab, &mut vam, i)?;
13621        }
13622        let mut hn = e.uninit(t * n_embd)?;
13623        e.rms_norm(&xp, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
13624        cache.pos += t;
13625        Ok((vam, hn))
13626    }
13627
13628    /// E4B verify + host logits + POST-output_norm hidden stack (the short-prompt spec
13629    /// prime path — mirror of `gemma4_decode_step_t_h`).
13630    pub(crate) fn gemma4_e4b_decode_step_t_h(
13631        &self,
13632        e: &Engine,
13633        tokens: &[u32],
13634        pos0: usize,
13635        cache: &mut Cache,
13636    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13637        let n_embd = self.cfg.n_embd as usize;
13638        let eps = self.cfg.rms_eps;
13639        let t = tokens.len();
13640        let (ld, xp) = self.gemma4_e4b_trunk(e, tokens, pos0, cache, false)?;
13641        let mut hn = e.uninit(t * n_embd)?;
13642        e.rms_norm(&xp, self.output_norm.float_data(), &mut hn, n_embd, t, eps)?;
13643        cache.pos += t;
13644        Ok((e.dtoh(&ld)?, hn))
13645    }
13646
13647    /// E4B GRAPH-CAPTURABLE dc step: same trunk as the dc step but token_d is updated IN
13648    /// PLACE (self-feeding replay) and every launch arg is a device counter — pos from
13649    /// pos_d (inc'd in-stream), KV slots from len_d (advanced in-stream), attention from
13650    /// fa_decode_dc at `bucket`. Host mirrors (cache.pos / kvl.len) advance in the caller's
13651    /// replay loop. UNDER-WINDOW regime only (the caller gates pos + budget < window).
13652    pub fn gemma4_e4b_decode_step_dcg(
13653        &self,
13654        e: &Engine,
13655        token_d: &mut CudaSlice<u32>,
13656        pos_d: &mut CudaSlice<i32>,
13657        embd_gpu: &CudaSlice<u8>,
13658        embd_qt: i32,
13659        embd_rb: usize,
13660        cache: &mut Cache,
13661        n_vocab: usize,
13662        bucket: usize,
13663    ) -> Result<(), Box<dyn std::error::Error>> {
13664        let n_embd = self.cfg.n_embd as usize;
13665        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
13666        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
13667        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, token_d, &x, 1)?;
13668        let (ld, _x) =
13669            self.gemma4_e4b_trunk_core(e, x, inp_pl, pos_d, 1, cache, Some(bucket), false, false)?;
13670        e.argmax_token_device_into(&ld, token_d, n_vocab)?;
13671        e.inc_seqlen(pos_d)?;
13672        Ok(())
13673    }
13674
13675    /// E4B DEVICE-COUNTER decode step (the dc serving arm): token id rides `token_d`, the
13676    /// greedy argmax lands in the returned device buffer — 4B/token host traffic. The layer
13677    /// stack is gemma4_e4b_trunk_core, i.e. the SAME functions the eager chain runs (stream
13678    /// identity by construction, not by twin-kernel parity). Host KV mirrors advance like the
13679    /// 26B dc-eager arm (window views are host math); len_d stays synced by the caller's
13680    /// entry sync + the appends here don't read it. Graph capture is NOT wired (no
13681    /// cap_bucket_max) — the E4B graph arc comes after the perf gates.
13682    #[allow(clippy::too_many_arguments)]
13683    pub fn gemma4_e4b_decode_step_dc(
13684        &self,
13685        e: &Engine,
13686        token_d: &CudaSlice<u32>,
13687        pos_d: &mut CudaSlice<i32>,
13688        embd_gpu: &CudaSlice<u8>,
13689        embd_qt: i32,
13690        embd_rb: usize,
13691        cache: &mut Cache,
13692        n_vocab: usize,
13693    ) -> Result<CudaSlice<u32>, Box<dyn std::error::Error>> {
13694        let n_embd = self.cfg.n_embd as usize;
13695        let eps = self.cfg.rms_eps;
13696        let mut x = e.embed_gather_device(embd_gpu, token_d, n_embd, embd_qt, embd_rb)?;
13697        e.scale_inplace(&mut x, (n_embd as f32).sqrt(), n_embd)?;
13698        let inp_pl = self.gemma4_e4b_inp_pl_dev(e, token_d, &x, 1)?;
13699        let (ld, _x) =
13700            self.gemma4_e4b_trunk_core(e, x, inp_pl, pos_d, 1, cache, None, false, false)?;
13701        let mut tok_out = e.stream().alloc_zeros::<u32>(1)?;
13702        e.argmax_token_device_into(&ld, &mut tok_out, n_vocab)?;
13703        e.inc_seqlen(pos_d)?;
13704        cache.pos += 1;
13705        let _ = eps;
13706        Ok(tok_out)
13707    }
13708
13709    /// E4B eager T=1 decode step (decode_step_h contract): returns (softcapped logits host,
13710    /// pre-output_norm hidden). Advances cache.pos.
13711    pub(crate) fn gemma4_e4b_decode_step_h(
13712        &self,
13713        e: &Engine,
13714        token: u32,
13715        cache: &mut Cache,
13716    ) -> Result<(Vec<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13717        let (ld, x) = self.gemma4_e4b_trunk(e, &[token], cache.pos, cache, false)?;
13718        let logits = e.dtoh(&ld)?;
13719        cache.pos += 1;
13720        Ok((logits, x))
13721    }
13722
13723    /// E4B batched prime (prime_cache contract: (last-row logits host, h_seed pre-norm last
13724    /// row, hidden stack)). First-light: the t-wide trunk (per-row attention) — correct, not
13725    /// fast; the prefill fa arms come later.
13726    pub(crate) fn gemma4_e4b_prime(
13727        &self,
13728        e: &Engine,
13729        tokens: &[u32],
13730        cache: &mut Cache,
13731    ) -> Result<(Vec<f32>, CudaSlice<f32>, CudaSlice<f32>), Box<dyn std::error::Error>> {
13732        // Err, not assert (2026-08-07, lane/gemma4-serve-gaps): same served-chunked-prompt
13733        // process-kill as gemma4_prime — refuse per-request.
13734        if cache.pos != 0 {
13735            return Err(
13736                "e4b prime is fresh-prompt only (v0) — prime the full prompt in one \
13737                        call or decode tokenwise"
13738                    .into(),
13739            );
13740        }
13741        let n_embd = self.cfg.n_embd as usize;
13742        let t = tokens.len();
13743        let (ld, x) = self.gemma4_e4b_trunk(e, tokens, 0, cache, true)?;
13744        cache.pos += t;
13745        let last = e.dtoh(&ld)?; // head_last: ld is already the final row only
13746        let xv = e.view(&x, t * n_embd);
13747        let row = xv.slice((t - 1) * n_embd..t * n_embd);
13748        let mut h_seed = e.uninit(n_embd)?;
13749        e.copy_view_into(&mut h_seed, 0, &row, n_embd)?;
13750        Ok((last, h_seed, x))
13751    }
13752
13753    /// E4B prefill logits (forward contract — no persistent cache; scratch cache internally).
13754    pub(crate) fn gemma4_e4b_forward(
13755        &self,
13756        e: &Engine,
13757        tokens: &[u32],
13758        last_only: bool,
13759    ) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
13760        let mut cache = Cache::new(e, &self.cfg, tokens.len() + 8)?;
13761        let (ld, _x) = self.gemma4_e4b_trunk(e, tokens, 0, &mut cache, last_only)?;
13762        Ok(e.dtoh(&ld)?) // head_last already reduced to the final row when last_only
13763    }
13764}
13765
13766#[cfg(test)]
13767mod prime_chunk_schedule_tests {
13768    use super::{
13769        PRIME_MIN_T, PRIME_PIPE_MIN_CHUNK, dynamic_prime_chunk_ranges, fixed_prime_chunk_ranges,
13770        fixed_prime_chunk_ranges_for_ring,
13771    };
13772
13773    fn sizes(ranges: &[(usize, usize)]) -> Vec<usize> {
13774        ranges.iter().map(|(start, end)| end - start).collect()
13775    }
13776
13777    fn auto_chunk(t: usize) -> usize {
13778        t.div_ceil(8).max(PRIME_PIPE_MIN_CHUNK).min(4096)
13779    }
13780
13781    #[test]
13782    fn fixed_schedule_retains_measured_geometry() {
13783        assert_eq!(
13784            sizes(&fixed_prime_chunk_ranges(461, 128)),
13785            vec![128, 128, 128, 77]
13786        );
13787        assert_eq!(
13788            sizes(&fixed_prime_chunk_ranges(1833, 230)),
13789            vec![230, 230, 230, 230, 230, 230, 230, 223]
13790        );
13791        assert_eq!(sizes(&fixed_prime_chunk_ranges(4096, 512)), vec![512; 8]);
13792        let capped = sizes(&fixed_prime_chunk_ranges_for_ring(8200, 4096, true));
13793        assert_eq!(capped, vec![4096, 4088, 16]);
13794        assert!(capped.iter().all(|&rows| rows <= 4096));
13795        assert_eq!(
13796            sizes(&fixed_prime_chunk_ranges_for_ring(4100, 4096, false)),
13797            vec![4100],
13798            "flag-off schedule remains byte-for-byte the legacy monolithic tail",
13799        );
13800    }
13801
13802    #[test]
13803    fn dynamic_schedule_matches_registered_shapes() {
13804        let cases = [
13805            (461, vec![64, 141, 132, 124]),
13806            (1833, vec![115, 269, 260, 252, 244, 237, 231, 225]),
13807            (4096, vec![256, 602, 580, 563, 545, 531, 516, 503]),
13808        ];
13809        for (t, expected) in cases {
13810            let chunk = auto_chunk(t);
13811            let fixed = fixed_prime_chunk_ranges(t, chunk);
13812            assert_eq!(
13813                sizes(&dynamic_prime_chunk_ranges(t, chunk, &fixed)),
13814                expected
13815            );
13816        }
13817    }
13818
13819    #[test]
13820    fn dynamic_schedule_covers_exactly_and_shrinks_after_fill() {
13821        for t in 256..=8192 {
13822            let chunk = auto_chunk(t);
13823            let fixed = fixed_prime_chunk_ranges(t, chunk);
13824            let dynamic = dynamic_prime_chunk_ranges(t, chunk, &fixed);
13825            assert_eq!(dynamic.len(), fixed.len(), "T={t}");
13826            assert_eq!(dynamic.first().unwrap().0, 0, "T={t}");
13827            assert_eq!(dynamic.last().unwrap().1, t, "T={t}");
13828            for pair in dynamic.windows(2) {
13829                assert_eq!(pair[0].1, pair[1].0, "T={t}");
13830            }
13831            assert!(
13832                dynamic
13833                    .iter()
13834                    .all(|(start, end)| end - start >= PRIME_MIN_T),
13835                "T={t} sizes={:?}",
13836                sizes(&dynamic)
13837            );
13838            if dynamic.len() >= 3 {
13839                let chunk_sizes = sizes(&dynamic);
13840                assert!(
13841                    chunk_sizes[0] < chunk_sizes[1],
13842                    "T={t} sizes={chunk_sizes:?}"
13843                );
13844                assert!(
13845                    chunk_sizes[1..].windows(2).all(|pair| pair[0] >= pair[1]),
13846                    "T={t} sizes={chunk_sizes:?}"
13847                );
13848            }
13849        }
13850    }
13851}
13852
13853#[cfg(test)]
13854mod page_prefetch_tests {
13855    use super::{
13856        grouped_worker_prefetch_position, page_prefetch_positions,
13857        page_prefetch_window_from_values, worker_prefetch_positions,
13858    };
13859
13860    #[test]
13861    fn page_prefetch_window_keeps_existing_opt_in_default() {
13862        assert_eq!(page_prefetch_window_from_values(false, None), 0);
13863        assert_eq!(page_prefetch_window_from_values(false, Some("8")), 0);
13864        assert_eq!(page_prefetch_window_from_values(true, None), 1);
13865        assert_eq!(page_prefetch_window_from_values(true, Some("bad")), 1);
13866        assert_eq!(page_prefetch_window_from_values(true, Some("0")), 0);
13867        assert_eq!(page_prefetch_window_from_values(true, Some("8")), 8);
13868    }
13869
13870    #[test]
13871    fn rolling_page_prefetch_advises_each_future_expert_once() {
13872        let advised: Vec<_> = (0..7)
13873            .flat_map(|position| page_prefetch_positions(position, 7, 3))
13874            .collect();
13875        assert_eq!(advised, vec![1, 2, 3, 4, 5, 6]);
13876
13877        let one_ahead: Vec<_> = (0..4)
13878            .flat_map(|position| page_prefetch_positions(position, 4, 1))
13879            .collect();
13880        assert_eq!(one_ahead, vec![1, 2, 3]);
13881        assert!(page_prefetch_positions(0, 4, 0).is_empty());
13882    }
13883
13884    #[test]
13885    fn grouped_worker_prefetch_primes_first_then_each_known_next_once() {
13886        assert_eq!(grouped_worker_prefetch_position(0, None), None);
13887        let positions: Vec<_> = std::iter::once(grouped_worker_prefetch_position(4, None).unwrap())
13888            .chain(
13889                (0..4).filter_map(|position| grouped_worker_prefetch_position(4, Some(position))),
13890            )
13891            .collect();
13892        assert_eq!(positions, vec![0, 1, 2, 3]);
13893        assert_eq!(grouped_worker_prefetch_position(1, Some(0)), None);
13894    }
13895
13896    #[test]
13897    fn rolling_worker_prefetch_primes_current_and_each_future_expert_once() {
13898        let queued: Vec<_> = (0..8)
13899            .flat_map(|position| worker_prefetch_positions(position, 8, 5))
13900            .collect();
13901        assert_eq!(queued, (0..8).collect::<Vec<_>>());
13902
13903        let one_at_a_time: Vec<_> = (0..4)
13904            .flat_map(|position| worker_prefetch_positions(position, 4, 1))
13905            .collect();
13906        assert_eq!(one_at_a_time, vec![0, 1, 2, 3]);
13907        assert!(worker_prefetch_positions(0, 4, 0).is_empty());
13908    }
13909}
13910
13911pub struct G4DcSlots {
13912    x: CudaSlice<f32>,
13913    xn: CudaSlice<f32>,
13914    cur: CudaSlice<f32>,
13915    hq: CudaSlice<i8>,
13916    hd_: CudaSlice<f32>,
13917    q0: CudaSlice<f32>,
13918    k0: CudaSlice<f32>,
13919    v0: CudaSlice<f32>,
13920    q: CudaSlice<f32>,
13921    k: CudaSlice<f32>,
13922    v: CudaSlice<f32>,
13923    attn: CudaSlice<f32>,
13924    o: CudaSlice<f32>,
13925    attn_out: CudaSlice<f32>,
13926    zsh: CudaSlice<f32>,
13927    zq: CudaSlice<i8>,
13928    zd: CudaSlice<f32>,
13929    gate: CudaSlice<f32>,
13930    up: CudaSlice<f32>,
13931    act: CudaSlice<f32>,
13932    actq: CudaSlice<i8>,
13933    actd: CudaSlice<f32>,
13934    f0: CudaSlice<f32>,
13935    sn: CudaSlice<f32>,
13936    hn: CudaSlice<f32>,
13937    logits: CudaSlice<f32>,
13938}