metrale_model_layers/layers/ops/
gemv_q2.rs1use anyhow::Result;
13use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
14use metrale_gpu_runtime::kernel_args::{KernelLaunch, div_ceil};
15
16use crate::weight_map::PackedQ2Weight;
17
18pub fn q2_0_gemv(
22 gpu: &dyn GpuBackend,
23 kernel: KernelHandle,
24 input: DevicePtr,
25 weight: &PackedQ2Weight,
26 output: DevicePtr,
27 stream: u64,
28) -> Result<()> {
29 KernelLaunch::new(gpu, kernel)
30 .grid([div_ceil(weight.n, 4), 1, 1])
31 .block([256, 1, 1])
32 .arg_ptr(input)
33 .arg_ptr(weight.weight)
34 .arg_ptr(output)
35 .arg_u32(weight.n)
36 .arg_u32(weight.k)
37 .arg_u32(weight.group as u32)
38 .launch(stream)
39}
40
41#[allow(clippy::too_many_arguments)]
47pub fn q2_0_gemv_batchm(
48 gpu: &dyn GpuBackend,
49 kernel: KernelHandle,
50 input: DevicePtr,
51 weight: &PackedQ2Weight,
52 output: DevicePtr,
53 m: u32,
54 stream: u64,
55) -> Result<()> {
56 KernelLaunch::new(gpu, kernel)
57 .grid([div_ceil(weight.n, 4), 1, 1])
58 .block([256, 1, 1])
59 .arg_ptr(input)
60 .arg_ptr(weight.weight)
61 .arg_ptr(output)
62 .arg_u32(weight.n)
63 .arg_u32(weight.k)
64 .arg_u32(weight.group as u32)
65 .arg_u32(m)
66 .launch(stream)
67}
68
69#[allow(clippy::too_many_arguments)]
77pub fn dequant_q2_0_gn_to_bf16(
78 gpu: &dyn GpuBackend,
79 kernel: KernelHandle,
80 blocks: DevicePtr,
81 out: DevicePtr,
82 n: u32,
83 k: u32,
84 group: u32,
85 stream: u64,
86) -> Result<()> {
87 let n_blocks = n * (k / group);
88 let block_bytes = 2 + group / 4;
89 KernelLaunch::new(gpu, kernel)
90 .grid([n_blocks, 1, 1])
91 .block([256, 1, 1])
92 .arg_ptr(blocks)
93 .arg_ptr(out)
94 .arg_u32(n_blocks)
95 .arg_u32(group)
96 .arg_u32(block_bytes)
97 .launch(stream)
98}
99
100#[cfg(test)]
101mod tests {
102 use half::f16;
103
104 fn pack_block(d: f32, codes: &[u8]) -> Vec<u8> {
108 let group = codes.len();
109 let mut b = Vec::with_capacity(2 + group / 4);
110 b.extend_from_slice(&f16::from_f32(d).to_le_bytes());
111 for chunk in codes.chunks(4) {
112 let mut byte = 0u8;
113 for (t, &c) in chunk.iter().enumerate() {
114 debug_assert!(c < 4);
115 byte |= (c & 3) << (2 * t as u8);
116 }
117 b.push(byte);
118 }
119 b
120 }
121
122 fn packed_dot(row_bytes: &[u8], a: &[f32], group: usize) -> f32 {
126 let block_bytes = 2 + group / 4;
127 let blocks = a.len() / group;
128 let mut acc = 0.0f32;
129 for b in 0..blocks {
130 let blk = &row_bytes[b * block_bytes..(b + 1) * block_bytes];
131 let d = f16::from_le_bytes([blk[0], blk[1]]).to_f32();
132 let qs = &blk[2..];
133 for j in 0..group {
134 let code = (qs[j >> 2] >> (2 * (j & 3))) & 3;
135 acc += a[b * group + j] * ((code as i32 - 1) as f32) * d;
136 }
137 }
138 acc
139 }
140
141 fn dense_dot(codes: &[u8], d: &[f32], a: &[f32], group: usize) -> f32 {
144 (0..a.len())
145 .map(|k| a[k] * ((codes[k] as i32 - 1) as f32) * d[k / group])
146 .sum()
147 }
148
149 #[test]
150 fn q2_block_layout_reference_matches_dense_for_supported_groups() {
151 for group in [64usize, 128] {
152 let k = group * 2;
153 let codes: Vec<u8> = (0..k).map(|i| ((i * 7 + 3) % 4) as u8).collect();
154 let a: Vec<f32> = (0..k).map(|i| ((i % 11) as f32 - 5.0) * 0.25).collect();
155 let d = [0.0123f32, -0.0456f32];
156
157 let mut row = Vec::new();
158 row.extend(pack_block(d[0], &codes[0..group]));
159 row.extend(pack_block(d[1], &codes[group..]));
160 assert_eq!(row.len(), 2 * (2 + group / 4), "group {group}");
161
162 let got = packed_dot(&row, &a, group);
163 let want = dense_dot(&codes, &d, &a, group);
164 assert!(
165 (got - want).abs() < 1e-3,
166 "group {group}: packed {got} vs dense {want}"
167 );
168 }
169 }
170
171 #[test]
172 fn ternary_symbols_are_code_minus_one() {
173 let group = 4usize;
175 let codes = [0u8, 1, 2, 3];
176 let a = [1.0f32, 1.0, 1.0, 1.0];
177 let d = [2.0f32];
178 let row = pack_block(d[0], &codes);
179 assert!((packed_dot(&row, &a, group) - 4.0).abs() < 1e-4);
181 assert_eq!(row[2], 0xE4);
183 }
184
185 #[test]
186 fn shipped_kernels_use_the_q2_layout_and_symbol_mapping() {
187 let baseline = include_str!("../../../../../kernels/gb10/common/q2_0_gemv.cu");
188 let vector = include_str!("../../../../../kernels/gb10/common/q2_0_gemv_vec.cu");
189 let strip_comments = |source: &str| {
190 source
191 .lines()
192 .map(|line| line.split_once("//").map_or(line, |(code, _)| code))
193 .collect::<Vec<_>>()
194 .join("\n")
195 };
196 let baseline = strip_comments(baseline);
197 let vector = strip_comments(vector);
198
199 assert_eq!(
200 baseline
201 .matches("const unsigned int block_bytes = 2u + group / 4u;")
202 .count(),
203 2,
204 "baseline single-row and batch kernels must use the inline-scale layout"
205 );
206 assert_eq!(
207 baseline.matches("const float d = q2_rd_f16(blk);").count(),
208 2
209 );
210 assert_eq!(
211 baseline
212 .matches("const unsigned char* qs = blk + 2;")
213 .count(),
214 2
215 );
216 assert!(baseline.contains("acc += a * (float)(code - 1) * d;"));
217 assert!(baseline.contains("const float wv = (float)(code - 1) * d;"));
218
219 assert_eq!(
220 vector
221 .matches("const unsigned int block_bytes = 2u + group / 4u;")
222 .count(),
223 2,
224 "vector single-row and batch kernels must use the inline-scale layout"
225 );
226 assert_eq!(
227 vector.matches("const float d = q2v_rd_f16(blk);").count(),
228 2
229 );
230 assert_eq!(vector.matches("q2v_rd_u32(blk + 2 + jg * 4u)").count(), 2);
231 assert!(vector.contains("acc += a[j] * (float)(code - 1) * d;"));
232 assert!(vector.contains("wv[j] = (float)((int)((codes >> (2 * j)) & 3u) - 1) * d;"));
233 }
234}