metrale_model_layers/layers/ops/
gemm_quant_moe.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for MoE routing and grouped GEMMs: the fused NVFP4
4//! gate top-k, the tile work-list builder, and the FP8, W8A8 and BF16 grouped
5//! GEMMs.
6//!
7//! Owner: model-layers ops.
8//! Invariants: none beyond the types.
9
10use super::*;
11
12/// 2026-09-25: Fused gate GEMV and top-k for one token (kernel
13/// `moe_gate_topk_fused`): `gate[num_experts] = A[K] @ B_gate[num_experts,
14/// K]^T` with an NVFP4 gate weight, then the top-k expert indices and weights
15/// `exp(logit - max)` over those k, divided by their sum when `normalize` is
16/// nonzero. One 256-thread CTA; `K` BF16 values of dynamic shared memory hold
17/// the input.
18#[allow(clippy::too_many_arguments)]
19pub fn moe_gate_topk_fused(
20    gpu: &dyn GpuBackend,
21    kernel: KernelHandle,
22    input: DevicePtr,
23    gate_weight: &QuantizedWeight,
24    expert_indices: DevicePtr,
25    expert_weights: DevicePtr,
26    num_experts: u32,
27    k: u32,
28    top_k: u32,
29    normalize: u32,
30    stream: u64,
31) -> Result<()> {
32    let smem_bytes = k as usize * 2;
33    KernelLaunch::new(gpu, kernel)
34        .grid([1, 1, 1])
35        .block([256, 1, 1])
36        .shared_mem(smem_bytes as u32)
37        .arg_ptr(input)
38        .arg_ptr(gate_weight.weight)
39        .arg_ptr(gate_weight.weight_scale)
40        .arg_f32(gate_weight.weight_scale_2)
41        .arg_ptr(expert_indices)
42        .arg_ptr(expert_weights)
43        .arg_u32(num_experts)
44        .arg_u32(k)
45        .arg_u32(top_k)
46        .arg_u32(normalize)
47        .launch(stream)
48}
49
50/// 2026-09-25: Build the compacted work-list for the grouped-GEMM grid: one
51/// item per (expert, m-tile, n-tile) of every expert that has rows and a
52/// non-NULL weight pointer, experts in ascending order. One CTA; thread 0
53/// writes `worklist[2w] = expert`, `worklist[2w + 1] = (m-tile index << 6) |
54/// n-tile index`, and `total_tiles[0] = w`.
55///
56/// `n_tiles` is `ceil(N / 64)` and `m_tile` is 128, as `PM4_N_TILE` and
57/// `PM4_M_TILE` in `moe_fp8_grouped_gemm.cu`. The n-tile index has 6 bits, so
58/// `n_tiles` must be at most 64; the kernel asserts it. `expert_offsets` is
59/// `[num_experts + 1]` and `weight_ptrs` `[num_experts]`; an expert whose
60/// pointer is NULL gets no items.
61///
62/// No event orders this before the grouped GEMM that reads the list, so
63/// launch that GEMM on the same `stream`.
64#[allow(clippy::too_many_arguments)]
65pub fn moe_build_tile_worklist(
66    gpu: &dyn GpuBackend,
67    kernel: KernelHandle,
68    expert_offsets: DevicePtr,
69    weight_ptrs: DevicePtr,
70    worklist: DevicePtr,
71    total_tiles: DevicePtr,
72    num_experts: u32,
73    n_tiles: u32,
74    m_tile: u32,
75    stream: u64,
76) -> Result<()> {
77    KernelLaunch::new(gpu, kernel)
78        .grid([1, 1, 1])
79        .block([256, 1, 1])
80        .arg_ptr(expert_offsets)
81        .arg_ptr(weight_ptrs)
82        .arg_ptr(worklist)
83        .arg_ptr(total_tiles)
84        .arg_u32(num_experts)
85        .arg_u32(n_tiles)
86        .arg_u32(m_tile)
87        .launch(stream)
88}
89
90/// 2026-09-25: FP8 grouped GEMM for sorted MoE prefill over the work-list from
91/// [`moe_build_tile_worklist`], which must run first on the same `stream`.
92///
93/// The kernel strides over the work-list by `gridDim.x`, so the grid is
94/// `max_tiles` (the caller's upper bound on the tile count) clamped to
95/// `1..=16384`. Extra CTAs exit at once; a grid smaller than the list is
96/// slower but still covers every tile.
97///
98/// `input` `[total_tokens, K]` BF16; `weight_ptrs[e]` points at `[N, K]` FP8
99/// and `scale_ptrs[e]` at `[N/128, K/128]` FP32; `output`
100/// `[total_expanded, N]` BF16; `expert_offsets` `[num_experts + 1]`;
101/// `sorted_token_ids` `[total_expanded]`, or NULL to read input row `i` for
102/// output row `i`.
103#[allow(clippy::too_many_arguments)]
104pub fn moe_fp8_grouped_gemm(
105    gpu: &dyn GpuBackend,
106    kernel: KernelHandle,
107    input: DevicePtr,
108    weight_ptrs: DevicePtr,
109    scale_ptrs: DevicePtr,
110    output: DevicePtr,
111    expert_offsets: DevicePtr,
112    sorted_token_ids: DevicePtr,
113    num_experts: u32,
114    n: u32,
115    k: u32,
116    worklist: DevicePtr,
117    total_tiles: DevicePtr,
118    max_tiles: u32,
119    stream: u64,
120) -> Result<()> {
121    const MAX_GRID_CTAS: u32 = 16384;
122    let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
123    // 2026-09-25: The block must equal `PM4_THREADS` of the kernel this target
124    // compiles: 512 (16 warps) in `kernels/strix-hip/common/moe_fp8_grouped_gemm.cu`
125    // for the `metrale_hip` build, and 256 in
126    // `kernels/gb10/common/moe_fp8_grouped_gemm.cu` everywhere else.
127    #[cfg(metrale_hip)]
128    let block = [512u32, 1, 1];
129    #[cfg(not(metrale_hip))]
130    let block = [256u32, 1, 1];
131    KernelLaunch::new(gpu, kernel)
132        .grid([grid_ctas, 1, 1])
133        .block(block)
134        .arg_ptr(input)
135        .arg_ptr(weight_ptrs)
136        .arg_ptr(scale_ptrs)
137        .arg_ptr(output)
138        .arg_ptr(expert_offsets)
139        .arg_ptr(sorted_token_ids)
140        .arg_u32(num_experts)
141        .arg_u32(n)
142        .arg_u32(k)
143        .arg_ptr(worklist)
144        .arg_ptr(total_tiles)
145        .launch(stream)
146}
147
148/// 2026-09-25: W8A8 grouped MoE GEMM on a dense grid
149/// `(ceil(N/64), max_m_tiles, num_experts)`. `a_fp8` comes from
150/// [`per_token_group_quant_fp8`]; `a_scale` (`[total_tokens, K/128]` FP32) and
151/// the per-block `b_scale` (`scale_ptrs[e]` → `[N/128, K/128]` FP32) are
152/// applied in an FP32 epilogue per 128-element K-group. `weight_ptrs[e]`
153/// points at `[N, K]` FP8; `output` is `[total_expanded, N]` BF16;
154/// `sorted_token_ids` is `[total_expanded]` or NULL.
155#[allow(clippy::too_many_arguments)]
156pub fn moe_w8a8_grouped_gemm(
157    gpu: &dyn GpuBackend,
158    kernel: KernelHandle,
159    a_fp8: DevicePtr,
160    a_scale: DevicePtr,
161    weight_ptrs: DevicePtr,
162    scale_ptrs: DevicePtr,
163    output: DevicePtr,
164    expert_offsets: DevicePtr,
165    sorted_token_ids: DevicePtr,
166    num_experts: u32,
167    n: u32,
168    k: u32,
169    max_m_tiles: u32,
170    stream: u64,
171) -> Result<()> {
172    KernelLaunch::new(gpu, kernel)
173        .grid([div_ceil(n, 64), max_m_tiles, num_experts])
174        .block([128, 1, 1])
175        .arg_ptr(a_fp8)
176        .arg_ptr(a_scale)
177        .arg_ptr(weight_ptrs)
178        .arg_ptr(scale_ptrs)
179        .arg_ptr(output)
180        .arg_ptr(expert_offsets)
181        .arg_ptr(sorted_token_ids)
182        .arg_u32(num_experts)
183        .arg_u32(n)
184        .arg_u32(k)
185        .launch(stream)
186}
187
188/// 2026-09-25: [`moe_w8a8_grouped_gemm`] over the work-list from
189/// [`moe_build_tile_worklist`] (kernel `moe_w8a8_grouped_gemm_pm4`, same
190/// module), which must run first on the same `stream`. The grid follows the
191/// rule of [`moe_fp8_grouped_gemm`]: `max_tiles` clamped to `1..=16384`.
192#[allow(clippy::too_many_arguments)]
193pub fn moe_w8a8_grouped_gemm_pm4(
194    gpu: &dyn GpuBackend,
195    kernel: KernelHandle,
196    a_fp8: DevicePtr,
197    a_scale: DevicePtr,
198    weight_ptrs: DevicePtr,
199    scale_ptrs: DevicePtr,
200    output: DevicePtr,
201    expert_offsets: DevicePtr,
202    sorted_token_ids: DevicePtr,
203    num_experts: u32,
204    n: u32,
205    k: u32,
206    worklist: DevicePtr,
207    total_tiles: DevicePtr,
208    max_tiles: u32,
209    stream: u64,
210) -> Result<()> {
211    const MAX_GRID_CTAS: u32 = 16384;
212    let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
213    // 2026-09-25: 256 threads, `W8PM4_THREADS` in
214    // `kernels/gb10/common/moe_w8a8_grouped_gemm.cu`.
215    KernelLaunch::new(gpu, kernel)
216        .grid([grid_ctas, 1, 1])
217        .block([256, 1, 1])
218        .arg_ptr(a_fp8)
219        .arg_ptr(a_scale)
220        .arg_ptr(weight_ptrs)
221        .arg_ptr(scale_ptrs)
222        .arg_ptr(output)
223        .arg_ptr(expert_offsets)
224        .arg_ptr(sorted_token_ids)
225        .arg_u32(num_experts)
226        .arg_u32(n)
227        .arg_u32(k)
228        .arg_ptr(worklist)
229        .arg_ptr(total_tiles)
230        .launch(stream)
231}
232
233/// 2026-09-27: One entry point of `kernels/gb10/common/moe_w8a8_grouped_gemm_e4m3.cu`: its work-list
234/// item shape (`m_tile` rows by `n_tile` columns, so the builder takes `n_tiles = N / n_tile`), its
235/// dynamic shared memory and how many of its 256-thread CTAs fit on one SM.
236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct MoeE4m3Tile {
238    pub m_tile: u32,
239    pub n_tile: u32,
240    pub smem_bytes: u32,
241    pub ctas_per_sm: u32,
242}
243
244impl MoeE4m3Tile {
245    /// 2026-09-27: `STAGES * (BM + BN) * 64 + STAGES * BM * 4 + BM * 4` for a 3-stage ring.
246    const fn new(m_tile: u32, n_tile: u32) -> Self {
247        Self {
248            m_tile,
249            n_tile,
250            smem_bytes: 3 * (m_tile + n_tile) * 64 + 3 * m_tile * 4 + m_tile * 4,
251            ctas_per_sm: 2,
252        }
253    }
254
255    /// 2026-09-27: Whether an `[N, K]` projection fits: whole items in N, one b_scale row per
256    /// item (n_tile divides 128), whole 128-K scale blocks.
257    pub fn fits(&self, n: u32, k: u32) -> bool {
258        n > 0 && k > 0 && n.is_multiple_of(self.n_tile) && k.is_multiple_of(128)
259    }
260}
261
262/// 2026-09-27: `moe_w8a8_grouped_gemm_e4m3_gu` (gate/up: N = inter, K = hidden).
263pub const MOE_E4M3_GU: MoeE4m3Tile = MoeE4m3Tile::new(64, 128);
264/// 2026-09-27: `moe_w8a8_grouped_gemm_e4m3_dn` (down: N = hidden, K = inter).
265pub const MOE_E4M3_DN: MoeE4m3Tile = MoeE4m3Tile::new(128, 64);
266
267/// 2026-09-27: [`moe_w8a8_grouped_gemm_pm4`] on the native e4m3 tensor-core MMA (the `_gu` or
268/// `_dn` entry, described by `tile`): same operands and output, bit-identical, over a work-list
269/// that [`moe_build_tile_worklist`] built on the same `stream` with `tile.m_tile` and
270/// `n / tile.n_tile`. `grid_ctas` (clamped to `1..=16384`) CTAs stride over the items.
271#[allow(clippy::too_many_arguments)]
272pub fn moe_w8a8_grouped_gemm_e4m3(
273    gpu: &dyn GpuBackend,
274    kernel: KernelHandle,
275    tile: MoeE4m3Tile,
276    a_fp8: DevicePtr,
277    a_scale: DevicePtr,
278    weight_ptrs: DevicePtr,
279    scale_ptrs: DevicePtr,
280    output: DevicePtr,
281    expert_offsets: DevicePtr,
282    sorted_token_ids: DevicePtr,
283    n: u32,
284    k: u32,
285    worklist: DevicePtr,
286    total_tiles: DevicePtr,
287    grid_ctas: u32,
288    stream: u64,
289) -> Result<()> {
290    anyhow::ensure!(
291        tile.fits(n, k),
292        "moe_w8a8_grouped_gemm_e4m3: N={n} K={k} does not fit {tile:?}"
293    );
294    KernelLaunch::new(gpu, kernel)
295        .grid([grid_ctas.clamp(1, 16384), 1, 1])
296        .block([256, 1, 1])
297        .shared_mem(tile.smem_bytes)
298        .arg_ptr(a_fp8)
299        .arg_ptr(a_scale)
300        .arg_ptr(weight_ptrs)
301        .arg_ptr(scale_ptrs)
302        .arg_ptr(output)
303        .arg_ptr(expert_offsets)
304        .arg_ptr(sorted_token_ids)
305        .arg_u32(n)
306        .arg_u32(k)
307        .arg_ptr(worklist)
308        .arg_ptr(total_tiles)
309        .launch(stream)
310}
311
312/// 2026-09-28: `moe_w8a8_gateup_silu_e4m3_w1` / `_w2`: the gate and up grouped GEMMs of one
313/// work-list item (the `_gu` shape, `MOE_E4M3_GU`) followed by SiLU(gate) * up quantized to
314/// E4M3 per 128-column group, bit-identical to the two `_gu` launches plus
315/// `silu_mul_quant_fp8`. Writes `out_fp8` `[total_expanded, n]` and `out_scale`
316/// `[total_expanded, n / 128]`. The work-list is built with `MOE_E4M3_GU`'s shape.
317#[allow(clippy::too_many_arguments)]
318pub fn moe_w8a8_gateup_silu_e4m3(
319    gpu: &dyn GpuBackend,
320    kernel: KernelHandle,
321    a_fp8: DevicePtr,
322    a_scale: DevicePtr,
323    gate: (DevicePtr, DevicePtr),
324    up: (DevicePtr, DevicePtr),
325    out_fp8: DevicePtr,
326    out_scale: DevicePtr,
327    expert_offsets: DevicePtr,
328    sorted_token_ids: DevicePtr,
329    n: u32,
330    k: u32,
331    worklist: DevicePtr,
332    total_tiles: DevicePtr,
333    grid_ctas: u32,
334    stream: u64,
335) -> Result<()> {
336    anyhow::ensure!(
337        MOE_E4M3_GU.fits(n, k),
338        "moe_w8a8_gateup_silu_e4m3: N={n} K={k} does not fit {MOE_E4M3_GU:?}"
339    );
340    KernelLaunch::new(gpu, kernel)
341        .grid([grid_ctas.clamp(1, 16384), 1, 1])
342        .block([256, 1, 1])
343        .shared_mem(MOE_E4M3_GU.smem_bytes)
344        .arg_ptr(a_fp8)
345        .arg_ptr(a_scale)
346        .arg_ptr(gate.0)
347        .arg_ptr(gate.1)
348        .arg_ptr(up.0)
349        .arg_ptr(up.1)
350        .arg_ptr(out_fp8)
351        .arg_ptr(out_scale)
352        .arg_ptr(expert_offsets)
353        .arg_ptr(sorted_token_ids)
354        .arg_u32(n)
355        .arg_u32(k)
356        .arg_ptr(worklist)
357        .arg_ptr(total_tiles)
358        .launch(stream)
359}
360
361/// 2026-09-25: BF16 grouped GEMM for sorted MoE prefill, without scales:
362/// `input` `[total_tokens, K]` BF16, `weight_ptrs[e]` → `[N, K]` BF16,
363/// `output` `[total_expanded, N]` BF16, `sorted_token_ids` `[total_expanded]`
364/// or NULL. The MoE layer's long-prefill path uses it when its experts are
365/// BF16, which the loader sets up when `METRALE_FP8_DEQUANT_MOE_TO_BF16=1`
366/// dequantizes FP8 experts at load (`qwen35/load_layers.rs`).
367#[allow(clippy::too_many_arguments)]
368pub fn moe_bf16_grouped_gemm(
369    gpu: &dyn GpuBackend,
370    kernel: KernelHandle,
371    input: DevicePtr,
372    weight_ptrs: DevicePtr,
373    output: DevicePtr,
374    expert_offsets: DevicePtr,
375    sorted_token_ids: DevicePtr,
376    num_experts: u32,
377    n: u32,
378    k: u32,
379    max_m_tiles: u32,
380    stream: u64,
381) -> Result<()> {
382    KernelLaunch::new(gpu, kernel)
383        .grid([div_ceil(n, 64), max_m_tiles, num_experts])
384        .block([128, 1, 1])
385        .arg_ptr(input)
386        .arg_ptr(weight_ptrs)
387        .arg_ptr(output)
388        .arg_ptr(expert_offsets)
389        .arg_ptr(sorted_token_ids)
390        .arg_u32(num_experts)
391        .arg_u32(n)
392        .arg_u32(k)
393        .launch(stream)
394}