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"))]
181 unsafe {
184 self.matvec_neon(x, y)
185 }
186
187 #[cfg(not(all(target_arch = "aarch64", feature = "simd")))]
188 self.matvec_scalar(x, y);
189 }
190
191 #[cfg_attr(all(target_arch = "aarch64", feature = "simd"), allow(dead_code))]
198 #[allow(clippy::needless_range_loop)]
199 fn matvec_scalar(&self, x: &[f32], y: &mut [f32]) {
200 let gs = GROUP_SIZE.min(self.in_feat);
201 let num_pairs = self.num_groups * gs / 2;
202
203 for pair in 0..num_pairs {
204 let r0 = pair * 2;
205 let r1 = pair * 2 + 1;
206 let g = r0 / gs;
207 let x0 = if r0 < self.in_feat { x[r0] } else { 0.0 };
208 let x1 = if r1 < self.in_feat { x[r1] } else { 0.0 };
209 let base = pair * self.out_feat;
210 let scale_base = g * self.out_feat;
211
212 for o in 0..self.out_feat {
213 let byte = self.data[base + o];
214 let lo = sign_extend4(byte & 0x0F) as f32;
215 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
216 let scale = self.scales[scale_base + o];
217 y[o] += (lo * x0 + hi * x1) * scale;
218 }
219 }
220 }
221
222 #[cfg(all(target_arch = "x86_64", feature = "simd"))]
223 #[target_feature(enable = "avx2")]
224 unsafe fn matvec_avx2(&self, x: &[f32], y: &mut [f32]) {
225 use core::arch::x86_64::*;
226
227 let gs = GROUP_SIZE.min(self.in_feat);
228 let num_pairs = self.num_groups * gs / 2;
229 let out_feat = self.out_feat;
230
231 let mask_lo = _mm256_set1_epi32(0x0F0F0F0F_u32 as i32);
235
236 for pair in 0..num_pairs {
237 let r0 = pair * 2;
238 let r1 = pair * 2 + 1;
239 let g = r0 / gs;
240 let x0 = if r0 < self.in_feat { x[r0] } else { 0.0 };
241 let x1 = if r1 < self.in_feat { x[r1] } else { 0.0 };
242 let x0v = _mm256_set1_ps(x0);
243 let x1v = _mm256_set1_ps(x1);
244
245 let data_base = pair * out_feat;
246 let scale_base = g * out_feat;
247
248 let full_blocks = out_feat / 8;
250 let remainder = out_feat % 8;
251
252 for blk in 0..full_blocks {
253 let o = blk * 8;
254
255 let bytes_128 =
257 _mm_loadl_epi64(self.data[data_base + o..].as_ptr() as *const __m128i);
258 let bytes_256 = _mm256_cvtepu8_epi32(bytes_128);
260
261 let lo_256 = _mm256_and_si256(bytes_256, mask_lo);
263 let hi_256 = _mm256_and_si256(_mm256_srli_epi32(bytes_256, 4), mask_lo);
265
266 let lo_sign = _mm256_and_si256(lo_256, _mm256_set1_epi32(8));
269 let lo_neg_mask = _mm256_cmpeq_epi32(lo_sign, _mm256_set1_epi32(8));
270 let lo_ext = _mm256_and_si256(lo_neg_mask, _mm256_set1_epi32(!0x0F));
271 let lo_i32 = _mm256_or_si256(lo_256, lo_ext);
272
273 let hi_sign = _mm256_and_si256(hi_256, _mm256_set1_epi32(8));
274 let hi_neg_mask = _mm256_cmpeq_epi32(hi_sign, _mm256_set1_epi32(8));
275 let hi_ext = _mm256_and_si256(hi_neg_mask, _mm256_set1_epi32(!0x0F));
276 let hi_i32 = _mm256_or_si256(hi_256, hi_ext);
277
278 let lo_f32 = _mm256_cvtepi32_ps(lo_i32);
280 let hi_f32 = _mm256_cvtepi32_ps(hi_i32);
281
282 let scale_v = _mm256_loadu_ps(self.scales[scale_base + o..].as_ptr());
284
285 let contrib_lo = _mm256_mul_ps(lo_f32, x0v);
288 let contrib = _mm256_fmadd_ps(hi_f32, x1v, contrib_lo);
289 let scaled = _mm256_mul_ps(contrib, scale_v);
290
291 let y_ptr = y[o..].as_mut_ptr();
293 let y_vec = _mm256_loadu_ps(y_ptr);
294 let y_new = _mm256_add_ps(y_vec, scaled);
295 _mm256_storeu_ps(y_ptr, y_new);
296 }
297
298 if remainder > 0 {
300 let o_base = full_blocks * 8;
301 let scale_off = scale_base + o_base;
302 let data_off = data_base + o_base;
303 for o in 0..remainder {
304 let byte = self.data[data_off + o];
305 let lo = sign_extend4(byte & 0x0F) as f32;
306 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
307 let scale = self.scales[scale_off + o];
308 y[o_base + o] += (lo * x0 + hi * x1) * scale;
309 }
310 }
311 }
312 }
313
314 #[cfg(all(target_arch = "aarch64", feature = "simd"))]
319 #[target_feature(enable = "neon")]
320 unsafe fn matvec_neon(&self, x: &[f32], y: &mut [f32]) {
321 use core::arch::aarch64::*;
322
323 let gs = GROUP_SIZE.min(self.in_feat);
324 let num_pairs = self.num_groups * gs / 2;
325 let out_feat = self.out_feat;
326
327 let mask_u8 = vdup_n_u8(0x0F);
328 let eight_u8 = vdup_n_u8(8);
329 let sixteen = vdup_n_u8(16);
330
331 for pair in 0..num_pairs {
332 let r0 = pair * 2;
333 let r1 = pair * 2 + 1;
334 let g = r0 / gs;
335 let x0 = if r0 < self.in_feat {
336 *x.get_unchecked(r0)
337 } else {
338 0.0
339 };
340 let x1 = if r1 < self.in_feat {
341 *x.get_unchecked(r1)
342 } else {
343 0.0
344 };
345 let x0v = vdupq_n_f32(x0);
346 let x1v = vdupq_n_f32(x1);
347
348 let data_base = pair * out_feat;
349 let scale_base = g * out_feat;
350
351 let full8 = out_feat / 8;
353
354 for blk in 0..full8 {
355 let o = blk * 8;
356
357 let bytes = vld1_u8(self.data.get_unchecked(data_base + o) as *const u8);
359
360 let lo_raw = vand_u8(bytes, mask_u8);
362 let hi_raw = vshr_n_u8::<4>(bytes); let lo_s8 = vreinterpret_s8_u8(vsub_u8(
366 lo_raw,
367 vand_u8(vcge_u8(lo_raw, eight_u8), sixteen),
368 ));
369 let hi_s8 = vreinterpret_s8_u8(vsub_u8(
370 hi_raw,
371 vand_u8(vcge_u8(hi_raw, eight_u8), sixteen),
372 ));
373
374 let lo_s16 = vmovl_s8(lo_s8);
376 let hi_s16 = vmovl_s8(hi_s8);
377
378 let lo_f32_lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(lo_s16)));
380 let hi_f32_lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(hi_s16)));
381 let scale_lo = vld1q_f32(self.scales.get_unchecked(scale_base + o) as *const f32);
382 let contrib_lo = vmlaq_f32(vmulq_f32(lo_f32_lo, x0v), hi_f32_lo, x1v);
383 let y_ptr_lo = y.get_unchecked_mut(o) as *mut f32;
384 vst1q_f32(
385 y_ptr_lo,
386 vaddq_f32(vld1q_f32(y_ptr_lo), vmulq_f32(contrib_lo, scale_lo)),
387 );
388
389 let lo_f32_hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(lo_s16)));
391 let hi_f32_hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(hi_s16)));
392 let scale_hi =
393 vld1q_f32(self.scales.get_unchecked(scale_base + o + 4) as *const f32);
394 let contrib_hi = vmlaq_f32(vmulq_f32(lo_f32_hi, x0v), hi_f32_hi, x1v);
395 let y_ptr_hi = y.get_unchecked_mut(o + 4) as *mut f32;
396 vst1q_f32(
397 y_ptr_hi,
398 vaddq_f32(vld1q_f32(y_ptr_hi), vmulq_f32(contrib_hi, scale_hi)),
399 );
400 }
401
402 let tail_start = full8 * 8;
404 for o in tail_start..out_feat {
405 let byte = *self.data.get_unchecked(data_base + o);
406 let lo = sign_extend4(byte & 0x0F) as f32;
407 let hi = sign_extend4((byte >> 4) & 0x0F) as f32;
408 let scale = *self.scales.get_unchecked(scale_base + o);
409 *y.get_unchecked_mut(o) += (lo * x0 + hi * x1) * scale;
410 }
411 }
412 }
413
414 pub fn matmul(&self, x: &[f32], batch: usize, y: &mut [f32]) {
416 assert_eq!(x.len(), batch * self.in_feat);
417 assert_eq!(y.len(), batch * self.out_feat);
418 for b in 0..batch {
419 self.matvec(
420 &x[b * self.in_feat..(b + 1) * self.in_feat],
421 &mut y[b * self.out_feat..(b + 1) * self.out_feat],
422 );
423 }
424 }
425}
426
427#[inline(always)]
428fn sign_extend4(nibble: u8) -> i8 {
429 if nibble & 0x8 != 0 {
430 (nibble | 0xF0) as i8
431 } else {
432 nibble as i8
433 }
434}
435
436#[cfg(test)]
437mod tests {
438
439 #[test]
444 fn simd_matches_scalar_reference() {
445 for &(in_feat, out_feat) in &[(32usize, 8usize), (64, 16), (512, 512), (100, 12), (33, 7)] {
446 let w: Vec<f32> = (0..in_feat * out_feat)
448 .map(|i| ((i * 31 % 197) as f32 - 98.0) / 37.0)
449 .collect();
450 let q = QuantizedWeight::quantize(&w, in_feat, out_feat);
451 let x: Vec<f32> = (0..in_feat)
452 .map(|i| (i as f32 * 0.37).sin() * 3.0 - 0.5)
453 .collect();
454
455 let mut dispatched = vec![0.0f32; out_feat];
456 q.matvec(&x, &mut dispatched);
457
458 let mut scalar = vec![0.0f32; out_feat];
459 q.matvec_scalar(&x, &mut scalar);
460
461 let l1: f32 = x.iter().map(|v| v.abs()).sum();
462 for o in 0..out_feat {
463 assert!(
466 (dispatched[o] - scalar[o]).abs() <= 1e-4 * l1.max(1.0),
467 "{in_feat}x{out_feat} out[{o}]: dispatched {} vs scalar {}",
468 dispatched[o],
469 scalar[o]
470 );
471 }
472 }
473 }
474 use super::*;
475
476 #[test]
477 fn test_sign_extend() {
478 assert_eq!(sign_extend4(0x7), 7);
479 assert_eq!(sign_extend4(0x8), -8);
480 assert_eq!(sign_extend4(0xF), -1);
481 assert_eq!(sign_extend4(0x0), 0);
482 }
483
484 #[test]
485 fn test_roundtrip_identity() {
486 let in_feat = 64;
487 let out_feat = 32;
488 let mut w = alloc::vec![0.0f32; in_feat * out_feat];
489 for i in 0..in_feat {
490 for o in 0..out_feat {
491 w[i * out_feat + o] = ((i + o) % 8) as f32;
492 }
493 }
494 let qw = QuantizedWeight::quantize(&w, in_feat, out_feat);
495 let mut out = alloc::vec![0.0f32; in_feat * out_feat];
496 qw.dequantize_to(&mut out);
497
498 for i in 0..w.len() {
499 let diff = (w[i] - out[i]).abs();
500 assert!(diff < 1e-5, "index {i}: expected {}, got {}", w[i], out[i]);
501 }
502 }
503
504 #[test]
505 fn test_matvec_matches_dequantize() {
506 let in_feat = 64;
507 let out_feat = 32;
508 let w: alloc::vec::Vec<f32> = (0..in_feat * out_feat)
509 .map(|i| (i as f32 - 32.0) * 0.05)
510 .collect();
511 let x: alloc::vec::Vec<f32> = (0..in_feat).map(|i| (i as f32 * 0.1).sin()).collect();
512
513 let qw = QuantizedWeight::quantize(&w, in_feat, out_feat);
514
515 let mut dq = alloc::vec![0.0f32; in_feat * out_feat];
517 qw.dequantize_to(&mut dq);
518 let mut y_ref = alloc::vec![0.0f32; out_feat];
519 for o in 0..out_feat {
520 for i in 0..in_feat {
521 y_ref[o] += x[i] * dq[i * out_feat + o];
522 }
523 }
524
525 let mut y_got = alloc::vec![0.0f32; out_feat];
527 qw.matvec(&x, &mut y_got);
528
529 for o in 0..out_feat {
530 let diff = (y_got[o] - y_ref[o]).abs();
531 let tol = 1e-3 * (y_ref[o].abs().max(1.0));
533 assert!(
534 diff < tol,
535 "out[{o}]: matvec={} dequant+dot={} diff={diff} tol={tol}",
536 y_got[o],
537 y_ref[o]
538 );
539 }
540 }
541}