spark_model/layers/ops/gemv_q2_vec.rs
1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Launch wrappers for CANDIDATE B of the native keep-packed ternary Q2_0
4//! decode GEMV (`kernels/gb10/common/q2_0_gemv_vec.cu`, module stem
5//! `q2_0_gemv_vec`).
6//!
7//! Same call surface as [`super::gemv_q2`] — the Q2_0 weight carries its fp16
8//! scale INLINE in each `block_q2_0`, so there is no separate scale-pointer
9//! argument. The only launch-geometry difference is the CANDIDATE-B thread map:
10//! ONE warp per output row, EIGHT rows per 256-thread CTA, so the grid is
11//! `(ceil(N/8),1,1)` (vs the 2-warp/4-row `ceil(N/4)` of the baseline kernel).
12
13use anyhow::Result;
14use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
15use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
16
17use crate::weight_map::PackedQ2Weight;
18
19/// Q2_0 GEMV (M=1 decode), CANDIDATE B: `C[1,N] = A[1,K] @ dequant(B)`.
20///
21/// Vectorized code loads (one `uint32` = 16 ternary codes per lane) + shared-
22/// memory activation staging. `A` BF16 `[1,K]`, `B` raw `block_q2_0`, `C` BF16
23/// `[1,N]`. Dequant `(code-1)*d` happens inside the dot-product.
24///
25/// Kernel: `q2_0_gemv_vec(A, B, C, N, K, group)` Grid: (ceil(N/8),1,1) Block: (256,1,1)
26pub fn q2_0_gemv_vec(
27 gpu: &dyn GpuBackend,
28 kernel: KernelHandle,
29 input: DevicePtr,
30 weight: &PackedQ2Weight,
31 output: DevicePtr,
32 stream: u64,
33) -> Result<()> {
34 KernelLaunch::new(gpu, kernel)
35 .grid([div_ceil(weight.n, 8), 1, 1])
36 .block([256, 1, 1])
37 .arg_ptr(input)
38 .arg_ptr(weight.weight)
39 .arg_ptr(output)
40 .arg_u32(weight.n)
41 .arg_u32(weight.k)
42 .arg_u32(weight.group as u32)
43 .launch(stream)
44}
45
46/// Shared-memory row cap of `q2_0_gemv_vec_batchm` (`MAX_M` in the .cu). The
47/// kernel stages exactly `M` activation rows in `s_A[MAX_M * TILE_K]`; passing
48/// `M > MAX_M` overflows that tile (OOB smem write) AND drops output rows >= 8
49/// (compute/write loops iterate `m < MAX_M`). Callers with more rows MUST chunk
50/// — done transparently by [`q2_0_gemv_vec_batchm`].
51pub const Q2_BATCHM_MAX_M: u32 = 8;
52
53/// Row-group boundaries for driving the batchm kernel at arbitrary `m`: yields
54/// `(r0, m_chunk)` with `1 <= m_chunk <= Q2_BATCHM_MAX_M`, contiguous from 0,
55/// summing to `m` (empty when `m == 0`). Each output row is independent and the
56/// kernel is bit-consistent with M=1, so splitting by rows is numerically inert.
57fn batchm_row_chunks(m: u32) -> impl Iterator<Item = (u32, u32)> {
58 (0..m)
59 .step_by(Q2_BATCHM_MAX_M as usize)
60 .map(move |r0| (r0, (m - r0).min(Q2_BATCHM_MAX_M)))
61}
62
63/// `(input_byte_offset, output_byte_offset, rows)` for each kernel launch.
64fn batchm_launch_chunks(m: u32, k: u32, n: u32) -> impl Iterator<Item = (usize, usize, u32)> {
65 batchm_row_chunks(m).map(move |(r0, rows)| {
66 (
67 r0 as usize * k as usize * 2,
68 r0 as usize * n as usize * 2,
69 rows,
70 )
71 })
72}
73
74/// Q2_0 batched GEMV (M>=1 decode), CANDIDATE B: `C[M,N] = A[M,K] @ dequant(B)`.
75///
76/// Reads each weight word once and MAC's it into all `m` accumulators (all `m`
77/// activation rows staged in smem). `A` BF16 `[M,K]` row-major, `C` BF16
78/// `[M,N]` row-major. Bit-consistent with running the M=1 kernel `M` times.
79///
80/// The kernel itself is capped at `Q2_BATCHM_MAX_M` rows/launch, so `m` beyond
81/// that is served by CHUNKING: successive <=8-row launches with the `[M,K]`
82/// input and `[M,N]` output base pointers advanced by whole rows (BF16 = 2 B).
83/// Chunking a caller with `m <= 8` costs one launch (identical to the direct
84/// call); it exists so a wide concurrent-decode step (max-num-seqs up to 16)
85/// can never drive the kernel into its OOB path.
86///
87/// Kernel: `q2_0_gemv_vec_batchm(A, B, C, N, K, group, M)`.
88#[allow(clippy::too_many_arguments)]
89pub fn q2_0_gemv_vec_batchm(
90 gpu: &dyn GpuBackend,
91 kernel: KernelHandle,
92 input: DevicePtr,
93 weight: &PackedQ2Weight,
94 output: DevicePtr,
95 m: u32,
96 stream: u64,
97) -> Result<()> {
98 // BF16 row strides: input is [M,K], output is [M,N].
99 for (input_offset, output_offset, m_chunk) in batchm_launch_chunks(m, weight.k, weight.n) {
100 KernelLaunch::new(gpu, kernel)
101 .grid([div_ceil(weight.n, 8), 1, 1])
102 .block([256, 1, 1])
103 .arg_ptr(input.offset(input_offset))
104 .arg_ptr(weight.weight)
105 .arg_ptr(output.offset(output_offset))
106 .arg_u32(weight.n)
107 .arg_u32(weight.k)
108 .arg_u32(weight.group as u32)
109 .arg_u32(m_chunk)
110 .launch(stream)?;
111 }
112 Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117 use super::{batchm_launch_chunks, batchm_row_chunks};
118
119 use half::{bf16, f16};
120
121 // Weight scale `d` is fp16-precise (stored inline fp16 in each block_q2_0);
122 // round-trip it exactly as the kernel's inline scale read does.
123 fn f16_rt(x: f32) -> f32 {
124 f16::from_f32(x).to_f32()
125 }
126
127 /// One output row of the Q2_0 GEMV — `out[n] = sum_k a[k] * (code(n,k)-1) *
128 /// d(n, k/128)` in fp32, then BF16 store. This is BOTH the M=1 kernel and any
129 /// single `m`-row of `q2_0_gemv_vec_batchm` (the batchm kernel keeps M
130 /// independent fp32 accumulators over the SAME per-row arithmetic), so a
131 /// batched output row must equal this bit-for-bit. Returns BF16 bit patterns.
132 fn gemv_row(a: &[f32], codes: &[u8], scales: &[f32], n: usize, k: usize) -> Vec<u16> {
133 let bpr = k / 128; // group-128 blocks per weight row
134 let mut out = vec![0u16; n];
135 for (col, o) in out.iter_mut().enumerate() {
136 let mut acc = 0f32;
137 for ki in 0..k {
138 let d = f16_rt(scales[col * bpr + ki / 128]);
139 let w = ((codes[col * k + ki] & 0x3) as i32 - 1) as f32 * d;
140 acc += a[ki] * w;
141 }
142 *o = bf16::from_f32(acc).to_bits();
143 }
144 out
145 }
146
147 fn gen_inputs(m: usize, n: usize, k: usize) -> (Vec<f32>, Vec<u8>, Vec<f32>) {
148 let mut act = vec![0f32; m * k];
149 for (i, a) in act.iter_mut().enumerate() {
150 *a = (i as f32 * 0.09131).sin() * 1.3;
151 }
152 let mut codes = vec![0u8; n * k];
153 for (i, c) in codes.iter_mut().enumerate() {
154 *c = (((i * 2654435761usize) >> 6) % 3) as u8; // ternary {0,1,2}
155 }
156 let mut scales = vec![0f32; n * (k / 128)];
157 for (i, s) in scales.iter_mut().enumerate() {
158 *s = 0.015 + 0.01 * ((i % 5) as f32);
159 }
160 (act, codes, scales)
161 }
162
163 #[test]
164 fn batchm_row_chunks_cover_all_rows_exactly() {
165 for &m in &[0u32, 1, 2, 3, 7, 8, 9, 15, 16, 17] {
166 let chunks: Vec<(u32, u32)> = batchm_row_chunks(m).collect();
167 let mut next_r0 = 0u32;
168 let mut total = 0u32;
169 for &(r0, mc) in &chunks {
170 assert_eq!(r0, next_r0, "M={m}: chunk starts must be contiguous");
171 assert!(
172 (1..=super::Q2_BATCHM_MAX_M).contains(&mc),
173 "M={m}: bad chunk {mc}"
174 );
175 next_r0 += mc;
176 total += mc;
177 }
178 assert_eq!(total, m, "M={m}: chunks must cover every row once");
179 assert_eq!(chunks.is_empty(), m == 0);
180 }
181 }
182
183 #[test]
184 fn launch_chunks_advance_bf16_input_and_output_rows_independently() {
185 assert_eq!(
186 batchm_launch_chunks(17, 256, 5).collect::<Vec<_>>(),
187 vec![(0, 0, 8), (4096, 80, 8), (8192, 160, 1)]
188 );
189 }
190
191 /// The invariant the wiring relies on: driving the batchm kernel via the
192 /// chunking loop (row-group launches with `r0*K` / `r0*N` pointer offsets)
193 /// reproduces the per-row M=1 GEMV for EVERY row, bit-for-bit — including the
194 /// M>8 chunk boundaries. Models the launch loop in `q2_0_gemv_vec_batchm`.
195 #[test]
196 fn chunked_batchm_equals_per_row_gemv() {
197 let (n, k) = (5usize, 256usize);
198 for &m in &[1usize, 2, 3, 8, 9, 16] {
199 let (act, codes, scales) = gen_inputs(m, n, k);
200
201 // Reference: each row through the standalone M=1 GEMV.
202 let mut reference = vec![0u16; m * n];
203 for row in 0..m {
204 let r = gemv_row(&act[row * k..(row + 1) * k], &codes, &scales, n, k);
205 reference[row * n..(row + 1) * n].copy_from_slice(&r);
206 }
207
208 // Simulate the wrapper: iterate chunk boundaries, offset the [M,K]
209 // input by r0*K and write [M,N] output at r0*N per chunk row.
210 let mut got = vec![0u16; m * n];
211 for (input_offset, output_offset, mc) in
212 batchm_launch_chunks(m as u32, k as u32, n as u32)
213 {
214 let r0 = input_offset / (k * 2);
215 assert_eq!(output_offset, r0 * n * 2);
216 for i in 0..mc as usize {
217 let row = r0 + i;
218 let r = gemv_row(&act[row * k..(row + 1) * k], &codes, &scales, n, k);
219 got[row * n..(row + 1) * n].copy_from_slice(&r);
220 }
221 }
222 assert_eq!(
223 got, reference,
224 "M={m}: chunked batchm must equal per-row M=1 GEMV bit-for-bit"
225 );
226 }
227 }
228}