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/// Grid: (K/128, M, 1)  Block: (128, 1, 1)
397#[allow(clippy::too_many_arguments)]
398pub fn per_token_group_quant_fp8(
399    gpu: &dyn GpuBackend,
400    kernel: KernelHandle,
401    input_bf16: DevicePtr,
402    output_fp8: DevicePtr,
403    a_scale: DevicePtr,
404    m: u32,
405    k: u32,
406    stream: u64,
407) -> Result<()> {
408    // Grid: (M, K/128, 1). Putting M on grid X (max 2^31-1) avoids the
409    // 65535 limit on grid Y for large MoE total_expanded counts.
410    KernelLaunch::new(gpu, kernel)
411        .grid([m, k / 128, 1])
412        .block([128, 1, 1])
413        .arg_ptr(input_bf16)
414        .arg_ptr(output_fp8)
415        .arg_ptr(a_scale)
416        .arg_u32(m)
417        .arg_u32(k)
418        .launch(stream)
419}
420
421/// W8A8 + FP32 epilogue GEMM with per-token activation scales and
422/// per-block weight scales — vLLM-equivalent FP8 numerics.
423///
424///   C[M, N] = bf16( Σ_g (FP8 MMA over K-group g) × a_scale[M, g] × b_scale[N/128, g] )
425///
426/// Inputs:
427///   - `a_fp8`     [M, K] FP8 E4M3
428///   - `a_scale`   [M, K/128] FP32 (from per_token_group_quant_fp8)
429///   - `b_fp8`     [N, K] FP8 E4M3
430///   - `b_scale`   [N/128, K/128] BF16 (existing checkpoint layout)
431///   - `output`    [M, N] BF16
432///
433/// Grid: (ceil(N/128), ceil(M/64), 1)  Block: (128, 1, 1)
434#[allow(clippy::too_many_arguments)]
435pub fn fp8_gemm_t_blockscaled(
436    gpu: &dyn GpuBackend,
437    kernel: KernelHandle,
438    a_fp8: DevicePtr,
439    a_scale: DevicePtr,
440    b_fp8: DevicePtr,
441    b_scale: DevicePtr,
442    output: DevicePtr,
443    m: u32,
444    n: u32,
445    k: u32,
446    stream: u64,
447) -> Result<()> {
448    super::log_gemm_shape(gpu, "fp8_gemm_t_blockscaled", m, n, k);
449    KernelLaunch::new(gpu, kernel)
450        .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
451        .block([128, 1, 1])
452        .arg_ptr(a_fp8)
453        .arg_ptr(a_scale)
454        .arg_ptr(b_fp8)
455        .arg_ptr(b_scale)
456        .arg_ptr(output)
457        .arg_u32(m)
458        .arg_u32(n)
459        .arg_u32(k)
460        .launch(stream)
461}
462
463/// Fused gate GEMV + topK softmax for M=1 decode.
464///
465/// Single kernel that computes `gate[num_experts] = A[K] @ B_gate[num_experts, K]`
466/// then extracts top-K indices + softmax weights. Saves 1 launch vs separate
467/// gate GEMV + topK kernels.
468///
469/// Grid: (1, 1, 1)  Block: (256, 1, 1) — single CTA, uses shared memory reduction
470#[allow(clippy::too_many_arguments)]
471pub fn moe_gate_topk_fused(
472    gpu: &dyn GpuBackend,
473    kernel: KernelHandle,
474    input: DevicePtr,
475    gate_weight: &QuantizedWeight,
476    expert_indices: DevicePtr,
477    expert_weights: DevicePtr,
478    num_experts: u32,
479    k: u32,
480    top_k: u32,
481    normalize: u32,
482    stream: u64,
483) -> Result<()> {
484    // Dynamic shared memory: K BF16 values for input broadcast
485    let smem_bytes = k as usize * 2;
486    KernelLaunch::new(gpu, kernel)
487        .grid([1, 1, 1])
488        .block([256, 1, 1])
489        .shared_mem(smem_bytes as u32)
490        .arg_ptr(input)
491        .arg_ptr(gate_weight.weight)
492        .arg_ptr(gate_weight.weight_scale)
493        .arg_f32(gate_weight.weight_scale_2)
494        .arg_ptr(expert_indices)
495        .arg_ptr(expert_weights)
496        .arg_u32(num_experts)
497        .arg_u32(k)
498        .arg_u32(top_k)
499        .arg_u32(normalize)
500        .launch(stream)
501}
502
503/// Build the compacted (expert, m_tile, n_tile) work-list for the
504/// persistent grouped-GEMM grid. Single-block, thread-0 serial — mirrors the
505/// `moe_sort_by_expert` launch style (grid `[1,1,1]`, block `[256,1,1]`).
506///
507/// `n_tiles = div_ceil(N, 64)` (PM4_N_TILE) and `m_tile = 128` (PM4_M_TILE).
508/// Writes `worklist[*total_tiles * 2]` (word0=expert, word1=(m_tile<<6)|n_tile)
509/// and `total_tiles[0]`.
510///
511/// SAME-STREAM INVARIANT: the caller MUST launch `moe_fp8_grouped_gemm` on
512/// the SAME `stream` so the kernel's read of `total_tiles`/`worklist`
513/// happens-after this write (no cross-stream event is inserted).
514#[allow(clippy::too_many_arguments)]
515pub fn moe_build_tile_worklist(
516    gpu: &dyn GpuBackend,
517    kernel: KernelHandle,
518    expert_offsets: DevicePtr, // [num_experts + 1]
519    weight_ptrs: DevicePtr,    // [num_experts] → [N, K] FP8 (0 = remote)
520    worklist: DevicePtr,       // [worst_case_tiles * 2] u32 (out)
521    total_tiles: DevicePtr,    // [1] i32 (out)
522    num_experts: u32,
523    n_tiles: u32, // div_ceil(N, 64) — PM4_N_TILE
524    m_tile: u32,  // PM4_M_TILE = 128
525    stream: u64,
526) -> Result<()> {
527    KernelLaunch::new(gpu, kernel)
528        .grid([1, 1, 1])
529        .block([256, 1, 1])
530        .arg_ptr(expert_offsets)
531        .arg_ptr(weight_ptrs)
532        .arg_ptr(worklist)
533        .arg_ptr(total_tiles)
534        .arg_u32(num_experts)
535        .arg_u32(n_tiles)
536        .arg_u32(m_tile)
537        .launch(stream)
538}
539
540/// FP8 grouped GEMM for sorted MoE prefill — grid-compaction over the COMPACTED
541/// work-list built by `moe_build_tile_worklist`. THE routed-expert FP8 prefill
542/// kernel.
543///
544/// The kernel grid-strides by `gridDim.x`, so the launch is sized to
545/// `max_tiles` — the caller's exact upper bound on the work-item (tile) count
546/// (`wl_cap_items`). This covers the whole work-list in ~one pass instead of
547/// serializing dozens of tiles per CTA behind sync barriers (the old fixed
548/// 96-CTA persistent grid left the GPU >90% idle: ~0.2% occupancy / ~16%
549/// MemUnitBusy, measured on gfx1151). Oversubscription is safe (extra CTAs
550/// exit the loop immediately); undersizing is merely slower, never wrong.
551///
552/// `max_tiles` is clamped to `MAX_GRID_CTAS` so a pathological worklist bound
553/// cannot request an unbounded grid.
554///
555/// SAME-STREAM INVARIANT: MUST be launched on the SAME `stream` as the
556/// preceding `moe_build_tile_worklist` (read-after-write of `total_tiles`).
557///
558/// Grid: (max_tiles.clamp(1, MAX_GRID_CTAS), 1, 1)  Block: (256, 1, 1)
559#[allow(clippy::too_many_arguments)]
560pub fn moe_fp8_grouped_gemm(
561    gpu: &dyn GpuBackend,
562    kernel: KernelHandle,
563    input: DevicePtr,            // [total_tokens, K] BF16
564    weight_ptrs: DevicePtr,      // [num_experts] → [N, K] FP8
565    scale_ptrs: DevicePtr,       // [num_experts] → [N/128, K/128] FP32
566    output: DevicePtr,           // [total_expanded, N] BF16
567    expert_offsets: DevicePtr,   // [num_experts + 1]
568    sorted_token_ids: DevicePtr, // [total_expanded] or NULL
569    num_experts: u32,
570    n: u32,
571    k: u32,
572    worklist: DevicePtr,    // [*total_tiles * 2] u32 (built on the same stream)
573    total_tiles: DevicePtr, // [1] i32 (built on the same stream)
574    max_tiles: u32,         // caller's upper bound on tile count (wl_cap_items)
575    stream: u64,
576) -> Result<()> {
577    // The kernel strides by gridDim.x, so the grid is sized to the work-list's
578    // tile-count upper bound. Clamp to MAX_GRID_CTAS to bound the launch.
579    const MAX_GRID_CTAS: u32 = 16384;
580    let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
581    // Block size is target-specific because the kernel SOURCE differs. The
582    // native-HIP (gfx1151) kernel is a 16-warp / 512-thread block with a 2-D
583    // (8 warp-rows x 2 warp-cols) warp grid: it keeps the 128x64 tile geometry
584    // (so the work-list packing is unchanged) but splits the 4 WMMA n-sub-tiles
585    // across 2 warp-columns, doubling warp occupancy for latency hiding on the
586    // long-K gate/up GEMM (kernels/strix-hip/common/moe_fp8_grouped_gemm.cu).
587    // Every other target keeps the 8-warp / 256-thread M-only kernel
588    // (kernels/gb10/common/moe_fp8_grouped_gemm.cu). Keep this in lockstep with
589    // that .cu PM4_THREADS.
590    #[cfg(atlas_hip)]
591    let block = [512u32, 1, 1];
592    #[cfg(not(atlas_hip))]
593    let block = [256u32, 1, 1];
594    KernelLaunch::new(gpu, kernel)
595        .grid([grid_ctas, 1, 1])
596        .block(block)
597        .arg_ptr(input)
598        .arg_ptr(weight_ptrs)
599        .arg_ptr(scale_ptrs)
600        .arg_ptr(output)
601        .arg_ptr(expert_offsets)
602        .arg_ptr(sorted_token_ids)
603        .arg_u32(num_experts)
604        .arg_u32(n)
605        .arg_u32(k)
606        .arg_ptr(worklist)
607        .arg_ptr(total_tiles)
608        .launch(stream)
609}
610
611/// W8A8 + FP32 epilogue grouped MoE GEMM (vLLM-equivalent).
612///
613/// A_fp8 must be pre-quantized via `per_token_group_quant_fp8`. Both
614/// `a_scale` (per-token, FP32) and `b_scale` (per-block, BF16) are applied
615/// in the FP32 epilogue per K=128 block.
616#[allow(clippy::too_many_arguments)]
617pub fn moe_w8a8_grouped_gemm(
618    gpu: &dyn GpuBackend,
619    kernel: KernelHandle,
620    a_fp8: DevicePtr,            // [total_tokens, K] FP8 E4M3
621    a_scale: DevicePtr,          // [total_tokens, K/128] FP32
622    weight_ptrs: DevicePtr,      // [num_experts] → [N, K] FP8
623    scale_ptrs: DevicePtr,       // [num_experts] → [N/128, K/128] BF16
624    output: DevicePtr,           // [total_expanded, N] BF16
625    expert_offsets: DevicePtr,   // [num_experts + 1]
626    sorted_token_ids: DevicePtr, // [total_expanded] or NULL
627    num_experts: u32,
628    n: u32,
629    k: u32,
630    max_m_tiles: u32,
631    stream: u64,
632) -> Result<()> {
633    KernelLaunch::new(gpu, kernel)
634        .grid([div_ceil(n, 64), max_m_tiles, num_experts])
635        .block([128, 1, 1])
636        .arg_ptr(a_fp8)
637        .arg_ptr(a_scale)
638        .arg_ptr(weight_ptrs)
639        .arg_ptr(scale_ptrs)
640        .arg_ptr(output)
641        .arg_ptr(expert_offsets)
642        .arg_ptr(sorted_token_ids)
643        .arg_u32(num_experts)
644        .arg_u32(n)
645        .arg_u32(k)
646        .launch(stream)
647}
648
649/// W8A8 + FP32 epilogue grouped MoE GEMM — PM4 geometry over the COMPACTED
650/// work-list built by `moe_build_tile_worklist` (kernel
651/// `moe_w8a8_grouped_gemm_pm4`, same module/numerics as
652/// `moe_w8a8_grouped_gemm`: bit-identical output, measured).
653///
654/// Same grid-compaction contract as `moe_fp8_grouped_gemm`: the kernel
655/// grid-strides by `gridDim.x` over the work-list, so the launch is sized to
656/// `max_tiles` (`wl_cap_items`), clamped to `MAX_GRID_CTAS`. Oversubscription
657/// is safe; undersizing is merely slower, never wrong.
658///
659/// SAME-STREAM INVARIANT: MUST be launched on the SAME `stream` as the
660/// preceding `moe_build_tile_worklist` (read-after-write of `total_tiles`).
661///
662/// Grid: (max_tiles.clamp(1, MAX_GRID_CTAS), 1, 1)  Block: (256, 1, 1)
663#[allow(clippy::too_many_arguments)]
664pub fn moe_w8a8_grouped_gemm_pm4(
665    gpu: &dyn GpuBackend,
666    kernel: KernelHandle,
667    a_fp8: DevicePtr,            // [total_tokens, K] FP8 E4M3
668    a_scale: DevicePtr,          // [total_tokens, K/128] FP32
669    weight_ptrs: DevicePtr,      // [num_experts] → [N, K] FP8
670    scale_ptrs: DevicePtr,       // [num_experts] → [N/128, K/128] FP32
671    output: DevicePtr,           // [total_expanded, N] BF16
672    expert_offsets: DevicePtr,   // [num_experts + 1]
673    sorted_token_ids: DevicePtr, // [total_expanded] or NULL
674    num_experts: u32,
675    n: u32,
676    k: u32,
677    worklist: DevicePtr,    // [*total_tiles * 2] u32 (built on the same stream)
678    total_tiles: DevicePtr, // [1] i32 (built on the same stream)
679    max_tiles: u32,         // caller's upper bound on tile count (wl_cap_items)
680    stream: u64,
681) -> Result<()> {
682    const MAX_GRID_CTAS: u32 = 16384;
683    let grid_ctas = max_tiles.clamp(1, MAX_GRID_CTAS);
684    // gb10-only kernel (256 threads, __launch_bounds__(256,2)); other targets
685    // fall back to the dense-grid `moe_w8a8_grouped_gemm` (handle gating at
686    // the dispatch site).
687    KernelLaunch::new(gpu, kernel)
688        .grid([grid_ctas, 1, 1])
689        .block([256, 1, 1])
690        .arg_ptr(a_fp8)
691        .arg_ptr(a_scale)
692        .arg_ptr(weight_ptrs)
693        .arg_ptr(scale_ptrs)
694        .arg_ptr(output)
695        .arg_ptr(expert_offsets)
696        .arg_ptr(sorted_token_ids)
697        .arg_u32(num_experts)
698        .arg_u32(n)
699        .arg_u32(k)
700        .arg_ptr(worklist)
701        .arg_ptr(total_tiles)
702        .launch(stream)
703}
704
705/// BF16 grouped GEMM for sorted MoE prefill (FP8-dequant-on-load path).
706///
707/// BF16 activations × BF16 expert weights via pointer table. No scale.
708/// Used when expert weights have been dequanted from FP8 to BF16 at load
709/// time (ATLAS_FP8_DEQUANT_MOE_TO_BF16=1). Eliminates the per-layer 0.989
710/// cosine ceiling that comes from FP8 quantization itself.
711///
712/// Grid: (ceil(N/64), max_m_tiles, num_experts)  Block: (128, 1, 1)
713#[allow(clippy::too_many_arguments)]
714pub fn moe_bf16_grouped_gemm(
715    gpu: &dyn GpuBackend,
716    kernel: KernelHandle,
717    input: DevicePtr,            // [total_tokens, K] BF16
718    weight_ptrs: DevicePtr,      // [num_experts] → [N, K] BF16
719    output: DevicePtr,           // [total_expanded, N] BF16
720    expert_offsets: DevicePtr,   // [num_experts + 1]
721    sorted_token_ids: DevicePtr, // [total_expanded] or NULL
722    num_experts: u32,
723    n: u32,
724    k: u32,
725    max_m_tiles: u32,
726    stream: u64,
727) -> Result<()> {
728    KernelLaunch::new(gpu, kernel)
729        .grid([div_ceil(n, 64), max_m_tiles, num_experts])
730        .block([128, 1, 1])
731        .arg_ptr(input)
732        .arg_ptr(weight_ptrs)
733        .arg_ptr(output)
734        .arg_ptr(expert_offsets)
735        .arg_ptr(sorted_token_ids)
736        .arg_u32(num_experts)
737        .arg_u32(n)
738        .arg_u32(k)
739        .launch(stream)
740}
741
742/// W8A16 Transposed GEMM: `C[M,N] = A[M,K] @ dequant(B_t[K,N])` with coalesced reads.
743///
744/// Uses transposed FP8 weights `B_t[K,N]` and `block_scale_t[K/128, N/128]` for
745/// coalesced N-dimension reads. ~14x faster than non-transposed w8a16_gemm at long M.
746/// Grid: (ceil(N/64), ceil(M/64), 1)  Block: (128, 1, 1)
747#[allow(clippy::too_many_arguments)]
748pub fn w8a16_gemm_t(
749    gpu: &dyn GpuBackend,
750    kernel: KernelHandle,
751    input: DevicePtr,
752    weight_t: DevicePtr,      // [K, N] FP8 transposed
753    block_scale_t: DevicePtr, // [K/128, N/128] BF16 transposed
754    output: DevicePtr,
755    m: u32,
756    n: u32,
757    k: u32,
758    stream: u64,
759) -> Result<()> {
760    KernelLaunch::new(gpu, kernel)
761        .grid([div_ceil(n, 64), div_ceil(m, 64), 1])
762        .block([128, 1, 1])
763        .arg_ptr(input)
764        .arg_ptr(weight_t)
765        .arg_ptr(block_scale_t)
766        .arg_ptr(output)
767        .arg_u32(m)
768        .arg_u32(n)
769        .arg_u32(k)
770        .launch(stream)
771}
772
773/// W8A16 transposed M128 GEMM (kernel `w8a16_gemm_t_m128`): FP8 E4M3 analog of
774/// `w4a16_gemm_n128_m128_v2`. 128×128 (M×N) tile, two 64-row chunks, 8 warps,
775/// parallel-chunk `m16n8k16.bf16.bf16` MMA + two-level FP32 block-scale fold.
776/// Same transposed contract as `w8a16_gemm_t` (`B_t[K,N]` + block_scale_t[K/128,
777/// N/128]); reuses the transpose_fp8 / transpose_block_scale output as-is.
778/// Grid: (ceil(N/128), ceil(M/128), 1)  Block: (256, 1, 1)
779#[allow(clippy::too_many_arguments)]
780pub fn w8a16_gemm_n128_m128(
781    gpu: &dyn GpuBackend,
782    kernel: KernelHandle,
783    input: DevicePtr,
784    weight_t: DevicePtr,      // [K, N] FP8 transposed
785    block_scale_t: DevicePtr, // [K/128, N/128] FP32 transposed
786    output: DevicePtr,
787    m: u32,
788    n: u32,
789    k: u32,
790    stream: u64,
791) -> Result<()> {
792    super::log_gemm_shape(gpu, "w8a16_gemm_t_m128", m, n, k);
793    KernelLaunch::new(gpu, kernel)
794        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
795        .block([256, 1, 1])
796        .arg_ptr(input)
797        .arg_ptr(weight_t)
798        .arg_ptr(block_scale_t)
799        .arg_ptr(output)
800        .arg_u32(m)
801        .arg_u32(n)
802        .arg_u32(k)
803        .launch(stream)
804}
805
806/// Pipelined transposed W8A16 GEMM (kernel `w8a16_gemm_t_pipelined`): same
807/// transposed args as `w8a16_gemm_t`, ~4.2x via smem-LUT + K_STEP32 +
808/// K-contiguous smem_B + 128x32 occupancy tile.
809/// Grid: (ceil(N/32), ceil(M/128), 1)  Block: (256, 1, 1)
810#[allow(clippy::too_many_arguments)]
811pub fn w8a16_gemm_t_pipelined(
812    gpu: &dyn GpuBackend,
813    kernel: KernelHandle,
814    input: DevicePtr,
815    weight_t: DevicePtr,
816    block_scale_t: DevicePtr,
817    output: DevicePtr,
818    m: u32,
819    n: u32,
820    k: u32,
821    stream: u64,
822) -> Result<()> {
823    super::log_gemm_shape(gpu, "w8a16_gemm_t_pipelined", m, n, k);
824    KernelLaunch::new(gpu, kernel)
825        .grid([div_ceil(n, 32), div_ceil(m, 128), 1])
826        .block([256, 1, 1])
827        .arg_ptr(input)
828        .arg_ptr(weight_t)
829        .arg_ptr(block_scale_t)
830        .arg_ptr(output)
831        .arg_u32(m)
832        .arg_u32(n)
833        .arg_u32(k)
834        .launch(stream)
835}
836
837/// Transpose FP8 weight matrix on GPU: `B[N,K]` → `B_t[K,N]`.
838/// Grid: (ceil(N*K/256), 1, 1)  Block: (256, 1, 1)
839pub fn transpose_fp8(
840    gpu: &dyn GpuBackend,
841    kernel: KernelHandle,
842    src: DevicePtr, // [N, K]
843    dst: DevicePtr, // [K, N]
844    n: u32,
845    k: u32,
846    stream: u64,
847) -> Result<()> {
848    let total = n as u64 * k as u64;
849    KernelLaunch::new(gpu, kernel)
850        .grid([div_ceil(total as u32, 256), 1, 1])
851        .block([256, 1, 1])
852        .arg_ptr(src)
853        .arg_ptr(dst)
854        .arg_u32(n)
855        .arg_u32(k)
856        .launch(stream)
857}
858
859/// Widen an FP8 block-scale tensor to FP32 on the GPU.
860///
861/// `src` is `[total]` BF16 (0), FP32 (1), or F8_E8M0 (2); `dst` is `[total]`
862/// FP32. E8M0 uses the exact `exp << 23` power-of-two representation.
863/// Run once at load so downstream FP8 block-scale kernels read `const float*`.
864/// Grid: (ceil(total/256), 1, 1)  Block: (256, 1, 1)
865pub fn widen_block_scale_f32(
866    gpu: &dyn GpuBackend,
867    kernel: KernelHandle,
868    src: DevicePtr,
869    dst: DevicePtr,
870    total: u32,
871    input_dtype: u32,
872    stream: u64,
873) -> Result<()> {
874    KernelLaunch::new(gpu, kernel)
875        .grid([div_ceil(total, 256), 1, 1])
876        .block([256, 1, 1])
877        .arg_ptr(src)
878        .arg_ptr(dst)
879        .arg_u32(total)
880        .arg_u32(input_dtype)
881        .launch(stream)
882}
883
884/// Transpose block scales: [N/128, K/128] → [K/128, N/128].
885pub fn transpose_block_scale(
886    gpu: &dyn GpuBackend,
887    kernel: KernelHandle,
888    src: DevicePtr,
889    dst: DevicePtr,
890    n_blocks: u32,
891    k_blocks: u32,
892    stream: u64,
893) -> Result<()> {
894    let total = n_blocks * k_blocks;
895    KernelLaunch::new(gpu, kernel)
896        .grid([div_ceil(total, 256), 1, 1])
897        .block([256, 1, 1])
898        .arg_ptr(src)
899        .arg_ptr(dst)
900        .arg_u32(n_blocks)
901        .arg_u32(k_blocks)
902        .launch(stream)
903}
904
905// ── Unified quantization dispatch ────────────────────────────────────
906//
907// These wrappers select the correct kernel based on the QuantWeight
908// variant. Adding a new quant format requires only a new match arm here.
909
910/// The three BF16 dense kernels one projection site can land on, resolved once.
911///
912/// GLM-5.3 binds one of these per mixer/MLP site; `batchm` is `0` on a backend that
913/// does not carry `dense_gemv_bf16_batchm`, and [`dense_mm_bf16`] then falls back to
914/// the tile GEMM exactly as before.
915#[derive(Clone, Copy)]
916pub struct DenseMmKernels {
917    /// `dense_gemm_bf16` — 16×16 tile GEMM. The only arm that handles `M > 8`.
918    pub gemm: KernelHandle,
919    /// `dense_gemv_bf16` — `M == 1`.
920    pub gemv: KernelHandle,
921    /// `dense_gemv_bf16_batchm` — `2 ..= 8`, ONE weight sweep. `0` = unavailable.
922    pub batchm: KernelHandle,
923}
924
925/// `C[M, N] = A[M, K] @ B[N, K]^T`, BF16 in and out, output row stride `N`.
926///
927/// 🔴 **The M dispatch is the whole point.** At `M == 1` the tile GEMM's grid collapses
928/// (73 GB/s against a 254 GB/s part). At `2 ..= 8` it is ~94 % padding and measured 3.6×
929/// SLOWER than the batched GEMV on this exact workload (`multi_seq/qkv.rs::wide_verify_gemm`).
930/// `batchm` reads the weight matrix ONCE for all M rows — which is what makes a K-token
931/// speculative verify cost one weight sweep instead of K.
932///
933/// 🪤 `batchm` is **bit-identical to M separate `dense_gemv` calls** (same K-iteration order
934/// and reduction tree per row, `--fmad=false`), so batching K rows that were previously K
935/// serial single-row decodes does not move a single bit. The tile-GEMM arm is NOT
936/// bit-identical to either — it reassociates. Widening a site past 8 rows changes numerics.
937#[allow(clippy::too_many_arguments)]
938pub fn dense_mm_bf16(
939    gpu: &dyn GpuBackend,
940    k: &DenseMmKernels,
941    a: DevicePtr,
942    b: DevicePtr,
943    c: DevicePtr,
944    m: usize,
945    n: usize,
946    kk: usize,
947    stream: u64,
948) -> Result<()> {
949    // 🪤 Grid is COUPLED to each kernel's `N_PER_BLOCK` (4 outputs / 256-thread block for
950    // both GEMV arms, `GEMM_TILE` for the tile arm). Never hand-roll these div_ceils.
951    if m == 1 && k.gemv.0 != 0 {
952        return KernelLaunch::new(gpu, k.gemv)
953            .grid([div_ceil(n as u32, 4), 1, 1])
954            .block([256, 1, 1])
955            .arg_ptr(a)
956            .arg_ptr(b)
957            .arg_ptr(c)
958            .arg_u32(n as u32)
959            .arg_u32(kk as u32)
960            .launch(stream);
961    }
962    // 🪤 A missing batchm handle falls back SILENTLY to the tile GEMM, which is 3.6x slower
963    // at these widths — exactly the failure `announce_dispatch` exists to prevent elsewhere.
964    if m > 1 && k.batchm.0 == 0 {
965        static ONCE: std::sync::Once = std::sync::Once::new();
966        ONCE.call_once(|| {
967            tracing::warn!(
968                "dense_mm_bf16: no dense_gemv_bf16_batchm on this target -- M>1 sites fall \
969                 back to the tile GEMM (measured 3.6x slower at M<=8)"
970            );
971        });
972    }
973    if (2..=DENSE_GEMV_BATCHM_MAX_M as usize).contains(&m) && k.batchm.0 != 0 {
974        return KernelLaunch::new(gpu, k.batchm)
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(m as u32)
981            .arg_u32(n as u32)
982            .arg_u32(kk as u32)
983            // Contiguous `[M, N]` output — the layout `dense_gemm_bf16` writes.
984            .arg_u32(n as u32)
985            .launch(stream);
986    }
987    const GEMM_TILE: u32 = 16;
988    KernelLaunch::new(gpu, k.gemm)
989        .grid([
990            (n as u32).div_ceil(GEMM_TILE),
991            (m as u32).div_ceil(GEMM_TILE),
992            1,
993        ])
994        .block([GEMM_TILE, GEMM_TILE, 1])
995        .arg_ptr(a)
996        .arg_ptr(b)
997        .arg_ptr(c)
998        .arg_u32(m as u32)
999        .arg_u32(n as u32)
1000        .arg_u32(kk as u32)
1001        .launch(stream)
1002}