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