metrale_model_layers/layers/ops/
quant_dispatch.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: GEMV/GEMM dispatch on the weight's quantization format, and the
4//! NVFP4 (W4A16) GEMV launchers: single row, fixed 2 and 3 rows, the batched
5//! tiers, and the variants that deinterleave Q/Gate or QKVZ as they store.
6//!
7//! Owner: model-layers ops.
8//! Invariants: none beyond the types.
9
10#![allow(unused_imports)]
11
12use anyhow::Result;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
14use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
15
16use crate::layers::moe;
17use crate::weight_map::{DenseWeight, Fp8DenseWeight, Fp8Weight, QuantizedWeight};
18
19use super::*;
20
21/// 2026-09-25: GEMV through the kernel for the weight's format: NVFP4, FP8 or
22/// dense BF16. `PackedQ2` weights return an error.
23#[allow(clippy::too_many_arguments)]
24pub fn quant_gemv(
25    gpu: &dyn GpuBackend,
26    gemv_nvfp4: KernelHandle,
27    gemv_fp8: KernelHandle,
28    gemv_dense: KernelHandle,
29    input: DevicePtr,
30    weight: &crate::weight_map::QuantWeight,
31    output: DevicePtr,
32    n: u32,
33    k: u32,
34    stream: u64,
35) -> Result<()> {
36    use crate::weight_map::QuantWeight;
37    match weight {
38        QuantWeight::Nvfp4(w) => w4a16_gemv(gpu, gemv_nvfp4, input, w, output, n, k, stream),
39        QuantWeight::Fp8(w) => w8a16_gemv(
40            gpu,
41            gemv_fp8,
42            input,
43            w.weight,
44            w.row_scale,
45            output,
46            n,
47            k,
48            stream,
49        ),
50        QuantWeight::Dense(w) => dense_gemv(gpu, gemv_dense, input, w, output, n, k, stream),
51        // 2026-09-25: No handle here serves PackedQ2; its GEMV is `q2_0_gemv_vec`,
52        // launched by the layers themselves.
53        QuantWeight::PackedQ2(_) => anyhow::bail!(
54            "quant_gemv: PackedQ2 not routed through the generic dispatcher; use q2_0_gemv_vec"
55        ),
56    }
57}
58
59/// 2026-09-25: GEMM (`m` rows) through the kernel for the weight's format:
60/// NVFP4, FP8 or dense BF16. `PackedQ2` weights return an error.
61#[allow(clippy::too_many_arguments)]
62pub fn quant_gemm(
63    gpu: &dyn GpuBackend,
64    gemm_nvfp4: KernelHandle,
65    gemm_fp8: KernelHandle,
66    gemm_dense: KernelHandle,
67    input: DevicePtr,
68    weight: &crate::weight_map::QuantWeight,
69    output: DevicePtr,
70    m: u32,
71    n: u32,
72    k: u32,
73    stream: u64,
74) -> Result<()> {
75    use crate::weight_map::QuantWeight;
76    match weight {
77        QuantWeight::Nvfp4(w) => w4a16_gemm(gpu, gemm_nvfp4, input, w, output, m, n, k, stream),
78        QuantWeight::Fp8(w) => w8a16_gemm(
79            gpu,
80            gemm_fp8,
81            input,
82            w.weight,
83            w.row_scale,
84            output,
85            m,
86            n,
87            k,
88            stream,
89        ),
90        QuantWeight::Dense(w) => dense_gemm(gpu, gemm_dense, input, w, output, m, n, k, stream),
91        QuantWeight::PackedQ2(_) => anyhow::bail!(
92            "quant_gemm: PackedQ2 not routed through the generic dispatcher; \
93             use the layer's transient-dequant prefill path"
94        ),
95    }
96}
97
98/// 2026-09-25: `C[1, N] = A[1, K] @ dequant(B)` with BF16 A and C and an NVFP4
99/// weight.
100pub fn w4a16_gemv(
101    gpu: &dyn GpuBackend,
102    kernel: KernelHandle,
103    input: DevicePtr,
104    weight: &QuantizedWeight,
105    output: DevicePtr,
106    n: u32,
107    k: u32,
108    stream: u64,
109) -> Result<()> {
110    KernelLaunch::new(gpu, kernel)
111        .grid([w4a16_gemv_grid_x(n), 1, 1])
112        .block([256, 1, 1])
113        .arg_ptr(input)
114        .arg_ptr(weight.weight)
115        .arg_ptr(weight.weight_scale)
116        .arg_f32(weight.weight_scale_2)
117        .arg_ptr(output)
118        .arg_u32(n)
119        .arg_u32(k)
120        .launch(stream)
121}
122
123/// 2026-09-25: [`w4a16_gemv`] for 2 contiguous rows in one pass over the
124/// weight. `gemv_tc::tc_fixed_m` takes the launch to the tensor-core kernel
125/// when it routes it (`METRALE_W4A16_TC_WIDE`); otherwise `kernel` runs.
126pub fn w4a16_gemv_batch2(
127    gpu: &dyn GpuBackend,
128    kernel: KernelHandle,
129    input: DevicePtr,
130    weight: &QuantizedWeight,
131    output: DevicePtr,
132    n: u32,
133    k: u32,
134    stream: u64,
135) -> Result<()> {
136    if super::gemv_tc::tc_fixed_m(gpu, input, weight, output, 2, n, k, stream)? {
137        return Ok(());
138    }
139    KernelLaunch::new(gpu, kernel)
140        .grid([div_ceil(n, 4), 1, 1])
141        .block([256, 1, 1])
142        .arg_ptr(input)
143        .arg_ptr(weight.weight)
144        .arg_ptr(weight.weight_scale)
145        .arg_f32(weight.weight_scale_2)
146        .arg_ptr(output)
147        .arg_u32(n)
148        .arg_u32(k)
149        .launch(stream)
150}
151
152/// 2026-09-25: [`w4a16_gemv_batch2`] for 3 rows.
153pub fn w4a16_gemv_batch3(
154    gpu: &dyn GpuBackend,
155    kernel: KernelHandle,
156    input: DevicePtr,
157    weight: &QuantizedWeight,
158    output: DevicePtr,
159    n: u32,
160    k: u32,
161    stream: u64,
162) -> Result<()> {
163    if super::gemv_tc::tc_fixed_m(gpu, input, weight, output, 3, n, k, stream)? {
164        return Ok(());
165    }
166    KernelLaunch::new(gpu, kernel)
167        .grid([div_ceil(n, 4), 1, 1])
168        .block([256, 1, 1])
169        .arg_ptr(input)
170        .arg_ptr(weight.weight)
171        .arg_ptr(weight.weight_scale)
172        .arg_f32(weight.weight_scale_2)
173        .arg_ptr(output)
174        .arg_u32(n)
175        .arg_u32(k)
176        .launch(stream)
177}
178
179/// 2026-09-25: [`w4a16_gemv`] for `m` contiguous rows in one pass over the
180/// weight. `kernel` is the CUDA-core `w4a16_gemv_batch{M}` tier the caller
181/// picked (`W4a16BatchmTiers::kernel`); `gemv_tc::tc_kernel` replaces it with
182/// the tensor-core `w4a16_gemv_tc8`/`tc16` when it routes the shape.
183pub fn w4a16_gemv_batchm(
184    gpu: &dyn GpuBackend,
185    kernel: KernelHandle,
186    input: DevicePtr,
187    weight: &QuantizedWeight,
188    output: DevicePtr,
189    m: u32,
190    n: u32,
191    k: u32,
192    stream: u64,
193) -> Result<()> {
194    // 2026-09-25: A tier writes only its first `MAX_M` rows (`w4a16_gemv.cu`), and
195    // the widest, `w4a16_gemv_batch32`, stops at 32. For a weight that
196    // declares FP4 activations, `W4a16BatchmTiers::kernel_for` returns that handle
197    // for 33..=64 rows as well, rows the W4A4 path is meant to serve, so a
198    // launch here above 32 rows is refused instead of partly written.
199    anyhow::ensure!(m <= 32, "w4a16_gemv_batchm caps at M=32 (batch32; m={m})");
200    // 2026-09-25: The tensor-core kernel takes the same arguments with its own grid
201    // and block (`gemv_tc`).
202    let (kernel, grid_x, block_x) = match super::gemv_tc::tc_kernel(gpu, m, n, k) {
203        Some((tc, grid_x)) => (tc, grid_x, super::gemv_tc::TC_BLOCK),
204        None => (kernel, div_ceil(n, 4), 256),
205    };
206    KernelLaunch::new(gpu, kernel)
207        .grid([grid_x, 1, 1])
208        .block([block_x, 1, 1])
209        .arg_ptr(input)
210        .arg_ptr(weight.weight)
211        .arg_ptr(weight.weight_scale)
212        .arg_f32(weight.weight_scale_2)
213        .arg_ptr(output)
214        .arg_u32(m)
215        .arg_u32(n)
216        .arg_u32(k)
217        .launch(stream)
218}
219
220/// 2026-09-25: [`w4a16_gemv`] over a weight whose rows interleave Q and Gate per
221/// head; the output is stored deinterleaved, `[Q of all heads | Gate of all
222/// heads]`, with `n = num_heads * head_dim * 2`.
223#[allow(clippy::too_many_arguments)]
224pub fn w4a16_gemv_qg(
225    gpu: &dyn GpuBackend,
226    kernel: KernelHandle,
227    input: DevicePtr,
228    weight: &QuantizedWeight,
229    output: DevicePtr,
230    n: u32,
231    k: u32,
232    num_heads: u32,
233    head_dim: u32,
234    stream: u64,
235) -> Result<()> {
236    KernelLaunch::new(gpu, kernel)
237        .grid([div_ceil(n, 4), 1, 1])
238        .block([256, 1, 1])
239        .arg_ptr(input)
240        .arg_ptr(weight.weight)
241        .arg_ptr(weight.weight_scale)
242        .arg_f32(weight.weight_scale_2)
243        .arg_ptr(output)
244        .arg_u32(n)
245        .arg_u32(k)
246        .arg_u32(num_heads)
247        .arg_u32(head_dim)
248        .launch(stream)
249}
250
251/// 2026-09-25: [`w4a16_gemv`] over a weight whose rows interleave Q, K, V and Z
252/// per group; the output is stored deinterleaved as `[Q | K | V | Z]`.
253#[allow(clippy::too_many_arguments)]
254pub fn w4a16_gemv_qkvz(
255    gpu: &dyn GpuBackend,
256    kernel: KernelHandle,
257    input: DevicePtr,
258    weight: &QuantizedWeight,
259    output: DevicePtr,
260    n: u32,
261    k: u32,
262    num_groups: u32,
263    head_k_dim: u32,
264    vheads_per_group: u32,
265    head_v_dim: u32,
266    stream: u64,
267) -> Result<()> {
268    KernelLaunch::new(gpu, kernel)
269        .grid([div_ceil(n, 4), 1, 1])
270        .block([256, 1, 1])
271        .arg_ptr(input)
272        .arg_ptr(weight.weight)
273        .arg_ptr(weight.weight_scale)
274        .arg_f32(weight.weight_scale_2)
275        .arg_ptr(output)
276        .arg_u32(n)
277        .arg_u32(k)
278        .arg_u32(num_groups)
279        .arg_u32(head_k_dim)
280        .arg_u32(vheads_per_group)
281        .arg_u32(head_v_dim)
282        .launch(stream)
283}
284
285/// 2026-09-25: [`w4a16_gemv_qg`] for 2 rows: A is `[2, K]`, C is `[2, N]`,
286/// each row deinterleaved.
287#[allow(clippy::too_many_arguments)]
288pub fn w4a16_gemv_qg_batch2(
289    gpu: &dyn GpuBackend,
290    kernel: KernelHandle,
291    input: DevicePtr,
292    weight: &QuantizedWeight,
293    output: DevicePtr,
294    n: u32,
295    k: u32,
296    num_heads: u32,
297    head_dim: u32,
298    stream: u64,
299) -> Result<()> {
300    KernelLaunch::new(gpu, kernel)
301        .grid([div_ceil(n, 4), 1, 1])
302        .block([256, 1, 1])
303        .arg_ptr(input)
304        .arg_ptr(weight.weight)
305        .arg_ptr(weight.weight_scale)
306        .arg_f32(weight.weight_scale_2)
307        .arg_ptr(output)
308        .arg_u32(n)
309        .arg_u32(k)
310        .arg_u32(num_heads)
311        .arg_u32(head_dim)
312        .launch(stream)
313}
314
315/// 2026-09-25: [`w4a16_gemv_qg`] for 3 rows: A is `[3, K]`, C is `[3, N]`,
316/// each row deinterleaved.
317#[allow(clippy::too_many_arguments)]
318pub fn w4a16_gemv_qg_batch3(
319    gpu: &dyn GpuBackend,
320    kernel: KernelHandle,
321    input: DevicePtr,
322    weight: &QuantizedWeight,
323    output: DevicePtr,
324    n: u32,
325    k: u32,
326    num_heads: u32,
327    head_dim: u32,
328    stream: u64,
329) -> Result<()> {
330    KernelLaunch::new(gpu, kernel)
331        .grid([div_ceil(n, 4), 1, 1])
332        .block([256, 1, 1])
333        .arg_ptr(input)
334        .arg_ptr(weight.weight)
335        .arg_ptr(weight.weight_scale)
336        .arg_f32(weight.weight_scale_2)
337        .arg_ptr(output)
338        .arg_u32(n)
339        .arg_u32(k)
340        .arg_u32(num_heads)
341        .arg_u32(head_dim)
342        .launch(stream)
343}
344
345/// 2026-09-25: Two NVFP4 GEMVs over the same 3-row input `[3, K]`; `blockIdx.z`
346/// picks `weight0` or `weight1`, and each writes `[3, N]`.
347#[allow(clippy::too_many_arguments)]
348pub fn w4a16_gemv_dual_batch3(
349    gpu: &dyn GpuBackend,
350    kernel: KernelHandle,
351    input: DevicePtr,
352    weight0: &QuantizedWeight,
353    output0: DevicePtr,
354    weight1: &QuantizedWeight,
355    output1: DevicePtr,
356    n: u32,
357    k: u32,
358    stream: u64,
359) -> Result<()> {
360    // 2026-09-25: One tensor-core launch per projection. The route depends only on
361    // `(m, n, k)`, the cached handles and switches read once per process, all the
362    // same for both calls, so the second launches whenever the first did.
363    if super::gemv_tc::tc_fixed_m(gpu, input, weight0, output0, 3, n, k, stream)? {
364        super::gemv_tc::tc_fixed_m(gpu, input, weight1, output1, 3, n, k, stream)?;
365        return Ok(());
366    }
367    KernelLaunch::new(gpu, kernel)
368        .grid([div_ceil(n, 4), 1, 2])
369        .block([256, 1, 1])
370        .arg_ptr(input)
371        .arg_ptr(weight0.weight)
372        .arg_ptr(weight0.weight_scale)
373        .arg_f32(weight0.weight_scale_2)
374        .arg_ptr(output0)
375        .arg_ptr(weight1.weight)
376        .arg_ptr(weight1.weight_scale)
377        .arg_f32(weight1.weight_scale_2)
378        .arg_ptr(output1)
379        .arg_u32(n)
380        .arg_u32(k)
381        .launch(stream)
382}
383
384/// 2026-09-25: [`w4a16_gemv_dual_batch3`] for 2 rows.
385#[allow(clippy::too_many_arguments)]
386pub fn w4a16_gemv_dual_batch2(
387    gpu: &dyn GpuBackend,
388    kernel: KernelHandle,
389    input: DevicePtr,
390    weight0: &QuantizedWeight,
391    output0: DevicePtr,
392    weight1: &QuantizedWeight,
393    output1: DevicePtr,
394    n: u32,
395    k: u32,
396    stream: u64,
397) -> Result<()> {
398    // 2026-09-25: One tensor-core launch per projection. The route depends only on
399    // `(m, n, k)`, the cached handles and switches read once per process, all the
400    // same for both calls, so the second launches whenever the first did.
401    if super::gemv_tc::tc_fixed_m(gpu, input, weight0, output0, 2, n, k, stream)? {
402        super::gemv_tc::tc_fixed_m(gpu, input, weight1, output1, 2, n, k, stream)?;
403        return Ok(());
404    }
405    KernelLaunch::new(gpu, kernel)
406        .grid([div_ceil(n, 4), 1, 2])
407        .block([256, 1, 1])
408        .arg_ptr(input)
409        .arg_ptr(weight0.weight)
410        .arg_ptr(weight0.weight_scale)
411        .arg_f32(weight0.weight_scale_2)
412        .arg_ptr(output0)
413        .arg_ptr(weight1.weight)
414        .arg_ptr(weight1.weight_scale)
415        .arg_f32(weight1.weight_scale_2)
416        .arg_ptr(output1)
417        .arg_u32(n)
418        .arg_u32(k)
419        .launch(stream)
420}