spark_model/layers/ops/
quant_dispatch.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/// Unified GEMV dispatch: select kernel based on weight quantization format.
17///
18/// Eliminates cascading if/else chains in layer forward methods. The enum
19/// branch (~1 cycle) is negligible vs GPU kernel launch overhead (~5μs).
20#[allow(clippy::too_many_arguments)]
21pub fn quant_gemv(
22    gpu: &dyn GpuBackend,
23    gemv_nvfp4: KernelHandle,
24    gemv_fp8: KernelHandle,
25    gemv_dense: KernelHandle,
26    input: DevicePtr,
27    weight: &crate::weight_map::QuantWeight,
28    output: DevicePtr,
29    n: u32,
30    k: u32,
31    stream: u64,
32) -> Result<()> {
33    use crate::weight_map::QuantWeight;
34    match weight {
35        QuantWeight::Nvfp4(w) => w4a16_gemv(gpu, gemv_nvfp4, input, w, output, n, k, stream),
36        QuantWeight::Fp8(w) => w8a16_gemv(
37            gpu,
38            gemv_fp8,
39            input,
40            w.weight,
41            w.row_scale,
42            output,
43            n,
44            k,
45            stream,
46        ),
47        QuantWeight::Dense(w) => dense_gemv(gpu, gemv_dense, input, w, output, n, k, stream),
48        // PackedQ2 has no companion kernel handle here (its GEMV is
49        // `q2_0_gemv_vec`, dispatched at the layer's own sites, not via this
50        // generic 3-kernel helper). Bail rather than misdispatch.
51        QuantWeight::PackedQ2(_) => anyhow::bail!(
52            "quant_gemv: PackedQ2 not routed through the generic dispatcher; use q2_0_gemv_vec"
53        ),
54    }
55}
56
57/// Unified GEMM dispatch: select kernel based on weight quantization format.
58///
59/// For M>1 prefill projections (Q/K/V/O). Falls back to dense GEMM for BF16.
60#[allow(clippy::too_many_arguments)]
61pub fn quant_gemm(
62    gpu: &dyn GpuBackend,
63    gemm_nvfp4: KernelHandle,
64    gemm_fp8: KernelHandle,
65    gemm_dense: KernelHandle,
66    input: DevicePtr,
67    weight: &crate::weight_map::QuantWeight,
68    output: DevicePtr,
69    m: u32,
70    n: u32,
71    k: u32,
72    stream: u64,
73) -> Result<()> {
74    use crate::weight_map::QuantWeight;
75    match weight {
76        QuantWeight::Nvfp4(w) => w4a16_gemm(gpu, gemm_nvfp4, input, w, output, m, n, k, stream),
77        QuantWeight::Fp8(w) => w8a16_gemm(
78            gpu,
79            gemm_fp8,
80            input,
81            w.weight,
82            w.row_scale,
83            output,
84            m,
85            n,
86            k,
87            stream,
88        ),
89        QuantWeight::Dense(w) => dense_gemm(gpu, gemm_dense, input, w, output, m, n, k, stream),
90        QuantWeight::PackedQ2(_) => anyhow::bail!(
91            "quant_gemm: PackedQ2 not routed through the generic dispatcher; \
92             use the layer's transient-dequant prefill path"
93        ),
94    }
95}
96
97/// W4A16 GEMV (M=1): C = A @ dequant(B) for single-row activations.
98///
99/// A: [1, K] BF16, B: NVFP4 packed, C: [1, N] BF16.
100/// 4 outputs/block, 64 threads (2 warps) per output. Cross-warp smem reduction.
101///
102/// Kernel: `w4a16_gemv(A, B_packed, B_scale, scale2, C, N, K)`
103/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
104pub fn w4a16_gemv(
105    gpu: &dyn GpuBackend,
106    kernel: KernelHandle,
107    input: DevicePtr,
108    weight: &QuantizedWeight,
109    output: DevicePtr,
110    n: u32,
111    k: u32,
112    stream: u64,
113) -> Result<()> {
114    KernelLaunch::new(gpu, kernel)
115        .grid([w4a16_gemv_grid_x(n), 1, 1])
116        .block([256, 1, 1])
117        .arg_ptr(input)
118        .arg_ptr(weight.weight)
119        .arg_ptr(weight.weight_scale)
120        .arg_f32(weight.weight_scale_2)
121        .arg_ptr(output)
122        .arg_u32(n)
123        .arg_u32(k)
124        .launch(stream)
125}
126
127/// W4A16 double-GEMV (M=2): reads weights once, computes 2 outputs.
128///
129/// A: [2, K] BF16 contiguous, B: NVFP4 packed, C: [2, N] BF16 contiguous.
130/// Same weight bandwidth as single GEMV — eliminates GEMM M=2 tile waste.
131///
132/// Kernel: `w4a16_gemv_batch2(A, B_packed, B_scale, scale2, C, N, K)`
133/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
134pub fn w4a16_gemv_batch2(
135    gpu: &dyn GpuBackend,
136    kernel: KernelHandle,
137    input: DevicePtr,
138    weight: &QuantizedWeight,
139    output: DevicePtr,
140    n: u32,
141    k: u32,
142    stream: u64,
143) -> Result<()> {
144    KernelLaunch::new(gpu, kernel)
145        .grid([div_ceil(n, 4), 1, 1])
146        .block([256, 1, 1])
147        .arg_ptr(input)
148        .arg_ptr(weight.weight)
149        .arg_ptr(weight.weight_scale)
150        .arg_f32(weight.weight_scale_2)
151        .arg_ptr(output)
152        .arg_u32(n)
153        .arg_u32(k)
154        .launch(stream)
155}
156
157/// W4A16 triple-GEMV (M=3): reads weights once, computes 3 outputs.
158///
159/// A: [3, K] BF16 contiguous, B: NVFP4 packed, C: [3, N] BF16 contiguous.
160/// For K=3 speculative verification.
161///
162/// Kernel: `w4a16_gemv_batch3(A, B_packed, B_scale, scale2, C, N, K)`
163/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
164pub fn w4a16_gemv_batch3(
165    gpu: &dyn GpuBackend,
166    kernel: KernelHandle,
167    input: DevicePtr,
168    weight: &QuantizedWeight,
169    output: DevicePtr,
170    n: u32,
171    k: u32,
172    stream: u64,
173) -> Result<()> {
174    KernelLaunch::new(gpu, kernel)
175        .grid([div_ceil(n, 4), 1, 1])
176        .block([256, 1, 1])
177        .arg_ptr(input)
178        .arg_ptr(weight.weight)
179        .arg_ptr(weight.weight_scale)
180        .arg_f32(weight.weight_scale_2)
181        .arg_ptr(output)
182        .arg_u32(n)
183        .arg_u32(k)
184        .launch(stream)
185}
186
187/// W4A16 batched GEMV (M<=MAX_M) — the NVFP4 sibling of `w8a16_gemv_batch4/16`.
188///
189/// Reads the NVFP4 weight matrix ONCE and computes `m` outputs (one per seq),
190/// amortizing the weight read across the batch. `kernel` is `w4a16_gemv_batch4`
191/// (M<=4), `w4a16_gemv_batch8` (M<=8, chain verify) or `w4a16_gemv_batch16`
192/// (M<=16). A:`[m,K]` BF16, C:`[m,N]` BF16.
193///
194/// Kernel: `w4a16_gemv_batch4/8/16(A, B_packed, B_scale, scale2, C, M, N, K)`
195/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
196pub fn w4a16_gemv_batchm(
197    gpu: &dyn GpuBackend,
198    kernel: KernelHandle,
199    input: DevicePtr,
200    weight: &QuantizedWeight,
201    output: DevicePtr,
202    m: u32,
203    n: u32,
204    k: u32,
205    stream: u64,
206) -> Result<()> {
207    // Largest template is w4a16_gemv_batch16 (MAX_M=16). Above that the
208    // kernel SILENTLY truncates: rows 0..15 computed, rows 16.. never
209    // written — garbage output, not a crash.
210    debug_assert!(m <= 16, "w4a16_gemv_batchm caps at M=16 (m={m})");
211    KernelLaunch::new(gpu, kernel)
212        .grid([div_ceil(n, 4), 1, 1])
213        .block([256, 1, 1])
214        .arg_ptr(input)
215        .arg_ptr(weight.weight)
216        .arg_ptr(weight.weight_scale)
217        .arg_f32(weight.weight_scale_2)
218        .arg_ptr(output)
219        .arg_u32(m)
220        .arg_u32(n)
221        .arg_u32(k)
222        .launch(stream)
223}
224
225/// W4A16 GEMV with inline Q/Gate deinterleave on output write.
226///
227/// Same as `w4a16_gemv` but writes Q and Gate to deinterleaved positions,
228/// eliminating the separate `deinterleave_qg` kernel (12 graph nodes saved).
229///
230/// Kernel: `w4a16_gemv_qg(A, B, S, s2, C, N, K, num_heads, head_dim)`
231/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
232#[allow(clippy::too_many_arguments)]
233pub fn w4a16_gemv_qg(
234    gpu: &dyn GpuBackend,
235    kernel: KernelHandle,
236    input: DevicePtr,
237    weight: &QuantizedWeight,
238    output: DevicePtr,
239    n: u32,
240    k: u32,
241    num_heads: u32,
242    head_dim: u32,
243    stream: u64,
244) -> Result<()> {
245    KernelLaunch::new(gpu, kernel)
246        .grid([div_ceil(n, 4), 1, 1])
247        .block([256, 1, 1])
248        .arg_ptr(input)
249        .arg_ptr(weight.weight)
250        .arg_ptr(weight.weight_scale)
251        .arg_f32(weight.weight_scale_2)
252        .arg_ptr(output)
253        .arg_u32(n)
254        .arg_u32(k)
255        .arg_u32(num_heads)
256        .arg_u32(head_dim)
257        .launch(stream)
258}
259
260/// W4A16 GEMV with inline QKVZ deinterleave on output write.
261///
262/// Same as `w4a16_gemv` but writes to deinterleaved output locations,
263/// eliminating the separate `deinterleave_qkvz` kernel.
264///
265/// Kernel: `w4a16_gemv_qkvz(A, B, S, s2, C, N, K, ng, kd, vpg, vd)`
266/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
267#[allow(clippy::too_many_arguments)]
268pub fn w4a16_gemv_qkvz(
269    gpu: &dyn GpuBackend,
270    kernel: KernelHandle,
271    input: DevicePtr,
272    weight: &QuantizedWeight,
273    output: DevicePtr,
274    n: u32,
275    k: u32,
276    num_groups: u32,
277    head_k_dim: u32,
278    vheads_per_group: u32,
279    head_v_dim: u32,
280    stream: u64,
281) -> Result<()> {
282    KernelLaunch::new(gpu, kernel)
283        .grid([div_ceil(n, 4), 1, 1])
284        .block([256, 1, 1])
285        .arg_ptr(input)
286        .arg_ptr(weight.weight)
287        .arg_ptr(weight.weight_scale)
288        .arg_f32(weight.weight_scale_2)
289        .arg_ptr(output)
290        .arg_u32(n)
291        .arg_u32(k)
292        .arg_u32(num_groups)
293        .arg_u32(head_k_dim)
294        .arg_u32(vheads_per_group)
295        .arg_u32(head_v_dim)
296        .launch(stream)
297}
298
299/// Q+Gate GEMV for 2 tokens with inline deinterleave.
300///
301/// Reads the Q+Gate weight matrix once, produces 2 deinterleaved output
302/// vectors (Q|Gate for each token). Replaces 2× `w4a16_gemv_qg` calls.
303///
304/// Kernel: `w4a16_gemv_qg_batch2(A, B, S, s2, C, N, K, num_heads, head_dim)`
305/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
306/// Input A: [2, K], Output C: [2, N] deinterleaved [Q|G] per token.
307#[allow(clippy::too_many_arguments)]
308pub fn w4a16_gemv_qg_batch2(
309    gpu: &dyn GpuBackend,
310    kernel: KernelHandle,
311    input: DevicePtr,
312    weight: &QuantizedWeight,
313    output: DevicePtr,
314    n: u32,
315    k: u32,
316    num_heads: u32,
317    head_dim: u32,
318    stream: u64,
319) -> Result<()> {
320    KernelLaunch::new(gpu, kernel)
321        .grid([div_ceil(n, 4), 1, 1])
322        .block([256, 1, 1])
323        .arg_ptr(input)
324        .arg_ptr(weight.weight)
325        .arg_ptr(weight.weight_scale)
326        .arg_f32(weight.weight_scale_2)
327        .arg_ptr(output)
328        .arg_u32(n)
329        .arg_u32(k)
330        .arg_u32(num_heads)
331        .arg_u32(head_dim)
332        .launch(stream)
333}
334
335/// W4A16 GEMV batch3 with inline Q/Gate deinterleave.
336///
337/// Reads the Q+Gate weight matrix once, produces 3 deinterleaved output
338/// vectors (Q|Gate for each token). For K=3 speculative verification.
339///
340/// Kernel: `w4a16_gemv_qg_batch3(A, B, S, s2, C, N, K, num_heads, head_dim)`
341/// Grid: (ceil(N/4), 1, 1)  Block: (256, 1, 1)
342/// Input A: [3, K], Output C: [3, N] deinterleaved [Q|G] per token.
343#[allow(clippy::too_many_arguments)]
344pub fn w4a16_gemv_qg_batch3(
345    gpu: &dyn GpuBackend,
346    kernel: KernelHandle,
347    input: DevicePtr,
348    weight: &QuantizedWeight,
349    output: DevicePtr,
350    n: u32,
351    k: u32,
352    num_heads: u32,
353    head_dim: u32,
354    stream: u64,
355) -> Result<()> {
356    KernelLaunch::new(gpu, kernel)
357        .grid([div_ceil(n, 4), 1, 1])
358        .block([256, 1, 1])
359        .arg_ptr(input)
360        .arg_ptr(weight.weight)
361        .arg_ptr(weight.weight_scale)
362        .arg_f32(weight.weight_scale_2)
363        .arg_ptr(output)
364        .arg_u32(n)
365        .arg_u32(k)
366        .arg_u32(num_heads)
367        .arg_u32(head_dim)
368        .launch(stream)
369}
370
371/// Dual-projection GEMV for 3 tokens (K+V or any 2 weight matrices).
372///
373/// Reads each weight matrix once, produces 3 output vectors per projection.
374/// `blockIdx.z` selects projection 0 or 1.
375///
376/// Kernel: `w4a16_gemv_dual_batch3(A, B0, S0, s2_0, C0, B1, S1, s2_1, C1, N, K)`
377/// Grid: (ceil(N/4), 1, 2)  Block: (256, 1, 1)
378/// Input A: [3, K], Output C0: [3, N], C1: [3, N].
379#[allow(clippy::too_many_arguments)]
380pub fn w4a16_gemv_dual_batch3(
381    gpu: &dyn GpuBackend,
382    kernel: KernelHandle,
383    input: DevicePtr,
384    weight0: &QuantizedWeight,
385    output0: DevicePtr,
386    weight1: &QuantizedWeight,
387    output1: DevicePtr,
388    n: u32,
389    k: u32,
390    stream: u64,
391) -> Result<()> {
392    KernelLaunch::new(gpu, kernel)
393        .grid([div_ceil(n, 4), 1, 2])
394        .block([256, 1, 1])
395        .arg_ptr(input)
396        .arg_ptr(weight0.weight)
397        .arg_ptr(weight0.weight_scale)
398        .arg_f32(weight0.weight_scale_2)
399        .arg_ptr(output0)
400        .arg_ptr(weight1.weight)
401        .arg_ptr(weight1.weight_scale)
402        .arg_f32(weight1.weight_scale_2)
403        .arg_ptr(output1)
404        .arg_u32(n)
405        .arg_u32(k)
406        .launch(stream)
407}
408
409/// Dual-projection GEMV for 2 tokens (K+V or any 2 weight matrices).
410///
411/// Reads each weight matrix once, produces 2 output vectors per projection.
412/// `blockIdx.z` selects projection 0 or 1.
413///
414/// Kernel: `w4a16_gemv_dual_batch2(A, B0, S0, s2_0, C0, B1, S1, s2_1, C1, N, K)`
415/// Grid: (ceil(N/4), 1, 2)  Block: (256, 1, 1)
416/// Input A: [2, K], Output C0: [2, N], C1: [2, N].
417#[allow(clippy::too_many_arguments)]
418pub fn w4a16_gemv_dual_batch2(
419    gpu: &dyn GpuBackend,
420    kernel: KernelHandle,
421    input: DevicePtr,
422    weight0: &QuantizedWeight,
423    output0: DevicePtr,
424    weight1: &QuantizedWeight,
425    output1: DevicePtr,
426    n: u32,
427    k: u32,
428    stream: u64,
429) -> Result<()> {
430    KernelLaunch::new(gpu, kernel)
431        .grid([div_ceil(n, 4), 1, 2])
432        .block([256, 1, 1])
433        .arg_ptr(input)
434        .arg_ptr(weight0.weight)
435        .arg_ptr(weight0.weight_scale)
436        .arg_f32(weight0.weight_scale_2)
437        .arg_ptr(output0)
438        .arg_ptr(weight1.weight)
439        .arg_ptr(weight1.weight_scale)
440        .arg_f32(weight1.weight_scale_2)
441        .arg_ptr(output1)
442        .arg_u32(n)
443        .arg_u32(k)
444        .launch(stream)
445}
446
447// ── Position embeddings ────────────────────────────────────────────