1use std::iter::Sum;
8use std::ops::AddAssign;
9use std::sync::Arc;
10
11use crate::{Error, Result};
12use arrow_array::{
13 Array, FixedSizeListArray, Float32Array,
14 cast::AsArray,
15 types::{Float16Type, Float32Type, Float64Type, Int8Type},
16};
17use arrow_schema::DataType;
18use half::{bf16, f16};
19use lance_arrow::{ArrowFloatType, FixedSizeListArrayExt, FloatArray};
20use lance_core::assume_eq;
21use lance_core::deepsize::DeepSizeOf;
22use lance_core::utils::cpu::SIMD_SUPPORT;
23#[cfg(any(feature = "fp16kernels", target_arch = "x86_64"))]
26use lance_core::utils::cpu::SimdSupport;
27use num_traits::{AsPrimitive, Num};
28
29#[cfg(all(
30 target_arch = "x86_64",
31 not(all(target_feature = "avx2", target_feature = "fma"))
32))]
33use crate::distance::BatchIter;
34
35pub trait L2: Num {
38 fn l2(x: &[Self], y: &[Self]) -> f32;
40
41 fn l2_batch<'a>(
53 x: &'a [Self],
54 y: &'a [Self],
55 dimension: usize,
56 ) -> impl Iterator<Item = f32> + 'a {
57 y.chunks_exact(dimension).map(move |v| Self::l2(x, v))
58 }
59}
60
61#[inline]
62pub fn l2<T: L2>(from: &[T], to: &[T]) -> f32 {
63 T::l2(from, to)
64}
65
66#[inline]
75pub fn l2_f32(x: &[f32], y: &[f32]) -> f32 {
76 #[cfg(target_arch = "x86_64")]
77 {
78 if matches!(*SIMD_SUPPORT, SimdSupport::Avx512 | SimdSupport::Avx512FP16) {
79 return unsafe { l2_f32_avx512(x, y) };
81 }
82 }
83 l2(x, y)
84}
85
86#[cfg(target_arch = "x86_64")]
87#[target_feature(enable = "avx512f")]
88unsafe fn l2_f32_avx512(x: &[f32], y: &[f32]) -> f32 {
89 use std::arch::x86_64::*;
90 debug_assert_eq!(x.len(), y.len());
91 let n = x.len();
92 let mut acc = _mm512_setzero_ps();
93 let mut i = 0usize;
94 while i + 16 <= n {
95 let a = _mm512_loadu_ps(x.as_ptr().add(i));
96 let b = _mm512_loadu_ps(y.as_ptr().add(i));
97 let diff = _mm512_sub_ps(a, b);
98 acc = _mm512_fmadd_ps(diff, diff, acc);
99 i += 16;
100 }
101 let mut sum = _mm512_reduce_add_ps(acc);
102 while i < n {
103 let diff = x[i] - y[i];
104 sum += diff * diff;
105 i += 1;
106 }
107 sum
108}
109
110#[inline]
112pub fn l2_distance_uint_scalar(key: &[u8], target: &[u8]) -> f32 {
113 key.iter()
114 .zip(target.iter())
115 .map(|(&x, &y)| (x.abs_diff(y) as u32).pow(2))
116 .sum::<u32>() as f32
117}
118
119#[inline]
125pub fn l2_scalar<
126 T: AsPrimitive<Output>,
127 Output: Num + Copy + Sum + AddAssign + 'static,
128 const LANES: usize,
129>(
130 from: &[T],
131 to: &[T],
132) -> Output {
133 let x_chunks = from.chunks_exact(LANES);
134 let y_chunks = to.chunks_exact(LANES);
135
136 let s = if !x_chunks.remainder().is_empty() {
137 x_chunks
138 .remainder()
139 .iter()
140 .zip(y_chunks.remainder())
141 .map(|(&x, &y)| {
142 let diff = x.as_() - y.as_();
143 diff * diff
144 })
145 .sum::<Output>()
146 } else {
147 Output::zero()
148 };
149
150 let mut sums = [Output::zero(); LANES];
151 for (x, y) in x_chunks.zip(y_chunks) {
152 for i in 0..LANES {
153 let diff = x[i].as_() - y[i].as_();
154 sums[i] += diff * diff;
155 }
156 }
157
158 s + sums.iter().copied().sum()
159}
160
161impl L2 for u8 {
162 #[inline]
163 fn l2(x: &[Self], y: &[Self]) -> f32 {
164 super::l2_u8::l2_u8(x, y) as f32
165 }
166}
167
168#[cfg(feature = "fp16kernels")]
169mod bf16_kernel {
170 use half::bf16;
171
172 unsafe extern "C" {
175 #[cfg(target_arch = "aarch64")]
176 pub fn l2_bf16_neon(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
177 #[cfg(all(kernel_support = "avx512_bf16", target_arch = "x86_64"))]
178 pub fn l2_bf16_avx512(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
179 #[cfg(target_arch = "x86_64")]
180 pub fn l2_bf16_avx2(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
181 #[cfg(target_arch = "loongarch64")]
182 pub fn l2_bf16_lsx(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
183 #[cfg(target_arch = "loongarch64")]
184 pub fn l2_bf16_lasx(ptr1: *const bf16, ptr2: *const bf16, len: u32) -> f32;
185 }
186}
187
188impl L2 for bf16 {
189 #[inline]
190 fn l2(x: &[Self], y: &[Self]) -> f32 {
191 match *SIMD_SUPPORT {
192 #[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
193 SimdSupport::Neon => unsafe {
194 bf16_kernel::l2_bf16_neon(x.as_ptr(), y.as_ptr(), x.len() as u32)
195 },
196 #[cfg(all(
197 feature = "fp16kernels",
198 kernel_support = "avx512_bf16",
199 target_arch = "x86_64"
200 ))]
201 SimdSupport::Avx512FP16 => unsafe {
202 bf16_kernel::l2_bf16_avx512(x.as_ptr(), y.as_ptr(), x.len() as u32)
203 },
204 #[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
205 SimdSupport::Avx2 | SimdSupport::Avx512 => unsafe {
206 bf16_kernel::l2_bf16_avx2(x.as_ptr(), y.as_ptr(), x.len() as u32)
207 },
208 #[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
209 SimdSupport::Lasx => unsafe {
210 bf16_kernel::l2_bf16_lasx(x.as_ptr(), y.as_ptr(), x.len() as u32)
211 },
212 #[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
213 SimdSupport::Lsx => unsafe {
214 bf16_kernel::l2_bf16_lsx(x.as_ptr(), y.as_ptr(), x.len() as u32)
215 },
216 _ => l2_scalar::<Self, f32, 16>(x, y),
220 }
221 }
222}
223
224#[cfg(feature = "fp16kernels")]
225mod kernel {
226 use super::*;
227
228 unsafe extern "C" {
231 #[cfg(target_arch = "aarch64")]
232 pub fn l2_f16_neon(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
233 #[cfg(all(kernel_support = "avx512_f16", target_arch = "x86_64"))]
234 pub fn l2_f16_avx512(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
235 #[cfg(target_arch = "x86_64")]
236 pub fn l2_f16_avx2(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
237 #[cfg(target_arch = "loongarch64")]
238 pub fn l2_f16_lsx(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
239 #[cfg(target_arch = "loongarch64")]
240 pub fn l2_f16_lasx(ptr1: *const f16, ptr2: *const f16, len: u32) -> f32;
241 }
242}
243
244impl L2 for f16 {
245 #[inline]
246 fn l2(x: &[Self], y: &[Self]) -> f32 {
247 match *SIMD_SUPPORT {
248 #[cfg(all(feature = "fp16kernels", target_arch = "aarch64"))]
249 SimdSupport::Neon => unsafe {
250 kernel::l2_f16_neon(x.as_ptr(), y.as_ptr(), x.len() as u32)
251 },
252 #[cfg(all(
253 feature = "fp16kernels",
254 kernel_support = "avx512_f16",
255 target_arch = "x86_64"
256 ))]
257 SimdSupport::Avx512FP16 => unsafe {
258 kernel::l2_f16_avx512(x.as_ptr(), y.as_ptr(), x.len() as u32)
259 },
260 #[cfg(all(feature = "fp16kernels", target_arch = "x86_64"))]
261 SimdSupport::Avx2 => unsafe {
262 kernel::l2_f16_avx2(x.as_ptr(), y.as_ptr(), x.len() as u32)
263 },
264 #[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
265 SimdSupport::Lasx => unsafe {
266 kernel::l2_f16_lasx(x.as_ptr(), y.as_ptr(), x.len() as u32)
267 },
268 #[cfg(all(feature = "fp16kernels", target_arch = "loongarch64"))]
269 SimdSupport::Lsx => unsafe {
270 kernel::l2_f16_lsx(x.as_ptr(), y.as_ptr(), x.len() as u32)
271 },
272 _ => l2_scalar::<Self, f32, 16>(x, y),
276 }
277 }
278}
279
280impl L2 for f32 {
281 #[inline]
282 fn l2(x: &[Self], y: &[Self]) -> f32 {
283 l2_f32_dispatched(x, y)
288 }
289
290 fn l2_batch<'a>(
291 x: &'a [Self],
292 y: &'a [Self],
293 dimension: usize,
294 ) -> impl Iterator<Item = Self> + 'a {
295 #[cfg(all(
298 target_arch = "x86_64",
299 target_feature = "avx2",
300 target_feature = "fma"
301 ))]
302 {
303 let narrow = dimension <= 16;
313 y.chunks_exact(dimension).map(move |v| {
314 if narrow {
315 unsafe { x86::l2_f32_avx_fma(x, v) }
316 } else {
317 l2_f32_scalar(x, v)
318 }
319 })
320 }
321 #[cfg(all(
322 target_arch = "x86_64",
323 not(all(target_feature = "avx2", target_feature = "fma"))
324 ))]
325 {
326 l2_batch_f32_runtime_dispatch(x, y, dimension)
327 }
328 #[cfg(not(target_arch = "x86_64"))]
329 {
330 y.chunks_exact(dimension).map(move |v| Self::l2(x, v))
331 }
332 }
333}
334
335#[cfg(all(
337 target_arch = "x86_64",
338 not(all(target_feature = "avx2", target_feature = "fma"))
339))]
340#[inline]
341fn l2_batch_f32_runtime_dispatch<'a>(
342 x: &'a [f32],
343 y: &'a [f32],
344 dimension: usize,
345) -> impl Iterator<Item = f32> + 'a {
346 match *SIMD_SUPPORT {
348 SimdSupport::Avx512 | SimdSupport::Avx512FP16 => {
349 BatchIter::Eager(unsafe { x86::l2_batch_f32_avx512(x, y, dimension) }.into_iter())
350 }
351 SimdSupport::Avx2 | SimdSupport::AvxFma => {
352 BatchIter::Eager(unsafe { x86::l2_batch_f32_avx_fma(x, y, dimension) }.into_iter())
353 }
354 SimdSupport::Avx => {
355 BatchIter::Eager(unsafe { x86::l2_batch_f32_avx(x, y, dimension) }.into_iter())
356 }
357 _ => BatchIter::Lazy(y.chunks_exact(dimension).map(move |v| l2_f32_scalar(x, v))),
358 }
359}
360
361#[inline]
365fn l2_f32_dispatched(x: &[f32], y: &[f32]) -> f32 {
366 #[cfg(target_arch = "x86_64")]
367 {
368 match *SIMD_SUPPORT {
369 SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe { x86::l2_f32_avx512(x, y) },
370 SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe { x86::l2_f32_avx_fma(x, y) },
371 SimdSupport::Avx => unsafe { x86::l2_f32_avx(x, y) },
372 _ => l2_f32_scalar(x, y),
373 }
374 }
375 #[cfg(not(target_arch = "x86_64"))]
376 {
377 l2_f32_scalar(x, y)
378 }
379}
380
381#[inline]
385fn l2_f32_scalar(x: &[f32], y: &[f32]) -> f32 {
386 l2_scalar::<f32, f32, 16>(x, y)
389}
390
391impl L2 for f64 {
392 #[inline]
393 fn l2(x: &[Self], y: &[Self]) -> f32 {
394 l2_f64_simd(x, y)
395 }
396}
397
398#[inline]
402fn l2_f64_simd(x: &[f64], y: &[f64]) -> f32 {
403 #[cfg(target_arch = "x86_64")]
404 {
405 match *SIMD_SUPPORT {
406 SimdSupport::Avx512 | SimdSupport::Avx512FP16 => unsafe { x86::l2_f64_avx512(x, y) },
407 SimdSupport::Avx2 | SimdSupport::AvxFma => unsafe { x86::l2_f64_avx_fma(x, y) },
408 SimdSupport::Avx => unsafe { x86::l2_f64_avx(x, y) },
409 _ => l2_f64_scalar(x, y),
410 }
411 }
412 #[cfg(not(target_arch = "x86_64"))]
413 {
414 l2_f64_simd_other(x, y)
415 }
416}
417
418#[cfg(target_arch = "x86_64")]
421#[inline]
422fn l2_f64_scalar(x: &[f64], y: &[f64]) -> f32 {
423 x.iter()
424 .zip(y.iter())
425 .map(|(&a, &b)| {
426 let diff = a - b;
427 diff * diff
428 })
429 .sum::<f64>() as f32
430}
431
432#[cfg(target_arch = "x86_64")]
433mod x86 {
434 use std::arch::x86_64::*;
435
436 use crate::simd::f64::{f64x4, f64x8};
437 use crate::simd::x86::hsum256_ps;
438 use crate::simd::{FloatSimd, SIMD};
439
440 #[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
451 #[target_feature(enable = "avx512f")]
452 pub(super) unsafe fn l2_batch_f32_avx512(
453 x: &[f32],
454 batch: &[f32],
455 dimension: usize,
456 ) -> Vec<f32> {
457 batch
458 .chunks_exact(dimension)
459 .map(|y| unsafe { l2_f32_avx512(x, y) })
460 .collect()
461 }
462
463 #[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
468 #[target_feature(enable = "avx,fma")]
469 pub(super) unsafe fn l2_batch_f32_avx_fma(
470 x: &[f32],
471 batch: &[f32],
472 dimension: usize,
473 ) -> Vec<f32> {
474 batch
475 .chunks_exact(dimension)
476 .map(|y| unsafe { l2_f32_avx_fma(x, y) })
477 .collect()
478 }
479
480 #[cfg(not(all(target_feature = "avx2", target_feature = "fma")))]
485 #[target_feature(enable = "avx")]
486 pub(super) unsafe fn l2_batch_f32_avx(x: &[f32], batch: &[f32], dimension: usize) -> Vec<f32> {
487 batch
488 .chunks_exact(dimension)
489 .map(|y| unsafe { l2_f32_avx(x, y) })
490 .collect()
491 }
492
493 #[target_feature(enable = "avx512f")]
495 pub unsafe fn l2_f64_avx512(x: &[f64], y: &[f64]) -> f32 {
496 let dim = x.len();
497 let unrolled_len = dim / 8 * 8;
498
499 let mut acc = _mm512_setzero_pd();
500 for i in (0..unrolled_len).step_by(8) {
501 let a = _mm512_loadu_pd(x.as_ptr().add(i));
502 let b = _mm512_loadu_pd(y.as_ptr().add(i));
503 let diff = _mm512_sub_pd(a, b);
504 acc = _mm512_fmadd_pd(diff, diff, acc);
505 }
506
507 let tail: f64 = x[unrolled_len..]
508 .iter()
509 .zip(y[unrolled_len..].iter())
510 .map(|(&a, &b)| {
511 let diff = a - b;
512 diff * diff
513 })
514 .sum();
515
516 (_mm512_reduce_add_pd(acc) + tail) as f32
517 }
518
519 #[target_feature(enable = "avx,fma")]
521 pub unsafe fn l2_f64_avx_fma(x: &[f64], y: &[f64]) -> f32 {
522 let dim = x.len();
523 let unrolled_len = dim / 8 * 8;
524
525 let mut acc8 = f64x8::zeros();
526 for i in (0..unrolled_len).step_by(8) {
527 let a = f64x8::load_unaligned(x.as_ptr().add(i));
528 let b = f64x8::load_unaligned(y.as_ptr().add(i));
529 let diff = a - b;
530 acc8.multiply_add(diff, diff);
531 }
532
533 let aligned_len = dim / 4 * 4;
534 let mut acc4 = f64x4::zeros();
535 for i in (unrolled_len..aligned_len).step_by(4) {
536 let a = f64x4::load_unaligned(x.as_ptr().add(i));
537 let b = f64x4::load_unaligned(y.as_ptr().add(i));
538 let diff = a - b;
539 acc4.multiply_add(diff, diff);
540 }
541
542 let tail: f64 = x[aligned_len..]
543 .iter()
544 .zip(y[aligned_len..].iter())
545 .map(|(&a, &b)| {
546 let diff = a - b;
547 diff * diff
548 })
549 .sum();
550
551 (acc8.reduce_sum() + acc4.reduce_sum() + tail) as f32
552 }
553
554 #[target_feature(enable = "avx")]
556 pub unsafe fn l2_f64_avx(x: &[f64], y: &[f64]) -> f32 {
557 let dim = x.len();
558 let unrolled_len = dim / 4 * 4;
559
560 let mut acc = _mm256_setzero_pd();
561 for i in (0..unrolled_len).step_by(4) {
562 let a = _mm256_loadu_pd(x.as_ptr().add(i));
563 let b = _mm256_loadu_pd(y.as_ptr().add(i));
564 let diff = _mm256_sub_pd(a, b);
565 acc = _mm256_add_pd(acc, _mm256_mul_pd(diff, diff));
566 }
567
568 let lo = _mm256_castpd256_pd128(acc);
570 let hi = _mm256_extractf128_pd(acc, 1);
571 let sum128 = _mm_add_pd(lo, hi);
572 let sum64 = _mm_add_pd(sum128, _mm_unpackhi_pd(sum128, sum128));
573 let acc_sum = _mm_cvtsd_f64(sum64);
574
575 let tail: f64 = x[unrolled_len..]
576 .iter()
577 .zip(y[unrolled_len..].iter())
578 .map(|(&a, &b)| {
579 let diff = a - b;
580 diff * diff
581 })
582 .sum();
583
584 (acc_sum + tail) as f32
585 }
586
587 #[target_feature(enable = "avx512f")]
589 pub unsafe fn l2_f32_avx512(x: &[f32], y: &[f32]) -> f32 {
590 let dim = x.len();
591 let unrolled_len = dim / 16 * 16;
592
593 let mut acc = _mm512_setzero_ps();
594 for i in (0..unrolled_len).step_by(16) {
595 let a = _mm512_loadu_ps(x.as_ptr().add(i));
596 let b = _mm512_loadu_ps(y.as_ptr().add(i));
597 let diff = _mm512_sub_ps(a, b);
598 acc = _mm512_fmadd_ps(diff, diff, acc);
599 }
600
601 let tail: f32 = x[unrolled_len..]
602 .iter()
603 .zip(y[unrolled_len..].iter())
604 .map(|(&a, &b)| {
605 let diff = a - b;
606 diff * diff
607 })
608 .sum();
609
610 _mm512_reduce_add_ps(acc) + tail
611 }
612
613 #[target_feature(enable = "avx,fma")]
615 pub unsafe fn l2_f32_avx_fma(x: &[f32], y: &[f32]) -> f32 {
616 let dim = x.len();
617 let unrolled_len = dim / 8 * 8;
618
619 let mut acc = _mm256_setzero_ps();
620 for i in (0..unrolled_len).step_by(8) {
621 let a = _mm256_loadu_ps(x.as_ptr().add(i));
622 let b = _mm256_loadu_ps(y.as_ptr().add(i));
623 let diff = _mm256_sub_ps(a, b);
624 acc = _mm256_fmadd_ps(diff, diff, acc);
625 }
626
627 let tail: f32 = x[unrolled_len..]
628 .iter()
629 .zip(y[unrolled_len..].iter())
630 .map(|(&a, &b)| {
631 let diff = a - b;
632 diff * diff
633 })
634 .sum();
635
636 hsum256_ps(acc) + tail
637 }
638
639 #[target_feature(enable = "avx")]
641 pub unsafe fn l2_f32_avx(x: &[f32], y: &[f32]) -> f32 {
642 let dim = x.len();
643 let unrolled_len = dim / 8 * 8;
644
645 let mut acc = _mm256_setzero_ps();
646 for i in (0..unrolled_len).step_by(8) {
647 let a = _mm256_loadu_ps(x.as_ptr().add(i));
648 let b = _mm256_loadu_ps(y.as_ptr().add(i));
649 let diff = _mm256_sub_ps(a, b);
650 acc = _mm256_add_ps(acc, _mm256_mul_ps(diff, diff));
651 }
652
653 let tail: f32 = x[unrolled_len..]
654 .iter()
655 .zip(y[unrolled_len..].iter())
656 .map(|(&a, &b)| {
657 let diff = a - b;
658 diff * diff
659 })
660 .sum();
661
662 hsum256_ps(acc) + tail
663 }
664}
665
666#[cfg(not(target_arch = "x86_64"))]
667#[inline]
668fn l2_f64_simd_other(x: &[f64], y: &[f64]) -> f32 {
669 use crate::simd::f64::{f64x4, f64x8};
670 use crate::simd::{FloatSimd, SIMD};
671
672 let dim = x.len();
673 let unrolled_len = dim / 8 * 8;
674
675 let mut acc8 = f64x8::zeros();
676 for i in (0..unrolled_len).step_by(8) {
677 unsafe {
678 let a = f64x8::load_unaligned(x.as_ptr().add(i));
679 let b = f64x8::load_unaligned(y.as_ptr().add(i));
680 let diff = a - b;
681 acc8.multiply_add(diff, diff);
682 }
683 }
684
685 let aligned_len = dim / 4 * 4;
686 let mut acc4 = f64x4::zeros();
687 for i in (unrolled_len..aligned_len).step_by(4) {
688 unsafe {
689 let a = f64x4::load_unaligned(x.as_ptr().add(i));
690 let b = f64x4::load_unaligned(y.as_ptr().add(i));
691 let diff = a - b;
692 acc4.multiply_add(diff, diff);
693 }
694 }
695
696 let tail: f64 = x[aligned_len..]
697 .iter()
698 .zip(y[aligned_len..].iter())
699 .map(|(&a, &b)| {
700 let diff = a - b;
701 diff * diff
702 })
703 .sum();
704
705 (acc8.reduce_sum() + acc4.reduce_sum() + tail) as f32
706}
707
708#[inline(never)]
714fn accumulate_l2_dimension(q: f32, row: &[f32], result: &mut [f32]) {
715 for (dist, &target) in result.iter_mut().zip(row.iter()) {
716 let diff = q - target;
717 *dist += diff * diff;
718 }
719}
720
721#[derive(Debug, Clone, DeepSizeOf)]
734pub struct L2Prepared {
735 transposed: Vec<f32>,
736 dimension: usize,
737 num_targets: usize,
738}
739
740impl L2Prepared {
741 pub fn new(targets: &[f32], dimension: usize) -> Self {
743 let num_targets = targets.len() / dimension;
744 debug_assert_eq!(targets.len(), num_targets * dimension);
745
746 let mut transposed = vec![0.0f32; targets.len()];
747 for t in 0..num_targets {
748 for d in 0..dimension {
749 transposed[d * num_targets + t] = targets[t * dimension + d];
750 }
751 }
752
753 Self {
754 transposed,
755 dimension,
756 num_targets,
757 }
758 }
759
760 pub fn distances_into(&self, query: &[f32], out: &mut [f32]) {
764 debug_assert_eq!(query.len(), self.dimension);
765 debug_assert_eq!(out.len(), self.num_targets);
766
767 out.fill(0.0);
768 for (d, &q) in query.iter().enumerate() {
769 let row = &self.transposed[d * self.num_targets..][..self.num_targets];
770 accumulate_l2_dimension(q, row, out);
771 }
772 }
773
774 pub fn distances(&self, query: &[f32]) -> Vec<f32> {
776 let mut result = vec![0.0f32; self.num_targets];
777 self.distances_into(query, &mut result);
778 result
779 }
780
781 pub fn nearest_into(&self, query: &[f32], buf: &mut [f32]) -> Option<u32> {
785 self.distances_into(query, buf);
786 crate::kernels::argmin_value_float(buf.iter().copied()).map(|(idx, _)| idx)
787 }
788
789 pub fn nearest(&self, query: &[f32]) -> Option<u32> {
791 self.nearest_into(query, &mut vec![0.0f32; self.num_targets])
792 }
793
794 pub fn num_targets(&self) -> usize {
796 self.num_targets
797 }
798
799 pub fn dimension(&self) -> usize {
801 self.dimension
802 }
803
804 pub fn size_bytes(&self) -> usize {
806 self.transposed.len() * std::mem::size_of::<f32>()
807 }
808}
809
810#[inline]
812pub fn l2_distance(from: &[f32], to: &[f32]) -> f32 {
813 l2(from, to)
814}
815
816pub fn l2_distance_batch<'a, T: L2>(
828 from: &'a [T],
829 to: &'a [T],
830 dimension: usize,
831) -> impl Iterator<Item = f32> + 'a {
832 assume_eq!(from.len(), dimension);
833 assume_eq!(to.len() % dimension, 0);
834
835 T::l2_batch(from, to, dimension)
836}
837
838fn do_l2_distance_arrow_batch<T: ArrowFloatType>(
839 from: &T::ArrayType,
840 to: &FixedSizeListArray,
841) -> Result<Arc<Float32Array>>
842where
843 T::Native: L2,
844{
845 let dimension = to.value_length() as usize;
846 debug_assert_eq!(from.len(), dimension);
847
848 let to_values =
850 to.values()
851 .as_any()
852 .downcast_ref::<T::ArrayType>()
853 .ok_or(Error::ComputeError(format!(
854 "Cannot downcast to the same type: {} != {}",
855 T::FLOAT_TYPE,
856 to.value_type()
857 )))?;
858 let dists = l2_distance_batch(from.as_slice(), to_values.as_slice(), dimension);
859
860 Ok(Arc::new(Float32Array::new(
861 dists.collect(),
862 to.nulls().cloned(),
863 )))
864}
865
866pub fn l2_distance_arrow_batch(
879 from: &dyn Array,
880 to: &FixedSizeListArray,
881) -> Result<Arc<Float32Array>> {
882 match *from.data_type() {
883 DataType::Float16 => do_l2_distance_arrow_batch::<Float16Type>(from.as_primitive(), to),
884 DataType::Float32 => do_l2_distance_arrow_batch::<Float32Type>(from.as_primitive(), to),
885 DataType::Float64 => do_l2_distance_arrow_batch::<Float64Type>(from.as_primitive(), to),
886 DataType::Int8 => do_l2_distance_arrow_batch::<Float32Type>(
887 &from
888 .as_primitive::<Int8Type>()
889 .into_iter()
890 .map(|x| x.unwrap() as f32)
891 .collect(),
892 &to.convert_to_floating_point()?,
893 ),
894 _ => Err(Error::ComputeError(format!(
895 "Unsupported data type: {}",
896 from.data_type()
897 ))),
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use super::*;
904
905 use approx::assert_relative_eq;
906 use num_traits::ToPrimitive;
907 use proptest::prelude::*;
908
909 use crate::test_utils::{
910 arbitrary_bf16, arbitrary_f16, arbitrary_f32, arbitrary_f64, arbitrary_vector_pair,
911 };
912
913 #[test]
914 fn test_l2_f32_dispatch_matches_scalar() {
915 for dim in [1usize, 7, 15, 16, 17, 31, 33, 64, 100, 1024] {
917 let x: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.5 - 3.0).collect();
918 let y: Vec<f32> = (0..dim).map(|i| (i as f32) * -0.25 + 1.5).collect();
919 assert_relative_eq!(l2_f32(&x, &y), l2(&x, &y), max_relative = 1e-5);
920 }
921 }
922
923 #[test]
924 fn test_euclidean_distance() {
925 let mat = FixedSizeListArray::from_iter_primitive::<Float32Type, _, _>(
926 vec![
927 Some((0..8).map(|v| Some(v as f32)).collect::<Vec<_>>()),
928 Some((1..9).map(|v| Some(v as f32)).collect::<Vec<_>>()),
929 Some((2..10).map(|v| Some(v as f32)).collect::<Vec<_>>()),
930 Some((3..11).map(|v| Some(v as f32)).collect::<Vec<_>>()),
931 ],
932 8,
933 );
934 let point = Float32Array::from((2..10).map(|v| Some(v as f32)).collect::<Vec<_>>());
935 let distances = l2_distance_batch(
936 point.values(),
937 mat.values().as_primitive::<Float32Type>().values(),
938 8,
939 )
940 .collect::<Vec<_>>();
941
942 assert_eq!(distances, vec![32.0, 8.0, 0.0, 8.0]);
943 }
944
945 #[test]
946 fn test_not_aligned() {
947 let mat = (0..6)
948 .chain(0..8)
949 .chain(1..9)
950 .chain(2..10)
951 .chain(3..11)
952 .map(|v| v as f32)
953 .collect::<Vec<_>>();
954 let point = Float32Array::from((0..10).map(|v| Some(v as f32)).collect::<Vec<_>>());
955 let distances = l2_distance_batch(&point.values()[2..], &mat[6..], 8).collect::<Vec<_>>();
956
957 assert_eq!(distances, vec![32.0, 8.0, 0.0, 8.0]);
958 }
959
960 #[test]
961 fn test_odd_length_vector() {
962 let mat = Float32Array::from_iter((0..5).map(|v| Some(v as f32)));
963 let point = Float32Array::from((2..7).map(|v| Some(v as f32)).collect::<Vec<_>>());
964 let distances = l2_distance_batch(point.values(), mat.values(), 5).collect::<Vec<_>>();
965
966 assert_eq!(distances, vec![20.0]);
967 }
968
969 #[test]
970 fn test_l2_distance_cases() {
971 let values: Float32Array = vec![
972 0.25335717, 0.24663818, 0.26330215, 0.14988247, 0.06042378, 0.21077952, 0.26687378,
973 0.22145681, 0.18319066, 0.18688454, 0.05216244, 0.11470364, 0.10554603, 0.19964123,
974 0.06387895, 0.18992095, 0.00123718, 0.13500804, 0.09516747, 0.19508345, 0.2582458,
975 0.1211653, 0.21121833, 0.24809816, 0.04078768, 0.19586588, 0.16496408, 0.14766085,
976 0.04898421, 0.14728612, 0.21263947, 0.16763233,
977 ]
978 .into();
979
980 let q: Float32Array = vec![
981 0.18549609,
982 0.29954708,
983 0.28318876,
984 0.05424477,
985 0.093134984,
986 0.21580857,
987 0.2951282,
988 0.19866848,
989 0.13868214,
990 0.19819534,
991 0.23271298,
992 0.047727287,
993 0.14394054,
994 0.023316395,
995 0.18589257,
996 0.037315924,
997 0.07037327,
998 0.32609823,
999 0.07344752,
1000 0.020155912,
1001 0.18485495,
1002 0.32763934,
1003 0.14296658,
1004 0.04498596,
1005 0.06254237,
1006 0.24348071,
1007 0.16009757,
1008 0.053892266,
1009 0.05918874,
1010 0.040363103,
1011 0.19913352,
1012 0.14545348,
1013 ]
1014 .into();
1015
1016 let d = l2_distance_batch(q.values(), values.values(), 32).collect::<Vec<_>>();
1017 assert_relative_eq!(0.319_357_84, d[0]);
1018 }
1019
1020 fn l2_distance_reference(x: &[f64], y: &[f64]) -> f64 {
1024 x.iter()
1025 .zip(y.iter())
1026 .map(|(x, y)| (*x - *y).powi(2))
1027 .sum::<f64>()
1028 }
1029
1030 fn do_l2_test<T: L2 + ToPrimitive>(x: &[T], y: &[T]) -> std::result::Result<(), TestCaseError> {
1031 let x_f64 = x.iter().map(|v| v.to_f64().unwrap()).collect::<Vec<f64>>();
1032 let y_f64 = y.iter().map(|v| v.to_f64().unwrap()).collect::<Vec<f64>>();
1033
1034 let result = l2(x, y);
1035 let reference = l2_distance_reference(&x_f64, &y_f64) as f32;
1036
1037 prop_assert!(approx::relative_eq!(result, reference, max_relative = 1e-6));
1038 Ok(())
1039 }
1040
1041 #[test]
1042 fn test_l2_distance_f16_max() {
1043 let x = vec![f16::MAX; 4048];
1044 let y = vec![-f16::MAX; 4048];
1045 do_l2_test(&x, &y).unwrap();
1046 }
1047
1048 proptest::proptest! {
1053 #[test]
1054 fn test_l2_distance_f16((x, y) in arbitrary_vector_pair(arbitrary_f16, 4..4048)) {
1055 do_l2_test(&x, &y)?;
1056 }
1057
1058 #[test]
1059 fn test_l2_distance_bf16((x, y) in arbitrary_vector_pair(arbitrary_bf16, 4..4048)){
1060 do_l2_test(&x, &y)?;
1061 }
1062
1063 #[test]
1064 fn test_l2_distance_f32((x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)){
1065 do_l2_test(&x, &y)?;
1066 }
1067
1068 #[test]
1069 fn test_l2_distance_f64((x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)){
1070 do_l2_test(&x, &y)?;
1071 }
1072
1073 #[cfg(target_arch = "x86_64")]
1078 #[test]
1079 fn test_l2_f64_scalar_simd_parity(
1080 (x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
1081 ) {
1082 let scalar = l2_f64_scalar(&x, &y);
1083 let simd = l2_f64_simd(&x, &y);
1084 prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-6));
1085 }
1086
1087 #[test]
1094 fn test_l2_f32_scalar_simd_parity(
1095 (x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
1096 ) {
1097 let scalar = x
1098 .iter()
1099 .zip(y.iter())
1100 .map(|(&a, &b)| ((a as f64) - (b as f64)).powi(2))
1101 .sum::<f64>() as f32;
1102 let simd = <f32 as L2>::l2(&x, &y);
1103 prop_assert!(approx::relative_eq!(scalar, simd, max_relative = 1e-3));
1104 }
1105
1106 #[cfg(target_arch = "x86_64")]
1111 #[test]
1112 fn test_l2_f64_scalar_vs_avx512_parity(
1113 (x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
1114 ) {
1115 if !std::is_x86_feature_detected!("avx512f") {
1116 return Ok(());
1117 }
1118 let scalar = l2_f64_scalar(&x, &y);
1119 let avx512 = unsafe { x86::l2_f64_avx512(&x, &y) };
1120 prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-6));
1121 }
1122
1123 #[cfg(target_arch = "x86_64")]
1127 #[test]
1128 fn test_l2_f64_scalar_vs_avx_fma_parity(
1129 (x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
1130 ) {
1131 if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
1132 return Ok(());
1133 }
1134 let scalar = l2_f64_scalar(&x, &y);
1135 let avx_fma = unsafe { x86::l2_f64_avx_fma(&x, &y) };
1136 prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-6));
1137 }
1138
1139 #[cfg(target_arch = "x86_64")]
1143 #[test]
1144 fn test_l2_f64_scalar_vs_avx_parity(
1145 (x, y) in arbitrary_vector_pair(arbitrary_f64, 4..4048)
1146 ) {
1147 if !std::is_x86_feature_detected!("avx") {
1148 return Ok(());
1149 }
1150 let scalar = l2_f64_scalar(&x, &y);
1151 let avx = unsafe { x86::l2_f64_avx(&x, &y) };
1152 prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-6));
1153 }
1154
1155 #[cfg(target_arch = "x86_64")]
1159 #[test]
1160 fn test_l2_f32_scalar_vs_avx512_parity(
1161 (x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
1162 ) {
1163 if !std::is_x86_feature_detected!("avx512f") {
1164 return Ok(());
1165 }
1166 let scalar = l2_f32_scalar(&x, &y);
1167 let avx512 = unsafe { x86::l2_f32_avx512(&x, &y) };
1168 prop_assert!(approx::relative_eq!(scalar, avx512, max_relative = 1e-3));
1169 }
1170
1171 #[cfg(target_arch = "x86_64")]
1175 #[test]
1176 fn test_l2_f32_scalar_vs_avx_fma_parity(
1177 (x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
1178 ) {
1179 if !(std::is_x86_feature_detected!("avx") && std::is_x86_feature_detected!("fma")) {
1180 return Ok(());
1181 }
1182 let scalar = l2_f32_scalar(&x, &y);
1183 let avx_fma = unsafe { x86::l2_f32_avx_fma(&x, &y) };
1184 prop_assert!(approx::relative_eq!(scalar, avx_fma, max_relative = 1e-3));
1185 }
1186
1187 #[cfg(target_arch = "x86_64")]
1191 #[test]
1192 fn test_l2_f32_scalar_vs_avx_parity(
1193 (x, y) in arbitrary_vector_pair(arbitrary_f32, 4..4048)
1194 ) {
1195 if !std::is_x86_feature_detected!("avx") {
1196 return Ok(());
1197 }
1198 let scalar = l2_f32_scalar(&x, &y);
1199 let avx = unsafe { x86::l2_f32_avx(&x, &y) };
1200 prop_assert!(approx::relative_eq!(scalar, avx, max_relative = 1e-3));
1201 }
1202 }
1203
1204 #[test]
1205 fn test_uint8_l2_edge_cases() {
1206 let q = vec![0_u8; 2048];
1207 let v = vec![0_u8; 2048];
1208 assert_eq!(l2_distance_uint_scalar(&q, &v), 0.0);
1209
1210 let q = vec![0_u8; 2048];
1211 let v = vec![255_u8; 2048];
1212 assert_eq!(
1213 l2_distance_uint_scalar(&q, &v),
1214 (255_u32.pow(2) * 2048) as f32
1215 );
1216 assert_eq!(
1217 l2_distance_uint_scalar(&v, &q),
1218 (255_u32.pow(2) * 2048) as f32
1219 );
1220 }
1221
1222 #[test]
1223 fn test_l2_targets_matches_scalar() {
1224 let cases = vec![
1225 (16, 8), (16, 16), (16, 256), (16, 17), (16, 31), (1, 32), (3, 20), (128, 64), ];
1234
1235 for (dim, num_targets) in cases {
1236 let query: Vec<f32> = (0..dim).map(|i| (i as f32) * 0.1 + 0.05).collect();
1237 let targets: Vec<f32> = (0..dim * num_targets)
1238 .map(|i| ((i * 7 + 3) % 100) as f32 * 0.01)
1239 .collect();
1240
1241 let expected: Vec<f32> = targets
1242 .chunks_exact(dim)
1243 .map(|v| l2_scalar::<f32, f32, 16>(&query, v))
1244 .collect();
1245
1246 let prepared = L2Prepared::new(&targets, dim);
1247 let actual = prepared.distances(&query);
1248
1249 assert_eq!(
1250 actual.len(),
1251 expected.len(),
1252 "length mismatch for dim={dim}, num_targets={num_targets}"
1253 );
1254 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
1255 assert!(
1256 approx::relative_eq!(a, e, max_relative = 1e-6),
1257 "mismatch at index {i} for dim={dim}, num_targets={num_targets}: \
1258 prepared={a}, scalar={e}"
1259 );
1260 }
1261 }
1262 }
1263
1264 #[test]
1265 fn test_l2_targets_zeros() {
1266 let dim = 16;
1267 let num_targets = 32;
1268 let query = vec![0.0f32; dim];
1269 let targets = vec![0.0f32; dim * num_targets];
1270
1271 let prepared = L2Prepared::new(&targets, dim);
1272 let distances = prepared.distances(&query);
1273 assert_eq!(distances.len(), num_targets);
1274 for d in &distances {
1275 assert_eq!(*d, 0.0);
1276 }
1277 }
1278
1279 #[test]
1280 fn test_l2_targets_known_values() {
1281 let dim = 2;
1282 let query = vec![1.0f32, 0.0];
1283
1284 let mut targets = vec![1.0, 0.0, 0.0, 1.0, 2.0, 0.0, 0.0, 0.0];
1286 for _ in 4..16 {
1287 targets.extend_from_slice(&[0.0, 0.0]);
1288 }
1289
1290 let prepared = L2Prepared::new(&targets, dim);
1291 let distances = prepared.distances(&query);
1292 assert_eq!(distances.len(), 16);
1293 assert_relative_eq!(distances[0], 0.0);
1294 assert_relative_eq!(distances[1], 2.0);
1295 assert_relative_eq!(distances[2], 1.0);
1296 assert_relative_eq!(distances[3], 1.0);
1297 for d in &distances[4..] {
1298 assert_relative_eq!(*d, 1.0);
1299 }
1300 }
1301
1302 #[test]
1303 fn test_l2_targets_reuse() {
1304 let dim = 4;
1306 let targets = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
1307 let prepared = L2Prepared::new(&targets, dim);
1308
1309 let q1 = vec![1.0, 2.0, 3.0, 4.0];
1310 let q2 = vec![5.0, 6.0, 7.0, 8.0];
1311
1312 let d1 = prepared.distances(&q1);
1313 let d2 = prepared.distances(&q2);
1314
1315 assert_relative_eq!(d1[0], 0.0); assert_relative_eq!(d2[1], 0.0); }
1318
1319 #[rstest::rstest]
1323 #[case::dim_8(8)]
1324 #[case::dim_16(16)]
1325 #[case::dim_32(32)]
1326 #[case::dim_1024(1024)]
1327 fn test_l2_batch_f32_matches_per_vector_l2(#[case] dimension: usize) {
1328 let num_vectors = 5;
1329 let x: Vec<f32> = (0..dimension)
1330 .map(|i| ((i % 13) as f32) * 0.25 + 1.0)
1331 .collect();
1332 let batch: Vec<f32> = (0..dimension * num_vectors)
1333 .map(|i| ((i % 11) as f32) * 0.5 - 2.0)
1334 .collect();
1335
1336 let got: Vec<f32> = f32::l2_batch(&x, &batch, dimension).collect();
1337 let want: Vec<f32> = batch
1338 .chunks_exact(dimension)
1339 .map(|y| f32::l2(&x, y))
1340 .collect();
1341
1342 assert_eq!(got.len(), num_vectors);
1343 for (g, w) in got.iter().zip(want.iter()) {
1344 assert!(
1345 approx::relative_eq!(g, w, epsilon = 1e-4),
1346 "dim {dimension}: batch {g} != per-vector {w}"
1347 );
1348 }
1349 }
1350
1351 #[cfg(all(
1354 target_arch = "x86_64",
1355 not(all(target_feature = "avx2", target_feature = "fma"))
1356 ))]
1357 fn check_l2_batch_kernel(kernel: unsafe fn(&[f32], &[f32], usize) -> Vec<f32>) {
1358 for dimension in [8_usize, 16, 40] {
1359 let num_vectors = 3;
1360 let x: Vec<f32> = (0..dimension).map(|i| (i as f32) * 0.5 + 1.0).collect();
1361 let batch: Vec<f32> = (0..dimension * num_vectors)
1362 .map(|i| ((i % 7) as f32) + 1.0)
1363 .collect();
1364
1365 let got = unsafe { kernel(&x, &batch, dimension) };
1366 assert_eq!(got.len(), num_vectors);
1367 for (chunk, &g) in batch.chunks_exact(dimension).zip(got.iter()) {
1368 let want = l2_scalar::<f32, f32, 16>(&x, chunk);
1369 assert!(
1370 approx::relative_eq!(g, want, epsilon = 1e-4),
1371 "dim {dimension}: kernel {g} != scalar {want}"
1372 );
1373 }
1374 }
1375 }
1376
1377 #[cfg(all(
1380 target_arch = "x86_64",
1381 not(all(target_feature = "avx2", target_feature = "fma"))
1382 ))]
1383 #[test]
1384 fn test_l2_batch_avx_fma_matches_scalar() {
1385 if !std::is_x86_feature_detected!("avx") || !std::is_x86_feature_detected!("fma") {
1386 return;
1387 }
1388 check_l2_batch_kernel(x86::l2_batch_f32_avx_fma);
1389 }
1390
1391 #[cfg(all(
1392 target_arch = "x86_64",
1393 not(all(target_feature = "avx2", target_feature = "fma"))
1394 ))]
1395 #[test]
1396 fn test_l2_batch_avx_matches_scalar() {
1397 if !std::is_x86_feature_detected!("avx") {
1398 return;
1399 }
1400 check_l2_batch_kernel(x86::l2_batch_f32_avx);
1401 }
1402
1403 #[cfg(all(
1404 target_arch = "x86_64",
1405 not(all(target_feature = "avx2", target_feature = "fma"))
1406 ))]
1407 #[test]
1408 fn test_l2_batch_avx512_matches_scalar() {
1409 if !std::is_x86_feature_detected!("avx512f") {
1410 return;
1411 }
1412 check_l2_batch_kernel(x86::l2_batch_f32_avx512);
1413 }
1414}