spark_model/layers/glm5next_kda/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2//! GLM-5.3-Flash **KDA attention block** — the reusable production component.
3//!
4//! One [`Glm5NextKdaLayer`] is a fully bound KDA `self_attn` block. Any of the checkpoint's
5//! **34** KDA layers instantiates through the same path; the family is structurally uniform
6//! (one distinct name/shape/dtype signature across all 34 — see [`binding`]).
7//!
8//! ```text
9//! q|k|v_proj -> pack -> conv1d + SiLU -> L2(q,k only) -> kda_gate / sigmoid(b_proj)
10//!            -> kda_chunk (prefill) | kda_recurrent (decode)
11//!            -> sigmoid-gated RMSNorm(o_norm, g_b(g_a(h))) -> o_proj
12//! ```
13//!
14//! Scope: the attention block only. No DSA/MLA, no MoE/dense FFN, no mHC hyper-connection, no
15//! scheduler or cache integration. **The model is not loadable on this alone.**
16//!
17//! # Facts this component encodes (proven, do not re-derive)
18//!
19//! * **Nothing in a KDA block is quantised.** All 15 `self_attn` tensors are BF16 except `A_log`
20//!   and `dt_bias`, which are F32 — verified across all 34 blocks, 0 artefacts. There is no NVFP4
21//!   dequant and no NVFP4 GEMM anywhere on this path, so a real-checkpoint KDA oracle *is* the
22//!   production numerics.
23//! * ðŸŠĪ **The checkpoint SPLITS the conv; HF FUSES it.** HF holds one depthwise
24//!   `nn.Conv1d(conv_dim)`; the checkpoint stores `q_conv1d`/`k_conv1d`/`v_conv1d`, each
25//!   `[qkv, 1, kernel]`. Binding is `concat([q, k, v], dim=0)` **in that order** — the same order
26//!   as `mixed_qkv = cat([q_proj, k_proj, v_proj])`. Reordering is silent.
27//! * ðŸŠĪ **`squeeze(1)` is a shape-only fix.** `[dim, 1, ks]` and `[dim, ks]` have identical
28//!   row-major bytes, so nothing moves — but a loader trusting `shape.len() == 2` rejects the
29//!   tensor outright. [`binding`] asserts rank 3 and squeezes exactly once.
30//! * ðŸŠĪ **`o_norm` is ADAPT, not REUSE.** Every Atlas gated RMSNorm applies **SiLU** to the gate
31//!   (`kernels/gb10/common/rms_norm.cu`); GLM's `Glm5NextTextRMSNormGated` sets
32//!   `activation = "sigmoid"`. Shapes, dtypes and launch geometry all agree, which is why it was
33//!   first mis-classified. Hence `kda_o_norm_gated_*` in `kernels/gb10/common/kda_layer_ops.cu`.
34//! * ðŸŠĪ **`dense_gemm_bf16` writes `C[row * N + col]`** — its output row stride is `N` and there
35//!   is no caller-supplied output stride, so the three q/k/v projections **cannot** be aimed at
36//!   offsets inside one `[T, 3*qkv]` buffer. They would overwrite each other for `T > 1`, while
37//!   being silently correct at `T = 1`. Hence the separate parts buffer plus `kda_pack_qkv_bf16`.
38//! * **Conv state widths differ.** HF keeps `kernel - 1` slots, Atlas keeps `kernel` and shifts
39//!   left before convolving, so `HF[0..k-1] == Atlas[1..k]` and Atlas slot 0 is a don't-care.
40//! * **Decode conv fuses L2; prefill does not** and needs a separate `l2_norm_bf16` over q|k.
41//!   q/k are normalised **exactly once**; **V never**.
42//! * **The recurrent state is FP32 by reference semantics**, not Atlas policy — HF stores it via
43//!   `.to(torch.float32)` and vLLM's `kda_state_dtype` hardcodes fp32.
44
45pub mod binding;
46pub mod tp;
47/// Applying the TP plan: the shard copies, as an upstream adapter so the binder is untouched.
48pub mod tp_bind;
49
50use anyhow::{Result, bail};
51use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
52use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
53
54use crate::layers::ops;
55use crate::weight_map::DenseWeight;
56
57/// Dynamic shared memory available with no `cuFuncSetAttribute` opt-in in `AvarokCudaBackend`.
58/// GB10 reports `sharedMemPerBlockOptin = 101376`, real but unreachable from Atlas today.
59pub const SMEM_CEILING: usize = 49_152;
60
61const BLOCK: u32 = 128;
62
63/// Geometry and the config values that MUST be read from the checkpoint.
64///
65/// `gate_lower_bound`, `rms_norm_eps` and `hidden_act` coincide with a library default on
66/// GLM-5.3-Flash **by accident** — vLLM looks up the legacy key `lower_bound`, misses it, and
67/// falls back to a matching `-5.0`; it never passes `rms_norm_eps`; it hardcodes `"silu"`.
68/// Inheriting any of those defaults is a latent bug on the next checkpoint.
69#[derive(Clone, Copy, Debug)]
70pub struct Glm5NextKdaConfig {
71    pub hidden: usize,
72    pub heads: usize,
73    pub head_dim: usize,
74    pub conv_kernel: usize,
75    /// `linear_attn_config.gate_lower_bound`.
76    pub gate_lower_bound: f32,
77    /// `rms_norm_eps`, consumed by `o_norm`.
78    pub rms_norm_eps: f32,
79    /// FLA convention `1/sqrt(sum + eps)`, not `max(norm, eps)`. Not a config key.
80    pub l2_eps: f32,
81    /// Prefill tiling width. A tiling parameter only — results are identical at `C = 2..32`, and
82    /// agree with HF's `C = 64` — but bounded by [`SMEM_CEILING`].
83    pub chunk: usize,
84}
85
86impl Glm5NextKdaConfig {
87    pub fn qkv_dim(&self) -> usize {
88        self.heads * self.head_dim
89    }
90    /// q | k | v concatenated: what the fused depthwise conv sees.
91    pub fn conv_dim(&self) -> usize {
92        3 * self.qkv_dim()
93    }
94    /// Only q | k are L2-normalised. V never is.
95    pub fn qk_channels(&self) -> usize {
96        2 * self.qkv_dim()
97    }
98    /// FP32 `[heads, head_dim, head_dim]`, K-major. Mandatory dtype, not a policy choice.
99    pub fn recurrent_state_elems(&self) -> usize {
100        self.heads * self.head_dim * self.head_dim
101    }
102    /// FP32 `[conv_dim, conv_kernel]` — Atlas's width, one slot wider than HF's.
103    pub fn conv_state_elems(&self) -> usize {
104        self.conv_dim() * self.conv_kernel
105    }
106    pub fn smem_prepare(&self) -> usize {
107        (self.chunk * self.head_dim + self.chunk * self.chunk + self.chunk) * 4
108    }
109    pub fn smem_scan(&self) -> usize {
110        (2 * self.chunk * self.head_dim + self.chunk * self.chunk) * 4
111    }
112
113    pub fn validate(&self) -> Result<()> {
114        if !self.qk_channels().is_multiple_of(256) {
115            bail!("causal_conv1d_update_l2norm requires qk_channels % 256 == 0");
116        }
117        if self.head_dim != 128 {
118            bail!("the fused conv+L2 kernel hardcodes 2 heads per 256-thread block");
119        }
120        if self.conv_kernel > 4 {
121            bail!("the conv kernels keep the sliding window in 4 registers");
122        }
123        let (p, s) = (self.smem_prepare(), self.smem_scan());
124        if p > SMEM_CEILING || s > SMEM_CEILING {
125            bail!(
126                "chunk={} needs {p}/{s} B shared, ceiling {SMEM_CEILING}",
127                self.chunk
128            );
129        }
130        Ok(())
131    }
132}
133
134/// One KDA block's device weights. Torch `Linear` layout `[out, in]`, BF16, except the two F32
135/// gate parameters. There is **no `Z` tensor** — the output gate is low-rank `g_a`/`g_b`.
136pub struct Glm5NextKdaWeights {
137    pub q_proj: DenseWeight,
138    pub k_proj: DenseWeight,
139    pub v_proj: DenseWeight,
140    /// `[conv_dim, conv_kernel]` BF16 = `concat([q, k, v]).squeeze(1)`.
141    pub conv: DenseWeight,
142    pub f_a: DenseWeight,
143    pub f_b: DenseWeight,
144    /// `[heads * head_dim]` F32 — per **channel**.
145    pub dt_bias: DevicePtr,
146    /// `[heads]` F32 — per **head**. The asymmetry with `dt_bias` is the highest-risk line.
147    pub a_log: DevicePtr,
148    pub b_proj: DenseWeight,
149    pub g_a: DenseWeight,
150    pub g_b: DenseWeight,
151    /// `[head_dim]` BF16.
152    pub o_norm: DenseWeight,
153    pub o_proj: DenseWeight,
154}
155
156/// Every kernel the block launches. Resolved with `kernel()` (not `try_kernel`) so a missing
157/// entry point is a hard error rather than a silent fallback. Shareable across all 34 layers.
158/// V-columns one block of the 1R+1W recurrent kernel owns. One warp: 32 threads, and at
159/// `head_dim = 128` a 17.9 KiB scratch that leaves two blocks resident per SM.
160const KDA_V_PER_BLOCK: usize = 32;
161/// Shared memory the launcher will request without opting in past the default limit.
162const KDA_SMEM_BUDGET: usize = 48 * 1024;
163
164/// `AVAROK_GLM_KDA_NO_SMEM=1` restores the 2R+2W recurrent kernel. Read once — this is on
165/// the per-layer decode path.
166fn kda_no_smem() -> bool {
167    static F: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
168    *F.get_or_init(|| std::env::var("AVAROK_GLM_KDA_NO_SMEM").as_deref() == Ok("1"))
169}
170
171#[derive(Clone, Copy)]
172pub struct Glm5NextKdaKernels {
173    pub gemm: KernelHandle,
174    /// ðŸ”ī The DECODE weight kernel. `dense_gemm_bf16` tiles 16x16 over (N, M); at M=1 the
175    /// grid collapses and it measured 58 GB/s against a 254 GB/s part — 32 % of the whole
176    /// GLM decode step (2026-08-28 profile). Every KDA projection is M=1 at decode.
177    pub gemv: KernelHandle,
178    /// ðŸ”ī The BATCHED weight kernel, `2 ..= 8` rows in ONE weight sweep. This is what makes a
179    /// K-token speculative verify cost one pass over q/k/v/f_a/f_b/g_a/g_b/o instead of K.
180    /// `0` on a backend without it — [`ops::dense_mm_bf16`] then falls back to the tile GEMM.
181    pub gemv_batchm: KernelHandle,
182    pub conv_decode: KernelHandle,
183    pub conv_prefill: KernelHandle,
184    pub l2: KernelHandle,
185    pub gate: KernelHandle,
186    pub chunk_prepare: KernelHandle,
187    pub chunk_scan: KernelHandle,
188    pub recurrent: KernelHandle,
189    /// 1R+1W sibling of `recurrent`, **bit-identical**: the decayed state column lives in
190    /// shared memory between the two passes instead of being re-read from global.
191    /// `try_kernel` — a target without it falls back to the 2R+2W kernel.
192    pub recurrent_smem: KernelHandle,
193    pub o_norm: KernelHandle,
194    pub split_widen: KernelHandle,
195    pub sigmoid: KernelHandle,
196    pub fill: KernelHandle,
197    pub pack: KernelHandle,
198}
199
200impl Glm5NextKdaKernels {
201    pub const ENTRY_POINTS: usize = 14;
202
203    pub fn resolve(gpu: &dyn GpuBackend) -> Result<Self> {
204        Ok(Self {
205            // Scalar strict-order BF16 GEMM: `C = A @ B^T`, no reassociation.
206            gemm: gpu.kernel("gemm", "dense_gemm_bf16")?,
207            gemv: gpu.kernel("gemv", "dense_gemv_bf16")?,
208            gemv_batchm: crate::layers::try_kernel(
209                gpu,
210                "dense_gemv_bf16_batchm",
211                "dense_gemv_bf16_batchm",
212            ),
213            conv_decode: gpu.kernel("causal_conv1d", "causal_conv1d_update_l2norm")?,
214            conv_prefill: gpu.kernel("causal_conv1d", "causal_conv1d_update_prefill")?,
215            l2: gpu.kernel("norm", "l2_norm_bf16")?,
216            gate: gpu.kernel("kda_gate", "kda_gate_bf16")?,
217            chunk_prepare: gpu.kernel("kda_chunk", "kda_chunk_prepare")?,
218            chunk_scan: gpu.kernel("kda_chunk", "kda_chunk_scan")?,
219            recurrent: gpu.kernel("kda_recurrent", "kda_recurrent_decode_bf16")?,
220            recurrent_smem: crate::layers::try_kernel(
221                gpu,
222                "kda_recurrent",
223                "kda_recurrent_decode_bf16_smem",
224            ),
225            o_norm: gpu.kernel("kda_layer_ops", "kda_o_norm_gated_bf16")?,
226            split_widen: gpu.kernel("kda_layer_ops", "kda_split_widen")?,
227            sigmoid: gpu.kernel("kda_layer_ops", "kda_sigmoid_bf16_f32")?,
228            fill: gpu.kernel("kda_layer_ops", "kda_fill_f32")?,
229            pack: gpu.kernel("kda_layer_ops", "kda_pack_qkv_bf16")?,
230        })
231    }
232}
233
234/// The per-sequence state a KDA layer carries. Both buffers are read-modify-write.
235///
236/// ðŸŠĪ The recurrent element is **FP32 by reference semantics** — HF stores it via
237/// `last_recurrent_state.to(torch.float32)` and vLLM's `kda_state_dtype` returns
238/// `(conv_dtype, torch.float32)` regardless of `mamba_cache_dtype`. `--ssm-h-dtype f16` is not
239/// available to KDA without deviating from the reference.
240///
241/// ðŸŠĪ The conv buffer is Atlas's **`conv_kernel`-wide** convention, one slot wider than HF's
242/// `conv_kernel - 1`: Atlas shifts left before convolving, so slot 0 is shifted out and never
243/// participates. `HF[0..k-1] == Atlas[1..k]` pre-shift. Any code moving state between the two
244/// conventions must apply that offset.
245#[derive(Clone, Copy, Debug)]
246pub struct KdaSeqState {
247    /// `[conv_dim, conv_kernel]` FP32.
248    pub conv: DevicePtr,
249    /// `[heads, head_dim, head_dim]` FP32, K-major.
250    pub recurrent: DevicePtr,
251}
252
253/// Scratch owned by the forward path. Sized once for `max_tokens` and reused across layers —
254/// the whole KDA family shares one workspace because every block has identical geometry.
255///
256/// The intermediate buffers are `pub` on purpose: the numeric oracle compares stage by stage, and
257/// a residual quoted only at the layer output cannot separate a kernel bug from a rounding floor.
258pub struct Glm5NextKdaWorkspace {
259    /// `[3, T, qkv]` BF16 — the three projections as `dense_gemm_bf16` writes them.
260    pub qkv_parts: DevicePtr,
261    /// `[T, conv_dim]` BF16 — q|k|v per token, pre-conv.
262    pub qkv_proj: DevicePtr,
263    /// `[T, conv_dim]` BF16 — post conv + SiLU. On the decode path L2 is already fused in.
264    pub conv_out: DevicePtr,
265    /// `[T_pad, qkv]` FP32 — post-L2 q, post-L2 k, raw v. Prefill only.
266    pub q_f32: DevicePtr,
267    pub k_f32: DevicePtr,
268    pub v_f32: DevicePtr,
269    /// `[T_pad, heads, head_dim]` FP32 — bounded log-decay from `kda_gate`.
270    pub gate: DevicePtr,
271    /// `[T_pad, heads]` FP32 — already sigmoided.
272    pub beta: DevicePtr,
273    /// `[T_pad, heads, head_dim]` FP32 — KDA core output, pre-norm.
274    pub core: DevicePtr,
275    /// `[T, qkv]` BF16 — `f_b(f_a(h))`, the forget-gate projection `kda_gate` consumes. Kept
276    /// separate from `out_gate`: same shape, same kind of low-rank pair, aliasing is silent.
277    pub g_raw: DevicePtr,
278    /// `[T, qkv]` BF16 — `g_b(g_a(h))`, the low-rank output gate.
279    pub out_gate: DevicePtr,
280    /// `[T, qkv]` BF16 — after the sigmoid-gated RMSNorm.
281    pub o_norm_out: DevicePtr,
282    /// `[T, hidden]` BF16 — the block output.
283    pub final_out: DevicePtr,
284    lowrank: DevicePtr,
285    beta_bf16: DevicePtr,
286    chunk_gc: DevicePtr,
287    chunk_u: DevicePtr,
288    chunk_w: DevicePtr,
289    max_tokens: usize,
290    t_pad: usize,
291}
292
293impl Glm5NextKdaWorkspace {
294    pub fn new(gpu: &dyn GpuBackend, cfg: &Glm5NextKdaConfig, max_tokens: usize) -> Result<Self> {
295        cfg.validate()?;
296        if max_tokens == 0 {
297            bail!("workspace needs max_tokens >= 1");
298        }
299        let (qkv, cd, hd, h) = (cfg.qkv_dim(), cfg.conv_dim(), cfg.head_dim, cfg.heads);
300        let t = max_tokens;
301        let t_pad = t.div_ceil(cfg.chunk) * cfg.chunk;
302        let n = t_pad * qkv;
303        Ok(Self {
304            qkv_parts: gpu.alloc(3 * t * qkv * 2)?,
305            qkv_proj: gpu.alloc(t * cd * 2)?,
306            conv_out: gpu.alloc(t * cd * 2)?,
307            q_f32: gpu.alloc(n * 4)?,
308            k_f32: gpu.alloc(n * 4)?,
309            v_f32: gpu.alloc(n * 4)?,
310            gate: gpu.alloc(n * 4)?,
311            beta: gpu.alloc(t_pad * h * 4)?,
312            core: gpu.alloc(n * 4)?,
313            g_raw: gpu.alloc(t * qkv * 2)?,
314            out_gate: gpu.alloc(t * qkv * 2)?,
315            o_norm_out: gpu.alloc(t * qkv * 2)?,
316            final_out: gpu.alloc(t * cfg.hidden * 2)?,
317            lowrank: gpu.alloc(t * hd * 2)?,
318            beta_bf16: gpu.alloc(t * h * 2)?,
319            chunk_gc: gpu.alloc(n * 4)?,
320            chunk_u: gpu.alloc(n * 4)?,
321            chunk_w: gpu.alloc(n * 4)?,
322            max_tokens: t,
323            t_pad,
324        })
325    }
326
327    pub fn max_tokens(&self) -> usize {
328        self.max_tokens
329    }
330    pub fn t_pad(&self) -> usize {
331        self.t_pad
332    }
333}
334
335/// One bound KDA attention block.
336pub struct Glm5NextKdaLayer {
337    /// Index in the checkpoint's 45-layer text stack, for diagnostics.
338    pub layer_idx: usize,
339    pub cfg: Glm5NextKdaConfig,
340    pub weights: Glm5NextKdaWeights,
341    pub kernels: Glm5NextKdaKernels,
342}
343
344impl Glm5NextKdaLayer {
345    pub fn new(
346        layer_idx: usize,
347        cfg: Glm5NextKdaConfig,
348        weights: Glm5NextKdaWeights,
349        kernels: Glm5NextKdaKernels,
350    ) -> Result<Self> {
351        cfg.validate()?;
352        Ok(Self {
353            layer_idx,
354            cfg,
355            weights,
356            kernels,
357        })
358    }
359
360    #[allow(clippy::too_many_arguments)]
361    fn gemm(
362        &self,
363        gpu: &dyn GpuBackend,
364        input: DevicePtr,
365        weight: &DenseWeight,
366        out: DevicePtr,
367        m: usize,
368        n: usize,
369        k: usize,
370        stream: u64,
371    ) -> Result<()> {
372        // ðŸ”ī PREFILL WIDTHS GO TO cuBLASLt. `dense_gemm_bf16` is, by its own doc, a
373        // "scalar strict-order BF16 GEMM, no reassociation" — correctness-first, not a
374        // tensor-core kernel. With the KDA bucket split three ways it is the single
375        // largest leaf in prefill: `kda_front` 642.4 ms/tok = 24.4% (and `kda_back`
376        // another 7.7%), against `kda_recur` — the per-token recurrence everyone assumes
377        // is the problem — at just 4.6%. At 256 rows that is 19.8 ms per layer-call to
378        // move ~268 MB of weights, ~20x off a 273 GB/s part.
379        //
380        // ðŸŠĪ The threshold is not a tuning knob, it is the numerics boundary.
381        // `dense_mm_bf16` routes M=1 to the GEMV and M=2..=16 to the batched single-sweep
382        // kernel, both of which accumulate per row in FP32 in a fixed K order and are
383        // BIT-IDENTICAL. Only above `DENSE_GEMV_BATCHM_MAX_M` does it fall to the tile
384        // GEMM, which already reassociates. So this swaps one non-bit-exact path for
385        // another and leaves every bit-exact tier — decode and the speculative verify —
386        // untouched. A verify must still match what decode produced; it is M<=16 and
387        // never reaches here.
388        if m > ops::DENSE_GEMV_BATCHM_MAX_M as usize
389            && crate::layers::glm5next_layer::cublas_wide_proj()
390        {
391            return ops::cublas_bf16_proj_dense(
392                input,
393                weight.weight,
394                out,
395                m as u32,
396                n as u32,
397                k as u32,
398                stream,
399            );
400        }
401        // M=1 decode -> GEMV, M=2..8 verify/short-chunk -> ONE weight sweep, wider -> tile GEMM.
402        ops::dense_mm_bf16(
403            gpu,
404            &ops::DenseMmKernels {
405                gemm: self.kernels.gemm,
406                gemv: self.kernels.gemv,
407                batchm: self.kernels.gemv_batchm,
408            },
409            input,
410            weight.weight,
411            out,
412            m,
413            n,
414            k,
415            stream,
416        )
417    }
418
419    /// Projections, forget gate, beta and output gate — identical on both paths, and all driven
420    /// by the RAW hidden state, never by the post-conv activations.
421    fn front_end(
422        &self,
423        gpu: &dyn GpuBackend,
424        hidden: DevicePtr,
425        t: usize,
426        ws: &Glm5NextKdaWorkspace,
427        stream: u64,
428    ) -> Result<()> {
429        let c = &self.cfg;
430        let (hid, qkv, hd) = (c.hidden, c.qkv_dim(), c.head_dim);
431
432        // Three separate [T, qkv] GEMMs, then one pack. See the `dense_gemm_bf16` note above:
433        // aiming them at offsets in one [T, 3*qkv] buffer is silently correct at T = 1 only.
434        for (i, w) in [
435            &self.weights.q_proj,
436            &self.weights.k_proj,
437            &self.weights.v_proj,
438        ]
439        .into_iter()
440        .enumerate()
441        {
442            self.gemm(
443                gpu,
444                hidden,
445                w,
446                ws.qkv_parts.offset(i * t * qkv * 2),
447                t,
448                qkv,
449                hid,
450                stream,
451            )?;
452        }
453        KernelLaunch::new(gpu, self.kernels.pack)
454            .grid([div_ceil(qkv as u32, 256), t as u32, 1])
455            .block([256, 1, 1])
456            .arg_ptr(ws.qkv_parts)
457            .arg_ptr(ws.qkv_parts.offset(t * qkv * 2))
458            .arg_ptr(ws.qkv_parts.offset(2 * t * qkv * 2))
459            .arg_ptr(ws.qkv_proj)
460            .arg_u32(t as u32)
461            .arg_u32(qkv as u32)
462            .launch(stream)?;
463
464        // Low-rank forget gate: hidden -> head_dim -> heads*head_dim, then the BOUNDED law
465        // `lower_bound * sigmoid(exp(A_log[h]) * (g[c] + dt_bias[c]))`. `A_log` is per HEAD,
466        // `dt_bias` per CHANNEL; the asymmetry is why they are separate kernel arguments.
467        self.gemm(
468            gpu,
469            hidden,
470            &self.weights.f_a,
471            ws.lowrank,
472            t,
473            hd,
474            hid,
475            stream,
476        )?;
477        self.gemm(
478            gpu,
479            ws.lowrank,
480            &self.weights.f_b,
481            ws.g_raw,
482            t,
483            qkv,
484            hd,
485            stream,
486        )?;
487        KernelLaunch::new(gpu, self.kernels.gate)
488            .grid([(t * c.heads) as u32, 1, 1])
489            .block([BLOCK, 1, 1])
490            .arg_ptr(ws.g_raw)
491            .arg_ptr(self.weights.dt_bias)
492            .arg_ptr(self.weights.a_log)
493            .arg_ptr(ws.gate)
494            .arg_u32(t as u32)
495            .arg_u32(c.heads as u32)
496            .arg_u32(hd as u32)
497            .arg_f32(c.gate_lower_bound)
498            .launch(stream)?;
499
500        // beta = sigmoid(b_proj(hidden)); the KDA kernels take it ALREADY sigmoided.
501        self.gemm(
502            gpu,
503            hidden,
504            &self.weights.b_proj,
505            ws.beta_bf16,
506            t,
507            c.heads,
508            hid,
509            stream,
510        )?;
511        let n = t * c.heads;
512        KernelLaunch::new(gpu, self.kernels.sigmoid)
513            .grid([div_ceil(n as u32, 256), 1, 1])
514            .block([256, 1, 1])
515            .arg_ptr(ws.beta_bf16)
516            .arg_ptr(ws.beta)
517            .arg_u32(n as u32)
518            .launch(stream)?;
519
520        // Low-rank OUTPUT gate — a KDA checkpoint has no `Z` tensor.
521        self.gemm(
522            gpu,
523            hidden,
524            &self.weights.g_a,
525            ws.lowrank,
526            t,
527            hd,
528            hid,
529            stream,
530        )?;
531        self.gemm(
532            gpu,
533            ws.lowrank,
534            &self.weights.g_b,
535            ws.out_gate,
536            t,
537            qkv,
538            hd,
539            stream,
540        )
541    }
542
543    /// Sigmoid-gated RMSNorm then `o_proj`.
544    fn back_end(
545        &self,
546        gpu: &dyn GpuBackend,
547        t: usize,
548        ws: &Glm5NextKdaWorkspace,
549        stream: u64,
550    ) -> Result<()> {
551        let c = &self.cfg;
552        KernelLaunch::new(gpu, self.kernels.o_norm)
553            .grid([(t * c.heads) as u32, 1, 1])
554            .block([c.head_dim as u32, 1, 1])
555            .arg_ptr(ws.core)
556            .arg_ptr(ws.out_gate)
557            .arg_ptr(self.weights.o_norm.weight)
558            .arg_ptr(ws.o_norm_out)
559            .arg_u32(c.head_dim as u32)
560            .arg_f32(c.rms_norm_eps)
561            .launch(stream)?;
562        self.gemm(
563            gpu,
564            ws.o_norm_out,
565            &self.weights.o_proj,
566            ws.final_out,
567            t,
568            c.hidden,
569            c.qkv_dim(),
570            stream,
571        )
572    }
573
574    /// The stateful half of one KDA token: conv window update (with SiLU + L2 fused) then the
575    /// recurrent scan, both reading row `row` of the workspace and updating `state` IN PLACE.
576    ///
577    /// ðŸ”ī This is the part that CANNOT be batched. The recurrent state after token `t + 1` is a
578    /// function of the state after `t`, so K verify rows walk it K times in order — which is why
579    /// [`Self::decode_k`] batches only the projections around it. The chunked [`Self::prefill`]
580    /// scan computes the same mathematics by a different association and is NOT bit-identical to
581    /// this; using it for a verify would move the output of an ACCEPTED token.
582    ///
583    /// ðŸŠĪ The conv fuses SiLU **and** L2, so `q`/`k` reach `kda_recurrent` already normalised —
584    /// exactly the pre-normalised contract that kernel takes. Re-normalising would silently
585    /// restore the bf16 rounding the fused write destroyed and look like a kernel bug.
586    fn stateful_row(
587        &self,
588        gpu: &dyn GpuBackend,
589        row: usize,
590        state: &KdaSeqState,
591        ws: &Glm5NextKdaWorkspace,
592        stream: u64,
593    ) -> Result<()> {
594        let c = &self.cfg;
595        let qkv = c.qkv_dim();
596        let cd = c.conv_dim();
597
598        ops::conv1d_update_l2norm(
599            gpu,
600            self.kernels.conv_decode,
601            state.conv,
602            ws.qkv_proj.offset(row * cd * 2),
603            &self.weights.conv,
604            ws.conv_out.offset(row * cd * 2),
605            c.conv_dim() as u32,
606            c.conv_kernel as u32,
607            1,
608            c.qk_channels() as u32,
609            c.head_dim as u32,
610            c.l2_eps,
611            stream,
612        )?;
613
614        let d = c.head_dim;
615        // 1R+1W when the target carries the shared-memory sibling. The V axis has no
616        // cross-thread dependency, so a block owns a SLICE of it: `vpb` columns need
617        // `vpb * (d + 1)` floats of scratch (the `+1` is the bank-conflict pad the kernel
618        // documents) and grid.y covers the rest. One warp per block keeps the request
619        // inside the 48 KiB default at the production `head_dim = 128`.
620        let vpb = KDA_V_PER_BLOCK.min(d);
621        let smem_smem = (3 * d + vpb * (d + 1)) * 4;
622        if self.kernels.recurrent_smem.0 != 0
623            && d.is_multiple_of(vpb)
624            && smem_smem <= KDA_SMEM_BUDGET
625            && !kda_no_smem()
626        {
627            KernelLaunch::new(gpu, self.kernels.recurrent_smem)
628                .grid([c.heads as u32, (d / vpb) as u32, 1])
629                .block([vpb as u32, 1, 1])
630                .shared_mem(smem_smem as u32)
631                .arg_ptr(ws.conv_out.offset(row * cd * 2))
632                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 2))
633                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 4))
634                .arg_ptr(ws.gate.offset(row * qkv * 4))
635                .arg_ptr(ws.beta.offset(row * c.heads * 4))
636                .arg_ptr(state.recurrent)
637                .arg_ptr(ws.core.offset(row * qkv * 4))
638                .arg_u32(c.heads as u32)
639                .arg_u32(d as u32)
640                .arg_f32(1.0 / (d as f32).sqrt())
641                .arg_u32(vpb as u32)
642                .launch(stream)?;
643        } else {
644            KernelLaunch::new(gpu, self.kernels.recurrent)
645                .grid([c.heads as u32, 1, 1])
646                .block([BLOCK.min(d as u32), 1, 1])
647                .shared_mem((3 * d * 4) as u32)
648                .arg_ptr(ws.conv_out.offset(row * cd * 2))
649                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 2))
650                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 4))
651                .arg_ptr(ws.gate.offset(row * qkv * 4))
652                .arg_ptr(ws.beta.offset(row * c.heads * 4))
653                .arg_ptr(state.recurrent)
654                .arg_ptr(ws.core.offset(row * qkv * 4))
655                .arg_u32(c.heads as u32)
656                .arg_u32(d as u32)
657                .arg_f32(1.0 / (d as f32).sqrt())
658                .launch(stream)?;
659        }
660
661        Ok(())
662    }
663
664    /// Single-token decode, carrying both states.
665    ///
666    /// The conv fuses SiLU **and** L2, so `q`/`k` reach `kda_recurrent` already normalised —
667    /// exactly the pre-normalised contract that kernel takes. Re-normalising here would silently
668    /// restore the bf16 rounding the fused write destroyed and look like a kernel bug.
669    ///
670    /// Result lands in `ws.final_out`; `state` is updated in place.
671    pub fn decode(
672        &self,
673        gpu: &dyn GpuBackend,
674        hidden: DevicePtr,
675        state: &KdaSeqState,
676        ws: &Glm5NextKdaWorkspace,
677        stream: u64,
678    ) -> Result<()> {
679        self.front_end(gpu, hidden, 1, ws, stream)?;
680        self.stateful_row(gpu, 0, state, ws, stream)?;
681        self.back_end(gpu, 1, ws, stream)
682    }
683
684    /// K tokens of ONE sequence: the projections batched, the recurrence NOT.
685    ///
686    /// This is the speculative-verify body. The weight-heavy halves — `Self::front_end`'s
687    /// q/k/v, both low-rank gate pairs and `b_proj`, and `Self::back_end`'s `o_proj` — run
688    /// once over all K rows, so a K-token verify reads KDA's 4.7 GB/rank/token ONCE instead of
689    /// K times. That is the entire reason speculation can pay on this model.
690    ///
691    /// ðŸ”ī **Bit-identical to K serial [`Self::decode`] calls**, which is not a nicety: an
692    /// accepted draft token must be the token the unspeculated engine would have emitted, or
693    /// speculation is silently lossy. It holds because `dense_gemv_bf16_batchm` reproduces each
694    /// row's exact K-iteration order and reduction tree (`ops::dense_mm_bf16`), the pack / gate
695    /// / sigmoid / `o_norm` kernels are grid-parallel over the token axis, and
696    /// `Self::stateful_row` walks the state one token at a time exactly as `decode` does.
697    ///
698    /// `snapshots[t]` — `(h_dst, conv_dst)` — receives the state AFTER row `t`, which is what a
699    /// partial accept rewinds to. Pass `k - 1` of them (a full accept never rewinds) or none.
700    pub fn decode_k(
701        &self,
702        gpu: &dyn GpuBackend,
703        hidden: DevicePtr,
704        k: usize,
705        state: &KdaSeqState,
706        ws: &Glm5NextKdaWorkspace,
707        snapshots: &[(DevicePtr, DevicePtr)],
708        stream: u64,
709    ) -> Result<()> {
710        if k == 0 || k > ws.max_tokens {
711            bail!(
712                "KDA decode_k of {k} tokens does not fit a workspace built for {}",
713                ws.max_tokens
714            );
715        }
716        use crate::layers::glm5next_layer::profile;
717        let c = &self.cfg;
718        let (h_bytes, conv_bytes) = (c.recurrent_state_elems() * 4, c.conv_state_elems() * 4);
719        // Three spans, not one: see the KDA_FRONT doc comment. Each is closed where it
720        // ends — a bucket ended inside a loop against a start taken outside it is exactly
721        // the double-count that made dsa_proj read 94.6%.
722        let t_front = profile::start();
723        self.front_end(gpu, hidden, k, ws, stream)?;
724        profile::end(profile::KDA_FRONT, t_front, gpu, stream);
725        let t_recur = profile::start();
726        for row in 0..k {
727            self.stateful_row(gpu, row, state, ws, stream)?;
728            if let Some((h_dst, conv_dst)) = snapshots.get(row) {
729                gpu.copy_d2d_async(state.recurrent, *h_dst, h_bytes, stream)?;
730                gpu.copy_d2d_async(state.conv, *conv_dst, conv_bytes, stream)?;
731            }
732        }
733        profile::end(profile::KDA_RECUR, t_recur, gpu, stream);
734        let t_back = profile::start();
735        let r = self.back_end(gpu, k, ws, stream);
736        profile::end(profile::KDA_BACK, t_back, gpu, stream);
737        r
738    }
739
740    /// Chunked prefill over `t` tokens from the carried state.
741    pub fn prefill(
742        &self,
743        gpu: &dyn GpuBackend,
744        hidden: DevicePtr,
745        t: usize,
746        state: &KdaSeqState,
747        ws: &Glm5NextKdaWorkspace,
748        stream: u64,
749    ) -> Result<()> {
750        self.prefill_with_pad_fill(gpu, hidden, t, state, ws, 0.0, stream)
751    }
752
753    /// [`Self::prefill`] with the padded q/k/v tails primed to an arbitrary value.
754    ///
755    /// Production passes zero. The numeric gate passes poison, because `kda_chunk_*` guard past
756    /// `T` **in-kernel** and that guard needs a test with teeth: the Slice-5 bug wrote entirely
757    /// correct outputs while leaving the carried recurrent state off by 1.623e13, so a pad tail
758    /// the caller zeroes proves nothing.
759    #[allow(clippy::too_many_arguments)]
760    pub fn prefill_with_pad_fill(
761        &self,
762        gpu: &dyn GpuBackend,
763        hidden: DevicePtr,
764        t: usize,
765        state: &KdaSeqState,
766        ws: &Glm5NextKdaWorkspace,
767        pad_fill: f32,
768        stream: u64,
769    ) -> Result<()> {
770        let c = &self.cfg;
771        let (qkv, cd, hd) = (c.qkv_dim(), c.conv_dim(), c.head_dim);
772        if t == 0 || t > ws.max_tokens {
773            bail!(
774                "prefill of {t} tokens does not fit a workspace built for {}",
775                ws.max_tokens
776            );
777        }
778        let nchunks = t.div_ceil(c.chunk);
779        let tp = nchunks * c.chunk;
780        // Same three buckets as `decode_k`, so the recurrent and chunked arms can be
781        // compared part by part rather than only on a total.
782        let t_front = crate::layers::glm5next_layer::profile::start();
783        self.front_end(gpu, hidden, t, ws, stream)?;
784        crate::layers::glm5next_layer::profile::end(
785            crate::layers::glm5next_layer::profile::KDA_FRONT,
786            t_front,
787            gpu,
788            stream,
789        );
790        let t_recur = crate::layers::glm5next_layer::profile::start();
791
792        // Prefill conv is conv + SiLU ONLY — L2 is a separate launch over q|k.
793        KernelLaunch::new(gpu, self.kernels.conv_prefill)
794            .grid([div_ceil(cd as u32, 256), 1, 1])
795            .block([256, 1, 1])
796            .arg_ptr(state.conv)
797            .arg_ptr(ws.qkv_proj)
798            .arg_ptr(self.weights.conv.weight)
799            .arg_ptr(DevicePtr::NULL)
800            .arg_ptr(ws.conv_out)
801            .arg_u32(cd as u32)
802            .arg_u32(c.conv_kernel as u32)
803            .arg_u32(t as u32)
804            .arg_u32(cd as u32)
805            .arg_u32(cd as u32)
806            .launch(stream)?;
807        KernelLaunch::new(gpu, self.kernels.l2)
808            .grid([(c.qk_channels() / hd) as u32, t as u32, 1])
809            .block([hd as u32, 1, 1])
810            .arg_ptr(ws.conv_out)
811            .arg_u32(hd as u32)
812            .arg_f32(c.l2_eps)
813            .arg_u32(cd as u32)
814            .launch(stream)?;
815
816        // Gate and beta are read at padded positions by `kda_chunk_prepare`; zero them so a guard
817        // failure surfaces in q/k/v, which the regression deliberately poisons.
818        for (buf, real, padded) in [
819            (ws.gate, t * qkv, tp * qkv),
820            (ws.beta, t * c.heads, tp * c.heads),
821        ] {
822            if padded == real {
823                continue;
824            }
825            KernelLaunch::new(gpu, self.kernels.fill)
826                .grid([div_ceil((padded - real) as u32, 256), 1, 1])
827                .block([256, 1, 1])
828                .arg_ptr(buf.offset(real * 4))
829                .arg_u32((padded - real) as u32)
830                .arg_f32(0.0)
831                .launch(stream)?;
832        }
833        for p in [ws.q_f32, ws.k_f32, ws.v_f32] {
834            KernelLaunch::new(gpu, self.kernels.fill)
835                .grid([div_ceil((tp * qkv) as u32, 256), 1, 1])
836                .block([256, 1, 1])
837                .arg_ptr(p)
838                .arg_u32((tp * qkv) as u32)
839                .arg_f32(pad_fill)
840                .launch(stream)?;
841        }
842        KernelLaunch::new(gpu, self.kernels.split_widen)
843            .grid([div_ceil(qkv as u32, 256), t as u32, 1])
844            .block([256, 1, 1])
845            .arg_ptr(ws.conv_out)
846            .arg_ptr(ws.q_f32)
847            .arg_ptr(ws.k_f32)
848            .arg_ptr(ws.v_f32)
849            .arg_u32(t as u32)
850            .arg_u32(qkv as u32)
851            .launch(stream)?;
852
853        KernelLaunch::new(gpu, self.kernels.chunk_prepare)
854            .grid([nchunks as u32, c.heads as u32, 1])
855            .block([BLOCK, 1, 1])
856            .shared_mem(c.smem_prepare() as u32)
857            .arg_ptr(ws.k_f32)
858            .arg_ptr(ws.v_f32)
859            .arg_ptr(ws.gate)
860            .arg_ptr(ws.beta)
861            .arg_ptr(ws.chunk_gc)
862            .arg_ptr(ws.chunk_u)
863            .arg_ptr(ws.chunk_w)
864            .arg_u32(c.heads as u32)
865            .arg_u32(hd as u32)
866            .arg_u32(c.chunk as u32)
867            .arg_u32(t as u32)
868            .launch(stream)?;
869        KernelLaunch::new(gpu, self.kernels.chunk_scan)
870            .grid([c.heads as u32, 1, 1])
871            .block([BLOCK, 1, 1])
872            .shared_mem(c.smem_scan() as u32)
873            .arg_ptr(ws.q_f32)
874            .arg_ptr(ws.k_f32)
875            .arg_ptr(ws.chunk_gc)
876            .arg_ptr(ws.chunk_u)
877            .arg_ptr(ws.chunk_w)
878            .arg_ptr(state.recurrent)
879            .arg_ptr(ws.core)
880            .arg_u32(c.heads as u32)
881            .arg_u32(hd as u32)
882            .arg_u32(c.chunk as u32)
883            .arg_u32(nchunks as u32)
884            .arg_u32(t as u32)
885            .arg_f32(1.0 / (hd as f32).sqrt())
886            .launch(stream)?;
887        crate::layers::glm5next_layer::profile::end(
888            crate::layers::glm5next_layer::profile::KDA_RECUR,
889            t_recur,
890            gpu,
891            stream,
892        );
893
894        let t_back = crate::layers::glm5next_layer::profile::start();
895        let r = self.back_end(gpu, t, ws, stream);
896        crate::layers::glm5next_layer::profile::end(
897            crate::layers::glm5next_layer::profile::KDA_BACK,
898            t_back,
899            gpu,
900            stream,
901        );
902        r
903    }
904}