metrale_model_arch/glm5next_kda/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The GLM-5.3-Flash KDA attention block: one bound `self_attn` block per KDA layer.
4//!
5//! Owner: model-arch (GLM-5.3-Flash KDA).
6//! Invariants:
7//! - [`Glm5NextKdaLayer::new`] and [`Glm5NextKdaWorkspace::new`] refuse a config that fails
8//!   [`Glm5NextKdaConfig::validate`].
9//! - The forward calls (`decode`, `decode_k`, `prefill`) allocate nothing: they write into the
10//!   caller's [`Glm5NextKdaWorkspace`] and [`KdaSeqState`].
11//!
12//! ```text
13//! q|k|v_proj -> pack -> conv1d + SiLU -> L2(q,k only) -> kda_gate / sigmoid(b_proj)
14//!            -> kda_chunk (prefill) | kda_recurrent (decode)
15//!            -> sigmoid-gated RMSNorm(o_norm, g_b(g_a(h))) -> o_proj
16//! ```
17//!
18//! * The binder accepts only BF16 and F32 tensors ([`binding::KDA_TENSORS`]), and every
19//!   projection runs a BF16 kernel, so there is no dequantisation on this path.
20//! * The checkpoint stores `q_conv1d`/`k_conv1d`/`v_conv1d` separately; the binder concatenates
21//!   them in q, k, v order, the order `front_end` packs the projections in.
22//! * `o_norm` gates with a sigmoid, not SiLU, so it runs `kda_o_norm_gated_bf16`
23//!   (`kernels/gb10/common/kda_layer_ops.cu`) rather than `gated_rms_norm`.
24//! * `dense_mm_bf16` and `cublas_bf16_proj_dense` take no output row stride, so the three q/k/v
25//!   projections go to separate `[T, qkv]` buffers and `kda_pack_qkv_bf16` interleaves them.
26//! * The conv state keeps `conv_kernel` slots and the conv kernels shift left before convolving;
27//!   see [`KdaSeqState`].
28//! * Decode fuses conv, SiLU and L2 in one kernel; prefill runs a separate `l2_norm_bf16` over the
29//!   q|k channels. V is never normalised.
30
31pub mod binding;
32pub mod tp;
33pub mod tp_bind;
34
35mod config;
36mod decode;
37mod kernels;
38mod prefill;
39pub use config::{Glm5NextKdaConfig, Glm5NextKdaWeights};
40pub use kernels::Glm5NextKdaKernels;
41
42use anyhow::{Result, bail};
43use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
44use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
45
46use metrale_model_layers::layers::ops;
47use metrale_model_layers::weight_map::DenseWeight;
48
49/// 2026-09-25: Largest shared memory a prefill chunk may need. [`Glm5NextKdaConfig::validate`]
50/// refuses a `chunk` whose `smem_prepare` or `smem_scan` exceeds it.
51pub const SMEM_CEILING: usize = 49_152;
52
53const BLOCK: u32 = 128;
54
55/// 2026-09-25: V columns one block of the 1R+1W recurrent kernel owns: one warp. At
56/// `head_dim = 128` its request is `(3 * 128 + 32 * 129) * 4` = 18,048 B.
57const KDA_V_PER_BLOCK: usize = 32;
58/// 2026-09-25: Largest shared memory `stateful_row` requests for the 1R+1W kernel; a larger need
59/// launches the 2R+2W kernel instead.
60const KDA_SMEM_BUDGET: usize = 48 * 1024;
61
62/// 2026-09-25: `METRALE_GLM_KDA_NO_SMEM=1` selects the 2R+2W recurrent kernel. Read once per
63/// process; it is checked on every decode row.
64fn kda_no_smem() -> bool {
65    static F: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
66    *F.get_or_init(|| std::env::var("METRALE_GLM_KDA_NO_SMEM").as_deref() == Ok("1"))
67}
68
69/// 2026-09-25: The per-sequence state a KDA layer carries. The kernels update both buffers in place.
70///
71/// The conv buffer holds `conv_kernel` slots per channel. The conv kernels shift the window left
72/// and write the new input into the last slot before convolving, so slot 0 is shifted out
73/// unread. A state kept as `conv_kernel - 1` slots (the golden generators' layout) maps to
74/// slots `1..conv_kernel` here, before the shift.
75#[derive(Clone, Copy, Debug)]
76pub struct KdaSeqState {
77    /// 2026-09-25: `[conv_dim, conv_kernel]` FP32.
78    pub conv: DevicePtr,
79    /// 2026-09-25: `[heads, head_dim, head_dim]` FP32, K-major (`S[k * head_dim + v]`).
80    pub recurrent: DevicePtr,
81}
82
83/// 2026-09-25: Scratch for the forward path, sized once for `max_tokens` (prefill buffers padded
84/// to a whole number of chunks). The loader shares one workspace across the KDA layers.
85///
86/// The intermediate buffers are `pub` so the numeric tests can compare each stage, not only the
87/// layer output.
88pub struct Glm5NextKdaWorkspace {
89    /// 2026-09-25: `[3, T, qkv]` BF16: the q, k and v projection outputs, before the pack.
90    pub qkv_parts: DevicePtr,
91    /// 2026-09-25: `[T, conv_dim]` BF16: q|k|v per token, before the conv.
92    pub qkv_proj: DevicePtr,
93    /// 2026-09-25: `[T, conv_dim]` BF16: after conv + SiLU, with q|k L2-normalised (inside the
94    /// conv kernel on decode, by `l2_norm_bf16` in place on prefill).
95    pub conv_out: DevicePtr,
96    /// 2026-09-25: `[T_pad, qkv]` FP32: q, k and v widened for the chunked prefill. Prefill only.
97    pub q_f32: DevicePtr,
98    pub k_f32: DevicePtr,
99    pub v_f32: DevicePtr,
100    /// 2026-09-25: `[T_pad, heads, head_dim]` FP32: the bounded log-decay `kda_gate` writes.
101    pub gate: DevicePtr,
102    /// 2026-09-25: `[T_pad, heads]` FP32: `sigmoid(b_proj(h))`.
103    pub beta: DevicePtr,
104    /// 2026-09-25: `[T_pad, heads, head_dim]` FP32: the KDA core output, before `o_norm`.
105    pub core: DevicePtr,
106    /// 2026-09-25: `[T, qkv]` BF16: `f_b(f_a(h))`, the forget-gate input `kda_gate` reads. It has
107    /// the same shape as `out_gate` and must not alias it.
108    pub g_raw: DevicePtr,
109    /// 2026-09-25: `[T, qkv]` BF16: `g_b(g_a(h))`, the low-rank output gate.
110    pub out_gate: DevicePtr,
111    /// 2026-09-25: `[T, qkv]` BF16: after the sigmoid-gated RMSNorm.
112    pub o_norm_out: DevicePtr,
113    /// 2026-09-25: `[T, hidden]` BF16: the block output.
114    pub final_out: DevicePtr,
115    lowrank: DevicePtr,
116    beta_bf16: DevicePtr,
117    chunk_gc: DevicePtr,
118    chunk_u: DevicePtr,
119    chunk_w: DevicePtr,
120    max_tokens: usize,
121    t_pad: usize,
122}
123
124impl Glm5NextKdaWorkspace {
125    pub fn new(gpu: &dyn GpuBackend, cfg: &Glm5NextKdaConfig, max_tokens: usize) -> Result<Self> {
126        cfg.validate()?;
127        if max_tokens == 0 {
128            bail!("workspace needs max_tokens >= 1");
129        }
130        let (qkv, cd, hd, h) = (cfg.qkv_dim(), cfg.conv_dim(), cfg.head_dim, cfg.heads);
131        let t = max_tokens;
132        let t_pad = t.div_ceil(cfg.chunk) * cfg.chunk;
133        let n = t_pad * qkv;
134        Ok(Self {
135            qkv_parts: gpu.alloc(3 * t * qkv * 2)?,
136            qkv_proj: gpu.alloc(t * cd * 2)?,
137            conv_out: gpu.alloc(t * cd * 2)?,
138            q_f32: gpu.alloc(n * 4)?,
139            k_f32: gpu.alloc(n * 4)?,
140            v_f32: gpu.alloc(n * 4)?,
141            gate: gpu.alloc(n * 4)?,
142            beta: gpu.alloc(t_pad * h * 4)?,
143            core: gpu.alloc(n * 4)?,
144            g_raw: gpu.alloc(t * qkv * 2)?,
145            out_gate: gpu.alloc(t * qkv * 2)?,
146            o_norm_out: gpu.alloc(t * qkv * 2)?,
147            final_out: gpu.alloc(t * cfg.hidden * 2)?,
148            lowrank: gpu.alloc(t * hd * 2)?,
149            beta_bf16: gpu.alloc(t * h * 2)?,
150            chunk_gc: gpu.alloc(n * 4)?,
151            chunk_u: gpu.alloc(n * 4)?,
152            chunk_w: gpu.alloc(n * 4)?,
153            max_tokens: t,
154            t_pad,
155        })
156    }
157
158    pub fn max_tokens(&self) -> usize {
159        self.max_tokens
160    }
161    pub fn t_pad(&self) -> usize {
162        self.t_pad
163    }
164}
165
166/// 2026-09-25: One bound KDA attention block.
167pub struct Glm5NextKdaLayer {
168    /// 2026-09-25: The checkpoint layer index this block was bound from.
169    pub layer_idx: usize,
170    pub cfg: Glm5NextKdaConfig,
171    pub weights: Glm5NextKdaWeights,
172    pub kernels: Glm5NextKdaKernels,
173}
174
175impl Glm5NextKdaLayer {
176    pub fn new(
177        layer_idx: usize,
178        cfg: Glm5NextKdaConfig,
179        weights: Glm5NextKdaWeights,
180        kernels: Glm5NextKdaKernels,
181    ) -> Result<Self> {
182        cfg.validate()?;
183        Ok(Self {
184            layer_idx,
185            cfg,
186            weights,
187            kernels,
188        })
189    }
190
191    #[allow(clippy::too_many_arguments)]
192    fn gemm(
193        &self,
194        gpu: &dyn GpuBackend,
195        input: DevicePtr,
196        weight: &DenseWeight,
197        out: DevicePtr,
198        m: usize,
199        n: usize,
200        k: usize,
201        stream: u64,
202    ) -> Result<()> {
203        // 2026-09-25: Only M above `DENSE_GEMV_BATCHM_MAX_M` goes to cuBLASLt. Below it
204        // `dense_mm_bf16` runs the M = 1 GEMV or the batched GEMV, whose rows carry the same
205        // bits, so the cuBLASLt switch never changes those widths.
206        if m > ops::DENSE_GEMV_BATCHM_MAX_M as usize && crate::glm5next_layer::cublas_wide_proj() {
207            return ops::cublas_bf16_proj_dense(
208                input,
209                weight.weight,
210                out,
211                m as u32,
212                n as u32,
213                k as u32,
214                stream,
215            );
216        }
217        ops::dense_mm_bf16(
218            gpu,
219            &ops::DenseMmKernels {
220                gemm: self.kernels.gemm,
221                gemv: self.kernels.gemv,
222                batchm: self.kernels.gemv_batchm,
223            },
224            input,
225            weight.weight,
226            out,
227            m,
228            n,
229            k,
230            stream,
231        )
232    }
233
234    /// 2026-09-25: Projections, forget gate, beta and output gate, shared by decode and prefill.
235    /// All of them read the block's input hidden state, never the post-conv activations.
236    fn front_end(
237        &self,
238        gpu: &dyn GpuBackend,
239        hidden: DevicePtr,
240        t: usize,
241        ws: &Glm5NextKdaWorkspace,
242        stream: u64,
243    ) -> Result<()> {
244        let c = &self.cfg;
245        let (hid, qkv, hd) = (c.hidden, c.qkv_dim(), c.head_dim);
246
247        // 2026-09-25: Three separate `[T, qkv]` projections, then one pack (see the module doc).
248        for (i, w) in [
249            &self.weights.q_proj,
250            &self.weights.k_proj,
251            &self.weights.v_proj,
252        ]
253        .into_iter()
254        .enumerate()
255        {
256            self.gemm(
257                gpu,
258                hidden,
259                w,
260                ws.qkv_parts.offset(i * t * qkv * 2),
261                t,
262                qkv,
263                hid,
264                stream,
265            )?;
266        }
267        KernelLaunch::new(gpu, self.kernels.pack)
268            .grid([div_ceil(qkv as u32, 256), t as u32, 1])
269            .block([256, 1, 1])
270            .arg_ptr(ws.qkv_parts)
271            .arg_ptr(ws.qkv_parts.offset(t * qkv * 2))
272            .arg_ptr(ws.qkv_parts.offset(2 * t * qkv * 2))
273            .arg_ptr(ws.qkv_proj)
274            .arg_u32(t as u32)
275            .arg_u32(qkv as u32)
276            .launch(stream)?;
277
278        // 2026-09-25: Low-rank forget gate, hidden -> head_dim -> heads * head_dim, then
279        // `lower_bound * sigmoid(exp(A_log[h]) * (g[c] + dt_bias[c]))` in `kda_gate_bf16`.
280        // `A_log` is per head and `dt_bias` per channel.
281        self.gemm(
282            gpu,
283            hidden,
284            &self.weights.f_a,
285            ws.lowrank,
286            t,
287            hd,
288            hid,
289            stream,
290        )?;
291        self.gemm(
292            gpu,
293            ws.lowrank,
294            &self.weights.f_b,
295            ws.g_raw,
296            t,
297            qkv,
298            hd,
299            stream,
300        )?;
301        KernelLaunch::new(gpu, self.kernels.gate)
302            .grid([(t * c.heads) as u32, 1, 1])
303            .block([BLOCK, 1, 1])
304            .arg_ptr(ws.g_raw)
305            .arg_ptr(self.weights.dt_bias)
306            .arg_ptr(self.weights.a_log)
307            .arg_ptr(ws.gate)
308            .arg_u32(t as u32)
309            .arg_u32(c.heads as u32)
310            .arg_u32(hd as u32)
311            .arg_f32(c.gate_lower_bound)
312            .launch(stream)?;
313
314        // 2026-09-25: beta = sigmoid(b_proj(hidden)); the KDA kernels read it after the sigmoid.
315        self.gemm(
316            gpu,
317            hidden,
318            &self.weights.b_proj,
319            ws.beta_bf16,
320            t,
321            c.heads,
322            hid,
323            stream,
324        )?;
325        let n = t * c.heads;
326        KernelLaunch::new(gpu, self.kernels.sigmoid)
327            .grid([div_ceil(n as u32, 256), 1, 1])
328            .block([256, 1, 1])
329            .arg_ptr(ws.beta_bf16)
330            .arg_ptr(ws.beta)
331            .arg_u32(n as u32)
332            .launch(stream)?;
333
334        // 2026-09-25: Low-rank output gate; a KDA block has no `Z` tensor (`KDA_TENSORS`).
335        self.gemm(
336            gpu,
337            hidden,
338            &self.weights.g_a,
339            ws.lowrank,
340            t,
341            hd,
342            hid,
343            stream,
344        )?;
345        self.gemm(
346            gpu,
347            ws.lowrank,
348            &self.weights.g_b,
349            ws.out_gate,
350            t,
351            qkv,
352            hd,
353            stream,
354        )
355    }
356
357    /// 2026-09-25: Sigmoid-gated RMSNorm, then `o_proj`.
358    fn back_end(
359        &self,
360        gpu: &dyn GpuBackend,
361        t: usize,
362        ws: &Glm5NextKdaWorkspace,
363        stream: u64,
364    ) -> Result<()> {
365        let c = &self.cfg;
366        KernelLaunch::new(gpu, self.kernels.o_norm)
367            .grid([(t * c.heads) as u32, 1, 1])
368            .block([c.head_dim as u32, 1, 1])
369            .arg_ptr(ws.core)
370            .arg_ptr(ws.out_gate)
371            .arg_ptr(self.weights.o_norm.weight)
372            .arg_ptr(ws.o_norm_out)
373            .arg_u32(c.head_dim as u32)
374            .arg_f32(c.rms_norm_eps)
375            .launch(stream)?;
376        self.gemm(
377            gpu,
378            ws.o_norm_out,
379            &self.weights.o_proj,
380            ws.final_out,
381            t,
382            c.hidden,
383            c.qkv_dim(),
384            stream,
385        )
386    }
387}