1use std::{marker::PhantomData, num::NonZeroUsize};
47
48use diskann_utils::views::rowmajor::{self, Matrix, MatrixMut};
49use diskann_vector::distance::Metric as VectorMetric;
50use diskann_wide::{
51 SIMDFloat, SIMDSumTree, SIMDVector,
52 arch::{Architecture, Dispatched3, FTarget3, Scalar, Target, dispatch_no_features},
53 lifetime::Ref,
54};
55use thiserror::Error;
56
57#[cfg(target_arch = "x86_64")]
58use diskann_wide::arch::x86_64::{V3, V4};
59
60#[cfg(target_arch = "aarch64")]
61use diskann_wide::arch::aarch64::Neon;
62
63use crate::{product::tables::BasicTableView, views::ChunkOffsets};
64
65#[derive(Debug, Clone)]
84pub struct PaddedTable {
85 pivots: rowmajor::Owned<f32>,
90 offsets: ChunkOffsets,
91 pivots_per_chunk: usize,
92 simd_width: SIMDWidth,
93 arch: RuntimeArch,
94}
95
96impl PaddedTable {
97 pub fn from_basic(basic: BasicTableView<'_>) -> Self {
99 let arch = RuntimeArch::new();
100 Self::from_basic_with(basic, arch)
101 }
102
103 fn from_basic_with(basic: BasicTableView<'_>, arch: RuntimeArch) -> Self {
104 let pivots = basic.view_pivots();
105 let offsets = basic.view_offsets();
106
107 let pivots_per_chunk = pivots.nrows();
108
109 let max_chunk_dim = offsets.max_chunk_dim();
115 let simd_width = arch.select_simd_width(max_chunk_dim);
116 let padded_dim = max_chunk_dim
117 .get()
118 .next_multiple_of(simd_width.as_nonzero().get());
119
120 let rows = pivots_per_chunk * offsets.len();
121 let mut padded = rowmajor::Owned::from_element(rows, padded_dim, 0.0);
122 let mut row = 0;
123
124 fn copy_from_slice_subset(dst: &mut [f32], src: &[f32]) {
126 dst[..src.len()].copy_from_slice(src)
127 }
128
129 (0..offsets.len()).for_each(|i| {
131 #[expect(clippy::expect_used, reason = "`i` should be in-bounds")]
132 let view = basic.pivots_for(i).expect("`i` should be in-bounds");
133 view.rows().for_each(|src| {
134 copy_from_slice_subset(padded.row_mut(row), src);
135 row += 1;
136 });
137 });
138
139 Self {
140 pivots: padded,
141 offsets: offsets.to_owned(),
142 pivots_per_chunk,
143 simd_width,
144 arch,
145 }
146 }
147
148 pub fn vtable(&self, metric: Metric) -> VTable {
156 self.arch.dispatch(self.simd_width, metric)
157 }
158
159 pub fn ncenters(&self) -> usize {
161 self.pivots_per_chunk
162 }
163
164 pub fn nchunks(&self) -> usize {
166 self.offsets.len()
167 }
168
169 pub fn dim(&self) -> usize {
171 self.offsets.dim()
172 }
173}
174
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum Metric {
178 SquaredL2,
179 InnerProduct,
180 Cosine,
181}
182
183impl From<VectorMetric> for Metric {
184 fn from(metric: VectorMetric) -> Self {
185 match metric {
186 VectorMetric::L2 => Self::SquaredL2,
187 VectorMetric::InnerProduct => Self::InnerProduct,
188 VectorMetric::Cosine => Self::Cosine,
189 VectorMetric::CosineNormalized => Self::Cosine,
190 }
191 }
192}
193
194type Distance = Dispatched3<Result<f32, DistanceError>, Ref<PaddedTable>, Ref<[f32]>, Ref<[u8]>>;
195type SelfDistance =
196 Dispatched3<Result<f32, SelfDistanceError>, Ref<PaddedTable>, Ref<[u8]>, Ref<[u8]>>;
197
198#[derive(Debug, Clone, Copy)]
202pub struct VTable {
203 distance: Distance,
204 self_distance: SelfDistance,
205}
206
207impl VTable {
208 fn new<O>(arch: O::Arch) -> Self
209 where
210 O: Op,
211 {
212 Self {
213 distance: arch.dispatch3::<
214 OpTarget<O>,
215 Result<f32, DistanceError>,
216 Ref<PaddedTable>,
217 Ref<[f32]>,
218 Ref<[u8]>,
219 >(),
220 self_distance: arch.dispatch3::<
221 OpTarget<O>,
222 Result<f32, SelfDistanceError>,
223 Ref<PaddedTable>,
224 Ref<[u8]>,
225 Ref<[u8]>,
226 >(),
227 }
228 }
229
230 #[inline]
244 pub fn distance(
245 &self,
246 padded: &PaddedTable,
247 vector: &[f32],
248 codes: &[u8],
249 ) -> Result<f32, DistanceError> {
250 (self.distance).call(padded, vector, codes)
251 }
252
253 #[inline]
267 pub fn self_distance(
268 &self,
269 padded: &PaddedTable,
270 a: &[u8],
271 b: &[u8],
272 ) -> Result<f32, SelfDistanceError> {
273 (self.self_distance).call(padded, a, b)
274 }
275}
276
277const FOUR: NonZeroUsize = NonZeroUsize::new(4).unwrap();
282
283#[cfg(target_arch = "x86_64")]
284const EIGHT: NonZeroUsize = NonZeroUsize::new(8).unwrap();
285
286#[derive(Debug, Clone, Copy)]
287enum SIMDWidth {
288 Four,
289 #[cfg(target_arch = "x86_64")]
290 Eight,
291}
292
293impl SIMDWidth {
294 fn as_nonzero(self) -> NonZeroUsize {
295 match self {
296 Self::Four => FOUR,
297 #[cfg(target_arch = "x86_64")]
298 Self::Eight => EIGHT,
299 }
300 }
301}
302
303#[derive(Debug, Clone, Copy)]
305enum RuntimeArch {
306 Scalar(Scalar),
307 #[cfg(target_arch = "x86_64")]
308 V3(V3),
309 #[cfg(target_arch = "aarch64")]
310 Neon(Neon),
311}
312
313impl RuntimeArch {
314 fn new() -> Self {
315 dispatch_no_features(GetArch)
316 }
317
318 #[cfg_attr(
319 not(target_arch = "x86_64"),
320 expect(
321 unused_variables,
322 reason = "the same result is returned regardless of the actual chunk size"
323 )
324 )]
325 fn select_simd_width(&self, max_chunk_size: NonZeroUsize) -> SIMDWidth {
326 match self {
327 Self::Scalar(_) => SIMDWidth::Four,
328 #[cfg(target_arch = "x86_64")]
330 Self::V3(_) => match max_chunk_size.get() {
331 0..=4 => SIMDWidth::Four,
332 _ => SIMDWidth::Eight,
333 },
334 #[cfg(target_arch = "aarch64")]
335 Self::Neon(_) => SIMDWidth::Four,
336 }
337 }
338
339 fn dispatch(&self, simd_width: SIMDWidth, metric: Metric) -> VTable {
340 diskann_wide::alias!(f32x4<A> = f32x4);
341 diskann_wide::alias!(f32x8<A> = f32x8);
342
343 match (*self, simd_width, metric) {
344 (Self::Scalar(a), _, Metric::SquaredL2) => VTable::new::<SquaredL2<f32x4<Scalar>>>(a),
345 (Self::Scalar(a), _, Metric::InnerProduct) => {
346 VTable::new::<InnerProduct<f32x4<Scalar>>>(a)
347 }
348 (Self::Scalar(a), _, Metric::Cosine) => VTable::new::<Cosine<f32x4<Scalar>>>(a),
349
350 #[cfg(target_arch = "x86_64")]
352 (Self::V3(a), SIMDWidth::Four, Metric::SquaredL2) => {
353 VTable::new::<SquaredL2<f32x4<V3>>>(a)
354 }
355 #[cfg(target_arch = "x86_64")]
356 (Self::V3(a), SIMDWidth::Four, Metric::InnerProduct) => {
357 VTable::new::<InnerProduct<f32x4<V3>>>(a)
358 }
359 #[cfg(target_arch = "x86_64")]
360 (Self::V3(a), SIMDWidth::Four, Metric::Cosine) => VTable::new::<Cosine<f32x4<V3>>>(a),
361
362 #[cfg(target_arch = "x86_64")]
363 (Self::V3(a), SIMDWidth::Eight, Metric::SquaredL2) => {
364 VTable::new::<SquaredL2<f32x8<V3>>>(a)
365 }
366 #[cfg(target_arch = "x86_64")]
367 (Self::V3(a), SIMDWidth::Eight, Metric::InnerProduct) => {
368 VTable::new::<InnerProduct<f32x8<V3>>>(a)
369 }
370 #[cfg(target_arch = "x86_64")]
371 (Self::V3(a), SIMDWidth::Eight, Metric::Cosine) => VTable::new::<Cosine<f32x8<V3>>>(a),
372
373 #[cfg(target_arch = "aarch64")]
375 (Self::Neon(a), _, Metric::SquaredL2) => VTable::new::<SquaredL2<f32x4<Neon>>>(a),
376 #[cfg(target_arch = "aarch64")]
377 (Self::Neon(a), _, Metric::InnerProduct) => VTable::new::<InnerProduct<f32x4<Neon>>>(a),
378 #[cfg(target_arch = "aarch64")]
379 (Self::Neon(a), _, Metric::Cosine) => VTable::new::<Cosine<f32x4<Neon>>>(a),
380 }
381 }
382}
383
384#[derive(Debug, Clone, Copy)]
385struct GetArch;
386
387impl Target<Scalar, RuntimeArch> for GetArch {
388 #[inline(always)]
389 fn run(self, arch: Scalar) -> RuntimeArch {
390 RuntimeArch::Scalar(arch)
391 }
392}
393
394#[cfg(target_arch = "x86_64")]
395impl Target<V3, RuntimeArch> for GetArch {
396 #[inline(always)]
397 fn run(self, arch: V3) -> RuntimeArch {
398 RuntimeArch::V3(arch)
399 }
400}
401
402#[cfg(target_arch = "x86_64")]
403impl Target<V4, RuntimeArch> for GetArch {
404 #[inline(always)]
405 fn run(self, arch: V4) -> RuntimeArch {
406 RuntimeArch::V3(arch.retarget())
407 }
408}
409
410#[cfg(target_arch = "aarch64")]
411impl Target<Neon, RuntimeArch> for GetArch {
412 #[inline(always)]
413 fn run(self, arch: Neon) -> RuntimeArch {
414 RuntimeArch::Neon(arch)
415 }
416}
417
418trait Op {
423 type Arch: Architecture;
424 type Accum;
425 type Vector: SIMDVector<Scalar = f32, Arch = Self::Arch>;
426
427 fn init(arch: Self::Arch) -> Self::Accum;
428 fn accum(acc: Self::Accum, x: Self::Vector, y: Self::Vector) -> Self::Accum;
429 fn reduce_pair(a: Self::Accum, b: Self::Accum) -> f32;
430}
431
432#[derive(Debug)]
433struct SquaredL2<V>(PhantomData<V>);
434
435impl<V> Op for SquaredL2<V>
436where
437 V: SIMDFloat<Scalar = f32> + SIMDSumTree,
438 V::Arch: Architecture,
439{
440 type Arch = V::Arch;
441 type Accum = V;
442 type Vector = V;
443
444 fn init(arch: Self::Arch) -> V {
445 V::default(arch)
446 }
447
448 fn accum(acc: V, x: V, y: V) -> V {
449 let d = x - y;
450 d.mul_add_simd(d, acc)
451 }
452
453 fn reduce_pair(a: V, b: V) -> f32 {
454 (a + b).sum_tree()
455 }
456}
457
458#[derive(Debug)]
459struct InnerProduct<V>(PhantomData<V>);
460
461impl<V> Op for InnerProduct<V>
462where
463 V: SIMDFloat<Scalar = f32> + SIMDSumTree,
464 V::Arch: Architecture,
465{
466 type Arch = V::Arch;
467 type Accum = V;
468 type Vector = V;
469
470 fn init(arch: Self::Arch) -> V {
471 V::default(arch)
472 }
473
474 fn accum(acc: V, x: V, y: V) -> V {
475 x.mul_add_simd(y, acc)
476 }
477
478 fn reduce_pair(a: V, b: V) -> f32 {
479 -(a + b).sum_tree()
480 }
481}
482
483#[derive(Debug)]
484struct Cosine<V>(PhantomData<V>);
485
486#[derive(Debug)]
487struct CosineAccumulator<V> {
488 xy: V,
489 xnorm: V,
490 ynorm: V,
491}
492
493fn finish_cosine(xy: f32, xnorm: f32, ynorm: f32) -> f32 {
494 if xnorm < f32::MIN_POSITIVE || ynorm < f32::MIN_POSITIVE {
495 1.0
496 } else {
497 let v = xy / (xnorm.sqrt() * ynorm.sqrt());
498 1.0 - (-1.0f32).max(1.0f32.min(v))
499 }
500}
501
502impl<V> Op for Cosine<V>
503where
504 V: SIMDFloat<Scalar = f32> + SIMDSumTree,
505 V::Arch: Architecture,
506{
507 type Arch = V::Arch;
508 type Accum = CosineAccumulator<V>;
509 type Vector = V;
510
511 fn init(arch: Self::Arch) -> Self::Accum {
512 CosineAccumulator {
513 xy: V::default(arch),
514 xnorm: V::default(arch),
515 ynorm: V::default(arch),
516 }
517 }
518
519 fn accum(acc: Self::Accum, x: V, y: V) -> Self::Accum {
520 CosineAccumulator {
521 xy: x.mul_add_simd(y, acc.xy),
522 xnorm: x.mul_add_simd(x, acc.xnorm),
523 ynorm: y.mul_add_simd(y, acc.ynorm),
524 }
525 }
526
527 fn reduce_pair(a: Self::Accum, b: Self::Accum) -> f32 {
528 let xy = (a.xy + b.xy).sum_tree();
529 let xnorm = (a.xnorm + b.xnorm).sum_tree();
530 let ynorm = (a.ynorm + b.ynorm).sum_tree();
531 finish_cosine(xy, xnorm, ynorm)
532 }
533}
534
535#[derive(Debug, Clone, Copy)]
536struct OpTarget<O>(PhantomData<O>);
537
538impl<O> FTarget3<O::Arch, Result<f32, DistanceError>, &PaddedTable, &[f32], &[u8]> for OpTarget<O>
539where
540 O: Op,
541{
542 #[inline(always)]
543 fn run(arch: O::Arch, table: &PaddedTable, a: &[f32], b: &[u8]) -> Result<f32, DistanceError> {
544 distance::<O>(arch, table, a, b)
545 }
546}
547
548impl<O> FTarget3<O::Arch, Result<f32, SelfDistanceError>, &PaddedTable, &[u8], &[u8]>
549 for OpTarget<O>
550where
551 O: Op,
552{
553 #[inline(always)]
554 fn run(
555 arch: O::Arch,
556 table: &PaddedTable,
557 a: &[u8],
558 b: &[u8],
559 ) -> Result<f32, SelfDistanceError> {
560 self_distance::<O>(arch, table, a, b)
561 }
562}
563
564#[derive(Debug, Error)]
570#[non_exhaustive]
571pub enum DistanceError {
572 #[error("table has dimension {} but full vector has length {}", dim, alen)]
573 ALen { dim: usize, alen: usize },
574 #[error("table has {} chunks but codes slice has length {}", chunks, blen)]
575 BLen { chunks: usize, blen: usize },
576 #[error("codes have a value that exceeds the number of pivots {}", ncenters)]
577 OutOfBounds { ncenters: usize },
578 #[error("An invalid SIMD width was chosen - ensure the correct vtable is used")]
579 InvalidSimd,
580}
581
582#[inline(always)]
583fn distance<O>(
584 arch: O::Arch,
585 table: &PaddedTable,
586 a: &[f32],
587 b: &[u8],
588) -> Result<f32, DistanceError>
589where
590 O: Op,
591{
592 if !table
594 .simd_width
595 .as_nonzero()
596 .get()
597 .is_multiple_of(O::Vector::LANES)
598 {
599 return Err(DistanceError::InvalidSimd);
600 }
601
602 if a.len() != table.dim() {
604 return Err(DistanceError::ALen {
605 dim: table.dim(),
606 alen: a.len(),
607 });
608 }
609
610 if b.len() != table.nchunks() {
612 return Err(DistanceError::BLen {
613 chunks: table.nchunks(),
614 blen: b.len(),
615 });
616 }
617
618 if let Ok(ncenters) = u8::try_from(table.ncenters())
620 && let Some(max) = b.iter().max()
621 && *max >= ncenters
622 {
623 return Err(DistanceError::OutOfBounds {
624 ncenters: table.ncenters(),
625 });
626 }
627
628 let pivots = &table.pivots;
631
632 let pivot_stride = pivots.ncols();
633 let chunk_stride = table.pivots_per_chunk * pivot_stride;
634
635 let nchunks = b.len();
636 let mut d0 = O::init(arch);
637 let mut d1 = O::init(arch);
638
639 let mut i = 0;
640 let mut p = pivots.as_ptr();
641
642 let offsets = table.offsets.as_slice();
643 let aptr = a.as_ptr();
644
645 let lanes = O::Vector::LANES;
646
647 while i + 2 <= nchunks {
674 let (o0, o1, o2) = unsafe {
676 (
677 *offsets.get_unchecked(i),
678 *offsets.get_unchecked(i + 1),
679 *offsets.get_unchecked(i + 2),
680 )
681 };
682
683 let (a0, a1) = unsafe { (aptr.add(o0), aptr.add(o1)) };
685
686 let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
689
690 let b1 = unsafe { p.add(chunk_stride + pivot_stride * (*b.get_unchecked(i + 1) as usize)) };
693
694 let full0 = (o1 - o0) / lanes;
695 let full1 = (o2 - o1) / lanes;
696
697 let common = full0.min(full1);
698 for j in 0..common {
699 unsafe {
703 let va = O::Vector::load_simd(arch, a0.add(lanes * j));
704 let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
705 d0 = O::accum(d0, va, vb);
706
707 let va = O::Vector::load_simd(arch, a1.add(lanes * j));
708 let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
709 d1 = O::accum(d1, va, vb);
710 }
711 }
712
713 for j in common..full0 {
715 unsafe {
718 let va = O::Vector::load_simd(arch, a0.add(lanes * j));
719 let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
720 d0 = O::accum(d0, va, vb);
721 }
722 }
723
724 let a0_remaining = (o1 - o0) - full0 * lanes;
725 if a0_remaining != 0 {
726 let va =
729 unsafe { O::Vector::load_simd_first(arch, a0.add(lanes * full0), a0_remaining) };
730 let vb = unsafe { O::Vector::load_simd(arch, b0.add(lanes * full0)) };
733 d0 = O::accum(d0, va, vb);
734 }
735
736 for j in common..full1 {
738 unsafe {
740 let va = O::Vector::load_simd(arch, a1.add(lanes * j));
741 let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
742 d1 = O::accum(d1, va, vb);
743 }
744 }
745
746 let a1_remaining = (o2 - o1) - full1 * lanes;
747 if a1_remaining != 0 {
748 let va =
750 unsafe { O::Vector::load_simd_first(arch, a1.add(lanes * full1), a1_remaining) };
751 let vb = unsafe { O::Vector::load_simd(arch, b1.add(lanes * full1)) };
753 d1 = O::accum(d1, va, vb);
754 }
755
756 i += 2;
757
758 p = unsafe { p.add(2 * chunk_stride) };
762 }
763
764 if i < nchunks {
765 debug_assert!(i + 1 == nchunks);
766
767 let (o0, o1) = unsafe { (*offsets.get_unchecked(i), *offsets.get_unchecked(i + 1)) };
769
770 let a0 = unsafe { aptr.add(o0) };
772
773 let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
775
776 let full = (o1 - o0) / lanes;
778 for j in 0..full {
779 unsafe {
782 let va = O::Vector::load_simd(arch, a0.add(lanes * j));
783 let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
784 d0 = O::accum(d0, va, vb);
785 }
786 }
787
788 let remaining = (o1 - o0) - full * lanes;
789 if remaining != 0 {
790 let va = unsafe { O::Vector::load_simd_first(arch, a0.add(lanes * full), remaining) };
793 let vb = unsafe { O::Vector::load_simd(arch, b0.add(lanes * full)) };
795 d0 = O::accum(d0, va, vb);
796 }
797 }
798
799 Ok(O::reduce_pair(d0, d1))
800}
801
802#[derive(Debug, Error)]
808#[non_exhaustive]
809pub enum SelfDistanceError {
810 #[error("table has {} chunks but codes slice has length {}", chunks, len)]
811 Len { chunks: usize, len: usize },
812 #[error("codes have a value that exceeds the number of pivots {}", ncenters)]
813 OutOfBounds { ncenters: usize },
814 #[error("An invalid SIMD width was chosen - ensure the correct vtable is used")]
815 InvalidSimd,
816}
817
818#[inline(always)]
819fn self_distance<O>(
820 arch: O::Arch,
821 table: &PaddedTable,
822 a: &[u8],
823 b: &[u8],
824) -> Result<f32, SelfDistanceError>
825where
826 O: Op,
827{
828 if !table
830 .simd_width
831 .as_nonzero()
832 .get()
833 .is_multiple_of(O::Vector::LANES)
834 {
835 return Err(SelfDistanceError::InvalidSimd);
836 }
837
838 let chunks = table.nchunks();
839 if a.len() != chunks {
841 return Err(SelfDistanceError::Len {
842 chunks,
843 len: a.len(),
844 });
845 }
846
847 if b.len() != chunks {
849 return Err(SelfDistanceError::Len {
850 chunks,
851 len: b.len(),
852 });
853 }
854
855 if let Ok(ncenters) = u8::try_from(table.ncenters()) {
857 if let Some(max) = a.iter().max()
858 && *max >= ncenters
859 {
860 return Err(SelfDistanceError::OutOfBounds {
861 ncenters: ncenters.into(),
862 });
863 }
864
865 if let Some(max) = b.iter().max()
866 && *max >= ncenters
867 {
868 return Err(SelfDistanceError::OutOfBounds {
869 ncenters: ncenters.into(),
870 });
871 }
872 }
873
874 let pivots = &table.pivots;
877
878 let steps = pivots.ncols() / O::Vector::LANES;
880
881 let pivot_stride = pivots.ncols();
882 let chunk_stride = table.pivots_per_chunk * pivot_stride;
883
884 let len = a.len();
885 let mut d0 = O::init(arch);
886 let mut d1 = O::init(arch);
887
888 let mut p = pivots.as_ptr();
889
890 let lanes = O::Vector::LANES;
891 let mut i = 0;
892
893 while i + 2 <= len {
912 let a0 = unsafe { p.add(pivot_stride * (*a.get_unchecked(i) as usize)) };
915
916 let a1 = unsafe { p.add(chunk_stride + pivot_stride * (*a.get_unchecked(i + 1) as usize)) };
919
920 let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
922 let b1 = unsafe { p.add(chunk_stride + pivot_stride * (*b.get_unchecked(i + 1) as usize)) };
924
925 for j in 0..steps {
926 unsafe {
929 let va = O::Vector::load_simd(arch, a0.add(lanes * j));
931 let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
932 d0 = O::accum(d0, va, vb);
933
934 let va = O::Vector::load_simd(arch, a1.add(lanes * j));
936 let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
937 d1 = O::accum(d1, va, vb);
938 }
939 }
940
941 i += 2;
942
943 p = unsafe { p.add(2 * chunk_stride) };
947 }
948
949 if i < len {
950 debug_assert!(i + 1 == len);
951
952 let a0 = unsafe { p.add(pivot_stride * (*a.get_unchecked(i) as usize)) };
955 let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
957
958 for j in 0..steps {
959 unsafe {
962 let va = O::Vector::load_simd(arch, a0.add(lanes * j));
963 let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
964 d0 = O::accum(d0, va, vb);
965 }
966 }
967 }
968
969 Ok(O::reduce_pair(d0, d1))
970}
971
972#[cfg(test)]
977mod tests {
978 use super::*;
979
980 use diskann_utils::assert_contains;
981
982 use crate::{
983 product::tables::test::{self as table_test, DistanceTestTable, QueryLike, SelfLike},
984 test_util::Check,
985 };
986
987 fn cases() -> &'static [Case] {
988 const CASES: &[Case] = &[
989 Case::new(&[1], 1, 0.0, Check::exact()),
991 Case::new(&[1, 1, 1], 2, -1.0, Check::exact()),
992 Case::new(&[3, 2, 2], 15, -7.0, Check::exact()),
993 Case::new(&[2, 2, 2, 2], 16, -8.0, Check::exact()),
994 Case::new(&[3, 3, 3, 2, 2], 17, -11.0, Check::exact()),
995 Case::new(&[8, 8, 8, 8], 32, -16.0, Check::exact()),
996 Case::new(&[6, 6, 5, 5, 5, 5, 5], 33, -20.0, Check::exact()),
997 Case::new(&[13, 12, 12], 33, -20.0, Check::exact()),
998 Case::new(&[4, 4, 3, 3, 3], 256, -128.0, Check::exact()),
999 Case::new(&[1, 23, 23], 17, -9.0, Check::exact()),
1002 Case::new(&[23, 1, 23], 17, -9.0, Check::exact()),
1003 Case::new(&[1, 17, 2, 9, 3], 17, -9.0, Check::exact()),
1004 ];
1005 CASES
1006 }
1007
1008 #[derive(Debug, Clone, Copy)]
1009 struct Case {
1010 chunk_dims: &'static [usize],
1011 pivots: usize,
1012 start: f32,
1013 check: Check,
1014 }
1015
1016 impl Case {
1017 const fn new(
1018 chunk_dims: &'static [usize],
1019 pivots: usize,
1020 start: f32,
1021 check: Check,
1022 ) -> Self {
1023 Self {
1024 chunk_dims,
1025 pivots,
1026 start,
1027 check,
1028 }
1029 }
1030 }
1031
1032 fn run_test(
1033 cases: &[Case],
1034 metric: Metric,
1035 arch: Option<RuntimeArch>,
1036 reference: &dyn Fn(&[f32], &[f32]) -> f32,
1037 ctx: &dyn std::fmt::Display,
1038 ) {
1039 let (num_queries, num_trials) = if cfg!(miri) {
1040 (1, 0)
1043 } else {
1044 (10, 10)
1045 };
1046
1047 #[derive(Debug)]
1048 struct Dut<'a> {
1049 table: &'a PaddedTable,
1050 query: Vec<f32>,
1051 vtable: VTable,
1052 }
1053
1054 impl QueryLike for Dut<'_> {
1055 fn preprocess(&mut self, query: &[f32]) {
1056 self.query.clear();
1057 self.query.extend_from_slice(query);
1058 }
1059
1060 fn evaluate(&mut self, code: &[u8]) -> f32 {
1061 self.vtable.distance(self.table, &self.query, code).unwrap()
1062 }
1063 }
1064
1065 impl SelfLike for Dut<'_> {
1066 fn evaluate(&mut self, a: &[u8], b: &[u8]) -> f32 {
1067 self.vtable.self_distance(self.table, a, b).unwrap()
1068 }
1069 }
1070
1071 for Case {
1072 chunk_dims,
1073 pivots,
1074 start,
1075 check,
1076 } in cases.iter().copied()
1077 {
1078 let driver = DistanceTestTable::from_chunk_dims(chunk_dims, pivots, start);
1079 let dim = driver.dim();
1080 let chunks = driver.chunks();
1081 let basic = driver.basic_table();
1082
1083 let table = match arch {
1084 Some(arch) => PaddedTable::from_basic_with(basic.as_view(), arch),
1085 None => PaddedTable::from_basic(basic.as_view()),
1086 };
1087
1088 let vtable = table.vtable(metric);
1089
1090 let mut dut = Dut {
1091 table: &table,
1092 query: Vec::new(),
1093 vtable,
1094 };
1095
1096 driver.drive_query_like(
1097 num_queries,
1098 num_trials,
1099 &mut driver.rng(0xc0ffee),
1100 check,
1101 reference,
1102 &mut dut,
1103 format_args!(
1104 "[{}] padded table - dim = {}, chunks = {}, pivots = {}",
1105 ctx, dim, chunks, pivots
1106 ),
1107 );
1108
1109 driver.drive_self_like(
1110 num_trials,
1111 &mut driver.rng(0xc0ffee),
1112 check,
1113 reference,
1114 &mut dut,
1115 format_args!(
1116 "[{}] padded table - dim = {}, chunks = {}, pivots = {}",
1117 ctx, dim, chunks, pivots
1118 ),
1119 );
1120 }
1121 }
1122
1123 #[test]
1125 fn test_l2_query_like() {
1126 run_test(
1127 cases(),
1128 Metric::SquaredL2,
1129 None,
1130 &table_test::squared_l2,
1131 &"squared l2 full x quant - auto-detect",
1132 );
1133
1134 run_test(
1135 cases(),
1136 Metric::SquaredL2,
1137 Some(RuntimeArch::Scalar(Scalar::new())),
1138 &table_test::squared_l2,
1139 &"squared l2 full x quant - scalar",
1140 );
1141
1142 #[cfg(target_arch = "x86_64")]
1143 if let Some(arch) = V3::new_checked() {
1144 run_test(
1145 cases(),
1146 Metric::SquaredL2,
1147 Some(RuntimeArch::V3(arch)),
1148 &table_test::squared_l2,
1149 &"squared l2 full x quant - V3",
1150 );
1151 }
1152
1153 #[cfg(target_arch = "aarch64")]
1154 if let Some(arch) = Neon::new_checked() {
1155 run_test(
1156 cases(),
1157 Metric::SquaredL2,
1158 Some(RuntimeArch::Neon(arch)),
1159 &table_test::squared_l2,
1160 &"squared l2 full x quant - V3",
1161 );
1162 }
1163 }
1164
1165 #[test]
1167 fn test_inner_product_query_like() {
1168 run_test(
1169 cases(),
1170 Metric::InnerProduct,
1171 None,
1172 &table_test::inner_product,
1173 &"inner-product full x quant - auto-detect",
1174 );
1175
1176 run_test(
1177 cases(),
1178 Metric::InnerProduct,
1179 Some(RuntimeArch::Scalar(Scalar::new())),
1180 &table_test::inner_product,
1181 &"inner-product full x quant - scalar",
1182 );
1183
1184 #[cfg(target_arch = "x86_64")]
1185 if let Some(arch) = V3::new_checked() {
1186 run_test(
1187 cases(),
1188 Metric::InnerProduct,
1189 Some(RuntimeArch::V3(arch)),
1190 &table_test::inner_product,
1191 &"inner-product full x quant - V3",
1192 );
1193 }
1194
1195 #[cfg(target_arch = "aarch64")]
1196 if let Some(arch) = Neon::new_checked() {
1197 run_test(
1198 cases(),
1199 Metric::InnerProduct,
1200 Some(RuntimeArch::Neon(arch)),
1201 &table_test::inner_product,
1202 &"inner-product full x quant - V3",
1203 );
1204 }
1205 }
1206
1207 #[test]
1209 fn test_cosine_query_like() {
1210 run_test(
1211 cases(),
1212 Metric::Cosine,
1213 None,
1214 &table_test::cosine,
1215 &"cosine full x quant - auto-detect",
1216 );
1217
1218 run_test(
1219 cases(),
1220 Metric::Cosine,
1221 Some(RuntimeArch::Scalar(Scalar::new())),
1222 &table_test::cosine,
1223 &"cosine full x quant - scalar",
1224 );
1225
1226 #[cfg(target_arch = "x86_64")]
1227 if let Some(arch) = V3::new_checked() {
1228 run_test(
1229 cases(),
1230 Metric::Cosine,
1231 Some(RuntimeArch::V3(arch)),
1232 &table_test::cosine,
1233 &"cosine full x quant - V3",
1234 );
1235 }
1236
1237 #[cfg(target_arch = "aarch64")]
1238 if let Some(arch) = Neon::new_checked() {
1239 run_test(
1240 cases(),
1241 Metric::Cosine,
1242 Some(RuntimeArch::Neon(arch)),
1243 &table_test::cosine,
1244 &"cosine full x quant - V3",
1245 );
1246 }
1247 }
1248
1249 fn error_table() -> (PaddedTable, VTable) {
1255 let driver = DistanceTestTable::new(7, 3, 3, 0.0);
1256 let basic = driver.basic_table();
1257 let table =
1258 PaddedTable::from_basic_with(basic.as_view(), RuntimeArch::Scalar(Scalar::new()));
1259 let vtable = table.vtable(Metric::SquaredL2);
1260 (table, vtable)
1261 }
1262
1263 #[test]
1264 fn test_distance_errors() {
1265 let (table, vtable) = error_table();
1266 let vector = vec![0.0; table.dim()];
1267 let codes = vec![0; table.nchunks()];
1268
1269 for len in [table.dim() - 1, table.dim() + 1] {
1270 let invalid = vec![0.0; len];
1271 let err = vtable.distance(&table, &invalid, &codes).unwrap_err();
1272 assert_contains!(
1273 err.to_string(),
1274 format!(
1275 "table has dimension {} but full vector has length {len}",
1276 table.dim()
1277 )
1278 );
1279 }
1280
1281 for len in [table.nchunks() - 1, table.nchunks() + 1] {
1282 let invalid = vec![0; len];
1283 let err = vtable.distance(&table, &vector, &invalid).unwrap_err();
1284 assert_contains!(
1285 err.to_string(),
1286 format!(
1287 "table has {} chunks but codes slice has length {len}",
1288 table.nchunks()
1289 )
1290 );
1291 }
1292
1293 let mut invalid = codes;
1294 invalid[1] = u8::try_from(table.ncenters()).unwrap();
1295 let err = vtable.distance(&table, &vector, &invalid).unwrap_err();
1296 assert_contains!(
1297 err.to_string(),
1298 format!(
1299 "codes have a value that exceeds the number of pivots {}",
1300 table.ncenters()
1301 )
1302 );
1303 }
1304
1305 #[test]
1306 fn test_self_distance_errors() {
1307 let (table, vtable) = error_table();
1308 let codes = vec![0; table.nchunks()];
1309
1310 for len in [table.nchunks() - 1, table.nchunks() + 1] {
1311 let invalid = vec![0; len];
1312
1313 let err = vtable.self_distance(&table, &invalid, &codes).unwrap_err();
1314 assert_contains!(
1315 err.to_string(),
1316 format!(
1317 "table has {} chunks but codes slice has length {len}",
1318 table.nchunks()
1319 )
1320 );
1321
1322 let err = vtable.self_distance(&table, &codes, &invalid).unwrap_err();
1323 assert_contains!(
1324 err.to_string(),
1325 format!(
1326 "table has {} chunks but codes slice has length {len}",
1327 table.nchunks()
1328 )
1329 );
1330 }
1331
1332 for invalid_operand in 0..2 {
1333 let mut a = codes.clone();
1334 let mut b = codes.clone();
1335 let invalid = if invalid_operand == 0 { &mut a } else { &mut b };
1336 invalid[1] = u8::try_from(table.ncenters()).unwrap();
1337
1338 let err = vtable.self_distance(&table, &a, &b).unwrap_err();
1339 assert_contains!(
1340 err.to_string(),
1341 format!(
1342 "codes have a value that exceeds the number of pivots {}",
1343 table.ncenters()
1344 )
1345 );
1346 }
1347 }
1348
1349 #[test]
1350 fn test_mismatched_simd_width_errors() {
1351 diskann_wide::alias!(f32x8<A> = f32x8);
1352
1353 let (table, _) = error_table();
1354 let invalid_vtable = VTable::new::<SquaredL2<f32x8<Scalar>>>(Scalar::new());
1355 let vector = vec![0.0; table.dim()];
1356 let codes = vec![0; table.nchunks()];
1357
1358 let err = invalid_vtable
1359 .distance(&table, &vector, &codes)
1360 .unwrap_err();
1361 assert_contains!(err.to_string(), "An invalid SIMD width was chosen");
1362
1363 let err = invalid_vtable
1364 .self_distance(&table, &codes, &codes)
1365 .unwrap_err();
1366 assert_contains!(err.to_string(), "An invalid SIMD width was chosen");
1367 }
1368}