spark_model/layers/ops/gemm_quant.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, ensure};
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/// FP8×FP8 GEMM: A [M, K] FP8 × B [N, K] FP8 → C [M, N] BF16.
17///
18/// Both A (activations) and B (weights) are pre-converted FP8 E4M3.
19/// No BF16→FP8 conversion in inner loop — pure MMA throughput.
20/// Grid: (ceil(N/128), ceil(M/64)) Block: (128, 1, 1)
21pub fn fp8_fp8_gemm_n128(
22 gpu: &dyn GpuBackend,
23 kernel: KernelHandle,
24 a_fp8: DevicePtr,
25 b_fp8: DevicePtr,
26 output: DevicePtr,
27 m: u32,
28 n: u32,
29 k: u32,
30 stream: u64,
31) -> Result<()> {
32 KernelLaunch::new(gpu, kernel)
33 .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
34 .block([128, 1, 1])
35 .arg_ptr(a_fp8)
36 .arg_ptr(b_fp8)
37 .arg_ptr(output)
38 .arg_u32(m)
39 .arg_u32(n)
40 .arg_u32(k)
41 .launch(stream)
42}
43
44/// M128 variant of fp8_gemm_n128: halves B re-reads for large M (ISL > 128).
45///
46/// Each CTA covers 128 rows of A, loading B once for both 64-row halves.
47/// ~2× speedup on out_proj (K=value_dim, N=h) at ISL≥128.
48///
49/// Grid: (ceil(N/128), ceil(M/128), 1) Block: (128, 1, 1)
50#[allow(clippy::too_many_arguments)]
51pub fn fp8_gemm_n128_m128(
52 gpu: &dyn GpuBackend,
53 kernel: KernelHandle,
54 input: DevicePtr,
55 b_fp8: DevicePtr,
56 output: DevicePtr,
57 m: u32,
58 n: u32,
59 k: u32,
60 stream: u64,
61) -> Result<()> {
62 KernelLaunch::new(gpu, kernel)
63 .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
64 .block([128, 1, 1])
65 .arg_ptr(input)
66 .arg_ptr(b_fp8)
67 .arg_ptr(output)
68 .arg_u32(m)
69 .arg_u32(n)
70 .arg_u32(k)
71 .launch(stream)
72}
73
74/// M128 variant of fp8_fp8_gemm_n128: halves B re-reads for large M (ISL > 128).
75///
76/// Each CTA covers 128 rows of A, loading B once for both 64-row halves.
77/// ~2× speedup on Q/K/V projections (FP8 activations × FP8 weights) at ISL≥128.
78/// Compact FP8 A smem → 6 blocks/SM vs 3 for fp8_gemm_t_m128.
79///
80/// Grid: (ceil(N/128), ceil(M/128), 1) Block: (128, 1, 1)
81#[allow(clippy::too_many_arguments)]
82pub fn fp8_fp8_gemm_n128_m128(
83 gpu: &dyn GpuBackend,
84 kernel: KernelHandle,
85 a_fp8: DevicePtr,
86 b_fp8: DevicePtr,
87 output: DevicePtr,
88 m: u32,
89 n: u32,
90 k: u32,
91 stream: u64,
92) -> Result<()> {
93 KernelLaunch::new(gpu, kernel)
94 .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
95 .block([128, 1, 1])
96 .arg_ptr(a_fp8)
97 .arg_ptr(b_fp8)
98 .arg_ptr(output)
99 .arg_u32(m)
100 .arg_u32(n)
101 .arg_u32(k)
102 .launch(stream)
103}
104
105/// Dense BF16 GEMV (M=1): C = A @ B^T for single-row activations.
106///
107/// A: [1, K] BF16, B: [N, K] BF16, C: [1, N] BF16.
108/// 8 outputs/block, 32 threads (1 warp) per output. Single-warp shuffle reduction.
109///
110/// Kernel: `dense_gemv_bf16(A, B, C, N, K)`
111/// Grid: (ceil(N/4), 1, 1) Block: (256, 1, 1)
112pub fn dense_gemv(
113 gpu: &dyn GpuBackend,
114 kernel: KernelHandle,
115 input: DevicePtr,
116 weight: &DenseWeight,
117 output: DevicePtr,
118 n: u32,
119 k: u32,
120 stream: u64,
121) -> Result<()> {
122 KernelLaunch::new(gpu, kernel)
123 .grid([div_ceil(n, 4), 1, 1])
124 .block([256, 1, 1])
125 .arg_ptr(input)
126 .arg_ptr(weight.weight)
127 .arg_ptr(output)
128 .arg_u32(n)
129 .arg_u32(k)
130 .launch(stream)
131}
132
133/// Dense BF16 GEMV, batched over 2 rows (M=2): one pass over the weight
134/// produces both output rows, halving weight bandwidth vs two `dense_gemv`
135/// launches. Bit-identical to two M=1 `dense_gemv` calls — each row's
136/// accumulator follows the same K-iteration/reduction order.
137///
138/// `input`: `[2, K]` BF16 (contiguous); `output`: two rows at
139/// `output + t * out_stride` (BF16 elements). Used by the K=2 MTP verify
140/// path for the GDN `in_proj_qkvz` (dequant-to-BF16 on FP8 checkpoints),
141/// which otherwise re-read the full projection weight once per verify token.
142///
143/// Kernel: `dense_gemv_bf16_batch2(A, B, C, N, K, out_stride)`
144#[allow(clippy::too_many_arguments)]
145pub fn dense_gemv_batch2(
146 gpu: &dyn GpuBackend,
147 kernel: KernelHandle,
148 input: DevicePtr,
149 weight: &DenseWeight,
150 output: DevicePtr,
151 n: u32,
152 k: u32,
153 out_stride: u32,
154 stream: u64,
155) -> Result<()> {
156 KernelLaunch::new(gpu, kernel)
157 .grid([div_ceil(n, 4), 1, 1])
158 .block([256, 1, 1])
159 .arg_ptr(input)
160 .arg_ptr(weight.weight)
161 .arg_ptr(output)
162 .arg_u32(n)
163 .arg_u32(k)
164 .arg_u32(out_stride)
165 .launch(stream)
166}
167
168/// Dense BF16 batched GEMV (M rows): `C[t] = A[t] @ B^T` for `t` in `[0, M)`.
169///
170/// The M-row generalisation of [`dense_gemv_batch2`]. Reads the BF16 weight
171/// matrix ONCE for all M rows instead of M times, which is the whole point:
172/// at decode the BF16 projections (q/k/v/o + shared expert) are pure weight
173/// streaming, so M separate M=1 GEMVs make the step scale linearly with the
174/// number of concurrent sequences.
175///
176/// Bit-identical to M separate `dense_gemv` calls (same K-iteration order and
177/// reduction tree per row; the kernel dir builds with --fmad=false).
178///
179/// `input`: `[M, K]` BF16 contiguous. `output`: M rows at
180/// `output + t * out_stride` (BF16 elements). Caller must pass `m <= 8`
181/// (MAX_M in the kernel); larger batches should use a tiled GEMM.
182///
183/// Kernel: `dense_gemv_bf16_batchm(A, B, C, M, N, K, out_stride)`
184/// Grid: (ceil(N/4), 1, 1) Block: (256, 1, 1)
185#[allow(clippy::too_many_arguments)]
186/// Mirror of `MAX_M` in `kernels/gb10/common/dense_gemv_bf16_batchm.cu`.
187/// The kernel clamps silently above this, so the Rust side must refuse.
188///
189/// 🔴 16 since 2026-09-02. The old 8 was the kernel's compiled row array, never an
190/// arithmetic boundary: each row is an independent FP32 accumulator over the same `kv`
191/// order, `m` appears in no row's operand sequence, and the fold is per-row. So every
192/// width up to `MAX_M` is bit-identical both to the narrower tier and to M serial
193/// `dense_gemv_bf16` calls. Verified on the 12 real GLM-5.3 prefill shapes with cold
194/// weights, including the regression direction that matters — m <= 8 byte-unchanged,
195/// because decode, the MTP verify arm and the BF16 lm_head arm all run m <= 8 on this
196/// same kernel (`scripts/glm53-dense-bf16/bench_m16.cu`, spark-bench).
197///
198/// 🪤 This constant is load-bearing OUTSIDE the GEMV: it gates the lm_head batched arm
199/// (`model/impl_a3.rs`), the MTP row dispatch (`layers/mtp_head/row_dispatch.rs`) and it
200/// sizes `verify_k` for the KDA/DSA/MLP workspaces (`weight_loader/glm5_next_load.rs`).
201/// Raising it widens those arms and grows per-layer scratch — a memory-budget change, not
202/// only a kernel one.
203pub const DENSE_GEMV_BATCHM_MAX_M: u32 = 16;
204
205/// The band the batched GEMV is allowed to CLAIM on the decode paths: the MTP row dispatch
206/// and the BF16 lm_head arm.
207///
208/// 🔴 Deliberately still 8, and NOT the same thing as the kernel's `MAX_M`. Those two sites
209/// pick between `dense_gemv_bf16_batchm` and a **reassociating** kernel (the pipelined /
210/// tile GEMM), so the band's upper edge decides which bits a decode of that width produces.
211/// Widening the GEMV tier to 16 for prefill would silently move widths 9..=16 off the tile
212/// GEMM they have always used — a numerics change on the MTP / DFlash γ>8 window, on a path
213/// the prefill measurement says nothing about. Moving this edge needs its own A/B and its
214/// own byte gate against the sealed decode reference; until then the decode band is frozen
215/// where it was measured (+6 % at C=2, +24 % at C=4; NEGATIVE above 8 against the tile GEMM,
216/// -14.4 % at C=16 — commits 84d5b763c / 78d276832).
217pub const DENSE_GEMV_BATCHM_DECODE_MAX_M: u32 = 8;
218
219pub fn dense_gemv_batchm(
220 gpu: &dyn GpuBackend,
221 kernel: KernelHandle,
222 input: DevicePtr,
223 weight: &DenseWeight,
224 output: DevicePtr,
225 m: u32,
226 n: u32,
227 k: u32,
228 out_stride: u32,
229 stream: u64,
230) -> Result<()> {
231 // The kernel caps rows at a compile-time MAX_M 8 and CLAMPS rather than
232 // erroring, so an over-large m used to mean "rows 8..m are silently never
233 // written". Refuse instead: a caller that wants more rows must use a
234 // kernel that can do them (dense_gemm_tc), not get 8 rows of truth and
235 // stale memory for the rest.
236 ensure!(
237 (1..=DENSE_GEMV_BATCHM_MAX_M).contains(&m),
238 "dense_gemv_batchm: m={m} outside 1..={DENSE_GEMV_BATCHM_MAX_M} \
239 (kernel MAX_M clamps silently; use dense_gemm_tc for wider batches)"
240 );
241 KernelLaunch::new(gpu, kernel)
242 .grid([div_ceil(n, 4), 1, 1])
243 .block([256, 1, 1])
244 .arg_ptr(input)
245 .arg_ptr(weight.weight)
246 .arg_ptr(output)
247 .arg_u32(m)
248 .arg_u32(n)
249 .arg_u32(k)
250 .arg_u32(out_stride)
251 .launch(stream)
252}
253
254/// Dense FP8-weight GEMV (M=1): C = A @ (dequant(B_fp8) * row_scale).
255///
256/// A: `[1, K]` BF16, B: `[N, K]` FP8 E4M3, row_scale: `[N]` f32, C: `[1, N]` BF16.
257/// Halves weight bandwidth vs dense_gemv (1 byte/weight instead of 2).
258/// 4 outputs/block, 64 threads (2 warps) per output.
259///
260/// Kernel: `dense_gemv_fp8w(A, B, row_scale, C, N, K)`
261/// Grid: (ceil(N/4), 1, 1) Block: (256, 1, 1)
262pub fn dense_gemv_fp8w(
263 gpu: &dyn GpuBackend,
264 kernel: KernelHandle,
265 input: DevicePtr,
266 weight: &Fp8DenseWeight,
267 output: DevicePtr,
268 n: u32,
269 k: u32,
270 stream: u64,
271) -> Result<()> {
272 KernelLaunch::new(gpu, kernel)
273 .grid([div_ceil(n, 4), 1, 1])
274 .block([256, 1, 1])
275 .arg_ptr(input)
276 .arg_ptr(weight.weight)
277 .arg_ptr(weight.row_scale)
278 .arg_ptr(output)
279 .arg_u32(n)
280 .arg_u32(k)
281 .launch(stream)
282}
283
284/// W8A16 GEMV (M=1): C = A @ dequant_lut(B_fp8) * row_scale for FP8 E4M3 weights.
285///
286/// A: `[1, K]` BF16, B: `[N, K]` FP8 E4M3 bytes, row_scale: `[N]` f32, C: `[1, N]` BF16.
287/// Uses a 256-entry E4M3 LUT in shared memory for branchless dequant (no hardware
288/// FP4/FP8 conversion PTX needed — works on SM121 without `cvt.rn.satfinite`).
289/// 4 outputs/block, 64 threads (2 warps) per output. Cross-warp smem reduction.
290///
291/// Kernel: `w8a16_gemv(A, B, row_scale, C, N, K)`
292/// Grid: (ceil(N/4), 1, 1) Block: (256, 1, 1)
293#[allow(clippy::too_many_arguments)]
294pub fn w8a16_gemv(
295 gpu: &dyn GpuBackend,
296 kernel: KernelHandle,
297 input: DevicePtr,
298 weight: DevicePtr,
299 row_scale: DevicePtr,
300 output: DevicePtr,
301 n: u32,
302 k: u32,
303 stream: u64,
304) -> Result<()> {
305 KernelLaunch::new(gpu, kernel)
306 .grid([div_ceil(n, 4), 1, 1])
307 .block([256, 1, 1])
308 .arg_ptr(input)
309 .arg_ptr(weight)
310 .arg_ptr(row_scale)
311 .arg_ptr(output)
312 .arg_u32(n)
313 .arg_u32(k)
314 .launch(stream)
315}
316
317/// W8A16 GEMM (M>1): `C[M,N] = A[M,K] @ dequant(B[N,K])` for prefill.
318///
319/// Uses 256-entry E4M3 LUT + BF16 2D block scales.
320/// Grid: (ceil(N/64), ceil(M/64), 1) Block: (128, 1, 1)
321#[allow(clippy::too_many_arguments)]
322pub fn w8a16_gemm(
323 gpu: &dyn GpuBackend,
324 kernel: KernelHandle,
325 input: DevicePtr,
326 weight: DevicePtr,
327 block_scale: DevicePtr,
328 output: DevicePtr,
329 m: u32,
330 n: u32,
331 k: u32,
332 stream: u64,
333) -> Result<()> {
334 // Launch geometry is target-specific because the `w8a16_gemm` kernel SOURCE
335 // differs per target. The native-HIP (gfx1151) kernel is a 256×128 M×N
336 // tile / 512-thread (16-warp) block (kernels/strix-hip/common/w8a16_gemm.cu)
337 // — it raises warp occupancy and per-CTA M-reuse for prefill GEMM. Every
338 // other target keeps the original 64×64 / 128-thread kernel
339 // (kernels/gb10/common/w8a16_gemm.cu). Keep these two in lockstep with their
340 // `.cu` `M_TILE`/`N_TILE`/`THREADS`.
341 #[cfg(atlas_hip)]
342 let (grid, block) = ([div_ceil(n, 128), div_ceil(m, 256), 1], [512, 1, 1]);
343 #[cfg(not(atlas_hip))]
344 let (grid, block) = ([div_ceil(n, 64), div_ceil(m, 64), 1], [128, 1, 1]);
345 KernelLaunch::new(gpu, kernel)
346 .grid(grid)
347 .block(block)
348 .arg_ptr(input)
349 .arg_ptr(weight)
350 .arg_ptr(block_scale)
351 .arg_ptr(output)
352 .arg_u32(m)
353 .arg_u32(n)
354 .arg_u32(k)
355 .launch(stream)
356}
357
358/// W8A16 GEMM pipelined (M>1): bit-identical (cosine=1.0) faster rewrite of
359/// `w8a16_gemm` — same args, same numerics, ~4.6× faster on GB10/sm_121.
360///
361/// Fix-A occupancy + cp.async pipelined kernel: 128×32 tile (M×N), 256-thread
362/// block (8 warps). Geometry mirrors the validated `w8a16_microtest`
363/// `"w8a16_gemm_pipelined"` arm (PM_M_TILE=128, PM_N_TILE=32).
364///
365/// Grid: (ceil(N/32), ceil(M/128), 1) Block: (256, 1, 1)
366#[allow(clippy::too_many_arguments)]
367pub fn w8a16_gemm_pipelined(
368 gpu: &dyn GpuBackend,
369 kernel: KernelHandle,
370 input: DevicePtr,
371 weight: DevicePtr,
372 block_scale: DevicePtr,
373 output: DevicePtr,
374 m: u32,
375 n: u32,
376 k: u32,
377 stream: u64,
378) -> Result<()> {
379 KernelLaunch::new(gpu, kernel)
380 .grid([div_ceil(n, 32), div_ceil(m, 128), 1])
381 .block([256, 1, 1])
382 .arg_ptr(input)
383 .arg_ptr(weight)
384 .arg_ptr(block_scale)
385 .arg_ptr(output)
386 .arg_u32(m)
387 .arg_u32(n)
388 .arg_u32(k)
389 .launch(stream)
390}
391
392/// Per-token-per-128-K-group FP8 activation quantization. Output: A_fp8
393/// [M, K] FP8 E4M3 + a_scale [M, K/128] FP32. Matches vLLM's
394/// `per_token_group_quant_fp8`.
395///
396/// Launch geometry is target-specific because the KERNEL is, exactly as it is
397/// for `w8a16_gemm` above: [`Fp8ActQuant`] carries both handles and hands back
398/// the entry point and the grid TOGETHER, so a Hopper handle can never be
399/// launched on the shared kernel's grid. Block is 128 threads in both arms.
400///
401/// shared (`per_token_group_quant_fp8`) Grid: (M, K/128, 1)
402/// hopper (`per_token_group_quant_fp8_hopper`) Grid: (M, ceil(K/128 / 8), 1)
403///
404/// WHICH of the two runs is `Fp8ActQuant::pick`, and it is width-dependent:
405/// the twin is 3.30-3.59x at prefill M and 0.76x-0.95x at M <= 25 for
406/// K in {5120, 6144} (round-16 receipt SS 2.1), so it takes the launch only
407/// when its own grid clears `2 x sm_count` CTAs. Rule and thresholds:
408/// `layers/ops/fp8_act_quant_floor.rs`. The route line is said ONCE PER
409/// BRANCH from here — this is the single launch site, so a serve log carries
410/// the positive at the first prefill width and the negative at the first
411/// decode width.
412///
413/// M on grid X (max 2^31-1) in both: grid Y stops at 65535 and MoE
414/// `total_expanded` exceeds it. Keep the Hopper arm in lockstep with
415/// `kernels/hopper/common/fp8_act_quant_hopper.cu` — it re-derives its own
416/// group span from `gridDim.y`, so any Y in `1..=K/128` is CORRECT and this
417/// one is merely the fast one. Both kernels emit bit-identical FP8 bytes and
418/// scales (#928; `native_fp8_act_quant_hopper_microtest`).
419#[allow(clippy::too_many_arguments)]
420pub fn per_token_group_quant_fp8(
421 gpu: &dyn GpuBackend,
422 quant: Fp8ActQuant,
423 input_bf16: DevicePtr,
424 output_fp8: DevicePtr,
425 a_scale: DevicePtr,
426 m: u32,
427 k: u32,
428 stream: u64,
429) -> Result<()> {
430 let pick = quant.pick(m, k);
431 super::fp8_quant_log(&pick, m, k);
432 KernelLaunch::new(gpu, pick.kernel)
433 .grid(pick.grid)
434 .block([128, 1, 1])
435 .arg_ptr(input_bf16)
436 .arg_ptr(output_fp8)
437 .arg_ptr(a_scale)
438 .arg_u32(m)
439 .arg_u32(k)
440 .launch(stream)
441}
442
443/// W8A8 + FP32 epilogue GEMM with per-token activation scales and
444/// per-block weight scales — vLLM-equivalent FP8 numerics.
445///
446/// C[M, N] = bf16( Σ_g (FP8 MMA over K-group g) × a_scale[M, g] × b_scale[N/128, g] )
447///
448/// Inputs:
449/// - `a_fp8` [M, K] FP8 E4M3
450/// - `a_scale` [M, K/128] FP32 (from per_token_group_quant_fp8)
451/// - `b_fp8` [N, K] FP8 E4M3
452/// - `b_scale` [N/128, K/128] BF16 (existing checkpoint layout)
453/// - `output` [M, N] BF16
454///
455/// Grid: (ceil(N/128), ceil(M/64), 1) Block: (128, 1, 1)
456#[allow(clippy::too_many_arguments)]
457pub fn fp8_gemm_t_blockscaled(
458 gpu: &dyn GpuBackend,
459 kernel: KernelHandle,
460 a_fp8: DevicePtr,
461 a_scale: DevicePtr,
462 b_fp8: DevicePtr,
463 b_scale: DevicePtr,
464 output: DevicePtr,
465 m: u32,
466 n: u32,
467 k: u32,
468 stream: u64,
469) -> Result<()> {
470 super::log_gemm_shape(gpu, "fp8_gemm_t_blockscaled", m, n, k);
471 KernelLaunch::new(gpu, kernel)
472 .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
473 .block([128, 1, 1])
474 .arg_ptr(a_fp8)
475 .arg_ptr(a_scale)
476 .arg_ptr(b_fp8)
477 .arg_ptr(b_scale)
478 .arg_ptr(output)
479 .arg_u32(m)
480 .arg_u32(n)
481 .arg_u32(k)
482 .launch(stream)
483}
484
485/// Fused gate GEMV + topK softmax for M=1 decode.
486///
487/// Single kernel that computes `gate[num_experts] = A[K] @ B_gate[num_experts, K]`
488/// then extracts top-K indices + softmax weights. Saves 1 launch vs separate
489/// gate GEMV + topK kernels.
490///
491/// Grid: (1, 1, 1) Block: (256, 1, 1) — single CTA, uses shared memory reduction
492#[allow(clippy::too_many_arguments)]
493pub fn moe_gate_topk_fused(
494 gpu: &dyn GpuBackend,
495 kernel: KernelHandle,
496 input: DevicePtr,
497 gate_weight: &QuantizedWeight,
498 expert_indices: DevicePtr,
499 expert_weights: DevicePtr,
500 num_experts: u32,
501 k: u32,
502 top_k: u32,
503 normalize: u32,
504 stream: u64,
505) -> Result<()> {
506 // Dynamic shared memory: K BF16 values for input broadcast
507 let smem_bytes = k as usize * 2;
508 KernelLaunch::new(gpu, kernel)
509 .grid([1, 1, 1])
510 .block([256, 1, 1])
511 .shared_mem(smem_bytes as u32)
512 .arg_ptr(input)
513 .arg_ptr(gate_weight.weight)
514 .arg_ptr(gate_weight.weight_scale)
515 .arg_f32(gate_weight.weight_scale_2)
516 .arg_ptr(expert_indices)
517 .arg_ptr(expert_weights)
518 .arg_u32(num_experts)
519 .arg_u32(k)
520 .arg_u32(top_k)
521 .arg_u32(normalize)
522 .launch(stream)
523}
524
525/// Build the compacted (expert, m_tile, n_tile) work-list for the
526/// persistent grouped-GEMM grid. Single-block, thread-0 serial — mirrors the
527/// `moe_sort_by_expert` launch style (grid `[1,1,1]`, block `[256,1,1]`).
528///
529/// `n_tiles = div_ceil(N, 64)` (PM4_N_TILE) and `m_tile = 128` (PM4_M_TILE).
530/// Writes `worklist[*total_tiles * 2]` (word0=expert, word1=(m_tile<<6)|n_tile)
531/// and `total_tiles[0]`.
532///
533/// SAME-STREAM INVARIANT: the caller MUST launch `moe_fp8_grouped_gemm` on
534/// the SAME `stream` so the kernel's read of `total_tiles`/`worklist`
535/// happens-after this write (no cross-stream event is inserted).
536#[allow(clippy::too_many_arguments)]
537pub fn moe_build_tile_worklist(
538 gpu: &dyn GpuBackend,
539 kernel: KernelHandle,
540 expert_offsets: DevicePtr, // [num_experts + 1]
541 weight_ptrs: DevicePtr, // [num_experts] → [N, K] FP8 (0 = remote)
542 worklist: DevicePtr, // [worst_case_tiles * 2] u32 (out)
543 total_tiles: DevicePtr, // [1] i32 (out)
544 num_experts: u32,
545 n_tiles: u32, // div_ceil(N, 64) — PM4_N_TILE
546 m_tile: u32, // PM4_M_TILE = 128
547 stream: u64,
548) -> Result<()> {
549 KernelLaunch::new(gpu, kernel)
550 .grid([1, 1, 1])
551 .block([256, 1, 1])
552 .arg_ptr(expert_offsets)
553 .arg_ptr(weight_ptrs)
554 .arg_ptr(worklist)
555 .arg_ptr(total_tiles)
556 .arg_u32(num_experts)
557 .arg_u32(n_tiles)
558 .arg_u32(m_tile)
559 .launch(stream)
560}
561
562/// FP8 grouped GEMM for sorted MoE prefill — grid-compaction over the COMPACTED
563/// work-list built by `moe_build_tile_worklist`. THE routed-expert FP8 prefill
564/// kernel.
565///
566/// The kernel grid-strides by `gridDim.x`, so the launch is sized to
567/// `max_tiles` — the caller's exact upper bound on the work-item (tile) count
568/// (`wl_cap_items`). This covers the whole work-list in ~one pass instead of
569/// serializing dozens of tiles per CTA behind sync barriers (the old fixed
570/// 96-CTA persistent grid left the GPU >90% idle: ~0.2% occupancy / ~16%
571/// MemUnitBusy, measured on gfx1151). Oversubscription is safe (extra CTAs
572/// exit the loop immediately); undersizing is merely slower, never wrong.
573///
574/// `max_tiles` is clamped to `MAX_GRID_CTAS` so a pathological worklist bound
575/// cannot request an unbounded grid.
576///
577/// SAME-STREAM INVARIANT: MUST be launched on the SAME `stream` as the
578/// preceding `moe_build_tile_worklist` (read-after-write of `total_tiles`).
579///
580/// Grid: (max_tiles.clamp(1, MAX_GRID_CTAS), 1, 1) Block: (256, 1, 1)
581#[allow(clippy::too_many_arguments)]
582pub fn moe_fp8_grouped_gemm(
583 gpu: &dyn GpuBackend,
584 kernel: KernelHandle,
585 input: DevicePtr, // [total_tokens, K] BF16
586 weight_ptrs: DevicePtr, // [num_experts] → [N, K] FP8
587 scale_ptrs: DevicePtr, // [num_experts] → [N/128, K/128] FP32
588 output: DevicePtr, // [total_expanded, N] BF16
589 expert_offsets: DevicePtr, // [num_experts + 1]
590 sorted_token_ids: DevicePtr, // [total_expanded] or NULL
591 num_experts: u32,
592 n: u32,
593 k: u32,
594 worklist: DevicePtr, // [*total_tiles * 2] u32 (built on the same stream)
595 total_tiles: DevicePtr, // [1] i32 (built on the same stream)
596 max_tiles: u32, // caller's upper bound on tile count (wl_cap_items)
597 stream: u64,
598) -> Result<()> {
599 // The kernel strides by gridDim.x, so the grid is sized to the work-list's
600 // tile-count upper bound. Clamp to MAX_GRID_CTAS to bound the launch.
601 const MAX_GRID_CTAS: u32 = 16384;
602 let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
603 // Block size is target-specific because the kernel SOURCE differs. The
604 // native-HIP (gfx1151) kernel is a 16-warp / 512-thread block with a 2-D
605 // (8 warp-rows x 2 warp-cols) warp grid: it keeps the 128x64 tile geometry
606 // (so the work-list packing is unchanged) but splits the 4 WMMA n-sub-tiles
607 // across 2 warp-columns, doubling warp occupancy for latency hiding on the
608 // long-K gate/up GEMM (kernels/strix-hip/common/moe_fp8_grouped_gemm.cu).
609 // Every other target keeps the 8-warp / 256-thread M-only kernel
610 // (kernels/gb10/common/moe_fp8_grouped_gemm.cu). Keep this in lockstep with
611 // that .cu PM4_THREADS.
612 #[cfg(atlas_hip)]
613 let block = [512u32, 1, 1];
614 #[cfg(not(atlas_hip))]
615 let block = [256u32, 1, 1];
616 KernelLaunch::new(gpu, kernel)
617 .grid([grid_ctas, 1, 1])
618 .block(block)
619 .arg_ptr(input)
620 .arg_ptr(weight_ptrs)
621 .arg_ptr(scale_ptrs)
622 .arg_ptr(output)
623 .arg_ptr(expert_offsets)
624 .arg_ptr(sorted_token_ids)
625 .arg_u32(num_experts)
626 .arg_u32(n)
627 .arg_u32(k)
628 .arg_ptr(worklist)
629 .arg_ptr(total_tiles)
630 .launch(stream)
631}
632
633/// W8A8 + FP32 epilogue grouped MoE GEMM (vLLM-equivalent).
634///
635/// A_fp8 must be pre-quantized via `per_token_group_quant_fp8`. Both
636/// `a_scale` (per-token, FP32) and `b_scale` (per-block, BF16) are applied
637/// in the FP32 epilogue per K=128 block.
638#[allow(clippy::too_many_arguments)]
639pub fn moe_w8a8_grouped_gemm(
640 gpu: &dyn GpuBackend,
641 kernel: KernelHandle,
642 a_fp8: DevicePtr, // [total_tokens, K] FP8 E4M3
643 a_scale: DevicePtr, // [total_tokens, K/128] FP32
644 weight_ptrs: DevicePtr, // [num_experts] → [N, K] FP8
645 scale_ptrs: DevicePtr, // [num_experts] → [N/128, K/128] BF16
646 output: DevicePtr, // [total_expanded, N] BF16
647 expert_offsets: DevicePtr, // [num_experts + 1]
648 sorted_token_ids: DevicePtr, // [total_expanded] or NULL
649 num_experts: u32,
650 n: u32,
651 k: u32,
652 max_m_tiles: u32,
653 stream: u64,
654) -> Result<()> {
655 KernelLaunch::new(gpu, kernel)
656 .grid([div_ceil(n, 64), max_m_tiles, num_experts])
657 .block([128, 1, 1])
658 .arg_ptr(a_fp8)
659 .arg_ptr(a_scale)
660 .arg_ptr(weight_ptrs)
661 .arg_ptr(scale_ptrs)
662 .arg_ptr(output)
663 .arg_ptr(expert_offsets)
664 .arg_ptr(sorted_token_ids)
665 .arg_u32(num_experts)
666 .arg_u32(n)
667 .arg_u32(k)
668 .launch(stream)
669}
670
671/// W8A8 + FP32 epilogue grouped MoE GEMM — PM4 geometry over the COMPACTED
672/// work-list built by `moe_build_tile_worklist` (kernel
673/// `moe_w8a8_grouped_gemm_pm4`, same module/numerics as
674/// `moe_w8a8_grouped_gemm`: bit-identical output, measured).
675///
676/// Same grid-compaction contract as `moe_fp8_grouped_gemm`: the kernel
677/// grid-strides by `gridDim.x` over the work-list, so the launch is sized to
678/// `max_tiles` (`wl_cap_items`), clamped to `MAX_GRID_CTAS`. Oversubscription
679/// is safe; undersizing is merely slower, never wrong.
680///
681/// SAME-STREAM INVARIANT: MUST be launched on the SAME `stream` as the
682/// preceding `moe_build_tile_worklist` (read-after-write of `total_tiles`).
683///
684/// Grid: (max_tiles.clamp(1, MAX_GRID_CTAS), 1, 1) Block: (256, 1, 1)
685#[allow(clippy::too_many_arguments)]
686pub fn moe_w8a8_grouped_gemm_pm4(
687 gpu: &dyn GpuBackend,
688 kernel: KernelHandle,
689 a_fp8: DevicePtr, // [total_tokens, K] FP8 E4M3
690 a_scale: DevicePtr, // [total_tokens, K/128] FP32
691 weight_ptrs: DevicePtr, // [num_experts] → [N, K] FP8
692 scale_ptrs: DevicePtr, // [num_experts] → [N/128, K/128] FP32
693 output: DevicePtr, // [total_expanded, N] BF16
694 expert_offsets: DevicePtr, // [num_experts + 1]
695 sorted_token_ids: DevicePtr, // [total_expanded] or NULL
696 num_experts: u32,
697 n: u32,
698 k: u32,
699 worklist: DevicePtr, // [*total_tiles * 2] u32 (built on the same stream)
700 total_tiles: DevicePtr, // [1] i32 (built on the same stream)
701 max_tiles: u32, // caller's upper bound on tile count (wl_cap_items)
702 stream: u64,
703) -> Result<()> {
704 const MAX_GRID_CTAS: u32 = 16384;
705 let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
706 // gb10-only kernel (256 threads, __launch_bounds__(256,2)); other targets
707 // fall back to the dense-grid `moe_w8a8_grouped_gemm` (handle gating at
708 // the dispatch site).
709 KernelLaunch::new(gpu, kernel)
710 .grid([grid_ctas, 1, 1])
711 .block([256, 1, 1])
712 .arg_ptr(a_fp8)
713 .arg_ptr(a_scale)
714 .arg_ptr(weight_ptrs)
715 .arg_ptr(scale_ptrs)
716 .arg_ptr(output)
717 .arg_ptr(expert_offsets)
718 .arg_ptr(sorted_token_ids)
719 .arg_u32(num_experts)
720 .arg_u32(n)
721 .arg_u32(k)
722 .arg_ptr(worklist)
723 .arg_ptr(total_tiles)
724 .launch(stream)
725}
726
727/// BF16 grouped GEMM for sorted MoE prefill (FP8-dequant-on-load path).
728///
729/// BF16 activations × BF16 expert weights via pointer table. No scale.
730/// Used when expert weights have been dequanted from FP8 to BF16 at load
731/// time (ATLAS_FP8_DEQUANT_MOE_TO_BF16=1). Eliminates the per-layer 0.989
732/// cosine ceiling that comes from FP8 quantization itself.
733///
734/// Grid: (ceil(N/64), max_m_tiles, num_experts) Block: (128, 1, 1)
735#[allow(clippy::too_many_arguments)]
736pub fn moe_bf16_grouped_gemm(
737 gpu: &dyn GpuBackend,
738 kernel: KernelHandle,
739 input: DevicePtr, // [total_tokens, K] BF16
740 weight_ptrs: DevicePtr, // [num_experts] → [N, K] BF16
741 output: DevicePtr, // [total_expanded, N] BF16
742 expert_offsets: DevicePtr, // [num_experts + 1]
743 sorted_token_ids: DevicePtr, // [total_expanded] or NULL
744 num_experts: u32,
745 n: u32,
746 k: u32,
747 max_m_tiles: u32,
748 stream: u64,
749) -> Result<()> {
750 KernelLaunch::new(gpu, kernel)
751 .grid([div_ceil(n, 64), max_m_tiles, num_experts])
752 .block([128, 1, 1])
753 .arg_ptr(input)
754 .arg_ptr(weight_ptrs)
755 .arg_ptr(output)
756 .arg_ptr(expert_offsets)
757 .arg_ptr(sorted_token_ids)
758 .arg_u32(num_experts)
759 .arg_u32(n)
760 .arg_u32(k)
761 .launch(stream)
762}
763
764/// W8A16 Transposed GEMM: `C[M,N] = A[M,K] @ dequant(B_t[K,N])` with coalesced reads.
765///
766/// Uses transposed FP8 weights `B_t[K,N]` and `block_scale_t[K/128, N/128]` for
767/// coalesced N-dimension reads. ~14x faster than non-transposed w8a16_gemm at long M.
768/// Grid: (ceil(N/64), ceil(M/64), 1) Block: (128, 1, 1)
769#[allow(clippy::too_many_arguments)]
770pub fn w8a16_gemm_t(
771 gpu: &dyn GpuBackend,
772 kernel: KernelHandle,
773 input: DevicePtr,
774 weight_t: DevicePtr, // [K, N] FP8 transposed
775 block_scale_t: DevicePtr, // [K/128, N/128] BF16 transposed
776 output: DevicePtr,
777 m: u32,
778 n: u32,
779 k: u32,
780 stream: u64,
781) -> Result<()> {
782 KernelLaunch::new(gpu, kernel)
783 .grid([div_ceil(n, 64), div_ceil(m, 64), 1])
784 .block([128, 1, 1])
785 .arg_ptr(input)
786 .arg_ptr(weight_t)
787 .arg_ptr(block_scale_t)
788 .arg_ptr(output)
789 .arg_u32(m)
790 .arg_u32(n)
791 .arg_u32(k)
792 .launch(stream)
793}
794
795/// W8A16 transposed M128 GEMM (kernel `w8a16_gemm_t_m128`): FP8 E4M3 analog of
796/// `w4a16_gemm_n128_m128_v2`. 128×128 (M×N) tile, two 64-row chunks, 8 warps,
797/// parallel-chunk `m16n8k16.bf16.bf16` MMA + two-level FP32 block-scale fold.
798/// Same transposed contract as `w8a16_gemm_t` (`B_t[K,N]` + block_scale_t[K/128,
799/// N/128]); reuses the transpose_fp8 / transpose_block_scale output as-is.
800/// Grid: (ceil(N/128), ceil(M/128), 1) Block: (256, 1, 1)
801#[allow(clippy::too_many_arguments)]
802pub fn w8a16_gemm_n128_m128(
803 gpu: &dyn GpuBackend,
804 kernel: KernelHandle,
805 input: DevicePtr,
806 weight_t: DevicePtr, // [K, N] FP8 transposed
807 block_scale_t: DevicePtr, // [K/128, N/128] FP32 transposed
808 output: DevicePtr,
809 m: u32,
810 n: u32,
811 k: u32,
812 stream: u64,
813) -> Result<()> {
814 super::log_gemm_shape(gpu, "w8a16_gemm_t_m128", m, n, k);
815 KernelLaunch::new(gpu, kernel)
816 .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
817 .block([256, 1, 1])
818 .arg_ptr(input)
819 .arg_ptr(weight_t)
820 .arg_ptr(block_scale_t)
821 .arg_ptr(output)
822 .arg_u32(m)
823 .arg_u32(n)
824 .arg_u32(k)
825 .launch(stream)
826}
827
828/// Pipelined transposed W8A16 GEMM (kernel `w8a16_gemm_t_pipelined`): same
829/// transposed args as `w8a16_gemm_t`, ~4.2x via smem-LUT + K_STEP32 +
830/// K-contiguous smem_B + 128x32 occupancy tile.
831/// Grid: (ceil(N/32), ceil(M/128), 1) Block: (256, 1, 1)
832#[allow(clippy::too_many_arguments)]
833pub fn w8a16_gemm_t_pipelined(
834 gpu: &dyn GpuBackend,
835 kernel: KernelHandle,
836 input: DevicePtr,
837 weight_t: DevicePtr,
838 block_scale_t: DevicePtr,
839 output: DevicePtr,
840 m: u32,
841 n: u32,
842 k: u32,
843 stream: u64,
844) -> Result<()> {
845 super::log_gemm_shape(gpu, "w8a16_gemm_t_pipelined", m, n, k);
846 KernelLaunch::new(gpu, kernel)
847 .grid([div_ceil(n, 32), div_ceil(m, 128), 1])
848 .block([256, 1, 1])
849 .arg_ptr(input)
850 .arg_ptr(weight_t)
851 .arg_ptr(block_scale_t)
852 .arg_ptr(output)
853 .arg_u32(m)
854 .arg_u32(n)
855 .arg_u32(k)
856 .launch(stream)
857}
858
859/// Transpose FP8 weight matrix on GPU: `B[N,K]` → `B_t[K,N]`.
860/// Grid: (ceil(N*K/256), 1, 1) Block: (256, 1, 1)
861pub fn transpose_fp8(
862 gpu: &dyn GpuBackend,
863 kernel: KernelHandle,
864 src: DevicePtr, // [N, K]
865 dst: DevicePtr, // [K, N]
866 n: u32,
867 k: u32,
868 stream: u64,
869) -> Result<()> {
870 let total = n as u64 * k as u64;
871 KernelLaunch::new(gpu, kernel)
872 .grid([div_ceil(total as u32, 256), 1, 1])
873 .block([256, 1, 1])
874 .arg_ptr(src)
875 .arg_ptr(dst)
876 .arg_u32(n)
877 .arg_u32(k)
878 .launch(stream)
879}
880
881/// Widen an FP8 block-scale tensor to FP32 on the GPU.
882///
883/// `src` is `[total]` BF16 (0), FP32 (1), or F8_E8M0 (2); `dst` is `[total]`
884/// FP32. E8M0 uses the exact `exp << 23` power-of-two representation.
885/// Run once at load so downstream FP8 block-scale kernels read `const float*`.
886/// Grid: (ceil(total/256), 1, 1) Block: (256, 1, 1)
887pub fn widen_block_scale_f32(
888 gpu: &dyn GpuBackend,
889 kernel: KernelHandle,
890 src: DevicePtr,
891 dst: DevicePtr,
892 total: u32,
893 input_dtype: u32,
894 stream: u64,
895) -> Result<()> {
896 KernelLaunch::new(gpu, kernel)
897 .grid([div_ceil(total, 256), 1, 1])
898 .block([256, 1, 1])
899 .arg_ptr(src)
900 .arg_ptr(dst)
901 .arg_u32(total)
902 .arg_u32(input_dtype)
903 .launch(stream)
904}
905
906/// Transpose block scales: [N/128, K/128] → [K/128, N/128].
907pub fn transpose_block_scale(
908 gpu: &dyn GpuBackend,
909 kernel: KernelHandle,
910 src: DevicePtr,
911 dst: DevicePtr,
912 n_blocks: u32,
913 k_blocks: u32,
914 stream: u64,
915) -> Result<()> {
916 let total = n_blocks * k_blocks;
917 KernelLaunch::new(gpu, kernel)
918 .grid([div_ceil(total, 256), 1, 1])
919 .block([256, 1, 1])
920 .arg_ptr(src)
921 .arg_ptr(dst)
922 .arg_u32(n_blocks)
923 .arg_u32(k_blocks)
924 .launch(stream)
925}
926
927// ── Unified quantization dispatch ────────────────────────────────────
928//
929// These wrappers select the correct kernel based on the QuantWeight
930// variant. Adding a new quant format requires only a new match arm here.
931
932/// The three BF16 dense kernels one projection site can land on, resolved once.
933///
934/// GLM-5.3 binds one of these per mixer/MLP site; `batchm` is `0` on a backend that
935/// does not carry `dense_gemv_bf16_batchm`, and [`dense_mm_bf16`] then falls back to
936/// the tile GEMM exactly as before.
937#[derive(Clone, Copy)]
938pub struct DenseMmKernels {
939 /// `dense_gemm_bf16` — 16×16 tile GEMM. The only arm that handles `M > 8`.
940 pub gemm: KernelHandle,
941 /// `dense_gemv_bf16` — `M == 1`.
942 pub gemv: KernelHandle,
943 /// `dense_gemv_bf16_batchm` — `2 ..= 8`, ONE weight sweep. `0` = unavailable.
944 pub batchm: KernelHandle,
945}
946
947/// `C[M, N] = A[M, K] @ B[N, K]^T`, BF16 in and out, output row stride `N`.
948///
949/// 🔴 **The M dispatch is the whole point.** At `M == 1` the tile GEMM's grid collapses
950/// (73 GB/s against a 254 GB/s part). At `2 ..= 8` it is ~94 % padding and measured 3.6×
951/// SLOWER than the batched GEMV on this exact workload (`multi_seq/qkv.rs::wide_verify_gemm`).
952/// `batchm` reads the weight matrix ONCE for all M rows — which is what makes a K-token
953/// speculative verify cost one weight sweep instead of K.
954///
955/// 🪤 `batchm` is **bit-identical to M separate `dense_gemv` calls** (same K-iteration order
956/// and reduction tree per row, `--fmad=false`), so batching K rows that were previously K
957/// serial single-row decodes does not move a single bit. The tile-GEMM arm is NOT
958/// bit-identical to either — it reassociates. Widening a site past 8 rows changes numerics.
959#[allow(clippy::too_many_arguments)]
960pub fn dense_mm_bf16(
961 gpu: &dyn GpuBackend,
962 k: &DenseMmKernels,
963 a: DevicePtr,
964 b: DevicePtr,
965 c: DevicePtr,
966 m: usize,
967 n: usize,
968 kk: usize,
969 stream: u64,
970) -> Result<()> {
971 // 🪤 Grid is COUPLED to each kernel's `N_PER_BLOCK` (4 outputs / 256-thread block for
972 // both GEMV arms, `GEMM_TILE` for the tile arm). Never hand-roll these div_ceils.
973 if m == 1 && k.gemv.0 != 0 {
974 return KernelLaunch::new(gpu, k.gemv)
975 .grid([div_ceil(n as u32, 4), 1, 1])
976 .block([256, 1, 1])
977 .arg_ptr(a)
978 .arg_ptr(b)
979 .arg_ptr(c)
980 .arg_u32(n as u32)
981 .arg_u32(kk as u32)
982 .launch(stream);
983 }
984 // 🪤 A missing batchm handle falls back SILENTLY to the tile GEMM, which is 3.6x slower
985 // at these widths — exactly the failure `announce_dispatch` exists to prevent elsewhere.
986 if m > 1 && k.batchm.0 == 0 {
987 static ONCE: std::sync::Once = std::sync::Once::new();
988 ONCE.call_once(|| {
989 tracing::warn!(
990 "dense_mm_bf16: no dense_gemv_bf16_batchm on this target -- M>1 sites fall \
991 back to the tile GEMM (measured 3.6x slower at M<=8)"
992 );
993 });
994 }
995 if (2..=DENSE_GEMV_BATCHM_MAX_M as usize).contains(&m) && k.batchm.0 != 0 {
996 return KernelLaunch::new(gpu, k.batchm)
997 .grid([div_ceil(n as u32, 4), 1, 1])
998 .block([256, 1, 1])
999 .arg_ptr(a)
1000 .arg_ptr(b)
1001 .arg_ptr(c)
1002 .arg_u32(m as u32)
1003 .arg_u32(n as u32)
1004 .arg_u32(kk as u32)
1005 // Contiguous `[M, N]` output — the layout `dense_gemm_bf16` writes.
1006 .arg_u32(n as u32)
1007 .launch(stream);
1008 }
1009 const GEMM_TILE: u32 = 16;
1010 KernelLaunch::new(gpu, k.gemm)
1011 .grid([
1012 (n as u32).div_ceil(GEMM_TILE),
1013 (m as u32).div_ceil(GEMM_TILE),
1014 1,
1015 ])
1016 .block([GEMM_TILE, GEMM_TILE, 1])
1017 .arg_ptr(a)
1018 .arg_ptr(b)
1019 .arg_ptr(c)
1020 .arg_u32(m as u32)
1021 .arg_u32(n as u32)
1022 .arg_u32(kk as u32)
1023 .launch(stream)
1024}