spark_model/layers/ops/
gemm_fp8_prefill.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! FP8 prefill launchers: NVFP4->FP8 weight pre-dequant, BF16->FP8 activation
4//! cast, and the FP8-weight GEMMs. Split from `gemm_dense.rs` (500-LoC cap).
5
6#![allow(unused_imports)]
7
8use anyhow::Result;
9use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
10use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
11
12use crate::weight_map::{Fp8DenseWeight, QuantizedWeight};
13
14use super::*;
15
16/// Pre-dequanted FP8 GEMM (prefill): C = A @ B_fp8.
17///
18/// A: [M, K] BF16, B_fp8: [N, K] FP8 E4M3 (pre-dequanted from NVFP4), C: [M, N] BF16.
19/// Eliminates runtime NVFP4→FP8 dequant — only LOAD + FP8 MMA per K step.
20///
21/// Grid: (ceil(N/128), ceil(M/64), 1)  Block: (128, 1, 1)
22#[allow(clippy::too_many_arguments)]
23pub fn fp8_gemm_n128(
24    gpu: &dyn GpuBackend,
25    kernel: KernelHandle,
26    input: DevicePtr,
27    b_fp8: DevicePtr,
28    output: DevicePtr,
29    m: u32,
30    n: u32,
31    k: u32,
32    stream: u64,
33) -> Result<()> {
34    // DEFAULT-ON: route the GDN-projection prefill GEMM through the ldmatrix.x4
35    // A+B kernel (fp8_fp8_gemm_ldmab). ncu-proven 2.1x over the scalar-load
36    // fp8_gemm_t, cosine 1.000000 vs fp8_fp8_gemm_t, and a confirmed e2e warm-TTFT
37    // win. Quantizes the bf16 activation to e4m3 once into a persistent scratch,
38    // then launches the ldmatrix GEMM. K must be a multiple of 32 (the ldmab
39    // K-tile). Opt-OUT with ATLAS_FP8_LDMAB=0 (falls through to the scalar path).
40    if k.is_multiple_of(32) && std::env::var("ATLAS_FP8_LDMAB").as_deref() != Ok("0") {
41        // Handles and scratch live on the backend, not in statics: the
42        // handles point into this model's registry modules (unloaded when it
43        // drops) and the scratch is an allocation in this model's context.
44        // Cached process-wide, the next model would launch a kernel from an
45        // unloaded module and write activations through a freed pointer.
46        let cache = gpu.op_cache();
47        let qk = cache.kernel(gpu, "w4a16", "bf16_to_fp8")?;
48        let lk = cache.kernel(gpu, "w4a16_fp8_ldmab", "fp8_fp8_gemm_ldmab")?;
49        let need = (m as usize) * (k as usize); // e4m3 bytes
50        let a8 = cache.scratch(gpu, "fp8_prefill_activation", need)?;
51        bf16_to_fp8(gpu, qk, input, a8, m * k, stream)?;
52        return KernelLaunch::new(gpu, lk)
53            .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
54            .block([256, 1, 1])
55            .arg_ptr(a8)
56            .arg_ptr(b_fp8)
57            .arg_ptr(output)
58            .arg_u32(m)
59            .arg_u32(n)
60            .arg_u32(k)
61            .launch(stream);
62    }
63    KernelLaunch::new(gpu, kernel)
64        .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
65        .block([128, 1, 1])
66        .arg_ptr(input)
67        .arg_ptr(b_fp8)
68        .arg_ptr(output)
69        .arg_u32(m)
70        .arg_u32(n)
71        .arg_u32(k)
72        .launch(stream)
73}
74
75/// Pre-dequant NVFP4 → FP8 E4M3.  One-time conversion at model load.
76///
77/// Reads B_packed[N, K/2] + B_scale[N, K/GROUP_SIZE] + scale2 → B_fp8[N, K].
78///
79/// Grid: (ceil(N*K/2 / 256), 1, 1)  Block: (256, 1, 1)
80#[allow(clippy::too_many_arguments)]
81/// `fp8_gemm_t_mfast`: same GEMM as [`fp8_gemm_n128`] with the CTA grid axes
82/// swapped so M is the fast axis. The M-blocks that share a B panel then run
83/// co-resident and read it from L2 instead of DRAM; see the kernel comment.
84pub fn fp8_gemm_n128_mfast(
85    gpu: &dyn GpuBackend,
86    kernel: KernelHandle,
87    input: DevicePtr,
88    b_fp8: DevicePtr,
89    output: DevicePtr,
90    m: u32,
91    n: u32,
92    k: u32,
93    stream: u64,
94) -> Result<()> {
95    KernelLaunch::new(gpu, kernel)
96        .grid([div_ceil(m, 64), div_ceil(n, 128), 1])
97        .block([128, 1, 1])
98        .arg_ptr(input)
99        .arg_ptr(b_fp8)
100        .arg_ptr(output)
101        .arg_u32(m)
102        .arg_u32(n)
103        .arg_u32(k)
104        .launch(stream)
105}
106
107/// `fp8_gemm_t_m128_mfast`: 128-row M tile (2 chunks/CTA), m on the fast axis.
108/// Halves the B panel passes relative to [`fp8_gemm_n128_mfast`].
109pub fn fp8_gemm_m128_mfast(
110    gpu: &dyn GpuBackend,
111    kernel: KernelHandle,
112    input: DevicePtr,
113    b_fp8: DevicePtr,
114    output: DevicePtr,
115    m: u32,
116    n: u32,
117    k: u32,
118    stream: u64,
119) -> Result<()> {
120    KernelLaunch::new(gpu, kernel)
121        .grid([div_ceil(m, 128), div_ceil(n, 128), 1])
122        .block([128, 1, 1])
123        .arg_ptr(input)
124        .arg_ptr(b_fp8)
125        .arg_ptr(output)
126        .arg_u32(m)
127        .arg_u32(n)
128        .arg_u32(k)
129        .launch(stream)
130}
131
132/// `fp8_fp8_gemm_t_m128_mfast`: FP8 A x FP8 B, 128-row M tile, m on the fast
133/// axis. A must already be E4M3 (see `bf16_to_fp8`); the MMA consumed E4M3
134/// either way, so pre-casting A is numerically identical to the BF16-A kernel.
135pub fn fp8_fp8_gemm_m128_mfast(
136    gpu: &dyn GpuBackend,
137    kernel: KernelHandle,
138    input: DevicePtr,
139    b_fp8: DevicePtr,
140    output: DevicePtr,
141    m: u32,
142    n: u32,
143    k: u32,
144    stream: u64,
145) -> Result<()> {
146    KernelLaunch::new(gpu, kernel)
147        .grid([div_ceil(m, 128), div_ceil(n, 128), 1])
148        .block([128, 1, 1])
149        .arg_ptr(input)
150        .arg_ptr(b_fp8)
151        .arg_ptr(output)
152        .arg_u32(m)
153        .arg_u32(n)
154        .arg_u32(k)
155        .launch(stream)
156}
157
158pub fn predequant_nvfp4_to_fp8(
159    gpu: &dyn GpuBackend,
160    kernel: KernelHandle,
161    b_packed: DevicePtr,
162    b_scale: DevicePtr,
163    scale2: f32,
164    b_fp8: DevicePtr,
165    n: u32,
166    k: u32,
167    stream: u64,
168) -> Result<()> {
169    let total = n * k / 2;
170    KernelLaunch::new(gpu, kernel)
171        .grid([div_ceil(total, 256), 1, 1])
172        .block([256, 1, 1])
173        .arg_ptr(b_packed)
174        .arg_ptr(b_scale)
175        .arg_f32(scale2)
176        .arg_ptr(b_fp8)
177        .arg_u32(n)
178        .arg_u32(k)
179        .launch(stream)
180}
181
182/// Convert BF16 activations to FP8 E4M3 for FP8×FP8 GEMM.
183///
184/// Grid: (ceil(total_elements/2 / 256), 1, 1)  Block: (256, 1, 1)
185pub fn bf16_to_fp8(
186    gpu: &dyn GpuBackend,
187    kernel: KernelHandle,
188    src: DevicePtr,
189    dst: DevicePtr,
190    total_elements: u32,
191    stream: u64,
192) -> Result<()> {
193    let threads_needed = total_elements / 2;
194    KernelLaunch::new(gpu, kernel)
195        .grid([div_ceil(threads_needed, 256), 1, 1])
196        .block([256, 1, 1])
197        .arg_ptr(src)
198        .arg_ptr(dst)
199        .arg_u32(total_elements)
200        .launch(stream)
201}
202
203/// Quantize a BF16 weight matrix `[N, K]` to FP8 E4M3 `[N, K]` with per-row
204/// f32 scales `[N]`. One CTA per row, 256 threads — parallel absmax
205/// reduction over K, then per-element saturating cast to E4M3.
206///
207/// Called **once at model load time**, never on the decode hot path.
208///
209/// Phase G (DFlash drafter FP8): converts each BF16 q/k/v/o/gate/up/down
210/// weight at load time. Decode path then consumes the resulting
211/// `Fp8DenseWeight` via `fp8_gemm_n128`.
212///
213/// Kernel: `quantize_bf16_to_fp8(input, output, row_scales, N, K)` —
214/// `kernels/gb10/common/dense_gemv_fp8w.cu:36`.
215/// Grid: (N, 1, 1)  Block: (256, 1, 1)
216#[allow(clippy::too_many_arguments)]
217pub fn quantize_bf16_to_fp8(
218    gpu: &dyn GpuBackend,
219    kernel: KernelHandle,
220    input: DevicePtr,
221    output: DevicePtr,
222    row_scales: DevicePtr,
223    n: u32,
224    k: u32,
225    stream: u64,
226) -> Result<()> {
227    KernelLaunch::new(gpu, kernel)
228        .grid([n, 1, 1])
229        .block([256, 1, 1])
230        .arg_ptr(input)
231        .arg_ptr(output)
232        .arg_ptr(row_scales)
233        .arg_u32(n)
234        .arg_u32(k)
235        .launch(stream)
236}
237
238/// Small-M row-scaled FP8 GEMM (M ≤ 16) — single warp per CTA variant.
239///
240/// Same math as [`fp8_gemm_n128_row_scaled`] but M_TILE=16 instead of 64,
241/// so all M rows are valid (no wasted MMA cycles on bounds-checked rows).
242/// Uses 32 threads per CTA (1 warp) instead of 128, so 4× fewer threads
243/// for the same useful work. Critical for the DFlash drafter lm_head
244/// where M=γ=16 vs N=vocab_size=248320.
245///
246/// Kernel: `fp8_gemm_t_row_scaled_m16(A, B_fp8, row_scale, C, M, N, K)`.
247/// Grid: (ceil(N/128), 1, 1)  Block: (32, 1, 1)
248#[allow(clippy::too_many_arguments)]
249pub fn fp8_gemm_n128_row_scaled_m16(
250    gpu: &dyn GpuBackend,
251    kernel: KernelHandle,
252    input: DevicePtr,
253    weight: &Fp8DenseWeight,
254    output: DevicePtr,
255    m: u32,
256    n: u32,
257    k: u32,
258    stream: u64,
259) -> Result<()> {
260    KernelLaunch::new(gpu, kernel)
261        .grid([div_ceil(n, 128), 1, 1])
262        .block([32, 1, 1])
263        .arg_ptr(input)
264        .arg_ptr(weight.weight)
265        .arg_ptr(weight.row_scale)
266        .arg_ptr(output)
267        .arg_u32(m)
268        .arg_u32(n)
269        .arg_u32(k)
270        .launch(stream)
271}
272
273/// Row-scaled FP8 GEMM: `C[M, N] = A[M, K] @ (dequant(B_fp8[N, K]) * row_scale[N])`.
274///
275/// Same tiling and FP8 MMA as `fp8_gemm_n128` (BF16 × FP8 → BF16), with a
276/// per-column scale multiply before the BF16 write-out. Consumes the
277/// `Fp8DenseWeight` produced by [`crate::weight_map::DenseWeight::quantize_to_fp8`]
278/// — the per-row scale on `Fp8DenseWeight` matches the kernel's
279/// `row_scale` parameter.
280///
281/// Phase G (DFlash drafter FP8) hot-path GEMM. Replaces `dense_gemm` on
282/// the seven dense-GEMM call sites in `forward_block_layer_pre_attn` /
283/// `_post_attn` when `self.quant == DflashQuantization::Fp8Weights`.
284///
285/// Kernel: `fp8_gemm_t_row_scaled(A, B_fp8, row_scale, C, M, N, K)` —
286/// `kernels/gb10/qwen3.6-27b/nvfp4/w4a16_gemm.cu`.
287/// Grid: (ceil(N/128), ceil(M/64), 1)  Block: (128, 1, 1)
288#[allow(clippy::too_many_arguments)]
289pub fn fp8_gemm_n128_row_scaled(
290    gpu: &dyn GpuBackend,
291    kernel: KernelHandle,
292    input: DevicePtr,
293    weight: &Fp8DenseWeight,
294    output: DevicePtr,
295    m: u32,
296    n: u32,
297    k: u32,
298    stream: u64,
299) -> Result<()> {
300    KernelLaunch::new(gpu, kernel)
301        .grid([div_ceil(n, 128), div_ceil(m, 64), 1])
302        .block([128, 1, 1])
303        .arg_ptr(input)
304        .arg_ptr(weight.weight)
305        .arg_ptr(weight.row_scale)
306        .arg_ptr(output)
307        .arg_u32(m)
308        .arg_u32(n)
309        .arg_u32(k)
310        .launch(stream)
311}