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