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 `AtlasCudaBackend`.
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/// `ATLAS_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("ATLAS_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        // M=1 decode -> GEMV, M=2..8 verify/short-chunk -> ONE weight sweep, wider -> tile GEMM.
373        ops::dense_mm_bf16(
374            gpu,
375            &ops::DenseMmKernels {
376                gemm: self.kernels.gemm,
377                gemv: self.kernels.gemv,
378                batchm: self.kernels.gemv_batchm,
379            },
380            input,
381            weight.weight,
382            out,
383            m,
384            n,
385            k,
386            stream,
387        )
388    }
389
390    /// Projections, forget gate, beta and output gate — identical on both paths, and all driven
391    /// by the RAW hidden state, never by the post-conv activations.
392    fn front_end(
393        &self,
394        gpu: &dyn GpuBackend,
395        hidden: DevicePtr,
396        t: usize,
397        ws: &Glm5NextKdaWorkspace,
398        stream: u64,
399    ) -> Result<()> {
400        let c = &self.cfg;
401        let (hid, qkv, hd) = (c.hidden, c.qkv_dim(), c.head_dim);
402
403        // Three separate [T, qkv] GEMMs, then one pack. See the `dense_gemm_bf16` note above:
404        // aiming them at offsets in one [T, 3*qkv] buffer is silently correct at T = 1 only.
405        for (i, w) in [
406            &self.weights.q_proj,
407            &self.weights.k_proj,
408            &self.weights.v_proj,
409        ]
410        .into_iter()
411        .enumerate()
412        {
413            self.gemm(
414                gpu,
415                hidden,
416                w,
417                ws.qkv_parts.offset(i * t * qkv * 2),
418                t,
419                qkv,
420                hid,
421                stream,
422            )?;
423        }
424        KernelLaunch::new(gpu, self.kernels.pack)
425            .grid([div_ceil(qkv as u32, 256), t as u32, 1])
426            .block([256, 1, 1])
427            .arg_ptr(ws.qkv_parts)
428            .arg_ptr(ws.qkv_parts.offset(t * qkv * 2))
429            .arg_ptr(ws.qkv_parts.offset(2 * t * qkv * 2))
430            .arg_ptr(ws.qkv_proj)
431            .arg_u32(t as u32)
432            .arg_u32(qkv as u32)
433            .launch(stream)?;
434
435        // Low-rank forget gate: hidden -> head_dim -> heads*head_dim, then the BOUNDED law
436        // `lower_bound * sigmoid(exp(A_log[h]) * (g[c] + dt_bias[c]))`. `A_log` is per HEAD,
437        // `dt_bias` per CHANNEL; the asymmetry is why they are separate kernel arguments.
438        self.gemm(
439            gpu,
440            hidden,
441            &self.weights.f_a,
442            ws.lowrank,
443            t,
444            hd,
445            hid,
446            stream,
447        )?;
448        self.gemm(
449            gpu,
450            ws.lowrank,
451            &self.weights.f_b,
452            ws.g_raw,
453            t,
454            qkv,
455            hd,
456            stream,
457        )?;
458        KernelLaunch::new(gpu, self.kernels.gate)
459            .grid([(t * c.heads) as u32, 1, 1])
460            .block([BLOCK, 1, 1])
461            .arg_ptr(ws.g_raw)
462            .arg_ptr(self.weights.dt_bias)
463            .arg_ptr(self.weights.a_log)
464            .arg_ptr(ws.gate)
465            .arg_u32(t as u32)
466            .arg_u32(c.heads as u32)
467            .arg_u32(hd as u32)
468            .arg_f32(c.gate_lower_bound)
469            .launch(stream)?;
470
471        // beta = sigmoid(b_proj(hidden)); the KDA kernels take it ALREADY sigmoided.
472        self.gemm(
473            gpu,
474            hidden,
475            &self.weights.b_proj,
476            ws.beta_bf16,
477            t,
478            c.heads,
479            hid,
480            stream,
481        )?;
482        let n = t * c.heads;
483        KernelLaunch::new(gpu, self.kernels.sigmoid)
484            .grid([div_ceil(n as u32, 256), 1, 1])
485            .block([256, 1, 1])
486            .arg_ptr(ws.beta_bf16)
487            .arg_ptr(ws.beta)
488            .arg_u32(n as u32)
489            .launch(stream)?;
490
491        // Low-rank OUTPUT gate — a KDA checkpoint has no `Z` tensor.
492        self.gemm(
493            gpu,
494            hidden,
495            &self.weights.g_a,
496            ws.lowrank,
497            t,
498            hd,
499            hid,
500            stream,
501        )?;
502        self.gemm(
503            gpu,
504            ws.lowrank,
505            &self.weights.g_b,
506            ws.out_gate,
507            t,
508            qkv,
509            hd,
510            stream,
511        )
512    }
513
514    /// Sigmoid-gated RMSNorm then `o_proj`.
515    fn back_end(
516        &self,
517        gpu: &dyn GpuBackend,
518        t: usize,
519        ws: &Glm5NextKdaWorkspace,
520        stream: u64,
521    ) -> Result<()> {
522        let c = &self.cfg;
523        KernelLaunch::new(gpu, self.kernels.o_norm)
524            .grid([(t * c.heads) as u32, 1, 1])
525            .block([c.head_dim as u32, 1, 1])
526            .arg_ptr(ws.core)
527            .arg_ptr(ws.out_gate)
528            .arg_ptr(self.weights.o_norm.weight)
529            .arg_ptr(ws.o_norm_out)
530            .arg_u32(c.head_dim as u32)
531            .arg_f32(c.rms_norm_eps)
532            .launch(stream)?;
533        self.gemm(
534            gpu,
535            ws.o_norm_out,
536            &self.weights.o_proj,
537            ws.final_out,
538            t,
539            c.hidden,
540            c.qkv_dim(),
541            stream,
542        )
543    }
544
545    /// The stateful half of one KDA token: conv window update (with SiLU + L2 fused) then the
546    /// recurrent scan, both reading row `row` of the workspace and updating `state` IN PLACE.
547    ///
548    /// ðŸ”ī This is the part that CANNOT be batched. The recurrent state after token `t + 1` is a
549    /// function of the state after `t`, so K verify rows walk it K times in order — which is why
550    /// [`Self::decode_k`] batches only the projections around it. The chunked [`Self::prefill`]
551    /// scan computes the same mathematics by a different association and is NOT bit-identical to
552    /// this; using it for a verify would move the output of an ACCEPTED token.
553    ///
554    /// ðŸŠĪ The conv fuses SiLU **and** L2, so `q`/`k` reach `kda_recurrent` already normalised —
555    /// exactly the pre-normalised contract that kernel takes. Re-normalising would silently
556    /// restore the bf16 rounding the fused write destroyed and look like a kernel bug.
557    fn stateful_row(
558        &self,
559        gpu: &dyn GpuBackend,
560        row: usize,
561        state: &KdaSeqState,
562        ws: &Glm5NextKdaWorkspace,
563        stream: u64,
564    ) -> Result<()> {
565        let c = &self.cfg;
566        let qkv = c.qkv_dim();
567        let cd = c.conv_dim();
568
569        ops::conv1d_update_l2norm(
570            gpu,
571            self.kernels.conv_decode,
572            state.conv,
573            ws.qkv_proj.offset(row * cd * 2),
574            &self.weights.conv,
575            ws.conv_out.offset(row * cd * 2),
576            c.conv_dim() as u32,
577            c.conv_kernel as u32,
578            1,
579            c.qk_channels() as u32,
580            c.head_dim as u32,
581            c.l2_eps,
582            stream,
583        )?;
584
585        let d = c.head_dim;
586        // 1R+1W when the target carries the shared-memory sibling. The V axis has no
587        // cross-thread dependency, so a block owns a SLICE of it: `vpb` columns need
588        // `vpb * (d + 1)` floats of scratch (the `+1` is the bank-conflict pad the kernel
589        // documents) and grid.y covers the rest. One warp per block keeps the request
590        // inside the 48 KiB default at the production `head_dim = 128`.
591        let vpb = KDA_V_PER_BLOCK.min(d);
592        let smem_smem = (3 * d + vpb * (d + 1)) * 4;
593        if self.kernels.recurrent_smem.0 != 0
594            && d.is_multiple_of(vpb)
595            && smem_smem <= KDA_SMEM_BUDGET
596            && !kda_no_smem()
597        {
598            KernelLaunch::new(gpu, self.kernels.recurrent_smem)
599                .grid([c.heads as u32, (d / vpb) as u32, 1])
600                .block([vpb as u32, 1, 1])
601                .shared_mem(smem_smem as u32)
602                .arg_ptr(ws.conv_out.offset(row * cd * 2))
603                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 2))
604                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 4))
605                .arg_ptr(ws.gate.offset(row * qkv * 4))
606                .arg_ptr(ws.beta.offset(row * c.heads * 4))
607                .arg_ptr(state.recurrent)
608                .arg_ptr(ws.core.offset(row * qkv * 4))
609                .arg_u32(c.heads as u32)
610                .arg_u32(d as u32)
611                .arg_f32(1.0 / (d as f32).sqrt())
612                .arg_u32(vpb as u32)
613                .launch(stream)?;
614        } else {
615            KernelLaunch::new(gpu, self.kernels.recurrent)
616                .grid([c.heads as u32, 1, 1])
617                .block([BLOCK.min(d as u32), 1, 1])
618                .shared_mem((3 * d * 4) as u32)
619                .arg_ptr(ws.conv_out.offset(row * cd * 2))
620                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 2))
621                .arg_ptr(ws.conv_out.offset(row * cd * 2 + qkv * 4))
622                .arg_ptr(ws.gate.offset(row * qkv * 4))
623                .arg_ptr(ws.beta.offset(row * c.heads * 4))
624                .arg_ptr(state.recurrent)
625                .arg_ptr(ws.core.offset(row * qkv * 4))
626                .arg_u32(c.heads as u32)
627                .arg_u32(d as u32)
628                .arg_f32(1.0 / (d as f32).sqrt())
629                .launch(stream)?;
630        }
631
632        Ok(())
633    }
634
635    /// Single-token decode, carrying both states.
636    ///
637    /// The conv fuses SiLU **and** L2, so `q`/`k` reach `kda_recurrent` already normalised —
638    /// exactly the pre-normalised contract that kernel takes. Re-normalising here would silently
639    /// restore the bf16 rounding the fused write destroyed and look like a kernel bug.
640    ///
641    /// Result lands in `ws.final_out`; `state` is updated in place.
642    pub fn decode(
643        &self,
644        gpu: &dyn GpuBackend,
645        hidden: DevicePtr,
646        state: &KdaSeqState,
647        ws: &Glm5NextKdaWorkspace,
648        stream: u64,
649    ) -> Result<()> {
650        self.front_end(gpu, hidden, 1, ws, stream)?;
651        self.stateful_row(gpu, 0, state, ws, stream)?;
652        self.back_end(gpu, 1, ws, stream)
653    }
654
655    /// K tokens of ONE sequence: the projections batched, the recurrence NOT.
656    ///
657    /// This is the speculative-verify body. The weight-heavy halves — `Self::front_end`'s
658    /// q/k/v, both low-rank gate pairs and `b_proj`, and `Self::back_end`'s `o_proj` — run
659    /// once over all K rows, so a K-token verify reads KDA's 4.7 GB/rank/token ONCE instead of
660    /// K times. That is the entire reason speculation can pay on this model.
661    ///
662    /// ðŸ”ī **Bit-identical to K serial [`Self::decode`] calls**, which is not a nicety: an
663    /// accepted draft token must be the token the unspeculated engine would have emitted, or
664    /// speculation is silently lossy. It holds because `dense_gemv_bf16_batchm` reproduces each
665    /// row's exact K-iteration order and reduction tree (`ops::dense_mm_bf16`), the pack / gate
666    /// / sigmoid / `o_norm` kernels are grid-parallel over the token axis, and
667    /// `Self::stateful_row` walks the state one token at a time exactly as `decode` does.
668    ///
669    /// `snapshots[t]` — `(h_dst, conv_dst)` — receives the state AFTER row `t`, which is what a
670    /// partial accept rewinds to. Pass `k - 1` of them (a full accept never rewinds) or none.
671    pub fn decode_k(
672        &self,
673        gpu: &dyn GpuBackend,
674        hidden: DevicePtr,
675        k: usize,
676        state: &KdaSeqState,
677        ws: &Glm5NextKdaWorkspace,
678        snapshots: &[(DevicePtr, DevicePtr)],
679        stream: u64,
680    ) -> Result<()> {
681        if k == 0 || k > ws.max_tokens {
682            bail!(
683                "KDA decode_k of {k} tokens does not fit a workspace built for {}",
684                ws.max_tokens
685            );
686        }
687        let c = &self.cfg;
688        let (h_bytes, conv_bytes) = (c.recurrent_state_elems() * 4, c.conv_state_elems() * 4);
689        self.front_end(gpu, hidden, k, ws, stream)?;
690        for row in 0..k {
691            self.stateful_row(gpu, row, state, ws, stream)?;
692            if let Some((h_dst, conv_dst)) = snapshots.get(row) {
693                gpu.copy_d2d_async(state.recurrent, *h_dst, h_bytes, stream)?;
694                gpu.copy_d2d_async(state.conv, *conv_dst, conv_bytes, stream)?;
695            }
696        }
697        self.back_end(gpu, k, ws, stream)
698    }
699
700    /// Chunked prefill over `t` tokens from the carried state.
701    pub fn prefill(
702        &self,
703        gpu: &dyn GpuBackend,
704        hidden: DevicePtr,
705        t: usize,
706        state: &KdaSeqState,
707        ws: &Glm5NextKdaWorkspace,
708        stream: u64,
709    ) -> Result<()> {
710        self.prefill_with_pad_fill(gpu, hidden, t, state, ws, 0.0, stream)
711    }
712
713    /// [`Self::prefill`] with the padded q/k/v tails primed to an arbitrary value.
714    ///
715    /// Production passes zero. The numeric gate passes poison, because `kda_chunk_*` guard past
716    /// `T` **in-kernel** and that guard needs a test with teeth: the Slice-5 bug wrote entirely
717    /// correct outputs while leaving the carried recurrent state off by 1.623e13, so a pad tail
718    /// the caller zeroes proves nothing.
719    #[allow(clippy::too_many_arguments)]
720    pub fn prefill_with_pad_fill(
721        &self,
722        gpu: &dyn GpuBackend,
723        hidden: DevicePtr,
724        t: usize,
725        state: &KdaSeqState,
726        ws: &Glm5NextKdaWorkspace,
727        pad_fill: f32,
728        stream: u64,
729    ) -> Result<()> {
730        let c = &self.cfg;
731        let (qkv, cd, hd) = (c.qkv_dim(), c.conv_dim(), c.head_dim);
732        if t == 0 || t > ws.max_tokens {
733            bail!(
734                "prefill of {t} tokens does not fit a workspace built for {}",
735                ws.max_tokens
736            );
737        }
738        let nchunks = t.div_ceil(c.chunk);
739        let tp = nchunks * c.chunk;
740        self.front_end(gpu, hidden, t, ws, stream)?;
741
742        // Prefill conv is conv + SiLU ONLY — L2 is a separate launch over q|k.
743        KernelLaunch::new(gpu, self.kernels.conv_prefill)
744            .grid([div_ceil(cd as u32, 256), 1, 1])
745            .block([256, 1, 1])
746            .arg_ptr(state.conv)
747            .arg_ptr(ws.qkv_proj)
748            .arg_ptr(self.weights.conv.weight)
749            .arg_ptr(DevicePtr::NULL)
750            .arg_ptr(ws.conv_out)
751            .arg_u32(cd as u32)
752            .arg_u32(c.conv_kernel as u32)
753            .arg_u32(t as u32)
754            .arg_u32(cd as u32)
755            .arg_u32(cd as u32)
756            .launch(stream)?;
757        KernelLaunch::new(gpu, self.kernels.l2)
758            .grid([(c.qk_channels() / hd) as u32, t as u32, 1])
759            .block([hd as u32, 1, 1])
760            .arg_ptr(ws.conv_out)
761            .arg_u32(hd as u32)
762            .arg_f32(c.l2_eps)
763            .arg_u32(cd as u32)
764            .launch(stream)?;
765
766        // Gate and beta are read at padded positions by `kda_chunk_prepare`; zero them so a guard
767        // failure surfaces in q/k/v, which the regression deliberately poisons.
768        for (buf, real, padded) in [
769            (ws.gate, t * qkv, tp * qkv),
770            (ws.beta, t * c.heads, tp * c.heads),
771        ] {
772            if padded == real {
773                continue;
774            }
775            KernelLaunch::new(gpu, self.kernels.fill)
776                .grid([div_ceil((padded - real) as u32, 256), 1, 1])
777                .block([256, 1, 1])
778                .arg_ptr(buf.offset(real * 4))
779                .arg_u32((padded - real) as u32)
780                .arg_f32(0.0)
781                .launch(stream)?;
782        }
783        for p in [ws.q_f32, ws.k_f32, ws.v_f32] {
784            KernelLaunch::new(gpu, self.kernels.fill)
785                .grid([div_ceil((tp * qkv) as u32, 256), 1, 1])
786                .block([256, 1, 1])
787                .arg_ptr(p)
788                .arg_u32((tp * qkv) as u32)
789                .arg_f32(pad_fill)
790                .launch(stream)?;
791        }
792        KernelLaunch::new(gpu, self.kernels.split_widen)
793            .grid([div_ceil(qkv as u32, 256), t as u32, 1])
794            .block([256, 1, 1])
795            .arg_ptr(ws.conv_out)
796            .arg_ptr(ws.q_f32)
797            .arg_ptr(ws.k_f32)
798            .arg_ptr(ws.v_f32)
799            .arg_u32(t as u32)
800            .arg_u32(qkv as u32)
801            .launch(stream)?;
802
803        KernelLaunch::new(gpu, self.kernels.chunk_prepare)
804            .grid([nchunks as u32, c.heads as u32, 1])
805            .block([BLOCK, 1, 1])
806            .shared_mem(c.smem_prepare() as u32)
807            .arg_ptr(ws.k_f32)
808            .arg_ptr(ws.v_f32)
809            .arg_ptr(ws.gate)
810            .arg_ptr(ws.beta)
811            .arg_ptr(ws.chunk_gc)
812            .arg_ptr(ws.chunk_u)
813            .arg_ptr(ws.chunk_w)
814            .arg_u32(c.heads as u32)
815            .arg_u32(hd as u32)
816            .arg_u32(c.chunk as u32)
817            .arg_u32(t as u32)
818            .launch(stream)?;
819        KernelLaunch::new(gpu, self.kernels.chunk_scan)
820            .grid([c.heads as u32, 1, 1])
821            .block([BLOCK, 1, 1])
822            .shared_mem(c.smem_scan() as u32)
823            .arg_ptr(ws.q_f32)
824            .arg_ptr(ws.k_f32)
825            .arg_ptr(ws.chunk_gc)
826            .arg_ptr(ws.chunk_u)
827            .arg_ptr(ws.chunk_w)
828            .arg_ptr(state.recurrent)
829            .arg_ptr(ws.core)
830            .arg_u32(c.heads as u32)
831            .arg_u32(hd as u32)
832            .arg_u32(c.chunk as u32)
833            .arg_u32(nchunks as u32)
834            .arg_u32(t as u32)
835            .arg_f32(1.0 / (hd as f32).sqrt())
836            .launch(stream)?;
837
838        self.back_end(gpu, t, ws, stream)
839    }
840}