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}