metrale_model_layers/layers/ops/
ssm_preproc.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the GDN and gated-attention preprocessing kernels:
4//! QKVZ and Q/gate deinterleave, the fused Q norm (and MRoPE), the batched sigmoid
5//! gate, and the BA-projection gate transforms.
6//!
7//! Owner: model-layers ops.
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: Deinterleave the QKVZ projection output from GQA-grouped to
22/// sequential layout, per token.
23///
24/// Input row: [num_groups × (kd + kd + vpg*vd + vpg*vd)]
25/// Output row: [Q_total | K_total | V_total | Z_total]
26///
27/// Kernel: `deinterleave_qkvz(interleaved, output, num_groups, head_k_dim,
28///          vheads_per_group, head_v_dim)`
29/// Grid: (num_tokens, ceil(total/256), 1)  Block: (256, 1, 1)
30pub fn deinterleave_qkvz(
31    gpu: &dyn GpuBackend,
32    kernel: KernelHandle,
33    interleaved: DevicePtr,
34    output: DevicePtr,
35    num_tokens: u32,
36    num_groups: u32,
37    head_k_dim: u32,
38    vheads_per_group: u32,
39    head_v_dim: u32,
40    stream: u64,
41) -> Result<()> {
42    let group_dim = 2 * head_k_dim + 2 * vheads_per_group * head_v_dim;
43    let total = num_groups * group_dim;
44    KernelLaunch::new(gpu, kernel)
45        .grid([num_tokens, div_ceil(total, 256), 1])
46        .block([256, 1, 1])
47        .arg_ptr(interleaved)
48        .arg_ptr(output)
49        .arg_u32(num_groups)
50        .arg_u32(head_k_dim)
51        .arg_u32(vheads_per_group)
52        .arg_u32(head_v_dim)
53        .launch(stream)
54}
55
56/// 2026-09-25: Deinterleave Q/gate from per-head interleaved to contiguous
57/// layout, in place, per token. Rows are `stride` elements apart.
58///
59/// Input layout:  [Q_h0(hd), G_h0(hd), Q_h1(hd), G_h1(hd), ...]
60/// Output layout: [Q_h0(hd), Q_h1(hd), ..., G_h0(hd), G_h1(hd), ...]
61///
62/// Kernel: `deinterleave_qg(data, num_heads, head_dim, stride)`
63/// Grid: (num_tokens, 1, 1)  Block: (256, 1, 1)
64/// Dynamic shared memory: num_heads * head_dim * 2 BF16 values
65pub fn deinterleave_qg(
66    gpu: &dyn GpuBackend,
67    kernel: KernelHandle,
68    data: DevicePtr,
69    num_tokens: u32,
70    num_heads: u32,
71    head_dim: u32,
72    stride: u32,
73    stream: u64,
74) -> Result<()> {
75    let shared_bytes = num_heads * head_dim * 2 * 2;
76    KernelLaunch::new(gpu, kernel)
77        .grid([num_tokens, 1, 1])
78        .block([256, 1, 1])
79        .shared_mem(shared_bytes)
80        .arg_ptr(data)
81        .arg_u32(num_heads)
82        .arg_u32(head_dim)
83        .arg_u32(stride)
84        .launch(stream)
85}
86
87/// 2026-09-25: [`deinterleave_qg`] with Q written to `q_out` (contiguous
88/// `[num_tokens, num_heads * head_dim]`) instead of in place. The gate is
89/// written back into `data` at offset `num_heads * head_dim` of each row.
90pub fn deinterleave_qg_split(
91    gpu: &dyn GpuBackend,
92    kernel: KernelHandle,
93    data: DevicePtr,
94    q_out: DevicePtr,
95    num_tokens: u32,
96    num_heads: u32,
97    head_dim: u32,
98    stride: u32,
99    stream: u64,
100) -> Result<()> {
101    let shared_bytes = num_heads * head_dim * 2 * 2;
102    KernelLaunch::new(gpu, kernel)
103        .grid([num_tokens, 1, 1])
104        .block([256, 1, 1])
105        .shared_mem(shared_bytes)
106        .arg_ptr(data)
107        .arg_ptr(q_out)
108        .arg_u32(num_heads)
109        .arg_u32(head_dim)
110        .arg_u32(stride)
111        .launch(stream)
112}
113
114/// 2026-09-25: [`deinterleave_qg_split`] fused with the per-head Q RMS norm.
115///
116/// The gate goes to `data[q_total..]`; Q is normalized in shared memory and
117/// written once, to `q_out`. `q_norm_weight` is `[head_dim]`, shared by every
118/// head, and applied as `x * rms * (1 + w)`.
119pub fn deinterleave_qg_split_qnorm(
120    gpu: &dyn GpuBackend,
121    kernel: KernelHandle,
122    data: DevicePtr,
123    q_out: DevicePtr,
124    q_norm_weight: DevicePtr,
125    num_tokens: u32,
126    num_heads: u32,
127    head_dim: u32,
128    stride: u32,
129    eps: f32,
130    stream: u64,
131) -> Result<()> {
132    let shared_bytes = num_heads * head_dim * 2 * 2;
133    KernelLaunch::new(gpu, kernel)
134        .grid([num_tokens, 1, 1])
135        .block([256, 1, 1])
136        .shared_mem(shared_bytes)
137        .arg_ptr(data)
138        .arg_ptr(q_out)
139        .arg_ptr(q_norm_weight)
140        .arg_u32(num_heads)
141        .arg_u32(head_dim)
142        .arg_u32(stride)
143        .arg_f32(eps)
144        .launch(stream)
145}
146
147/// 2026-09-25: [`deinterleave_qg_split_qnorm`] plus rotate-half MRoPE on the
148/// first `rotary_dim` dimensions of each Q head, using the per-token positions
149/// `pos_t` / `pos_h` / `pos_w`.
150///
151/// The gate goes to `data[q_total..]`; Q is deinterleaved, normalized, rotated,
152/// then written to `q_out`. The interleaved-MRoPE prefill (`cache_skip.rs`)
153/// launches it under `METRALE_ATTN_PREFILL_FUSED_QROPE=1`.
154#[allow(clippy::too_many_arguments)]
155pub fn deinterleave_qg_split_qnorm_mrope(
156    gpu: &dyn GpuBackend,
157    kernel: KernelHandle,
158    data: DevicePtr,
159    q_out: DevicePtr,
160    q_norm_weight: DevicePtr,
161    pos_t: DevicePtr,
162    pos_h: DevicePtr,
163    pos_w: DevicePtr,
164    num_tokens: u32,
165    num_heads: u32,
166    head_dim: u32,
167    stride: u32,
168    rotary_dim: u32,
169    eps: f32,
170    theta: f32,
171    stream: u64,
172) -> Result<()> {
173    let raw_shared = num_heads * head_dim * 2 * 2;
174    let norm_shared = num_heads * head_dim * 2;
175    KernelLaunch::new(gpu, kernel)
176        .grid([num_tokens, 1, 1])
177        .block([256, 1, 1])
178        .shared_mem(raw_shared + norm_shared)
179        .arg_ptr(data)
180        .arg_ptr(q_out)
181        .arg_ptr(q_norm_weight)
182        .arg_ptr(pos_t)
183        .arg_ptr(pos_h)
184        .arg_ptr(pos_w)
185        .arg_u32(num_heads)
186        .arg_u32(head_dim)
187        .arg_u32(stride)
188        .arg_u32(rotary_dim)
189        .arg_f32(eps)
190        .arg_f32(theta)
191        .launch(stream)
192}
193
194/// 2026-09-25: [`sigmoid_gate_mul`] over `num_tokens` rows in one launch:
195/// `output[t, d] = input[t, d] * sigmoid(gate[t * gate_stride + d])`.
196/// `input` and `output` are contiguous `[num_tokens, dim]`.
197pub fn sigmoid_gate_mul_batched(
198    gpu: &dyn GpuBackend,
199    kernel: KernelHandle,
200    input: DevicePtr,
201    gate: DevicePtr,
202    output: DevicePtr,
203    dim: u32,
204    gate_stride: u32,
205    num_tokens: u32,
206    stream: u64,
207) -> Result<()> {
208    let total = num_tokens * dim;
209    KernelLaunch::new(gpu, kernel)
210        .grid([div_ceil(total, 256), 1, 1])
211        .block([256, 1, 1])
212        .arg_ptr(input)
213        .arg_ptr(gate)
214        .arg_ptr(output)
215        .arg_u32(dim)
216        .arg_u32(gate_stride)
217        .arg_u32(total)
218        .launch(stream)
219}
220
221/// 2026-09-25: GDN gates from the interleaved BA projection and the learned
222/// `A_log` / `dt_bias`: FP32 gate (decay) and beta (write gate) per value head
223/// and token.
224///
225/// Input rows are `ba_stride` BF16 elements apart. Output rows of both
226/// `gate_out` and `beta_out` are `2 * num_v_heads` floats apart.
227///
228/// Kernel: `compute_gdn_gates(ba_interleaved, A_log, dt_bias, gate_out,
229///          beta_out, num_v_heads, num_groups, vheads_per_group, ba_stride)`
230/// Grid: (num_tokens, 1, 1)  Block: (num_v_heads, 1, 1)
231pub fn compute_gdn_gates(
232    gpu: &dyn GpuBackend,
233    kernel: KernelHandle,
234    ba_interleaved: DevicePtr,
235    a_log: DevicePtr,
236    dt_bias: DevicePtr,
237    gate_out: DevicePtr,
238    beta_out: DevicePtr,
239    num_tokens: u32,
240    num_v_heads: u32,
241    num_groups: u32,
242    vheads_per_group: u32,
243    ba_stride: u32,
244    stream: u64,
245) -> Result<()> {
246    KernelLaunch::new(gpu, kernel)
247        .grid([num_tokens, 1, 1])
248        .block([num_v_heads, 1, 1])
249        .arg_ptr(ba_interleaved)
250        .arg_ptr(a_log)
251        .arg_ptr(dt_bias)
252        .arg_ptr(gate_out)
253        .arg_ptr(beta_out)
254        .arg_u32(num_v_heads)
255        .arg_u32(num_groups)
256        .arg_u32(vheads_per_group)
257        .arg_u32(ba_stride)
258        .launch(stream)
259}
260
261/// 2026-09-25: BA projection GEMV and the GDN gate/beta transforms in one
262/// kernel, for one token, with no intermediate BA buffer.
263///
264/// Kernel: `dense_gemv_ba_gates(A, B, A_log, dt_bias, gate, beta, N, K, vpg)`
265/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
266#[allow(clippy::too_many_arguments)]
267pub fn dense_gemv_ba_gates(
268    gpu: &dyn GpuBackend,
269    kernel: KernelHandle,
270    input: DevicePtr,
271    ba_weight: &DenseWeight,
272    a_log: DevicePtr,
273    dt_bias: DevicePtr,
274    gate_out: DevicePtr,
275    beta_out: DevicePtr,
276    n: u32,
277    k: u32,
278    vheads_per_group: u32,
279    stream: u64,
280) -> Result<()> {
281    KernelLaunch::new(gpu, kernel)
282        .grid([div_ceil(n, 4), 1, 1])
283        .block([256, 1, 1])
284        .arg_ptr(input)
285        .arg_ptr(ba_weight.weight)
286        .arg_ptr(a_log)
287        .arg_ptr(dt_bias)
288        .arg_ptr(gate_out)
289        .arg_ptr(beta_out)
290        .arg_u32(n)
291        .arg_u32(k)
292        .arg_u32(vheads_per_group)
293        .launch(stream)
294}
295
296/// 2026-09-25: BA GEMM and the GDN gate transforms for `m` tokens in one
297/// kernel, with no intermediate BA buffer.
298///
299/// `input` is `[m, k_stride]` BF16 activations and `ba_weight` `[n, k]` BF16,
300/// row-major. `gate_out` is one FP32 buffer, `gate_stride` floats per token:
301///   gate_out[token * gate_stride + vh]      = gate (alpha→exp transform)
302///   gate_out[token * gate_stride + nv + vh] = beta (sigmoid)
303///
304/// Kernel: `dense_gemm_ba_gates_prefill(A, B, A_log, dt_bias, gate_out, M, N, K,
305///          K_stride, gate_stride, nv, vpg)`
306/// Grid: (ceil(N/4), M_tokens, 1)  Block: (256, 1, 1)
307///
308/// `twin` is the one-CTA-per-token `dense_gemm_ba_gates_prefill_hopper` (module
309/// `ssm_ba_gates_hopper`), or `KernelHandle(0)` where the target lacks it.
310/// [`ba_gates_pick`] chooses between the two here, once, so the call sites do
311/// not branch. Both take the same arguments; the
312/// `native_ssm_ba_gates_hopper_microtest` example compares their outputs byte
313/// for byte.
314#[allow(clippy::too_many_arguments)]
315pub fn dense_gemm_ba_gates_prefill(
316    gpu: &dyn GpuBackend,
317    kernel: KernelHandle,
318    twin: KernelHandle,
319    input: DevicePtr,
320    ba_weight: &DenseWeight,
321    a_log: DevicePtr,
322    dt_bias: DevicePtr,
323    gate_out: DevicePtr,
324    m: u32,
325    n: u32,
326    k: u32,
327    k_stride: u32,
328    gate_stride: u32,
329    nv: u32,
330    vheads_per_group: u32,
331    stream: u64,
332) -> Result<()> {
333    let requested = ssm_ba_gates_hopper_enabled();
334    let pick = ba_gates_pick(
335        requested,
336        kernel,
337        twin,
338        m,
339        n,
340        k,
341        k_stride,
342        ba_gates_sm_count(gpu),
343    );
344    ba_gates_log(&pick, requested, m);
345    if pick.twin {
346        return dense_gemm_ba_gates_prefill_hopper(
347            gpu,
348            pick.kernel,
349            input,
350            ba_weight,
351            a_log,
352            dt_bias,
353            gate_out,
354            m,
355            n,
356            k,
357            k_stride,
358            gate_stride,
359            nv,
360            vheads_per_group,
361            stream,
362        );
363    }
364    KernelLaunch::new(gpu, pick.kernel)
365        .grid([div_ceil(n, 4), m, 1])
366        .block([256, 1, 1])
367        .arg_ptr(input)
368        .arg_ptr(ba_weight.weight)
369        .arg_ptr(a_log)
370        .arg_ptr(dt_bias)
371        .arg_ptr(gate_out)
372        .arg_u32(m)
373        .arg_u32(n)
374        .arg_u32(k)
375        .arg_u32(k_stride)
376        .arg_u32(gate_stride)
377        .arg_u32(nv)
378        .arg_u32(vheads_per_group)
379        .launch(stream)
380}