metrale_model_layers/layers/ops/
ssm_gdn_a.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the GDN decode kernels (single-token, fused-norm,
4//! strided, FP16 h-state, 2-token verify), the fused conv + decode kernel, the
5//! register-resident prefill recurrence, and the SSM h-state dtype converters.
6//!
7//! Owner: model-layers ops (GDN).
8//! Invariants: none beyond the types.
9
10#![allow(unused_imports)]
11
12use anyhow::Result;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
14use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
15
16use crate::layers::moe;
17use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
18
19use super::*;
20
21/// 2026-09-25: FP16 h-state twin of [`gdn_decode_f32_strided_norm`], used when
22/// `ssm_h_fp16_enabled()` is set. It adds one argument, `h_seq_stride`: the
23/// per-sequence slot pitch of the h-state pool in `__half` elements. The pitch
24/// depends on how the pool was sized, not on the head dims, so the caller
25/// passes it.
26#[allow(clippy::too_many_arguments)]
27pub fn gdn_decode_f16_strided_norm(
28    gpu: &dyn GpuBackend,
29    kernel: KernelHandle,
30    h_state: DevicePtr,
31    query: DevicePtr,
32    key: DevicePtr,
33    value: DevicePtr,
34    gate: DevicePtr,
35    beta: DevicePtr,
36    z_gate: DevicePtr,
37    norm_weight: DevicePtr,
38    output: DevicePtr,
39    batch_size: u32,
40    num_k_heads: u32,
41    num_v_heads: u32,
42    k_dim: u32,
43    v_dim: u32,
44    qk_stride: u32,
45    v_stride: u32,
46    gb_stride: u32,
47    z_stride: u32,
48    out_stride: u32,
49    h_seq_stride: u64,
50    eps: f32,
51    stream: u64,
52) -> Result<()> {
53    KernelLaunch::new(gpu, kernel)
54        .grid([num_v_heads, batch_size, 1])
55        .block([128, 1, 1])
56        .arg_ptr(h_state)
57        .arg_ptr(query)
58        .arg_ptr(key)
59        .arg_ptr(value)
60        .arg_ptr(gate)
61        .arg_ptr(beta)
62        .arg_ptr(z_gate)
63        .arg_ptr(norm_weight)
64        .arg_ptr(output)
65        .arg_u32(batch_size)
66        .arg_u32(num_k_heads)
67        .arg_u32(num_v_heads)
68        .arg_u32(k_dim)
69        .arg_u32(v_dim)
70        .arg_u32(qk_stride)
71        .arg_u32(v_stride)
72        .arg_u32(gb_stride)
73        .arg_u32(z_stride)
74        .arg_u32(out_stride)
75        .arg_u64(h_seq_stride)
76        .arg_f32(eps)
77        .launch(stream)
78}
79
80/// 2026-09-25: FP32 -> FP16 conversion of `n` h-state elements
81/// (`ssm_h_state_f32_to_f16`, round-to-nearest-even). The kernel is
82/// grid-stride, so the clamped grid still covers every element. `src` and
83/// `dst` must not alias: an in-place narrowing conversion is a data race.
84pub fn ssm_h_state_f32_to_f16(
85    gpu: &dyn GpuBackend,
86    kernel: KernelHandle,
87    src: DevicePtr,
88    dst: DevicePtr,
89    n: u64,
90    stream: u64,
91) -> Result<()> {
92    const BLOCK: u32 = 256;
93    let blocks = div_ceil(n as u32, BLOCK).clamp(1, 4096);
94    KernelLaunch::new(gpu, kernel)
95        .grid([blocks, 1, 1])
96        .block([BLOCK, 1, 1])
97        .arg_ptr(src)
98        .arg_ptr(dst)
99        .arg_u64(n)
100        .launch(stream)
101}
102
103/// 2026-09-25: FP16 -> FP32 widening of `n` h-state elements
104/// (`ssm_h_state_f16_to_f32`). Grid-stride; `src` and `dst` must not alias.
105pub fn ssm_h_state_f16_to_f32(
106    gpu: &dyn GpuBackend,
107    kernel: KernelHandle,
108    src: DevicePtr,
109    dst: DevicePtr,
110    n: u64,
111    stream: u64,
112) -> Result<()> {
113    const BLOCK: u32 = 256;
114    let blocks = div_ceil(n as u32, BLOCK).clamp(1, 4096);
115    KernelLaunch::new(gpu, kernel)
116        .grid([blocks, 1, 1])
117        .block([BLOCK, 1, 1])
118        .arg_ptr(src)
119        .arg_ptr(dst)
120        .arg_u64(n)
121        .launch(stream)
122}
123
124/// 2026-09-25: Single-token GDN decode (`gated_delta_rule_decode`). The
125/// `batch_size` h-states are contiguous: `[batch, num_v_heads, k_dim, v_dim]`
126/// FP32.
127pub fn gdn_decode(
128    gpu: &dyn GpuBackend,
129    kernel: KernelHandle,
130    h_state: DevicePtr,
131    query: DevicePtr,
132    key: DevicePtr,
133    value: DevicePtr,
134    gate: DevicePtr,
135    beta: DevicePtr,
136    output: DevicePtr,
137    batch_size: u32,
138    num_k_heads: u32,
139    num_v_heads: u32,
140    k_dim: u32,
141    v_dim: u32,
142    stream: u64,
143) -> Result<()> {
144    KernelLaunch::new(gpu, kernel)
145        .grid([num_v_heads, batch_size, 1])
146        .block([128, 1, 1])
147        .arg_ptr(h_state)
148        .arg_ptr(query)
149        .arg_ptr(key)
150        .arg_ptr(value)
151        .arg_ptr(gate)
152        .arg_ptr(beta)
153        .arg_ptr(output)
154        .arg_u32(batch_size)
155        .arg_u32(num_k_heads)
156        .arg_u32(num_v_heads)
157        .arg_u32(k_dim)
158        .arg_u32(v_dim)
159        .launch(stream)
160}
161
162/// 2026-09-25: FP32 GDN decode fused with the gated RMS norm: the kernel
163/// normalises each v-head's output in the same CTA and writes BF16, with no
164/// FP32 output buffer in global memory.
165#[allow(clippy::too_many_arguments)]
166pub fn gdn_decode_f32_norm(
167    gpu: &dyn GpuBackend,
168    kernel: KernelHandle,
169    h_state: DevicePtr,
170    query: DevicePtr,
171    key: DevicePtr,
172    value: DevicePtr,
173    gate: DevicePtr,
174    beta: DevicePtr,
175    z_gate: DevicePtr,
176    norm_weight: DevicePtr,
177    output: DevicePtr,
178    batch_size: u32,
179    num_k_heads: u32,
180    num_v_heads: u32,
181    k_dim: u32,
182    v_dim: u32,
183    eps: f32,
184    stream: u64,
185) -> Result<()> {
186    KernelLaunch::new(gpu, kernel)
187        .grid([num_v_heads, batch_size, 1])
188        .block([128, 1, 1])
189        .arg_ptr(h_state)
190        .arg_ptr(query)
191        .arg_ptr(key)
192        .arg_ptr(value)
193        .arg_ptr(gate)
194        .arg_ptr(beta)
195        .arg_ptr(z_gate)
196        .arg_ptr(norm_weight)
197        .arg_ptr(output)
198        .arg_u32(batch_size)
199        .arg_u32(num_k_heads)
200        .arg_u32(num_v_heads)
201        .arg_u32(k_dim)
202        .arg_u32(v_dim)
203        .arg_f32(eps)
204        .launch(stream)
205}
206
207/// 2026-09-25: Token-sequential prefill recurrence with H in registers
208/// (`gated_delta_rule_prefill_regresident`). One warp owns one v-column and
209/// holds its 128 k-rows (4 per lane); there is no shared-memory H and no block
210/// barrier. Requires `k_dim == 128` and `v_dim % 4 == 0` (4 warps per block).
211#[allow(clippy::too_many_arguments)]
212pub fn gdn_prefill_regresident(
213    gpu: &dyn GpuBackend,
214    kernel: KernelHandle,
215    h_state: DevicePtr,
216    query: DevicePtr,
217    key: DevicePtr,
218    value: DevicePtr,
219    gate: DevicePtr,
220    beta: DevicePtr,
221    output: DevicePtr,
222    batch_size: u32,
223    seq_len: u32,
224    num_k_heads: u32,
225    num_v_heads: u32,
226    k_dim: u32,
227    v_dim: u32,
228    qk_stride: u32,
229    v_stride: u32,
230    gb_stride: u32,
231    stream: u64,
232) -> Result<()> {
233    KernelLaunch::new(gpu, kernel)
234        .grid([num_v_heads, batch_size, v_dim / 4])
235        .block([128, 1, 1])
236        .arg_ptr(h_state)
237        .arg_ptr(query)
238        .arg_ptr(key)
239        .arg_ptr(value)
240        .arg_ptr(gate)
241        .arg_ptr(beta)
242        .arg_ptr(output)
243        .arg_u32(batch_size)
244        .arg_u32(seq_len)
245        .arg_u32(num_k_heads)
246        .arg_u32(num_v_heads)
247        .arg_u32(k_dim)
248        .arg_u32(v_dim)
249        .arg_u32(qk_stride)
250        .arg_u32(v_stride)
251        .arg_u32(gb_stride)
252        .launch(stream)
253}
254
255/// 2026-09-25: Conv1d update + L2 norm, the GDN recurrence and the gated RMS
256/// norm in one decode launch (`gated_delta_rule_decode_f32_conv_norm`).
257///
258/// One block per k-head: it owns k-head `kh` and its `head_repeat` v-heads, so
259/// it is the only writer of their q/k and v `conv_state` rows. The block is
260/// `head_repeat * v_dim` threads, and the kernel also requires
261/// `2 * k_dim <= head_repeat * v_dim` and `k_dim == v_dim`.
262#[allow(clippy::too_many_arguments)]
263pub fn gdn_decode_f32_conv_norm(
264    gpu: &dyn GpuBackend,
265    kernel: KernelHandle,
266    h_state: DevicePtr,
267    conv_state: DevicePtr,
268    new_input: DevicePtr,
269    conv_weight: DevicePtr,
270    gate: DevicePtr,
271    beta: DevicePtr,
272    z_gate: DevicePtr,
273    norm_weight: DevicePtr,
274    output: DevicePtr,
275    batch_size: u32,
276    num_k_heads: u32,
277    num_v_heads: u32,
278    k_dim: u32,
279    v_dim: u32,
280    conv_dim: u32,
281    d_conv: u32,
282    l2_eps: f32,
283    eps: f32,
284    stream: u64,
285) -> Result<()> {
286    let head_repeat = num_v_heads / num_k_heads;
287    KernelLaunch::new(gpu, kernel)
288        .grid([num_k_heads, batch_size, 1])
289        .block([head_repeat * v_dim, 1, 1])
290        .arg_ptr(h_state)
291        .arg_ptr(conv_state)
292        .arg_ptr(new_input)
293        .arg_ptr(conv_weight)
294        .arg_ptr(DevicePtr::NULL)
295        .arg_ptr(gate)
296        .arg_ptr(beta)
297        .arg_ptr(z_gate)
298        .arg_ptr(norm_weight)
299        .arg_ptr(output)
300        .arg_u32(batch_size)
301        .arg_u32(num_k_heads)
302        .arg_u32(num_v_heads)
303        .arg_u32(k_dim)
304        .arg_u32(v_dim)
305        .arg_u32(conv_dim)
306        .arg_u32(d_conv)
307        .arg_f32(l2_eps)
308        .arg_f32(eps)
309        .launch(stream)
310}
311
312/// 2026-09-29: Dynamic shared memory each [`gdn_decode_f32_strided`] CTA reserves and does
313/// not use, from [`GDN_DECODE_STRIDED_SMEM_CAP_MIN_BATCH`] sequences: it caps the CTAs resident
314/// per SM (3 on GB10's 100 KB), so a CTA's 64 KB head state is still in L2 when its update
315/// pass re-reads it; uncapped, the second pass of a wide launch misses L2. Measured
316/// 2026-09-29 on dgx2 (`gdn_decode_strided_microtest`, per layer, uncapped / capped /
317/// per-sequence launches), 32 value heads: B=16 365 / 362 / 428 us, B=64 1904 / 1362 / 1740,
318/// B=128 3857 / 2641 / 3541; 48 value heads: B=16 672 / 507 / 589, B=128 5737 / 3902 / 4766.
319/// The output and state bits do not depend on it.
320pub const GDN_DECODE_STRIDED_SMEM_CAP_BYTES: u32 = 32 << 10;
321
322/// 2026-09-29: Smallest batch that takes the cap. Below it the cap costs more than the L2 it
323/// saves (32 value heads, B=4: 34.1 us uncapped, 43.7 us capped).
324pub const GDN_DECODE_STRIDED_SMEM_CAP_MIN_BATCH: u32 = 16;
325
326/// 2026-09-29: The dynamic shared memory [`gdn_decode_f32_strided`] reserves for `batch_size`.
327pub fn gdn_decode_strided_smem(batch_size: u32) -> u32 {
328    if batch_size >= GDN_DECODE_STRIDED_SMEM_CAP_MIN_BATCH {
329        GDN_DECODE_STRIDED_SMEM_CAP_BYTES
330    } else {
331        0
332    }
333}
334
335/// 2026-09-25: Strided FP32 GDN decode for several sequences in one launch:
336/// Q/K/V, gate/beta and the output are rows `qk_stride`, `v_stride`,
337/// `gb_stride` and `out_stride` apart per sequence.
338#[allow(clippy::too_many_arguments)]
339pub fn gdn_decode_f32_strided(
340    gpu: &dyn GpuBackend,
341    kernel: KernelHandle,
342    h_state: DevicePtr,
343    query: DevicePtr,
344    key: DevicePtr,
345    value: DevicePtr,
346    gate: DevicePtr,
347    beta: DevicePtr,
348    output: DevicePtr,
349    batch_size: u32,
350    num_k_heads: u32,
351    num_v_heads: u32,
352    k_dim: u32,
353    v_dim: u32,
354    qk_stride: u32,
355    v_stride: u32,
356    gb_stride: u32,
357    out_stride: u32,
358    stream: u64,
359) -> Result<()> {
360    KernelLaunch::new(gpu, kernel)
361        .grid([num_v_heads, batch_size, 1])
362        .block([128, 1, 1])
363        .shared_mem(gdn_decode_strided_smem(batch_size))
364        .arg_ptr(h_state)
365        .arg_ptr(query)
366        .arg_ptr(key)
367        .arg_ptr(value)
368        .arg_ptr(gate)
369        .arg_ptr(beta)
370        .arg_ptr(output)
371        .arg_u32(batch_size)
372        .arg_u32(num_k_heads)
373        .arg_u32(num_v_heads)
374        .arg_u32(k_dim)
375        .arg_u32(v_dim)
376        .arg_u32(qk_stride)
377        .arg_u32(v_stride)
378        .arg_u32(gb_stride)
379        .arg_u32(out_stride)
380        .launch(stream)
381}
382
383/// 2026-09-25: [`gdn_decode_f32_strided`] fused with the gated RMS norm: it
384/// writes the BF16 post-norm output directly, `out_stride` apart per sequence.
385#[allow(clippy::too_many_arguments)]
386pub fn gdn_decode_f32_strided_norm(
387    gpu: &dyn GpuBackend,
388    kernel: KernelHandle,
389    h_state: DevicePtr,
390    query: DevicePtr,
391    key: DevicePtr,
392    value: DevicePtr,
393    gate: DevicePtr,
394    beta: DevicePtr,
395    z_gate: DevicePtr,
396    norm_weight: DevicePtr,
397    output: DevicePtr,
398    batch_size: u32,
399    num_k_heads: u32,
400    num_v_heads: u32,
401    k_dim: u32,
402    v_dim: u32,
403    qk_stride: u32,
404    v_stride: u32,
405    gb_stride: u32,
406    z_stride: u32,
407    out_stride: u32,
408    eps: f32,
409    stream: u64,
410) -> Result<()> {
411    KernelLaunch::new(gpu, kernel)
412        .grid([num_v_heads, batch_size, 1])
413        .block([128, 1, 1])
414        .arg_ptr(h_state)
415        .arg_ptr(query)
416        .arg_ptr(key)
417        .arg_ptr(value)
418        .arg_ptr(gate)
419        .arg_ptr(beta)
420        .arg_ptr(z_gate)
421        .arg_ptr(norm_weight)
422        .arg_ptr(output)
423        .arg_u32(batch_size)
424        .arg_u32(num_k_heads)
425        .arg_u32(num_v_heads)
426        .arg_u32(k_dim)
427        .arg_u32(v_dim)
428        .arg_u32(qk_stride)
429        .arg_u32(v_stride)
430        .arg_u32(gb_stride)
431        .arg_u32(z_stride)
432        .arg_u32(out_stride)
433        .arg_f32(eps)
434        .launch(stream)
435}
436
437/// 2026-09-25: Two tokens through the GDN recurrence in one launch
438/// (`gated_delta_rule_chunk2`). The state after the first token is written to
439/// `h_state_intermediate`, for rollback when the draft is rejected. The
440/// strides are in elements, not bytes.
441#[allow(clippy::too_many_arguments)]
442pub fn gdn_decode_chunk2(
443    gpu: &dyn GpuBackend,
444    kernel: KernelHandle,
445    h_state: DevicePtr,
446    query: DevicePtr,
447    key: DevicePtr,
448    value: DevicePtr,
449    gate: DevicePtr,
450    beta: DevicePtr,
451    output: DevicePtr,
452    h_state_intermediate: DevicePtr,
453    batch_size: u32,
454    num_k_heads: u32,
455    num_v_heads: u32,
456    k_dim: u32,
457    v_dim: u32,
458    qk_stride: u32,
459    v_stride: u32,
460    gb_stride: u32,
461    stream: u64,
462) -> Result<()> {
463    KernelLaunch::new(gpu, kernel)
464        .grid([num_v_heads, batch_size, 1])
465        .block([128, 1, 1])
466        .arg_ptr(h_state)
467        .arg_ptr(query)
468        .arg_ptr(key)
469        .arg_ptr(value)
470        .arg_ptr(gate)
471        .arg_ptr(beta)
472        .arg_ptr(output)
473        .arg_ptr(h_state_intermediate)
474        .arg_u32(batch_size)
475        .arg_u32(num_k_heads)
476        .arg_u32(num_v_heads)
477        .arg_u32(k_dim)
478        .arg_u32(v_dim)
479        .arg_u32(qk_stride)
480        .arg_u32(v_stride)
481        .arg_u32(gb_stride)
482        .launch(stream)
483}