spark_model/layers/ops/
kquant_mmq.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// provenance-id: 526f6e616c6420522e205374657369616b
3
4//! Host launches for the DeepSeek-V4.1 K-quant expert kernels (`kquant_moe` module,
5//! `kernels/gb10/deepseek-v4-flash/nvfp4/kquant_moe.cu`): raw Q2_K / Q3_K expert
6//! blocks consumed in place. Decode = `kquant_q8_1_rows` + `kquant_mmvq`; prefill =
7//! the MMQ activation quantiser in the type's q8_1 layout (launched through
8//! [`super::quantize_act_q8_1`] with the right kernel handle) + [`kquant_mmq_gemm`].
9
10use anyhow::Result;
11use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
12use spark_runtime::kernel_args::KernelLaunch;
13
14use super::q4k_mmq::QK_K;
15
16pub const KQUANT_MODULE: &str = "kquant_moe";
17/// Bytes per 256-value super-block on disk.
18pub const Q2K_BLOCK_BYTES: usize = 84;
19pub const Q3K_BLOCK_BYTES: usize = 110;
20pub const Q6K_BLOCK_BYTES: usize = 210;
21
22/// A resident `[N, K]` row-major projection as the GGUF path left it on the
23/// device: expanded bf16, or the raw `Q2_K` / `Q3_K` blocks
24/// (`WeightDtype::Q2K` / `Q3K`) for the K-quant GEMV (m <= 8, q8_1 rows) and
25/// MMQ (tensor cores, D2S6 / D4 tiles) kernels.
26#[derive(Clone, Copy, Debug)]
27pub enum ResidentMat {
28    Bf16(DevicePtr),
29    Q2K(DevicePtr),
30    Q3K(DevicePtr),
31}
32
33impl ResidentMat {
34    /// The sub-matrix starting `rows` rows in (each row `k` weights long).
35    pub fn at_rows(self, rows: usize, k: usize) -> ResidentMat {
36        let at = |p: DevicePtr, off: usize| DevicePtr(p.0 + off as u64);
37        match self {
38            ResidentMat::Bf16(p) => ResidentMat::Bf16(at(p, rows * k * 2)),
39            ResidentMat::Q2K(p) => ResidentMat::Q2K(at(p, rows * (k / 256) * Q2K_BLOCK_BYTES)),
40            ResidentMat::Q3K(p) => ResidentMat::Q3K(at(p, rows * (k / 256) * Q3K_BLOCK_BYTES)),
41        }
42    }
43}
44/// Dynamic shared memory for the 128x128 MMQ tile on the MMA path (`mmq_get_nbytes_shared`
45/// with MMQ_TILE_NE_K = 32): ids 512 B + tile_x (128 rows x tile_x_k ints) + tile_y (128 x 144 B).
46/// Q2_K tile_x_k = 2*32 + 32 + 4 = 100; Q3_K = 2*32 + 16 + 4 = 84.
47pub const Q2K_MMQ_SMEM: u32 = 512 + 128 * 100 * 4 + 128 * 144;
48pub const Q3K_MMQ_SMEM: u32 = 512 + 128 * 84 * 4 + 128 * 144;
49/// `block_q8_1`: 32 int8 + half2 (d, sum).
50pub const Q8_1_BLOCK_BYTES: usize = 36;
51
52fn div_ceil(a: u32, b: u32) -> u32 {
53    a.div_ceil(b)
54}
55
56/// Raw bytes of an `[n, k]` K-quant weight.
57pub fn kquant_weight_bytes(n: u32, k: u32, block_bytes: usize) -> usize {
58    (n as usize) * (k as usize / QK_K as usize) * block_bytes
59}
60
61/// Bytes of the plain `block_q8_1` buffer for `[m, k]` activations.
62pub fn kquant_q8_1_rows_bytes(m: u32, k: u32) -> usize {
63    (m as usize) * (k as usize / 32) * Q8_1_BLOCK_BYTES
64}
65
66/// Bytes of the MMQ-layout q8_1 buffer for `[m, k]` activations (what
67/// `quantize_act_q8_1` writes: 144 B per 128 values, rows padded to the 128 tile,
68/// k padded to 256).
69pub fn kquant_mmq_act_bytes(m: u32, k: u32) -> usize {
70    let mpad = div_ceil(m, 128) * 128;
71    let kpad = div_ceil(k, QK_K) * QK_K;
72    (mpad as usize) * (kpad as usize / 128) * 144
73}
74
75/// bf16 `[m, k]` -> `block_q8_1 [m, k/32]` (`kquant_q8_1_rows_bf16`). `k % 32 == 0`.
76pub fn kquant_q8_1_rows(
77    gpu: &dyn GpuBackend,
78    kernel: KernelHandle,
79    x_bf16: DevicePtr,
80    out_q8: DevicePtr,
81    m: u32,
82    k: u32,
83    stream: u64,
84) -> Result<()> {
85    anyhow::ensure!(
86        k.is_multiple_of(32),
87        "kquant_q8_1_rows: k={k} is not a multiple of 32"
88    );
89    let warps = m * (k / 32);
90    KernelLaunch::new(gpu, kernel)
91        .grid([div_ceil(warps * 32, 128), 1, 1])
92        .block([128, 1, 1])
93        .arg_ptr(x_bf16)
94        .arg_ptr(out_q8)
95        .arg_u32(k)
96        .arg_u32(m)
97        .launch(stream)
98}
99
100/// `moe_v41_swiglu` and [`kquant_q8_1_rows`] in one launch
101/// (`kquant_swiglu_q8_1_rows_bf16`): `h[rows, inter] = bf16(swiglu(gate, up) *
102/// w[row])`, then the q8_1 blocks of those bf16 rows; byte-identical to the two
103/// launches. `w` may be null. `inter % 32 == 0`.
104#[allow(clippy::too_many_arguments)]
105pub fn kquant_swiglu_q8_1_rows(
106    gpu: &dyn GpuBackend,
107    kernel: KernelHandle,
108    gate: DevicePtr,
109    up: DevicePtr,
110    w: DevicePtr,
111    h_bf16: DevicePtr,
112    out_q8: DevicePtr,
113    rows: u32,
114    inter: u32,
115    limit: f32,
116    stream: u64,
117) -> Result<()> {
118    anyhow::ensure!(
119        inter.is_multiple_of(32),
120        "kquant_swiglu_q8_1_rows: inter={inter} is not a multiple of 32"
121    );
122    let warps = rows * (inter / 32);
123    KernelLaunch::new(gpu, kernel)
124        .grid([div_ceil(warps * 32, 128), 1, 1])
125        .block([128, 1, 1])
126        .arg_ptr(gate)
127        .arg_ptr(up)
128        .arg_ptr(w)
129        .arg_ptr(h_bf16)
130        .arg_ptr(out_q8)
131        .arg_u32(rows)
132        .arg_u32(inter)
133        .arg_f32(limit)
134        .launch(stream)
135}
136
137/// Decode GEMV on raw blocks: `out[m][n] = sum_k xq8[m][k] * W[n][k]`, `m <= 8`
138/// (`kquant_mmvq_q2_k` / `kquant_mmvq_q3_k`). `w_blocks` is `[n][k/256]` raw
139/// super-blocks; `y_q8` is the `block_q8_1 [m][k/32]` buffer; `out_bf16` is `[m][n]`.
140pub fn kquant_mmvq(
141    gpu: &dyn GpuBackend,
142    kernel: KernelHandle,
143    w_blocks: DevicePtr,
144    y_q8: DevicePtr,
145    out_bf16: DevicePtr,
146    n: u32,
147    k: u32,
148    m: u32,
149    stream: u64,
150) -> Result<()> {
151    anyhow::ensure!((1..=8).contains(&m), "kquant_mmvq: m={m} outside 1..=8");
152    anyhow::ensure!(
153        k.is_multiple_of(QK_K),
154        "kquant_mmvq: k={k} is not a multiple of {QK_K}"
155    );
156    KernelLaunch::new(gpu, kernel)
157        .grid([n, 1, 1])
158        .block([32, 4, 1])
159        .arg_ptr(w_blocks)
160        .arg_ptr(y_q8)
161        .arg_ptr(out_bf16)
162        .arg_u32(k)
163        .arg_u32(n)
164        .arg_u32(m)
165        .launch(stream)
166}
167
168/// Prefill GEMM on raw blocks through the vendored MMQ engine:
169/// `out[m][n]` (bf16) = `A_q8[m][k]` x `W[n][k]`. `a_q8` must be in the type's MMQ
170/// q8_1 layout (D2S6 for Q2_K, D4 for Q3_K); `smem` is the matching `*_MMQ_SMEM`.
171#[allow(clippy::too_many_arguments)]
172/// `kquant_mmvq_*_experts`: one launch over `n_experts` experts whose block
173/// pointers sit in the device table `vxs` (`n_experts` pointers). The
174/// activation for expert `e` is `y_q8 + e * y_stride_bytes` (0 = shared), its
175/// output `out_bf16 + e * m * n`. Same per-row math as [`kquant_mmvq`].
176pub fn kquant_mmvq_experts(
177    gpu: &dyn GpuBackend,
178    kernel: KernelHandle,
179    vxs: DevicePtr,
180    y_q8: DevicePtr,
181    out_bf16: DevicePtr,
182    n: u32,
183    k: u32,
184    m: u32,
185    n_experts: u32,
186    y_stride_bytes: u32,
187    stream: u64,
188) -> Result<()> {
189    anyhow::ensure!(
190        (1..=8).contains(&m),
191        "kquant_mmvq_experts: m={m} outside 1..=8"
192    );
193    anyhow::ensure!(
194        k.is_multiple_of(QK_K),
195        "kquant_mmvq_experts: k={k} is not a multiple of {QK_K}"
196    );
197    KernelLaunch::new(gpu, kernel)
198        .grid([n, n_experts, 1])
199        .block([32, 4, 1])
200        .arg_ptr(vxs)
201        .arg_ptr(y_q8)
202        .arg_ptr(out_bf16)
203        .arg_u32(k)
204        .arg_u32(n)
205        .arg_u32(m)
206        .arg_u32(y_stride_bytes)
207        .launch(stream)
208}
209
210/// `kquant_mmvq_*_w`: the warp-per-row GEMV (four rows a block, no shared
211/// memory), same arguments as [`kquant_mmvq`].
212pub fn kquant_mmvq_w(
213    gpu: &dyn GpuBackend,
214    kernel: KernelHandle,
215    w_blocks: DevicePtr,
216    y_q8: DevicePtr,
217    out_bf16: DevicePtr,
218    n: u32,
219    k: u32,
220    m: u32,
221    stream: u64,
222) -> Result<()> {
223    anyhow::ensure!((1..=8).contains(&m), "kquant_mmvq_w: m={m} outside 1..=8");
224    anyhow::ensure!(
225        k.is_multiple_of(QK_K),
226        "kquant_mmvq_w: k={k} is not a multiple of {QK_K}"
227    );
228    KernelLaunch::new(gpu, kernel)
229        .grid([div_ceil(n, 4), 1, 1])
230        .block([32, 4, 1])
231        .arg_ptr(w_blocks)
232        .arg_ptr(y_q8)
233        .arg_ptr(out_bf16)
234        .arg_u32(k)
235        .arg_u32(n)
236        .arg_u32(m)
237        .launch(stream)
238}
239
240/// `kquant_mmvq_q2_k_groups_w`: `n_groups` row-blocks of one `[n_groups * n,
241/// n_groups * k]`-shaped projection, group `g` = weight rows `[g*n, (g+1)*n)`
242/// (`w_blocks` is `[n_groups * n][k/256]`) against activation columns
243/// `[g*k, (g+1)*k)` of the `block_q8_1 [m][n_groups * k / 32]` row `y_q8`,
244/// written to columns `[g*n, (g+1)*n)` of `out_bf16` (`[m][n_groups * n]`).
245/// Bit-identical to `n_groups` launches of [`kquant_mmvq_w`] over the slices.
246#[allow(clippy::too_many_arguments)]
247pub fn kquant_mmvq_groups_w(
248    gpu: &dyn GpuBackend,
249    kernel: KernelHandle,
250    w_blocks: DevicePtr,
251    y_q8: DevicePtr,
252    out_bf16: DevicePtr,
253    n: u32,
254    k: u32,
255    m: u32,
256    n_groups: u32,
257    stream: u64,
258) -> Result<()> {
259    anyhow::ensure!(
260        (1..=8).contains(&m),
261        "kquant_mmvq_groups_w: m={m} outside 1..=8"
262    );
263    anyhow::ensure!(
264        k.is_multiple_of(QK_K),
265        "kquant_mmvq_groups_w: k={k} is not a multiple of {QK_K}"
266    );
267    anyhow::ensure!(n_groups >= 1, "kquant_mmvq_groups_w: no groups");
268    KernelLaunch::new(gpu, kernel)
269        .grid([div_ceil(n, 4), n_groups, 1])
270        .block([32, 4, 1])
271        .arg_ptr(w_blocks)
272        .arg_ptr(y_q8)
273        .arg_ptr(out_bf16)
274        .arg_u32(k)
275        .arg_u32(n)
276        .arg_u32(m)
277        .arg_u32(n_groups)
278        .launch(stream)
279}
280
281/// `kquant_mmvq_q2_k_pair_w`: two `[n_i, k]` Q2_K projections of ONE
282/// activation (`y_q8`, `block_q8_1 [m][k/32]`) in one launch, `out_i` =
283/// `[m][n_i]` bf16. Bit-identical to two [`kquant_mmvq_w`] launches.
284#[allow(clippy::too_many_arguments)]
285pub fn kquant_mmvq_pair_w(
286    gpu: &dyn GpuBackend,
287    kernel: KernelHandle,
288    (w0, out0, n0): (DevicePtr, DevicePtr, u32),
289    (w1, out1, n1): (DevicePtr, DevicePtr, u32),
290    y_q8: DevicePtr,
291    k: u32,
292    m: u32,
293    stream: u64,
294) -> Result<()> {
295    anyhow::ensure!(
296        (1..=8).contains(&m),
297        "kquant_mmvq_pair_w: m={m} outside 1..=8"
298    );
299    anyhow::ensure!(
300        k.is_multiple_of(QK_K),
301        "kquant_mmvq_pair_w: k={k} is not a multiple of {QK_K}"
302    );
303    anyhow::ensure!(n0 >= 1 && n1 >= 1, "kquant_mmvq_pair_w: empty projection");
304    KernelLaunch::new(gpu, kernel)
305        .grid([div_ceil(n0.max(n1), 4), 2, 1])
306        .block([32, 4, 1])
307        .arg_ptr(w0)
308        .arg_ptr(out0)
309        .arg_u32(n0)
310        .arg_ptr(w1)
311        .arg_ptr(out1)
312        .arg_u32(n1)
313        .arg_ptr(y_q8)
314        .arg_u32(k)
315        .arg_u32(m)
316        .launch(stream)
317}
318
319/// [`kquant_mmvq_experts_w`] with `nwarps` rows a block (the `_w2` / `_w8`
320/// entries for 2 / 8; the plain `_w` entry is 4). Same bytes for any `nwarps`.
321#[allow(clippy::too_many_arguments)]
322pub fn kquant_mmvq_experts_wn(
323    gpu: &dyn GpuBackend,
324    kernel: KernelHandle,
325    w_tables: DevicePtr,
326    y_q8: DevicePtr,
327    out_bf16: DevicePtr,
328    n: u32,
329    k: u32,
330    m: u32,
331    n_experts: u32,
332    y_stride_bytes: u32,
333    nwarps: u32,
334    stream: u64,
335) -> Result<()> {
336    anyhow::ensure!(
337        (1..=8).contains(&m),
338        "kquant_mmvq_experts_wn: m={m} outside 1..=8"
339    );
340    anyhow::ensure!(
341        k.is_multiple_of(QK_K),
342        "kquant_mmvq_experts_wn: k={k} is not a multiple of {QK_K}"
343    );
344    anyhow::ensure!(
345        matches!(nwarps, 2 | 4 | 8),
346        "kquant_mmvq_experts_wn: nwarps={nwarps} not in 2/4/8"
347    );
348    KernelLaunch::new(gpu, kernel)
349        .grid([div_ceil(n, nwarps), n_experts, 1])
350        .block([32, nwarps, 1])
351        .arg_ptr(w_tables)
352        .arg_ptr(y_q8)
353        .arg_ptr(out_bf16)
354        .arg_u32(k)
355        .arg_u32(n)
356        .arg_u32(m)
357        .arg_u32(y_stride_bytes)
358        .launch(stream)
359}
360
361/// `kquant_mmvq_*_experts_w`: the warp-per-row expert batch, same arguments
362/// as [`kquant_mmvq_experts`].
363pub fn kquant_mmvq_experts_w(
364    gpu: &dyn GpuBackend,
365    kernel: KernelHandle,
366    vxs: DevicePtr,
367    y_q8: DevicePtr,
368    out_bf16: DevicePtr,
369    n: u32,
370    k: u32,
371    m: u32,
372    n_experts: u32,
373    y_stride_bytes: u32,
374    stream: u64,
375) -> Result<()> {
376    anyhow::ensure!(
377        (1..=8).contains(&m),
378        "kquant_mmvq_experts_w: m={m} outside 1..=8"
379    );
380    anyhow::ensure!(
381        k.is_multiple_of(QK_K),
382        "kquant_mmvq_experts_w: k={k} is not a multiple of {QK_K}"
383    );
384    KernelLaunch::new(gpu, kernel)
385        .grid([div_ceil(n, 4), n_experts, 1])
386        .block([32, 4, 1])
387        .arg_ptr(vxs)
388        .arg_ptr(y_q8)
389        .arg_ptr(out_bf16)
390        .arg_u32(k)
391        .arg_u32(n)
392        .arg_u32(m)
393        .arg_u32(y_stride_bytes)
394        .launch(stream)
395}
396
397pub fn kquant_mmq_gemm(
398    gpu: &dyn GpuBackend,
399    kernel_nc: KernelHandle,
400    kernel_wc: KernelHandle,
401    a_q8: DevicePtr,
402    w_blocks: DevicePtr,
403    out_bf16: DevicePtr,
404    m: u32,
405    n: u32,
406    k: u32,
407    smem: u32,
408    stream: u64,
409) -> Result<()> {
410    anyhow::ensure!(
411        k.is_multiple_of(QK_K),
412        "kquant_mmq_gemm: k={k} is not a multiple of {QK_K}"
413    );
414    let kernel = if n.is_multiple_of(128) {
415        kernel_nc
416    } else {
417        kernel_wc
418    };
419    KernelLaunch::new(gpu, kernel)
420        .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
421        .block([32, 8, 1])
422        .shared_mem(smem)
423        .arg_ptr(w_blocks) // x = weights
424        .arg_ptr(a_q8) // y = q8_1 activations
425        .arg_ptr(out_bf16) // dst
426        .arg_u32(n) // nrows_x
427        .arg_u32(m) // ncols_dst
428        .arg_u32(k) // ncols_x
429        .arg_u32(k / QK_K) // stride_row_x = K/256
430        .arg_u32(m) // ncols_y
431        .arg_u32(n) // stride_col_dst
432        .launch(stream)
433}
434
435#[cfg(test)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn sizes_match_the_block_geometry() {
441        // one V4.1 expert gate slice: 2304 rows x 5120, Q2_K
442        assert_eq!(kquant_weight_bytes(2304, 5120, Q2K_BLOCK_BYTES), 3_870_720);
443        // its down slice: 5120 rows x 2304, Q3_K
444        assert_eq!(kquant_weight_bytes(5120, 2304, Q3K_BLOCK_BYTES), 5_068_800);
445        assert_eq!(kquant_q8_1_rows_bytes(1, 5120), 160 * 36);
446        assert_eq!(kquant_mmq_act_bytes(200, 5120), 256 * 40 * 144);
447        assert_eq!((Q2K_MMQ_SMEM, Q3K_MMQ_SMEM), (70_144, 61_952));
448    }
449}