Skip to main content

memra_engine/
decode_batch.rs

1//! Batched decode step — B sequences share one fused pass (ARCHITECTURE-H100.md §3 B2').
2//!
3//! The bandwidth thesis: decode is weight-stream-bound, so every projection at m=B rows
4//! amortizes one weight read across B sequences. Row-parallel ops (norm/rope/quantize/
5//! activation) batch trivially — they are the SAME kernels prefill already runs at T rows.
6//! Only truly per-sequence state stays in a loop: KV append + fa_decode over each cache,
7//! and the GDN/conv recurrent step (v1: per-seq loop via the existing single-seq path;
8//! a blockIdx.z-batched GDN state kernel is the v2 fusion).
9//!
10//! EXACTNESS CONTRACT (the law this module lives under):
11//! - B == 1 must be BIT-IDENTICAL to `decode_step_h` (gate: decode-batch-gate).
12//! - 2 <= B <= 8: each row rides the m=2..9 verify-tier mmvq kernels, which are per-row
13//!   bit-identical to m=1 (the spec-exactness machinery decode_step_t relies on). Each
14//!   sequence's token stream must equal its isolated single-seq run (worker.rs contract:
15//!   "byte-identical to isolated").
16//! - 9 <= B <= 16 (the EXACT-16 tier, inc3 2026-08-01): admitted iff
17//!   `decode_batch_exact16_ok` — every matmul rides the b16 batched-mmvq class
18//!   (bit-identical per (token,row) to m=1; Q8_0 needs the q8rp mirror) under a
19//!   verify_exact scope that disables the m>=16 GEMM/MMQ arms. gate2 bit-strength
20//!   PASS at B=12/16 (research/batched-tick-inc3-20260801). Refused otherwise.
21//! - B > 16 crosses into GEMM/dp4a-tail numeric configs with NO exact kernel class —
22//!   refused (MEMRA_DECODE_BATCH_CAP stays a measurement door).
23//!
24//! v1 scope: the hybrid (Qwen3.5-class) non-gemma4 trunk. Fused m=1 micro-launches
25//! (fused3 QKV, cross-layer add+norm+q8 chain) are NOT used — the unfused sequence is
26//! bit-identical (kernel_check: add_rms_norm == add;rms_norm; _q8_1 == +quantize_q8_1)
27//! and keeps the batched path simple. Batched fusions are tuning work, not correctness.
28
29use crate::cache::Cache;
30use crate::hybrid::{HybridModel, Mixer};
31use crate::Engine;
32use cudarc::driver::CudaSlice;
33
34/// Per-step, per-LAYER-RANGE invariants the batched trunk needs: the device state-pointer
35/// table for the range's layers, the arm picks, and the per-row `t_kv` snapshot. Built once
36/// per step per range by `HybridModel::batch_layer_ctx`, consumed by `decode_batch_layers`.
37///
38/// WHY IT IS RANGE-SCOPED AND NOT STEP-SCOPED (this is the whole point of the struct):
39/// `ptr_table` is a `CudaSlice<u64>` of DEVICE ADDRESSES, uploaded through `e` — so it lives
40/// on `e`'s device, and its entries are pointers into caches that live on the device that
41/// OWNS those layers. Under a pp stage split, stage s runs layers [fence[s], fence[s+1])
42/// whose cache state was allocated by stage s's engine (`pp::new_cache` -> `Cache::new_ppn`),
43/// so stage s must build its OWN table through its OWN engine. One step-wide table built on
44/// the primary would put every stage's kernel arguments in stage-0's HBM — a peer read per
45/// pointer fetch, which is the exact cliff `pp::refuse_unsplit_if_remote` exists to stop.
46/// `lo`/`hi` are recorded so the consumer can assert the ctx it was handed matches the range
47/// it was asked to run (the offsets in `lin_base`/`attn_base` are only valid for that range).
48pub(crate) struct BatchLayerCtx {
49    /// Offset into `ptr_table` of layer il's [conv x B][ssm_in x B][ssm_out x B] block
50    /// (linear-attn layers only). Indexed by ABSOLUTE layer id; `None` off-range.
51    lin_base: Vec<Option<usize>>,
52    /// Offset into `ptr_table` of layer il's [k0,v0,k1,v1,..] block (full-attn layers only).
53    /// Indexed by ABSOLUTE layer id; `None` off-range.
54    attn_base: Vec<Option<usize>>,
55    ptr_table: Option<CudaSlice<u64>>,
56    /// Per-row `pos + 1` — the t_kv each sequence attends at this step. Layer-invariant
57    /// within a step, so the arm picks below are decided once.
58    t_kvs: Vec<usize>,
59    t_kv_max: usize,
60    /// The single `fa_split_keys` rung every row shares (the rows-twins straddle law).
61    sp0: usize,
62    seqs_append: bool,
63    seqs_fa: bool,
64    lo: usize,
65    hi: usize,
66}
67
68// ---- MEMRA_BATCH_PHASE=1 (diagnostics): sync-bounded per-phase accumulators for the batched
69// tick. Each boundary syncs the stream, so the TOTAL inflates (launch pipelining is destroyed);
70// the value is the RANKING/shares, not absolute ms. Read via `batch_phase_report()`.
71pub(crate) static BATCH_PHASE: std::sync::Mutex<[f64; 12]> = std::sync::Mutex::new([0.0; 12]);
72pub const BATCH_PHASE_NAMES: [&str; 12] = [
73    "setup(ptrs+embed H2D)",
74    "attn batched pre (norm/qkv/rope)",
75    "attn per-seq: kv append",
76    "attn per-seq: q/a dtod copies",
77    "attn per-seq: fa_decode",
78    "attn post (gate+o-proj)",
79    "gdn batched projections",
80    "gdn state ops (conv/prep/scan)",
81    "gdn out (gated norm+proj)",
82    "ffn (add/norm/gate/up/act/down)",
83    "lm_head (norm+matmul)",
84    "logits D2H + host split",
85];
86pub fn batch_phase_on() -> bool {
87    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
88    *ON.get_or_init(|| std::env::var("MEMRA_BATCH_PHASE").as_deref() == Ok("1"))
89}
90/// Accumulate the elapsed time since `last` into phase slot `slot` and re-stamp `last`.
91/// No-op unless `MEMRA_BATCH_PHASE=1`. Syncs the ambient stream first, so under a pp stage
92/// scope this bounds the STAGE's stream, which is what the caller is timing.
93///
94/// A free fn rather than the closure it replaced: `decode_batch_layers` (the pp stage seam)
95/// runs the instrumented layer loop, so the marker has to be callable from both the seam
96/// and its caller's epilogue. `batch_phase_on()` is a `OnceLock` memo, so per-call cost is
97/// the same atomic load the hoisted `ph_on` local was.
98fn ph_mark(
99    e: &Engine,
100    slot: usize,
101    last: &mut std::time::Instant,
102) -> Result<(), Box<dyn std::error::Error>> {
103    if batch_phase_on() {
104        e.stream().synchronize()?;
105        let now = std::time::Instant::now();
106        BATCH_PHASE.lock().unwrap()[slot] += (now - *last).as_secs_f64();
107        *last = now;
108    }
109    Ok(())
110}
111
112pub fn batch_phase_report() -> String {
113    let ph = BATCH_PHASE.lock().unwrap();
114    let tot: f64 = ph.iter().sum();
115    let mut rows: Vec<(usize, f64)> = ph.iter().copied().enumerate().collect();
116    rows.sort_by(|a, b| b.1.total_cmp(&a.1));
117    let mut s = format!("[batch-phase] total {:.1} ms (sync-bounded; shares rank, not walltime)\n", tot * 1e3);
118    for (i, v) in rows {
119        s += &format!("  {:>6.1} ms {:>5.1}%  {}\n", v * 1e3, v / tot * 100.0, BATCH_PHASE_NAMES[i]);
120    }
121    s
122}
123
124impl HybridModel {
125    /// Batched-decode width cap. 8 = the exactness-tier default (see the assert below);
126    /// MEMRA_DECODE_BATCH_CAP overrides for tier-probe measurement, clamped to 32.
127    pub fn decode_batch_cap() -> usize {
128        use std::sync::OnceLock;
129        static CAP: OnceLock<usize> = OnceLock::new();
130        *CAP.get_or_init(|| {
131            std::env::var("MEMRA_DECODE_BATCH_CAP").ok()
132                .and_then(|v| v.parse().ok())
133                .map(|c: usize| c.clamp(1, 32))
134                .unwrap_or(8)
135        })
136    }
137
138    /// EXACT-16 TIER admission (increment 3a, 2026-08-01, 5090 receipts
139    /// research/batched-tick-inc3-20260801): true iff EVERY matmul the batched decode step
140    /// runs has a per-(token,row) bit-exact kernel class at m=9..16 under the verify_exact
141    /// scope — i.e. the batched-mmvq b16 family (32-thread warp reduce, the exact m=1 mmvq
142    /// program per column) or the e4m3 grid.y=m mmvq catch-all. Q8_0 qualifies only with
143    /// the split-plane mirror (rp4, MEMRA_Q8RP): its b16 kernel exists only as the _rp twin.
144    /// Float matmuls (cuBLASLt, n-dependent reductions) and MoE FFNs disqualify the model.
145    /// Measured attribution for WHY the naked m=16 tier is not exact: the m>=16 arms
146    /// (MMQ int8-MMA `mul_mat_q` — MEMRA_PP_Q8MMQ default-on — and `qmatvec_gemm`, both
147    /// block-scale f32) and the m=9..15 dp4a tail (128-thread two-level reduce) all break
148    /// per-row bit-identity vs isolated decode (gate2 step-0 bit-diffs, maxdiff ~1.3-2.3e-1).
149    pub fn decode_batch_exact16_ok(&self) -> bool {
150        fn ok(w: &crate::model::GpuTensor) -> bool {
151            match w {
152                crate::model::GpuTensor::Quant { qtype, .. } =>
153                    *qtype == crate::QT_Q4_0 || *qtype == crate::QT_Q6_K
154                    || *qtype == crate::QT_F8_E4M3
155                    // BLOCK-128 FP8-ST (lane/rp-on-st, 2026-08-06): admitted now that the class
156                    // has a b16 batched kernel (`qmatvec_e4m3_blk_mmvq_b16`), bit-identical per
157                    // (token,row) to its m=1 launch. Before that kernel existed this class fell to
158                    // the grid.y=m form at every width — still EXACT, so the tier's correctness
159                    // bar was met, but it re-read the weight m times, which is why admitting it
160                    // without the kernel would have been a throughput trap rather than a win.
161                    || *qtype == crate::QT_F8_E4M3_BLK
162                    // NVFP4 (lane/rp-on-st, 2026-08-06) — THE blocker this lane measured. The
163                    // mixed FP8-ST 27B is 193 NVFP4 dense-MLP tensors, and this predicate is an
164                    // ALL over every matmul, so NVFP4's missing b16 refused the whole checkpoint
165                    // (`B=16 > cap 8 with no exact tier ... refused`) even with both e4m3 classes
166                    // admitted. It now has base + _rp b16 twins off its existing batched template
167                    // (bit-identical per (token,row) to the m=1 mmvq: same nibble decode, dp4a
168                    // order, ue4m3 scale, warp reduce). This also opens the tier for pure-NVFP4
169                    // GGUF models, which is a behavior change on the primary format — hence the
170                    // full decode-batch config+strict battery on both.
171                    || *qtype == crate::QT_NVFP4
172                    // Q4_K (lane/rp-on-st): named by MEMRA_EXACT16_WHY as the 9B NVFP4 GGUF's
173                    // refusing class (`L0.wqkv qtype=1`) — mixed NVFP4 checkpoints keep Q4_K
174                    // attention. Now has base + _rp b16.
175                    || *qtype == crate::QT_Q4_K
176                    // Q5_K (lane/rp-on-st): the FOURTH class the diagnostic named on the same 9B
177                    // GGUF (`L0.wqkv_gate qtype=3`). A shipped mixed checkpoint spreads ~500
178                    // matmuls over four/five classes, and this predicate is an ALL — so chunk 16
179                    // was unreachable for every real artifact until every class had a b16.
180                    || *qtype == crate::QT_Q5_K
181                    // Q8_0 NO LONGER requires the mirror (rp4): it has a base b16 too, so the
182                    // tier is reachable at zero VRAM. Named by the diagnostic as the FP8-ST
183                    // refusal — `L0.ssm_beta qtype=0 rp4=false`, a 23.9 MiB residual class that
184                    // was gating chunk 16 for a 16.4 GiB checkpoint.
185                    || *qtype == crate::QT_Q8_0,
186                _ => false,
187            }
188        }
189        // WHY-NOT DIAGNOSTIC (lane/rp-on-st, 2026-08-06): this predicate is a bare bool over
190        // ~500 tensors, so a refusal produced only `B=16 > cap 8 with no exact tier ... refused`
191        // with no way to tell WHICH class refused. That cost this lane two wrong hypotheses (the
192        // rp mirror, then e4m3-only) before the NVFP4 gap was found. MEMRA_EXACT16_WHY=1 names
193        // the first refusing tensor + its qtype. Diagnostic-only per flags doctrine; default off,
194        // zero cost when unread.
195        let why = std::env::var("MEMRA_EXACT16_WHY").is_ok();
196        macro_rules! chk {
197            ($t:expr, $label:expr) => {{
198                let r = ok($t);
199                if !r && why {
200                    // qtype = -1 means the tensor is NOT Quant at all (a float/BF16/F16
201                    // container), which the tier can never admit — a distinct diagnosis from
202                    // "quantized, but in a class with no b16 kernel".
203                    let (qt, rp4) = match $t {
204                        crate::model::GpuTensor::Quant { qtype, rp4, .. } => (*qtype, rp4.is_some()),
205                        _ => (-1, false),
206                    };
207                    eprintln!("[exact16] REFUSED by {} qtype={qt} rp4={rp4}", $label);
208                }
209                r
210            }};
211        }
212        if self.cfg.m3.is_some() || self.is_gemma4_e4b() || self.cfg.gemma4.is_some() {
213            if why { eprintln!("[exact16] REFUSED by architecture (m3/gemma4)"); }
214            return false;
215        }
216        self.layers.iter().enumerate().all(|(li, l)| {
217            let mix_ok = match &l.mixer {
218                Mixer::Full(fa) => chk!(&fa.wq, format!("L{li}.wq")) && chk!(&fa.wk, format!("L{li}.wk"))
219                    && chk!(&fa.wv, format!("L{li}.wv")) && chk!(&fa.wo, format!("L{li}.wo")),
220                Mixer::Linear(la) => chk!(&la.wqkv, format!("L{li}.wqkv"))
221                    && chk!(&la.wqkv_gate, format!("L{li}.wqkv_gate"))
222                    && chk!(&la.ssm_beta, format!("L{li}.ssm_beta"))
223                    && chk!(&la.ssm_alpha, format!("L{li}.ssm_alpha"))
224                    && chk!(&la.ssm_out, format!("L{li}.ssm_out")),
225                // MLA rides its own increment-4 arm; never admitted to the exact-16 tier here.
226                Mixer::Mla(_) => { if why { eprintln!("[exact16] REFUSED by L{li} MLA mixer"); } false }
227            };
228            let ffn_ok = match &l.ffn {
229                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } =>
230                    chk!(ffn_gate, format!("L{li}.ffn_gate")) && chk!(ffn_up, format!("L{li}.ffn_up"))
231                    && chk!(ffn_down, format!("L{li}.ffn_down")),
232                crate::hybrid::Ffn::Moe(_) => { if why { eprintln!("[exact16] REFUSED by L{li} MoE ffn"); } false }
233            };
234            mix_ok && ffn_ok
235        }) && chk!(&self.output, "output".to_string())
236    }
237
238    /// H3 rollback/A-B seam (serve-path phase 2): `MEMRA_SERVE_B1FAST=0` sends B=1 back
239    /// through the batched body (the pre-change tick, bit-for-bit). Default ON.
240    ///
241    /// EXACTNESS, stated precisely (measured on-box 2026-08-05, sm_120 q9 NVFP4-MTP):
242    /// the fast path is BIT-IDENTICAL TO `decode_step_h` — decode-batch-gate's STRICT
243    /// gate1 (`--mode strict`) PASSes with it ON and FAILs with it OFF at maxdiff
244    /// 1.591e-1. It is deliberately NOT bit-identical to the batched body: the two
245    /// carry the long-accepted decode-config FP-composition gap (same class gate1's
246    /// config mode tolerates), and this lever moves solo sessions onto the NAKED side
247    /// of it. That is the desired direction — a c=1 serve request now computes exactly
248    /// what `run-gen` computes for the same prompt. Token-stream receipts:
249    /// research/servepath-p2-20260805 (greedy 150 ids + seeded-sampled identical to the
250    /// run-gen oracle AND cross-arm, so the gap is sub-token here as designed).
251    ///
252    /// Read fresh (an `AtomicU8` memo, not a `OnceLock`): decode-batch-gate flips this
253    /// seam BETWEEN gates in-process — gate1 needs the fast path ON to prove bit-identity,
254    /// gate2 needs it pinned OFF to keep testing the batched body. A latch-once read would
255    /// bake whichever gate ran first, so the gate could never test both sides. The memo
256    /// caches the parse but `set_b1_fast` invalidates it.
257    pub fn b1_fast_on() -> bool {
258        // 0 = unknown/invalidated, 1 = off, 2 = on
259        match Self::b1_fast_memo().load(std::sync::atomic::Ordering::Relaxed) {
260            1 => false,
261            2 => true,
262            _ => {
263                let on = std::env::var("MEMRA_SERVE_B1FAST").as_deref() != Ok("0");
264                Self::b1_fast_memo()
265                    .store(if on { 2 } else { 1 }, std::sync::atomic::Ordering::Relaxed);
266                on
267            }
268        }
269    }
270
271    fn b1_fast_memo() -> &'static std::sync::atomic::AtomicU8 {
272        static MEMO: std::sync::atomic::AtomicU8 = std::sync::atomic::AtomicU8::new(0);
273        &MEMO
274    }
275
276    /// Test/gate seam: force the B=1 fast path on or off for the rest of the process,
277    /// overriding the env. Used by decode-batch-gate to pin gate2's reference arm.
278    pub fn set_b1_fast(on: bool) {
279        Self::b1_fast_memo()
280            .store(if on { 2 } else { 1 }, std::sync::atomic::Ordering::Relaxed);
281    }
282
283    /// H3 body: the m=1 FUSED trunk (`decode_layers_eager` — shared verbatim with
284    /// `decode_step_h`/the ppN stages) plus the batched path's own serving epilogue
285    /// (grammar mask, device sample, lean-logits park). See the call-site comment in
286    /// `decode_step_batch_sampled_lean_masked` for why this is bit-identical.
287    fn decode_step_b1_fast(
288        &self,
289        e: &Engine,
290        token: u32,
291        caches: &mut [&mut Cache],
292        samp: &[Option<(f32, u64, u32)>],
293        masks: &[Option<(&CudaSlice<u32>, usize)>],
294        lean: bool,
295    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
296        let n_embd = self.cfg.n_embd as usize;
297        let eps = self.cfg.rms_eps;
298        let pos = caches[0].pos;
299        let pos_d = e.htod_i32(&[pos as i32])?;
300        let x = e.htod(&self.embd.gather(n_embd, &[token]))?;
301        // the SHARED m=1 trunk: same function decode_step_h runs, so every m=1 fusion
302        // (cross-layer add+norm+q8_1, fused SwiGLU, lever 1's gate+up dual) fires here.
303        let x = self.decode_layers_eager(e, x, 0, self.layers.len(), &pos_d, pos, caches[0])?;
304        let mut hn = e.uninit(n_embd)?;
305        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, 1, eps)?;
306        let logits = e.matmul(&self.output, &hn, 1)?;
307
308        // ---- epilogue: byte-for-byte the batched path's, at b_n=1 ----
309        let n_vocab = self.output.out_features();
310        let mut logits = logits;
311        let mut pristine: Option<CudaSlice<f32>> = None;
312        if let Some((mask, words)) = masks.first().copied().flatten() {
313            assert!(samp.first().copied().flatten().is_some(),
314                    "grammar-masked row 0 must request a device sample");
315            if lean {
316                let cache = &mut caches[0];
317                if cache.last_logits_dev.as_ref().map(|d| d.len() < n_vocab).unwrap_or(true) {
318                    cache.last_logits_dev = Some(e.uninit(n_vocab)?);
319                }
320                let dst = cache.last_logits_dev.as_mut().unwrap();
321                e.dtod_copy_view(&logits.slice(0..n_vocab), dst)?;
322            } else {
323                let mut p = e.uninit(n_vocab)?;
324                e.dtod_copy_view(&logits.slice(0..n_vocab), &mut p)?;
325                pristine = Some(p);
326            }
327            e.mask_logits_col(&mut logits, mask, 0, n_vocab, words)?;
328        }
329
330        let mut next: Vec<Option<u32>> = vec![None; 1];
331        if let Some((temp, seed, ctr)) = samp.first().copied().flatten() {
332            let mut toks = e.alloc_u32_zeroed(1)?;
333            if temp <= 0.0 {
334                e.argmax_token_device_col(&logits, 0, n_vocab, &mut toks, 0)?;
335            } else {
336                let mut pb = e.zeros(n_vocab)?;
337                e.gumbel_perturb_col(&logits, 0, &mut pb, n_vocab, seed, ctr, temp)?;
338                e.argmax_token_device_col(&pb, 0, n_vocab, &mut toks, 0)?;
339            }
340            next[0] = Some(e.dtoh_u32(&toks)?[0]);
341        }
342
343        let sampled = samp.first().copied().flatten().is_some();
344        let rows: Vec<Vec<f32>> = if lean && sampled {
345            if masks.first().copied().flatten().is_none() {
346                let cache = &mut caches[0];
347                if cache.last_logits_dev.as_ref().map(|d| d.len() < n_vocab).unwrap_or(true) {
348                    cache.last_logits_dev = Some(e.uninit(n_vocab)?);
349                }
350                let dst = cache.last_logits_dev.as_mut().unwrap();
351                e.dtod_copy_view(&logits.slice(0..n_vocab), dst)?;
352            }
353            vec![Vec::new()]
354        } else if let Some(p) = pristine.as_ref() {
355            vec![e.dtoh(p)?]
356        } else {
357            vec![e.dtoh(&logits)?]
358        };
359        // decode_layers_eager does NOT advance cache.pos (decode_step_h advances it after
360        // the head); the batched path advances every cache at the tail — same here.
361        caches[0].pos += 1;
362        Ok((rows, next))
363    }
364
365    /// One batched greedy-decode step over B independent sequences.
366    /// `tokens[b]` is sequence b's input token; `caches[b]` its private cache (position,
367    /// quantized KV, GDN/conv state). Returns the B logits rows (host, [n_vocab] each).
368    /// Each cache's pos/len advance exactly as `decode_step_h` would.
369    pub fn decode_step_batch(
370        &self,
371        e: &Engine,
372        tokens: &[u32],
373        caches: &mut [&mut Cache],
374    ) -> Result<Vec<Vec<f32>>, Box<dyn std::error::Error>> {
375        let (rows, _) = self.decode_step_batch_sampled(e, tokens, caches, &[])?;
376        Ok(rows)
377    }
378
379    /// `decode_step_batch` + DEVICE-SIDE SAMPLING for eligible rows (the batched-tick lever,
380    /// 2026-08-01): the host sampler's temp-path is O(n_vocab) with a full-vocab exp per row
381    /// (measured 1.36 ms/row at the 9B's 248320 vocab = 10.9 ms/tick at B=8 — the single
382    /// largest component of the serving tick). Here each requested row samples ON DEVICE
383    /// between the lm_head matmul and the logits D2H:
384    ///   temp <= 0 (greedy): the 2-pass device argmax — bit-identical to host argmax
385    ///     (argmax-gate contract, same kernels as the dc serving path).
386    ///   temp > 0: gumbel_perturb(seed, ctr, temp) + the same argmax = ONE categorical draw
387    ///     from softmax(logits/temp) — the sampled-spec Philox machinery. Deterministic per
388    ///     (seed, ctr) and INDEPENDENT of batch composition (the isolation contract;
389    ///     decode-batch-gate gate3). NOTE: the draw stream differs from the host sampler's
390    ///     SplitMix64 (distribution-equal, seed-deterministic, NOT byte-equal to the old
391    ///     host draws) — greedy rows are unchanged bit-exact.
392    /// `samp[bi] = Some((temp, seed, ctr))` requests a device sample for row bi; the full
393    /// logits rows are still returned (worker keeps last_logits semantics + fallback rows).
394    pub fn decode_step_batch_sampled(
395        &self,
396        e: &Engine,
397        tokens: &[u32],
398        caches: &mut [&mut Cache],
399        samp: &[Option<(f32, u64, u32)>],
400    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
401        self.decode_step_batch_sampled_lean(e, tokens, caches, samp, false)
402    }
403
404    /// `decode_step_batch_sampled` + LEAN LOGITS (increment 2 component 3, 2026-08-01):
405    /// with `lean`, device-sampled rows SKIP the [n_vocab] logits D2H (9.4%/32.5% of the
406    /// pre-/post-inc2 tick profile) — their returned row is EMPTY. The audit-mapped
407    /// consumers: (a) the next tick's host sample — never fires, `device_next` carries the
408    /// token; (b) the graph-promotion argmax — reads only prefill logits (generated empty);
409    /// (c) the KV-reuse pool park at retire — the REAL consumer, served by a per-cache
410    /// device park: the row is dtod-copied into `cache.last_logits_dev` (device bandwidth)
411    /// and D2H'd ONCE at retire by the worker. Rows without a device sample keep a per-row
412    /// D2H. `lean=false` is bit-for-bit the previous method (gates + non-serving callers).
413    pub fn decode_step_batch_sampled_lean(
414        &self,
415        e: &Engine,
416        tokens: &[u32],
417        caches: &mut [&mut Cache],
418        samp: &[Option<(f32, u64, u32)>],
419        lean: bool,
420    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
421        self.decode_step_batch_sampled_lean_masked(e, tokens, caches, samp, &[], lean)
422    }
423
424    /// `decode_step_batch_sampled_lean` + GRAMMAR MASKS (constrained decoding, 2026-08-03):
425    /// `masks[bi] = Some((packed_bitset, words))` bans every unset-bit vocab id on row bi
426    /// (mask_logits_f32, -FLT_MAX) BETWEEN the lm_head matmul and the device sampler, so a
427    /// constrained row rides the SAME device-sample/lean-logits tick as everyone else — no
428    /// full-row D2H, no host O(n_vocab) sample. Contract: a masked row must also request a
429    /// device sample. The row's PRISTINE logits are preserved for their consumers before the
430    /// in-place ban: lean rows park the unmasked row into `cache.last_logits_dev` (the
431    /// retire-time reuse-pool park stays unmasked — continuations resume grammar-free, the
432    /// v1 host-path contract), non-lean rows D2H the unmasked row. `masks = &[]` is
433    /// bit-for-bit the unmasked method.
434    pub fn decode_step_batch_sampled_lean_masked(
435        &self,
436        e: &Engine,
437        tokens: &[u32],
438        caches: &mut [&mut Cache],
439        samp: &[Option<(f32, u64, u32)>],
440        masks: &[Option<(&CudaSlice<u32>, usize)>],
441        lean: bool,
442    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
443        // NOTE (inc3 3c, 2026-08-01, KILLED ARM): a deferred-token-readback variant (all
444        // chunks of a tick writing device-sampled tokens into one shared buffer, ONE
445        // dtoh_u32 after the last chunk instead of one per chunk) measured FLAT at serve
446        // level on the 5090 (N=4 medians within +-0.7% at c=8/16/32 — 3 saved syncs
447        // against a ~100 ms weight-bound tick is ~0.1%, below resolution). Killed per the
448        // flags doctrine; receipts research/batched-tick-inc3-20260801 (serve-points.jsonl
449        // base vs defer arms) are the record. The per-chunk [B]-u32 readback below IS the
450        // tick's only steady-state D2H — one per chunk, none per seq.
451        let b_n = tokens.len();
452        assert!(b_n >= 1 && b_n == caches.len(), "tokens/caches length mismatch");
453        // ---- PP DOOR: THE BATCHED STAGE SPLIT (pp2-batch 2026-08-06) ----------------------
454        // Until this increment this body had NO pp arm: it walked lo=0..n_layers on the
455        // primary engine's stream, with no stage split, no boundary, and no `rt.enter()`. With
456        // the door open and a sharded cross-device placement, every projection for the remote
457        // stages' layers was read over PCIe, per step, silently — measured 7.4 vs 208.9 tok/s
458        // at B=1 (28x), 47.4 vs 657.0 at B=8 (13.9x) on a PRO 6000 pair over Gen5 x16 P2P.
459        // Nothing failed or warned, because peer reads return identical bytes and all three
460        // `decode-batch-gate` gates PASS on that config — the failure mode was performance,
461        // and a green exactness battery hid it. `pp2-hardening` made that regime FAIL CLOSED
462        // (research/pp2-hardening-20260806); this lane makes it legitimately split, so the
463        // refusal lifts for the batched path.
464        //
465        // `decode_step_batch_ppn` runs each stage's layer range through that stage's engine
466        // and stream with a [B, n_embd] boundary transfer between them, i.e. every stage
467        // touches only LOCAL weights and LOCAL cache state. The refusal below still guards
468        // the residue: the door open with `MEMRA_PP_STREAMS=0` (the same-stream rollback,
469        // which also disables the sharded loader, so nothing is remote — `pp_shard_off` and
470        // `pp2_streams_off` both make `pp_sharded_cross_device()` false) or a placement whose
471        // PpNRt fails to build. Keeping the call means a future path that reaches here in a
472        // remote regime still refuses instead of regressing 28x.
473        if let Some(fence) = crate::pp::pp_cuts(self.layers.len()) {
474            if !crate::pp::pp2_streams_off() && crate::pp::batch_pp_on() {
475                return self.decode_step_batch_ppn(
476                    e, tokens, caches, samp, masks, lean, &fence,
477                );
478            }
479        }
480        crate::pp::refuse_unsplit_if_remote(
481            "decode_step_batch",
482            "drop MEMRA_PP_STREAMS=0 / MEMRA_BATCH_PP=0 so the batched path takes its OWN \
483             stage split (decode_step_batch_ppn), or serve single-stream over the eager pp \
484             arm (decode_step_h), which is also split",
485        )?;
486        // ---- H3: B=1 FAST-PATH (serve-path phase 2, 2026-08-05) ----------------------------
487        // At b_n==1 every projection below calls `matmul_pre(.., b_n)` with m=1, which is
488        // ALREADY the m=1 mmvq dispatch — so the m=1 *kernel family* was never the gap. What
489        // this body does NOT have is the m=1 *fusion chain* that `decode_step_h` carries:
490        //   - the cross-layer add+norm+quantize fusion (`add_rms_norm_q8_1`: 3 launches -> 1),
491        //   - the fused SwiGLU epilogue (`silu_mul_scaled_q8_1`: folds ffn_down's quantize
492        //     into its producer) and, with it, `matmul_pre_dual_noscale`'s gate+up pair
493        //     fusion — i.e. phase-1 LEVER 1.
494        // Routing b_n==1 through `decode_layers_eager` (the SHARED trunk `decode_step_h` and
495        // the ppN stages already use, lifted verbatim — not a copy) makes every present and
496        // future m=1 lever fire on the serve path automatically, which is the durable half of
497        // this change. The epilogue (grammar mask -> device sample -> lean logits park) is
498        // kept EXACTLY as the batched path runs it, so the serving contract is untouched.
499        // BIT-IDENTITY: the trunk is the same function `decode_step_h` calls, and every
500        // fusion it enables is kernel-check-pinned bit-identical to its unfused sequence
501        // (add_rms_norm == add;rms_norm | _q8_1 == +quantize_q8_1 | dual_noscale == two
502        // matmul_pre_noscale). Gate: decode-batch-gate B=1 vs decode_step_h + serve stream
503        // identity. MEMRA_SERVE_B1FAST=0 is the rollback/A-B seam.
504        if b_n == 1
505            && Self::b1_fast_on()
506            && !self.is_gemma4_e4b()
507            && self.cfg.gemma4.is_none()
508            && self.cfg.m3.is_none()
509            && crate::pp::pp_cuts(self.layers.len()).is_none()
510            && !e.verify_exact_on()
511        {
512            return self.decode_step_b1_fast(e, tokens[0], caches, samp, masks, lean);
513        }
514        // MEMRA_DECODE_BATCH_CAP (experimental door, serving-lane tier probe 2026-08-01):
515        // default 8 keeps the v1 exactness policy — B=2..8 rides the verify-tier batched
516        // mmvq arms, per-row bit-identical to isolated m=1 decode. Values >8 are a
517        // MEASUREMENT DOOR ONLY: m=9..15 falls to the grid.y=m dp4a tail (m weight
518        // re-reads + a different reduce shape) and m>=16 crosses into the GEMM tier
519        // (block-scale f32 rounding) — BOTH break the "byte-identical to isolated"
520        // serving contract. Never default this above 8 without the batched-tier
521        // exactness policy landing.
522        let cap = Self::decode_batch_cap();
523        // EXACT-16 TIER (increment 3a): chunks of 9..=16 are admitted WITHOUT the env door
524        // when every matmul has a bit-exact b16-class kernel (see decode_batch_exact16_ok).
525        // The verify_exact scope below pins that dispatch for the whole step: it turns off
526        // the m>=16 GEMM arms (qmatvec_gemm + MMQ + fp8/f16/fp4 — all block-scale/foreign
527        // numeric configs) so every projection rides the batched-mmvq b16 tier, which is
528        // per-(token,row) bit-identical to isolated m=1 decode (gate2 bit-strength PASS at
529        // B=12/16, s32+s160, 5090 receipts research/batched-tick-inc3-20260801). Without
530        // the exact tier, B>cap stays refused; the env door (MEMRA_DECODE_BATCH_CAP) keeps
531        // its old meaning as the non-exact measurement probe.
532        let exact16 = b_n > 8 && b_n <= 16 && self.decode_batch_exact16_ok();
533        assert!(
534            b_n <= cap || exact16,
535            "decode_step_batch: B={b_n} > cap {cap} with no exact tier — refused. Either \
536             B>16 (there is NO exact kernel class above 16: m>16 crosses GEMM/dp4a numeric \
537             configs; the serve scheduler chunks wider concurrency into <=16 groups instead), \
538             or some matmul in this checkpoint has no bit-exact b16 kernel — run with \
539             MEMRA_EXACT16_WHY=1 to see which tensor and qtype refuses"
540        );
541        struct ExactScope<'a>(&'a Engine, bool);
542        impl Drop for ExactScope<'_> {
543            fn drop(&mut self) {
544                if self.1 {
545                    self.0.set_verify_exact(false);
546                }
547            }
548        }
549        let _exact_scope = ExactScope(e, exact16);
550        if exact16 {
551            e.set_verify_exact(true);
552        }
553        // gemma4: NO batched arm at any B (per-layer SWA/global geometry, hd-512 MQA globals,
554        // weightless V-norm, softcapped head — none of it in the generic body below). This was
555        // an assert until 2026-08-07: one serve request panicked the worker, the respawn
556        // re-panicked on the queued request, and the process FATALed
557        // (research/gemma4-serve-20260807/raw/repro-panic-server-*.log). The worker now routes
558        // gemma4 sessions to the per-session eager loop and never calls here; this Err is the
559        // defense-in-depth backstop — a future path that reaches it refuses PER-REQUEST
560        // instead of killing the process. The eager arm (gemma4_decode_step_h) is the
561        // supported decode.
562        if self.is_gemma4_e4b() || self.cfg.gemma4.is_some() {
563            return Err("decode_step_batch has no gemma4 arm (per-layer swa/global geometry, \
564                        softcapped head) — serve gemma4 on the eager per-session path".into());
565        }
566        // step35 (lane/step35-batched-decode, 2026-08-08): its OWN batched walk. The generic
567        // body below is the uniform Full arm — global n_head, 128-dim rope on every layer, no
568        // SWA window, no head-wise gate — which on step35 produced HTTP-200 GARBAGE at c>1
569        // (research/step-sku-20260807/raw/b2ab-pre-*.log), so step35 NEVER enters it at any B.
570        // `step35_decode_batch_layers` carries the real geometry: per-layer n_head (64/96),
571        // partial rope (64 full / 128 SWA, dual base, rope_freqs on FULL only), per-SESSION
572        // SWA view offsets from each session's own kvl.len, the separate head-wise gate at
573        // m=B, and the sigmoid-router MoE via the same moe_ffn_il_zq8 the eager path uses.
574        // MEMRA_STEP35_BATCH=0 = the fail-closed rollback seam. The server caps chunks at
575        // B=1; on PP-N the B=1 correctness default also refuses the eager numeric class, while
576        // an unsplit deployment can still use its existing eager B=1 route.
577        if self.cfg.step35.is_some() {
578            if !Self::step35_batch_on() {
579                return Err("step35 batched decode is disabled (MEMRA_STEP35_BATCH=0) — \
580                            only a non-PP eager B=1 route remains available".into());
581            }
582            let n_embd = self.cfg.n_embd as usize;
583            let eps = self.cfg.rms_eps;
584            let mut ph_last = std::time::Instant::now();
585            let pos_v: Vec<i32> = caches.iter().map(|c| c.pos as i32).collect();
586            let pos_d = e.htod_i32(&pos_v)?;
587            let x = e.htod(&self.embd.gather(n_embd, tokens))?;
588            ph_mark(e, 0, &mut ph_last)?;
589            let x = self.step35_decode_batch_layers(
590                e, x, caches, &pos_d, 0, self.layers.len(), &mut ph_last)?;
591            let mut hn = e.uninit(b_n * n_embd)?;
592            e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, b_n, eps)?;
593            let logits = e.matmul(&self.output, &hn, b_n)?;
594            ph_mark(e, 10, &mut ph_last)?;
595            return self.decode_batch_epilogue(
596                e, caches, samp, masks, lean, logits, b_n, &mut ph_last);
597        }
598        let n_embd = self.cfg.n_embd as usize;
599        let eps = self.cfg.rms_eps;
600
601        // MEMRA_BATCH_PHASE=1: sync-bounded phase accumulation (diagnostics — see header note).
602        // Initialized BEFORE the tick-input assembly below so slot 0 covers the HOST side of
603        // setup (pos_v/ptr-table builds, embed gather) as well as the H2D sync — the audit-fix
604        // lane's Q6 instrumentation gap (research/audit-fixes2-20260805): the old placement
605        // started the clock after the assembly, so slot 0 under-reported setup.
606        let mut ph_last = std::time::Instant::now();
607
608        // Per-row rope positions (each sequence at its own depth).
609        let pos_v: Vec<i32> = caches.iter().map(|c| c.pos as i32).collect();
610        let pos_d = e.htod_i32(&pos_v)?;
611
612        // Per-step, whole-trunk layer context: state pointer table + arm picks. Under a pp
613        // split this call is made once PER STAGE with that stage's engine and range instead
614        // (see `batch_layer_ctx`'s doc for why the table cannot be shared across devices).
615        let n_layers = self.layers.len();
616        let ctx = self.batch_layer_ctx(e, caches, 0, n_layers)?;
617
618        // Embed all B tokens -> x [B, n_embd] (host gather, one H2D).
619        let x = e.htod(&self.embd.gather(n_embd, tokens))?;
620        ph_mark(e, 0, &mut ph_last)?;
621
622        let x = self.decode_batch_layers(e, x, caches, &ctx, &pos_d, &mut ph_last)?;
623
624        // ---- output norm + lm_head at m=B, one D2H ----
625        let mut hn = e.uninit(b_n * n_embd)?;
626        e.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, b_n, eps)?;
627        let logits = e.matmul(&self.output, &hn, b_n)?;
628        ph_mark(e, 10, &mut ph_last)?;
629
630        self.decode_batch_epilogue(e, caches, samp, masks, lean, logits, b_n, &mut ph_last)
631    }
632
633    /// THE BATCHED PP-N STEP (pp2-batch increment 2, 2026-08-06): the batched tick split
634    /// across `fence.len()-1` stages, each stage running ONLY its own layer range through
635    /// ITS OWN engine and stream, with a `[B, n_embd]` boundary activation between them.
636    /// The batched twin of `decode_step_h_ppn`, and the #1 item on the PP-2 serving bill —
637    /// without it a >VRAM SKU (Step-3.7-Flash: 105 GB, fits only across two cards) serves
638    /// SINGLE-STREAM only, because the batched path was the one loop with no stage split.
639    ///
640    /// STRUCTURE (mirrors the eager arm exactly, so the two stay comparable):
641    ///   stage 0        `rt.enter(0)` -> per-stage pos_d + embed -> range -> `rt.tx`
642    ///   middle stages  `rt.rx` -> per-stage pos_d -> range -> `rt.tx`
643    ///   last stage     `rt.rx` -> per-stage pos_d -> range -> output_norm + lm_head ->
644    ///                  the batched serving epilogue (masks, device sample, lean park)
645    ///
646    /// FOUR THINGS ARE PER-STAGE, and each is per-stage for a measured reason:
647    ///
648    /// 1. THE ENGINE (`rt.engine(s, e)`). Not just for the remote device: `Engine` owns
649    ///    lazily-grown stable-pointer scratch pools (`fa_part_pool`, `fa_vf16_scratch`,
650    ///    `argmax_partials`) that are single-stream-safe BY DESIGN. Two stage streams
651    ///    through one Engine is the shared-scratch race the pp2 lane hit (2026-08-02
652    ///    nondeterministic all-logits divergence, 35% flake). `PpNRt::build` already gives
653    ///    every stage s>0 its own Engine even on the primary device, so honouring
654    ///    `rt.engine(s, e)` here is what scopes the pools per stage — the batched path
655    ///    allocates MORE of that scratch than the eager one (fa at m=B), so this is the
656    ///    load-bearing half of the trap's mitigation, not an inherited nicety.
657    ///
658    /// 2. THE POINTER TABLE (`batch_layer_ctx(es, caches, lo, hi)`). See [`BatchLayerCtx`]:
659    ///    it holds DEVICE ADDRESSES of that range's cache state, uploaded through that
660    ///    stage's engine. One step-wide table on the primary would put every stage's kernel
661    ///    arguments in stage-0's HBM — a peer read per pointer fetch, the exact cliff this
662    ///    whole lane exists to remove.
663    ///
664    /// 3. `pos_d` (the M2 pipelining law, learned on the eager arm): each stage uploads its
665    ///    own copy of the step's per-row positions on ITS stream, so the buffer is
666    ///    allocated, consumed and freed on one stream. A shared stage-0 `pos_d` freed at fn
667    ///    return breaks under deferred readback — the free enqueues on stream 0 while later
668    ///    stages still dereference it.
669    ///
670    /// 4. THE HEAD + EPILOGUE run on the LAST stage: `output_norm`/`output` were uploaded
671    ///    through the last stage's engine by the sharded loader (`hybrid.rs`: `e_head =
672    ///    layer_engine(e, n_trunk, n_trunk-1)`), and `cache.last_logits_dev` must be
673    ///    allocated where the logits are.
674    ///
675    /// EXACTNESS: PP-N adds ZERO deviation. Each stage runs the SAME kernels on the SAME
676    /// bytes in the same order — the split only moves where the residual is materialized,
677    /// and the boundary is a straight f32 copy (dtod same-device / `cudaMemcpyPeerAsync`
678    /// cross-device, no conversion). So batched PP-N must be BIT-IDENTICAL to single-device
679    /// batched at the same B, in both placement orders. Gate: `decode-batch-gate --mode
680    /// pp` (logit-dump, both orders) — the batched analogue of the eager arm's 48 steps x
681    /// 248,320 f32 logits with zero differing bits.
682    ///
683    /// The B=1 fast path is NOT taken here (its condition already excludes an open door):
684    /// it routes through `decode_layers_eager` whole-trunk on one engine, which is exactly
685    /// the unsplit walk. B=1 under the door rides this function's B=1 case instead — the
686    /// same trade the eager arm's own ppn step makes, and the reason the pp2 lane measured
687    /// B=1 door-open at 0.854x (the lost fusion chain), not a cliff.
688    #[allow(clippy::too_many_arguments)]
689    fn decode_step_batch_ppn(
690        &self,
691        e: &Engine,
692        tokens: &[u32],
693        caches: &mut [&mut Cache],
694        samp: &[Option<(f32, u64, u32)>],
695        masks: &[Option<(&CudaSlice<u32>, usize)>],
696        lean: bool,
697        fence: &[usize],
698    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
699        let b_n = tokens.len();
700        assert!(b_n >= 1 && b_n == caches.len(), "tokens/caches length mismatch");
701        // gemma4: same no-arm refusal as the unsplit body (see decode_step_batch), Err not
702        // assert — a request must never kill the worker process.
703        if self.is_gemma4_e4b() || self.cfg.gemma4.is_some() {
704            return Err("decode_step_batch_ppn has no gemma4 arm — serve gemma4 on the eager \
705                        per-session path".into());
706        }
707        // Same width policy as the unsplit body — the stage split changes WHERE kernels run,
708        // never WHICH tier admits the width. Duplicated deliberately rather than hoisted:
709        // the exact-16 scope must wrap the whole multi-stage walk (`set_verify_exact` is
710        // per-Engine state read at dispatch on every stage), so it has to be established
711        // here, and a shared helper returning a guard would have to own `e` plus the flag.
712        let cap = Self::decode_batch_cap();
713        let exact16 = b_n > 8 && b_n <= 16 && self.decode_batch_exact16_ok();
714        assert!(
715            b_n <= cap || exact16,
716            "decode_step_batch_ppn: B={b_n} > cap {cap} with no exact tier — refused"
717        );
718        let rt = crate::pp::PpNRt::get(e)?;
719        let n_st = fence.len() - 1;
720        assert_eq!(
721            rt.n_stages(), n_st,
722            "PpNRt stage count {} != fence stages {n_st}", rt.n_stages()
723        );
724        // #87 REVERSE PUBLICATION (lane/pp2spec-crash): order every stage stream behind
725        // the caller before this body's first stage allocation can reuse a pool block
726        // whose queued primary-stream consumer has not read it yet. Anatomy:
727        // `PpNRt::fence_stages_behind`. (This body dtoh+syncs its own logits, but its
728        // PP-mode callers interleave with the spec verify's device-resident outputs in
729        // the same worker, so the entry fence is the uniform law, not an optimization.)
730        rt.fence_stages_behind(&e.stream())?;
731        let n_embd = self.cfg.n_embd as usize;
732        let eps = self.cfg.rms_eps;
733        let payload = b_n * n_embd;
734
735        // EXACT-16 SCOPE, PER STAGE ENGINE: `verify_exact` is per-Engine state (an AtomicBool
736        // on the Engine the dispatch reads), and each stage runs through a DIFFERENT Engine —
737        // so setting it on the primary alone would leave stages 1..N-1 dispatching the m>=16
738        // GEMM/MMQ arms while stage 0 used the exact b16 tier. That is a silent per-stage
739        // numeric split (the failure this tier exists to prevent), so the flag is set on
740        // every stage engine and cleared on all of them at scope exit.
741        struct ExactScopeN<'a>(Vec<&'a Engine>);
742        impl Drop for ExactScopeN<'_> {
743            fn drop(&mut self) {
744                for eng in &self.0 {
745                    eng.set_verify_exact(false);
746                }
747            }
748        }
749        let _exact_scope = if exact16 {
750            let engines: Vec<&Engine> = (0..n_st).map(|s| rt.engine(s, e)).collect();
751            for eng in &engines {
752                eng.set_verify_exact(true);
753            }
754            Some(ExactScopeN(engines))
755        } else {
756            None
757        };
758
759        let mut ph_last = std::time::Instant::now();
760
761        // B=1 PER-STAGE FAST PATH (measured 2026-08-06, PRO 6000 pair). The unsplit body's
762        // b1_fast guard includes `pp_cuts().is_none()`, so opening the pp door dropped every
763        // solo session off the m=1 FUSION chain (cross-layer add+norm+q8_1, fused SwiGLU,
764        // lever 1's gate+up dual) and onto the batched m=1 walk. Cost, arm A vs arm C at B=1:
765        // 208.5 vs 177.3 tok/s = -15.0% — and NOT a split cost, since arm B (stages=2 on ONE
766        // card) pays the same 177, and the prior lane's `MEMRA_PP_SHARD=0` batched-body B=1
767        // was 178.5. It was the fusion chain going missing, on the config the Step SKU serves
768        // solo requests from.
769        //
770        // `decode_layers_eager(lo, hi)` is ALREADY range-scoped and is exactly what the eager
771        // ppn arm (`decode_step_h_ppn`) calls per stage, so B=1 rides the same per-stage
772        // structure: same engines, same streams, same [1, n_embd] boundary slots, same
773        // stage-owned caches. Only the trunk kernels differ, and they differ identically to
774        // how they differ off-door. Exactness is therefore the SAME accepted decode-config FP
775        // class the unsplit b1_fast lever already carries (strict gate1 PASSes with it on,
776        // FAILs with it off at maxdiff 1.591e-1) — which is why the pp gate pins
777        // `set_b1_fast(false)`: with it on, the B=1 reference and the split arm would
778        // legitimately sit on opposite sides of that gap and the bit-identity arm would
779        // report a fake stage-split failure.
780        //
781        // Step3.5/Step3.7 are the exception (lane/cx-b1fix, 2026-08-10): their B>1 route is
782        // `step35_decode_batch_layers`, and the live scheduler may move a session from B=1
783        // to B>1. The eager/fused class and that batched class produce different greedy bytes,
784        // so selecting the eager arm at B=1 made output depend on load history. Keep one
785        // numeric class for this model family: Step35 always takes its stage-scoped batched
786        // trunk at every width. The live transition gate in step35-b2-geometry-gate pins it.
787        let b1_stage_fast = b_n == 1
788            && Self::b1_fast_on()
789            && !self.is_gemma4_e4b()
790            && self.cfg.gemma4.is_none()
791            && self.cfg.m3.is_none()
792            && self.cfg.step35.is_none()
793            && !e.verify_exact_on();
794        // step35 (lane/step35-batched-decode, 2026-08-08): B>1 rides its OWN stage-scoped
795        // batched walk (`step35_decode_batch_layers`) — the generic `decode_batch_layers`
796        // remains OFF-LIMITS for this arch at every B (its uniform geometry produced the
797        // b2ab HTTP-200 garbage: research/step-sku-20260807/raw/b2ab-pre-*.log). Since
798        // lane/cx-b1fix, B=1 also takes this walk: a Step35 PP-N session must not change
799        // numeric class when live decode width changes. The refusal below guards the
800        // rollback residue; under PP-N, disabling the only correct trunk makes Step35
801        // requests fail closed instead of falling back to the eager class.
802        let step35_batched = self.cfg.step35.is_some();
803        if step35_batched && !Self::step35_batch_on() {
804            return Err("step35 batched decode is disabled (MEMRA_STEP35_BATCH=0) — \
805                        PP-N Step35 decode is unavailable because eager B=1 is a different \
806                        numeric class".into());
807        }
808        // Hoisted: `caches[0].pos` as a value argument alongside `caches[0]` as `&mut` in one
809        // call is a borrow conflict; `pos` is Copy and the epilogue is what advances it.
810        let pos0 = if b1_stage_fast { caches[0].pos } else { 0 };
811
812        // ---- STAGE 0: embed (the table lives with stage 0) + layers [0, fence[1]) + TX ----
813        let mut slot = {
814            let _st0 = rt.enter(0);
815            let e0 = rt.engine(0, e);
816            let pos_v: Vec<i32> = caches.iter().map(|c| c.pos as i32).collect();
817            let pos_d = e0.htod_i32(&pos_v)?;
818            let x = e0.htod(&self.embd.gather(n_embd, tokens))?;
819            ph_mark(e0, 0, &mut ph_last)?;
820            let x = if b1_stage_fast {
821                self.decode_layers_eager(e0, x, fence[0], fence[1], &pos_d, pos0, caches[0])?
822            } else if step35_batched {
823                self.step35_decode_batch_layers(
824                    e0, x, caches, &pos_d, fence[0], fence[1], &mut ph_last)?
825            } else {
826                let ctx = self.batch_layer_ctx(e0, caches, fence[0], fence[1])?;
827                self.decode_batch_layers(e0, x, caches, &ctx, &pos_d, &mut ph_last)?
828            };
829            rt.tx(0, &x, payload)?
830            // x + pos_d + ctx.ptr_table drop here: freed stream-ordered on stage-0's stream.
831        };
832
833        // ---- MIDDLE STAGES: RX boundary s-1 -> range -> TX boundary s ----
834        for s in 1..n_st - 1 {
835            let _st = rt.enter(s);
836            let es = rt.engine(s, e);
837            let pos_v: Vec<i32> = caches.iter().map(|c| c.pos as i32).collect();
838            let pos_d = es.htod_i32(&pos_v)?;
839            let x = rt.rx(s - 1, slot, payload)?;
840            let x = if b1_stage_fast {
841                self.decode_layers_eager(es, x, fence[s], fence[s + 1], &pos_d, pos0, caches[0])?
842            } else if step35_batched {
843                self.step35_decode_batch_layers(
844                    es, x, caches, &pos_d, fence[s], fence[s + 1], &mut ph_last)?
845            } else {
846                let ctx = self.batch_layer_ctx(es, caches, fence[s], fence[s + 1])?;
847                self.decode_batch_layers(es, x, caches, &ctx, &pos_d, &mut ph_last)?
848            };
849            slot = rt.tx(s, &x, payload)?;
850        }
851
852        // ---- LAST STAGE: RX + final range + head + the batched serving epilogue ----
853        let _stl = rt.enter(n_st - 1);
854        let el = rt.engine(n_st - 1, e);
855        let pos_v: Vec<i32> = caches.iter().map(|c| c.pos as i32).collect();
856        let pos_d = el.htod_i32(&pos_v)?;
857        let x = rt.rx(n_st - 2, slot, payload)?;
858        let x = if b1_stage_fast {
859            self.decode_layers_eager(el, x, fence[n_st - 1], fence[n_st], &pos_d, pos0, caches[0])?
860        } else if step35_batched {
861            self.step35_decode_batch_layers(
862                el, x, caches, &pos_d, fence[n_st - 1], fence[n_st], &mut ph_last)?
863        } else {
864            let ctx = self.batch_layer_ctx(el, caches, fence[n_st - 1], fence[n_st])?;
865            self.decode_batch_layers(el, x, caches, &ctx, &pos_d, &mut ph_last)?
866        };
867
868        let mut hn = el.uninit(payload)?;
869        el.rms_norm(&x, self.output_norm.float_data(), &mut hn, n_embd, b_n, eps)?;
870        let logits = el.matmul(&self.output, &hn, b_n)?;
871        ph_mark(el, 10, &mut ph_last)?;
872
873        self.decode_batch_epilogue(el, caches, samp, masks, lean, logits, b_n, &mut ph_last)
874    }
875
876    /// Build the per-step layer context for layers `[lo, hi)`: the device state-pointer
877    /// table plus the step's arm picks. See [`BatchLayerCtx`] for why this is RANGE-scoped
878    /// (the table holds device addresses and must be uploaded through the engine whose
879    /// device runs those layers).
880    ///
881    /// Table layout is unchanged from the whole-trunk version — `lin_base`/`attn_base` are
882    /// still indexed by ABSOLUTE layer id, so `decode_batch_layers`' body indexes them
883    /// exactly as the old inline loop did. Only layers in `[lo, hi)` contribute entries; the
884    /// rest stay `None`, which is a loud `expect` if a range ever reads outside its own.
885    pub(crate) fn batch_layer_ctx(
886        &self,
887        e: &Engine,
888        caches: &[&mut Cache],
889        lo: usize,
890        hi: usize,
891    ) -> Result<BatchLayerCtx, Box<dyn std::error::Error>> {
892        let cfg = &self.cfg;
893        let head_dim = cfg.head_dim_k as usize;
894        // Per-step STATE POINTER TABLE (one H2D): for every linear layer, [conv x B]
895        // [ssm_in x B][ssm_out x B] device addresses. The batched state kernels read their
896        // sequence's pointer from these arrays — states stay per-cache (no pooling refactor),
897        // yet conv/prep/scan collapse from 3xB launches per layer to 3. Rebuilt every step
898        // because the ssm ping-pong swaps pointers host-side after each scan.
899        // INCREMENT 2 (2026-08-01): the SAME table now also carries, for every FULL-attn
900        // layer, [k0,v0,k1,v1,...] cache base addresses — the z-batched seqs append and
901        // seqs fa_decode kernels read their sequence's cache through it (the MoE
902        // expert-table pattern), collapsing 2xB launches per attn layer to 2.
903        let mut lin_base: Vec<Option<usize>> = vec![None; self.layers.len()];
904        let mut attn_base: Vec<Option<usize>> = vec![None; self.layers.len()];
905        let mut ptrs: Vec<u64> = Vec::new();
906        {
907            use cudarc::driver::DevicePtr;
908            let s = &e.gpu.stream();
909            for il in lo..hi {
910                match &self.layers[il].mixer {
911                    Mixer::Linear(_) => {
912                        lin_base[il] = Some(ptrs.len());
913                        for c in caches.iter() {
914                            let rl = c.recur[il].as_ref().unwrap();
915                            let (p, _g) = rl.conv_state.device_ptr(s);
916                            ptrs.push(p as u64);
917                        }
918                        for c in caches.iter() {
919                            let rl = c.recur[il].as_ref().unwrap();
920                            let (p, _g) = rl.ssm_state.device_ptr(s);
921                            ptrs.push(p as u64);
922                        }
923                        for c in caches.iter() {
924                            let rl = c.recur[il].as_ref().unwrap();
925                            let (p, _g) = rl.ssm_state_alt.device_ptr(s);
926                            ptrs.push(p as u64);
927                        }
928                    }
929                    Mixer::Full(_) => {
930                        attn_base[il] = Some(ptrs.len());
931                        for c in caches.iter() {
932                            let kvl = c.kv[il].as_ref().unwrap();
933                            let (pk, _g) = kvl.k.device_ptr(s);
934                            let (pv, _g2) = kvl.v.device_ptr(s);
935                            ptrs.push(pk as u64);
936                            ptrs.push(pv as u64);
937                        }
938                    }
939                    Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
940                }
941            }
942        }
943        let ptr_table = if ptrs.is_empty() { None } else { Some(e.htod_u64(&ptrs)?) };
944
945        // INCREMENT 2 arm picks (per STEP — t_kv is layer-invariant within a tick):
946        // - seqs APPEND: format-only condition (per-row program is t_kv-independent);
947        //   default flash module only (fp8-KV rides the per-seq g-module path).
948        // - seqs FA: every row must take the v4 eager arm at ITS OWN t_kv AND all rows
949        //   must share ONE fa_split_keys rung (the rows-twins' straddle law) — a rung
950        //   crossing inside the batch keeps the per-seq loop for that step, so each
951        //   sequence always executes the exact program its isolated run would.
952        // MEMRA_BATCH_APPEND=0 / MEMRA_BATCH_FA=0 are the rollback/A-B seams.
953        //
954        // The picks are t_kv-driven, and t_kv is layer-INVARIANT within a step, so every
955        // stage of a pp split independently computes the SAME arms from the same `caches`
956        // — a stage cannot silently take a different program than its unsplit self.
957        let t_kvs: Vec<usize> = caches.iter().map(|c| c.pos + 1).collect();
958        let t_kv_max = *t_kvs.iter().max().unwrap();
959        let seqs_append = {
960            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
961            *ON.get_or_init(|| std::env::var("MEMRA_BATCH_APPEND").as_deref() != Ok("0"))
962        } && !Engine::kv_fp8_on();
963        let sp0 = crate::fa_split_keys(t_kvs[0], cfg.n_head_kv as usize);
964        let seqs_fa = {
965            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
966            *ON.get_or_init(|| std::env::var("MEMRA_BATCH_FA").as_deref() != Ok("0"))
967        } && t_kvs.iter().all(|&t| crate::fa_seqs_eligible(t, head_dim))
968          && t_kvs.iter().all(|&t| crate::fa_split_keys(t, cfg.n_head_kv as usize) == sp0);
969
970        Ok(BatchLayerCtx {
971            lin_base,
972            attn_base,
973            ptr_table,
974            t_kvs,
975            t_kv_max,
976            sp0,
977            seqs_append,
978            seqs_fa,
979            lo,
980            hi,
981        })
982    }
983
984    /// THE PP SEAM (pp2-batch increment 1, 2026-08-06): run the batched trunk over layers
985    /// `[ctx.lo, ctx.hi)`, entering with a materialized `[B, n_embd]` residual and exiting
986    /// with the range's final residual materialized. The batched twin of
987    /// `decode_layers_eager` — the eager arm has had this seam since M1-PP2 and every ppN
988    /// stage calls it; the batched body had no equivalent, which is why every later PP-2
989    /// increment (and spec-over-PP2, whose verify is a batched T=K+1 forward) waited on this
990    /// extraction (`research/pp2-hardening-20260806/PROGRESS.md` bill item 1).
991    ///
992    /// SINGLE-DEVICE SEMANTICS ARE UNCHANGED BY CONSTRUCTION: the body is the old
993    /// `for (il, layer) in self.layers.iter().enumerate()` loop moved verbatim, with `for il
994    /// in ctx.lo..ctx.hi` as the header and the per-step invariants (`ptr_table`, arm picks,
995    /// `t_kv`) read from `ctx` instead of enclosing locals. At `lo=0, hi=n_layers` — every
996    /// call today — the launch sequence is identical, so the exactness contract in this
997    /// module's header carries over untouched rather than needing a re-proof.
998    ///
999    /// UNLIKE the eager seam, this one is NOT yet stage-callable: `caches` is `&mut [&mut
1000    /// Cache]` mutated in place (KV `len` bumps, ssm ping-pong swaps), and `pos_d`/`x` come
1001    /// from the caller's device. Wiring a stage split means per-stage `pos_d` + a boundary
1002    /// `[B, n_embd]` transfer around this call, which is the NEXT increment. The seam exists
1003    /// so that increment is a call-site change, not a 250-line surgery.
1004    #[allow(clippy::too_many_arguments)]
1005    pub(crate) fn decode_batch_layers(
1006        &self,
1007        e: &Engine,
1008        mut x: CudaSlice<f32>,
1009        caches: &mut [&mut Cache],
1010        ctx: &BatchLayerCtx,
1011        pos_d: &CudaSlice<i32>,
1012        ph_last: &mut std::time::Instant,
1013    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1014        let b_n = caches.len();
1015        let cfg = &self.cfg;
1016        let n_embd = cfg.n_embd as usize;
1017        let eps = cfg.rms_eps;
1018        let (lin_base, attn_base) = (&ctx.lin_base, &ctx.attn_base);
1019        let ptr_table = &ctx.ptr_table;
1020        let (seqs_append, seqs_fa, sp0, t_kv_max) =
1021            (ctx.seqs_append, ctx.seqs_fa, ctx.sp0, ctx.t_kv_max);
1022        debug_assert_eq!(ctx.t_kvs.len(), b_n, "ctx built for a different batch width");
1023
1024        for il in ctx.lo..ctx.hi {
1025            let layer = &self.layers[il];
1026            // ---- attn_norm + q8_1 quantize, batched (B rows) ----
1027            let anorm = layer.attn_norm.float_data();
1028            let mut xn = e.uninit(b_n * n_embd)?;
1029            e.rms_norm(&x, anorm, &mut xn, n_embd, b_n, eps)?;
1030            let (hq, hd) = e.quantize_q8_1(&xn, b_n, n_embd)?;
1031
1032            // ---- mixer ----
1033            let mixed: CudaSlice<f32> = match &layer.mixer {
1034                Mixer::Mla(_) => crate::hybrid::mla_forward_unimplemented(),
1035                Mixer::Full(fa) => {
1036                    let geometry = cfg.full_attention_geometry_at(il as u32);
1037                    let n_head = geometry.n_head as usize;
1038                    let n_head_kv = geometry.n_head_kv as usize;
1039                    let head_dim = geometry.head_dim_k as usize;
1040                    let rope_dims = geometry.n_rot as usize;
1041                    let rope_base = geometry.rope_base;
1042                    let scale = geometry.attention_scale();
1043                    // Batched projections: one weight read serves all B rows.
1044                    let qf = e.matmul_pre(&fa.wq, &hq, &hd, &xn, b_n)?;
1045                    let mut k = e.matmul_pre(&fa.wk, &hq, &hd, &xn, b_n)?;
1046                    let v = e.matmul_pre(&fa.wv, &hq, &hd, &xn, b_n)?;
1047
1048                    let gated = geometry.attention_gate
1049                        == memra_gguf::config::AttentionGateKind::FusedQ;
1050                    let (mut q, gate) = if gated {
1051                        let mut qs = e.uninit(b_n * n_head * head_dim)?;
1052                        let mut gs = e.uninit(b_n * n_head * head_dim)?;
1053                        e.q_gate_split(&qf, &mut qs, &mut gs, head_dim, n_head, b_n)?;
1054                        (qs, Some(gs))
1055                    } else {
1056                        (qf, None)
1057                    };
1058
1059                    // QK-norm over B*n_head rows, rope with per-row positions.
1060                    let mut qn = e.uninit(b_n * n_head * head_dim)?;
1061                    e.rms_norm(&q, fa.q_norm.float_data(), &mut qn, head_dim, b_n * n_head, eps)?;
1062                    q = qn;
1063                    let mut kn = e.uninit(b_n * n_head_kv * head_dim)?;
1064                    e.rms_norm(&k, fa.k_norm.float_data(), &mut kn, head_dim, b_n * n_head_kv, eps)?;
1065                    k = kn;
1066                    e.rope_neox(&mut q, &pos_d, head_dim, rope_dims, n_head, b_n,
1067                                rope_base, 1.0)?;
1068                    e.rope_neox(&mut k, &pos_d, head_dim, rope_dims, n_head_kv, b_n,
1069                                rope_base, 1.0)?;
1070                    ph_mark(e, 1, ph_last)?;
1071
1072                    // INCREMENT 2 (2026-08-01): the per-seq (append, attend) launch train
1073                    // becomes two phases. Phase A appends all B rows (one z-batched launch,
1074                    // or the per-seq loop on the seam/fp8 path); phase B attends all B
1075                    // sequences (one blockIdx.z launch + one combine on the batched arm —
1076                    // which also reads q / writes attn at row offsets, killing the per-seq
1077                    // q/a dtod copies — or the per-seq loop when any row is outside the v4
1078                    // arm / a split rung crosses inside the batch). Caches are disjoint per
1079                    // sequence, so the phase split leaves every row's math untouched.
1080                    let q_dim = n_head * head_dim;
1081                    let kv_dim = n_head_kv * head_dim;
1082                    let mut attn = e.uninit(b_n * q_dim)?;
1083                    // ---- phase A: KV append (all B rows) ----
1084                    if seqs_append {
1085                        let (kdk, kdv, ktb, vtb) = {
1086                            let kvl = caches[0].kv[il].as_ref().unwrap();
1087                            (kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes)
1088                        };
1089                        let base = attn_base[il].expect("full layer missing from pointer table");
1090                        let table = ptr_table.as_ref().expect("pointer table missing");
1091                        let kv_view = table.slice(base..base + 2 * b_n);
1092                        e.append_kv_quantized_seqs(&k, &v, &kv_view, &pos_d, b_n,
1093                                                   kdk, kdv, ktb, vtb)?;
1094                        for cache in caches.iter_mut() {
1095                            let kvl = cache.kv[il].as_mut().unwrap();
1096                            debug_assert_eq!(kvl.len, cache.pos, "kv len / pos out of lockstep");
1097                            kvl.len += 1;
1098                        }
1099                    } else {
1100                        for (bi, cache) in caches.iter_mut().enumerate() {
1101                            let kvl = cache.kv[il].as_mut().unwrap();
1102                            let k_row = k.slice(bi * kv_dim..(bi + 1) * kv_dim);
1103                            let v_row = v.slice(bi * kv_dim..(bi + 1) * kv_dim);
1104                            e.append_kv_quantized_view(
1105                                &k_row, &v_row, &mut kvl.k, &mut kvl.v, kvl.len,
1106                                kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes,
1107                                Engine::kv_fp8_on(),
1108                            )?;
1109                            kvl.len += 1;
1110                        }
1111                    }
1112                    ph_mark(e, 2, ph_last)?;
1113                    // ---- phase B: attention (all B sequences) ----
1114                    if seqs_fa {
1115                        let (ktb, vtb) = {
1116                            let kvl = caches[0].kv[il].as_ref().unwrap();
1117                            (kvl.k_tok_bytes, kvl.v_tok_bytes)
1118                        };
1119                        let base = attn_base[il].expect("full layer missing from pointer table");
1120                        let table = ptr_table.as_ref().expect("pointer table missing");
1121                        let kv_view = table.slice(base..base + 2 * b_n);
1122                        e.fa_decode_batch_seqs_v4(&q, &kv_view, &pos_d, &mut attn,
1123                                                  head_dim, n_head, n_head_kv, b_n,
1124                                                  t_kv_max, scale, sp0, ktb, vtb)?;
1125                        ph_mark(e, 4, ph_last)?;
1126                    } else {
1127                        for (bi, cache) in caches.iter_mut().enumerate() {
1128                            let kvl = cache.kv[il].as_mut().unwrap();
1129                            let t_kv = kvl.len;
1130                            let k_view = e.view_u8(&kvl.k, t_kv * kvl.k_tok_bytes);
1131                            let v_view = e.view_u8(&kvl.v, t_kv * kvl.v_tok_bytes);
1132                            // fa_decode wants a q slice starting at row bi: the fallback arm
1133                            // scratch-copies the row (q8-class µs cost); the seqs arm above
1134                            // reads/writes row offsets in place.
1135                            let mut q_row = e.uninit(q_dim)?;
1136                            e.dtod_copy_view(&q.slice(bi * q_dim..(bi + 1) * q_dim), &mut q_row)?;
1137                            ph_mark(e, 3, ph_last)?;
1138                            let mut a_row = e.uninit(q_dim)?;
1139                            e.fa_decode_kvmod(
1140                                &q_row, &k_view, &v_view, &mut a_row, head_dim, n_head, n_head_kv,
1141                                t_kv, scale, kvl.k_tok_bytes, kvl.v_tok_bytes, Engine::kv_fp8_on(),
1142                            )?;
1143                            ph_mark(e, 4, ph_last)?;
1144                            e.dtod_copy_into(&a_row, &mut attn, bi * q_dim)?;
1145                            ph_mark(e, 3, ph_last)?;
1146                        }
1147                    }
1148
1149                    // Output gate (element-wise — batches whole) + o-proj at m=B.
1150                    let attn_g = match &gate {
1151                        Some(g) => {
1152                            let n = b_n * q_dim;
1153                            let mut gsig = e.uninit(n)?;
1154                            e.sigmoid(g, &mut gsig, n)?;
1155                            let mut ag = e.uninit(n)?;
1156                            e.mul(&attn, &gsig, &mut ag, n)?;
1157                            ag
1158                        }
1159                        None => attn,
1160                    };
1161                    let o = e.matmul(&fa.wo, &attn_g, b_n)?;
1162                    ph_mark(e, 5, ph_last)?;
1163                    o
1164                }
1165                Mixer::Linear(la) => {
1166                    // v2 (the B-scaling fix): the GDN mixer's PROJECTIONS carry the layer's
1167                    // weight mass — batch them at m=B so wqkv/gate/beta/alpha/ssm_out stream
1168                    // ONCE per step instead of once per sequence. Only the recurrent state ops
1169                    // (fused conv ring, gdn prep, gdn scan) stay per-seq — they are state-bound
1170                    // micro-kernels, not weight readers. Composition unchanged vs v1 (matmul_pre
1171                    // == fused2 per (tensor,row); _bN mmvq per-row == m=1): same numeric config.
1172                    let ssm = cfg.ssm.as_ref().expect("linear mixer requires ssm cfg");
1173                    let d_state = ssm.state_size as usize;
1174                    let num_k = ssm.group_count as usize;
1175                    let num_v = ssm.time_step_rank as usize;
1176                    let d_conv = ssm.conv_kernel as usize;
1177                    let key_dim = d_state * num_k;
1178                    let value_dim = d_state * num_v;
1179                    let conv_dim = key_dim * 2 + value_dim;
1180                    let gdn_scale = 1.0 / (d_state as f32).sqrt();
1181
1182                    // ---- batched projections (the weight win) ----
1183                    let qkv_mixed = e.matmul_pre(&la.wqkv, &hq, &hd, &xn, b_n)?;
1184                    let z = e.matmul_pre(&la.wqkv_gate, &hq, &hd, &xn, b_n)?;
1185                    let beta_raw = e.matmul_pre(&la.ssm_beta, &hq, &hd, &xn, b_n)?;
1186                    let alpha = e.matmul_pre(&la.ssm_alpha, &hq, &hd, &xn, b_n)?;
1187                    ph_mark(e, 6, ph_last)?;
1188
1189                    // ---- batched recurrent state ops (3 launches for all B sequences) ----
1190                    let base = lin_base[il].expect("linear layer missing from pointer table");
1191                    let table = ptr_table.as_ref().expect("pointer table missing");
1192                    let conv_view = table.slice(base..base + b_n);
1193                    let in_view = table.slice(base + b_n..base + 2 * b_n);
1194                    let out_view = table.slice(base + 2 * b_n..base + 3 * b_n);
1195                    let mut conv_outs = e.uninit(b_n * conv_dim)?;
1196                    e.ssm_conv1d_fused_decode_b(&qkv_mixed, &conv_view,
1197                                                la.ssm_conv1d.float_data(), &mut conv_outs,
1198                                                conv_dim, d_conv, b_n)?;
1199                    let mut q_l2 = e.uninit(b_n * value_dim)?;
1200                    let mut k_l2 = e.uninit(b_n * value_dim)?;
1201                    let mut v_gd = e.uninit(b_n * value_dim)?;
1202                    let mut beta_b = e.uninit(b_n * num_v)?;
1203                    let mut g_log = e.uninit(b_n * num_v)?;
1204                    e.gdn_prep_decode_b(&conv_outs, &beta_raw, &alpha,
1205                                        la.ssm_dt.float_data(), la.ssm_a.float_data(),
1206                                        &mut q_l2, &mut k_l2, &mut v_gd, &mut beta_b, &mut g_log,
1207                                        d_state, num_v, num_k, key_dim, eps, conv_dim, b_n)?;
1208                    let mut o_all = e.uninit(b_n * value_dim)?;
1209                    e.gdn_scan_s128_batched(&q_l2, &k_l2, &v_gd, &g_log, &beta_b,
1210                                            &in_view, &out_view, &mut o_all,
1211                                            num_v, b_n, gdn_scale)?;
1212                    // ping-pong: scan wrote each seq's alt buffer; swap host handles (the
1213                    // NEXT step's table rebuild picks up the new canonical pointers).
1214                    for cache in caches.iter_mut() {
1215                        let rl = cache.recur[il].as_mut().unwrap();
1216                        std::mem::swap(&mut rl.ssm_state, &mut rl.ssm_state_alt);
1217                    }
1218                    ph_mark(e, 7, ph_last)?;
1219
1220                    // ---- batched gated norm + out-projection ----
1221                    let o = if e.uses_q8_1_fast(&la.ssm_out) {
1222                        let (gq, gd) = e.gated_rmsnorm_q8_1(&o_all, la.ssm_norm.float_data(),
1223                                                            &z, d_state, b_n * num_v, eps)?;
1224                        let g0 = e.zeros(0)?;
1225                        e.matmul_pre(&la.ssm_out, &gq, &gd, &g0, b_n)?
1226                    } else {
1227                        let mut gn = e.uninit(b_n * value_dim)?;
1228                        e.gated_rmsnorm(&o_all, la.ssm_norm.float_data(), &z, &mut gn,
1229                                        d_state, b_n * num_v, eps)?;
1230                        e.matmul(&la.ssm_out, &gn, b_n)?
1231                    };
1232                    ph_mark(e, 8, ph_last)?;
1233                    o
1234                }
1235            };
1236
1237            // ---- residual add + post_attn_norm + FFN, batched ----
1238            let pnorm = layer.post_attn_norm.float_data();
1239            let mut x1 = e.uninit(b_n * n_embd)?;
1240            let mut z = e.uninit(b_n * n_embd)?;
1241            e.add_rms_norm(&x, &mixed, pnorm, &mut x1, &mut z, n_embd, b_n, eps)?;
1242            let ffn_out = match &layer.ffn {
1243                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
1244                    // v1 covers the SiLU family; M3's swigluoai clamp rides a scaled epilogue
1245                    // (m=1 fused tier) — batched M3 lands with the batched-fusion pass.
1246                    assert!(self.cfg.m3.is_none(),
1247                            "decode_step_batch v1: M3 swigluoai FFN not yet batched");
1248                    let n_ff = ffn_gate.out_features();
1249                    let (zq, zd) = e.quantize_q8_1(&z, b_n, n_embd)?;
1250                    // REFUTED ARM (lane/q27-deepdive, 2026-08-05): fusing this gate+up pair
1251                    // into `matmul_q8_fused2_t` (the fused2_b8 tier) measured FLAT-TO-NEGATIVE
1252                    // at the serving tick — bench c=8 213.1/213.8, 213.9/214.4, 214.4/213.5
1253                    // (sign flips) and serve c=8 paired mean −0.20% over 3 passes. Mechanism:
1254                    // unlike m=1 (where the pair is 128 of 1015 launches in a 7.67%-gap tick),
1255                    // the c=8 tick is 73.2% one weight-bound kernel class with launch cost
1256                    // already hidden — halving 128 launches of ~28k buys nothing. The m=1 arm
1257                    // in `matmul_pre_dual_noscale` (+0.94%) stays; this call site keeps the two
1258                    // launches. Kernel + fused2_b8 wrapper retained: kernel-check gates it at
1259                    // m=5/8 and matmul_q8_fused2_t serves the verify tier. Receipts:
1260                    // research/q27-deepdive-20260805/ (lever3-bench-*, serve-points.jsonl).
1261                    let g = e.matmul_pre(ffn_gate, &zq, &zd, &z, b_n)?;
1262                    let u = e.matmul_pre(ffn_up, &zq, &zd, &z, b_n)?;
1263                    let mut act = e.uninit(b_n * n_ff)?;
1264                    e.silu_mul(&g, &u, &mut act, b_n * n_ff)?;
1265                    let (aq, ad) = e.quantize_q8_1(&act, b_n, n_ff)?;
1266                    e.matmul_pre(ffn_down, &aq, &ad, &act, b_n)?
1267                }
1268                crate::hybrid::Ffn::Moe(m) => self.moe_ffn_il_zq8(e, m, &z, None, b_n, il as u16)?,
1269            };
1270            // next-layer input x = x1 + ffn_out (batched element-wise add)
1271            let mut x2 = e.uninit(b_n * n_embd)?;
1272            e.add(&x1, &ffn_out, &mut x2, b_n * n_embd)?;
1273            x = x2;
1274            ph_mark(e, 9, ph_last)?;
1275        }
1276        Ok(x)
1277    }
1278
1279    /// Rollback seam for the step35 batched decode arm (lane/step35-batched-decode,
1280    /// 2026-08-08). Default ON; `MEMRA_STEP35_BATCH=0` caps serving at B=1 and makes the
1281    /// batched bodies return Err. Since lane/cx-b1fix, PP-N also refuses the eager B=1
1282    /// numeric class, so the seam disables PP-N Step35 decode rather than serving unstable
1283    /// bytes. Also the b2geo35 gate's CANARY seam — the live assertions must fail under it.
1284    pub fn step35_batch_on() -> bool {
1285        static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1286        *ON.get_or_init(|| std::env::var("MEMRA_STEP35_BATCH").as_deref() != Ok("0"))
1287    }
1288
1289    /// THE step35 BATCHED LAYER WALK (lane/step35-batched-decode, 2026-08-08): B sequences
1290    /// share one pass over layers `[lo, hi)` with the REAL step35 geometry — the arm that
1291    /// kills the B=1 pin (34 tok/s aggregate FLAT across c=1..8, round-robin serialized;
1292    /// research/step-sku-20260807 §4) without re-opening the b2ab garbage hole (the generic
1293    /// `decode_batch_layers` ran uniform n_head/full-width rope/no window/no gate over
1294    /// step35 weights and returned HTTP-200 garbage at c>1).
1295    ///
1296    /// SHAPE — batched where the weights are, per-session where the state is:
1297    ///   * attn_norm + quantize + wq/wk/wv/attn_gate projections + q/k norms + rope + head
1298    ///     gate + wo + residual/post-norm + FFN all run at m=B: ONE weight stream serves B
1299    ///     rows (decode is weight-BW-bound; this is the entire win).
1300    ///   * KV append + fa_decode stay a per-session loop — the SWA window makes each
1301    ///     session's KV view a function of ITS OWN `kvl.len` (`off = len-win` when past the
1302    ///     window), and the z-batched seqs kernels take one shared t_kv/rung, not per-row
1303    ///     offsets. This is the same shape as `decode_batch_layers`' per-seq fallback arm,
1304    ///     and it costs launches, not weight bandwidth (KV is per-session state either way).
1305    ///
1306    /// PER-LAYER GEOMETRY (the five mechanisms that make the generic body wrong here, all
1307    /// from `step35_geom`/cfg): n_head 64 full / 96 SWA (wq/wo/attn_gate widths per layer),
1308    /// partial rope (n_rot 64 full / 128 SWA), dual base (5e6/1e4) + `rope_freqs` factors
1309    /// on FULL layers only, SWA window 512 with per-SESSION view offsets, and the separate
1310    /// head-wise `attn_gate` (one pre-sigmoid scalar per (token, head), input = the
1311    /// post-attn_norm hidden, applied before wo).
1312    ///
1313    /// EXACTNESS (the isolation contract, decode-batch-gate gate2's bar): every kernel here
1314    /// is row-independent at m=B or per-session:
1315    ///   * `rms_norm`/`add_rms_norm`/`quantize_q8_1`/`attn_head_gate`/activations: per-row
1316    ///     programs, grid over rows — row bi's bytes are the 1-row call's bytes.
1317    ///   * projections via `matmul_pre` at m=2..8: Q8_0/Q6_K-class rides the b2/b4/b8
1318    ///     batched-mmvq tier (bit-identical per (token,row) to m=1 mmvq); IQ4_XS — this
1319    ///     SKU's trunk class — has no mmvq/batched kernel, so BOTH m=1 decode and the m=B
1320    ///     walk ride `qmatvec_iq4_XS_dp4a` (grid (out_f, m): each column IS the m=1 dp4a
1321    ///     program). Same class at every width = the decode-parity law by construction.
1322    ///   * `rope_neox2` takes per-row positions (tok = row / n_heads) — row bi rotates at
1323    ///     ITS pos with the layer's (n_rot, base, ff), same bits as its solo call.
1324    ///   * per-session append/fa_decode_kvmod: literally the eager arm's calls on that
1325    ///     session's own cache and views.
1326    ///   * MoE (`moe_ffn_il_zq8` at t=B): the router is per-column decode-exact at
1327    ///     t < PRIME_MIN_T (m=1 program per column), sigmoid routing + expert dispatch are
1328    ///     per-token — a session's experts are a function of its own row only.
1329    /// The known eager-vs-batched FP gap is why PP-N Step35 deliberately serves THIS walk at
1330    /// B=1 too: the scheduler can change width during a session, so one numeric class must
1331    /// cover every live width. `b2geo35` pins static widths and an explicit B=1 -> B>1
1332    /// transition under live defaults.
1333    ///
1334    /// STAGE-SCOPED FROM BIRTH: `[lo, hi)` + caller-supplied engine/pos_d, so
1335    /// `decode_step_batch_ppn` calls it per stage (per-stage engine, per-stage pos_d, the
1336    /// #87 entry fence and boundary slots unchanged) — the pp2-batch seam lesson.
1337    #[allow(clippy::too_many_arguments)]
1338    pub(crate) fn step35_decode_batch_layers(
1339        &self,
1340        e: &Engine,
1341        mut x: CudaSlice<f32>,
1342        caches: &mut [&mut Cache],
1343        pos_d: &CudaSlice<i32>,
1344        lo: usize,
1345        hi: usize,
1346        ph_last: &mut std::time::Instant,
1347    ) -> Result<CudaSlice<f32>, Box<dyn std::error::Error>> {
1348        let b_n = caches.len();
1349        let cfg = &self.cfg;
1350        let n_embd = cfg.n_embd as usize;
1351        let eps = cfg.rms_eps;
1352        cfg.step35.as_ref().ok_or("step35_decode_batch_layers requires step35 cfg")?;
1353        // b2geo35 gate evidence: one line, first B>1 walk only (grep-stable prefix).
1354        if b_n > 1 {
1355            static ONCE: std::sync::Once = std::sync::Once::new();
1356            ONCE.call_once(|| {
1357                eprintln!("[step35-batch] first B>1 batched step35 walk: B={b_n} layers=[{lo},{hi})");
1358            });
1359        }
1360
1361        for il in lo..hi {
1362            let layer = &self.layers[il];
1363            let Mixer::Full(fa) = &layer.mixer else {
1364                return Err(format!("step35 layer {il} is not full-attn — corrupt config").into());
1365            };
1366            let geometry = self.step35_geom(il);
1367            let hd = geometry.head_dim_k as usize;
1368            let nkv = geometry.n_head_kv as usize;
1369            let nh = geometry.n_head as usize;
1370            let rbase = geometry.rope_base;
1371            let scale = geometry.attention_scale();
1372            let swa = geometry.window.is_some();
1373            let win = geometry.window.unwrap_or(0) as usize;
1374            let n_rot = geometry.n_rot as usize;
1375            let q_dim = nh * hd;
1376            let kv_dim = nkv * hd;
1377
1378            // ---- attn_norm + q8_1 quantize, batched (B rows) ----
1379            let anorm = layer.attn_norm.float_data();
1380            let mut xn = e.uninit(b_n * n_embd)?;
1381            e.rms_norm(&x, anorm, &mut xn, n_embd, b_n, eps)?;
1382            let (hq, hdq) = e.quantize_q8_1(&xn, b_n, n_embd)?;
1383
1384            // ---- batched projections: q/k/v + the separate head-wise gate (one weight
1385            // stream for B rows; xn is the live f32 fallback for non-q8_1-fast classes) ----
1386            let q0 = e.matmul_pre(&fa.wq, &hq, &hdq, &xn, b_n)?;
1387            let k0 = e.matmul_pre(&fa.wk, &hq, &hdq, &xn, b_n)?;
1388            let v0 = e.matmul_pre(&fa.wv, &hq, &hdq, &xn, b_n)?;
1389            let gw = fa.attn_gate.as_ref()
1390                .ok_or("step35 layer is missing attn_gate.weight (head-wise attention gate)")?;
1391            // gate input = the post-attn_norm hidden (upstream `cur`) — same xn/q8 pair.
1392            let gt = e.matmul_pre(gw, &hq, &hdq, &xn, b_n)?;
1393
1394            // ---- q/k RMSNorm over head_dim rows + the per-layer PARTIAL rope ----
1395            let mut q = e.uninit(b_n * q_dim)?;
1396            e.rms_norm(&q0, fa.q_norm.float_data(), &mut q, hd, b_n * nh, eps)?;
1397            let mut k = e.uninit(b_n * kv_dim)?;
1398            e.rms_norm(&k0, fa.k_norm.float_data(), &mut k, hd, b_n * nkv, eps)?;
1399            let ff = if geometry.rope_factors {
1400                self.step35_aux.as_ref().and_then(|a| a.rope_freqs(e))
1401            } else {
1402                None
1403            };
1404            e.rope_neox2(&mut q, &mut k, pos_d, hd, n_rot, nh, nkv, b_n, rbase, 1.0, ff)?;
1405            ph_mark(e, 1, ph_last)?;
1406
1407            // ---- per-session: KV append + windowed/global fa_decode (each session's OWN
1408            // len drives its view offset — the iso-gap law, no cross-session term) ----
1409            let mut attn = e.uninit(b_n * q_dim)?;
1410            if b_n == 1 {
1411                // B=1 SPECIALIZED ENTRY (lane/cx-eagerpar): the general row loop below
1412                // materializes q_row and a_row because a B>1 FA call consumes/produces one
1413                // contiguous row at a time. At B=1, q and attn already ARE those whole rows.
1414                // Pass them directly to the same fa_decode_kvmod call: this removes two
1415                // arithmetic-free D2D copies (90 launches/token on Step3.7's 45 layers)
1416                // without changing any arithmetic kernel, shape, argument value, or order.
1417                // Keep the B>1 body verbatim below; b1fix's one-class/transition gates are
1418                // the promotion bar, not an FP-similarity tolerance.
1419                let kvl = caches[0].kv[il].as_mut().unwrap();
1420                let k_row = k.slice(0..kv_dim);
1421                let v_row = v0.slice(0..kv_dim);
1422                let next_len = kvl.len + 1;
1423                let (off, t_kv) = if swa && next_len > win {
1424                    (next_len - win, win)
1425                } else {
1426                    (0, next_len)
1427                };
1428                let write_row = e.prepare_kv_append(kvl, off & !31usize, 1)?;
1429                e.append_kv_quantized_view(
1430                    &k_row, &v_row, &mut kvl.k, &mut kvl.v, write_row,
1431                    kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes,
1432                    Engine::kv_fp8_on(),
1433                )?;
1434                kvl.len = next_len;
1435                ph_mark(e, 2, ph_last)?;
1436                let physical = kvl.physical_rows(off, off + t_kv)?;
1437                let k_view = e.view_u8_range(&kvl.k, physical.start * kvl.k_tok_bytes,
1438                                             physical.end * kvl.k_tok_bytes);
1439                let v_view = e.view_u8_range(&kvl.v, physical.start * kvl.v_tok_bytes,
1440                                             physical.end * kvl.v_tok_bytes);
1441                e.fa_decode_kvmod(&q, &k_view, &v_view, &mut attn, hd, nh, nkv,
1442                                  t_kv, scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
1443                                  Engine::kv_fp8_on())?;
1444                ph_mark(e, 4, ph_last)?;
1445            } else {
1446                for (bi, cache) in caches.iter_mut().enumerate() {
1447                    let kvl = cache.kv[il].as_mut().unwrap();
1448                    let k_row = k.slice(bi * kv_dim..(bi + 1) * kv_dim);
1449                    let v_row = v0.slice(bi * kv_dim..(bi + 1) * kv_dim);
1450                    let next_len = kvl.len + 1;
1451                    let (off, t_kv) = if swa && next_len > win {
1452                        (next_len - win, win)
1453                    } else {
1454                        (0, next_len)
1455                    };
1456                    let write_row = e.prepare_kv_append(kvl, off & !31usize, 1)?;
1457                    e.append_kv_quantized_view(
1458                        &k_row, &v_row, &mut kvl.k, &mut kvl.v, write_row,
1459                        kvl.kv_dim_k, kvl.kv_dim_v, kvl.k_tok_bytes, kvl.v_tok_bytes,
1460                        Engine::kv_fp8_on(),
1461                    )?;
1462                    kvl.len = next_len;
1463                    ph_mark(e, 2, ph_last)?;
1464                    // the eager arm's SWA view arithmetic, verbatim (step35_decode_attn):
1465                    // token-aligned offset, keys carry absolute rope, mask is positional.
1466                    let physical = kvl.physical_rows(off, off + t_kv)?;
1467                    let k_view = e.view_u8_range(&kvl.k, physical.start * kvl.k_tok_bytes,
1468                                                 physical.end * kvl.k_tok_bytes);
1469                    let v_view = e.view_u8_range(&kvl.v, physical.start * kvl.v_tok_bytes,
1470                                                 physical.end * kvl.v_tok_bytes);
1471                    let mut q_row = e.uninit(q_dim)?;
1472                    e.dtod_copy_view(&q.slice(bi * q_dim..(bi + 1) * q_dim), &mut q_row)?;
1473                    ph_mark(e, 3, ph_last)?;
1474                    let mut a_row = e.uninit(q_dim)?;
1475                    e.fa_decode_kvmod(&q_row, &k_view, &v_view, &mut a_row, hd, nh, nkv,
1476                                      t_kv, scale, kvl.k_tok_bytes, kvl.v_tok_bytes,
1477                                      Engine::kv_fp8_on())?;
1478                    ph_mark(e, 4, ph_last)?;
1479                    e.dtod_copy_into(&a_row, &mut attn, bi * q_dim)?;
1480                    ph_mark(e, 3, ph_last)?;
1481                }
1482            }
1483
1484            // ---- head-wise gate (one sigmoid per (token, head), pre-wo) + o-proj at m=B ----
1485            let mut ag = e.uninit(b_n * q_dim)?;
1486            e.attn_head_gate(&attn, &gt, &mut ag, None, hd, nh, b_n)?;
1487            let mixed = e.matmul(&fa.wo, &ag, b_n)?;
1488            ph_mark(e, 5, ph_last)?;
1489
1490            // ---- residual add + post_attn_norm + FFN, batched ----
1491            let pnorm = layer.post_attn_norm.float_data();
1492            let mut x1 = e.uninit(b_n * n_embd)?;
1493            let mut z = e.uninit(b_n * n_embd)?;
1494            e.add_rms_norm(&x, &mixed, pnorm, &mut x1, &mut z, n_embd, b_n, eps)?;
1495            let ffn_out = match &layer.ffn {
1496                crate::hybrid::Ffn::Dense { ffn_gate, ffn_up, ffn_down } => {
1497                    // A dense step35 FFN's clamp is the SHEXP array (upstream's one
1498                    // build_ffn serves dense + shared expert, llama-graph.cpp:1751);
1499                    // ffn_act_lim dispatches clamped/plain per layer. Layers 0-2 (the
1500                    // leading dense) have no live limit on this artifact, but the route
1501                    // is correct by construction, not by artifact.
1502                    let n_ff = ffn_gate.out_features();
1503                    let (zq, zd) = e.quantize_q8_1(&z, b_n, n_embd)?;
1504                    let g = e.matmul_pre(ffn_gate, &zq, &zd, &z, b_n)?;
1505                    let u = e.matmul_pre(ffn_up, &zq, &zd, &z, b_n)?;
1506                    let mut act = e.uninit(b_n * n_ff)?;
1507                    Self::ffn_act_lim(e, cfg, &g, &u, 1.0, 1.0,
1508                                      cfg.clamp_shexp_at(il as u32), &mut act, b_n * n_ff)?;
1509                    let (aq, ad) = e.quantize_q8_1(&act, b_n, n_ff)?;
1510                    e.matmul_pre(ffn_down, &aq, &ad, &act, b_n)?
1511                }
1512                // t=B < PRIME_MIN_T: per-column decode-exact router + host sigmoid routing
1513                // + per-token expert dispatch — the same per-token program as eager t=1,
1514                // including the per-layer SwiGLU clamp (43/44) via the sequential path's
1515                // ffn_act_lim. The sigmoid-router deny on dev/pairs holds by predicate.
1516                crate::hybrid::Ffn::Moe(m) =>
1517                    self.moe_ffn_il_zq8(e, m, &z, None, b_n, il as u16)?,
1518            };
1519            let mut x2 = e.uninit(b_n * n_embd)?;
1520            e.add(&x1, &ffn_out, &mut x2, b_n * n_embd)?;
1521            x = x2;
1522            ph_mark(e, 9, ph_last)?;
1523        }
1524        Ok(x)
1525    }
1526
1527    /// The batched tick's TAIL, after the trunk: grammar masks -> device sampling -> lean
1528    /// logits park -> `pos` bump. Split out with the pp seam (`decode_batch_layers`) because
1529    /// under a stage split this runs on the LAST stage's engine and device — the lm_head, the
1530    /// masks, the sampler, and `cache.last_logits_dev` all live where the final residual
1531    /// lands, and the caller must be able to place them there without duplicating 90 lines of
1532    /// serving contract. `logits` is `[b_n, n_vocab]` already computed by the caller (the
1533    /// output_norm + lm_head pair stays at the call site so a stage split can fence around
1534    /// it); everything after it is here, verbatim.
1535    #[allow(clippy::too_many_arguments)]
1536    fn decode_batch_epilogue(
1537        &self,
1538        e: &Engine,
1539        caches: &mut [&mut Cache],
1540        samp: &[Option<(f32, u64, u32)>],
1541        masks: &[Option<(&CudaSlice<u32>, usize)>],
1542        lean: bool,
1543        logits: CudaSlice<f32>,
1544        b_n: usize,
1545        ph_last: &mut std::time::Instant,
1546    ) -> Result<(Vec<Vec<f32>>, Vec<Option<u32>>), Box<dyn std::error::Error>> {
1547        // GRAMMAR MASKS (constrained decoding): preserve each masked row's PRISTINE logits
1548        // for its consumer (lean park into cache.last_logits_dev — the reuse-pool park stays
1549        // unmasked, the v1 contract — or the non-lean D2H), then ban in place BEFORE the
1550        // device sampler reads the row. All stream-ordered; masks=&[] takes no new branch.
1551        let n_vocab = self.output.out_features();
1552        let mut logits = logits;
1553        let mut pristine: Vec<Option<CudaSlice<f32>>> = Vec::new();
1554        if masks.iter().take(b_n).any(|m| m.is_some()) {
1555            pristine.resize_with(b_n, || None);
1556            for (bi, m) in masks.iter().take(b_n).enumerate() {
1557                let Some((mask, words)) = m else { continue };
1558                assert!(samp.get(bi).copied().flatten().is_some(),
1559                        "grammar-masked row {bi} must request a device sample");
1560                if lean {
1561                    let cache = &mut caches[bi];
1562                    if cache.last_logits_dev.as_ref().map(|d| d.len() < n_vocab).unwrap_or(true) {
1563                        cache.last_logits_dev = Some(e.uninit(n_vocab)?);
1564                    }
1565                    let dst = cache.last_logits_dev.as_mut().unwrap();
1566                    e.dtod_copy_view(&logits.slice(bi * n_vocab..(bi + 1) * n_vocab), dst)?;
1567                } else {
1568                    let mut p = e.uninit(n_vocab)?;
1569                    e.dtod_copy_view(&logits.slice(bi * n_vocab..(bi + 1) * n_vocab), &mut p)?;
1570                    pristine[bi] = Some(p);
1571                }
1572                e.mask_logits_col(&mut logits, mask, bi, n_vocab, *words)?;
1573            }
1574        }
1575
1576        // Device-side sampling for requested rows (see the method doc). Enqueued before the
1577        // big logits D2H so the tiny [B] token readback rides the same sync.
1578        let mut next: Vec<Option<u32>> = vec![None; b_n];
1579        if samp.iter().take(b_n).any(|s| s.is_some()) {
1580            let mut toks = e.alloc_u32_zeroed(b_n)?;
1581            let mut perturb: Option<CudaSlice<f32>> = None;
1582            for (bi, s) in samp.iter().take(b_n).enumerate() {
1583                let Some((temp, seed, ctr)) = s else { continue };
1584                if *temp <= 0.0 {
1585                    e.argmax_token_device_col(&logits, bi, n_vocab, &mut toks, bi)?;
1586                } else {
1587                    if perturb.is_none() {
1588                        perturb = Some(e.zeros(n_vocab)?);
1589                    }
1590                    let pb = perturb.as_mut().unwrap();
1591                    e.gumbel_perturb_col(&logits, bi, pb, n_vocab, *seed, *ctr, *temp)?;
1592                    e.argmax_token_device_col(pb, 0, n_vocab, &mut toks, bi)?;
1593                }
1594            }
1595            let host_toks = e.dtoh_u32(&toks)?;
1596            for (bi, s) in samp.iter().take(b_n).enumerate() {
1597                if s.is_some() {
1598                    next[bi] = Some(host_toks[bi]);
1599                }
1600            }
1601        }
1602
1603        let lean_any = lean && samp.iter().take(b_n).any(|s| s.is_some());
1604        let rows: Vec<Vec<f32>> = if lean_any {
1605            // LEAN: park device-sampled rows on-device (per-cache buffer, dtod); D2H only
1606            // the rows that still need host logits. No sampled rows + no fallback rows =
1607            // the big D2H disappears (the [B] token readback above already synced).
1608            for (bi, s) in samp.iter().take(b_n).enumerate() {
1609                if s.is_none() { continue; }
1610                // grammar-masked rows already parked their PRISTINE copy above — the
1611                // in-place ban has since poisoned this row for the reuse-pool consumer.
1612                if masks.get(bi).copied().flatten().is_some() { continue; }
1613                let cache = &mut caches[bi];
1614                if cache.last_logits_dev.as_ref().map(|d| d.len() < n_vocab).unwrap_or(true) {
1615                    cache.last_logits_dev = Some(e.uninit(n_vocab)?);
1616                }
1617                let dst = cache.last_logits_dev.as_mut().unwrap();
1618                e.dtod_copy_view(&logits.slice(bi * n_vocab..(bi + 1) * n_vocab), dst)?;
1619            }
1620            (0..b_n)
1621                .map(|bi| {
1622                    if samp.get(bi).copied().flatten().is_some() {
1623                        Ok(Vec::new())
1624                    } else {
1625                        e.dtoh_view(&logits.slice(bi * n_vocab..(bi + 1) * n_vocab))
1626                    }
1627                })
1628                .collect::<Result<_, _>>()?
1629        } else {
1630            let host = e.dtoh(&logits)?;
1631            (0..b_n).map(|bi| {
1632                // grammar-masked non-lean rows return the PRISTINE copy (the in-place ban
1633                // must never leak into last_logits — reuse-pool/park semantics unchanged).
1634                if let Some(p) = pristine.get(bi).and_then(|p| p.as_ref()) {
1635                    return e.dtoh(p);
1636                }
1637                Ok(host[bi * n_vocab..(bi + 1) * n_vocab].to_vec())
1638            }).collect::<Result<_, _>>()?
1639        };
1640        for c in caches.iter_mut() {
1641            c.pos += 1;
1642        }
1643        ph_mark(e, 11, ph_last)?;
1644        Ok((rows, next))
1645    }
1646}