spark_model/layers/glm5next_dsa/
layer.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `Glm5NextDsaLayer` — the DSA block: NoPE MLA attention over indexer-selected tokens.
4//!
5//! Decode, end to end:
6//!
7//! ```text
8//! hidden ─┬─ q_a_proj ─ RMSNorm ─┬─ q_absorb ────────────── Q (latent space)
9//!         │                      └─ indexer.wq_b ────────── q_idx  ─┐
10//!         ├─ indexer.wk ─ LayerNorm(w,b) ─ state.k_normed ──────────┤
11//!         ├─ compress_gate ─────────────── state.gate ──────────────┼─ select_tokens
12//!         ├─ weights_proj ──────────────── head weights ────────────┘        │
13//!         └─ kv_a_proj ─ RMSNorm ─ FP8 ─── paged latent cache                │
14//!                                                                            ▼
15//!                                            glm5next_dsa_mla_decode_fp8 (gather)
16//! ```
17//!
18//! # 🪤 Four silent-wrong-answer traps this file exists to hold
19//!
20//! * **Two RMSNorm kernels differ only by a `+1`.** `rms_norm` computes
21//!   `x * rms * (1 + w)`; `rms_norm_vanilla` computes `x * rms * w`. Same signature, same
22//!   shapes. GLM is plain, so every norm here takes the *vanilla* entry point.
23//! * **`indexer.k_norm` is an `nn.LayerNorm` with a bias**, not an RMSNorm at all — mean
24//!   subtraction plus a bias term. It takes `nllb_layernorm_bf16(x, w, b, …)`.
25//! * **`weights_proj` output must already carry `index_heads^-0.5`.** `dsa_index_scores`
26//!   does not apply it. Folded into the weight at load — see [`Glm5NextDsaWeights`].
27//! * **Q must be absorbed into latent space before it reaches the decode kernel.** The
28//!   kernel dots Q against the 512-dim latent directly, so `q_absorb` is `q_b_proj`
29//!   pre-multiplied by `kv_b_proj`'s K half. A raw `q_b_proj` is the right shape per head
30//!   (256 vs 512 is not) but the wrong space.
31
32use anyhow::{Context, Result, bail};
33use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
34use spark_runtime::kernel_args::KernelLaunch;
35use spark_runtime::kv_cache::PagedKvCache;
36
37use super::attend::{DsaDecodeInputs, DsaDecodePaging, Glm5NextDsaDecodeKernel, decode_attention};
38use super::select::{DsaSelectInputs, DsaSelectScratch, select_tokens};
39use super::state::Glm5NextDsaState;
40use super::{Glm5NextDsaConfig, Glm5NextDsaKernels};
41use crate::layer::{ForwardContext, LayerState, TransformerLayer};
42
43/// GEMM launch: `C[M, N] = A[M, K] @ B[N, K]^T`. Grid `(ceil(N/16), ceil(M/16))`,
44/// block `(16, 16)` — one thread per output element.
45fn gemm(
46    gpu: &dyn GpuBackend,
47    k: KernelHandle,
48    gemv: KernelHandle,
49    batchm: KernelHandle,
50    a: DevicePtr,
51    b: DevicePtr,
52    c: DevicePtr,
53    m: usize,
54    n: usize,
55    kk: usize,
56    stream: u64,
57) -> Result<()> {
58    // 🔴 Wide (prefill) shapes go to cuBLASLt — `dsa_proj` was 14.9% of prefill on the
59    // scalar tile GEMM. Same numerics boundary as the KDA block; see
60    // `glm5next_layer::cublas_wide_proj` for why M > DENSE_GEMV_BATCHM_MAX_M is the safe
61    // cut and why decode and the speculative verify cannot reach it.
62    if m > crate::layers::ops::DENSE_GEMV_BATCHM_MAX_M as usize
63        && crate::layers::glm5next_layer::cublas_wide_proj()
64    {
65        return crate::layers::ops::cublas_bf16_proj_dense(
66            a, b, c, m as u32, n as u32, kk as u32, stream,
67        );
68    }
69    // M=1 decode -> GEMV; M=2..8 (a K-token verify sweep) -> ONE weight read for all rows;
70    // wider -> the tile GEMM. `ops::dense_mm_bf16` owns the policy and the grid coupling.
71    crate::layers::ops::dense_mm_bf16(
72        gpu,
73        &crate::layers::ops::DenseMmKernels {
74            gemm: k,
75            gemv,
76            batchm,
77        },
78        a,
79        b,
80        c,
81        m,
82        n,
83        kk,
84        stream,
85    )
86}
87
88/// Every kernel a DSA block launches, beyond the selection set.
89#[derive(Clone, Copy)]
90pub struct Glm5NextDsaLayerKernels {
91    /// `C = A @ B^T`, BF16 out.
92    pub gemm: KernelHandle,
93    /// Same, FP32 out — the selector wants `q_idx` and the head weights in FP32.
94    pub gemm_f32: KernelHandle,
95    /// M=1 twins of the two above. `gemv_f32` may be a 0 handle on a target that predates
96    /// `dense_gemv_bf16_fp32out`; `gemm` refuses nothing and falls back to the tile arm.
97    pub gemv: KernelHandle,
98    pub gemv_f32: KernelHandle,
99    /// 🔴 `dense_gemv_bf16_batchm` — `2 ..= 8` rows in ONE weight sweep, the arm that makes a
100    /// K-token verify pay for q_a/q_b/kv_a/kv_b/o once instead of K times. `0` = unavailable.
101    pub gemv_batchm: KernelHandle,
102    /// 🪤 **vanilla** = `x * rms * w`. The other `rms_norm` adds 1 to the weight.
103    pub rms_norm: KernelHandle,
104    /// RMSNorm + FP8 + paged slot write, GLM-target.
105    pub latent_write: KernelHandle,
106}
107
108impl Glm5NextDsaLayerKernels {
109    pub fn resolve(gpu: &dyn GpuBackend) -> Result<Self> {
110        Ok(Self {
111            // 🪤 Module is "gemm", NOT the file stem. `common/KERNEL.toml` [modules] maps
112            // `dense_gemm_bf16 = "gemm"`, and an unlisted .cu takes its stem — so the two
113            // conventions coexist and only the TOML says which applies. Guessing the stem
114            // here resolved to nothing and would have failed at first construction.
115            gemm: gpu.kernel("gemm", "dense_gemm_bf16")?,
116            gemm_f32: gpu.kernel("gemm", "dense_gemm_bf16_f32out")?,
117            gemv: gpu.kernel("gemv", "dense_gemv_bf16")?,
118            // 🪤 try_kernel, not kernel: this entry point had ZERO Rust callers before
119            // 2026-08-28, so a target that never compiled it must fall back, not refuse.
120            gemv_f32: crate::layers::try_kernel(gpu, "gemv", "dense_gemv_bf16_fp32out"),
121            gemv_batchm: crate::layers::try_kernel(
122                gpu,
123                "dense_gemv_bf16_batchm",
124                "dense_gemv_bf16_batchm",
125            ),
126            rms_norm: gpu.kernel("rms_norm_vanilla", "rms_norm_vanilla")?,
127            latent_write: gpu
128                .kernel("glm5next_mla_latent_write", "glm5next_mla_latent_write_fp8")?,
129        })
130    }
131}
132
133/// One DSA block's weights, already sharded for this rank.
134pub struct Glm5NextDsaWeights {
135    // ── MLA ──
136    pub q_a_proj: DevicePtr,
137    pub q_a_layernorm: DevicePtr,
138    /// `[local_heads * kv_lora_rank, q_lora_rank]` BF16 — `q_b_proj` **absorbed** through
139    /// `kv_b_proj`'s K half, so Q arrives in latent space. See the module header.
140    pub q_absorb: DevicePtr,
141    pub kv_a_proj: DevicePtr,
142    pub kv_a_layernorm: DevicePtr,
143    /// `[hidden, local_heads * kv_lora_rank]` BF16, row-parallel — all-reduced by the caller.
144    ///
145    /// 🪤 **Absorbed**, not the raw checkpoint `o_proj`: the decode kernel leaves its output
146    /// in the 512-dim LATENT space, so the projection carries `kv_b_proj`'s V half folded in.
147    /// The raw weight is `local_heads * v_head_dim` wide — half of this — and feeding the
148    /// latent to it reads 2x past every row rather than merely computing the wrong thing.
149    pub o_absorb: DevicePtr,
150    // ── indexer (REPLICATED across ranks; see `tp`) ──
151    pub wk: DevicePtr,
152    pub k_norm_weight: DevicePtr,
153    /// 🪤 REQUIRED. `k_norm` is a LayerNorm; a `.weight`-only bind silently drops the
154    /// mean subtraction and the bias.
155    pub k_norm_bias: DevicePtr,
156    pub compress_gate: DevicePtr,
157    pub wq_b: DevicePtr,
158    /// 🪤 Pre-multiplied by `index_heads^-0.5` at load — `dsa_index_scores` does not scale.
159    pub weights_proj: DevicePtr,
160    /// `[index_kpool, index_head_dim]` **FP32**. 🪤 BF16 on disk; upconverted at load.
161    pub ape: DevicePtr,
162}
163
164/// The PREFILL selector runs once for the whole row group instead of once per token.
165/// ON by default; `AVAROK_DSA_SELECT_ROWS=0` is the kill-switch back to per-row selection.
166///
167/// 🔴 EXPLICIT PREFILL ONLY. The batched pass runs when — and only when — the caller states
168/// `is_prefill`; see [`batch_select_enabled`] for the whole table. Nothing in
169/// `ForwardContext` implies it. `decode_step` is false for prefill AND for a speculative
170/// verify, and `graph_capture` is false for prefill AND for an EAGER verify: `verify_a`
171/// hard-codes `graph_capture: false`, and `verify_b/c/c2/d/fused` take it from `use_graphs`,
172/// which is false under `AVAROK_GLM_VERIFY_GRAPHS=0`, under high-speed swap, and under
173/// `AVAROK_LORA_EAGER`. **Both eager and graphed verification keep the original per-row
174/// path.**
175///
176/// `!graph_capture` is required SEPARATELY, for its own reason rather than as a proxy for
177/// "not verify": `select_rows_batched` performs host copy/H2D work (`copy_h2d` of `q_pos`),
178/// which is `CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED` inside a recording stream.
179///
180/// Selection MEMBERSHIP is unaffected by the widened pool walk, and the kernels are
181/// untouched. `dsa_index_scores` already carries the row axis on `gridDim.y` with a per-row
182/// `q_pos[Q]`, and its candidacy test (`dsa_indexer.cu`) is *"a pool is a candidate only when
183/// it is complete AND its LAST token is visible to this query"* — `pool_valid[p] && end_c <=
184/// q_pos[r]`. Both terms are per-row or per-pool; neither reads the scalar `P`. So widening
185/// `P` from row `r`'s own pool count to the group's selects the same pools in the same order
186/// for row `r`: every extra pool is either incomplete or ends past `q_pos[r]`, scores
187/// `-FLT_MAX`, and `dsa_topk_pools` excludes it under a TOTAL order (score DESC, then pool
188/// index ASC — unique, so the top-`select_k` prefix is unique).
189///
190/// 🔴 Equal membership was NOT equal placement, and that distinction cost a review cycle.
191/// `dsa_expand_selection` writes the visible tail at `select_k * KP`, and `select_k` is a
192/// per-pass scalar: the original batched geometry planned it once from the group's FINAL
193/// cache length, so every earlier row's tail slid forward relative to its serial twin. The
194/// production attention (`glm5next_dsa_mla_decode_fp8`) splits the selection row into
195/// `NUM_WARPS = 8` slices, runs a per-warp online softmax and merges across warps, so a
196/// slid token is folded by the MERGE instead of by its warp's serial loop. **The original
197/// batched geometry was shown to ALTER warp FP reduction grouping** — measured on the real
198/// kernel, 14 of 18 crossing configurations differ by up to 2 BF16 ulp over 64 draws each
199/// (a single draw is byte-identical, which is why one probe proved nothing: the kernel
200/// accumulates in FP32 and writes BF16). Over a 9,000-token prefill at `PREFILL_ROWS = 16`,
201/// 1,920 of 8,999 rows moved their tail base and 42 moved a real token across a warp slice.
202///
203/// The correction is per-row geometry inside `dsa_expand_selection`:
204/// `row_select_k = min(select_k, (q_pos[r] + 1) / KP)`, applied to both the pool loop and
205/// the tail base. It **preserves serial selection-slot geometry** and is a no-op at
206/// `q_rows == 1`, so the serial and replay-safe paths are untouched. Exact selector parity
207/// was restored: real-kernel serial-vs-batched comparison over 2,397 rows went from 1,533
208/// mismatching rows to **0**.
209///
210/// Measured on the 9K prefill probe, n3+n4, fresh container, first request after launch:
211/// **146.073 s -> 134.001 s, -12.072 s / -8.26 %**, with the canonical six reproducing the
212/// sealed reference **6/6** and the sealed p9000 hash `4187fe63fa78d8b4` unchanged
213/// (2026-09-06, `scripts/glm53-dsa-promote/{gate-6probe.sh,ADJUDICATION.md}`).
214///
215/// The four `[max_rows]` twins are allocated only when this is on, so the kill-switch arm
216/// keeps the pre-batching heap layout byte for byte. That matters here: this workspace is
217/// the one where merely making an allocation unconditionally was itself enough to move the
218/// model's sampled output (see `bt`/`sl` below, and A55).
219/// Whether ONE selection pass covers the whole row group, or each row selects on its own.
220///
221/// Extracted from `decode_k` so the choice is a table a test can drive, not a boolean buried
222/// in a 300-line function. Every term is load-bearing:
223///
224/// * `workspace_ready` — the four `[max_rows]` twins exist. False under
225///   `AVAROK_DSA_SELECT_ROWS=0`, which is what keeps that arm's heap layout byte for byte.
226/// * `is_prefill` — stated by the caller. NOTHING in `ForwardContext` implies it:
227///   `decode_step` is false for prefill AND verify, and `graph_capture` is false for prefill
228///   AND for an eager verify (`verify_a` hard-codes it; `verify_b/c/c2/d/fused` take it from
229///   `use_graphs`, false under `AVAROK_GLM_VERIFY_GRAPHS=0`, high-speed swap, or
230///   `AVAROK_LORA_EAGER`).
231/// * `!graph_capture` — independent of the above, and kept for its own reason:
232///   `select_rows_batched` issues a host `copy_h2d`, which is
233///   `CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED` inside a recording stream.
234/// * `k > 1` — a single row has nothing to batch, and there is no behaviour to change.
235pub(crate) fn batch_select_enabled(
236    workspace_ready: bool,
237    is_prefill: bool,
238    graph_capture: bool,
239    k: usize,
240) -> bool {
241    workspace_ready && is_prefill && !graph_capture && k > 1
242}
243
244pub(crate) fn dsa_select_rows_enabled() -> bool {
245    std::env::var("AVAROK_DSA_SELECT_ROWS").as_deref() != Ok("0")
246}
247
248/// Scratch reused across decode steps. Allocated once per layer.
249pub struct Glm5NextDsaWorkspace {
250    q_a: DevicePtr,
251    q_resid: DevicePtr,
252    q_abs: DevicePtr,
253    kv_a: DevicePtr,
254    q_idx: DevicePtr,
255    head_weights: DevicePtr,
256    q_pos: DevicePtr,
257    q_mask: DevicePtr,
258    /// `[max_rows, ...]` twins of `q_idx` / `head_weights` / `q_pos` / `q_mask`, used only
259    /// by the batched prefill selector. NULL when `AVAROK_DSA_SELECT_ROWS=0` — see
260    /// [`dsa_select_rows_enabled`] for why they are not allocated unconditionally.
261    q_idx_rows: DevicePtr,
262    head_weights_rows: DevicePtr,
263    q_pos_rows: DevicePtr,
264    q_mask_rows: DevicePtr,
265    slot: DevicePtr,
266    attn_out: DevicePtr,
267    /// Block table for the paged gather, `[max_dsa_context]` i32. PERSISTENT.
268    /// 🔴 This used to be a `gpu.alloc` + `gpu.free` on EVERY DSA layer of EVERY
269    /// decode token — 11 allocs + 11 frees per token. `cuMemAlloc` serialises against
270    /// the driver, and nsys (2026-08-28) charged the alloc/copy/free cluster ~98 us of
271    /// GPU-idle per DSA layer, 1.08 ms of an 79 ms step.
272    bt: DevicePtr,
273    /// Sequence length for the paged gather, one i32. PERSISTENT, same reason.
274    sl: DevicePtr,
275    /// Capacity of `bt` in ENTRIES, so the forward can refuse rather than overrun it.
276    bt_cap: usize,
277    /// Widest verify this scratch can serve. `1` on the serial decode path.
278    max_rows: usize,
279    /// `[index_head_dim]` BF16 staging for the indexer row, at a FIXED address.
280    ///
281    /// 🔴 The projections used to write straight into `k_normed`/`gate` at
282    /// `offset(pos * D * 2)` — a host-computed address, which a captured graph freezes at
283    /// the capture-time row. Under capture they land here and `dsa_indexer_store` places
284    /// them from a device-side `pos`. Same arithmetic, one extra 256-byte copy.
285    stage_k: DevicePtr,
286    stage_gate: DevicePtr,
287    /// `[5]` i32 selector geometry, written on device once per step by `dsa_write_geom`.
288    geom_dev: DevicePtr,
289    select: DsaSelectScratch,
290}
291
292impl Glm5NextDsaWorkspace {
293    /// `max_rows` is the widest speculative verify this workspace serves. The projection
294    /// buffers, `attn_out` and the selector's OUTPUT row scale with it; the indexer staging
295    /// rows, the block table and the selector's within-pass temporaries stay per-row,
296    /// because [`Glm5NextDsaLayer::decode_k`] still SELECTS one token at a time.
297    pub fn new(gpu: &dyn GpuBackend, cfg: &Glm5NextDsaConfig, max_rows: usize) -> Result<Self> {
298        let rows = max_rows.max(1);
299        // Sized at the DSA context cap so a growing sequence never reallocates.
300        // `q_rows = rows`: selection still runs one row at a time, but its OUTPUT is
301        // `[max_rows, out_width]` so a K-row verify can attend every row in one launch.
302        let geom =
303            super::select::DsaSelectGeometry::plan(cfg, super::state::max_dsa_context(cfg), rows)?;
304        let bt_cap = super::state::max_dsa_context(cfg).max(1);
305        let persist = std::env::var("AVAROK_GLM_DSA_ALLOC_PER_STEP").as_deref() != Ok("1");
306        let batch_select = dsa_select_rows_enabled();
307        Ok(Self {
308            q_a: gpu.alloc(rows * (cfg.q_lora_rank * 2))?,
309            q_resid: gpu.alloc(rows * (cfg.q_lora_rank * 2))?,
310            q_abs: gpu.alloc(rows * (cfg.local_heads * cfg.kv_lora_rank * 2))?,
311            kv_a: gpu.alloc(rows * (cfg.kv_lora_rank * 2))?,
312            q_idx: gpu.alloc(cfg.index_heads * cfg.index_head_dim * 4)?,
313            head_weights: gpu.alloc(cfg.index_heads * 4)?,
314            q_pos: gpu.alloc(4)?,
315            q_idx_rows: if batch_select {
316                gpu.alloc(rows * cfg.index_heads * cfg.index_head_dim * 4)?
317            } else {
318                DevicePtr(0)
319            },
320            head_weights_rows: if batch_select {
321                gpu.alloc(rows * cfg.index_heads * 4)?
322            } else {
323                DevicePtr(0)
324            },
325            q_pos_rows: if batch_select {
326                gpu.alloc(rows * 4)?
327            } else {
328                DevicePtr(0)
329            },
330            q_mask_rows: if batch_select {
331                // Same "one real query position per row" the scalar `q_mask` encodes,
332                // written once so the batched pass never needs a per-step H2D for it.
333                let p = gpu.alloc(rows)?;
334                gpu.memset_async(p, 1, rows, 0)?;
335                gpu.synchronize(0)?;
336                p
337            } else {
338                DevicePtr(0)
339            },
340            q_mask: {
341                // Decode always presents one real query position. Set ONCE — writing it per
342                // token cost a blocking H2D per DSA layer and made the step uncapturable.
343                let p = gpu.alloc(1)?;
344                gpu.memset_async(p, 1, 1, 0)?;
345                gpu.synchronize(0)?;
346                p
347            },
348            slot: gpu.alloc(8)?,
349            attn_out: gpu.alloc(rows * (cfg.local_heads * cfg.kv_lora_rank * 2))?,
350            // One entry per cached token is the worst case (block_size == 1), so the
351            // DSA context cap bounds it for every block size.
352            //
353            // 🔴 Allocated ONLY when `AVAROK_GLM_DSA_PERSIST_BT=1`. Not a micro-optimisation:
354            // making these two allocations UNCONDITIONALLY — even leaving them unused —
355            // is by itself enough to change the model's sampled output (measured t27,
356            // 2026-08-28). See A55 and the note at the use site.
357            bt: if persist {
358                gpu.alloc(bt_cap * 4)?
359            } else {
360                DevicePtr(0)
361            },
362            // 🔴 ANOMALIES A65: `[rows]`, NOT one. `attend_rows` runs ONE launch for all
363            // k rows and `glm5next_dsa_mla_decode` reads `seq_lens[blockIdx.y]`, so a
364            // single i32 here left every row past the first reading past the allocation.
365            sl: if persist {
366                gpu.alloc(rows * 4)?
367            } else {
368                DevicePtr(0)
369            },
370            bt_cap,
371            max_rows: rows,
372            stage_k: gpu.alloc(cfg.index_head_dim * 2)?,
373            stage_gate: gpu.alloc(cfg.index_head_dim * 2)?,
374            geom_dev: gpu.alloc(5 * 4)?,
375            select: DsaSelectScratch::alloc(gpu, cfg, &geom)?,
376        })
377    }
378}
379
380pub struct Glm5NextDsaLayer {
381    pub cfg: Glm5NextDsaConfig,
382    pub weights: Glm5NextDsaWeights,
383    pub kernels: Glm5NextDsaLayerKernels,
384    pub select_kernels: Glm5NextDsaKernels,
385    pub decode_kernel: Glm5NextDsaDecodeKernel,
386    pub workspace: Glm5NextDsaWorkspace,
387    /// Index in the MODEL stack (0..num_hidden_layers). Diagnostics only.
388    pub layer_idx: usize,
389    /// Index in the KV POOL — the running ordinal over KV-cache-consuming layers,
390    /// which for GLM-5.3 is 0..11 over the sparse layers, not 0..45.
391    ///
392    /// 🪤 These two are NOT interchangeable. The pool is sized to
393    /// `ModelConfig::num_attention_layers()`; indexing it with `layer_idx` reads
394    /// past the end of the allocation on every layer after the first.
395    pub attn_layer_idx: usize,
396    pub rms_eps: f32,
397    /// FP8 latent-cache scale. Reads and writes must agree; the write takes `1/scale`.
398    pub kv_scale: f32,
399    /// Persistent block-table buffers instead of a `gpu.alloc`/`gpu.free` per DSA layer per
400    /// token. ON by default; `AVAROK_GLM_DSA_ALLOC_PER_STEP=1` restores the old path.
401    pub persist_bt: bool,
402}
403
404impl Glm5NextDsaLayer {
405    /// Project `hidden` into the indexer cache at position `pos`, then advance.
406    ///
407    /// Writes `k_normed` and `gate` **directly into the state rows** rather than through a
408    /// staging buffer: the selector reads `k[raw * D + d]` over the whole context, so the
409    /// cache is the natural destination and a copy would buy nothing.
410    pub fn indexer_forward(
411        &self,
412        gpu: &dyn GpuBackend,
413        hidden: DevicePtr,
414        state: &mut Glm5NextDsaState,
415        // Some(pos) => write through the FIXED staging row and let `dsa_indexer_store`
416        // place it from this device-side position. None => the host-offset path.
417        pos_dev: Option<DevicePtr>,
418        stream: u64,
419    ) -> Result<()> {
420        // 🔴 BEFORE any write. Everything below writes into row `state.len()`; past the cap
421        // that row is off the end of the buffer, and the resulting sticky CUDA 700 kills the
422        // whole context, not just this request. A62.
423        state.ensure_room(1)?;
424        let d = self.cfg.index_head_dim;
425        let pos = state.len();
426        let off = state.row_offset(pos);
427        let w = &self.workspace;
428        let (k_dst, gate_dst) = match pos_dev {
429            Some(_) => (w.stage_k, w.stage_gate),
430            None => (state.k_normed.offset(off), state.gate.offset(off)),
431        };
432
433        // k_raw -> the state row, then LayerNorm in place.
434        gemm(
435            gpu,
436            self.kernels.gemm,
437            self.kernels.gemv,
438            self.kernels.gemv_batchm,
439            hidden,
440            self.weights.wk,
441            k_dst,
442            1,
443            d,
444            self.cfg.hidden,
445            stream,
446        )?;
447        // 🪤 LayerNorm WITH BIAS, in place, one row.
448        KernelLaunch::new(gpu, self.select_kernels.k_norm)
449            .grid([1, 1, 1])
450            .block([d.min(1024) as u32, 1, 1])
451            .shared_mem((d.min(1024) * 4) as u32)
452            .arg_ptr(k_dst)
453            .arg_ptr(self.weights.k_norm_weight)
454            .arg_ptr(self.weights.k_norm_bias)
455            .arg_u32(1)
456            .arg_u32(d as u32)
457            .arg_f32(self.rms_eps)
458            .launch(stream)?;
459
460        gemm(
461            gpu,
462            self.kernels.gemm,
463            self.kernels.gemv,
464            self.kernels.gemv_batchm,
465            hidden,
466            self.weights.compress_gate,
467            gate_dst,
468            1,
469            d,
470            self.cfg.hidden,
471            stream,
472        )?;
473
474        // Per-head selector weights, FP32 straight out of the GEMM, from the LAYER INPUT.
475        // `weights_proj` is `[index_heads, hidden]` and the reference is
476        // `weights_proj(hidden) * index_heads**-0.5`, with the scale already folded into the
477        // weight at load (`build.rs` transform 2). Computed here rather than in
478        // `select_and_attend` for the plain reason that this is the function that HAS
479        // `hidden`; `select_and_attend` does not, which is how it came to read `q_resid`
480        // instead and overrun it by 5120 bytes. See A55.
481        gemm(
482            gpu,
483            self.kernels.gemm_f32,
484            self.kernels.gemv_f32,
485            // No FP32-out batchm twin exists; the selector's two sites stay on gemv/tile.
486            KernelHandle(0),
487            hidden,
488            self.weights.weights_proj,
489            self.workspace.head_weights,
490            1,
491            self.cfg.index_heads,
492            self.cfg.hidden,
493            stream,
494        )?;
495
496        match pos_dev {
497            // 🔴 Placement and the validity mark both from a DEVICE position — a memset at
498            // `valid.offset(pos)` is one more host-baked address a graph would freeze.
499            Some(pd) => {
500                KernelLaunch::new(gpu, self.select_kernels.indexer_store)
501                    .grid([1, 1, 1])
502                    .block([d.min(1024) as u32, 1, 1])
503                    .arg_ptr(w.stage_k)
504                    .arg_ptr(w.stage_gate)
505                    .arg_ptr(pd)
506                    .arg_ptr(state.k_normed)
507                    .arg_ptr(state.gate)
508                    .arg_ptr(state.valid)
509                    .arg_u32(d as u32)
510                    .launch(stream)?;
511            }
512            // Validity is per position and this one is real.
513            None => gpu.memset_async(state.valid.offset(pos), 1, 1, stream)?,
514        }
515        state.advance(1)
516    }
517
518    /// Everything after the indexer write: selector inputs and the selection for ONE query
519    /// row, into row `row` of the workspace's selection scratch.
520    ///
521    /// 🔴 SELECTION stays per-row inside a K-token verify: the selector's geometry, its
522    /// top-k over `[0, len)` and its visibility test are all functions of THIS token's
523    /// position, and the indexer cache grows by one row between them. The gather-ATTEND
524    /// does not — every row reads the same cache with its own index row — so it is hoisted
525    /// out to one K-row launch in `attend_rows`. Three serial launches of 32 head-blocks
526    /// each left most of the GPU idle: 7.13 ms/step of the K=3 budget (nsys 2026-08-29).
527    #[allow(clippy::too_many_arguments)]
528    fn select_row(
529        &self,
530        gpu: &dyn GpuBackend,
531        row: usize,
532        state: &Glm5NextDsaState,
533        q_pos_dev: DevicePtr,
534        replay_safe: bool,
535        stream: u64,
536    ) -> Result<()> {
537        let w = &self.workspace;
538        let geom = state.geometry(&self.cfg, 1)?;
539
540        // Selector Q and head weights, FP32 straight out of the GEMM.
541        gemm(
542            gpu,
543            self.kernels.gemm_f32,
544            self.kernels.gemv_f32,
545            // No FP32-out batchm twin exists; the selector's two sites stay on gemv/tile.
546            KernelHandle(0),
547            w.q_resid.offset(row * self.cfg.q_lora_rank * 2),
548            self.weights.wq_b,
549            w.q_idx,
550            1,
551            self.cfg.index_heads * self.cfg.index_head_dim,
552            self.cfg.q_lora_rank,
553            stream,
554        )?;
555        // 🔴 `head_weights` is NOT computed here any more — see `indexer_forward`. It used to
556        // be, from `w.q_resid` with `K = cfg.hidden`, which was wrong twice over: the
557        // reference projects the LAYER INPUT (`gen_dsa_indexer_golden.py`:
558        // `weights_proj(hidden) * NH**-0.5`), and `q_resid` is only `[q_lora_rank] = 1536`
559        // BF16, so reading 4096 of them ran **5120 bytes past the end of the allocation**.
560        // That out-of-bounds read was ANOMALIES A55: the head weights were a function of
561        // whatever the allocator had placed after `q_resid`, which is why the model's output
562        // moved when the heap moved, when allocations were zeroed, and when an unrelated
563        // buffer was added. Found by red-zoning the allocator and bisecting the guard bands.
564
565        let inputs = DsaSelectInputs {
566            k_normed: state.k_normed,
567            gate: state.gate,
568            valid: state.valid,
569            ape: self.weights.ape,
570            q: w.q_idx,
571            weights: w.head_weights,
572            q_pos: q_pos_dev,
573            // Always 1 for a decode step; written once at workspace alloc, never per token.
574            q_mask: w.q_mask,
575            first_key: 0,
576            geom_dev: if replay_safe {
577                w.geom_dev
578            } else {
579                DevicePtr::NULL
580            },
581        };
582        // Under capture the grid and the shared-memory request go to the context CEILING and
583        // the live extents come off `geom_dev`, so ONE graph serves every context length.
584        let launch = if replay_safe {
585            super::select::DsaSelectLaunch::Ceiling {
586                max_pools: super::select::contiguous_pool_count(
587                    self.cfg.index_kpool,
588                    super::state::max_dsa_context(&self.cfg),
589                ),
590            }
591        } else {
592            super::select::DsaSelectLaunch::Exact
593        };
594        let t = crate::layers::glm5next_layer::profile::start();
595        // Row `row` of the `[max_rows, out_width]` selection scratch. The kernels still run
596        // one query row (`q_rows == 1`), they just land in this row's slot, so `attend_rows`
597        // can read all K index rows in one launch.
598        select_tokens(
599            gpu,
600            &self.select_kernels,
601            &self.cfg,
602            &geom,
603            &inputs,
604            &w.select.row(row, &self.cfg),
605            launch,
606            stream,
607        )?;
608        use crate::layers::glm5next_layer::profile;
609        profile::end(profile::DSA_SELECT, t, gpu, stream);
610        Ok(())
611    }
612
613    /// The selection for ALL `k` query rows in ONE pass — the prefill twin of
614    /// [`Self::attend_rows`]. On by default; disabled by `AVAROK_DSA_SELECT_ROWS=0`.
615    ///
616    /// Per 9,000-token prefill this replaces 4 x 9,000 x 11 single-row launches. Measured
617    /// at `6228baa2` (nsys s3-candcap, n3+n4): `dsa_topk_pools` and `dsa_expand_selection`
618    /// each run **grid(1,1,1)** — one block, 48 SMs idle — 99,000 times for 5.773 s and
619    /// 5.071 s, with `dsa_index_scores` a further 4.211 s. 15.06 s of a 147.8 s prefill
620    /// spent at ~2 % occupancy. Each row's block does exactly the work its own launch did;
621    /// only the launch count changes.
622    ///
623    /// Exactness, in three parts, all of them properties the kernels already have:
624    ///   1. `q_pos` is `[Q]` and the candidacy test reads `q_pos[r]`, so causality is
625    ///      per-row. A pool that ends past row `r` is not a candidate for row `r`.
626    ///   2. `pool_valid[p]` gates completeness, so the in-progress pool is excluded for
627    ///      every row exactly as it is today.
628    ///   3. Non-candidates score `-FLT_MAX` and `dsa_topk_pools` selects under a total
629    ///      order over (score, pool index), so a longer `P` walk reaches the identical
630    ///      top-`select_k` set AND order.
631    ///
632    /// Widening `P` to the group's pool count therefore cannot change any row's selection.
633    ///
634    /// The `q_idx` projections stay per-row: there is no FP32-out `batchm` twin, and they
635    /// are 2.765 s of `dense_gemv_bf16_fp32out` that this lane does not claim.
636    #[allow(clippy::too_many_arguments)]
637    fn select_rows_batched(
638        &self,
639        gpu: &dyn GpuBackend,
640        k: usize,
641        state: &Glm5NextDsaState,
642        q_pos_host: &[i32],
643        stream: u64,
644    ) -> Result<()> {
645        let w = &self.workspace;
646        // `q_rows = k`, and `len` is the cache length AFTER all k indexer writes — which is
647        // the point: `P` is the group's final pool count and each row masks itself back down
648        // to its own horizon via `q_pos[r]`.
649        let geom = state.geometry(&self.cfg, k)?;
650        let idx_row = self.cfg.index_heads * self.cfg.index_head_dim;
651        // 🔴 ONE GEMM for all k rows, not k GEMVs. This loop used to run an M=1 GEMV per
652        // row — at 256 prefill rows x 11 DSA layers, 2,816 launches per chunk, each one
653        // sweeping the ENTIRE `wq_b` weight to produce a single row. 256 full weight reads
654        // where one suffices.
655        //
656        // It was written that way for a concrete reason, recorded in the comment this
657        // replaces: "no FP32-out batchm twin exists". `q_idx_rows` is FP32, and Atlas's
658        // batched-M kernel only has a BF16-out form, so `batchm` was passed as
659        // `KernelHandle(0)` and `dense_mm_bf16` had no batched tier to route to. The twin
660        // now exists — `cublas_bf16_proj_dense_f32_out` — so the reason is gone.
661        //
662        // Shapes line up with no repacking: `q_resid` is a contiguous `[k, q_lora_rank]`
663        // BF16 block (the loop indexed it by `row * q_lora_rank`) and `q_idx_rows` a
664        // contiguous `[k, idx_row]` FP32 one, which is exactly `out[M,N] = act[M,K] @ Wt`.
665        //
666        // 🪤 PREFILL ONLY, and not by inference: `select_rows_batched` is reached solely
667        // through `batch_select`, which requires `is_prefill && !graph_capture && k > 1`.
668        // Decode and the speculative verify go through `select_row`, whose M=1 GEMV is
669        // untouched — so the bit-exact tiers still produce what they always did. This does
670        // reassociate, and the DSA indexer feeds a top-k over KV positions, so a tie near
671        // the selection boundary could pick a different token: that is why the arm is
672        // measured on long-context needle recall, not just on throughput.
673        if crate::layers::glm5next_layer::dsa_batch_qidx() {
674            crate::layers::ops::cublas_bf16_proj_dense_f32_out(
675                w.q_resid,
676                self.weights.wq_b,
677                w.q_idx_rows,
678                k as u32,
679                idx_row as u32,
680                self.cfg.q_lora_rank as u32,
681                stream,
682            )?;
683        } else {
684            for row in 0..k {
685                gemm(
686                    gpu,
687                    self.kernels.gemm_f32,
688                    self.kernels.gemv_f32,
689                    // Same as `select_row`: no FP32-out batchm twin exists.
690                    KernelHandle(0),
691                    w.q_resid.offset(row * self.cfg.q_lora_rank * 2),
692                    self.weights.wq_b,
693                    w.q_idx_rows.offset(row * idx_row * 4),
694                    1,
695                    idx_row,
696                    self.cfg.q_lora_rank,
697                    stream,
698                )?;
699            }
700        }
701        let q_pos_bytes: Vec<u8> = q_pos_host.iter().flat_map(|p| p.to_le_bytes()).collect();
702        gpu.copy_h2d(&q_pos_bytes, w.q_pos_rows)?;
703        let inputs = DsaSelectInputs {
704            k_normed: state.k_normed,
705            gate: state.gate,
706            valid: state.valid,
707            ape: self.weights.ape,
708            q: w.q_idx_rows,
709            weights: w.head_weights_rows,
710            q_pos: w.q_pos_rows,
711            q_mask: w.q_mask_rows,
712            first_key: 0,
713            // Host geometry only. The device-geometry path is decode-only and
714            // `select_tokens` refuses it at `q_rows > 1`.
715            geom_dev: DevicePtr::NULL,
716        };
717        let t = crate::layers::glm5next_layer::profile::start();
718        // The BASE of the `[max_rows, out_width]` scratch, not a row slice: the kernels
719        // carry the row axis themselves, so rows 0..k land in their own slots and
720        // `attend_rows` reads all k exactly as before.
721        select_tokens(
722            gpu,
723            &self.select_kernels,
724            &self.cfg,
725            &geom,
726            &inputs,
727            &w.select,
728            super::select::DsaSelectLaunch::Exact,
729            stream,
730        )?;
731        crate::layers::glm5next_layer::profile::end(
732            crate::layers::glm5next_layer::profile::DSA_SELECT,
733            t,
734            gpu,
735            stream,
736        );
737        Ok(())
738    }
739
740    /// The gather-attend for ALL `rows` query rows in ONE launch.
741    ///
742    /// Every row reads the same paged latent cache with its own selection row, its own
743    /// `seq_len` and its own block-table row, so `gridDim.y` carries the row axis and the
744    /// per-row arithmetic is untouched — bit-identical to the serial launches it replaces.
745    /// `q_abs`, `attn_out`, the metadata's `seq_len`/`block_table` and the selection scratch
746    /// are all `[rows, ...]` at the SAME strides the kernel indexes.
747    #[allow(clippy::too_many_arguments)]
748    fn attend_rows(
749        &self,
750        gpu: &dyn GpuBackend,
751        rows: usize,
752        state: &Glm5NextDsaState,
753        kv_cache: &PagedKvCache,
754        block_table_dev: DevicePtr,
755        seq_lens_dev: DevicePtr,
756        paging: &DsaDecodePaging,
757        stream: u64,
758    ) -> Result<()> {
759        use crate::layers::glm5next_layer::profile;
760        let w = &self.workspace;
761        // Only `out_width` and the `q_rows == num_seqs` agreement are read here.
762        let geom = state.geometry(&self.cfg, rows)?;
763        let paging = DsaDecodePaging {
764            num_seqs: rows,
765            ..*paging
766        };
767        let t = profile::start();
768        let pool = kv_cache.k_pool_ptr(self.attn_layer_idx);
769        decode_attention(
770            gpu,
771            self.decode_kernel,
772            &self.cfg,
773            &geom,
774            &paging,
775            &DsaDecodeInputs {
776                q: w.q_abs,
777                k_cache: pool,
778                v_cache: pool, // absorbed NoPE MLA: K and V are the same latent
779                out: w.attn_out,
780                block_tables: block_table_dev,
781                seq_lens: seq_lens_dev,
782                sel_indices: w.select.tokens(),
783                k_scale: self.kv_scale,
784                v_scale: self.kv_scale,
785            },
786            stream,
787        )?;
788        profile::end(profile::DSA_ATTEND, t, gpu, stream);
789        Ok(())
790    }
791    /// ONE drafter CONTEXT row: the KV latent and the indexer entry, with no query, no
792    /// selection and no attend.
793    ///
794    /// The MTP drafter's context rows only have to EXIST in these two caches — their block
795    /// output is discarded. Both caches are pure functions of the row's own input, exactly as
796    /// the Qwen drafter prefill exploits, so a context row costs `kv_a` + `latent_write` + the
797    /// indexer's `wk`, not a decode step. No MoE, no `o_proj`, no `lm_head`.
798    ///
799    /// 🪤 `seq_len` is BOTH the row's KV slot and its RoPE position (the indexer takes its
800    /// position from `state.len()`), so the drafter's row space must stay DENSE — every pair
801    /// key from 0 up must have been written. That is what `prefill_drafter` + the catch-up
802    /// feed are for.
803    #[allow(clippy::too_many_arguments)]
804    pub fn write_kv_row(
805        &self,
806        hidden: DevicePtr,
807        state: &mut dyn LayerState,
808        kv_cache: &mut PagedKvCache,
809        seq_len: usize,
810        block_table: &mut Vec<u32>,
811        ctx: &ForwardContext,
812        stream: u64,
813    ) -> Result<()> {
814        let gpu = ctx.gpu;
815        let st = state
816            .as_any_mut()
817            .downcast_mut::<Glm5NextDsaState>()
818            .ok_or_else(|| {
819                anyhow::anyhow!("Glm5NextDsaLayer got a state that is not Glm5NextDsaState")
820            })?;
821        match st.len().cmp(&seq_len) {
822            std::cmp::Ordering::Greater => st.rewind_to(seq_len)?,
823            std::cmp::Ordering::Less => bail!(
824                "DSA layer {}: indexer cache holds {} rows but the drafter is at {seq_len} — \
825                 rows are MISSING, not merely stale.",
826                self.layer_idx,
827                st.len()
828            ),
829            std::cmp::Ordering::Equal => {}
830        }
831        let w = &self.workspace;
832        gemm(
833            gpu,
834            self.kernels.gemm,
835            self.kernels.gemv,
836            self.kernels.gemv_batchm,
837            hidden,
838            self.weights.kv_a_proj,
839            w.kv_a,
840            1,
841            self.cfg.kv_lora_rank,
842            self.cfg.hidden,
843            stream,
844        )?;
845        let block_size = kv_cache.config().block_size;
846        let logical = seq_len / block_size;
847        let physical = *block_table.get(logical).ok_or_else(|| {
848            anyhow::anyhow!(
849                "DSA layer {}: block table has {} entries, needs logical block {logical} for \
850                 drafter row {seq_len}",
851                self.layer_idx,
852                block_table.len()
853            )
854        })? as usize;
855        let slot = (physical * block_size + seq_len % block_size) as i64;
856        gpu.copy_h2d(&slot.to_le_bytes(), w.slot)?;
857        KernelLaunch::new(gpu, self.kernels.latent_write)
858            .grid([1, 1, 1])
859            .block([self.cfg.kv_lora_rank as u32, 1, 1])
860            .arg_ptr(w.kv_a)
861            .arg_ptr(self.weights.kv_a_layernorm)
862            .arg_ptr(kv_cache.k_pool_ptr(self.attn_layer_idx))
863            .arg_ptr(w.slot)
864            .arg_u32(self.cfg.kv_lora_rank as u32)
865            .arg_f32(self.rms_eps)
866            .arg_f32(1.0 / self.kv_scale)
867            .launch(stream)?;
868        self.indexer_forward(gpu, hidden, st, None, stream)
869    }
870
871    /// K tokens of one sequence: the projections batched, selection and attention NOT.
872    ///
873    /// The weight-heavy halves — `q_a`, the absorbed `q_b`, `kv_a` and the `o_absorb` output
874    /// projection — sweep their weights ONCE for all K rows (1,290 MB/rank/token between them).
875    /// Everything between them is a function of the individual token's position: the paged KV
876    /// slot, the indexer row, the selector geometry over `[0, len)` and the gather-attend.
877    ///
878    /// 🔴 Bit-identical to K serial [`TransformerLayer::decode`] calls, which is the
879    /// requirement: an accepted draft must be the token the unspeculated engine would have
880    /// emitted. `ops::dense_mm_bf16` reproduces each row's K-iteration order and reduction tree,
881    /// and `rms_norm_vanilla`'s grid is the token axis.
882    ///
883    /// 🪤 REFUSES a SCALAR (`num_seqs == 1`) `attn_metadata` at k > 1. Those scalars — position,
884    /// KV slot, seq len — describe ONE token, so K rows sharing them would write K queries into
885    /// the same paged slot and select over the same position: a wrong answer with no shape error.
886    ///
887    /// 🔴 It ACCEPTS a K-ROW `attn_metadata` (`num_seqs == k`), which is what the graphed verify
888    /// paths (`verify_b`/`verify_c`) already upload: positions `[k]` u32, slot `[k]` i64, seq_len
889    /// `[k]` i32, block_table `[k][max_blocks_per_seq]` i32, all at stable device addresses
890    /// written BEFORE capture or replay. Row `r` reads element `r` of each. Without this the
891    /// layer fell through to its own per-row `copy_h2d`, and an H2D on a capturing stream fails
892    /// with CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED — which is why the K-token verify was eager.
893    /// At `k == 1` every row offset is 0, so that path is unchanged byte for byte.
894    #[allow(clippy::too_many_arguments)]
895    pub fn decode_k(
896        &self,
897        hidden: DevicePtr,
898        k: usize,
899        state: &mut dyn LayerState,
900        kv_cache: &mut PagedKvCache,
901        seq_len: usize,
902        block_table: &mut Vec<u32>,
903        ctx: &ForwardContext,
904        stream: u64,
905        // TRUE only on a prefill sub-chunk. Passed explicitly by
906        // [`crate::layers::glm5next_layer::Glm5NextLayer::forward_k`]'s two call sites; it
907        // is NOT inferable from the context. `decode_step` is false for prefill AND for a
908        // speculative verify, and `graph_capture` is false for prefill AND for an EAGER
909        // verify — `verify_a` hard-codes `graph_capture: false`, and `verify_b/c/c2/d` set
910        // it from `use_graphs`, which is false under `AVAROK_GLM_VERIFY_GRAPHS=0`, under
911        // high-speed swap, and under `AVAROK_LORA_EAGER`. See `select_rows_batched`.
912        is_prefill: bool,
913    ) -> Result<()> {
914        use crate::layers::glm5next_layer::profile;
915        // Captured before the KV borrows below, for the block-table trim at the
916        // upload site. See the comment there (ANOMALIES A58).
917        let bt_block_size = kv_cache.block_size().max(1);
918        let st = state
919            .as_any_mut()
920            .downcast_mut::<Glm5NextDsaState>()
921            .ok_or_else(|| {
922                anyhow::anyhow!("Glm5NextDsaLayer got a state that is not Glm5NextDsaState")
923            })?;
924        // 🔴 The indexer stream must advance in lockstep with the KV cache — a drift selects
925        // over the wrong context. Two drifts are possible and they are NOT symmetric:
926        //
927        // * AHEAD (`len > seq_len`) is the speculative-verify reject. The K rows of a verify
928        //   were written, the sequence rolled back to the accepted prefix, and the rows past
929        //   it are now unreachable: the selector reads `[0, len)` and the next write starts
930        //   at `seq_len`, so they are overwritten before anything can select over them.
931        //   Rewind and continue — this is the KV cache's own semantics for rejected slots,
932        //   and making it self-healing here is why no rollback callback has to reach into
933        //   eleven DSA layers.
934        // * BEHIND (`len < seq_len`) means rows were never written. Nothing can repair that,
935        //   so it stays a hard error.
936        match st.len().cmp(&seq_len) {
937            std::cmp::Ordering::Greater => st.rewind_to(seq_len)?,
938            std::cmp::Ordering::Less => bail!(
939                "DSA layer {}: indexer cache holds {} tokens but the sequence is at {} — \
940                 rows are MISSING, not merely stale. The indexer stream must advance in \
941                 lockstep with the KV cache.",
942                self.layer_idx,
943                st.len(),
944                seq_len
945            ),
946            std::cmp::Ordering::Equal => {}
947        }
948        if k == 0 || k > self.workspace.max_rows {
949            bail!(
950                "DSA layer {}: a {k}-token verify does not fit a workspace built for {}",
951                self.layer_idx,
952                self.workspace.max_rows
953            );
954        }
955        // A K-row pass may use `attn_metadata` ONLY when it carries one entry per row.
956        if k > 1
957            && let Some(m) = ctx.attn_metadata.as_ref()
958            && m.num_seqs as usize != k
959            && ctx.decode_step
960        {
961            bail!(
962                "DSA layer {}: a {k}-row pass cannot share attn_metadata describing {} \
963                 token(s) — its position and KV slot describe a single token",
964                self.layer_idx,
965                m.num_seqs
966            );
967        }
968        // 🪤 Guarded on `num_seqs == k`, NOT on `decode_step`. A chunked prefill also passes
969        // metadata through this call at k == 1, and its `num_seqs` is the chunk width — so a
970        // one-token chunk is the only case where the two could be confused, and `decode_step`
971        // still separates them there.
972        let rowwise_meta = (k > 1)
973            .then_some(ctx.attn_metadata.as_ref())
974            .flatten()
975            .filter(|m| m.num_seqs as usize == k);
976        let gpu = ctx.gpu;
977        let w = &self.workspace;
978        let t_proj = crate::layers::glm5next_layer::profile::start();
979
980        // ── q path ──
981        gemm(
982            gpu,
983            self.kernels.gemm,
984            self.kernels.gemv,
985            self.kernels.gemv_batchm,
986            hidden,
987            self.weights.q_a_proj,
988            w.q_a,
989            k,
990            self.cfg.q_lora_rank,
991            self.cfg.hidden,
992            stream,
993        )?;
994        // 🪤 vanilla: x * rms * w, no `1 +`.
995        KernelLaunch::new(gpu, self.kernels.rms_norm)
996            // 🪤 `rms_norm_vanilla`'s grid IS the token axis, so k rows is one launch doing
997            // block-for-block what k launches did — bit-identical.
998            .grid([k as u32, 1, 1])
999            .block([256, 1, 1])
1000            .arg_ptr(w.q_a)
1001            .arg_ptr(self.weights.q_a_layernorm)
1002            .arg_ptr(w.q_resid)
1003            .arg_u32(self.cfg.q_lora_rank as u32)
1004            .arg_f32(self.rms_eps)
1005            .launch(stream)?;
1006        // Q absorbed into latent space in one GEMM.
1007        gemm(
1008            gpu,
1009            self.kernels.gemm,
1010            self.kernels.gemv,
1011            self.kernels.gemv_batchm,
1012            w.q_resid,
1013            self.weights.q_absorb,
1014            w.q_abs,
1015            k,
1016            self.cfg.local_heads * self.cfg.kv_lora_rank,
1017            self.cfg.q_lora_rank,
1018            stream,
1019        )?;
1020
1021        // ── kv path: latent -> FP8 -> paged slot ──
1022        gemm(
1023            gpu,
1024            self.kernels.gemm,
1025            self.kernels.gemv,
1026            self.kernels.gemv_batchm,
1027            hidden,
1028            self.weights.kv_a_proj,
1029            w.kv_a,
1030            k,
1031            self.cfg.kv_lora_rank,
1032            self.cfg.hidden,
1033            stream,
1034        )?;
1035        let mut attend_bt = DevicePtr::NULL;
1036        let mut attend_sl = DevicePtr::NULL;
1037        // Row 0 allocates the shared bt/sl scratch on the per-step-alloc path; it is freed
1038        // after the attend, which reads it. ANOMALIES A65.
1039        let mut owns_scratch = false;
1040        let mut attend_paging: Option<DsaDecodePaging> = None;
1041        // 🔴 PREFILL ONLY, and said so EXPLICITLY. `!ctx.graph_capture` does NOT mean
1042        // "prefill": `verify_a` builds its context with `graph_capture: false` outright, and
1043        // `verify_b/c/c2/d/fused` set it from `use_graphs`, which is false whenever
1044        // `AVAROK_GLM_VERIFY_GRAPHS=0`, high-speed swap is engaged, or `AVAROK_LORA_EAGER` is
1045        // set. `!ctx.decode_step` does not separate them either — a verify sets it false too.
1046        // So the caller states it. An eager K-row verify would otherwise silently take a path
1047        // that was measured and qualified on prefill alone.
1048        //
1049        // `!ctx.graph_capture` is KEPT as a second, independent condition rather than
1050        // replaced: `select_rows_batched` does a host `copy_h2d` of `q_pos`, which is
1051        // CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED inside a recording stream. Both must hold.
1052        //
1053        // The buffers are NULL under `AVAROK_DSA_SELECT_ROWS=0`, so this is false on that arm
1054        // and the heap layout is unchanged there.
1055        let batch_select =
1056            batch_select_enabled(w.q_idx_rows.0 != 0, is_prefill, ctx.graph_capture, k);
1057        let mut batch_q_pos: Vec<i32> = Vec::with_capacity(if batch_select { k } else { 0 });
1058        for row in 0..k {
1059            let pos = seq_len + row;
1060            let block_size = kv_cache.config().block_size;
1061            // 🔴 Every per-step scalar this layer needs — position, KV slot, seq_len, block
1062            // table — is ALREADY uploaded once per decode step by `decode_a` into
1063            // `attn_metadata`, at stable addresses, BEFORE any graph capture or replay. Reading
1064            // those pointers instead of doing our own `copy_h2d` removes FIVE blocking H2Ds
1065            // (each one a `cuStreamSynchronize`) per DSA layer per token — 55 stream drains on
1066            // this model — and is what makes the decode step capturable at all: an H2D inside a
1067            // capturing stream fails with CUDA_ERROR_STREAM_CAPTURE_UNSUPPORTED.
1068            //
1069            // 🪤 The two encodings must agree byte for byte, and they do: `positions` is the
1070            // u32 `seq_len` (same bits as our i32), `slot` the same i64 `block*block_size +
1071            // seq_len % block_size`, `seq_len` the same i32 `seq_len + 1`, and `block_table`
1072            // the same ids as i32 rather than u32.
1073            // 🪤 ONLY on a real decode step — `prefill_default` calls this same `decode` per
1074            // token with the prefill context, where these are arrays or NULL. See
1075            // `ForwardContext::decode_step`.
1076            let meta = if ctx.decode_step {
1077                ctx.attn_metadata.as_ref()
1078            } else {
1079                rowwise_meta
1080            };
1081            // Row strides into the K-row arrays. At k == 1 every one of these is 0.
1082            let bt_stride = meta.map_or(0, |m| m.max_blocks_per_seq as usize) * 4;
1083            let slot_dev = match meta {
1084                Some(m) => m.slot.offset(row * 8),
1085                None => {
1086                    let logical = pos / block_size;
1087                    let physical = *block_table.get(logical).ok_or_else(|| {
1088                        anyhow::anyhow!(
1089                            "DSA layer {}: block table has {} entries, needs logical block \
1090                             {logical} for position {pos}",
1091                            self.layer_idx,
1092                            block_table.len()
1093                        )
1094                    })? as usize;
1095                    let slot = (physical * block_size + pos % block_size) as i64;
1096                    gpu.copy_h2d(&slot.to_le_bytes(), w.slot)?;
1097                    w.slot
1098                }
1099            };
1100            KernelLaunch::new(gpu, self.kernels.latent_write)
1101                .grid([1, 1, 1])
1102                .block([self.cfg.kv_lora_rank as u32, 1, 1])
1103                .arg_ptr(w.kv_a.offset(row * self.cfg.kv_lora_rank * 2))
1104                .arg_ptr(self.weights.kv_a_layernorm)
1105                .arg_ptr(kv_cache.k_pool_ptr(self.attn_layer_idx))
1106                .arg_ptr(slot_dev)
1107                .arg_u32(self.cfg.kv_lora_rank as u32)
1108                .arg_f32(self.rms_eps)
1109                .arg_f32(1.0 / self.kv_scale)
1110                .launch(stream)?;
1111
1112            // ── indexer stream, then select + gather-attend ──
1113            use crate::layers::glm5next_layer::profile;
1114            profile::end(profile::DSA_PROJ, t_proj, gpu, stream);
1115            let t = profile::start();
1116            // Replay-safe placement only while a graph is RECORDING. An eager step keeps the
1117            // host-offset path, so the shipping numbers and byte-identity are untouched.
1118            let replay_safe = ctx.graph_capture
1119                && meta.is_some()
1120                && self.select_kernels.indexer_store.0 != 0
1121                && self.select_kernels.write_geom.0 != 0;
1122            let pos_dev = if replay_safe {
1123                meta.map(|m| m.positions.offset(row * 4))
1124            } else {
1125                None
1126            };
1127            self.indexer_forward(
1128                gpu,
1129                hidden.offset(row * self.cfg.hidden * 2),
1130                st,
1131                pos_dev,
1132                stream,
1133            )?;
1134            profile::end(profile::DSA_INDEXER, t, gpu, stream);
1135
1136            let (q_pos_dev, bt_dev_meta, sl_dev_meta) = match meta {
1137                Some(m) => (
1138                    m.positions.offset(row * 4),
1139                    Some(m.block_table.offset(row * bt_stride)),
1140                    Some(m.seq_len.offset(row * 4)),
1141                ),
1142                None => {
1143                    let qp = pos as i32;
1144                    gpu.copy_h2d(&qp.to_le_bytes(), w.q_pos)?;
1145                    (w.q_pos, None, None)
1146                }
1147            };
1148            let (d_bt, d_sl) = match (bt_dev_meta, sl_dev_meta) {
1149                // The step-scoped upload already holds both; nothing to copy.
1150                (Some(b), Some(l)) => (b, l),
1151                _ => {
1152                    // Upload only the prefix the gather can index. The paged gather reads
1153                    // `block_table[pos / block_size]` for `pos` in `[0, seq_len + k)` (the
1154                    // `block_table.get(logical)` site above), so every entry past
1155                    // `(seq_len + k) / block_size + 1` is dead weight on the wire.
1156                    //
1157                    // 🔴 ANOMALIES A58: it was also a silent kill switch for the GLM drafter.
1158                    // `Glm5NextMtpHead::alloc_state` pre-claims its whole private pool up front
1159                    // (`max_seq_len / 16 + 2` entries — a mid-decode allocation inside a captured
1160                    // region is not an option), so at `--max-seq-len 262144` it presented 16,386
1161                    // entries for a 20-token sequence against a `bt_cap` of 16,384. `bt_cap` is
1162                    // `max_dsa_context`, a count of TOKENS used as a count of BLOCKS — the two
1163                    // collide at 16,384. Every propose then bailed, the drafter produced nothing,
1164                    // and acceptance read exactly `p1 = 0.000` while the target kept verifying
1165                    // correctly and emitting byte-identical output. Measured 2026-08-30: healthy
1166                    // at 196,608, dead at 262,144, output identical on both.
1167                    let bt_used = {
1168                        let needed = bt_entries_needed(seq_len, k, bt_block_size);
1169                        &block_table[..needed.min(block_table.len())]
1170                    };
1171                    let bt: Vec<u8> = bt_used.iter().flat_map(|b| b.to_le_bytes()).collect();
1172                    if bt_used.len() > w.bt_cap {
1173                        anyhow::bail!(
1174                            "DSA layer {}: block table needs {} entries for seq_len {} + {} rows \
1175                     but the persistent buffer holds {}. This is a BLOCK count against a buffer \
1176                     sized by max_dsa_context (a TOKEN count); do not write past the allocation.",
1177                            self.layer_idx,
1178                            bt_used.len(),
1179                            seq_len,
1180                            k,
1181                            w.bt_cap
1182                        );
1183                    }
1184                    // Persistent `w.bt`/`w.sl` instead of a `gpu.alloc` + `gpu.free` per DSA layer per
1185                    // token: worth a measured 1.1 ms/token (nsys 2026-08-28 — 11 x ~98 us of GPU idle
1186                    // for the alloc/copy/free cluster). Kill switch `AVAROK_GLM_DSA_ALLOC_PER_STEP=1`.
1187                    //
1188                    // 🪤 This was gated OFF for most of a day because turning it on changed the model's
1189                    // output — which turned out to be ANOMALIES A55 and not this code at all: the DSA
1190                    // indexer was reading 5120 bytes past `q_resid`, so the answer depended on what the
1191                    // allocator had put next. With that fixed the two settings are byte-identical, and
1192                    // the whole engine is layout-independent (verified by 4 KB poisoned guard bands on
1193                    // 3431 allocations producing the same completions as no guard bands at all).
1194                    // 🔴 ANOMALIES A65. The deferred `attend_rows` reads `seq_lens[row]` and
1195                    // `block_tables + row * max_blocks_per_seq`, so these two buffers outlive
1196                    // the row that wrote them. `sl` is `[k]` and each row writes its OWN slot;
1197                    // the block table is uploaded once and shared with a row stride of ZERO
1198                    // (see `max_blocks_per_seq` below) — the k rows ARE one sequence, so they
1199                    // genuinely share one table. Writing a single-row `sl`/`bt` per row left
1200                    // every row past the first attending over another row's (or no) memory.
1201                    // `bt_entries_needed(seq_len, k, ..)` does not depend on `row`, so the
1202                    // table's length is the same on every pass and re-uploading it is a no-op.
1203                    let (d_bt, d_sl) = if self.persist_bt {
1204                        (w.bt, w.sl)
1205                    } else if row == 0 {
1206                        (gpu.alloc(bt.len().max(4))?, gpu.alloc(k * 4)?)
1207                    } else {
1208                        // Row 0 owns the scratch; later rows write their own `sl` slot into it.
1209                        (attend_bt, attend_sl)
1210                    };
1211                    gpu.copy_h2d(&bt, d_bt)?;
1212                    gpu.copy_h2d(&((pos + 1) as i32).to_le_bytes(), d_sl.offset(row * 4))?;
1213                    (d_bt, d_sl)
1214                }
1215            };
1216            let owns_bt = bt_dev_meta.is_none();
1217
1218            let paging = DsaDecodePaging {
1219                num_seqs: 1,
1220                num_q_heads: self.cfg.local_heads,
1221                num_kv_heads: 1,
1222                // 🔴 THIS IS A KERNEL ARGUMENT, so a CUDA graph BAKES IT IN at capture time.
1223                // `block_table.len()` grows every time the sequence crosses a block boundary,
1224                // so a captured graph replayed at a longer context keeps walking the capture's
1225                // block count — right answer for the first few tokens, wrong one after. Take
1226                // the metadata's ceiling, which verify_b/verify_c hold CONSTANT at
1227                // `self.max_blocks_per_seq` and zero-pad every uploaded row out to.
1228                // Without this the graphed K=3 verify diverged from eager on exactly the long
1229                // probes (pyadd/open128/open512) and matched on the 32-token ones.
1230                // 🔴 ANOMALIES A65: ZERO on the no-metadata path, which is the ROW STRIDE
1231                // the kernel applies to `block_tables`. All k rows of this call are the same
1232                // sequence and share the one table uploaded above, so a stride of 0 is the
1233                // correct sharing — `block_table.len()` walked row 1 off the end of a
1234                // single-row buffer. (The metadata path really does carry k padded rows.)
1235                max_blocks_per_seq: match meta {
1236                    Some(m) => m.max_blocks_per_seq as usize,
1237                    None => 0,
1238                },
1239                block_size,
1240                cache_stride_bytes: (block_size * self.cfg.kv_lora_rank) as u64,
1241            };
1242            if replay_safe {
1243                // S is exactly the `seq_len + 1` the attention metadata already holds, which is
1244                // `st.len()` after the indexer advance. Nothing about the pass is host-decided.
1245                KernelLaunch::new(gpu, self.select_kernels.write_geom)
1246                    .grid([1, 1, 1])
1247                    .block([1, 1, 1])
1248                    .arg_ptr(d_sl)
1249                    .arg_ptr(w.geom_dev)
1250                    .arg_u32(self.cfg.index_kpool as u32)
1251                    .arg_u32(self.cfg.index_topk as u32)
1252                    .arg_u32(super::select::topk_tile() as u32)
1253                    .launch(stream)?;
1254            }
1255            if batch_select {
1256                // `indexer_forward` left THIS row's head weights in the scalar slot; stash
1257                // them at row stride so the one batched pass below can read `weights[r*H]`.
1258                gpu.copy_d2d_async(
1259                    w.head_weights,
1260                    w.head_weights_rows.offset(row * self.cfg.index_heads * 4),
1261                    self.cfg.index_heads * 4,
1262                    stream,
1263                )?;
1264                batch_q_pos.push(pos as i32);
1265            } else {
1266                self.select_row(gpu, row, st, q_pos_dev, replay_safe, stream)?;
1267            }
1268            // The attend needs the BASE of the K-row metadata, not this row's slice: it
1269            // carries the row axis on `gridDim.y`. On the no-metadata path k is 1 and these
1270            // are the single-row `w.bt`/`w.sl`, so the base IS the row.
1271            if row == 0 {
1272                attend_bt = d_bt;
1273                attend_sl = d_sl;
1274                attend_paging = Some(paging);
1275                owns_scratch = owns_bt && !self.persist_bt;
1276            }
1277        }
1278
1279        // ── ONE selection pass for all K rows (default; off under ...SELECT_ROWS=0) ──
1280        // AFTER the row loop, so every row's indexer write is already in the cache. That
1281        // ordering is what makes the hoist exact rather than merely cheaper: row `r` gates
1282        // on `end_c <= q_pos[r]`, so rows appended after it stay invisible to it, and the
1283        // pools this pass compresses over are the group's final set.
1284        if batch_select && !batch_q_pos.is_empty() {
1285            self.select_rows_batched(gpu, k, st, &batch_q_pos, stream)?;
1286        }
1287
1288        // ── ONE gather-attend for all K rows ──
1289        if let Some(paging) = attend_paging {
1290            self.attend_rows(gpu, k, st, kv_cache, attend_bt, attend_sl, &paging, stream)?;
1291        }
1292        // 🔴 ANOMALIES A65: freed HERE, not in the row loop. The attend above reads both
1293        // buffers, so freeing them per row handed it memory that had already been released.
1294        if owns_scratch {
1295            gpu.free(attend_bt)?;
1296            gpu.free(attend_sl)?;
1297        }
1298
1299        // ── output projection, row-parallel: the caller all-reduces ──
1300        let t_proj = profile::start();
1301        gemm(
1302            gpu,
1303            self.kernels.gemm,
1304            self.kernels.gemv,
1305            self.kernels.gemv_batchm,
1306            w.attn_out,
1307            self.weights.o_absorb,
1308            hidden,
1309            k,
1310            self.cfg.hidden,
1311            self.cfg.local_heads * self.cfg.kv_lora_rank,
1312            stream,
1313        )?;
1314        profile::end(profile::DSA_PROJ, t_proj, gpu, stream);
1315        Ok(())
1316    }
1317}
1318
1319impl TransformerLayer for Glm5NextDsaLayer {
1320    fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn LayerState>> {
1321        Ok(Box::new(Glm5NextDsaState::alloc(gpu, &self.cfg)?))
1322    }
1323
1324    /// Release what `alloc_state` allocated — ANOMALIES A76. Reached by the non-composite
1325    /// paths that hold a bare `Glm5NextDsaLayer`; the composite `Glm5NextLayer` has its
1326    /// own, identical, override. Type-driven so a non-DSA state can never be freed here.
1327    fn release_state(&self, state: &mut dyn LayerState, gpu: &dyn GpuBackend) -> Result<()> {
1328        if let Some(dsa) = state.as_any_mut().downcast_mut::<Glm5NextDsaState>() {
1329            dsa.free(gpu)?;
1330        }
1331        Ok(())
1332    }
1333
1334    /// The replay's writes end at `seq_len + k`; the buffer ends at `capacity`. A62.
1335    fn check_replay_room(&self, state: &dyn LayerState, seq_len: usize, k: usize) -> Result<()> {
1336        state
1337            .as_any()
1338            .downcast_ref::<Glm5NextDsaState>()
1339            .ok_or_else(|| {
1340                anyhow::anyhow!("Glm5NextDsaLayer got a state that is not Glm5NextDsaState")
1341            })?
1342            .ensure_room_through(seq_len + k)
1343            .with_context(|| {
1344                format!("DSA replay pre-check (before launch_graph, seq_len {seq_len} + k {k})")
1345            })
1346    }
1347
1348    /// The indexer cache length is the one thing this layer keeps on the host. A replayed
1349    /// graph writes the next row (the store kernel reads its position from device memory)
1350    /// but never calls `decode`, so the counter has to be advanced here or the NEXT eager
1351    /// step plans its selection over a stale length — and `decode`'s own lockstep check
1352    /// would fire.
1353    fn sync_replayed_step(
1354        &self,
1355        state: &mut dyn LayerState,
1356        seq_len: usize,
1357        k: usize,
1358    ) -> Result<()> {
1359        state
1360            .as_any_mut()
1361            .downcast_mut::<Glm5NextDsaState>()
1362            .ok_or_else(|| {
1363                anyhow::anyhow!("Glm5NextDsaLayer got a state that is not Glm5NextDsaState")
1364            })?
1365            .sync_to(seq_len, k)
1366    }
1367
1368    #[allow(clippy::too_many_arguments)]
1369    fn decode(
1370        &self,
1371        hidden: DevicePtr,
1372        _residual: DevicePtr,
1373        state: &mut dyn LayerState,
1374        kv_cache: &mut PagedKvCache,
1375        seq_len: usize,
1376        block_table: &mut Vec<u32>,
1377        _disk_block_ids: &mut Vec<u32>,
1378        _disk_last_offloaded_per_layer: &mut Vec<u32>,
1379        ctx: &ForwardContext,
1380        stream: u64,
1381    ) -> Result<()> {
1382        self.decode_k(
1383            hidden,
1384            1,
1385            state,
1386            kv_cache,
1387            seq_len,
1388            block_table,
1389            ctx,
1390            stream,
1391            // A single-token decode, never a prefill sub-chunk. Moot at k == 1, stated anyway.
1392            false,
1393        )
1394    }
1395}
1396
1397#[cfg(test)]
1398mod tests;
1399
1400/// Block-table entries the paged gather can index for `k` query rows starting at
1401/// `seq_len`, plus one entry of slack.
1402///
1403/// The gather reads `block_table[pos / block_size]` for `pos` in `[0, seq_len + k)`,
1404/// so the highest index touched is `(seq_len + k - 1) / block_size`. Everything above
1405/// that is never read — see the A58 note at the upload site for why uploading it
1406/// anyway was a silent kill switch for the GLM drafter.
1407pub(super) fn bt_entries_needed(seq_len: usize, k: usize, block_size: usize) -> usize {
1408    (seq_len + k) / block_size.max(1) + 2
1409}
1410
1411#[cfg(test)]
1412mod bt_trim_tests {
1413    use super::bt_entries_needed;
1414
1415    /// The A58 reproducer, in arithmetic: the GLM drafter pre-claims
1416    /// `max_seq_len / 16 + 2` entries, so at `--max-seq-len 262144` it hands 16,386
1417    /// against a `bt_cap` of 16,384 — for a 20-token sequence. Trimmed, it needs 3.
1418    #[test]
1419    fn a58_short_sequence_at_262k_declared_context() {
1420        let pool = 262_144 / 16 + 2;
1421        assert_eq!(pool, 16_386, "the pre-claimed pool that overran bt_cap");
1422        assert!(pool > 16_384, "and it is over the persistent buffer");
1423        assert_eq!(bt_entries_needed(20, 3, 16), 3);
1424        assert!(bt_entries_needed(20, 3, 16) <= 16_384);
1425    }
1426
1427    /// Every position the gather can touch must be inside the trim.
1428    #[test]
1429    fn trim_covers_every_indexable_position() {
1430        for &(seq_len, k, bs) in &[
1431            (0usize, 1usize, 16usize),
1432            (1, 1, 16),
1433            (15, 1, 16),
1434            (16, 1, 16),
1435            (17, 4, 16),
1436            (4095, 4, 16),
1437            (131_072, 3, 16),
1438            (262_143, 4, 16),
1439            (1000, 1, 64),
1440        ] {
1441            let n = bt_entries_needed(seq_len, k, bs);
1442            let highest = (seq_len + k).saturating_sub(1) / bs;
1443            assert!(
1444                highest < n,
1445                "seq_len={seq_len} k={k} bs={bs}: highest index {highest} not < {n}"
1446            );
1447        }
1448    }
1449
1450    /// The trim must stay far under the persistent buffer for any sequence DSA can
1451    /// actually select over (`max_dsa_context` = 16,384 tokens).
1452    #[test]
1453    fn trim_fits_the_persistent_buffer_across_the_dsa_window() {
1454        assert!(bt_entries_needed(16_384, 4, 16) <= 16_384);
1455    }
1456
1457    /// block_size 0 must not divide by zero.
1458    #[test]
1459    fn zero_block_size_does_not_panic() {
1460        assert_eq!(bt_entries_needed(8, 1, 0), 11);
1461    }
1462}