1#[cfg(target_arch = "x86_64")]
6use std::arch::x86_64::*;
7
8#[cfg(target_arch = "aarch64")]
9use std::arch::aarch64::*;
10
11use std::sync::OnceLock;
12
13use super::simd_config;
14
15#[derive(Debug, Clone, Copy)]
19pub struct QuantizationParams {
20 pub scale: f32,
22 pub zero_point: i8,
24 pub min_val: f32,
26 pub max_val: f32,
28}
29
30impl QuantizationParams {
31 pub fn from_vector(vector: &[f32]) -> Self {
35 let mut min_val = f32::INFINITY;
37 let mut max_val = f32::NEG_INFINITY;
38
39 for &v in vector {
40 if v.is_finite() {
41 min_val = min_val.min(v);
42 max_val = max_val.max(v);
43 }
44 }
45
46 if !min_val.is_finite() || !max_val.is_finite() {
48 min_val = 0.0;
49 max_val = 0.0;
50 }
51
52 let max_abs = min_val.abs().max(max_val.abs());
54
55 let scale = if max_abs > 1e-10 {
57 127.0 / max_abs
58 } else {
59 1.0 };
61
62 Self {
63 scale,
64 zero_point: 0,
65 min_val,
66 max_val,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
75pub struct QuantizedVector {
76 data: Vec<i8>,
79 pub params: QuantizationParams,
81 pub norm: f32,
83}
84
85impl QuantizedVector {
86 #[inline]
88 pub fn data(&self) -> &[i8] {
89 &self.data
90 }
91
92 #[inline]
94 pub fn len(&self) -> usize {
95 self.data.len()
96 }
97
98 #[inline]
100 pub fn is_empty(&self) -> bool {
101 self.data.is_empty()
102 }
103}
104
105impl QuantizedVector {
106 pub fn from_f32(vector: &[f32]) -> Self {
108 let mut params = QuantizationParams::from_vector(vector);
109
110 if !params.scale.is_finite() || params.scale == 0.0 {
112 params.scale = 1.0;
113 }
114
115 let mut norm_sq = 0.0f32;
117 for &v in vector {
118 if v.is_finite() {
119 norm_sq += v * v;
120 }
121 }
122 let norm = norm_sq.sqrt();
123
124 let data: Vec<i8> = vector
125 .iter()
126 .map(|&v| {
127 if !v.is_finite() {
128 0
129 } else {
130 (v * params.scale).round().clamp(-127.0, 127.0) as i8
131 }
132 })
133 .collect();
134
135 Self { data, params, norm }
136 }
137
138 pub fn to_f32(&self) -> Vec<f32> {
149 let scale = if self.params.scale.is_finite() && self.params.scale != 0.0 {
150 self.params.scale
151 } else {
152 1.0
153 };
154
155 self.data.iter().map(|&v| v as f32 / scale).collect()
156 }
157
158 #[inline]
160 pub fn dot_product(&self, other: &QuantizedVector) -> f32 {
161 dot_product_i8(self, other)
162 }
163
164 #[inline]
166 pub fn cosine_similarity(&self, other: &QuantizedVector) -> f32 {
167 cosine_similarity_i8(self, other)
168 }
169}
170
171#[inline]
185pub fn dot_product_i8(a: &QuantizedVector, b: &QuantizedVector) -> f32 {
186 debug_assert!(a.data.iter().all(|&v| v != -128i8));
187 debug_assert!(b.data.iter().all(|&v| v != -128i8));
188
189 if a.data.len() != b.data.len() {
190 return 0.0;
191 }
192
193 let denom = a.params.scale * b.params.scale;
194 if denom == 0.0 || !denom.is_finite() {
195 return 0.0;
196 }
197
198 dot_product_i8_dispatch(&a.data, &b.data) / denom
199}
200
201#[inline]
206pub(crate) fn dot_product_i8_trusted(a: &QuantizedVector, b: &QuantizedVector) -> f32 {
207 if a.data.len() != b.data.len() {
208 return 0.0;
209 }
210 let denom = a.params.scale * b.params.scale;
211 if denom == 0.0 || !denom.is_finite() {
212 return 0.0;
213 }
214 debug_assert!(a.data.iter().all(|&v| v != i8::MIN));
215 debug_assert!(b.data.iter().all(|&v| v != i8::MIN));
216 dot_product_i8_dispatch(&a.data, &b.data) / denom
217}
218
219#[inline]
223pub fn cosine_similarity_i8(a: &QuantizedVector, b: &QuantizedVector) -> f32 {
224 let denom = a.norm * b.norm;
225 if denom == 0.0 || !denom.is_finite() {
226 return 0.0;
227 }
228 dot_product_i8(a, b) / denom
229}
230
231#[inline]
237pub(crate) fn cosine_similarity_i8_trusted(a: &QuantizedVector, b: &QuantizedVector) -> f32 {
238 let denom = a.norm * b.norm;
239 if denom == 0.0 || !denom.is_finite() {
240 return 0.0;
241 }
242 dot_product_i8_trusted(a, b) / denom
243}
244
245#[cfg(target_arch = "aarch64")]
263#[target_feature(enable = "dotprod")]
264unsafe fn dot_product_i8_neon_unrolled(a: &[i8], b: &[i8]) -> f32 {
265 const SIMD_WIDTH: usize = 16;
266 const UNROLL: usize = 4;
267 const CHUNK_SIZE: usize = SIMD_WIDTH * UNROLL;
268 const PREFETCH_DISTANCE: usize = CHUNK_SIZE;
269 let n = a.len();
270 debug_assert_eq!(n, b.len());
271 let chunks = n / CHUNK_SIZE;
272
273 let mut sum0 = vdupq_n_s32(0);
274 let mut sum1 = vdupq_n_s32(0);
275 let mut sum2 = vdupq_n_s32(0);
276 let mut sum3 = vdupq_n_s32(0);
277
278 for i in 0..chunks {
282 let base = i * CHUNK_SIZE;
283
284 let next_base = base + PREFETCH_DISTANCE;
285 if next_base + CHUNK_SIZE <= n {
286 core::arch::asm!(
287 "prfm pldl1keep, [{ptr}]",
288 ptr = in(reg) a.as_ptr().add(next_base),
289 options(nostack, readonly, preserves_flags)
290 );
291 core::arch::asm!(
292 "prfm pldl1keep, [{ptr}]",
293 ptr = in(reg) b.as_ptr().add(next_base),
294 options(nostack, readonly, preserves_flags)
295 );
296 }
297
298 let a0 = vld1q_s8(a.as_ptr().add(base));
299 let b0 = vld1q_s8(b.as_ptr().add(base));
300 let a1 = vld1q_s8(a.as_ptr().add(base + SIMD_WIDTH));
301 let b1 = vld1q_s8(b.as_ptr().add(base + SIMD_WIDTH));
302 let a2 = vld1q_s8(a.as_ptr().add(base + SIMD_WIDTH * 2));
303 let b2 = vld1q_s8(b.as_ptr().add(base + SIMD_WIDTH * 2));
304 let a3 = vld1q_s8(a.as_ptr().add(base + SIMD_WIDTH * 3));
305 let b3 = vld1q_s8(b.as_ptr().add(base + SIMD_WIDTH * 3));
306
307 core::arch::asm!(
308 "sdot {s0:v}.4s, {a0:v}.16b, {b0:v}.16b",
309 "sdot {s1:v}.4s, {a1:v}.16b, {b1:v}.16b",
310 "sdot {s2:v}.4s, {a2:v}.16b, {b2:v}.16b",
311 "sdot {s3:v}.4s, {a3:v}.16b, {b3:v}.16b",
312 s0 = inout(vreg) sum0,
313 a0 = in(vreg) a0,
314 b0 = in(vreg) b0,
315 s1 = inout(vreg) sum1,
316 a1 = in(vreg) a1,
317 b1 = in(vreg) b1,
318 s2 = inout(vreg) sum2,
319 a2 = in(vreg) a2,
320 b2 = in(vreg) b2,
321 s3 = inout(vreg) sum3,
322 a3 = in(vreg) a3,
323 b3 = in(vreg) b3,
324 options(nomem, nostack, preserves_flags)
325 );
326 }
327
328 let sum01 = vaddq_s32(sum0, sum1);
329 let sum23 = vaddq_s32(sum2, sum3);
330 let mut sum_vec = vaddq_s32(sum01, sum23);
331
332 let tail_start = chunks * CHUNK_SIZE;
334 let tail_chunks = (n - tail_start) / SIMD_WIDTH;
335 for j in 0..tail_chunks {
336 let base = tail_start + j * SIMD_WIDTH;
337 let at = vld1q_s8(a.as_ptr().add(base));
338 let bt = vld1q_s8(b.as_ptr().add(base));
339 core::arch::asm!(
340 "sdot {acc:v}.4s, {a:v}.16b, {b:v}.16b",
341 acc = inout(vreg) sum_vec,
342 a = in(vreg) at,
343 b = in(vreg) bt,
344 options(nomem, nostack, preserves_flags)
345 );
346 }
347
348 let sum = vaddvq_s32(sum_vec);
349
350 let remainder_start = tail_start + tail_chunks * SIMD_WIDTH;
352 let remainder: i32 = a[remainder_start..]
353 .iter()
354 .zip(b[remainder_start..].iter())
355 .map(|(&x, &y)| x as i32 * y as i32)
356 .sum();
357
358 (sum + remainder) as f32
359}
360
361#[cfg(all(target_arch = "x86_64", feature = "avx512"))]
368#[target_feature(enable = "avx512f", enable = "avx512bw")]
369#[inline]
370unsafe fn mm512_sign_epi8(b: __m512i, a: __m512i) -> __m512i {
371 let zero = _mm512_setzero_si512();
372 let neg_b = _mm512_sub_epi8(zero, b);
373 let mask_neg = _mm512_cmplt_epi8_mask(a, zero);
375 let mask_zero = _mm512_cmpeq_epi8_mask(a, zero);
377 let result = _mm512_mask_blend_epi8(mask_neg, b, neg_b);
379 _mm512_mask_blend_epi8(mask_zero, result, zero)
381}
382
383#[cfg(all(target_arch = "x86_64", feature = "avx512"))]
399#[target_feature(enable = "avx512f", enable = "avx512vnni", enable = "avx512bw")]
400unsafe fn dot_product_i8_avx512vnni(a: &[i8], b: &[i8]) -> f32 {
401 const SIMD_WIDTH: usize = 64; const UNROLL: usize = 4;
403 const CHUNK_SIZE: usize = SIMD_WIDTH * UNROLL;
404 let n = a.len();
405 debug_assert_eq!(n, b.len());
406 debug_assert!(a.iter().all(|&v| v != i8::MIN));
407 debug_assert!(b.iter().all(|&v| v != i8::MIN));
408 let chunks = n / CHUNK_SIZE;
409
410 let mut sum0 = _mm512_setzero_si512();
412 let mut sum1 = _mm512_setzero_si512();
413 let mut sum2 = _mm512_setzero_si512();
414 let mut sum3 = _mm512_setzero_si512();
415
416 for i in 0..chunks {
417 let base = i * CHUNK_SIZE;
418
419 let a0 = _mm512_loadu_si512(a.as_ptr().add(base) as *const __m512i);
422 let b0 = _mm512_loadu_si512(b.as_ptr().add(base) as *const __m512i);
423 let a0_abs = _mm512_abs_epi8(a0);
424 let b0_signed = mm512_sign_epi8(b0, a0);
425 sum0 = _mm512_dpbusd_epi32(sum0, a0_abs, b0_signed);
426
427 let a1 = _mm512_loadu_si512(a.as_ptr().add(base + SIMD_WIDTH) as *const __m512i);
428 let b1 = _mm512_loadu_si512(b.as_ptr().add(base + SIMD_WIDTH) as *const __m512i);
429 let a1_abs = _mm512_abs_epi8(a1);
430 let b1_signed = mm512_sign_epi8(b1, a1);
431 sum1 = _mm512_dpbusd_epi32(sum1, a1_abs, b1_signed);
432
433 let a2 = _mm512_loadu_si512(a.as_ptr().add(base + SIMD_WIDTH * 2) as *const __m512i);
434 let b2 = _mm512_loadu_si512(b.as_ptr().add(base + SIMD_WIDTH * 2) as *const __m512i);
435 let a2_abs = _mm512_abs_epi8(a2);
436 let b2_signed = mm512_sign_epi8(b2, a2);
437 sum2 = _mm512_dpbusd_epi32(sum2, a2_abs, b2_signed);
438
439 let a3 = _mm512_loadu_si512(a.as_ptr().add(base + SIMD_WIDTH * 3) as *const __m512i);
440 let b3 = _mm512_loadu_si512(b.as_ptr().add(base + SIMD_WIDTH * 3) as *const __m512i);
441 let a3_abs = _mm512_abs_epi8(a3);
442 let b3_signed = mm512_sign_epi8(b3, a3);
443 sum3 = _mm512_dpbusd_epi32(sum3, a3_abs, b3_signed);
444 }
445
446 let sum01 = _mm512_add_epi32(sum0, sum1);
448 let sum23 = _mm512_add_epi32(sum2, sum3);
449 let sum_vec = _mm512_add_epi32(sum01, sum23);
450
451 let sum = _mm512_reduce_add_epi32(sum_vec);
453
454 let remainder_start = chunks * CHUNK_SIZE;
456 let remainder: i32 = a[remainder_start..]
457 .iter()
458 .zip(b[remainder_start..].iter())
459 .map(|(&x, &y)| x as i32 * y as i32)
460 .sum();
461
462 (sum + remainder) as f32
463}
464
465#[cfg(target_arch = "x86_64")]
479#[target_feature(enable = "avx2")]
480unsafe fn dot_product_i8_avx2_unrolled(a: &[i8], b: &[i8]) -> f32 {
481 const SIMD_WIDTH: usize = 32;
482 const UNROLL: usize = 4;
483 const CHUNK_SIZE: usize = SIMD_WIDTH * UNROLL;
484 const PREFETCH_DISTANCE: usize = CHUNK_SIZE;
486 let n = a.len();
487 debug_assert_eq!(n, b.len());
488 debug_assert!(a.iter().all(|&v| v != i8::MIN));
489 debug_assert!(b.iter().all(|&v| v != i8::MIN));
490 let chunks = n / CHUNK_SIZE;
491
492 let mut sum0 = _mm256_setzero_si256();
494 let mut sum1 = _mm256_setzero_si256();
495 let mut sum2 = _mm256_setzero_si256();
496 let mut sum3 = _mm256_setzero_si256();
497
498 let ones = _mm256_set1_epi16(1);
499
500 for i in 0..chunks {
501 let base = i * CHUNK_SIZE;
502
503 let next_base = base + PREFETCH_DISTANCE;
505 if next_base + CHUNK_SIZE <= n {
506 _mm_prefetch(a.as_ptr().add(next_base), _MM_HINT_T0);
507 _mm_prefetch(b.as_ptr().add(next_base), _MM_HINT_T0);
508 }
509
510 let a0 = _mm256_loadu_si256(a.as_ptr().add(base) as *const __m256i);
512 let b0 = _mm256_loadu_si256(b.as_ptr().add(base) as *const __m256i);
513 let prod0 = _mm256_maddubs_epi16(_mm256_abs_epi8(a0), _mm256_sign_epi8(b0, a0));
514 let prod0_32 = _mm256_madd_epi16(prod0, ones);
515 sum0 = _mm256_add_epi32(sum0, prod0_32);
516
517 let a1 = _mm256_loadu_si256(a.as_ptr().add(base + SIMD_WIDTH) as *const __m256i);
519 let b1 = _mm256_loadu_si256(b.as_ptr().add(base + SIMD_WIDTH) as *const __m256i);
520 let prod1 = _mm256_maddubs_epi16(_mm256_abs_epi8(a1), _mm256_sign_epi8(b1, a1));
521 let prod1_32 = _mm256_madd_epi16(prod1, ones);
522 sum1 = _mm256_add_epi32(sum1, prod1_32);
523
524 let a2 = _mm256_loadu_si256(a.as_ptr().add(base + SIMD_WIDTH * 2) as *const __m256i);
526 let b2 = _mm256_loadu_si256(b.as_ptr().add(base + SIMD_WIDTH * 2) as *const __m256i);
527 let prod2 = _mm256_maddubs_epi16(_mm256_abs_epi8(a2), _mm256_sign_epi8(b2, a2));
528 let prod2_32 = _mm256_madd_epi16(prod2, ones);
529 sum2 = _mm256_add_epi32(sum2, prod2_32);
530
531 let a3 = _mm256_loadu_si256(a.as_ptr().add(base + SIMD_WIDTH * 3) as *const __m256i);
533 let b3 = _mm256_loadu_si256(b.as_ptr().add(base + SIMD_WIDTH * 3) as *const __m256i);
534 let prod3 = _mm256_maddubs_epi16(_mm256_abs_epi8(a3), _mm256_sign_epi8(b3, a3));
535 let prod3_32 = _mm256_madd_epi16(prod3, ones);
536 sum3 = _mm256_add_epi32(sum3, prod3_32);
537 }
538
539 let sum01 = _mm256_add_epi32(sum0, sum1);
541 let sum23 = _mm256_add_epi32(sum2, sum3);
542 let sum_vec = _mm256_add_epi32(sum01, sum23);
543
544 let sum128_lo = _mm256_castsi256_si128(sum_vec);
546 let sum128_hi = _mm256_extracti128_si256(sum_vec, 1);
547 let sum128 = _mm_add_epi32(sum128_lo, sum128_hi);
548 let sum64 = _mm_add_epi32(sum128, _mm_srli_si128(sum128, 8));
549 let sum32 = _mm_add_epi32(sum64, _mm_srli_si128(sum64, 4));
550 let sum = _mm_cvtsi128_si32(sum32);
551
552 let remainder_start = chunks * CHUNK_SIZE;
554 let remainder: i32 = a[remainder_start..]
555 .iter()
556 .zip(b[remainder_start..].iter())
557 .map(|(&x, &y)| x as i32 * y as i32)
558 .sum();
559
560 (sum + remainder) as f32
561}
562
563pub type I8DotKernel = fn(&[i8], &[i8]) -> f32;
569
570static I8_DOT_KERNEL: OnceLock<I8DotKernel> = OnceLock::new();
571
572#[inline]
577pub fn resolved_i8_dot_kernel() -> I8DotKernel {
578 *I8_DOT_KERNEL.get_or_init(resolve_i8_dot_kernel)
579}
580
581fn resolve_i8_dot_kernel() -> I8DotKernel {
582 let config = simd_config();
583
584 #[cfg(target_arch = "aarch64")]
585 {
586 if config.neon_enabled && config.dotprod_enabled {
589 return dot_product_i8_neon_kernel;
590 }
591 }
592
593 #[cfg(target_arch = "x86_64")]
594 {
595 #[cfg(feature = "avx512")]
596 {
597 if config.avx512vnni_enabled {
598 return dot_product_i8_avx512vnni_kernel;
599 }
600 }
601 if config.avx2_enabled {
602 return dot_product_i8_avx2_kernel;
603 }
604 }
605
606 dot_product_i8_scalar_kernel
607}
608
609#[cfg(target_arch = "aarch64")]
610fn dot_product_i8_neon_kernel(a: &[i8], b: &[i8]) -> f32 {
611 unsafe { dot_product_i8_neon_unrolled(a, b) }
613}
614
615#[cfg(all(target_arch = "x86_64", feature = "avx512"))]
616fn dot_product_i8_avx512vnni_kernel(a: &[i8], b: &[i8]) -> f32 {
617 debug_assert!(a.iter().all(|&v| v != i8::MIN));
618 debug_assert!(b.iter().all(|&v| v != i8::MIN));
619 unsafe { dot_product_i8_avx512vnni(a, b) }
621}
622
623#[cfg(target_arch = "x86_64")]
624fn dot_product_i8_avx2_kernel(a: &[i8], b: &[i8]) -> f32 {
625 debug_assert!(a.iter().all(|&v| v != i8::MIN));
626 debug_assert!(b.iter().all(|&v| v != i8::MIN));
627 unsafe { dot_product_i8_avx2_unrolled(a, b) }
629}
630
631fn dot_product_i8_scalar_kernel(a: &[i8], b: &[i8]) -> f32 {
632 a.iter()
633 .zip(b.iter())
634 .map(|(&x, &y)| x as i32 * y as i32)
635 .sum::<i32>() as f32
636}
637
638#[inline]
661fn dot_product_i8_dispatch(a: &[i8], b: &[i8]) -> f32 {
662 resolved_i8_dot_kernel()(a, b)
663}
664
665#[inline]
692pub fn dot_product_i8_raw(a: &[i8], b: &[i8]) -> f32 {
693 if a.len() != b.len() {
694 return 0.0;
695 }
696 debug_assert!(
697 a.iter().all(|&v| v != -128i8),
698 "dot_product_i8_raw: slice a contains -128, violating the [-127, 127] SIMD invariant"
699 );
700 debug_assert!(
701 b.iter().all(|&v| v != -128i8),
702 "dot_product_i8_raw: slice b contains -128, violating the [-127, 127] SIMD invariant"
703 );
704 dot_product_i8_dispatch(a, b)
705}
706
707#[cfg(test)]
708mod simd_parity_tests {
709 use super::*;
710
711 fn gen_vec(dim: usize, seed: u64) -> Vec<f32> {
712 let mut state = seed ^ ((dim as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15));
713 (0..dim)
714 .map(|i| {
715 state = state
716 .wrapping_mul(6364136223846793005)
717 .wrapping_add(1442695040888963407)
718 .wrapping_add(i as u64);
719 let unit = ((state >> 32) as u32) as f32 / u32::MAX as f32;
720 unit * 2.0 - 1.0
721 })
722 .collect()
723 }
724
725 #[test]
728 fn test_i8_neon_scalar_parity() {
729 #[cfg(target_arch = "aarch64")]
730 {
731 if !super::super::SimdConfig::detect().dotprod_enabled {
732 eprintln!("skipping SDOT parity test: dotprod not available");
733 return;
734 }
735 }
736 #[cfg(target_arch = "aarch64")]
737 for dim in [7usize, 16, 64, 128, 384, 768] {
738 let a_q = QuantizedVector::from_f32(&gen_vec(dim, 200 + dim as u64));
739 let b_q = QuantizedVector::from_f32(&gen_vec(dim, 300 + dim as u64));
740
741 let neon = unsafe { dot_product_i8_neon_unrolled(&a_q.data, &b_q.data) };
743 let scalar: f32 = a_q
744 .data
745 .iter()
746 .zip(b_q.data.iter())
747 .map(|(&x, &y)| x as i32 * y as i32)
748 .sum::<i32>() as f32;
749
750 let diff = (neon - scalar).abs();
751 assert!(
752 diff <= 1.0,
753 "NEON vs scalar i8 dot product dim={dim}: neon={neon} scalar={scalar} diff={diff}"
754 );
755 }
756 }
757
758 #[test]
760 fn test_i8_avx2_scalar_parity() {
761 #[cfg(target_arch = "x86_64")]
762 if std::arch::is_x86_feature_detected!("avx2") {
763 for dim in [7usize, 16, 64, 128, 384, 768] {
764 let a_q = QuantizedVector::from_f32(&gen_vec(dim, 400 + dim as u64));
765 let b_q = QuantizedVector::from_f32(&gen_vec(dim, 500 + dim as u64));
766
767 let avx2 = unsafe { dot_product_i8_avx2_unrolled(&a_q.data, &b_q.data) };
769 let scalar: f32 = a_q
770 .data
771 .iter()
772 .zip(b_q.data.iter())
773 .map(|(&x, &y)| x as i32 * y as i32)
774 .sum::<i32>() as f32;
775
776 let diff = (avx2 - scalar).abs();
777 assert!(
778 diff <= 1.0,
779 "AVX2 vs scalar i8 dot product dim={dim}: avx2={avx2} scalar={scalar} diff={diff}"
780 );
781 }
782 }
783 }
784}