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}