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;