1use anyhow::Result;
16use spark_runtime::gpu::{DevicePtr, GpuBackend, KernelHandle};
17use spark_runtime::kernel_args::{KernelLaunch, div_ceil};
18
19use crate::weight_map::PackedQ2Weight;
20
21pub const QK2_0: u32 = 128;
23pub const Q2_0_BLOCK_BYTES: usize = 34;
25
26pub fn native_q2_mmq_enabled() -> bool {
31 std::env::var("ATLAS_GGUF_NATIVE_Q2_MMQ").ok().as_deref() == Some("1")
32}
33
34pub fn q2_0_weight_bytes(n: u32, k: u32) -> usize {
36 (n as usize) * (k as usize / QK2_0 as usize) * Q2_0_BLOCK_BYTES
37}
38
39#[allow(clippy::too_many_arguments)]
46pub fn q2_0_mmq_gemm(
47 gpu: &dyn GpuBackend,
48 kernel_nc: KernelHandle, kernel_wc: KernelHandle, a_q8: DevicePtr, w_q2_0: DevicePtr, out_bf16: DevicePtr,
53 m: u32,
54 n: u32,
55 k: u32,
56 stream: u64,
57) -> Result<()> {
58 let kernel = if !n.is_multiple_of(128) {
59 kernel_wc
60 } else {
61 kernel_nc
62 };
63 KernelLaunch::new(gpu, kernel)
64 .grid([div_ceil(n, 128), div_ceil(m, 128), 1])
65 .block([32, 8, 1])
66 .shared_mem(super::q4k_mmq::Q4K_MMQ_SMEM)
67 .arg_ptr(w_q2_0) .arg_ptr(a_q8) .arg_ptr(out_bf16) .arg_u32(n) .arg_u32(m) .arg_u32(k) .arg_u32(k / QK2_0) .arg_u32(m) .arg_u32(n) .launch(stream)
77}
78
79pub fn q2_0_mmq_gemm_packed(
83 gpu: &dyn GpuBackend,
84 kernel_nc: KernelHandle,
85 kernel_wc: KernelHandle,
86 a_q8: DevicePtr,
87 w: &PackedQ2Weight,
88 out_bf16: DevicePtr,
89 m: u32,
90 stream: u64,
91) -> Result<()> {
92 anyhow::ensure!(
93 w.group == 128,
94 "Q2_0 MMQ requires group 128 (got {}); use the transient-dequant path for group 64",
95 w.group
96 );
97 q2_0_mmq_gemm(
98 gpu, kernel_nc, kernel_wc, a_q8, w.weight, out_bf16, m, w.n, w.k, stream,
99 )
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 use half::f16;
107
108 fn f32_to_f16_bits(x: f32) -> u16 {
111 f16::from_f32(x).to_bits()
112 }
113 fn f16_bits_to_f32(bits: u16) -> f32 {
114 f16::from_bits(bits).to_f32()
115 }
116
117 fn pack_q2_0(codes: &[u8], scales: &[f32], n: usize, k: usize) -> Vec<u8> {
121 assert_eq!(k % 128, 0);
122 let blocks_per_row = k / 128;
123 let mut out = vec![0u8; n * blocks_per_row * Q2_0_BLOCK_BYTES];
124 for row in 0..n {
125 for b in 0..blocks_per_row {
126 let blk = (row * blocks_per_row + b) * Q2_0_BLOCK_BYTES;
127 let dbits = f32_to_f16_bits(scales[row * blocks_per_row + b]);
128 out[blk] = (dbits & 0xff) as u8;
129 out[blk + 1] = (dbits >> 8) as u8;
130 for j in 0..128 {
131 let c = codes[row * k + b * 128 + j] & 0x3;
132 let byte = blk + 2 + j / 4;
133 out[byte] |= c << (2 * (j % 4));
134 }
135 }
136 }
137 out
138 }
139
140 fn mmq_cpu(
145 act: &[f32],
146 codes: &[u8],
147 scales: &[f32],
148 m: usize,
149 n: usize,
150 k: usize,
151 ) -> Vec<f32> {
152 let bpr = k / 128; let ng = k / 32;
155 let mut a_q = vec![0i8; m * k];
156 let mut a_d = vec![0f32; m * ng];
157 for r in 0..m {
158 for g in 0..ng {
159 let mut amax = 0f32;
160 for t in 0..32 {
161 amax = amax.max(act[r * k + g * 32 + t].abs());
162 }
163 let d = amax / 127.0;
164 a_d[r * ng + g] = d;
165 for t in 0..32 {
166 let q = if d > 0.0 {
167 (act[r * k + g * 32 + t] / d).round().clamp(-127.0, 127.0)
168 } else {
169 0.0
170 };
171 a_q[r * k + g * 32 + t] = q as i8;
172 }
173 }
174 }
175 let mut out = vec![0f32; m * n];
176 for r in 0..m {
177 for col in 0..n {
178 let mut acc = 0f32;
179 for g in 0..ng {
180 let b = g / 4; let dw = f16_bits_to_f32(f32_to_f16_bits(scales[col * bpr + b]));
182 let da = a_d[r * ng + g];
183 let mut isum = 0i32;
184 for t in 0..32 {
185 let ki = g * 32 + t;
186 let w = (codes[col * k + ki] & 0x3) as i32 - 1;
187 isum += w * a_q[r * k + ki] as i32;
188 }
189 acc += isum as f32 * dw * da;
190 }
191 out[r * n + col] = acc;
192 }
193 }
194 out
195 }
196
197 fn oracle(act: &[f32], codes: &[u8], scales: &[f32], m: usize, n: usize, k: usize) -> Vec<f32> {
199 let bpr = k / 128;
200 let mut out = vec![0f32; m * n];
201 for r in 0..m {
202 for col in 0..n {
203 let mut acc = 0f32;
204 for ki in 0..k {
205 let b = ki / 128;
206 let dw = f16_bits_to_f32(f32_to_f16_bits(scales[col * bpr + b]));
207 let w = ((codes[col * k + ki] & 0x3) as i32 - 1) as f32 * dw;
208 acc += w * act[r * k + ki];
209 }
210 out[r * n + col] = acc;
211 }
212 }
213 out
214 }
215
216 #[test]
217 fn q2_0_block_layout_matches_spec() {
218 assert_eq!(Q2_0_BLOCK_BYTES, 2 + 128 / 4);
220 assert_eq!(QK2_0, 128);
221 assert_eq!(q2_0_weight_bytes(256, 512), 256 * (512 / 128) * 34);
222 let codes: Vec<u8> = (0..128).map(|j| (j % 3) as u8).collect();
224 let packed = pack_q2_0(&codes, &[0.5], 1, 128);
225 assert_eq!(packed.len(), 34);
226 for j in 0..128usize {
227 let got = (packed[2 + j / 4] >> (2 * (j % 4))) & 0x3;
228 assert_eq!(got, (j % 3) as u8, "code {j} mispacked");
229 }
230
231 let launcher = include_str!("../../../../../kernels/gb10/qwen3.6-27b/nvfp4/q2_0_mmq.cu");
232 let vendor =
233 include_str!("../../../../../kernels/gb10/qwen3.6-27b/nvfp4/q4k_vendor/mmq.cuh");
234 assert!(launcher.contains("constexpr ggml_type type = GGML_TYPE_Q2_0;"));
235 assert!(launcher.contains("atlas_q2_0_tile<128, false>"));
236 assert!(launcher.contains("atlas_q2_0_tile<128, true>"));
237 assert!(vendor.contains("load_tiles = load_tiles_q2_0<mmq_y, need_check>;"));
238 assert!(vendor.contains(
239 "vec_dot_mma = vec_dot_q8_0_q8_1_mma<mmq_x, mmq_y, MMQ_Q8_1_DS_LAYOUT_DS4>;"
240 ));
241 assert_eq!(
242 vendor.matches("& 0x3) - 1;").count(),
243 4,
244 "the Q2 unpack must subtract one from all four packed codes"
245 );
246 }
247
248 #[test]
249 fn q2_0_mmq_reference_arithmetic_matches_fp_oracle() {
250 let (m, n, k) = (3usize, 5usize, 256usize);
252 let bpr = k / 128;
253 let mut codes = vec![0u8; n * k];
255 for (i, c) in codes.iter_mut().enumerate() {
256 *c = (((i * 2654435761usize) >> 5) % 3) as u8; }
258 let mut scales = vec![0f32; n * bpr];
259 for (i, s) in scales.iter_mut().enumerate() {
260 *s = 0.015 + 0.01 * ((i % 7) as f32); }
262 let mut act = vec![0f32; m * k];
263 for (i, a) in act.iter_mut().enumerate() {
264 let x = (i as f32 * 0.12345).sin();
265 *a = x * 1.7;
266 }
267
268 let mmq = mmq_cpu(&act, &codes, &scales, m, n, k);
269 let orc = oracle(&act, &codes, &scales, m, n, k);
270
271 let mut max_rel = 0f32;
274 let mut denom = 0f32;
275 let mut num = 0f32;
276 for i in 0..m * n {
277 let e = (mmq[i] - orc[i]).abs();
278 assert!(
279 e <= 0.02 * orc[i].abs().max(0.5),
280 "idx {i}: mmq {} vs oracle {}",
281 mmq[i],
282 orc[i]
283 );
284 num += e * e;
285 denom += orc[i] * orc[i];
286 let r = e / orc[i].abs().max(1e-3);
287 max_rel = max_rel.max(r);
288 }
289 let l2_rel = (num / denom.max(1e-12)).sqrt();
290 assert!(
291 l2_rel < 1e-2,
292 "Q2_0 MMQ L2 rel_err {l2_rel:.4e} exceeds 1e-2 (max pointwise {max_rel:.4e})"
293 );
294 }
295}