spark_model/layers/ops/
gemm_dense_int8.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! int8 W4A8 prefill GEMM wrappers, extracted piecewise from `gemm_dense.rs`
4//! (500-LoC cap): one-time NVFP4→int8 weight requant + the faith2 two-launch
5//! prefill (activation requant → int8×int8 block-scaled GEMM).
6
7use anyhow::Result;
8use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
9use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
10
11/// Requant an NVFP4 weight (packed E2M1 + per-16 E4M3 block scales + per-tensor
12/// `scale2`) into an int8 weight + per-32 F32 block scale, for the int8 W4A8
13/// prefill GEMM (`int8_gemm_faith2`). One-time conversion per weight at load (or
14/// lazily on first int8 prefill).
15///
16/// Reads `W_packed[N, K/2]`, `W_e4m3[N, K/16]`, `scale2` → `W_i8[N, K]` (signed
17/// int8) + `W_scale[N, K/32]` (F32). The per-16 NVFP4 scales are re-blocked to
18/// per-32 int8 scales by the kernel.
19///
20/// Grid: (ceil(N*(K/32) / 128), 1, 1)  Block: (128, 1, 1)
21#[allow(clippy::too_many_arguments)]
22pub fn requant_w_nvfp4_int8(
23    gpu: &dyn GpuBackend,
24    kernel: KernelHandle,
25    w_packed: DevicePtr,
26    w_e4m3: DevicePtr,
27    scale2: f32,
28    w_i8: DevicePtr,
29    w_scale: DevicePtr,
30    n: u32,
31    k: u32,
32    stream: u64,
33) -> Result<()> {
34    let blocks = n * (k / 32);
35    KernelLaunch::new(gpu, kernel)
36        .grid([div_ceil(blocks, 128), 1, 1])
37        .block([128, 1, 1])
38        .arg_ptr(w_packed)
39        .arg_ptr(w_e4m3)
40        .arg_f32(scale2)
41        .arg_ptr(w_i8)
42        .arg_ptr(w_scale)
43        .arg_u32(n)
44        .arg_u32(k)
45        .launch(stream)
46}
47
48/// int8 W4A8 prefill GEMM: requant BF16 activations to int8 (per-32 F32 scale)
49/// then `C = (A_i8 * W_i8)` folded with per-32 A/W block scales via
50/// `int8_gemm_faith2`. The weight is already int8 (see `requant_w_nvfp4_int8`).
51///
52/// A_bf16: [M, K] BF16 activations. W_i8: [N, K] int8 weights. W_scale: [N, K/32]
53/// F32. `a_i8_scratch` / `a_scale_scratch` are caller-owned scratch buffers of at
54/// least `M*K` bytes and `M*(K/32)*4` bytes respectively. Out: [M, N] BF16.
55///
56/// Two launches on `stream` (stream-ordered): requant_a → faith2.
57///   requant_a grid: (ceil(M*(K/32) / 128), 1, 1)  block: (128, 1, 1)
58///   faith2    grid: (ceil(N/128), ceil(M/128), 1)  block: (256, 1, 1)
59#[allow(clippy::too_many_arguments)]
60pub fn int8_gemm_faith2_prefill(
61    gpu: &dyn GpuBackend,
62    faith2_kernel: KernelHandle,
63    requant_a_kernel: KernelHandle,
64    a_bf16: DevicePtr,
65    w_i8: DevicePtr,
66    w_scale: DevicePtr,
67    a_i8_scratch: DevicePtr,
68    a_scale_scratch: DevicePtr,
69    out: DevicePtr,
70    m: u32,
71    n: u32,
72    k: u32,
73    stream: u64,
74) -> Result<()> {
75    // (a) BF16 acts → int8 + per-32 F32 scale.
76    let a_blocks = m * (k / 32);
77    KernelLaunch::new(gpu, requant_a_kernel)
78        .grid([div_ceil(a_blocks, 128), 1, 1])
79        .block([128, 1, 1])
80        .arg_ptr(a_bf16)
81        .arg_ptr(a_i8_scratch)
82        .arg_ptr(a_scale_scratch)
83        .arg_u32(m)
84        .arg_u32(k)
85        .launch(stream)?;
86    // (b) int8 × int8 GEMM with per-32 block scales → BF16 out.
87    KernelLaunch::new(gpu, faith2_kernel)
88        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
89        .block([256, 1, 1])
90        .arg_ptr(a_i8_scratch)
91        .arg_ptr(w_i8)
92        .arg_ptr(a_scale_scratch)
93        .arg_ptr(w_scale)
94        .arg_ptr(out)
95        .arg_u32(m)
96        .arg_u32(n)
97        .arg_u32(k)
98        .launch(stream)
99}