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}