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}