1use ailake_core::{Centroid, VectorMetric};
3use half::f16;
4
5pub fn dot_product(a: &[f32], b: &[f32]) -> f32 {
8 debug_assert_eq!(
9 a.len(),
10 b.len(),
11 "dot_product: dimension mismatch {} vs {}",
12 a.len(),
13 b.len()
14 );
15 #[cfg(target_arch = "x86_64")]
16 {
17 #[cfg(feature = "avx512")]
18 if is_x86_feature_detected!("avx512f") {
19 return unsafe { avx512::dot(a, b) };
20 }
21 if is_x86_feature_detected!("avx2") {
22 return unsafe { avx2::dot(a, b) };
23 }
24 }
25 #[cfg(target_arch = "aarch64")]
26 if std::arch::is_aarch64_feature_detected!("neon") {
27 return unsafe { neon_impl::dot(a, b) };
28 }
29 dot_scalar(a, b)
30}
31
32pub fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
33 debug_assert_eq!(
34 a.len(),
35 b.len(),
36 "euclidean_distance: dimension mismatch {} vs {}",
37 a.len(),
38 b.len()
39 );
40 #[cfg(target_arch = "x86_64")]
41 {
42 #[cfg(feature = "avx512")]
43 if is_x86_feature_detected!("avx512f") {
44 return unsafe { avx512::euclidean(a, b) };
45 }
46 if is_x86_feature_detected!("avx2") {
47 return unsafe { avx2::euclidean(a, b) };
48 }
49 }
50 #[cfg(target_arch = "aarch64")]
51 if std::arch::is_aarch64_feature_detected!("neon") {
52 return unsafe { neon_impl::euclidean(a, b) };
53 }
54 euclidean_scalar(a, b)
55}
56
57pub fn cosine_distance(a: &[f32], b: &[f32]) -> f32 {
58 debug_assert_eq!(
59 a.len(),
60 b.len(),
61 "cosine_distance: dimension mismatch {} vs {}",
62 a.len(),
63 b.len()
64 );
65 #[cfg(target_arch = "x86_64")]
66 {
67 #[cfg(feature = "avx512")]
68 if is_x86_feature_detected!("avx512f") {
69 return unsafe { avx512::cosine(a, b) };
70 }
71 if is_x86_feature_detected!("avx2") {
72 return unsafe { avx2::cosine(a, b) };
73 }
74 }
75 #[cfg(target_arch = "aarch64")]
76 if std::arch::is_aarch64_feature_detected!("neon") {
77 return unsafe { neon_impl::cosine(a, b) };
78 }
79 cosine_scalar(a, b)
80}
81
82pub fn exact_distance(metric: VectorMetric, a: &[f32], b: &[f32]) -> f32 {
83 match metric {
84 VectorMetric::Cosine => cosine_distance(a, b),
85 VectorMetric::Euclidean => euclidean_distance(a, b),
86 VectorMetric::DotProduct => -dot_product(a, b),
87 VectorMetric::NormalizedCosine => normalized_cosine_distance(a, b),
88 }
89}
90
91pub fn cosine_distance_f16(a: &[f32], b: &[f16]) -> f32 {
101 debug_assert_eq!(
102 a.len(),
103 b.len(),
104 "cosine_distance_f16: dimension mismatch {} vs {}",
105 a.len(),
106 b.len()
107 );
108 #[cfg(target_arch = "x86_64")]
109 {
110 #[cfg(feature = "avx512")]
111 if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
112 return unsafe { avx512::cosine_f16(a, b) };
113 }
114 if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
115 return unsafe { avx2_f16c::cosine(a, b) };
116 }
117 }
118 cosine_f16_scalar(a, b)
119}
120
121pub fn euclidean_distance_f16(a: &[f32], b: &[f16]) -> f32 {
122 debug_assert_eq!(
123 a.len(),
124 b.len(),
125 "euclidean_distance_f16: dimension mismatch {} vs {}",
126 a.len(),
127 b.len()
128 );
129 #[cfg(target_arch = "x86_64")]
130 {
131 #[cfg(feature = "avx512")]
132 if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
133 return unsafe { avx512::euclidean_f16(a, b) };
134 }
135 if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
136 return unsafe { avx2_f16c::euclidean(a, b) };
137 }
138 }
139 euclidean_f16_scalar(a, b)
140}
141
142pub fn dot_product_f16(a: &[f32], b: &[f16]) -> f32 {
143 debug_assert_eq!(
144 a.len(),
145 b.len(),
146 "dot_product_f16: dimension mismatch {} vs {}",
147 a.len(),
148 b.len()
149 );
150 #[cfg(target_arch = "x86_64")]
151 {
152 #[cfg(feature = "avx512")]
153 if is_x86_feature_detected!("avx512f") && is_x86_feature_detected!("f16c") {
154 return unsafe { avx512::dot_f16(a, b) };
155 }
156 if is_x86_feature_detected!("avx2") && is_x86_feature_detected!("f16c") {
157 return unsafe { avx2_f16c::dot(a, b) };
158 }
159 }
160 dot_f16_scalar(a, b)
161}
162
163pub fn normalize_l2(v: &[f32]) -> Vec<f32> {
165 let norm_sq: f32 = v.iter().map(|x| x * x).sum();
166 if norm_sq < 1e-12 {
167 return v.to_vec();
168 }
169 let inv = 1.0 / norm_sq.sqrt();
170 v.iter().map(|x| x * inv).collect()
171}
172
173pub fn normalized_cosine_distance(a: &[f32], b: &[f32]) -> f32 {
176 1.0 - dot_product(a, b)
177}
178
179pub fn normalized_cosine_distance_f16(a: &[f32], b: &[f16]) -> f32 {
180 1.0 - dot_product_f16(a, b)
181}
182
183pub fn compute_centroid_and_radius(vectors: &[Vec<f32>], metric: VectorMetric) -> Centroid {
184 let finite: Vec<&Vec<f32>> = vectors
190 .iter()
191 .filter(|v| v.iter().all(|x| x.is_finite()))
192 .collect();
193 if finite.is_empty() {
194 return Centroid {
195 values: vec![],
196 radius: 0.0,
197 metric,
198 };
199 }
200 let dim = finite[0].len();
201 let n = finite.len() as f32;
202 let centroid: Vec<f32> = (0..dim)
203 .map(|i| finite.iter().map(|v| v[i]).sum::<f32>() / n)
204 .collect();
205 let radius = finite
212 .iter()
213 .map(|v| exact_distance(metric, ¢roid, v))
214 .filter(|d| d.is_finite())
215 .fold(0.0_f32, f32::max);
216 Centroid {
217 values: centroid,
218 radius,
219 metric,
220 }
221}
222
223#[inline(always)]
226fn dot_scalar(a: &[f32], b: &[f32]) -> f32 {
227 debug_assert_eq!(
228 a.len(),
229 b.len(),
230 "dot_scalar: dimension mismatch {} vs {}",
231 a.len(),
232 b.len()
233 );
234 a.iter().zip(b.iter()).map(|(x, y)| x * y).sum()
235}
236
237#[inline(always)]
238fn euclidean_scalar(a: &[f32], b: &[f32]) -> f32 {
239 debug_assert_eq!(
240 a.len(),
241 b.len(),
242 "euclidean_scalar: dimension mismatch {} vs {}",
243 a.len(),
244 b.len()
245 );
246 a.iter()
247 .zip(b.iter())
248 .map(|(x, y)| (x - y) * (x - y))
249 .sum::<f32>()
250 .sqrt()
251}
252
253#[inline(always)]
254fn cosine_scalar(a: &[f32], b: &[f32]) -> f32 {
255 debug_assert_eq!(
256 a.len(),
257 b.len(),
258 "cosine_scalar: dimension mismatch {} vs {}",
259 a.len(),
260 b.len()
261 );
262 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
263 let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
264 let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
265 if na == 0.0 || nb == 0.0 {
266 return 1.0;
267 }
268 1.0 - dot / (na * nb)
269}
270
271#[inline(always)]
272fn cosine_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
273 let n = a.len().min(b.len());
274 let mut dot = 0.0f32;
275 let mut norm_a = 0.0f32;
276 let mut norm_b = 0.0f32;
277 for i in 0..n {
278 let ai = a[i];
279 let bi = b[i].to_f32();
280 dot += ai * bi;
281 norm_a += ai * ai;
282 norm_b += bi * bi;
283 }
284 let denom = (norm_a * norm_b).sqrt();
285 if denom < 1e-8 {
286 1.0
287 } else {
288 1.0 - dot / denom
289 }
290}
291
292#[inline(always)]
293fn euclidean_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
294 let n = a.len().min(b.len());
295 let mut sum = 0.0f32;
296 for i in 0..n {
297 let diff = a[i] - b[i].to_f32();
298 sum += diff * diff;
299 }
300 sum.sqrt()
301}
302
303#[inline(always)]
304fn dot_f16_scalar(a: &[f32], b: &[f16]) -> f32 {
305 let n = a.len().min(b.len());
306 let mut acc = 0.0f32;
307 for i in 0..n {
308 acc += a[i] * b[i].to_f32();
309 }
310 acc
311}
312
313#[cfg(target_arch = "x86_64")]
320mod avx2 {
321 use std::arch::x86_64::*;
322
323 #[inline(always)]
324 pub unsafe fn hsum256(v: __m256) -> f32 {
325 let hi = _mm256_extractf128_ps(v, 1);
326 let lo = _mm256_castps256_ps128(v);
327 let s = _mm_add_ps(lo, hi);
328 let shuf = _mm_movehdup_ps(s);
329 let sums = _mm_add_ps(s, shuf);
330 let shuf = _mm_movehl_ps(shuf, sums);
331 _mm_cvtss_f32(_mm_add_ss(sums, shuf))
332 }
333
334 #[target_feature(enable = "avx2,fma")]
336 pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
337 let n = a.len().min(b.len());
338 let ap = a.as_ptr();
339 let bp = b.as_ptr();
340
341 let mut acc0 = _mm256_setzero_ps();
342 let mut acc1 = _mm256_setzero_ps();
343
344 let chunks16 = n / 16;
345 for i in 0..chunks16 {
346 let base = i * 16;
347 let a0 = _mm256_loadu_ps(ap.add(base));
348 let b0 = _mm256_loadu_ps(bp.add(base));
349 let a1 = _mm256_loadu_ps(ap.add(base + 8));
350 let b1 = _mm256_loadu_ps(bp.add(base + 8));
351 acc0 = _mm256_fmadd_ps(a0, b0, acc0);
352 acc1 = _mm256_fmadd_ps(a1, b1, acc1);
353 }
354
355 let chunks8 = n / 8;
356 if chunks8 > chunks16 * 2 {
357 let base = chunks16 * 16;
358 let a0 = _mm256_loadu_ps(ap.add(base));
359 let b0 = _mm256_loadu_ps(bp.add(base));
360 acc0 = _mm256_fmadd_ps(a0, b0, acc0);
361 }
362
363 let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
364 for i in (chunks8 * 8)..n {
365 sum += *ap.add(i) * *bp.add(i);
366 }
367 sum
368 }
369
370 #[target_feature(enable = "avx2,fma")]
372 pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
373 let n = a.len().min(b.len());
374 let ap = a.as_ptr();
375 let bp = b.as_ptr();
376
377 let mut acc0 = _mm256_setzero_ps();
378 let mut acc1 = _mm256_setzero_ps();
379
380 let chunks16 = n / 16;
381 for i in 0..chunks16 {
382 let base = i * 16;
383 let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), _mm256_loadu_ps(bp.add(base)));
384 let d1 = _mm256_sub_ps(
385 _mm256_loadu_ps(ap.add(base + 8)),
386 _mm256_loadu_ps(bp.add(base + 8)),
387 );
388 acc0 = _mm256_fmadd_ps(d0, d0, acc0);
389 acc1 = _mm256_fmadd_ps(d1, d1, acc1);
390 }
391
392 let chunks8 = n / 8;
393 if chunks8 > chunks16 * 2 {
394 let base = chunks16 * 16;
395 let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), _mm256_loadu_ps(bp.add(base)));
396 acc0 = _mm256_fmadd_ps(d0, d0, acc0);
397 }
398
399 let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
400 for i in (chunks8 * 8)..n {
401 let d = *ap.add(i) - *bp.add(i);
402 sum += d * d;
403 }
404 sum.sqrt()
405 }
406
407 #[target_feature(enable = "avx2,fma")]
409 pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
410 let n = a.len().min(b.len());
411 let ap = a.as_ptr();
412 let bp = b.as_ptr();
413
414 let mut dot_acc = _mm256_setzero_ps();
415 let mut na_acc = _mm256_setzero_ps();
416 let mut nb_acc = _mm256_setzero_ps();
417
418 let chunks8 = n / 8;
419 for i in 0..chunks8 {
420 let base = i * 8;
421 let av = _mm256_loadu_ps(ap.add(base));
422 let bv = _mm256_loadu_ps(bp.add(base));
423 dot_acc = _mm256_fmadd_ps(av, bv, dot_acc);
424 na_acc = _mm256_fmadd_ps(av, av, na_acc);
425 nb_acc = _mm256_fmadd_ps(bv, bv, nb_acc);
426 }
427
428 let mut dot = hsum256(dot_acc);
429 let mut na2 = hsum256(na_acc);
430 let mut nb2 = hsum256(nb_acc);
431
432 for i in (chunks8 * 8)..n {
433 let ai = *ap.add(i);
434 let bi = *bp.add(i);
435 dot += ai * bi;
436 na2 += ai * ai;
437 nb2 += bi * bi;
438 }
439
440 let na = na2.sqrt();
441 let nb = nb2.sqrt();
442 if na == 0.0 || nb == 0.0 {
443 return 1.0;
444 }
445 1.0 - dot / (na * nb)
446 }
447}
448
449#[cfg(target_arch = "x86_64")]
456mod avx2_f16c {
457 use half::f16;
458 use std::arch::x86_64::*;
459
460 use super::avx2::hsum256;
461
462 #[target_feature(enable = "avx2,f16c,fma")]
464 pub unsafe fn dot(a: &[f32], b: &[f16]) -> f32 {
465 let n = a.len().min(b.len());
466 let ap = a.as_ptr();
467 let bp = b.as_ptr() as *const u16;
468
469 let mut acc0 = _mm256_setzero_ps();
470 let mut acc1 = _mm256_setzero_ps();
471
472 let chunks16 = n / 16;
473 for i in 0..chunks16 {
474 let base = i * 16;
475 let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
476 let b1 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
477 let a0 = _mm256_loadu_ps(ap.add(base));
478 let a1 = _mm256_loadu_ps(ap.add(base + 8));
479 acc0 = _mm256_fmadd_ps(a0, b0, acc0);
480 acc1 = _mm256_fmadd_ps(a1, b1, acc1);
481 }
482
483 let chunks8 = n / 8;
484 if chunks8 > chunks16 * 2 {
485 let base = chunks16 * 16;
486 let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
487 let a0 = _mm256_loadu_ps(ap.add(base));
488 acc0 = _mm256_fmadd_ps(a0, b0, acc0);
489 }
490
491 let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
492 for i in (chunks8 * 8)..n {
493 sum += *ap.add(i) * f16::from_bits(*bp.add(i)).to_f32();
494 }
495 sum
496 }
497
498 #[target_feature(enable = "avx2,f16c,fma")]
500 pub unsafe fn euclidean(a: &[f32], b: &[f16]) -> f32 {
501 let n = a.len().min(b.len());
502 let ap = a.as_ptr();
503 let bp = b.as_ptr() as *const u16;
504
505 let mut acc0 = _mm256_setzero_ps();
506 let mut acc1 = _mm256_setzero_ps();
507
508 let chunks16 = n / 16;
509 for i in 0..chunks16 {
510 let base = i * 16;
511 let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
512 let b1 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
513 let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), b0);
514 let d1 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base + 8)), b1);
515 acc0 = _mm256_fmadd_ps(d0, d0, acc0);
516 acc1 = _mm256_fmadd_ps(d1, d1, acc1);
517 }
518
519 let chunks8 = n / 8;
520 if chunks8 > chunks16 * 2 {
521 let base = chunks16 * 16;
522 let b0 = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
523 let d0 = _mm256_sub_ps(_mm256_loadu_ps(ap.add(base)), b0);
524 acc0 = _mm256_fmadd_ps(d0, d0, acc0);
525 }
526
527 let mut sum = hsum256(_mm256_add_ps(acc0, acc1));
528 for i in (chunks8 * 8)..n {
529 let diff = *ap.add(i) - f16::from_bits(*bp.add(i)).to_f32();
530 sum += diff * diff;
531 }
532 sum.sqrt()
533 }
534
535 #[target_feature(enable = "avx2,f16c,fma")]
537 pub unsafe fn cosine(a: &[f32], b: &[f16]) -> f32 {
538 let n = a.len().min(b.len());
539 let ap = a.as_ptr();
540 let bp = b.as_ptr() as *const u16;
541
542 let mut dot_acc = _mm256_setzero_ps();
543 let mut na_acc = _mm256_setzero_ps();
544 let mut nb_acc = _mm256_setzero_ps();
545
546 let chunks8 = n / 8;
547 for i in 0..chunks8 {
548 let base = i * 8;
549 let av = _mm256_loadu_ps(ap.add(base));
550 let bv = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
551 dot_acc = _mm256_fmadd_ps(av, bv, dot_acc);
552 na_acc = _mm256_fmadd_ps(av, av, na_acc);
553 nb_acc = _mm256_fmadd_ps(bv, bv, nb_acc);
554 }
555
556 let mut dot = hsum256(dot_acc);
557 let mut na2 = hsum256(na_acc);
558 let mut nb2 = hsum256(nb_acc);
559
560 for i in (chunks8 * 8)..n {
561 let ai = *ap.add(i);
562 let bi = f16::from_bits(*bp.add(i)).to_f32();
563 dot += ai * bi;
564 na2 += ai * ai;
565 nb2 += bi * bi;
566 }
567
568 let denom = (na2 * nb2).sqrt();
569 if denom < 1e-8 {
570 1.0
571 } else {
572 1.0 - dot / denom
573 }
574 }
575}
576
577#[cfg(all(target_arch = "x86_64", feature = "avx512"))]
585mod avx512 {
586 use half::f16;
587 use std::arch::x86_64::*;
588
589 #[inline(always)]
590 unsafe fn hsum512(v: __m512) -> f32 {
591 let mut buf = [0.0f32; 16];
594 _mm512_storeu_ps(buf.as_mut_ptr(), v);
595 let lo = _mm256_loadu_ps(buf.as_ptr());
596 let hi = _mm256_loadu_ps(buf.as_ptr().add(8));
597 let sum256 = _mm256_add_ps(lo, hi);
598 let hi128 = _mm256_extractf128_ps(sum256, 1);
599 let lo128 = _mm256_castps256_ps128(sum256);
600 let sum128 = _mm_add_ps(lo128, hi128);
601 let shuf = _mm_movehdup_ps(sum128);
602 let sums = _mm_add_ps(sum128, shuf);
603 let shuf2 = _mm_movehl_ps(shuf, sums);
604 _mm_cvtss_f32(_mm_add_ss(sums, shuf2))
605 }
606
607 #[target_feature(enable = "avx512f,fma")]
608 pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
609 let n = a.len().min(b.len());
610 let ap = a.as_ptr();
611 let bp = b.as_ptr();
612 let mut acc = _mm512_setzero_ps();
613 let chunks16 = n / 16;
614 for i in 0..chunks16 {
615 let base = i * 16;
616 acc = _mm512_fmadd_ps(
617 _mm512_loadu_ps(ap.add(base)),
618 _mm512_loadu_ps(bp.add(base)),
619 acc,
620 );
621 }
622 let mut sum = hsum512(acc);
623 for i in (chunks16 * 16)..n {
624 sum += *ap.add(i) * *bp.add(i);
625 }
626 sum
627 }
628
629 #[target_feature(enable = "avx512f,fma")]
630 pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
631 let n = a.len().min(b.len());
632 let ap = a.as_ptr();
633 let bp = b.as_ptr();
634 let mut acc = _mm512_setzero_ps();
635 let chunks16 = n / 16;
636 for i in 0..chunks16 {
637 let base = i * 16;
638 let d = _mm512_sub_ps(_mm512_loadu_ps(ap.add(base)), _mm512_loadu_ps(bp.add(base)));
639 acc = _mm512_fmadd_ps(d, d, acc);
640 }
641 let mut sum = hsum512(acc);
642 for i in (chunks16 * 16)..n {
643 let d = *ap.add(i) - *bp.add(i);
644 sum += d * d;
645 }
646 sum.sqrt()
647 }
648
649 #[target_feature(enable = "avx512f,fma")]
650 pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
651 let n = a.len().min(b.len());
652 let ap = a.as_ptr();
653 let bp = b.as_ptr();
654 let mut dot_acc = _mm512_setzero_ps();
655 let mut na_acc = _mm512_setzero_ps();
656 let mut nb_acc = _mm512_setzero_ps();
657 let chunks16 = n / 16;
658 for i in 0..chunks16 {
659 let base = i * 16;
660 let av = _mm512_loadu_ps(ap.add(base));
661 let bv = _mm512_loadu_ps(bp.add(base));
662 dot_acc = _mm512_fmadd_ps(av, bv, dot_acc);
663 na_acc = _mm512_fmadd_ps(av, av, na_acc);
664 nb_acc = _mm512_fmadd_ps(bv, bv, nb_acc);
665 }
666 let mut dot = hsum512(dot_acc);
667 let mut na2 = hsum512(na_acc);
668 let mut nb2 = hsum512(nb_acc);
669 for i in (chunks16 * 16)..n {
670 let ai = *ap.add(i);
671 let bi = *bp.add(i);
672 dot += ai * bi;
673 na2 += ai * ai;
674 nb2 += bi * bi;
675 }
676 let (na, nb) = (na2.sqrt(), nb2.sqrt());
677 if na == 0.0 || nb == 0.0 {
678 return 1.0;
679 }
680 1.0 - dot / (na * nb)
681 }
682
683 #[target_feature(enable = "avx512f,f16c,fma")]
685 pub unsafe fn dot_f16(a: &[f32], b: &[f16]) -> f32 {
686 let n = a.len().min(b.len());
687 let ap = a.as_ptr();
688 let bp = b.as_ptr() as *const u16;
689 let mut acc = _mm512_setzero_ps();
690 let chunks16 = n / 16;
691 for i in 0..chunks16 {
692 let base = i * 16;
693 let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
694 let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
695 let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
696 acc = _mm512_fmadd_ps(_mm512_loadu_ps(ap.add(base)), bv, acc);
697 }
698 let mut sum = hsum512(acc);
699 for i in (chunks16 * 16)..n {
700 sum += *ap.add(i) * f16::from_bits(*bp.add(i)).to_f32();
701 }
702 sum
703 }
704
705 #[target_feature(enable = "avx512f,f16c,fma")]
706 pub unsafe fn euclidean_f16(a: &[f32], b: &[f16]) -> f32 {
707 let n = a.len().min(b.len());
708 let ap = a.as_ptr();
709 let bp = b.as_ptr() as *const u16;
710 let mut acc = _mm512_setzero_ps();
711 let chunks16 = n / 16;
712 for i in 0..chunks16 {
713 let base = i * 16;
714 let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
715 let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
716 let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
717 let d = _mm512_sub_ps(_mm512_loadu_ps(ap.add(base)), bv);
718 acc = _mm512_fmadd_ps(d, d, acc);
719 }
720 let mut sum = hsum512(acc);
721 for i in (chunks16 * 16)..n {
722 let d = *ap.add(i) - f16::from_bits(*bp.add(i)).to_f32();
723 sum += d * d;
724 }
725 sum.sqrt()
726 }
727
728 #[target_feature(enable = "avx512f,f16c,fma")]
729 pub unsafe fn cosine_f16(a: &[f32], b: &[f16]) -> f32 {
730 let n = a.len().min(b.len());
731 let ap = a.as_ptr();
732 let bp = b.as_ptr() as *const u16;
733 let mut dot_acc = _mm512_setzero_ps();
734 let mut na_acc = _mm512_setzero_ps();
735 let mut nb_acc = _mm512_setzero_ps();
736 let chunks16 = n / 16;
737 for i in 0..chunks16 {
738 let base = i * 16;
739 let b_lo = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base) as *const __m128i));
740 let b_hi = _mm256_cvtph_ps(_mm_loadu_si128(bp.add(base + 8) as *const __m128i));
741 let bv = _mm512_insertf32x8(_mm512_castps256_ps512(b_lo), b_hi, 1);
742 let av = _mm512_loadu_ps(ap.add(base));
743 dot_acc = _mm512_fmadd_ps(av, bv, dot_acc);
744 na_acc = _mm512_fmadd_ps(av, av, na_acc);
745 nb_acc = _mm512_fmadd_ps(bv, bv, nb_acc);
746 }
747 let mut dot = hsum512(dot_acc);
748 let mut na2 = hsum512(na_acc);
749 let mut nb2 = hsum512(nb_acc);
750 for i in (chunks16 * 16)..n {
751 let ai = *ap.add(i);
752 let bi = f16::from_bits(*bp.add(i)).to_f32();
753 dot += ai * bi;
754 na2 += ai * ai;
755 nb2 += bi * bi;
756 }
757 let denom = (na2 * nb2).sqrt();
758 if denom < 1e-8 {
759 1.0
760 } else {
761 1.0 - dot / denom
762 }
763 }
764}
765
766#[cfg(target_arch = "aarch64")]
769mod neon_impl {
770 use std::arch::aarch64::*;
771
772 #[target_feature(enable = "neon")]
773 pub unsafe fn dot(a: &[f32], b: &[f32]) -> f32 {
774 let n = a.len().min(b.len());
775 let mut acc = vdupq_n_f32(0.0);
776 let chunks = n / 4;
777 for i in 0..chunks {
778 let base = i * 4;
779 let av = vld1q_f32(a.as_ptr().add(base));
780 let bv = vld1q_f32(b.as_ptr().add(base));
781 acc = vmlaq_f32(acc, av, bv);
782 }
783 let mut sum = vaddvq_f32(acc);
784 for i in (chunks * 4)..n {
785 sum += a[i] * b[i];
786 }
787 sum
788 }
789
790 #[target_feature(enable = "neon")]
791 pub unsafe fn euclidean(a: &[f32], b: &[f32]) -> f32 {
792 let n = a.len().min(b.len());
793 let mut acc = vdupq_n_f32(0.0);
794 let chunks = n / 4;
795 for i in 0..chunks {
796 let base = i * 4;
797 let d = vsubq_f32(
798 vld1q_f32(a.as_ptr().add(base)),
799 vld1q_f32(b.as_ptr().add(base)),
800 );
801 acc = vmlaq_f32(acc, d, d);
802 }
803 let mut sum = vaddvq_f32(acc);
804 for i in (chunks * 4)..n {
805 let d = a[i] - b[i];
806 sum += d * d;
807 }
808 sum.sqrt()
809 }
810
811 #[target_feature(enable = "neon")]
812 pub unsafe fn cosine(a: &[f32], b: &[f32]) -> f32 {
813 let n = a.len().min(b.len());
814 let mut dot_acc = vdupq_n_f32(0.0);
815 let mut na_acc = vdupq_n_f32(0.0);
816 let mut nb_acc = vdupq_n_f32(0.0);
817 let chunks = n / 4;
818 for i in 0..chunks {
819 let base = i * 4;
820 let av = vld1q_f32(a.as_ptr().add(base));
821 let bv = vld1q_f32(b.as_ptr().add(base));
822 dot_acc = vmlaq_f32(dot_acc, av, bv);
823 na_acc = vmlaq_f32(na_acc, av, av);
824 nb_acc = vmlaq_f32(nb_acc, bv, bv);
825 }
826 let mut dot = vaddvq_f32(dot_acc);
827 let mut na2 = vaddvq_f32(na_acc);
828 let mut nb2 = vaddvq_f32(nb_acc);
829 for i in (chunks * 4)..n {
830 dot += a[i] * b[i];
831 na2 += a[i] * a[i];
832 nb2 += b[i] * b[i];
833 }
834 let (na, nb) = (na2.sqrt(), nb2.sqrt());
835 if na == 0.0 || nb == 0.0 {
836 return 1.0;
837 }
838 1.0 - dot / (na * nb)
839 }
840}
841
842#[cfg(test)]
845mod tests {
846 use super::*;
847 use proptest::prelude::*;
848
849 fn arb_vec(dim: usize) -> impl Strategy<Value = Vec<f32>> {
852 proptest::collection::vec(proptest::num::f32::ANY, dim)
853 }
854
855 fn arb_vecs() -> impl Strategy<Value = (Vec<f32>, Vec<f32>)> {
856 (1usize..2048).prop_flat_map(|dim| (arb_vec(dim), arb_vec(dim)))
857 }
858
859 fn arb_f16_vec(dim: usize) -> impl Strategy<Value = Vec<f16>> {
860 proptest::collection::vec(proptest::num::f32::ANY, dim)
861 .prop_map(|v| v.into_iter().map(f16::from_f32).collect())
862 }
863
864 fn arb_f16_vecs() -> impl Strategy<Value = (Vec<f32>, Vec<f16>)> {
865 (1usize..2048).prop_flat_map(|dim| (arb_vec(dim), arb_f16_vec(dim)))
866 }
867
868 proptest! {
869 #[test]
870 fn prop_dot_matches_scalar((a, b) in arb_vecs()) {
871 let expected = dot_scalar(&a, &b);
872 let actual = dot_product(&a, &b);
873 let max_err = (expected.abs().max(1.0)) * 1e-4;
874 if expected.is_finite() && actual.is_finite() {
875 prop_assert!(
876 (actual - expected).abs() <= max_err,
877 "dot simd={actual} scalar={expected} dim={}",
878 a.len()
879 );
880 }
881 }
882
883 #[test]
884 fn prop_euclidean_matches_scalar((a, b) in arb_vecs()) {
885 let expected = euclidean_scalar(&a, &b);
886 let actual = euclidean_distance(&a, &b);
887 let max_err = (expected.abs().max(1.0)) * 1e-4;
888 if expected.is_finite() && actual.is_finite() {
889 prop_assert!(
890 (actual - expected).abs() <= max_err,
891 "euclidean simd={actual} scalar={expected} dim={}",
892 a.len()
893 );
894 }
895 }
896
897 #[test]
898 fn prop_cosine_matches_scalar((a, b) in arb_vecs()) {
899 let expected = cosine_scalar(&a, &b);
900 let actual = cosine_distance(&a, &b);
901 let max_err = (expected.abs().max(1.0)) * 1e-4;
902 if expected.is_finite() && actual.is_finite() {
903 prop_assert!(
904 (actual - expected).abs() <= max_err,
905 "cosine simd={actual} scalar={expected} dim={}",
906 a.len()
907 );
908 }
909 }
910
911 #[test]
912 fn prop_dot_f16_matches_scalar((a, b) in arb_f16_vecs()) {
913 let expected = dot_f16_scalar(&a, &b);
914 let actual = dot_product_f16(&a, &b);
915 let max_err = (expected.abs().max(1.0)) * 1e-3;
916 if expected.is_finite() && actual.is_finite() {
917 prop_assert!(
918 (actual - expected).abs() <= max_err,
919 "f16 dot simd={actual} scalar={expected} dim={}",
920 a.len()
921 );
922 }
923 }
924
925 #[test]
926 fn prop_euclidean_f16_matches_scalar((a, b) in arb_f16_vecs()) {
927 let expected = euclidean_f16_scalar(&a, &b);
928 let actual = euclidean_distance_f16(&a, &b);
929 let max_err = (expected.abs().max(1.0)) * 1e-3;
930 if expected.is_finite() && actual.is_finite() {
931 prop_assert!(
932 (actual - expected).abs() <= max_err,
933 "f16 euclidean simd={actual} scalar={expected} dim={}",
934 a.len()
935 );
936 }
937 }
938
939 #[test]
940 fn prop_cosine_f16_matches_scalar((a, b) in arb_f16_vecs()) {
941 let expected = cosine_f16_scalar(&a, &b);
942 let actual = cosine_distance_f16(&a, &b);
943 let max_err = (expected.abs().max(1.0)) * 1e-3;
944 if expected.is_finite() && actual.is_finite() {
945 prop_assert!(
946 (actual - expected).abs() <= max_err,
947 "f16 cosine simd={actual} scalar={expected} dim={}",
948 a.len()
949 );
950 }
951 }
952 }
953
954 #[test]
957 fn cosine_identical() {
958 let v = vec![1.0f32, 0.0, 0.0];
959 assert!(cosine_distance(&v, &v).abs() < 1e-5);
960 }
961
962 #[test]
963 fn cosine_orthogonal() {
964 assert!((cosine_distance(&[1.0f32, 0.0], &[0.0f32, 1.0]) - 1.0).abs() < 1e-5);
965 }
966
967 #[test]
968 fn euclidean_basic() {
969 assert!((euclidean_distance(&[0.0f32, 0.0], &[3.0f32, 4.0]) - 5.0).abs() < 1e-5);
970 }
971
972 #[test]
973 fn dot_basic() {
974 assert!((dot_product(&[1.0f32, 2.0, 3.0], &[4.0f32, 5.0, 6.0]) - 32.0).abs() < 1e-5);
975 }
976
977 #[test]
978 fn simd_matches_scalar_dim128() {
979 use rand::{rngs::StdRng, Rng, SeedableRng};
980 let mut rng = StdRng::seed_from_u64(99);
981 let a: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
982 let b: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
983
984 let dot_s = dot_scalar(&a, &b);
985 let euclid_s = euclidean_scalar(&a, &b);
986 let cos_s = cosine_scalar(&a, &b);
987
988 let dot_f = dot_product(&a, &b);
989 let euclid_f = euclidean_distance(&a, &b);
990 let cos_f = cosine_distance(&a, &b);
991
992 assert!(
993 (dot_f - dot_s).abs() < 1e-4,
994 "dot mismatch: {dot_f} vs {dot_s}"
995 );
996 assert!(
997 (euclid_f - euclid_s).abs() < 1e-4,
998 "euclidean mismatch: {euclid_f} vs {euclid_s}"
999 );
1000 assert!(
1001 (cos_f - cos_s).abs() < 1e-4,
1002 "cosine mismatch: {cos_f} vs {cos_s}"
1003 );
1004 }
1005
1006 #[test]
1007 fn f16_simd_matches_scalar() {
1008 use rand::{rngs::StdRng, Rng, SeedableRng};
1009 let mut rng = StdRng::seed_from_u64(42);
1010 let a: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
1011 let b_f32: Vec<f32> = (0..128).map(|_| rng.gen::<f32>() * 2.0 - 1.0).collect();
1012 let b: Vec<f16> = b_f32.iter().map(|&x| f16::from_f32(x)).collect();
1013
1014 let dot_s = dot_f16_scalar(&a, &b);
1015 let euclid_s = euclidean_f16_scalar(&a, &b);
1016 let cos_s = cosine_f16_scalar(&a, &b);
1017
1018 let dot_f = dot_product_f16(&a, &b);
1019 let euclid_f = euclidean_distance_f16(&a, &b);
1020 let cos_f = cosine_distance_f16(&a, &b);
1021
1022 assert!(
1024 (dot_f - dot_s).abs() < 1e-3,
1025 "f16 dot mismatch: {dot_f} vs {dot_s}"
1026 );
1027 assert!(
1028 (euclid_f - euclid_s).abs() < 1e-3,
1029 "f16 euclidean mismatch: {euclid_f} vs {euclid_s}"
1030 );
1031 assert!(
1032 (cos_f - cos_s).abs() < 1e-3,
1033 "f16 cosine mismatch: {cos_f} vs {cos_s}"
1034 );
1035 }
1036
1037 #[test]
1038 fn normalize_l2_unit() {
1039 let v = vec![3.0f32, 4.0];
1040 let n = normalize_l2(&v);
1041 let norm: f32 = n.iter().map(|x| x * x).sum::<f32>().sqrt();
1042 assert!((norm - 1.0).abs() < 1e-6, "norm={norm}");
1043 assert!((n[0] - 0.6).abs() < 1e-6);
1044 assert!((n[1] - 0.8).abs() < 1e-6);
1045 }
1046
1047 #[test]
1048 fn normalized_cosine_matches_cosine_on_unit_vecs() {
1049 let a = normalize_l2(&[1.0f32, 1.0, 0.0]);
1050 let b = normalize_l2(&[1.0f32, 0.0, 1.0]);
1051 let cos = cosine_distance(&a, &b);
1052 let ncos = normalized_cosine_distance(&a, &b);
1053 assert!((cos - ncos).abs() < 1e-5, "cos={cos} ncos={ncos}");
1054 }
1055
1056 #[test]
1057 fn centroid_single() {
1058 let v = vec![vec![1.0f32, 2.0, 3.0]];
1059 let c = compute_centroid_and_radius(&v, VectorMetric::Cosine);
1060 assert_eq!(c.values, vec![1.0, 2.0, 3.0]);
1061 assert!(c.radius < 1e-6, "radius={}", c.radius);
1062 }
1063
1064 #[test]
1065 fn centroid_two_points() {
1066 let vs = vec![vec![0.0f32, 0.0], vec![2.0f32, 2.0]];
1067 let c = compute_centroid_and_radius(&vs, VectorMetric::Euclidean);
1068 assert!((c.values[0] - 1.0).abs() < 1e-6);
1069 assert!(c.radius > 0.0);
1070 }
1071
1072 #[test]
1073 fn centroid_excludes_nan_poisoned_vector_instead_of_collapsing_radius() {
1074 let vs = vec![
1080 vec![10.0f32, 10.0],
1081 vec![11.0f32, 9.0],
1082 vec![9.0f32, 11.0],
1083 vec![f32::NAN, 10.0],
1084 ];
1085 let c = compute_centroid_and_radius(&vs, VectorMetric::Euclidean);
1086 assert!(c.values.iter().all(|x| x.is_finite()), "{:?}", c.values);
1087 assert!((c.values[0] - 10.0).abs() < 1e-6, "{:?}", c.values);
1088 assert!((c.values[1] - 10.0).abs() < 1e-6, "{:?}", c.values);
1089 assert!(c.radius > 0.5, "radius collapsed to {}", c.radius);
1090 }
1091
1092 #[test]
1095 fn dot_product_nan_inputs() {
1096 let a = vec![f32::NAN, 1.0];
1097 let b = vec![1.0, 2.0];
1098 let result = dot_product(&a, &b);
1099 assert!(
1100 result.is_nan(),
1101 "dot with NaN should produce NaN, got {result}"
1102 );
1103 }
1104
1105 #[test]
1106 fn dot_product_inf_inputs() {
1107 let a = vec![f32::INFINITY, 1.0];
1108 let b = vec![1.0, 2.0];
1109 let result = dot_product(&a, &b);
1110 assert!(
1111 result.is_infinite(),
1112 "dot with Inf should produce Inf, got {result}"
1113 );
1114 }
1115
1116 #[test]
1117 fn dot_product_zero_vector() {
1118 let a = vec![0.0f32; 4];
1119 let b = vec![1.0, 2.0, 3.0, 4.0];
1120 let result = dot_product(&a, &b);
1121 assert_eq!(
1122 result, 0.0,
1123 "dot with zero vector should be 0, got {result}"
1124 );
1125 }
1126
1127 #[test]
1128 fn euclidean_nan_input() {
1129 let a = vec![f32::NAN, 1.0];
1130 let b = vec![1.0, 2.0];
1131 let result = euclidean_distance(&a, &b);
1132 assert!(
1133 result.is_nan(),
1134 "euclidean with NaN should produce NaN, got {result}"
1135 );
1136 }
1137
1138 #[test]
1139 fn euclidean_zero_vector() {
1140 let a = vec![0.0f32; 3];
1141 let b = vec![3.0, 4.0, 0.0];
1142 let result = euclidean_distance(&a, &b);
1143 assert!((result - 5.0).abs() < 1e-5, "expected 5.0, got {result}");
1144 }
1145
1146 #[test]
1147 fn cosine_zero_vector_handles_gracefully() {
1148 let zero = vec![0.0f32; 4];
1149 let other = vec![1.0, 0.0, 0.0, 0.0];
1150 let result = cosine_distance(&zero, &other);
1153 assert!(
1154 result.is_finite(),
1155 "cosine with zero vector should return finite value, got {result}"
1156 );
1157 }
1158
1159 #[test]
1160 fn cosine_both_zero_vectors() {
1161 let zero = vec![0.0f32; 4];
1162 let result = cosine_distance(&zero, &zero);
1163 assert!(
1164 result.is_finite(),
1165 "cosine with both zero should return finite value, got {result}"
1166 );
1167 }
1168
1169 #[test]
1170 fn normalize_l2_zero_vector() {
1171 let v = vec![0.0f32; 4];
1172 let n = normalize_l2(&v);
1173 for x in &n {
1175 assert!(x.is_finite(), "normalized zero vector has non-finite {x}");
1176 }
1177 let norm: f32 = n.iter().map(|x| x * x).sum::<f32>().sqrt();
1178 assert!(norm.abs() < 1e-6, "norm should be 0, got {norm}");
1179 }
1180
1181 #[test]
1184 #[should_panic(expected = "dimension mismatch")]
1185 fn dot_dimension_mismatch_panics() {
1186 let a = vec![1.0f32; 3];
1187 let b = vec![2.0f32; 5];
1188 dot_product(&a, &b);
1189 }
1190
1191 #[test]
1192 #[should_panic(expected = "dimension mismatch")]
1193 fn euclidean_dimension_mismatch_panics() {
1194 euclidean_distance(&[1.0f32; 3], &[2.0f32; 5]);
1195 }
1196
1197 #[test]
1198 #[should_panic(expected = "dimension mismatch")]
1199 fn cosine_dimension_mismatch_panics() {
1200 cosine_distance(&[1.0f32; 3], &[2.0f32; 5]);
1201 }
1202
1203 #[cfg(miri)]
1204 mod miri_tests {
1205 use super::*;
1206
1207 #[test]
1209 fn miri_scalar_dot_product() {
1210 let a = vec![1.0f32; 100];
1211 let b = vec![2.0f32; 100];
1212 let r = dot_scalar(&a, &b);
1213 assert!((r - 200.0).abs() < 1e-5);
1214 }
1215
1216 #[test]
1218 fn miri_scalar_cosine_zero_vectors() {
1219 let zero = vec![0.0f32; 64];
1220 let r = cosine_scalar(&zero, &zero);
1221 assert!(r.is_finite());
1222 }
1223
1224 #[test]
1226 #[should_panic(expected = "dimension mismatch")]
1227 fn miri_scalar_dimension_mismatch() {
1228 dot_scalar(&[1.0f32; 3], &[2.0f32; 5]);
1229 }
1230
1231 #[test]
1233 fn miri_normalize_l2_zero() {
1234 let v = vec![0.0f32; 32];
1235 let n = normalize_l2(&v);
1236 for x in &n {
1237 assert!(x.is_finite(), "non-finite {x}");
1238 }
1239 }
1240 }
1241
1242 #[test]
1245 fn normalized_cosine_identity() {
1246 let v = vec![0.5f32, 0.5, 0.5, 0.5];
1247 let d = normalized_cosine_distance(&v, &v);
1248 assert!(d.abs() < 1e-6, "identity distance should be 0, got {d}");
1249 }
1250
1251 #[test]
1252 fn normalized_cosine_orthogonal() {
1253 let a = normalize_l2(&[1.0f32, 0.0]);
1254 let b = normalize_l2(&[0.0f32, 1.0]);
1255 let d = normalized_cosine_distance(&a, &b);
1256 assert!(
1257 (d - 1.0).abs() < 1e-5,
1258 "orthogonal distance should be 1, got {d}"
1259 );
1260 }
1261}