metrale_model_layers/layers/ops/
gemv_q2_vec.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Launchers for the vectorized packed Q2_0 decode GEMVs (kernels/gb10/common/q2_0_gemv_vec.cu), which `dense_ffn` loads for decode.
4//!
5//! Owner: model-layers ops.
6//! Invariants:
7//! - The call surface matches [`super::gemv_q2`]: the fp16 scale is inside each `block_q2_0`,
8//!   so there is no scale pointer.
9//! - One warp per output row and 8 rows per 256-thread CTA, so every grid is `ceil(N/8)`
10//!   (`WARPS_PER_BLOCK` in the kernel file).
11//! - No batched launch passes more than `Q2_BATCHM_MAX_M` rows to the kernel.
12
13use anyhow::Result;
14use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
15use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
16
17use crate::weight_map::PackedQ2Weight;
18
19/// 2026-09-25: Q2_0 GEMV for M=1 decode: `C[1,N] = A[1,K] @ dequant(B)^T`. Each lane reads one
20/// `uint32` of 16 codes, and the CTA stages the activation tile in shared memory. `A` and `C`
21/// are BF16; `B` is the raw `block_q2_0` buffer; each weight is `(code-1)*d`.
22pub fn q2_0_gemv_vec(
23    gpu: &dyn GpuBackend,
24    kernel: KernelHandle,
25    input: DevicePtr,
26    weight: &PackedQ2Weight,
27    output: DevicePtr,
28    stream: u64,
29) -> Result<()> {
30    KernelLaunch::new(gpu, kernel)
31        .grid([div_ceil(weight.n, 8), 1, 1])
32        .block([256, 1, 1])
33        .arg_ptr(input)
34        .arg_ptr(weight.weight)
35        .arg_ptr(output)
36        .arg_u32(weight.n)
37        .arg_u32(weight.k)
38        .arg_u32(weight.group as u32)
39        .launch(stream)
40}
41
42/// 2026-09-25: Row cap of the `q2_0_gemv_vec_batchm` kernel (`MAX_M` in the .cu). The kernel
43/// stages `M` activation rows into `s_A[MAX_M * TILE_K]`, so `M > MAX_M` writes past that
44/// tile, and its compute and store loops stop at `MAX_M`, so rows from 8 on are never
45/// written. [`q2_0_gemv_vec_batchm`] chunks larger `m` itself.
46pub const Q2_BATCHM_MAX_M: u32 = 8;
47
48/// 2026-09-25: Row groups for driving the batchm kernel at any `m`: yields `(r0, m_chunk)`
49/// with `1 <= m_chunk <= Q2_BATCHM_MAX_M`, contiguous from 0 and summing to `m` (empty when
50/// `m == 0`). The kernel computes each row with its own accumulator, so a row's result does
51/// not depend on which chunk it is in.
52fn batchm_row_chunks(m: u32) -> impl Iterator<Item = (u32, u32)> {
53    (0..m)
54        .step_by(Q2_BATCHM_MAX_M as usize)
55        .map(move |r0| (r0, (m - r0).min(Q2_BATCHM_MAX_M)))
56}
57
58/// 2026-09-25: `(input_byte_offset, output_byte_offset, rows)` for each kernel launch, with
59/// BF16 rows of `k` and `n` elements.
60fn batchm_launch_chunks(m: u32, k: u32, n: u32) -> impl Iterator<Item = (usize, usize, u32)> {
61    batchm_row_chunks(m).map(move |(r0, rows)| {
62        (
63            r0 as usize * k as usize * 2,
64            r0 as usize * n as usize * 2,
65            rows,
66        )
67    })
68}
69
70/// 2026-09-25: Batched Q2_0 GEMV for any `m`: `C[M,N] = A[M,K] @ dequant(B)^T`, `A` and `C`
71/// row-major BF16. Each weight word is read once per launch and applied to every staged row.
72///
73/// `m` is split into launches of at most `Q2_BATCHM_MAX_M` rows, with the input and output
74/// pointers advanced by whole rows; `m <= 8` is one launch, and `m == 0` launches nothing.
75#[allow(clippy::too_many_arguments)]
76pub fn q2_0_gemv_vec_batchm(
77    gpu: &dyn GpuBackend,
78    kernel: KernelHandle,
79    input: DevicePtr,
80    weight: &PackedQ2Weight,
81    output: DevicePtr,
82    m: u32,
83    stream: u64,
84) -> Result<()> {
85    for (input_offset, output_offset, m_chunk) in batchm_launch_chunks(m, weight.k, weight.n) {
86        KernelLaunch::new(gpu, kernel)
87            .grid([div_ceil(weight.n, 8), 1, 1])
88            .block([256, 1, 1])
89            .arg_ptr(input.offset(input_offset))
90            .arg_ptr(weight.weight)
91            .arg_ptr(output.offset(output_offset))
92            .arg_u32(weight.n)
93            .arg_u32(weight.k)
94            .arg_u32(weight.group as u32)
95            .arg_u32(m_chunk)
96            .launch(stream)?;
97    }
98    Ok(())
99}
100
101#[cfg(test)]
102mod tests {
103    use super::{batchm_launch_chunks, batchm_row_chunks};
104
105    use half::{bf16, f16};
106
107    // 2026-09-25: The scale `d` is stored as fp16 in each block, so the model rounds it
108    // through fp16 as the kernel's scale read does.
109    fn f16_rt(x: f32) -> f32 {
110        f16::from_f32(x).to_f32()
111    }
112
113    /// 2026-09-25: Host model of one activation row through the Q2_0 GEMV at group 128:
114    /// `out[n] = sum_k a[k] * (code(n,k)-1) * d(n, k/128)` in fp32, returned as BF16 bits.
115    fn gemv_row(a: &[f32], codes: &[u8], scales: &[f32], n: usize, k: usize) -> Vec<u16> {
116        let bpr = k / 128; // 2026-09-25: group-128 blocks per weight row
117        let mut out = vec![0u16; n];
118        for (col, o) in out.iter_mut().enumerate() {
119            let mut acc = 0f32;
120            for ki in 0..k {
121                let d = f16_rt(scales[col * bpr + ki / 128]);
122                let w = ((codes[col * k + ki] & 0x3) as i32 - 1) as f32 * d;
123                acc += a[ki] * w;
124            }
125            *o = bf16::from_f32(acc).to_bits();
126        }
127        out
128    }
129
130    fn gen_inputs(m: usize, n: usize, k: usize) -> (Vec<f32>, Vec<u8>, Vec<f32>) {
131        let mut act = vec![0f32; m * k];
132        for (i, a) in act.iter_mut().enumerate() {
133            *a = (i as f32 * 0.09131).sin() * 1.3;
134        }
135        let mut codes = vec![0u8; n * k];
136        for (i, c) in codes.iter_mut().enumerate() {
137            *c = (((i * 2654435761usize) >> 6) % 3) as u8;
138        }
139        let mut scales = vec![0f32; n * (k / 128)];
140        for (i, s) in scales.iter_mut().enumerate() {
141            *s = 0.015 + 0.01 * ((i % 5) as f32);
142        }
143        (act, codes, scales)
144    }
145
146    #[test]
147    fn batchm_row_chunks_cover_all_rows_exactly() {
148        for &m in &[0u32, 1, 2, 3, 7, 8, 9, 15, 16, 17] {
149            let chunks: Vec<(u32, u32)> = batchm_row_chunks(m).collect();
150            let mut next_r0 = 0u32;
151            let mut total = 0u32;
152            for &(r0, mc) in &chunks {
153                assert_eq!(r0, next_r0, "M={m}: chunk starts must be contiguous");
154                assert!(
155                    (1..=super::Q2_BATCHM_MAX_M).contains(&mc),
156                    "M={m}: bad chunk {mc}"
157                );
158                next_r0 += mc;
159                total += mc;
160            }
161            assert_eq!(total, m, "M={m}: chunks must cover every row once");
162            assert_eq!(chunks.is_empty(), m == 0);
163        }
164    }
165
166    #[test]
167    fn launch_chunks_advance_bf16_input_and_output_rows_independently() {
168        assert_eq!(
169            batchm_launch_chunks(17, 256, 5).collect::<Vec<_>>(),
170            vec![(0, 0, 8), (4096, 80, 8), (8192, 160, 1)]
171        );
172    }
173
174    /// 2026-09-25: Walks the launch chunks of `q2_0_gemv_vec_batchm` on the host and checks that
175    /// the input and output offsets address every row exactly once, across the 8-row chunk
176    /// boundaries, by comparing against a per-row pass of the host model.
177    #[test]
178    fn chunked_batchm_equals_per_row_gemv() {
179        let (n, k) = (5usize, 256usize);
180        for &m in &[1usize, 2, 3, 8, 9, 16] {
181            let (act, codes, scales) = gen_inputs(m, n, k);
182
183            let mut reference = vec![0u16; m * n];
184            for row in 0..m {
185                let r = gemv_row(&act[row * k..(row + 1) * k], &codes, &scales, n, k);
186                reference[row * n..(row + 1) * n].copy_from_slice(&r);
187            }
188
189            let mut got = vec![0u16; m * n];
190            for (input_offset, output_offset, mc) in
191                batchm_launch_chunks(m as u32, k as u32, n as u32)
192            {
193                let r0 = input_offset / (k * 2);
194                assert_eq!(output_offset, r0 * n * 2);
195                for i in 0..mc as usize {
196                    let row = r0 + i;
197                    let r = gemv_row(&act[row * k..(row + 1) * k], &codes, &scales, n, k);
198                    got[row * n..(row + 1) * n].copy_from_slice(&r);
199                }
200            }
201            assert_eq!(
202                got, reference,
203                "M={m}: chunked batchm must equal per-row M=1 GEMV bit-for-bit"
204            );
205        }
206    }
207}