1use alloc::vec;
13use alloc::vec::Vec;
14#[cfg(all(target_arch = "x86_64", feature = "simd"))]
15use core::sync::atomic::{AtomicU8, Ordering};
16
17pub const GROUP_SIZE: usize = 32;
18pub const SCALE_MIN: f32 = 1e-8;
19
20#[cfg(all(target_arch = "x86_64", feature = "simd"))]
24static AVX2_DETECTED: AtomicU8 = AtomicU8::new(0);
25
26#[cfg(all(target_arch = "x86_64", feature = "simd"))]
27#[inline]
28pub fn has_avx2() -> bool {
29 let cached = AVX2_DETECTED.load(Ordering::Relaxed);
30 if cached != 0 {
31 return cached == 1;
32 }
33 #[allow(unused_unsafe)]
36 let result = unsafe {
37 use core::arch::x86_64::__cpuid_count;
38 let r = __cpuid_count(7, 0);
39 (r.ebx >> 5) & 1 == 1
40 };
41 AVX2_DETECTED.store(if result { 1 } else { 2 }, Ordering::Relaxed);
42 result
43}
44
45pub struct QuantizedWeight {
53 pub data: Vec<u8>,
54 pub scales: Vec<f32>,
55 pub in_feat: usize,
56 pub out_feat: usize,
57 pub num_groups: usize,
58}
59
60impl QuantizedWeight {
61 pub fn quantize(w: &[f32], in_feat: usize, out_feat: usize) -> Self {
64 assert_eq!(w.len(), in_feat * out_feat);
65
66 let gs = GROUP_SIZE.min(in_feat);
67 let pad = (gs - in_feat % gs) % gs;
68 let in_padded = in_feat + pad;
69 let num_groups = in_padded / gs;
70 let num_pairs = in_padded / 2;
71
72 let mut scales = vec![0.0f32; num_groups * out_feat];
74 for g in 0..num_groups {
75 let row_start = g * gs;
76 for o in 0..out_feat {
77 let mut max_abs = 0.0f32;
78 for r in 0..gs {
79 let global_row = row_start + r;
80 let val = if global_row < in_feat {
81 w[global_row * out_feat + o].abs()
82 } else {
83 0.0
84 };
85 if val > max_abs {
86 max_abs = val;
87 }
88 }
89 scales[g * out_feat + o] = (max_abs / 7.0).max(SCALE_MIN);
90 }
91 }
92
93 let mut data = vec![0u8; num_pairs * out_feat];
97 for pair in 0..num_pairs {
98 let r0 = pair * 2;
99 let r1 = pair * 2 + 1;
100 let g = r0 / gs; for o in 0..out_feat {
102 let scale = scales[g * out_feat + o];
103 let v0 = if r0 < in_feat {
104 w[r0 * out_feat + o]
105 } else {
106 0.0
107 };
108 let v1 = if r1 < in_feat {
109 w[r1 * out_feat + o]
110 } else {
111 0.0
112 };
113 let q0 = crate::math::round(v0 / scale).clamp(-8.0, 7.0) as i8;
114 let q1 = crate::math::round(v1 / scale).clamp(-8.0, 7.0) as i8;
115 let lo = (q0 as u8) & 0x0F;
116 let hi = ((q1 as u8) & 0x0F) << 4;
117 data[pair * out_feat + o] = lo | hi;
118 }
119 }
120
121 Self {
122 data,
123 scales,
124 in_feat,
125 out_feat,
126 num_groups,
127 }
128 }
129
130 pub fn dequantize_to(&self, out: &mut [f32]) {
132 assert_eq!(out.len(), self.in_feat * self.out_feat);
133
134 let gs = GROUP_SIZE.min(self.in_feat);
135 let num_pairs = self.num_groups * gs / 2;
136
137 for pair in 0..num_pairs {
138 let r0 = pair * 2;
139 let r1 = pair * 2 + 1;
140 let g = r0 / gs;
141 let base = pair * self.out_feat;
142 for o in 0..self.out_feat {
143 let byte = self.data[base + o];
144 let lo = sign_extend4(byte & 0x0F);
145 let hi = sign_extend4((byte >> 4) & 0x0F);
146 let scale = self.scales[g * self.out_feat + o];
147 if r0 < self.in_feat {
148 out[r0 * self.out_feat + o] = lo as f32 * scale;
149 }
150 if r1 < self.in_feat {
151 out[r1 * self.out_feat + o] = hi as f32 * scale;
152 }
153 }
154 }
155 }
156
157 pub fn matvec(&self, x: &[f32], y: &mut [f32]) {
161 assert_eq!(x.len(), self.in_feat);
162 assert_eq!(y.len(), self.out_feat);
163
164 for v in y.iter_mut() {
165 *v = 0.0;
166 }
167
168 #[cfg(all(target_arch = "x86_64", feature = "simd"))]
169 if has_avx2() {
170 return unsafe { self.matvec_avx2(x, y) };
172 }
173
174 #[cfg(all(target_arch = "aarch64", feature = "simd"))]
176 {
177 return unsafe { self.matvec_neon(x, y) };
178 }
179
180 self.matvec_scalar(x, y);
181 }
182
183 #[allow(clippy::needless_range_loop)]
184 fn matvec_scalar(&self, x: &[f32], y: &mut [f32]) {
185 let gs = GROUP_SIZE.min(self.in_feat);
186 let num_pairs = self.num_groups * gs / 2;
187
188 for pair in 0..num_pairs {
189 let r0 = pair * 2;
190 let r1 = pair * 2 + 1;
191 let g = r0 / gs;
192 let x0 = if r0 < self.in_feat { x[r0] } else { 0.0 };
193 let x1 = if r1 < self.in_feat { x[r1] } else { 0.0 };
194 let base = pair * self.out_feat;
195 let scale_base = g * self.out_feat;
196
197 for o in 0..self.out_feat {
198 let byte = self.data[base + o];
199 let lo = sign_extend4(byte & 0x0F) as f32;
200 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
201 let scale = self.scales[scale_base + o];
202 y[o] += (lo * x0 + hi * x1) * scale;
203 }
204 }
205 }
206
207 #[cfg(all(target_arch = "x86_64", feature = "simd"))]
208 #[target_feature(enable = "avx2")]
209 unsafe fn matvec_avx2(&self, x: &[f32], y: &mut [f32]) {
210 use core::arch::x86_64::*;
211
212 let gs = GROUP_SIZE.min(self.in_feat);
213 let num_pairs = self.num_groups * gs / 2;
214 let out_feat = self.out_feat;
215
216 let mask_lo = _mm256_set1_epi32(0x0F0F0F0F_u32 as i32);
220
221 for pair in 0..num_pairs {
222 let r0 = pair * 2;
223 let r1 = pair * 2 + 1;
224 let g = r0 / gs;
225 let x0 = if r0 < self.in_feat { x[r0] } else { 0.0 };
226 let x1 = if r1 < self.in_feat { x[r1] } else { 0.0 };
227 let x0v = _mm256_set1_ps(x0);
228 let x1v = _mm256_set1_ps(x1);
229
230 let data_base = pair * out_feat;
231 let scale_base = g * out_feat;
232
233 let full_blocks = out_feat / 8;
235 let remainder = out_feat % 8;
236
237 for blk in 0..full_blocks {
238 let o = blk * 8;
239
240 let bytes_128 =
242 _mm_loadl_epi64(self.data[data_base + o..].as_ptr() as *const __m128i);
243 let bytes_256 = _mm256_cvtepu8_epi32(bytes_128);
245
246 let lo_256 = _mm256_and_si256(bytes_256, mask_lo);
248 let hi_256 = _mm256_and_si256(_mm256_srli_epi32(bytes_256, 4), mask_lo);
250
251 let lo_sign = _mm256_and_si256(lo_256, _mm256_set1_epi32(8));
254 let lo_neg_mask = _mm256_cmpeq_epi32(lo_sign, _mm256_set1_epi32(8));
255 let lo_ext = _mm256_and_si256(lo_neg_mask, _mm256_set1_epi32(!0x0F));
256 let lo_i32 = _mm256_or_si256(lo_256, lo_ext);
257
258 let hi_sign = _mm256_and_si256(hi_256, _mm256_set1_epi32(8));
259 let hi_neg_mask = _mm256_cmpeq_epi32(hi_sign, _mm256_set1_epi32(8));
260 let hi_ext = _mm256_and_si256(hi_neg_mask, _mm256_set1_epi32(!0x0F));
261 let hi_i32 = _mm256_or_si256(hi_256, hi_ext);
262
263 let lo_f32 = _mm256_cvtepi32_ps(lo_i32);
265 let hi_f32 = _mm256_cvtepi32_ps(hi_i32);
266
267 let scale_v = _mm256_loadu_ps(self.scales[scale_base + o..].as_ptr());
269
270 let contrib_lo = _mm256_mul_ps(lo_f32, x0v);
273 let contrib = _mm256_fmadd_ps(hi_f32, x1v, contrib_lo);
274 let scaled = _mm256_mul_ps(contrib, scale_v);
275
276 let y_ptr = y[o..].as_mut_ptr();
278 let y_vec = _mm256_loadu_ps(y_ptr);
279 let y_new = _mm256_add_ps(y_vec, scaled);
280 _mm256_storeu_ps(y_ptr, y_new);
281 }
282
283 if remainder > 0 {
285 let o_base = full_blocks * 8;
286 let scale_off = scale_base + o_base;
287 let data_off = data_base + o_base;
288 for o in 0..remainder {
289 let byte = self.data[data_off + o];
290 let lo = sign_extend4(byte & 0x0F) as f32;
291 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
292 let scale = self.scales[scale_off + o];
293 y[o_base + o] += (lo * x0 + hi * x1) * scale;
294 }
295 }
296 }
297 }
298
299 #[cfg(all(target_arch = "aarch64", feature = "simd"))]
304 #[target_feature(enable = "neon")]
305 unsafe fn matvec_neon(&self, x: &[f32], y: &mut [f32]) {
306 use core::arch::aarch64::*;
307
308 let gs = GROUP_SIZE.min(self.in_feat);
309 let num_pairs = self.num_groups * gs / 2;
310 let out_feat = self.out_feat;
311
312 let mask_u8 = vdup_n_u8(0x0F);
313 let eight_u8 = vdup_n_u8(8);
314 let sixteen = vdup_n_u8(16);
315
316 for pair in 0..num_pairs {
317 let r0 = pair * 2;
318 let r1 = pair * 2 + 1;
319 let g = r0 / gs;
320 let x0 = if r0 < self.in_feat {
321 *x.get_unchecked(r0)
322 } else {
323 0.0
324 };
325 let x1 = if r1 < self.in_feat {
326 *x.get_unchecked(r1)
327 } else {
328 0.0
329 };
330 let x0v = vdupq_n_f32(x0);
331 let x1v = vdupq_n_f32(x1);
332
333 let data_base = pair * out_feat;
334 let scale_base = g * out_feat;
335
336 let full8 = out_feat / 8;
338
339 for blk in 0..full8 {
340 let o = blk * 8;
341
342 let bytes = vld1_u8(self.data.get_unchecked(data_base + o) as *const u8);
344
345 let lo_raw = vand_u8(bytes, mask_u8);
347 let hi_raw = vshr_n_u8::<4>(bytes); let lo_s8 = vreinterpret_s8_u8(vsub_u8(
351 lo_raw,
352 vand_u8(vcge_u8(lo_raw, eight_u8), sixteen),
353 ));
354 let hi_s8 = vreinterpret_s8_u8(vsub_u8(
355 hi_raw,
356 vand_u8(vcge_u8(hi_raw, eight_u8), sixteen),
357 ));
358
359 let lo_s16 = vmovl_s8(lo_s8);
361 let hi_s16 = vmovl_s8(hi_s8);
362
363 let lo_f32_lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo_s16)));
365 let hi_f32_lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi_s16)));
366 let scale_lo = vld1q_f32(self.scales.get_unchecked(scale_base + o) as *const f32);
367 let contrib_lo = vmlaq_f32(vmulq_f32(lo_f32_lo, x0v), hi_f32_lo, x1v);
368 let y_ptr_lo = y.get_unchecked_mut(o) as *mut f32;
369 vst1q_f32(
370 y_ptr_lo,
371 vaddq_f32(vld1q_f32(y_ptr_lo), vmulq_f32(contrib_lo, scale_lo)),
372 );
373
374 let lo_f32_hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo_s16)));
376 let hi_f32_hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi_s16)));
377 let scale_hi =
378 vld1q_f32(self.scales.get_unchecked(scale_base + o + 4) as *const f32);
379 let contrib_hi = vmlaq_f32(vmulq_f32(lo_f32_hi, x0v), hi_f32_hi, x1v);
380 let y_ptr_hi = y.get_unchecked_mut(o + 4) as *mut f32;
381 vst1q_f32(
382 y_ptr_hi,
383 vaddq_f32(vld1q_f32(y_ptr_hi), vmulq_f32(contrib_hi, scale_hi)),
384 );
385 }
386
387 let tail_start = full8 * 8;
389 for o in tail_start..out_feat {
390 let byte = *self.data.get_unchecked(data_base + o);
391 let lo = sign_extend4(byte & 0x0F) as f32;
392 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
393 let scale = *self.scales.get_unchecked(scale_base + o);
394 *y.get_unchecked_mut(o) += (lo * x0 + hi * x1) * scale;
395 }
396 }
397 }
398
399 pub fn matmul(&self, x: &[f32], batch: usize, y: &mut [f32]) {
401 assert_eq!(x.len(), batch * self.in_feat);
402 assert_eq!(y.len(), batch * self.out_feat);
403 for b in 0..batch {
404 self.matvec(
405 &x[b * self.in_feat..(b + 1) * self.in_feat],
406 &mut y[b * self.out_feat..(b + 1) * self.out_feat],
407 );
408 }
409 }
410}
411
412#[inline(always)]
413fn sign_extend4(nibble: u8) -> i8 {
414 if nibble & 0x8 != 0 {
415 (nibble | 0xF0) as i8
416 } else {
417 nibble as i8
418 }
419}
420
421#[cfg(test)]
422mod tests {
423 use super::*;
424
425 #[test]
426 fn test_sign_extend() {
427 assert_eq!(sign_extend4(0x7), 7);
428 assert_eq!(sign_extend4(0x8), -8);
429 assert_eq!(sign_extend4(0xF), -1);
430 assert_eq!(sign_extend4(0x0), 0);
431 }
432
433 #[test]
434 fn test_roundtrip_identity() {
435 let in_feat = 64;
436 let out_feat = 32;
437 let mut w = alloc::vec![0.0f32; in_feat * out_feat];
438 for i in 0..in_feat {
439 for o in 0..out_feat {
440 w[i * out_feat + o] = ((i + o) % 8) as f32;
441 }
442 }
443 let qw = QuantizedWeight::quantize(&w, in_feat, out_feat);
444 let mut out = alloc::vec![0.0f32; in_feat * out_feat];
445 qw.dequantize_to(&mut out);
446
447 for i in 0..w.len() {
448 let diff = (w[i] - out[i]).abs();
449 assert!(diff < 1e-5, "index {i}: expected {}, got {}", w[i], out[i]);
450 }
451 }
452
453 #[test]
454 fn test_matvec_matches_dequantize() {
455 let in_feat = 64;
456 let out_feat = 32;
457 let w: alloc::vec::Vec<f32> = (0..in_feat * out_feat)
458 .map(|i| (i as f32 - 32.0) * 0.05)
459 .collect();
460 let x: alloc::vec::Vec<f32> = (0..in_feat).map(|i| (i as f32 * 0.1).sin()).collect();
461
462 let qw = QuantizedWeight::quantize(&w, in_feat, out_feat);
463
464 let mut dq = alloc::vec![0.0f32; in_feat * out_feat];
466 qw.dequantize_to(&mut dq);
467 let mut y_ref = alloc::vec![0.0f32; out_feat];
468 for o in 0..out_feat {
469 for i in 0..in_feat {
470 y_ref[o] += x[i] * dq[i * out_feat + o];
471 }
472 }
473
474 let mut y_got = alloc::vec![0.0f32; out_feat];
476 qw.matvec(&x, &mut y_got);
477
478 for o in 0..out_feat {
479 let diff = (y_got[o] - y_ref[o]).abs();
480 let tol = 1e-3 * (y_ref[o].abs().max(1.0));
482 assert!(
483 diff < tol,
484 "out[{o}]: matvec={} dequant+dot={} diff={diff} tol={tol}",
485 y_got[o],
486 y_ref[o]
487 );
488 }
489 }
490}