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}