metrale_model_layers/layers/ops/
ssm_mamba.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the causal conv1d updates (decode, fused L2 norm,
4//! strided, prefill) and the Mamba-2 SSM prefill kernels, plus the re-export
5//! of the GDN verify conv launchers.
6//!
7//! Owner: model-layers ops (SSM).
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#[path = "ssm_mamba_gdn_verify.rs"]
21mod gdn_verify;
22pub use gdn_verify::{
23    gdn_verify_fused_conv_k2, gdn_verify_fused_conv_kn, gdn_verify_fused_conv_kn_batched,
24    gdn_verify_fused_norm_k2,
25};
26
27/// 2026-09-25: Causal conv1d update + SiLU for one decode token per sequence
28/// (`causal_conv1d_update`). `conv_state`, `input` and `output` are contiguous
29/// per sequence: `[batch, d_inner, d_conv]`, `[batch, d_inner]`,
30/// `[batch, d_inner]`.
31pub fn conv1d_update(
32    gpu: &dyn GpuBackend,
33    kernel: KernelHandle,
34    conv_state: DevicePtr,
35    input: DevicePtr,
36    weight: &DenseWeight,
37    output: DevicePtr,
38    d_inner: u32,
39    d_conv: u32,
40    batch_size: u32,
41    stream: u64,
42) -> Result<()> {
43    let bias_ptr = DevicePtr::NULL;
44    KernelLaunch::new(gpu, kernel)
45        .grid([div_ceil(d_inner, 256), batch_size, 1])
46        .block([256, 1, 1])
47        .arg_ptr(conv_state)
48        .arg_ptr(input)
49        .arg_ptr(weight.weight)
50        .arg_ptr(bias_ptr)
51        .arg_ptr(output)
52        .arg_u32(batch_size)
53        .arg_u32(d_inner)
54        .arg_u32(d_conv)
55        .launch(stream)
56}
57
58/// 2026-09-25: Conv1d update + SiLU, then an L2 norm per `head_dim` group over
59/// the Q and K channels (`0..qk_channels`), in one kernel
60/// (`causal_conv1d_update_l2norm`). The V channels get SiLU only.
61#[allow(clippy::too_many_arguments)]
62pub fn conv1d_update_l2norm(
63    gpu: &dyn GpuBackend,
64    kernel: KernelHandle,
65    conv_state: DevicePtr,
66    input: DevicePtr,
67    weight: &DenseWeight,
68    output: DevicePtr,
69    d_inner: u32,
70    d_conv: u32,
71    batch_size: u32,
72    qk_channels: u32,
73    head_dim: u32,
74    l2_eps: f32,
75    stream: u64,
76) -> Result<()> {
77    let bias_ptr = DevicePtr::NULL;
78    KernelLaunch::new(gpu, kernel)
79        .grid([div_ceil(d_inner, 256), batch_size, 1])
80        .block([256, 1, 1])
81        .arg_ptr(conv_state)
82        .arg_ptr(input)
83        .arg_ptr(weight.weight)
84        .arg_ptr(bias_ptr)
85        .arg_ptr(output)
86        .arg_u32(batch_size)
87        .arg_u32(d_inner)
88        .arg_u32(d_conv)
89        .arg_u32(qk_channels)
90        .arg_u32(head_dim)
91        .arg_f32(l2_eps)
92        .launch(stream)
93}
94
95/// 2026-09-25: Conv1d update + SiLU + L2 norm for `batch_size` decode
96/// sequences in one launch, with FP32 output
97/// (`causal_conv1d_update_l2norm_f32_strided`). The input and output row
98/// strides are separate arguments because the concurrent-decode input rows
99/// are the QKVZ projection's, `qkvz_size` apart, while the output rows are
100/// `d_inner` apart; a kernel that used `d_inner` for both would read sequence
101/// `b >= 1` from the wrong row.
102///
103/// `conv_state` keeps the `(b * d_inner + ch) * d_conv` layout, so the caller
104/// must have checked that the sequences' pool slots are contiguous.
105#[allow(clippy::too_many_arguments)]
106pub fn conv1d_update_l2norm_strided(
107    gpu: &dyn GpuBackend,
108    kernel: KernelHandle,
109    conv_state: DevicePtr,
110    input: DevicePtr,
111    weight: &DenseWeight,
112    output: DevicePtr,
113    d_inner: u32,
114    d_conv: u32,
115    batch_size: u32,
116    qk_channels: u32,
117    head_dim: u32,
118    l2_eps: f32,
119    input_stride: u32,
120    output_stride: u32,
121    stream: u64,
122) -> Result<()> {
123    let bias_ptr = DevicePtr::NULL;
124    KernelLaunch::new(gpu, kernel)
125        .grid([div_ceil(d_inner, 256), batch_size, 1])
126        .block([256, 1, 1])
127        .arg_ptr(conv_state)
128        .arg_ptr(input)
129        .arg_ptr(weight.weight)
130        .arg_ptr(bias_ptr)
131        .arg_ptr(output)
132        .arg_u32(batch_size)
133        .arg_u32(d_inner)
134        .arg_u32(d_conv)
135        .arg_u32(qk_channels)
136        .arg_u32(head_dim)
137        .arg_f32(l2_eps)
138        .arg_u32(input_stride)
139        .arg_u32(output_stride)
140        .launch(stream)
141}
142
143/// 2026-09-25: Conv1d update + SiLU over `seq_len` prefill tokens, with
144/// separate per-token input and output strides. Runs the token-parallel kernel
145/// (`conv1d_prefill_tp_k`) when its handle is non-zero and
146/// `METRALE_CONV1D_TP` is not `0`, else `causal_conv1d_update_prefill`, one
147/// thread per channel walking the tokens.
148#[allow(clippy::too_many_arguments)]
149pub fn conv1d_update_prefill(
150    gpu: &dyn GpuBackend,
151    kernel: KernelHandle,
152    conv1d_prefill_tp_k: KernelHandle,
153    conv_state: DevicePtr,
154    input: DevicePtr,
155    weight: &DenseWeight,
156    bias: DevicePtr,
157    output: DevicePtr,
158    d_inner: u32,
159    d_conv: u32,
160    seq_len: u32,
161    input_stride: u32,
162    output_stride: u32,
163    stream: u64,
164) -> Result<()> {
165    // 2026-09-25: Tokens can run in parallel because output[t] depends only on
166    // the inputs x[t-3..=t], never on an earlier output. In the token-parallel
167    // kernel a warp spans 32 channels, so the `[t * stride + ch]` loads
168    // coalesce, and each thread produces 8 consecutive tokens: grid y is
169    // `ceil(seq_len / 64)` for blocks of 8 thread rows.
170    let tp = std::env::var("METRALE_CONV1D_TP").ok().as_deref() != Some("0")
171        && conv1d_prefill_tp_k.0 != 0;
172    let (k, grid, block) = if tp {
173        (
174            conv1d_prefill_tp_k,
175            [div_ceil(d_inner, 32), div_ceil(seq_len, 64), 1],
176            [32u32, 8u32, 1u32],
177        )
178    } else {
179        (kernel, [div_ceil(d_inner, 256), 1, 1], [256u32, 1u32, 1u32])
180    };
181    KernelLaunch::new(gpu, k)
182        .grid(grid)
183        .block(block)
184        .arg_ptr(conv_state)
185        .arg_ptr(input)
186        .arg_ptr(weight.weight)
187        .arg_ptr(bias)
188        .arg_ptr(output)
189        .arg_u32(d_inner)
190        .arg_u32(d_conv)
191        .arg_u32(seq_len)
192        .arg_u32(input_stride)
193        .arg_u32(output_stride)
194        .launch(stream)?;
195    // 2026-09-30: The token-parallel kernel reads the incoming state from several blocks, so it
196    // cannot also write the new one (it did: tokens 0..2 of a prompt over 64 tokens then read the
197    // prompt's own tail whenever the last block ran first). The next launch on this stream writes
198    // it, after every block has read.
199    if tp {
200        let state_k = gpu
201            .op_cache()
202            .kernel(gpu, "causal_conv1d", "causal_conv1d_prefill_state")?;
203        KernelLaunch::new(gpu, state_k)
204            .grid([div_ceil(d_inner, 256), 1, 1])
205            .block([256, 1, 1])
206            .arg_ptr(conv_state)
207            .arg_ptr(input)
208            .arg_u32(d_inner)
209            .arg_u32(d_conv)
210            .arg_u32(seq_len)
211            .arg_u32(input_stride)
212            .launch(stream)?;
213    }
214    Ok(())
215}
216
217/// 2026-09-25: Mamba-2 SSM prefill (`mamba2_ssm_prefill`): the token-sequential
218/// recurrence over `seq_len` tokens in one launch. `x_stride`, `bc_stride`,
219/// `dt_stride` and `y_stride` are BF16 elements between consecutive tokens.
220#[allow(clippy::too_many_arguments)]
221pub fn mamba2_ssm_prefill(
222    gpu: &dyn GpuBackend,
223    kernel: KernelHandle,
224    h_state: DevicePtr,
225    x: DevicePtr,
226    b_proj: DevicePtr,
227    c_proj: DevicePtr,
228    dt_raw: DevicePtr,
229    a_log: DevicePtr,
230    d_param: DevicePtr,
231    dt_bias: DevicePtr,
232    output: DevicePtr,
233    batch_size: u32,
234    seq_len: u32,
235    num_heads: u32,
236    head_dim: u32,
237    state_size: u32,
238    n_groups: u32,
239    dt_min: f32,
240    dt_max: f32,
241    x_stride: u32,
242    bc_stride: u32,
243    dt_stride: u32,
244    y_stride: u32,
245    stream: u64,
246) -> Result<()> {
247    KernelLaunch::new(gpu, kernel)
248        .grid([num_heads, batch_size, 1])
249        .block([state_size, 1, 1])
250        .arg_ptr(h_state)
251        .arg_ptr(x)
252        .arg_ptr(b_proj)
253        .arg_ptr(c_proj)
254        .arg_ptr(dt_raw)
255        .arg_ptr(a_log)
256        .arg_ptr(d_param)
257        .arg_ptr(dt_bias)
258        .arg_ptr(output)
259        .arg_u32(batch_size)
260        .arg_u32(seq_len)
261        .arg_u32(num_heads)
262        .arg_u32(head_dim)
263        .arg_u32(state_size)
264        .arg_u32(n_groups)
265        .arg_f32(dt_min)
266        .arg_f32(dt_max)
267        .arg_u32(x_stride)
268        .arg_u32(bc_stride)
269        .arg_u32(dt_stride)
270        .arg_u32(y_stride)
271        .launch(stream)
272}
273
274/// 2026-09-25: Mamba-2 SSM prefill with each block's H slice in shared memory
275/// for the whole token loop (`mamba2_ssm_prefill_persistent`). Same arguments
276/// as [`mamba2_ssm_prefill`], but launched with `head_dim * SUB` threads and
277/// dynamic shared memory.
278#[allow(clippy::too_many_arguments)]
279pub fn mamba2_ssm_prefill_persistent(
280    gpu: &dyn GpuBackend,
281    kernel: KernelHandle,
282    h_state: DevicePtr,
283    x: DevicePtr,
284    b_proj: DevicePtr,
285    c_proj: DevicePtr,
286    dt_raw: DevicePtr,
287    a_log: DevicePtr,
288    d_param: DevicePtr,
289    dt_bias: DevicePtr,
290    output: DevicePtr,
291    batch_size: u32,
292    seq_len: u32,
293    num_heads: u32,
294    head_dim: u32,
295    state_size: u32,
296    n_groups: u32,
297    dt_min: f32,
298    dt_max: f32,
299    x_stride: u32,
300    bc_stride: u32,
301    dt_stride: u32,
302    y_stride: u32,
303    stream: u64,
304) -> Result<()> {
305    // 2026-09-25: Must match the kernel's dynamic shared-memory layout:
306    //   sH     : head_dim * (state_size + 1)  (the +1 pad avoids bank conflicts)
307    //   smem_x : head_dim
308    //   smem_B : state_size   (dt*B for the current token)
309    //   smem_C : state_size
310    let smem = head_dim * (state_size + 1) * 4 + head_dim * 4 + state_size * 4 + state_size * 4;
311    // 2026-09-25: SUB threads cooperate per head_dim row; must equal the
312    // kernel's `SUB`.
313    const SUB: u32 = 4;
314    KernelLaunch::new(gpu, kernel)
315        .grid([num_heads, batch_size, 1])
316        .block([head_dim * SUB, 1, 1])
317        .shared_mem(smem)
318        .arg_ptr(h_state)
319        .arg_ptr(x)
320        .arg_ptr(b_proj)
321        .arg_ptr(c_proj)
322        .arg_ptr(dt_raw)
323        .arg_ptr(a_log)
324        .arg_ptr(d_param)
325        .arg_ptr(dt_bias)
326        .arg_ptr(output)
327        .arg_u32(batch_size)
328        .arg_u32(seq_len)
329        .arg_u32(num_heads)
330        .arg_u32(head_dim)
331        .arg_u32(state_size)
332        .arg_u32(n_groups)
333        .arg_f32(dt_min)
334        .arg_f32(dt_max)
335        .arg_u32(x_stride)
336        .arg_u32(bc_stride)
337        .arg_u32(dt_stride)
338        .arg_u32(y_stride)
339        .launch(stream)
340}