spark_model/layers/ops/
gemm_dense.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;
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/// Dense BF16 GEMM: C = A @ B^T.
17///
18/// A: [M, K] row-major (activations)
19/// B: [N, K] row-major (weights, HuggingFace layout)
20/// C: [M, N] row-major (output)
21///
22/// Kernel: `dense_gemm_bf16(A, B, C, M, N, K)`
23/// Grid: (ceil(N/16), ceil(M/16), 1)  Block: (16, 16, 1)
24/// Tensor-core BF16 GEMM: m16n8k16 MMA for 3-5x speedup over scalar.
25/// Grid: (ceil(N/64), ceil(M/16), 1), Block: (128, 1, 1)
26pub fn dense_gemm_tc(
27    gpu: &dyn GpuBackend,
28    kernel: KernelHandle,
29    input: DevicePtr,
30    weight: &DenseWeight,
31    output: DevicePtr,
32    m: u32,
33    n: u32,
34    k: u32,
35    stream: u64,
36) -> Result<()> {
37    KernelLaunch::new(gpu, kernel)
38        .grid([div_ceil(n, 64), div_ceil(m, 16), 1])
39        .block([128, 1, 1])
40        .arg_ptr(input)
41        .arg_ptr(weight.weight)
42        .arg_ptr(output)
43        .arg_u32(m)
44        .arg_u32(n)
45        .arg_u32(k)
46        .launch(stream)
47}
48
49/// `output[m, n] += scale * bf16(input[m, k] @ weight[n, k]^T)` in ONE pass.
50///
51/// The fused epilogue of [`dense_gemm_tc`], for LoRA's expand+fold. The
52/// unfused pair (GEMM into scratch, then `scaled_add`) writes an [m, n]
53/// tensor, reads it back, and read-modify-writes the destination; this does
54/// the last of those only. On a 27B prefill with n = intermediate = 17408
55/// that scratch round-trip dominated — it measured as a 5.6x prefill
56/// slowdown with a LoRA adapter resident.
57///
58/// BIT-IDENTICAL to the unfused pair: the kernel rounds the delta to BF16
59/// before applying `scale`, exactly as storing to a BF16 scratch and running
60/// `bf16_scaled_add` over it did.
61#[allow(clippy::too_many_arguments)]
62pub fn dense_gemm_tc_scaled_acc(
63    gpu: &dyn GpuBackend,
64    kernel: KernelHandle,
65    input: DevicePtr,
66    weight: &DenseWeight,
67    output: DevicePtr,
68    m: u32,
69    n: u32,
70    k: u32,
71    scale: f32,
72    stream: u64,
73) -> Result<()> {
74    KernelLaunch::new(gpu, kernel)
75        .grid([div_ceil(n, 64), div_ceil(m, 16), 1])
76        .block([128, 1, 1])
77        .arg_ptr(input)
78        .arg_ptr(weight.weight)
79        .arg_ptr(output)
80        .arg_u32(m)
81        .arg_u32(n)
82        .arg_u32(k)
83        .arg_f32(scale)
84        .launch(stream)
85}
86
87/// Split-K GEMM: partial products over K_splits chunks, then reduce.
88/// Uses FP32 workspace of size K_splits * M * N * 4 bytes.
89#[allow(clippy::too_many_arguments)]
90pub fn dense_gemm_splitk(
91    gpu: &dyn GpuBackend,
92    partial_kernel: KernelHandle,
93    reduce_kernel: KernelHandle,
94    input: DevicePtr,
95    weight: &DenseWeight,
96    output: DevicePtr,
97    workspace: DevicePtr,
98    m: u32,
99    n: u32,
100    k: u32,
101    k_splits: u32,
102    stream: u64,
103) -> Result<()> {
104    // Phase 1: partial products
105    KernelLaunch::new(gpu, partial_kernel)
106        .grid([div_ceil(n, 16), div_ceil(m, 16), k_splits])
107        .block([16, 16, 1])
108        .arg_ptr(input)
109        .arg_ptr(weight.weight)
110        .arg_ptr(workspace)
111        .arg_u32(m)
112        .arg_u32(n)
113        .arg_u32(k)
114        .arg_u32(k_splits)
115        .launch(stream)?;
116    // Phase 2: reduce and write BF16
117    KernelLaunch::new(gpu, reduce_kernel)
118        .grid([div_ceil(n, 256), m, 1])
119        .block([256, 1, 1])
120        .arg_ptr(workspace)
121        .arg_ptr(output)
122        .arg_u32(m)
123        .arg_u32(n)
124        .arg_u32(k_splits)
125        .launch(stream)
126}
127
128pub fn dense_gemm(
129    gpu: &dyn GpuBackend,
130    kernel: KernelHandle,
131    input: DevicePtr,
132    weight: &DenseWeight,
133    output: DevicePtr,
134    m: u32,
135    n: u32,
136    k: u32,
137    stream: u64,
138) -> Result<()> {
139    KernelLaunch::new(gpu, kernel)
140        .grid([div_ceil(n, 16), div_ceil(m, 16), 1])
141        .block([16, 16, 1])
142        .arg_ptr(input)
143        .arg_ptr(weight.weight)
144        .arg_ptr(output)
145        .arg_u32(m)
146        .arg_u32(n)
147        .arg_u32(k)
148        .launch(stream)
149}
150
151/// Order-preserving register-blocked BF16 GEMM (kernel `dense_gemm_bf16_router`).
152///
153/// Same math AND the same per-output FP32 accumulation order (strict
154/// k = 0..K-1) as the scalar `dense_gemm` — bit-identical output under the
155/// kernel dir's `--fmad=false` build (verified 0 differing elements at the
156/// router shapes M=4510/M=2255, `[M,2048]x[2048,256]`) — at ~2x the speed via
157/// register blocking + vectorized smem staging. This is the ONLY fast GEMM
158/// that satisfies the 2026-08-12 router-numerics pin (see
159/// `router_gate_gemm_dense`); tensor-core kernels reassociate and stay
160/// forbidden there.
161///
162/// Grid: (ceil(N/64), ceil(M/16), 1)  Block: (16, 16, 1)
163pub fn dense_gemm_router(
164    gpu: &dyn GpuBackend,
165    kernel: KernelHandle,
166    input: DevicePtr,
167    weight: &DenseWeight,
168    output: DevicePtr,
169    m: u32,
170    n: u32,
171    k: u32,
172    stream: u64,
173) -> Result<()> {
174    KernelLaunch::new(gpu, kernel)
175        .grid([div_ceil(n, 64), div_ceil(m, 16), 1])
176        .block([16, 16, 1])
177        .arg_ptr(input)
178        .arg_ptr(weight.weight)
179        .arg_ptr(output)
180        .arg_u32(m)
181        .arg_u32(n)
182        .arg_u32(k)
183        .launch(stream)
184}
185
186/// Pipelined tensor-core BF16 GEMM — drop-in faster `dense_gemm` (kernel
187/// `dense_gemm_bf16_pipelined`): mma.sync.m16n8k16 + cp.async 2-stage, 128x128
188/// tile. ~40x the scalar `dense_gemm` on large-M shapes (cosine=1.0, same math).
189/// Grid: (ceil(N/128), ceil(M/128), 1)  Block: (256, 1, 1)
190#[allow(clippy::too_many_arguments)]
191pub fn dense_gemm_bf16_pipelined(
192    gpu: &dyn GpuBackend,
193    kernel: KernelHandle,
194    input: DevicePtr,
195    weight: &DenseWeight,
196    output: DevicePtr,
197    m: u32,
198    n: u32,
199    k: u32,
200    stream: u64,
201) -> Result<()> {
202    KernelLaunch::new(gpu, kernel)
203        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
204        .block([256, 1, 1])
205        .arg_ptr(input)
206        .arg_ptr(weight.weight)
207        .arg_ptr(output)
208        .arg_u32(m)
209        .arg_u32(n)
210        .arg_u32(k)
211        .launch(stream)
212}
213
214/// Dense BF16 prefill GEMM. Prefer the pipelined tensor-core kernel when the
215/// selected target ships it, and retain the scalar kernel as an explicit
216/// compatibility fallback for older targets.
217#[allow(clippy::too_many_arguments)]
218pub fn dense_gemm_prefill(
219    gpu: &dyn GpuBackend,
220    fallback_kernel: KernelHandle,
221    pipelined_kernel: KernelHandle,
222    input: DevicePtr,
223    weight: &DenseWeight,
224    output: DevicePtr,
225    m: u32,
226    n: u32,
227    k: u32,
228    stream: u64,
229) -> Result<()> {
230    if pipelined_kernel.0 != 0 {
231        dense_gemm_bf16_pipelined(
232            gpu,
233            pipelined_kernel,
234            input,
235            weight,
236            output,
237            m,
238            n,
239            k,
240            stream,
241        )
242    } else {
243        dense_gemm(gpu, fallback_kernel, input, weight, output, m, n, k, stream)
244    }
245}
246
247/// W4A16 GEMM: C = A @ dequant(B).
248///
249/// A: [M, K] BF16 activations
250/// B: NVFP4 packed weights (E2M1 + FP8 scales + FP32 per-tensor scale)
251/// C: [M, N] BF16 output
252///
253/// Kernel: `w4a16_gemm(A, B_packed, B_scale, scale2, C, M, N, K)`
254/// Grid: (ceil(N/64), ceil(M/64), 1)  Block: (128, 1, 1)
255///
256/// Also the launcher for `w4a16_gemm_t_k64_n64_p3` — the deep-K twin carries
257/// the same 64-wide N tile and the identical argument list, so the two share
258/// this grid rather than duplicating it.
259pub fn w4a16_gemm(
260    gpu: &dyn GpuBackend,
261    kernel: KernelHandle,
262    input: DevicePtr,
263    weight: &QuantizedWeight,
264    output: DevicePtr,
265    m: u32,
266    n: u32,
267    k: u32,
268    stream: u64,
269) -> Result<()> {
270    KernelLaunch::new(gpu, kernel)
271        .grid([div_ceil(n, 64), div_ceil(m, 64), 1])
272        .block([128, 1, 1])
273        .arg_ptr(input)
274        .arg_ptr(weight.weight)
275        .arg_ptr(weight.weight_scale)
276        .arg_f32(weight.weight_scale_2)
277        .arg_ptr(output)
278        .arg_u32(m)
279        .arg_u32(n)
280        .arg_u32(k)
281        .launch(stream)
282}
283
284/// W4A16 GEMM with N_TILE=128: same kernel signature, wider N tile.
285///
286/// Grid: (ceil(N/128), ceil(M/64), 1)  Block: (128, 1, 1)
287#[allow(clippy::too_many_arguments)]
288/// `w4a16_gemm_n128` with an explicit transposed-B ROW STRIDE.
289///
290/// Needed when N is not a multiple of 16: the kernel's B loads are 16-byte
291/// `cp.async`, which requires 16-byte-aligned sources, and row r sits at
292/// `r * ldb`. lm_head is the motivating case — its N is the vocab size, 248077
293/// on this checkpoint, which is ODD and made 15 of every 16 k-rows fault with
294/// CUDA_ERROR_MISALIGNED_ADDRESS (the campaign's long-standing "716").
295/// Pass `ldb = align_up(n, 128)` with the pad columns zero-filled.
296pub fn w4a16_gemm_n128_ldb(
297    gpu: &dyn GpuBackend,
298    kernel: KernelHandle,
299    input: DevicePtr,
300    weight: &QuantizedWeight,
301    output: DevicePtr,
302    m: u32,
303    n: u32,
304    k: u32,
305    ldb: u32,
306    stream: u64,
307) -> Result<()> {
308    KernelLaunch::new(gpu, kernel)
309        .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
310        .block([128, 1, 1])
311        .arg_ptr(input)
312        .arg_ptr(weight.weight)
313        .arg_ptr(weight.weight_scale)
314        .arg_f32(weight.weight_scale_2)
315        .arg_ptr(output)
316        .arg_u32(m)
317        .arg_u32(n)
318        .arg_u32(k)
319        .arg_u32(ldb)
320        .launch(stream)
321}
322
323pub fn w4a16_gemm_n128(
324    gpu: &dyn GpuBackend,
325    kernel: KernelHandle,
326    input: DevicePtr,
327    weight: &QuantizedWeight,
328    output: DevicePtr,
329    m: u32,
330    n: u32,
331    k: u32,
332    stream: u64,
333) -> Result<()> {
334    // Packed case: the transposed B rows are exactly N apart.
335    w4a16_gemm_n128_ldb(gpu, kernel, input, weight, output, m, n, k, n, stream)
336}
337
338/// W4A16 GEMM v3: MiniMax-only shadow with K_STEP=64 (was 32 in v2).
339/// Halves K-iteration count; doubles per-iter MMA count. 1 CTA/SM
340/// (was 3 for v2) due to larger SMEM footprint.
341#[allow(clippy::too_many_arguments)]
342pub fn w4a16_gemm_n128_m128_v3(
343    gpu: &dyn GpuBackend,
344    kernel: KernelHandle,
345    input: DevicePtr,
346    weight: &QuantizedWeight,
347    output: DevicePtr,
348    m: u32,
349    n: u32,
350    k: u32,
351    stream: u64,
352) -> Result<()> {
353    KernelLaunch::new(gpu, kernel)
354        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
355        .block([256, 1, 1])
356        .arg_ptr(input)
357        .arg_ptr(weight.weight)
358        .arg_ptr(weight.weight_scale)
359        .arg_f32(weight.weight_scale_2)
360        .arg_ptr(output)
361        .arg_u32(m)
362        .arg_u32(n)
363        .arg_u32(k)
364        .launch(stream)
365}
366
367/// W4A16 GEMM v2: shadow of `w4a16_gemm_n128_m128` (minimax, step3p7, and —
368/// since the 27B port — qwen3.6-27b).
369///
370/// Same CTA tile (M=128, N=128, K_STEP=32) but:
371///   - blockDim 256 (8 warps) instead of 128 (4 warps)
372///   - Chunk 0 (rows 0-63) and chunk 1 (rows 64-127) MMAs run in parallel
373///     across warps 0-3 and 4-7 instead of being serialized.
374///
375/// Grid: (ceil(N/128), ceil(M/128), 1)  Block: (256, 1, 1)
376/// SMEM: 30,336 B/CTA (2-stage pipeline, padded B_fp8 rows) → 3 CTAs/SM, same
377/// footprint as v1 — 768 resident threads/SM vs v1's 384. (An earlier version
378/// of this doc claimed 3-stage/42.6 KB/2 CTAs — that described a prototype,
379/// not the shipped kernel.)
380#[allow(clippy::too_many_arguments)]
381pub fn w4a16_gemm_n128_m128_v2(
382    gpu: &dyn GpuBackend,
383    kernel: KernelHandle,
384    input: DevicePtr,
385    weight: &QuantizedWeight,
386    output: DevicePtr,
387    m: u32,
388    n: u32,
389    k: u32,
390    stream: u64,
391) -> Result<()> {
392    KernelLaunch::new(gpu, kernel)
393        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
394        .block([256, 1, 1])
395        .arg_ptr(input)
396        .arg_ptr(weight.weight)
397        .arg_ptr(weight.weight_scale)
398        .arg_f32(weight.weight_scale_2)
399        .arg_ptr(output)
400        .arg_u32(m)
401        .arg_u32(n)
402        .arg_u32(k)
403        .launch(stream)
404}
405
406/// W4A16 GEMM: C = A @ B with 2-M-chunk CTA (M_TILE2=128).
407///
408/// Halves weight re-reads vs `w4a16_gemm_n128` for large M (ISL > 128):
409/// each CTA covers 128 rows of A, loading B once for both 64-row halves.
410/// ~2× speedup on qkvz (K=2048, N=12288) at ISL=1016.
411///
412/// Grid: (ceil(N/128), ceil(M/128), 1)  Block: (128, 1, 1)
413/// SMEM: ~29.8 KB → 3 blocks/SM (vs 5 for m64 at ~19.6 KB).
414///
415/// GRID CONTRACT — N is the FAST axis (blockIdx.x = N-block, blockIdx.y = M-block).
416/// Every `w4a16_gemm_t_m128` kernel across all model dirs reads it this way. This
417/// launcher is SHARED (qwen3_attention, dense_ffn, qwen3_ssm, nemotron_*), so the
418/// axes must NOT be swapped here to suit one model: doing so silently mis-maps every
419/// CTA for the other 18 kernels and produces garbage output with no error. If a model
420/// wants the m-fast (L2-friendly) order, add a SEPARATELY NAMED kernel + launcher
421/// (see `w4a4_gemm_mfast` / `fp8_gemm_t_m128_mfast`) rather than mutating this one.
422#[allow(clippy::too_many_arguments)]
423pub fn w4a16_gemm_n128_m128(
424    gpu: &dyn GpuBackend,
425    kernel: KernelHandle,
426    input: DevicePtr,
427    weight: &QuantizedWeight,
428    output: DevicePtr,
429    m: u32,
430    n: u32,
431    k: u32,
432    stream: u64,
433) -> Result<()> {
434    KernelLaunch::new(gpu, kernel)
435        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
436        .block([128, 1, 1])
437        .arg_ptr(input)
438        .arg_ptr(weight.weight)
439        .arg_ptr(weight.weight_scale)
440        .arg_f32(weight.weight_scale_2)
441        .arg_ptr(output)
442        .arg_u32(m)
443        .arg_u32(n)
444        .arg_u32(k)
445        .launch(stream)
446}
447
448/// W4A16 GEMM — LOSSLESS BF16 prefill variant of `w4a16_gemm_n128_m128`.
449///
450/// Identical launch config (grid/block/SMEM, M_TILE2=128) and weight layout
451/// (transposed NVFP4) to `w4a16_gemm_n128_m128`, but launches the
452/// `w4a16_gemm_t_m128_bf16` kernel: FP4→BF16 dequant + BF16 m16n8k16 MMA
453/// (FP32 accum), i.e. the base `w4a16_gemm` math at the fast 128x128 tiling.
454/// Unlike the default `t_m128` (which crushes weights+acts to FP8 E4M3 on
455/// NVIDIA), this preserves prefill outputs bit-for-bit vs the base kernel.
456///
457/// Grid: (ceil(N/128), ceil(M/128), 1)  Block: (128, 1, 1)
458#[allow(clippy::too_many_arguments)]
459/// `w4a16_gemm_n128_m128_bf16` with an explicit transposed-B row stride, for the
460/// LOSSLESS BF16-MMA path. Needed for the same reason as `w4a16_gemm_n128_ldb`:
461/// the B loads are 16-byte `cp.async` and lm_head's N is the vocab size (248077,
462/// odd), so an unpadded stride misaligns 15 of every 16 k-rows.
463pub fn w4a16_gemm_n128_m128_bf16_ldb(
464    gpu: &dyn GpuBackend,
465    kernel: KernelHandle,
466    input: DevicePtr,
467    weight: &QuantizedWeight,
468    output: DevicePtr,
469    m: u32,
470    n: u32,
471    k: u32,
472    ldb: u32,
473    stream: u64,
474) -> Result<()> {
475    KernelLaunch::new(gpu, kernel)
476        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
477        .block([128, 1, 1])
478        .arg_ptr(input)
479        .arg_ptr(weight.weight)
480        .arg_ptr(weight.weight_scale)
481        .arg_f32(weight.weight_scale_2)
482        .arg_ptr(output)
483        .arg_u32(m)
484        .arg_u32(n)
485        .arg_u32(k)
486        .arg_u32(ldb)
487        .launch(stream)
488}
489
490/// 8-arg launcher for `w4a16_gemm_t_m128_bf16` (v1) ONLY. The `_v2` sibling's
491/// compiled signature has a 9th `ldb` param — launching it through this helper
492/// makes cuLaunchKernel read one-past-the-end of the param array (observed as
493/// CUDA_ERROR_INVALID_VALUE or a host SIGSEGV depending on the neighboring
494/// heap word). Launch v2 via `w4a16_gemm_n128_m128_bf16_ldb` (ldb = N when the
495/// transposed twin is unpadded).
496pub fn w4a16_gemm_n128_m128_bf16(
497    gpu: &dyn GpuBackend,
498    kernel: KernelHandle,
499    input: DevicePtr,
500    weight: &QuantizedWeight,
501    output: DevicePtr,
502    m: u32,
503    n: u32,
504    k: u32,
505    stream: u64,
506) -> Result<()> {
507    KernelLaunch::new(gpu, kernel)
508        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
509        .block([128, 1, 1])
510        .arg_ptr(input)
511        .arg_ptr(weight.weight)
512        .arg_ptr(weight.weight_scale)
513        .arg_f32(weight.weight_scale_2)
514        .arg_ptr(output)
515        .arg_u32(m)
516        .arg_u32(n)
517        .arg_u32(k)
518        .launch(stream)
519}
520
521#[cfg(test)]
522#[path = "gemm_dense_tests.rs"]
523mod gemm_dense_tests;