spark_model/layers/ops/
ssm_mamba.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Auto-extracted from `ops.rs` during refactor wave 4a.
4
5#![allow(unused_imports)]
6
7use anyhow::Result;
8use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
9use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
10
11use crate::layers::moe;
12use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
13
14use super::*;
15
16/// Causal conv1d update (decode step, supports batched sequences).
17///
18/// Kernel: `causal_conv1d_update(conv_state, new_input, weight, bias,
19///          output, batch, dim, d_conv)`
20/// Grid: (ceil(dim/256), batch, 1)  Block: (256, 1, 1)
21///
22/// For batch > 1, conv_state and input must be contiguous [batch, ...].
23pub fn conv1d_update(
24    gpu: &dyn GpuBackend,
25    kernel: KernelHandle,
26    conv_state: DevicePtr,
27    input: DevicePtr,
28    weight: &DenseWeight,
29    output: DevicePtr,
30    d_inner: u32,
31    d_conv: u32,
32    batch_size: u32,
33    stream: u64,
34) -> Result<()> {
35    let bias_ptr = DevicePtr::NULL;
36    KernelLaunch::new(gpu, kernel)
37        .grid([div_ceil(d_inner, 256), batch_size, 1])
38        .block([256, 1, 1])
39        .arg_ptr(conv_state)
40        .arg_ptr(input)
41        .arg_ptr(weight.weight)
42        .arg_ptr(bias_ptr)
43        .arg_ptr(output)
44        .arg_u32(batch_size)
45        .arg_u32(d_inner)
46        .arg_u32(d_conv)
47        .launch(stream)
48}
49
50/// Fused conv1d update + SiLU + L2 normalization for Q/K channels.
51///
52/// Combines `causal_conv1d_update` and `l2_norm_bf16` into a single kernel.
53/// Q+K channels (0..qk_channels) get L2-normalized per head after SiLU.
54/// V channels (qk_channels..d_inner) get SiLU only.
55///
56/// Saves 1 kernel launch per SSM layer (36 launches/step for 35B/80B).
57#[allow(clippy::too_many_arguments)]
58pub fn conv1d_update_l2norm(
59    gpu: &dyn GpuBackend,
60    kernel: KernelHandle,
61    conv_state: DevicePtr,
62    input: DevicePtr,
63    weight: &DenseWeight,
64    output: DevicePtr,
65    d_inner: u32,
66    d_conv: u32,
67    batch_size: u32,
68    qk_channels: u32,
69    head_dim: u32,
70    l2_eps: f32,
71    stream: u64,
72) -> Result<()> {
73    let bias_ptr = DevicePtr::NULL;
74    KernelLaunch::new(gpu, kernel)
75        .grid([div_ceil(d_inner, 256), batch_size, 1])
76        .block([256, 1, 1])
77        .arg_ptr(conv_state)
78        .arg_ptr(input)
79        .arg_ptr(weight.weight)
80        .arg_ptr(bias_ptr)
81        .arg_ptr(output)
82        .arg_u32(batch_size)
83        .arg_u32(d_inner)
84        .arg_u32(d_conv)
85        .arg_u32(qk_channels)
86        .arg_u32(head_dim)
87        .arg_f32(l2_eps)
88        .launch(stream)
89}
90
91/// `conv1d_update_l2norm` with INDEPENDENT input/output row strides, so N
92/// concurrent decode sequences go in ONE launch instead of N.
93///
94/// Identical math to `conv1d_update_l2norm`; the only difference is that the
95/// input and output row strides are passed explicitly instead of both being
96/// assumed equal to `d_inner`. The concurrent-decode path feeds this straight
97/// from the QKVZ projection, whose rows are `qkvz_size` apart, while the conv
98/// output is `d_inner`-strided — so the non-strided kernel would read sequence
99/// b>=1 from the previous sequence's Z-gate region (correct at n=1, silently
100/// corrupt at n>=2). See `causal_conv1d_update_l2norm_f32_strided`.
101///
102/// `conv_state` keeps the `(b * d_inner + ch) * d_conv` layout, so the caller
103/// must have verified the per-sequence pool slots are contiguous.
104#[allow(clippy::too_many_arguments)]
105pub fn conv1d_update_l2norm_strided(
106    gpu: &dyn GpuBackend,
107    kernel: KernelHandle,
108    conv_state: DevicePtr,
109    input: DevicePtr,
110    weight: &DenseWeight,
111    output: DevicePtr,
112    d_inner: u32,
113    d_conv: u32,
114    batch_size: u32,
115    qk_channels: u32,
116    head_dim: u32,
117    l2_eps: f32,
118    input_stride: u32,
119    output_stride: u32,
120    stream: u64,
121) -> Result<()> {
122    let bias_ptr = DevicePtr::NULL;
123    KernelLaunch::new(gpu, kernel)
124        .grid([div_ceil(d_inner, 256), batch_size, 1])
125        .block([256, 1, 1])
126        .arg_ptr(conv_state)
127        .arg_ptr(input)
128        .arg_ptr(weight.weight)
129        .arg_ptr(bias_ptr)
130        .arg_ptr(output)
131        .arg_u32(batch_size)
132        .arg_u32(d_inner)
133        .arg_u32(d_conv)
134        .arg_u32(qk_channels)
135        .arg_u32(head_dim)
136        .arg_f32(l2_eps)
137        .arg_u32(input_stride)
138        .arg_u32(output_stride)
139        .launch(stream)
140}
141
142/// STAGE 1 fused K=2 MTP-verify conv1d+L2norm: both draft positions in one
143/// launch, with the position-0 conv-state snapshot written inline (replaces
144/// the per-token `conv1d_update_l2norm` ×2 + intervening `copy_d2d`).
145///
146/// Bit-identical to the per-token path (proven by gdn_verify_fused_microtest,
147/// cos == 1.0). `conv_state` is left holding the committed (post position-1)
148/// window; `conv_state_inter` holds the position-0 rollback snapshot.
149///
150/// Kernel: `gdn_verify_fused_conv_k2(conv_state, new_input, weight, output,
151///          conv_state_inter, dim, d_conv, qk_channels, head_dim,
152///          input_stride, output_stride, l2_eps)`
153/// Grid: (ceil(dim/256), 1, 1)  Block: (256, 1, 1)
154#[allow(clippy::too_many_arguments)]
155pub fn gdn_verify_fused_conv_k2(
156    gpu: &dyn GpuBackend,
157    kernel: KernelHandle,
158    conv_state: DevicePtr,
159    new_input: DevicePtr,
160    weight: &DenseWeight,
161    output: DevicePtr,
162    conv_state_inter: DevicePtr,
163    d_inner: u32,
164    d_conv: u32,
165    qk_channels: u32,
166    head_dim: u32,
167    input_stride: u32,
168    output_stride: u32,
169    l2_eps: f32,
170    stream: u64,
171) -> Result<()> {
172    KernelLaunch::new(gpu, kernel)
173        .grid([div_ceil(d_inner, 256), 1, 1])
174        .block([256, 1, 1])
175        .arg_ptr(conv_state)
176        .arg_ptr(new_input)
177        .arg_ptr(weight.weight)
178        .arg_ptr(output)
179        .arg_ptr(conv_state_inter)
180        .arg_u32(d_inner)
181        .arg_u32(d_conv)
182        .arg_u32(qk_channels)
183        .arg_u32(head_dim)
184        .arg_u32(input_stride)
185        .arg_u32(output_stride)
186        .arg_f32(l2_eps)
187        .launch(stream)
188}
189
190/// Fused generic-K DFlash-verify conv1d+L2norm: ALL K draft positions in one
191/// launch, with every per-token conv-state rollback snapshot written inline
192/// to a strided intermediates array (replaces the per-token
193/// `conv1d_update_l2norm` ×K + `copy_d2d` ×K sequence — 34 serialized ops at
194/// K=17). `conv_state` is left holding the committed (post final-position)
195/// window, which the kernel also duplicates as snapshot K-1, so the caller
196/// issues NO copies.
197///
198/// Same numerics as the per-token path (identical accumulation order under
199/// --fmad=false; the K=2 twin is proven bit-identical by
200/// gdn_verify_fused_microtest).
201///
202/// Kernel: `gdn_verify_fused_conv_kn(conv_state, new_input, weight, output,
203///          conv_state_inter, num_tokens, dim, d_conv, qk_channels, head_dim,
204///          input_stride, output_stride, inter_stride, l2_eps)`
205/// Grid: (ceil(dim/256), 1, 1)  Block: (256, 1, 1)
206/// BATCHED verify conv: `n_seq` sequences x `num_tokens` positions in ONE launch.
207///
208/// Step 1 of batched speculative decoding. `spec_step.rs:94` currently calls
209/// `decode_verify` one sequence at a time, so MTP at C=n runs n full model
210/// forwards and re-reads ~9.6 GB of weights n times — measured as a 3.4% LOSS at
211/// C=2 and a HALVING of C=4. One launch over gridDim.y = n_seq is the first
212/// piece of making n*(K+1) rows share a single weight read.
213///
214/// Bit-identical to n separate `gdn_verify_fused_conv_kn` calls: each sequence's
215/// conv window is independent, so only the base addresses differ.
216///
217/// Grid: (ceil(d_inner/256), n_seq, 1)  Block: (256, 1, 1)
218#[allow(clippy::too_many_arguments)]
219pub fn gdn_verify_fused_conv_kn_batched(
220    gpu: &dyn GpuBackend,
221    kernel: KernelHandle,
222    conv_state: DevicePtr,
223    new_input: DevicePtr,
224    weight: &DenseWeight,
225    output: DevicePtr,
226    conv_state_inter: DevicePtr,
227    num_tokens: u32,
228    d_inner: u32,
229    d_conv: u32,
230    qk_channels: u32,
231    head_dim: u32,
232    input_stride: u32,
233    output_stride: u32,
234    inter_stride: u32,
235    l2_eps: f32,
236    n_seq: u32,
237    conv_state_seq_stride: u32,
238    input_seq_stride: u32,
239    output_seq_stride: u32,
240    inter_seq_stride: u32,
241    stream: u64,
242) -> Result<()> {
243    KernelLaunch::new(gpu, kernel)
244        .grid([div_ceil(d_inner, 256), n_seq, 1])
245        .block([256, 1, 1])
246        .arg_ptr(conv_state)
247        .arg_ptr(new_input)
248        .arg_ptr(weight.weight)
249        .arg_ptr(output)
250        .arg_ptr(conv_state_inter)
251        .arg_u32(num_tokens)
252        .arg_u32(d_inner)
253        .arg_u32(d_conv)
254        .arg_u32(qk_channels)
255        .arg_u32(head_dim)
256        .arg_u32(input_stride)
257        .arg_u32(output_stride)
258        .arg_u32(inter_stride)
259        .arg_f32(l2_eps)
260        .arg_u32(conv_state_seq_stride)
261        .arg_u32(input_seq_stride)
262        .arg_u32(output_seq_stride)
263        .arg_u32(inter_seq_stride)
264        .launch(stream)
265}
266
267#[allow(clippy::too_many_arguments)]
268pub fn gdn_verify_fused_conv_kn(
269    gpu: &dyn GpuBackend,
270    kernel: KernelHandle,
271    conv_state: DevicePtr,
272    new_input: DevicePtr,
273    weight: &DenseWeight,
274    output: DevicePtr,
275    conv_state_inter: DevicePtr,
276    num_tokens: u32,
277    d_inner: u32,
278    d_conv: u32,
279    qk_channels: u32,
280    head_dim: u32,
281    input_stride: u32,
282    output_stride: u32,
283    inter_stride: u32,
284    l2_eps: f32,
285    stream: u64,
286) -> Result<()> {
287    KernelLaunch::new(gpu, kernel)
288        .grid([div_ceil(d_inner, 256), 1, 1])
289        .block([256, 1, 1])
290        .arg_ptr(conv_state)
291        .arg_ptr(new_input)
292        .arg_ptr(weight.weight)
293        .arg_ptr(output)
294        .arg_ptr(conv_state_inter)
295        .arg_u32(num_tokens)
296        .arg_u32(d_inner)
297        .arg_u32(d_conv)
298        .arg_u32(qk_channels)
299        .arg_u32(head_dim)
300        .arg_u32(input_stride)
301        .arg_u32(output_stride)
302        .arg_u32(inter_stride)
303        .arg_f32(l2_eps)
304        .launch(stream)
305}
306
307/// STAGE 1 fused K=2 MTP-verify gated-RMS-norm: both draft positions in one
308/// launch (replaces the per-token `gated_rms_norm` ×2). The Z gate is read
309/// from the deinterleaved [Q|K|V|Z] buffer at `z_offset` per position.
310///
311/// Bit-identical to the per-token path (proven by gdn_verify_fused_microtest,
312/// cos == 1.0).
313///
314/// Kernel: `gdn_verify_fused_norm_k2(gdn_out, deint, weight, output,
315///          hidden_size, eps, deint_stride, z_offset, out_stride)`
316/// Grid: (num_v_heads, 2, 1)  Block: (hidden_size, 1, 1)
317#[allow(clippy::too_many_arguments)]
318pub fn gdn_verify_fused_norm_k2(
319    gpu: &dyn GpuBackend,
320    kernel: KernelHandle,
321    gdn_out: DevicePtr,
322    deint: DevicePtr,
323    weight: &DenseWeight,
324    output: DevicePtr,
325    num_v_heads: u32,
326    hidden_size: u32,
327    eps: f32,
328    deint_stride: u32,
329    z_offset: u32,
330    out_stride: u32,
331    stream: u64,
332) -> Result<()> {
333    KernelLaunch::new(gpu, kernel)
334        .grid([num_v_heads, 2, 1])
335        .block([hidden_size, 1, 1])
336        .arg_ptr(gdn_out)
337        .arg_ptr(deint)
338        .arg_ptr(weight.weight)
339        .arg_ptr(output)
340        .arg_u32(hidden_size)
341        .arg_f32(eps)
342        .arg_u32(deint_stride)
343        .arg_u32(z_offset)
344        .arg_u32(out_stride)
345        .launch(stream)
346}
347
348/// Multi-token conv1d sliding window update + SiLU for prefill.
349///
350/// Processes `seq_len` tokens sequentially per channel in registers.
351/// Input/output may be non-contiguous (different strides between tokens).
352///
353/// Kernel: `causal_conv1d_update_prefill(conv_state, input, weight, bias,
354///          output, dim, d_conv, seq_len, input_stride, output_stride)`
355/// Grid: (ceil(dim/256), 1, 1)  Block: (256, 1, 1)
356#[allow(clippy::too_many_arguments)]
357pub fn conv1d_update_prefill(
358    gpu: &dyn GpuBackend,
359    kernel: KernelHandle,
360    conv1d_prefill_tp_k: KernelHandle,
361    conv_state: DevicePtr,
362    input: DevicePtr,
363    weight: &DenseWeight,
364    bias: DevicePtr,
365    output: DevicePtr,
366    d_inner: u32,
367    d_conv: u32,
368    seq_len: u32,
369    input_stride: u32,
370    output_stride: u32,
371    stream: u64,
372) -> Result<()> {
373    // TOKEN-PARALLEL prefill conv1d is the default (`ATLAS_CONV1D_TP=0` disables).
374    //
375    // The serial kernel runs one thread per channel walking `for t in 0..seq_len`,
376    // so it launches only ceil(dim/256) CTAs — tens of blocks on a 48-SM part —
377    // with each thread doing a seq_len-long loop. It measured 30.6 ms of the 35B
378    // cold-prefill budget.
379    //
380    // There is no recurrence to serialize: `s[0..3]` is a sliding window over
381    // INPUTS, so output[t] = b + sum_k w[k]*x[t-3+k] depends on no prior output.
382    // Parallelising over (channel, token) is 3.32x on the isolated kernel and
383    // BIT-IDENTICAL — 0 of 22,118,400 elements differ, because the accumulation
384    // order is unchanged. Block (32,8) keeps a warp spanning channels so the
385    // [t*stride + ch] loads stay coalesced; 8 tokens per thread give a rolling
386    // window (11 input reads per 8 outputs instead of 32).
387    let tp =
388        std::env::var("ATLAS_CONV1D_TP").ok().as_deref() != Some("0") && conv1d_prefill_tp_k.0 != 0;
389    let (k, grid, block) = if tp {
390        (
391            conv1d_prefill_tp_k,
392            [div_ceil(d_inner, 32), div_ceil(seq_len, 64), 1],
393            [32u32, 8u32, 1u32],
394        )
395    } else {
396        (kernel, [div_ceil(d_inner, 256), 1, 1], [256u32, 1u32, 1u32])
397    };
398    KernelLaunch::new(gpu, k)
399        .grid(grid)
400        .block(block)
401        .arg_ptr(conv_state)
402        .arg_ptr(input)
403        .arg_ptr(weight.weight)
404        .arg_ptr(bias)
405        .arg_ptr(output)
406        .arg_u32(d_inner)
407        .arg_u32(d_conv)
408        .arg_u32(seq_len)
409        .arg_u32(input_stride)
410        .arg_u32(output_stride)
411        .launch(stream)
412}
413
414/// Mamba-2 SSM prefill: sequential recurrence across `seq_len` tokens in a single kernel.
415///
416/// Same algorithm as decode but loops over tokens, avoiding per-token launch overhead.
417/// Supports non-contiguous layouts via per-tensor strides (BF16 elements between tokens).
418///
419/// Grid: (num_heads, batch_size, 1)  Block: (state_size, 1, 1)
420#[allow(clippy::too_many_arguments)]
421pub fn mamba2_ssm_prefill(
422    gpu: &dyn GpuBackend,
423    kernel: KernelHandle,
424    h_state: DevicePtr,
425    x: DevicePtr,
426    b_proj: DevicePtr,
427    c_proj: DevicePtr,
428    dt_raw: DevicePtr,
429    a_log: DevicePtr,
430    d_param: DevicePtr,
431    dt_bias: DevicePtr,
432    output: DevicePtr,
433    batch_size: u32,
434    seq_len: u32,
435    num_heads: u32,
436    head_dim: u32,
437    state_size: u32,
438    n_groups: u32,
439    dt_min: f32,
440    dt_max: f32,
441    x_stride: u32,
442    bc_stride: u32,
443    dt_stride: u32,
444    y_stride: u32,
445    stream: u64,
446) -> Result<()> {
447    KernelLaunch::new(gpu, kernel)
448        .grid([num_heads, batch_size, 1])
449        .block([state_size, 1, 1])
450        .arg_ptr(h_state)
451        .arg_ptr(x)
452        .arg_ptr(b_proj)
453        .arg_ptr(c_proj)
454        .arg_ptr(dt_raw)
455        .arg_ptr(a_log)
456        .arg_ptr(d_param)
457        .arg_ptr(dt_bias)
458        .arg_ptr(output)
459        .arg_u32(batch_size)
460        .arg_u32(seq_len)
461        .arg_u32(num_heads)
462        .arg_u32(head_dim)
463        .arg_u32(state_size)
464        .arg_u32(n_groups)
465        .arg_f32(dt_min)
466        .arg_f32(dt_max)
467        .arg_u32(x_stride)
468        .arg_u32(bc_stride)
469        .arg_u32(dt_stride)
470        .arg_u32(y_stride)
471        .launch(stream)
472}
473
474/// Persistent Mamba-2 SSM prefill: H in shared memory, reduces global traffic.
475/// Same parameters and launch config as mamba2_ssm_prefill.
476#[allow(clippy::too_many_arguments)]
477pub fn mamba2_ssm_prefill_persistent(
478    gpu: &dyn GpuBackend,
479    kernel: KernelHandle,
480    h_state: DevicePtr,
481    x: DevicePtr,
482    b_proj: DevicePtr,
483    c_proj: DevicePtr,
484    dt_raw: DevicePtr,
485    a_log: DevicePtr,
486    d_param: DevicePtr,
487    dt_bias: DevicePtr,
488    output: DevicePtr,
489    batch_size: u32,
490    seq_len: u32,
491    num_heads: u32,
492    head_dim: u32,
493    state_size: u32,
494    n_groups: u32,
495    dt_min: f32,
496    dt_max: f32,
497    x_stride: u32,
498    bc_stride: u32,
499    dt_stride: u32,
500    y_stride: u32,
501    stream: u64,
502) -> Result<()> {
503    // Dynamic shared memory, must match the kernel's layout:
504    //   sH     : head_dim * (state_size + 1)  (+1 pad avoids smem bank conflicts)
505    //   smem_x : head_dim
506    //   smem_B : state_size   (dt*B for the current token)
507    //   smem_C : state_size
508    // Must match the kernel layout: sH + sX + sB + sC.
509    let smem = head_dim * (state_size + 1) * 4 + head_dim * 4 + state_size * 4 + state_size * 4;
510    // SUB=4 threads cooperate per head_dim row (must match the kernel's `SUB`).
511    const SUB: u32 = 4;
512    KernelLaunch::new(gpu, kernel)
513        .grid([num_heads, batch_size, 1])
514        .block([head_dim * SUB, 1, 1])
515        .shared_mem(smem)
516        .arg_ptr(h_state)
517        .arg_ptr(x)
518        .arg_ptr(b_proj)
519        .arg_ptr(c_proj)
520        .arg_ptr(dt_raw)
521        .arg_ptr(a_log)
522        .arg_ptr(d_param)
523        .arg_ptr(dt_bias)
524        .arg_ptr(output)
525        .arg_u32(batch_size)
526        .arg_u32(seq_len)
527        .arg_u32(num_heads)
528        .arg_u32(head_dim)
529        .arg_u32(state_size)
530        .arg_u32(n_groups)
531        .arg_f32(dt_min)
532        .arg_f32(dt_max)
533        .arg_u32(x_stride)
534        .arg_u32(bc_stride)
535        .arg_u32(dt_stride)
536        .arg_u32(y_stride)
537        .launch(stream)
538}
539
540// ── Mamba-2 SSD chunked prefill scan ──────────────────────────────────────────
541//
542// Replaces the token-sequential recurrence with the chunked (state-space duality)
543// formulation: the scan becomes tensor-core matmuls with only ceil(T/64) sequential
544// links instead of T. See kernels/gb10/common/mamba2_ssd_chunk.cu.