1use std::{fmt::Debug, marker::PhantomData};
137
138use diskann_utils::{Reborrow, ReborrowMut};
139use diskann_vector::{DistanceFunction, PreprocessedDistanceFunction};
140use diskann_wide::{
141 Architecture,
142 arch::{Scalar, Target1, Target2},
143};
144#[cfg(feature = "flatbuffers")]
145use flatbuffers::FlatBufferBuilder;
146use thiserror::Error;
147
148#[cfg(target_arch = "x86_64")]
149use diskann_wide::arch::x86_64::{V3, V4};
150
151#[cfg(target_arch = "aarch64")]
152use diskann_wide::arch::aarch64::Neon;
153
154use super::{
155 CompensatedCosine, CompensatedIP, CompensatedSquaredL2, Data, DataMut, DataRef, FullQuery,
156 FullQueryMut, FullQueryRef, Query, QueryMut, QueryRef, SphericalQuantizer, SupportedMetric,
157 quantizer,
158};
159use crate::{
160 AsFunctor, CompressIntoWith,
161 alloc::{
162 Allocator, AllocatorCore, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone,
163 },
164 bits::{self, Representation, Unsigned},
165 distances::{self, UnequalLengths},
166 error::InlineError,
167 meta,
168 num::PowerOfTwo,
169 poly,
170};
171#[cfg(feature = "flatbuffers")]
172use crate::{alloc::CompoundError, flatbuffers as fb};
173
174type Rf32 = distances::Result<f32>;
176
177#[derive(Debug, Clone)]
183pub struct QueryBufferDescription {
184 size: usize,
185 align: PowerOfTwo,
186}
187
188impl QueryBufferDescription {
189 pub fn new(size: usize, align: PowerOfTwo) -> Self {
191 Self { size, align }
192 }
193
194 pub fn bytes(&self) -> usize {
196 self.size
197 }
198
199 pub fn align(&self) -> PowerOfTwo {
201 self.align
202 }
203}
204
205pub trait Quantizer<A = GlobalAllocator>: Send + Sync + Debug
227where
228 A: Allocator + std::panic::UnwindSafe + Send + Sync + 'static,
229{
230 fn nbits(&self) -> usize;
232
233 fn bytes(&self) -> usize;
235
236 fn dim(&self) -> usize;
238
239 fn full_dim(&self) -> usize;
241
242 fn distance_computer(&self, allocator: A) -> Result<DistanceComputer<A>, AllocatorError>;
250
251 fn distance_computer_ref(&self) -> &dyn DynDistanceComputer;
256
257 fn query_computer(
267 &self,
268 layout: QueryLayout,
269 allocator: A,
270 ) -> Result<DistanceComputer<A>, DistanceComputerError>;
271
272 fn query_buffer_description(
277 &self,
278 layout: QueryLayout,
279 ) -> Result<QueryBufferDescription, UnsupportedQueryLayout>;
280
281 fn compress_query(
288 &self,
289 x: &[f32],
290 layout: QueryLayout,
291 allow_rescale: bool,
292 buffer: OpaqueMut<'_>,
293 scratch: ScopedAllocator<'_>,
294 ) -> Result<(), QueryCompressionError>;
295
296 fn fused_query_computer(
303 &self,
304 x: &[f32],
305 layout: QueryLayout,
306 allow_rescale: bool,
307 allocator: A,
308 scratch: ScopedAllocator<'_>,
309 ) -> Result<QueryComputer<A>, QueryComputerError>;
310
311 fn is_supported(&self, layout: QueryLayout) -> bool;
313
314 fn compress(
321 &self,
322 x: &[f32],
323 into: OpaqueMut<'_>,
324 scratch: ScopedAllocator<'_>,
325 ) -> Result<(), CompressionError>;
326
327 fn metric(&self) -> SupportedMetric;
329
330 fn try_clone_into(&self, allocator: A) -> Result<Poly<dyn Quantizer<A>, A>, AllocatorError>;
332
333 crate::utils::features! {
334 #![feature = "flatbuffers"]
335 fn serialize(&self, allocator: A) -> Result<Poly<[u8], A>, AllocatorError>;
338 }
339}
340
341#[derive(Debug, Error)]
342#[error("Layout {layout} is not supported for {desc}")]
343pub struct UnsupportedQueryLayout {
344 layout: QueryLayout,
345 desc: &'static str,
346}
347
348impl UnsupportedQueryLayout {
349 fn new(layout: QueryLayout, desc: &'static str) -> Self {
350 Self { layout, desc }
351 }
352}
353
354#[derive(Debug, Error)]
355#[non_exhaustive]
356pub enum DistanceComputerError {
357 #[error(transparent)]
358 UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
359 #[error(transparent)]
360 AllocatorError(#[from] AllocatorError),
361}
362
363#[derive(Debug, Error)]
364#[non_exhaustive]
365pub enum QueryCompressionError {
366 #[error(transparent)]
367 UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
368 #[error(transparent)]
369 CompressionError(#[from] CompressionError),
370 #[error(transparent)]
371 NotCanonical(#[from] NotCanonical),
372 #[error(transparent)]
373 AllocatorError(#[from] AllocatorError),
374}
375
376#[derive(Debug, Error)]
377#[non_exhaustive]
378pub enum QueryComputerError {
379 #[error(transparent)]
380 UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
381 #[error(transparent)]
382 CompressionError(#[from] CompressionError),
383 #[error(transparent)]
384 AllocatorError(#[from] AllocatorError),
385}
386
387#[derive(Debug, Error)]
389#[error("Error occured during query compression")]
390pub enum CompressionError {
391 NotCanonical(#[source] InlineError<16>),
393
394 CompressionError(#[source] quantizer::CompressionError),
398}
399
400impl CompressionError {
401 fn not_canonical<E>(error: E) -> Self
402 where
403 E: std::error::Error + Send + Sync + 'static,
404 {
405 Self::NotCanonical(InlineError::new(error))
406 }
407}
408
409#[derive(Debug, Error)]
410#[error("An opaque argument did not have the required alignment or length")]
411pub struct NotCanonical {
412 source: Box<dyn std::error::Error + Send + Sync>,
413}
414
415impl NotCanonical {
416 fn new<E>(err: E) -> Self
417 where
418 E: std::error::Error + Send + Sync + 'static,
419 {
420 Self {
421 source: Box::new(err),
422 }
423 }
424}
425
426#[derive(Debug, Clone, Copy)]
433#[repr(transparent)]
434pub struct Opaque<'a>(&'a [u8]);
435
436impl<'a> Opaque<'a> {
437 pub fn new(slice: &'a [u8]) -> Self {
439 Self(slice)
440 }
441
442 pub fn into_inner(self) -> &'a [u8] {
444 self.0
445 }
446}
447
448impl std::ops::Deref for Opaque<'_> {
449 type Target = [u8];
450 fn deref(&self) -> &[u8] {
451 self.0
452 }
453}
454impl<'short> Reborrow<'short> for Opaque<'_> {
455 type Target = Opaque<'short>;
456 fn reborrow(&'short self) -> Self::Target {
457 *self
458 }
459}
460
461#[derive(Debug)]
464#[repr(transparent)]
465pub struct OpaqueMut<'a>(&'a mut [u8]);
466
467impl<'a> OpaqueMut<'a> {
468 pub fn new(slice: &'a mut [u8]) -> Self {
470 Self(slice)
471 }
472
473 pub fn inspect(&mut self) -> &mut [u8] {
475 self.0
476 }
477}
478
479impl std::ops::Deref for OpaqueMut<'_> {
480 type Target = [u8];
481 fn deref(&self) -> &[u8] {
482 self.0
483 }
484}
485
486impl std::ops::DerefMut for OpaqueMut<'_> {
487 fn deref_mut(&mut self) -> &mut [u8] {
488 self.0
489 }
490}
491
492#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub enum QueryLayout {
499 SameAsData,
503
504 FourBitTransposed,
506
507 ScalarQuantized,
510
511 FullPrecision,
513}
514
515impl QueryLayout {
516 #[cfg(test)]
517 fn all() -> [Self; 4] {
518 [
519 Self::SameAsData,
520 Self::FourBitTransposed,
521 Self::ScalarQuantized,
522 Self::FullPrecision,
523 ]
524 }
525}
526
527impl std::fmt::Display for QueryLayout {
528 fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529 <Self as Debug>::fmt(self, fmt)
530 }
531}
532
533trait ReportQueryLayout {
543 fn report_query_layout(&self) -> QueryLayout;
544}
545
546impl<T, M, L, R> ReportQueryLayout for Reify<T, M, L, R>
547where
548 T: ReportQueryLayout,
549{
550 fn report_query_layout(&self) -> QueryLayout {
551 self.inner.report_query_layout()
552 }
553}
554
555impl<D, Q> ReportQueryLayout for Curried<D, Q>
556where
557 Q: ReportQueryLayout,
558{
559 fn report_query_layout(&self) -> QueryLayout {
560 self.query.report_query_layout()
561 }
562}
563
564impl<const NBITS: usize, A> ReportQueryLayout for Data<NBITS, A>
565where
566 Unsigned: Representation<NBITS>,
567 A: AllocatorCore,
568{
569 fn report_query_layout(&self) -> QueryLayout {
570 QueryLayout::SameAsData
571 }
572}
573
574impl<const NBITS: usize, A> ReportQueryLayout for Query<NBITS, bits::Dense, A>
575where
576 Unsigned: Representation<NBITS>,
577 A: AllocatorCore,
578{
579 fn report_query_layout(&self) -> QueryLayout {
580 QueryLayout::ScalarQuantized
581 }
582}
583
584impl<A> ReportQueryLayout for Query<4, bits::BitTranspose, A>
585where
586 A: AllocatorCore,
587{
588 fn report_query_layout(&self) -> QueryLayout {
589 QueryLayout::FourBitTransposed
590 }
591}
592
593impl<A> ReportQueryLayout for FullQuery<A>
594where
595 A: AllocatorCore,
596{
597 fn report_query_layout(&self) -> QueryLayout {
598 QueryLayout::FullPrecision
599 }
600}
601
602trait FromOpaque: 'static + Send + Sync + Debug {
611 type Target<'a>;
612 type Error: std::error::Error + Send + Sync + 'static;
613
614 fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error>;
615}
616
617#[derive(Debug, Default)]
619pub(super) struct AsFull;
620
621#[derive(Debug, Default)]
623pub(super) struct AsData<const NBITS: usize>;
624
625#[derive(Debug)]
627pub(super) struct AsQuery<const NBITS: usize, Perm = bits::Dense> {
628 _marker: PhantomData<Perm>,
629}
630
631impl<const NBITS: usize, Perm> Default for AsQuery<NBITS, Perm> {
633 fn default() -> Self {
634 Self {
635 _marker: PhantomData,
636 }
637 }
638}
639
640impl FromOpaque for AsFull {
641 type Target<'a> = FullQueryRef<'a>;
642 type Error = meta::slice::NotCanonical;
643
644 fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
645 Self::Target::from_canonical(query.into_inner(), dim)
646 }
647}
648
649impl ReportQueryLayout for AsFull {
650 fn report_query_layout(&self) -> QueryLayout {
651 QueryLayout::FullPrecision
652 }
653}
654
655impl<const NBITS: usize> FromOpaque for AsData<NBITS>
656where
657 Unsigned: Representation<NBITS>,
658{
659 type Target<'a> = DataRef<'a, NBITS>;
660 type Error = meta::NotCanonical;
661
662 fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
663 Self::Target::from_canonical_back(query.into_inner(), dim)
664 }
665}
666
667impl<const NBITS: usize> ReportQueryLayout for AsData<NBITS> {
668 fn report_query_layout(&self) -> QueryLayout {
669 QueryLayout::SameAsData
670 }
671}
672
673impl<const NBITS: usize, Perm> FromOpaque for AsQuery<NBITS, Perm>
674where
675 Unsigned: Representation<NBITS>,
676 Perm: bits::PermutationStrategy<NBITS> + Debug + Send + Sync + 'static,
677{
678 type Target<'a> = QueryRef<'a, NBITS, Perm>;
679 type Error = meta::NotCanonical;
680
681 fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
682 Self::Target::from_canonical_back(query.into_inner(), dim)
683 }
684}
685
686impl<const NBITS: usize> ReportQueryLayout for AsQuery<NBITS, bits::Dense> {
687 fn report_query_layout(&self) -> QueryLayout {
688 QueryLayout::ScalarQuantized
689 }
690}
691
692impl<const NBITS: usize> ReportQueryLayout for AsQuery<NBITS, bits::BitTranspose> {
693 fn report_query_layout(&self) -> QueryLayout {
694 QueryLayout::FourBitTransposed
695 }
696}
697
698#[derive(Debug)]
704pub(super) struct Reify<T, M, L, R> {
705 inner: T,
706 dim: usize,
707 arch: M,
708 _markers: PhantomData<(L, R)>,
709}
710
711impl<T, M, L, R> Reify<T, M, L, R> {
712 pub(super) fn new(inner: T, dim: usize, arch: M) -> Self {
713 Self {
714 inner,
715 dim,
716 arch,
717 _markers: PhantomData,
718 }
719 }
720}
721
722impl<M, T, R> DynQueryComputer for Reify<T, M, (), R>
723where
724 M: Architecture,
725 R: FromOpaque,
726 T: ReportQueryLayout + Debug + Send + Sync,
727 for<'a> &'a T: Target1<M, Rf32, R::Target<'a>>,
728{
729 fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
730 self.arch.run2(
731 |this: &Self, x| {
732 let x = R::from_opaque(x, this.dim)
733 .map_err(|err| QueryDistanceError::XReify(InlineError::new(err)))?;
734 this.arch
735 .run1(&this.inner, x)
736 .map_err(QueryDistanceError::UnequalLengths)
737 },
738 self,
739 x,
740 )
741 }
742
743 fn layout(&self) -> QueryLayout {
744 self.inner.report_query_layout()
745 }
746}
747
748impl<T, M, Q, R> DynDistanceComputer for Reify<T, M, Q, R>
749where
750 M: Architecture,
751 Q: FromOpaque + Default + ReportQueryLayout,
752 R: FromOpaque,
753 T: for<'a> Target2<M, Rf32, Q::Target<'a>, R::Target<'a>> + Copy + Debug + Send + Sync,
754{
755 fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
756 self.arch.run3(
757 |this: &Self, query, x| {
758 let query = Q::from_opaque(query, this.dim)
759 .map_err(|err| DistanceError::QueryReify(InlineError::<24>::new(err)))?;
760
761 let x = R::from_opaque(x, this.dim)
762 .map_err(|err| DistanceError::XReify(InlineError::<16>::new(err)))?;
763
764 this.arch
765 .run2_inline(this.inner, query, x)
766 .map_err(DistanceError::UnequalLengths)
767 },
768 self,
769 query,
770 x,
771 )
772 }
773
774 fn layout(&self) -> QueryLayout {
775 Q::default().report_query_layout()
776 }
777}
778
779#[derive(Debug, Error)]
785pub enum QueryDistanceError {
786 #[error("trouble trying to reify the argument")]
788 XReify(#[source] InlineError<16>),
789
790 #[error("encountered while trying to compute distances")]
792 UnequalLengths(#[source] UnequalLengths),
793}
794
795pub trait DynQueryComputer: Send + Sync + Debug {
796 fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError>;
797 fn layout(&self) -> QueryLayout;
798}
799
800#[derive(Debug)]
809pub struct QueryComputer<A = GlobalAllocator>
810where
811 A: AllocatorCore,
812{
813 inner: Poly<dyn DynQueryComputer, A>,
814}
815
816impl<A> QueryComputer<A>
817where
818 A: AllocatorCore,
819{
820 fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
821 where
822 T: DynQueryComputer + 'static,
823 {
824 let inner = Poly::new(inner, allocator)?;
825 Ok(Self {
826 inner: poly!(DynQueryComputer, inner),
827 })
828 }
829
830 pub fn layout(&self) -> QueryLayout {
832 self.inner.layout()
833 }
834
835 pub fn into_inner(self) -> Poly<dyn DynQueryComputer, A> {
837 self.inner
838 }
839}
840
841impl<A> PreprocessedDistanceFunction<Opaque<'_>, Result<f32, QueryDistanceError>>
842 for QueryComputer<A>
843where
844 A: AllocatorCore,
845{
846 fn evaluate_similarity(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
847 self.inner.evaluate(x)
848 }
849}
850
851#[derive(Debug)]
858pub(super) struct Curried<D, Q> {
859 inner: D,
860 query: Q,
861}
862
863impl<D, Q> Curried<D, Q> {
864 pub(super) fn new(inner: D, query: Q) -> Self {
865 Self { inner, query }
866 }
867}
868
869impl<A, D, Q, T, R> Target1<A, R, T> for &Curried<D, Q>
870where
871 A: Architecture,
872 Q: for<'a> Reborrow<'a>,
873 D: for<'a> Target2<A, R, <Q as Reborrow<'a>>::Target, T> + Copy,
874{
875 fn run(self, arch: A, x: T) -> R {
876 self.inner.run(arch, self.query.reborrow(), x)
877 }
878}
879
880#[derive(Debug, Error)]
886pub enum DistanceError {
887 #[error("trouble trying to reify the left-hand argument")]
889 QueryReify(InlineError<24>),
890
891 #[error("trouble trying to reify the right-hand argument")]
893 XReify(InlineError<16>),
894
895 #[error("encountered while trying to compute distances")]
899 UnequalLengths(UnequalLengths),
900}
901
902pub trait DynDistanceComputer: Send + Sync + Debug {
903 fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError>;
904 fn layout(&self) -> QueryLayout;
905}
906
907#[derive(Debug)]
918pub struct DistanceComputer<A = GlobalAllocator>
919where
920 A: AllocatorCore,
921{
922 inner: Poly<dyn DynDistanceComputer, A>,
923}
924
925impl<A> DistanceComputer<A>
926where
927 A: AllocatorCore,
928{
929 pub(super) fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
930 where
931 T: DynDistanceComputer + 'static,
932 {
933 let inner = Poly::new(inner, allocator)?;
934 Ok(Self {
935 inner: poly!(DynDistanceComputer, inner),
936 })
937 }
938
939 pub fn layout(&self) -> QueryLayout {
941 self.inner.layout()
942 }
943
944 pub fn into_inner(self) -> Poly<dyn DynDistanceComputer, A> {
945 self.inner
946 }
947}
948
949impl<A> DistanceFunction<Opaque<'_>, Opaque<'_>, Result<f32, DistanceError>> for DistanceComputer<A>
950where
951 A: AllocatorCore,
952{
953 fn evaluate_similarity(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
954 self.inner.evaluate(query, x)
955 }
956}
957
958#[cfg(all(not(test), feature = "flatbuffers"))]
964const DEFAULT_SERIALIZED_BYTES: usize = 1024;
965
966#[cfg(all(test, feature = "flatbuffers"))]
968const DEFAULT_SERIALIZED_BYTES: usize = 1;
969
970#[derive(Debug)]
973pub struct Impl<const NBITS: usize, A = GlobalAllocator>
974where
975 A: Allocator,
976{
977 quantizer: SphericalQuantizer<A>,
978 distance: Poly<dyn DynDistanceComputer, A>,
979}
980
981pub trait Constructible<A = GlobalAllocator>
984where
985 A: Allocator,
986{
987 fn dispatch_distance(
988 quantizer: &SphericalQuantizer<A>,
989 ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError>;
990}
991
992impl<const NBITS: usize, A: Allocator> Constructible<A> for Impl<NBITS, A>
993where
994 A: Allocator,
995 AsData<NBITS>: FromOpaque,
996 SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
997{
998 fn dispatch_distance(
999 quantizer: &SphericalQuantizer<A>,
1000 ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError> {
1001 diskann_wide::arch::dispatch2_no_features(
1002 ComputerDispatcher::<AsData<NBITS>, NBITS>::new(),
1003 quantizer,
1004 quantizer.allocator().clone(),
1005 )
1006 .map(|obj| obj.inner)
1007 }
1008}
1009
1010impl<const NBITS: usize, A> TryClone for Impl<NBITS, A>
1011where
1012 A: Allocator,
1013 AsData<NBITS>: FromOpaque,
1014 SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
1015{
1016 fn try_clone(&self) -> Result<Self, AllocatorError> {
1017 Self::new(self.quantizer.try_clone()?)
1018 }
1019}
1020
1021impl<const NBITS: usize, A: Allocator> Impl<NBITS, A> {
1022 pub fn new(quantizer: SphericalQuantizer<A>) -> Result<Self, AllocatorError>
1024 where
1025 Self: Constructible<A>,
1026 {
1027 let distance = Self::dispatch_distance(&quantizer)?;
1028 Ok(Self {
1029 quantizer,
1030 distance,
1031 })
1032 }
1033
1034 pub fn quantizer(&self) -> &SphericalQuantizer<A> {
1036 &self.quantizer
1037 }
1038
1039 pub fn supports(layout: QueryLayout) -> bool {
1043 if const { NBITS == 1 } {
1044 [
1045 QueryLayout::SameAsData,
1046 QueryLayout::FourBitTransposed,
1047 QueryLayout::FullPrecision,
1048 ]
1049 .contains(&layout)
1050 } else {
1051 [
1052 QueryLayout::SameAsData,
1053 QueryLayout::ScalarQuantized,
1054 QueryLayout::FullPrecision,
1055 ]
1056 .contains(&layout)
1057 }
1058 }
1059
1060 fn query_computer<Q, B>(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError>
1063 where
1064 Q: FromOpaque,
1065 B: AllocatorCore,
1066 SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1067 {
1068 diskann_wide::arch::dispatch2_no_features(
1069 ComputerDispatcher::<Q, NBITS>::new(),
1070 &self.quantizer,
1071 allocator,
1072 )
1073 }
1074
1075 fn compress_query<'a, T>(
1076 &self,
1077 query: &'a [f32],
1078 storage: T,
1079 scratch: ScopedAllocator<'a>,
1080 ) -> Result<(), QueryCompressionError>
1081 where
1082 SphericalQuantizer<A>: CompressIntoWith<&'a [f32], T, ScopedAllocator<'a>, Error = quantizer::CompressionError>,
1083 {
1084 self.quantizer
1085 .compress_into_with(query, storage, scratch)
1086 .map_err(|err| CompressionError::CompressionError(err).into())
1087 }
1088
1089 fn fused_query_computer<Q, T, B>(
1092 &self,
1093 query: &[f32],
1094 mut storage: T,
1095 allocator: B,
1096 scratch: ScopedAllocator<'_>,
1097 ) -> Result<QueryComputer<B>, QueryComputerError>
1098 where
1099 Q: FromOpaque,
1100 T: for<'a> ReborrowMut<'a>
1101 + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1102 + ReportQueryLayout
1103 + Debug
1104 + Send
1105 + Sync
1106 + 'static,
1107 B: AllocatorCore,
1108 SphericalQuantizer<A>: for<'a> CompressIntoWith<
1109 &'a [f32],
1110 <T as ReborrowMut<'a>>::Target,
1111 ScopedAllocator<'a>,
1112 Error = quantizer::CompressionError,
1113 >,
1114 SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1115 {
1116 if let Err(err) = self
1117 .quantizer
1118 .compress_into_with(query, storage.reborrow_mut(), scratch)
1119 {
1120 return Err(CompressionError::CompressionError(err).into());
1121 }
1122
1123 diskann_wide::arch::dispatch3_no_features(
1124 ComputerDispatcher::<Q, NBITS>::new(),
1125 &self.quantizer,
1126 storage,
1127 allocator,
1128 )
1129 .map_err(|e| e.into())
1130 }
1131
1132 #[cfg(feature = "flatbuffers")]
1134 #[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
1135 pub fn try_deserialize(data: &[u8], alloc: A) -> Result<Self, DeserializationError>
1136 where
1137 Self: Constructible<A>,
1138 {
1139 if !fb::spherical::quantizer_buffer_has_identifier(data) {
1141 return Err(DeserializationError::InvalidIdentifier);
1142 }
1143
1144 let root = fb::spherical::root_as_quantizer(data)?;
1148 let nbits = root.nbits();
1149 let proto = root.quantizer();
1150
1151 if nbits as usize == NBITS {
1152 Self::try_deserialize_from(proto, alloc)
1153 } else {
1154 Err(DeserializationError::UnsupportedBitWidth(nbits))
1155 }
1156 }
1157
1158 #[cfg(feature = "flatbuffers")]
1159 fn try_deserialize_from(
1160 proto: fb::spherical::SphericalQuantizer<'_>,
1161 alloc: A,
1162 ) -> Result<Self, DeserializationError>
1163 where
1164 Self: Constructible<A>,
1165 {
1166 let quantizer = SphericalQuantizer::try_unpack(alloc, proto)?;
1167 Ok(Self::new(quantizer)?)
1168 }
1169
1170 #[cfg(feature = "flatbuffers")]
1171 fn serialize<B>(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError>
1172 where
1173 B: Allocator + std::panic::UnwindSafe,
1174 A: std::panic::RefUnwindSafe,
1175 {
1176 let mut buf = FlatBufferBuilder::new_in(Poly::broadcast(
1177 0u8,
1178 DEFAULT_SERIALIZED_BYTES,
1179 allocator.clone(),
1180 )?);
1181
1182 let quantizer = &self.quantizer;
1183
1184 let (root, mut buf) = match std::panic::catch_unwind(move || {
1185 let offset = quantizer.pack(&mut buf);
1186
1187 let root = fb::spherical::Quantizer::create(
1188 &mut buf,
1189 &fb::spherical::QuantizerArgs {
1190 quantizer: Some(offset),
1191 nbits: NBITS as u32,
1192 },
1193 );
1194 (root, buf)
1195 }) {
1196 Ok(ret) => ret,
1197 Err(err) => match err.downcast_ref::<String>() {
1198 Some(msg) => {
1199 if msg.contains("AllocatorError") {
1200 return Err(AllocatorError);
1201 } else {
1202 std::panic::resume_unwind(err);
1203 }
1204 }
1205 None => std::panic::resume_unwind(err),
1206 },
1207 };
1208
1209 fb::spherical::finish_quantizer_buffer(&mut buf, root);
1211 Poly::from_iter(buf.finished_data().iter().copied(), allocator)
1212 }
1213}
1214
1215trait BuildComputer<M, Q, const N: usize>
1232where
1233 M: Architecture,
1234 Q: FromOpaque,
1235{
1236 fn build_computer<A>(
1241 &self,
1242 arch: M,
1243 allocator: A,
1244 ) -> Result<DistanceComputer<A>, AllocatorError>
1245 where
1246 A: AllocatorCore;
1247
1248 fn build_fused_computer<R, A>(
1253 &self,
1254 arch: M,
1255 query: R,
1256 allocator: A,
1257 ) -> Result<QueryComputer<A>, AllocatorError>
1258 where
1259 R: ReportQueryLayout
1260 + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1261 + Debug
1262 + Send
1263 + Sync
1264 + 'static,
1265 A: AllocatorCore;
1266}
1267
1268fn identity<T>(x: T) -> T {
1269 x
1270}
1271
1272macro_rules! dispatch_map {
1273 ($N:literal, $Q:ty, $arch:ty) => {
1274 dispatch_map!($N, $Q, $arch, identity);
1275 };
1276 ($N:literal, $Q:ty, $arch:ty, $op:ident) => {
1277 impl<A> BuildComputer<$arch, $Q, $N> for SphericalQuantizer<A>
1278 where
1279 A: Allocator,
1280 {
1281 fn build_computer<B>(
1282 &self,
1283 input_arch: $arch,
1284 allocator: B,
1285 ) -> Result<DistanceComputer<B>, AllocatorError>
1286 where
1287 B: AllocatorCore,
1288 {
1289 type D = AsData<$N>;
1290
1291 let arch = ($op)(input_arch);
1293 let dim = self.output_dim();
1294 match self.metric() {
1295 SupportedMetric::SquaredL2 => {
1296 let reify = Reify::<CompensatedSquaredL2, _, $Q, D>::new(
1297 self.as_functor(),
1298 dim,
1299 arch,
1300 );
1301 DistanceComputer::new(reify, allocator)
1302 }
1303 SupportedMetric::InnerProduct => {
1304 let reify =
1305 Reify::<CompensatedIP, _, $Q, D>::new(self.as_functor(), dim, arch);
1306 DistanceComputer::new(reify, allocator)
1307 }
1308 SupportedMetric::Cosine => {
1309 let reify =
1310 Reify::<CompensatedCosine, _, $Q, D>::new(self.as_functor(), dim, arch);
1311 DistanceComputer::new(reify, allocator)
1312 }
1313 }
1314 }
1315
1316 fn build_fused_computer<R, B>(
1317 &self,
1318 input_arch: $arch,
1319 query: R,
1320 allocator: B,
1321 ) -> Result<QueryComputer<B>, AllocatorError>
1322 where
1323 R: ReportQueryLayout
1324 + for<'a> Reborrow<'a, Target = <$Q as FromOpaque>::Target<'a>>
1325 + Debug
1326 + Send
1327 + Sync
1328 + 'static,
1329 B: AllocatorCore,
1330 {
1331 type D = AsData<$N>;
1332 let arch = ($op)(input_arch);
1333 let dim = self.output_dim();
1334 match self.metric() {
1335 SupportedMetric::SquaredL2 => {
1336 let computer: CompensatedSquaredL2 = self.as_functor();
1337 let curried = Curried::new(computer, query);
1338 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1339 Ok(QueryComputer::new(reify, allocator)?)
1340 }
1341 SupportedMetric::InnerProduct => {
1342 let computer: CompensatedIP = self.as_functor();
1343 let curried = Curried::new(computer, query);
1344 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1345 Ok(QueryComputer::new(reify, allocator)?)
1346 }
1347 SupportedMetric::Cosine => {
1348 let computer: CompensatedCosine = self.as_functor();
1349 let curried = Curried::new(computer, query);
1350 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1351 Ok(QueryComputer::new(reify, allocator)?)
1352 }
1353 }
1354 }
1355 }
1356 };
1357}
1358
1359dispatch_map!(1, AsFull, Scalar);
1360dispatch_map!(2, AsFull, Scalar);
1361dispatch_map!(4, AsFull, Scalar);
1362dispatch_map!(8, AsFull, Scalar);
1363
1364dispatch_map!(1, AsData<1>, Scalar);
1365dispatch_map!(2, AsData<2>, Scalar);
1366dispatch_map!(4, AsData<4>, Scalar);
1367dispatch_map!(8, AsData<8>, Scalar);
1368
1369dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Scalar);
1371dispatch_map!(2, AsQuery<2>, Scalar);
1372dispatch_map!(4, AsQuery<4>, Scalar);
1373dispatch_map!(8, AsQuery<8>, Scalar);
1374
1375cfg_if::cfg_if! {
1376 if #[cfg(target_arch = "x86_64")] {
1377 fn downcast_to_v3(arch: V4) -> V3 {
1378 arch.into()
1379 }
1380
1381 dispatch_map!(1, AsFull, V3);
1383 dispatch_map!(2, AsFull, V3);
1384 dispatch_map!(4, AsFull, V3);
1385 dispatch_map!(8, AsFull, V3);
1386
1387 dispatch_map!(1, AsData<1>, V3);
1388 dispatch_map!(2, AsData<2>, V3);
1389 dispatch_map!(4, AsData<4>, V3);
1390 dispatch_map!(8, AsData<8>, V3);
1391
1392 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V3);
1393 dispatch_map!(2, AsQuery<2>, V3);
1394 dispatch_map!(4, AsQuery<4>, V3);
1395 dispatch_map!(8, AsQuery<8>, V3);
1396
1397 dispatch_map!(1, AsFull, V4, downcast_to_v3);
1399 dispatch_map!(2, AsFull, V4, downcast_to_v3);
1400 dispatch_map!(4, AsFull, V4, downcast_to_v3);
1401 dispatch_map!(8, AsFull, V4, downcast_to_v3);
1402
1403 dispatch_map!(1, AsData<1>, V4, downcast_to_v3);
1404 dispatch_map!(2, AsData<2>, V4); dispatch_map!(4, AsData<4>, V4); dispatch_map!(8, AsData<8>, V4, downcast_to_v3);
1407
1408 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V4, downcast_to_v3);
1409 dispatch_map!(2, AsQuery<2>, V4); dispatch_map!(4, AsQuery<4>, V4); dispatch_map!(8, AsQuery<8>, V4, downcast_to_v3);
1412 } else if #[cfg(target_arch = "aarch64")] {
1413 fn downcast(arch: Neon) -> Scalar {
1414 arch.retarget()
1415 }
1416
1417 dispatch_map!(1, AsFull, Neon, downcast);
1418 dispatch_map!(2, AsFull, Neon, downcast);
1419 dispatch_map!(4, AsFull, Neon, downcast);
1420 dispatch_map!(8, AsFull, Neon, downcast);
1421
1422 dispatch_map!(1, AsData<1>, Neon, downcast);
1423 dispatch_map!(2, AsData<2>, Neon);
1424 dispatch_map!(4, AsData<4>, Neon);
1425 dispatch_map!(8, AsData<8>, Neon, downcast);
1426
1427 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Neon, downcast);
1428 dispatch_map!(2, AsQuery<2>, Neon);
1429 dispatch_map!(4, AsQuery<4>, Neon);
1430 dispatch_map!(8, AsQuery<8>, Neon, downcast);
1431 }
1432}
1433
1434#[derive(Debug, Clone, Copy)]
1446struct ComputerDispatcher<Q, const N: usize> {
1447 _query_type: std::marker::PhantomData<Q>,
1448}
1449
1450impl<Q, const N: usize> ComputerDispatcher<Q, N> {
1451 fn new() -> Self {
1452 Self {
1453 _query_type: std::marker::PhantomData,
1454 }
1455 }
1456}
1457
1458impl<M, const N: usize, A, B, Q>
1459 diskann_wide::arch::Target2<
1460 M,
1461 Result<DistanceComputer<B>, AllocatorError>,
1462 &SphericalQuantizer<A>,
1463 B,
1464 > for ComputerDispatcher<Q, N>
1465where
1466 M: Architecture,
1467 A: Allocator,
1468 B: AllocatorCore,
1469 Q: FromOpaque,
1470 SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1471{
1472 fn run(
1473 self,
1474 arch: M,
1475 quantizer: &SphericalQuantizer<A>,
1476 allocator: B,
1477 ) -> Result<DistanceComputer<B>, AllocatorError> {
1478 quantizer.build_computer(arch, allocator)
1479 }
1480}
1481
1482impl<M, const N: usize, A, R, B, Q>
1483 diskann_wide::arch::Target3<
1484 M,
1485 Result<QueryComputer<B>, AllocatorError>,
1486 &SphericalQuantizer<A>,
1487 R,
1488 B,
1489 > for ComputerDispatcher<Q, N>
1490where
1491 M: Architecture,
1492 A: Allocator,
1493 B: AllocatorCore,
1494 Q: FromOpaque,
1495 R: ReportQueryLayout
1496 + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1497 + Debug
1498 + Send
1499 + Sync
1500 + 'static,
1501 SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1502{
1503 fn run(
1504 self,
1505 arch: M,
1506 quantizer: &SphericalQuantizer<A>,
1507 query: R,
1508 allocator: B,
1509 ) -> Result<QueryComputer<B>, AllocatorError> {
1510 quantizer.build_fused_computer(arch, query, allocator)
1511 }
1512}
1513
1514#[cfg(target_arch = "x86_64")]
1515trait Dispatchable<Q, const N: usize>:
1516 BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>
1517where
1518 Q: FromOpaque,
1519{
1520}
1521
1522#[cfg(target_arch = "x86_64")]
1523impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1524where
1525 Q: FromOpaque,
1526 T: BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>,
1527{
1528}
1529
1530#[cfg(target_arch = "aarch64")]
1531trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>
1532where
1533 Q: FromOpaque,
1534{
1535}
1536
1537#[cfg(target_arch = "aarch64")]
1538impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1539where
1540 Q: FromOpaque,
1541 T: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>,
1542{
1543}
1544
1545#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1546trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N>
1547where
1548 Q: FromOpaque,
1549{
1550}
1551
1552#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1553impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1554where
1555 Q: FromOpaque,
1556 T: BuildComputer<Scalar, Q, N>,
1557{
1558}
1559
1560impl<A, B> Quantizer<B> for Impl<1, A>
1565where
1566 A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
1567 B: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
1568{
1569 fn nbits(&self) -> usize {
1570 1
1571 }
1572
1573 fn dim(&self) -> usize {
1574 self.quantizer.output_dim()
1575 }
1576
1577 fn full_dim(&self) -> usize {
1578 self.quantizer.input_dim()
1579 }
1580
1581 fn bytes(&self) -> usize {
1582 DataRef::<1>::canonical_bytes(self.quantizer.output_dim())
1583 }
1584
1585 fn distance_computer(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError> {
1586 self.query_computer::<AsData<1>, _>(allocator)
1587 }
1588
1589 fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1590 &*self.distance
1591 }
1592
1593 fn query_computer(
1594 &self,
1595 layout: QueryLayout,
1596 allocator: B,
1597 ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1598 match layout {
1599 QueryLayout::SameAsData => Ok(self.query_computer::<AsData<1>, _>(allocator)?),
1600 QueryLayout::FourBitTransposed => {
1601 Ok(self.query_computer::<AsQuery<4, bits::BitTranspose>, _>(allocator)?)
1602 }
1603 QueryLayout::ScalarQuantized => {
1604 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1605 }
1606 QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1607 }
1608 }
1609
1610 fn query_buffer_description(
1611 &self,
1612 layout: QueryLayout,
1613 ) -> Result<QueryBufferDescription, UnsupportedQueryLayout> {
1614 let dim = <Self as Quantizer<B>>::dim(self);
1615 match layout {
1616 QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1617 DataRef::<1>::canonical_bytes(dim),
1618 PowerOfTwo::alignment_of::<u8>(),
1619 )),
1620 QueryLayout::FourBitTransposed => Ok(QueryBufferDescription::new(
1621 QueryRef::<4, bits::BitTranspose>::canonical_bytes(dim),
1622 PowerOfTwo::alignment_of::<u8>(),
1623 )),
1624 QueryLayout::ScalarQuantized => {
1625 Err(UnsupportedQueryLayout::new(layout, "1-bit compression"))
1626 }
1627 QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1628 FullQueryRef::canonical_bytes(dim),
1629 FullQueryRef::canonical_align(),
1630 )),
1631 }
1632 }
1633
1634 fn compress_query(
1635 &self,
1636 x: &[f32],
1637 layout: QueryLayout,
1638 allow_rescale: bool,
1639 mut buffer: OpaqueMut<'_>,
1640 scratch: ScopedAllocator<'_>,
1641 ) -> Result<(), QueryCompressionError> {
1642 let dim = <Self as Quantizer<B>>::dim(self);
1643 let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1644 match layout {
1645 QueryLayout::SameAsData => self.compress_query(
1646 v,
1647 DataMut::<1>::from_canonical_back_mut(&mut buffer, dim)
1648 .map_err(NotCanonical::new)?,
1649 scratch,
1650 ),
1651 QueryLayout::FourBitTransposed => self.compress_query(
1652 v,
1653 QueryMut::<4, bits::BitTranspose>::from_canonical_back_mut(&mut buffer, dim)
1654 .map_err(NotCanonical::new)?,
1655 scratch,
1656 ),
1657 QueryLayout::ScalarQuantized => {
1658 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1659 }
1660 QueryLayout::FullPrecision => self.compress_query(
1661 v,
1662 FullQueryMut::from_canonical_mut(&mut buffer, dim)
1663 .map_err(NotCanonical::new)?,
1664 scratch,
1665 ),
1666 }
1667 };
1668
1669 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1670 let mut copy = x.to_owned();
1671 self.quantizer.rescale(&mut copy);
1672 finish(©)
1673 } else {
1674 finish(x)
1675 }
1676 }
1677
1678 fn fused_query_computer(
1679 &self,
1680 x: &[f32],
1681 layout: QueryLayout,
1682 allow_rescale: bool,
1683 allocator: B,
1684 scratch: ScopedAllocator<'_>,
1685 ) -> Result<QueryComputer<B>, QueryComputerError> {
1686 let dim = <Self as Quantizer<B>>::dim(self);
1687 let finish = |v: &[f32], allocator: B| -> Result<QueryComputer<B>, QueryComputerError> {
1688 match layout {
1689 QueryLayout::SameAsData => self.fused_query_computer::<AsData<1>, Data<1, _>, _>(
1690 v,
1691 Data::new_in(dim, allocator.clone())?,
1692 allocator,
1693 scratch,
1694 ),
1695 QueryLayout::FourBitTransposed => self
1696 .fused_query_computer::<AsQuery<4, bits::BitTranspose>, Query<4, bits::BitTranspose, _>, _>(
1697 v,
1698 Query::new_in(dim, allocator.clone())?,
1699 allocator,
1700 scratch,
1701 ),
1702 QueryLayout::ScalarQuantized => {
1703 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1704 }
1705 QueryLayout::FullPrecision => self.fused_query_computer::<AsFull, FullQuery<_>, _>(
1706 v,
1707 FullQuery::empty(dim, allocator.clone())?,
1708 allocator,
1709 scratch,
1710 ),
1711 }
1712 };
1713
1714 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1715 let mut copy = x.to_owned();
1716 self.quantizer.rescale(&mut copy);
1717 finish(©, allocator)
1718 } else {
1719 finish(x, allocator)
1720 }
1721 }
1722
1723 fn is_supported(&self, layout: QueryLayout) -> bool {
1724 Self::supports(layout)
1725 }
1726
1727 fn compress(
1728 &self,
1729 x: &[f32],
1730 mut into: OpaqueMut<'_>,
1731 scratch: ScopedAllocator<'_>,
1732 ) -> Result<(), CompressionError> {
1733 let dim = <Self as Quantizer<B>>::dim(self);
1734 let into = DataMut::<1>::from_canonical_back_mut(into.inspect(), dim)
1735 .map_err(CompressionError::not_canonical)?;
1736 self.quantizer
1737 .compress_into_with(x, into, scratch)
1738 .map_err(CompressionError::CompressionError)
1739 }
1740
1741 fn metric(&self) -> SupportedMetric {
1742 self.quantizer.metric()
1743 }
1744
1745 fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1746 let clone = (*self).try_clone()?;
1747 poly!({ Quantizer<B> }, clone, allocator)
1748 }
1749
1750 #[cfg(feature = "flatbuffers")]
1751 fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1752 Impl::<1, A>::serialize(self, allocator)
1753 }
1754}
1755
1756macro_rules! plan {
1757 ($N:literal) => {
1758 impl<A, B> Quantizer<B> for Impl<$N, A>
1759 where
1760 A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
1761 B: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
1762 {
1763 fn nbits(&self) -> usize {
1764 $N
1765 }
1766
1767 fn dim(&self) -> usize {
1768 self.quantizer.output_dim()
1769 }
1770
1771 fn full_dim(&self) -> usize {
1772 self.quantizer.input_dim()
1773 }
1774
1775 fn bytes(&self) -> usize {
1776 DataRef::<$N>::canonical_bytes(<Self as Quantizer<B>>::dim(self))
1777 }
1778
1779 fn distance_computer(
1780 &self,
1781 allocator: B
1782 ) -> Result<DistanceComputer<B>, AllocatorError> {
1783 self.query_computer::<AsData<$N>, _>(allocator)
1784 }
1785
1786 fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1787 &*self.distance
1788 }
1789
1790 fn query_computer(
1791 &self,
1792 layout: QueryLayout,
1793 allocator: B,
1794 ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1795 match layout {
1796 QueryLayout::SameAsData => Ok(self.query_computer::<AsData<$N>, _>(allocator)?)
1797 ,
1798 QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout::new(
1799 layout,
1800 concat!($N, "-bit compression"),
1801 ).into()),
1802 QueryLayout::ScalarQuantized => {
1803 Ok(self.query_computer::<AsQuery<$N, bits::Dense>, _>(allocator)?)
1804 },
1805 QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1806
1807 }
1808 }
1809
1810 fn query_buffer_description(
1811 &self,
1812 layout: QueryLayout
1813 ) -> Result<QueryBufferDescription, UnsupportedQueryLayout>
1814 {
1815 let dim = <Self as Quantizer<B>>::dim(self);
1816 match layout {
1817 QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1818 DataRef::<$N>::canonical_bytes(dim),
1819 PowerOfTwo::alignment_of::<u8>(),
1820 )),
1821 QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout {
1822 layout,
1823 desc: concat!($N, "-bit compression"),
1824 }),
1825 QueryLayout::ScalarQuantized => Ok(QueryBufferDescription::new(
1826 QueryRef::<$N, bits::Dense>::canonical_bytes(dim),
1827 PowerOfTwo::alignment_of::<u8>(),
1828 )),
1829 QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1830 FullQueryRef::canonical_bytes(dim),
1831 FullQueryRef::canonical_align(),
1832 )),
1833 }
1834 }
1835
1836 fn compress_query(
1837 &self,
1838 x: &[f32],
1839 layout: QueryLayout,
1840 allow_rescale: bool,
1841 mut buffer: OpaqueMut<'_>,
1842 scratch: ScopedAllocator<'_>,
1843 ) -> Result<(), QueryCompressionError> {
1844 let dim = <Self as Quantizer<B>>::dim(self);
1845 let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1846 match layout {
1847 QueryLayout::SameAsData => self.compress_query(
1848 v,
1849 DataMut::<$N>::from_canonical_back_mut(
1850 &mut buffer,
1851 dim,
1852 ).map_err(NotCanonical::new)?,
1853 scratch,
1854 ),
1855 QueryLayout::FourBitTransposed => {
1856 Err(UnsupportedQueryLayout::new(
1857 layout,
1858 concat!($N, "-bit compression"),
1859 ).into())
1860 },
1861 QueryLayout::ScalarQuantized => self.compress_query(
1862 v,
1863 QueryMut::<$N, bits::Dense>::from_canonical_back_mut(
1864 &mut buffer,
1865 dim,
1866 ).map_err(NotCanonical::new)?,
1867 scratch,
1868 ),
1869 QueryLayout::FullPrecision => self.compress_query(
1870 v,
1871 FullQueryMut::from_canonical_mut(
1872 &mut buffer,
1873 dim,
1874 ).map_err(NotCanonical::new)?,
1875 scratch,
1876 ),
1877 }
1878 };
1879
1880 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1881 let mut copy = x.to_owned();
1882 self.quantizer.rescale(&mut copy);
1883 finish(©)
1884 } else {
1885 finish(x)
1886 }
1887 }
1888
1889 fn fused_query_computer(
1890 &self,
1891 x: &[f32],
1892 layout: QueryLayout,
1893 allow_rescale: bool,
1894 allocator: B,
1895 scratch: ScopedAllocator<'_>,
1896 ) -> Result<QueryComputer<B>, QueryComputerError>
1897 {
1898 let dim = <Self as Quantizer<B>>::dim(self);
1899 let finish = |v: &[f32]| -> Result<QueryComputer<B>, QueryComputerError> {
1900 match layout {
1901 QueryLayout::SameAsData => {
1902 self.fused_query_computer::<AsData<$N>, Data<$N, _>, B>(
1903 v,
1904 Data::new_in(dim, allocator.clone())?,
1905 allocator,
1906 scratch,
1907 )
1908 },
1909 QueryLayout::FourBitTransposed => {
1910 Err(UnsupportedQueryLayout::new(
1911 layout,
1912 concat!($N, "-bit compression"),
1913 ).into())
1914 },
1915 QueryLayout::ScalarQuantized => {
1916 self.fused_query_computer::<AsQuery<$N, bits::Dense>, Query<$N, bits::Dense, _>, B>(
1917 v,
1918 Query::new_in(dim, allocator.clone())?,
1919 allocator,
1920 scratch,
1921 )
1922 },
1923 QueryLayout::FullPrecision => {
1924 self.fused_query_computer::<AsFull, FullQuery<_>, B>(
1925 v,
1926 FullQuery::empty(dim, allocator.clone())?,
1927 allocator,
1928 scratch,
1929 )
1930 },
1931 }
1932 };
1933
1934 let metric = <Self as Quantizer<B>>::metric(self);
1935 if allow_rescale && metric == SupportedMetric::InnerProduct {
1936 let mut copy = x.to_owned();
1937 self.quantizer.rescale(&mut copy);
1938 finish(©)
1939 } else {
1940 finish(x)
1941 }
1942 }
1943
1944 fn is_supported(&self, layout: QueryLayout) -> bool {
1945 Self::supports(layout)
1946 }
1947
1948 fn compress(
1949 &self,
1950 x: &[f32],
1951 mut into: OpaqueMut<'_>,
1952 scratch: ScopedAllocator<'_>,
1953 ) -> Result<(), CompressionError> {
1954 let dim = <Self as Quantizer<B>>::dim(self);
1955 let into = DataMut::<$N>::from_canonical_back_mut(into.inspect(), dim)
1956 .map_err(CompressionError::not_canonical)?;
1957
1958 self.quantizer.compress_into_with(x, into, scratch)
1959 .map_err(CompressionError::CompressionError)
1960 }
1961
1962 fn metric(&self) -> SupportedMetric {
1963 self.quantizer.metric()
1964 }
1965
1966 fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1967 let clone = (&*self).try_clone()?;
1968 poly!({ Quantizer<B> }, clone, allocator)
1969 }
1970
1971 #[cfg(feature = "flatbuffers")]
1972 fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1973 Impl::<$N, A>::serialize(self, allocator)
1974 }
1975 }
1976 };
1977 ($N:literal, $($Ns:literal),*) => {
1978 plan!($N);
1979 $(plan!($Ns);)*
1980 }
1981}
1982
1983plan!(2, 4, 8);
1984
1985#[cfg(feature = "flatbuffers")]
1990#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
1991#[derive(Debug, Clone, Error)]
1992#[non_exhaustive]
1993pub enum DeserializationError {
1994 #[error("unhandled file identifier in flatbuffer")]
1995 InvalidIdentifier,
1996
1997 #[error("unsupported number of bits ({0})")]
1998 UnsupportedBitWidth(u32),
1999
2000 #[error(transparent)]
2001 InvalidQuantizer(#[from] super::quantizer::DeserializationError),
2002
2003 #[error(transparent)]
2004 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
2005
2006 #[error(transparent)]
2007 AllocatorError(#[from] AllocatorError),
2008}
2009
2010#[cfg(feature = "flatbuffers")]
2016#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
2017pub fn try_deserialize<O, A>(
2018 data: &[u8],
2019 alloc: A,
2020) -> Result<Poly<dyn Quantizer<O>, A>, DeserializationError>
2021where
2022 O: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
2023 A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
2024{
2025 fn unpack_bits<'a, const NBITS: usize, O, A>(
2030 proto: fb::spherical::SphericalQuantizer<'_>,
2031 alloc: A,
2032 ) -> Result<Poly<dyn Quantizer<O> + 'a, A>, DeserializationError>
2033 where
2034 O: Allocator + Send + Sync + std::panic::UnwindSafe + 'static,
2035 A: Allocator + Send + Sync + 'a,
2036 Impl<NBITS, A>: Quantizer<O> + Constructible<A>,
2037 {
2038 let imp = match Poly::new_with(
2039 |alloc| Impl::<NBITS, A>::try_deserialize_from(proto, alloc),
2040 alloc,
2041 ) {
2042 Ok(imp) => imp,
2043 Err(CompoundError::Allocator(err)) => {
2044 return Err(err.into());
2045 }
2046 Err(CompoundError::Constructor(err)) => {
2047 return Err(err);
2048 }
2049 };
2050 Ok(poly!({ Quantizer<O> }, imp))
2051 }
2052
2053 if !fb::spherical::quantizer_buffer_has_identifier(data) {
2055 return Err(DeserializationError::InvalidIdentifier);
2056 }
2057
2058 let root = fb::spherical::root_as_quantizer(data)?;
2062 let nbits = root.nbits();
2063 let proto = root.quantizer();
2064
2065 match nbits {
2066 1 => unpack_bits::<1, _, _>(proto, alloc),
2067 2 => unpack_bits::<2, _, _>(proto, alloc),
2068 4 => unpack_bits::<4, _, _>(proto, alloc),
2069 8 => unpack_bits::<8, _, _>(proto, alloc),
2070 n => Err(DeserializationError::UnsupportedBitWidth(n)),
2071 }
2072}
2073
2074#[cfg(test)]
2079mod tests {
2080 use diskann_utils::views::rowmajor::{self, Matrix};
2081 use rand::{SeedableRng, rngs::StdRng};
2082
2083 use super::*;
2084 use crate::{
2085 algorithms::{TransformKind, transforms::TargetDim},
2086 alloc::{AlignedAllocator, GlobalAllocator, Poly},
2087 num::PowerOfTwo,
2088 spherical::PreScale,
2089 };
2090
2091 fn test_plan_1_bit(plan: &dyn Quantizer) {
2096 assert_eq!(
2097 plan.nbits(),
2098 1,
2099 "this test only applies to 1-bit quantization"
2100 );
2101
2102 for layout in QueryLayout::all() {
2104 match layout {
2105 QueryLayout::SameAsData
2106 | QueryLayout::FourBitTransposed
2107 | QueryLayout::FullPrecision => assert!(
2108 plan.is_supported(layout),
2109 "expected {} to be supported",
2110 layout
2111 ),
2112 QueryLayout::ScalarQuantized => assert!(
2113 !plan.is_supported(layout),
2114 "expected {} to not be supported",
2115 layout
2116 ),
2117 }
2118 }
2119 }
2120
2121 fn test_plan_n_bit(plan: &dyn Quantizer, nbits: usize) {
2122 assert_ne!(nbits, 1, "there is another test for 1-bit quantizers");
2123 assert_eq!(
2124 plan.nbits(),
2125 nbits,
2126 "this test only applies to 1-bit quantization"
2127 );
2128
2129 for layout in QueryLayout::all() {
2131 match layout {
2132 QueryLayout::SameAsData
2133 | QueryLayout::ScalarQuantized
2134 | QueryLayout::FullPrecision => assert!(
2135 plan.is_supported(layout),
2136 "expected {} to be supported",
2137 layout
2138 ),
2139 QueryLayout::FourBitTransposed => assert!(
2140 !plan.is_supported(layout),
2141 "expected {} to not be supported",
2142 layout
2143 ),
2144 }
2145 }
2146 }
2147
2148 #[inline(never)]
2149 fn test_plan(plan: &dyn Quantizer, nbits: usize, dataset: rowmajor::Ref<f32>) {
2150 if nbits == 1 {
2152 test_plan_1_bit(plan);
2153 } else {
2154 test_plan_n_bit(plan, nbits);
2155 }
2156
2157 assert_eq!(plan.full_dim(), dataset.ncols());
2159
2160 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2162 let mut a = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2163 let mut b = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2164 let scoped_global = ScopedAllocator::global();
2165
2166 plan.compress(dataset.row(0), OpaqueMut::new(&mut a), scoped_global)
2167 .unwrap();
2168 plan.compress(dataset.row(1), OpaqueMut::new(&mut b), scoped_global)
2169 .unwrap();
2170
2171 let f = plan.distance_computer(GlobalAllocator).unwrap();
2172 let _: f32 = f
2173 .evaluate_similarity(Opaque::new(&a), Opaque::new(&b))
2174 .unwrap();
2175
2176 let test_errors = |f: &dyn DynDistanceComputer| {
2177 let err = f
2179 .evaluate(Opaque::new(&a[..a.len() - 1]), Opaque::new(&b))
2180 .unwrap_err();
2181 assert!(matches!(err, DistanceError::QueryReify(_)));
2182
2183 let err = f
2185 .evaluate(Opaque::new(&vec![0u8; a.len() + 1]), Opaque::new(&b))
2186 .unwrap_err();
2187 assert!(matches!(err, DistanceError::QueryReify(_)));
2188
2189 let err = f
2191 .evaluate(Opaque::new(&a), Opaque::new(&b[..b.len() - 1]))
2192 .unwrap_err();
2193 assert!(matches!(err, DistanceError::XReify(_)));
2194
2195 let err = f
2197 .evaluate(Opaque::new(&a), Opaque::new(&vec![0u8; b.len() + 1]))
2198 .unwrap_err();
2199 assert!(matches!(err, DistanceError::XReify(_)));
2200 };
2201
2202 test_errors(&*f.inner);
2203
2204 let f = plan.distance_computer_ref();
2205 let _: f32 = f.evaluate(Opaque::new(&a), Opaque::new(&b)).unwrap();
2206 test_errors(f);
2207
2208 for layout in QueryLayout::all() {
2210 if !plan.is_supported(layout) {
2211 let check_message = |msg: &str| {
2212 assert!(
2213 msg.contains(&(layout.to_string())),
2214 "error message ({}) should contain the layout \"{}\"",
2215 msg,
2216 layout
2217 );
2218 assert!(
2219 msg.contains(&format!("{}", nbits)),
2220 "error message ({}) should contain the number of bits \"{}\"",
2221 msg,
2222 nbits
2223 );
2224 };
2225
2226 {
2228 let err = plan
2229 .fused_query_computer(
2230 dataset.row(1),
2231 layout,
2232 false,
2233 GlobalAllocator,
2234 scoped_global,
2235 )
2236 .unwrap_err();
2237
2238 let msg = err.to_string();
2239 check_message(&msg);
2240 }
2241
2242 {
2244 let err = plan.query_buffer_description(layout).unwrap_err();
2245 let msg = err.to_string();
2246 check_message(&msg);
2247 }
2248
2249 {
2251 let buffer = &mut [];
2252 let err = plan
2253 .compress_query(
2254 dataset.row(1),
2255 layout,
2256 true,
2257 OpaqueMut::new(buffer),
2258 scoped_global,
2259 )
2260 .unwrap_err();
2261 let msg = err.to_string();
2262 check_message(&msg);
2263 }
2264
2265 {
2267 let err = plan.query_computer(layout, GlobalAllocator).unwrap_err();
2268 let msg = err.to_string();
2269 check_message(&msg);
2270 }
2271
2272 continue;
2273 }
2274
2275 let g = plan
2276 .fused_query_computer(
2277 dataset.row(1),
2278 layout,
2279 false,
2280 GlobalAllocator,
2281 scoped_global,
2282 )
2283 .unwrap();
2284 assert_eq!(
2285 g.layout(),
2286 layout,
2287 "the query computer should faithfully preserve the requested layout"
2288 );
2289
2290 let direct: f32 = g.evaluate_similarity(Opaque(&a)).unwrap();
2291
2292 {
2294 let err = g
2295 .evaluate_similarity(Opaque::new(&a[..a.len() - 1]))
2296 .unwrap_err();
2297 assert!(matches!(err, QueryDistanceError::XReify(_)));
2298
2299 let err = g
2300 .evaluate_similarity(Opaque::new(&vec![0u8; a.len() + 1]))
2301 .unwrap_err();
2302 assert!(matches!(err, QueryDistanceError::XReify(_)));
2303 }
2304
2305 let sizes = plan.query_buffer_description(layout).unwrap();
2306 let mut buf =
2307 Poly::broadcast(0u8, sizes.bytes(), AlignedAllocator::new(sizes.align())).unwrap();
2308
2309 plan.compress_query(
2310 dataset.row(1),
2311 layout,
2312 false,
2313 OpaqueMut::new(&mut buf),
2314 scoped_global,
2315 )
2316 .unwrap();
2317
2318 let standalone = plan.query_computer(layout, GlobalAllocator).unwrap();
2319
2320 assert_eq!(
2321 standalone.layout(),
2322 layout,
2323 "the standalone computer did not preserve the requested layout",
2324 );
2325
2326 let indirect: f32 = standalone
2327 .evaluate_similarity(Opaque(&buf), Opaque(&a))
2328 .unwrap();
2329
2330 assert_eq!(
2331 direct, indirect,
2332 "the two different query computation APIs did not return the same result"
2333 );
2334
2335 let too_small = &dataset.row(0)[..dataset.ncols() - 1];
2337 assert!(
2338 plan.fused_query_computer(too_small, layout, false, GlobalAllocator, scoped_global)
2339 .is_err()
2340 );
2341 }
2342
2343 {
2345 let mut too_small = vec![u8::default(); plan.bytes() - 1];
2346 assert!(
2347 plan.compress(dataset.row(0), OpaqueMut(&mut too_small), scoped_global)
2348 .is_err()
2349 );
2350
2351 let mut too_big = vec![u8::default(); plan.bytes() + 1];
2352 assert!(
2353 plan.compress(dataset.row(0), OpaqueMut(&mut too_big), scoped_global)
2354 .is_err()
2355 );
2356
2357 let mut just_right = vec![u8::default(); plan.bytes()];
2358 assert!(
2359 plan.compress(
2360 &dataset.row(0)[..dataset.ncols() - 1],
2361 OpaqueMut(&mut just_right),
2362 scoped_global
2363 )
2364 .is_err()
2365 );
2366 }
2367 }
2368
2369 fn make_impl<const NBITS: usize>(metric: SupportedMetric) -> (Impl<NBITS>, rowmajor::Owned<f32>)
2370 where
2371 Impl<NBITS>: Constructible,
2372 {
2373 let data = test_dataset();
2374 let mut rng = StdRng::seed_from_u64(0x7d535118722ff197);
2375
2376 let quantizer = SphericalQuantizer::train(
2377 data.as_view(),
2378 TransformKind::PaddingHadamard {
2379 target_dim: TargetDim::Natural,
2380 },
2381 metric,
2382 PreScale::None,
2383 &mut rng,
2384 GlobalAllocator,
2385 )
2386 .unwrap();
2387
2388 (Impl::new(quantizer).unwrap(), data)
2389 }
2390
2391 #[test]
2392 fn test_plan_1bit_l2() {
2393 let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2394 test_plan(&plan, 1, data.as_view());
2395 }
2396
2397 #[test]
2398 fn test_plan_1bit_ip() {
2399 let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2400 test_plan(&plan, 1, data.as_view());
2401 }
2402
2403 #[test]
2404 fn test_plan_1bit_cosine() {
2405 let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2406 test_plan(&plan, 1, data.as_view());
2407 }
2408
2409 #[test]
2410 fn test_plan_2bit_l2() {
2411 let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2412 test_plan(&plan, 2, data.as_view());
2413 }
2414
2415 #[test]
2416 fn test_plan_2bit_ip() {
2417 let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2418 test_plan(&plan, 2, data.as_view());
2419 }
2420
2421 #[test]
2422 fn test_plan_2bit_cosine() {
2423 let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2424 test_plan(&plan, 2, data.as_view());
2425 }
2426
2427 #[test]
2428 fn test_plan_4bit_l2() {
2429 let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2430 test_plan(&plan, 4, data.as_view());
2431 }
2432
2433 #[test]
2434 fn test_plan_4bit_ip() {
2435 let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2436 test_plan(&plan, 4, data.as_view());
2437 }
2438
2439 #[test]
2440 fn test_plan_4bit_cosine() {
2441 let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2442 test_plan(&plan, 4, data.as_view());
2443 }
2444
2445 #[test]
2446 fn test_plan_8bit_l2() {
2447 let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2448 test_plan(&plan, 8, data.as_view());
2449 }
2450
2451 #[test]
2452 fn test_plan_8bit_ip() {
2453 let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2454 test_plan(&plan, 8, data.as_view());
2455 }
2456
2457 #[test]
2458 fn test_plan_8bit_cosine() {
2459 let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2460 test_plan(&plan, 8, data.as_view());
2461 }
2462
2463 fn test_dataset() -> rowmajor::Owned<f32> {
2464 let data = vec![
2465 0.28657,
2466 -0.0318168,
2467 0.0666847,
2468 0.0329265,
2469 -0.00829283,
2470 0.168735,
2471 -0.000846311,
2472 -0.360779, -0.0968938,
2474 0.161921,
2475 -0.0979579,
2476 0.102228,
2477 -0.259928,
2478 -0.139634,
2479 0.165384,
2480 -0.293443, 0.130205,
2482 0.265737,
2483 0.401816,
2484 -0.407552,
2485 0.13012,
2486 -0.0475244,
2487 0.511723,
2488 -0.4372, -0.0979126,
2490 0.135861,
2491 -0.0154144,
2492 -0.14047,
2493 -0.0250029,
2494 -0.190279,
2495 0.407283,
2496 -0.389184, -0.264153,
2498 0.0696822,
2499 -0.145585,
2500 0.370284,
2501 0.186825,
2502 -0.140736,
2503 0.274703,
2504 -0.334563, 0.247613,
2506 0.513165,
2507 -0.0845867,
2508 0.0532264,
2509 -0.00480601,
2510 -0.122408,
2511 0.47227,
2512 -0.268301, 0.103198,
2514 0.30756,
2515 -0.316293,
2516 -0.0686877,
2517 -0.330729,
2518 -0.461997,
2519 0.550857,
2520 -0.240851, 0.128258,
2522 0.786291,
2523 -0.0268103,
2524 0.111763,
2525 -0.308962,
2526 -0.17407,
2527 0.437154,
2528 -0.159879, 0.00374063,
2530 0.490301,
2531 0.0327826,
2532 -0.0340962,
2533 -0.118605,
2534 0.163879,
2535 0.2737,
2536 -0.299942, -0.284077,
2538 0.249377,
2539 -0.0307734,
2540 -0.0661631,
2541 0.233854,
2542 0.427987,
2543 0.614132,
2544 -0.288649, -0.109492,
2546 0.203939,
2547 -0.73956,
2548 -0.130748,
2549 0.22072,
2550 0.0647836,
2551 0.328726,
2552 -0.374602, -0.223114,
2554 0.0243489,
2555 0.109195,
2556 -0.416914,
2557 0.0201052,
2558 -0.0190542,
2559 0.947078,
2560 -0.333229, -0.165869,
2562 -0.00296729,
2563 -0.414378,
2564 0.231321,
2565 0.205365,
2566 0.161761,
2567 0.148608,
2568 -0.395063, -0.0498255,
2570 0.193279,
2571 -0.110946,
2572 -0.181174,
2573 -0.274578,
2574 -0.227511,
2575 0.190208,
2576 -0.256174, -0.188106,
2578 -0.0292958,
2579 0.0930939,
2580 0.0558456,
2581 0.257437,
2582 0.685481,
2583 0.307922,
2584 -0.320006, 0.250035,
2586 0.275942,
2587 -0.0856306,
2588 -0.352027,
2589 -0.103509,
2590 -0.00890859,
2591 0.276121,
2592 -0.324718, ];
2594
2595 rowmajor::Owned::try_from_data(data.into(), 16, 8).unwrap()
2596 }
2597
2598 #[cfg(feature = "flatbuffers")]
2599 mod serialization {
2600 use std::sync::{
2601 Arc,
2602 atomic::{AtomicBool, Ordering},
2603 };
2604
2605 use super::*;
2606 use crate::alloc::{BumpAllocator, GlobalAllocator};
2607
2608 fn test_deserialization_inner(
2609 quantizer: &dyn Quantizer,
2610 deserialized: &dyn Quantizer,
2611 nbits: usize,
2612 dataset: rowmajor::Ref<'_, f32>,
2613 ) {
2614 let scoped_global = ScopedAllocator::global();
2615
2616 assert_eq!(quantizer.nbits(), nbits);
2617 assert_eq!(deserialized.nbits(), nbits);
2618 assert_eq!(deserialized.bytes(), quantizer.bytes());
2619 assert_eq!(deserialized.dim(), quantizer.dim());
2620 assert_eq!(deserialized.full_dim(), quantizer.full_dim());
2621 assert_eq!(deserialized.metric(), quantizer.metric());
2622
2623 for layout in QueryLayout::all() {
2624 assert_eq!(
2625 deserialized.is_supported(layout),
2626 quantizer.is_supported(layout)
2627 );
2628 }
2629
2630 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2632 {
2633 let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2634 let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2635
2636 for row in dataset.rows() {
2637 quantizer
2638 .compress(row, OpaqueMut::new(&mut a), scoped_global)
2639 .unwrap();
2640 deserialized
2641 .compress(row, OpaqueMut::new(&mut b), scoped_global)
2642 .unwrap();
2643
2644 assert_eq!(a, b);
2646 }
2647 }
2648
2649 {
2651 let mut a0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2652 let mut a1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2653 let mut b0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2654 let mut b1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2655
2656 let q_computer = quantizer.distance_computer(GlobalAllocator).unwrap();
2657 let q_computer_ref = quantizer.distance_computer_ref();
2658 let d_computer = deserialized.distance_computer(GlobalAllocator).unwrap();
2659 let d_computer_ref = deserialized.distance_computer_ref();
2660
2661 for r0 in dataset.rows() {
2662 quantizer
2663 .compress(r0, OpaqueMut::new(&mut a0), scoped_global)
2664 .unwrap();
2665 deserialized
2666 .compress(r0, OpaqueMut::new(&mut b0), scoped_global)
2667 .unwrap();
2668 for r1 in dataset.rows() {
2669 quantizer
2670 .compress(r1, OpaqueMut::new(&mut a1), scoped_global)
2671 .unwrap();
2672 deserialized
2673 .compress(r1, OpaqueMut::new(&mut b1), scoped_global)
2674 .unwrap();
2675
2676 let a0 = Opaque::new(&a0);
2677 let a1 = Opaque::new(&a1);
2678
2679 let q_computer_dist = q_computer.evaluate_similarity(a0, a1).unwrap();
2680 let d_computer_dist = d_computer.evaluate_similarity(a0, a1).unwrap();
2681
2682 assert_eq!(q_computer_dist, d_computer_dist);
2683
2684 let q_computer_ref_dist = q_computer_ref.evaluate(a0, a1).unwrap();
2685
2686 assert_eq!(q_computer_dist, q_computer_ref_dist);
2687
2688 let d_computer_ref_dist = d_computer_ref.evaluate(a0, a1).unwrap();
2689 assert_eq!(d_computer_dist, d_computer_ref_dist);
2690 }
2691 }
2692 }
2693
2694 {
2696 let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2697 let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2698
2699 for layout in QueryLayout::all() {
2700 if !quantizer.is_supported(layout) {
2701 continue;
2702 }
2703
2704 for r in dataset.rows() {
2705 let q_computer = quantizer
2706 .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2707 .unwrap();
2708 let d_computer = deserialized
2709 .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2710 .unwrap();
2711
2712 for u in dataset.rows() {
2713 quantizer
2714 .compress(u, OpaqueMut::new(&mut a), scoped_global)
2715 .unwrap();
2716 deserialized
2717 .compress(u, OpaqueMut::new(&mut b), scoped_global)
2718 .unwrap();
2719
2720 assert_eq!(
2721 q_computer.evaluate_similarity(Opaque::new(&a)).unwrap(),
2722 d_computer.evaluate_similarity(Opaque::new(&b)).unwrap(),
2723 );
2724 }
2725 }
2726 }
2727 }
2728 }
2729
2730 #[inline(never)]
2731 fn test_plan_serialization(
2732 quantizer: &dyn Quantizer,
2733 nbits: usize,
2734 dataset: rowmajor::Ref<f32>,
2735 ) {
2736 let global = GlobalAllocator;
2737
2738 assert_eq!(quantizer.full_dim(), dataset.ncols());
2740
2741 let serialized = quantizer.serialize(global).unwrap();
2742 let deserialized = try_deserialize::<GlobalAllocator, _>(&serialized, global).unwrap();
2743
2744 test_deserialization_inner(quantizer, &*deserialized, nbits, dataset);
2745
2746 match nbits {
2748 1 => {
2749 test_deserialization_inner(
2750 quantizer,
2751 &Impl::<1, _>::try_deserialize(&serialized, global).unwrap(),
2752 nbits,
2753 dataset,
2754 );
2755
2756 assert!(matches!(
2758 Impl::<2, _>::try_deserialize(&serialized, global),
2759 Err(DeserializationError::UnsupportedBitWidth(1)),
2760 ));
2761 }
2762 2 => {
2763 test_deserialization_inner(
2764 quantizer,
2765 &Impl::<2, _>::try_deserialize(&serialized, global).unwrap(),
2766 nbits,
2767 dataset,
2768 );
2769
2770 assert!(matches!(
2772 Impl::<1, _>::try_deserialize(&serialized, global),
2773 Err(DeserializationError::UnsupportedBitWidth(2)),
2774 ));
2775 }
2776 4 => {
2777 test_deserialization_inner(
2778 quantizer,
2779 &Impl::<4, _>::try_deserialize(&serialized, global).unwrap(),
2780 nbits,
2781 dataset,
2782 );
2783
2784 assert!(matches!(
2786 Impl::<8, _>::try_deserialize(&serialized, global),
2787 Err(DeserializationError::UnsupportedBitWidth(4)),
2788 ));
2789 }
2790 8 => {
2791 test_deserialization_inner(
2792 quantizer,
2793 &Impl::<8, _>::try_deserialize(&serialized, global).unwrap(),
2794 nbits,
2795 dataset,
2796 );
2797
2798 assert!(matches!(
2800 Impl::<4, _>::try_deserialize(&serialized, global),
2801 Err(DeserializationError::UnsupportedBitWidth(8)),
2802 ));
2803 }
2804 _ => panic!("bit width {} is not supported", nbits),
2805 }
2806 }
2807
2808 #[derive(Debug, Clone)]
2810 struct FlakyAllocator {
2811 have_allocated: Arc<AtomicBool>,
2812 }
2813
2814 impl FlakyAllocator {
2815 fn new(have_allocated: Arc<AtomicBool>) -> Self {
2816 Self { have_allocated }
2817 }
2818 }
2819
2820 unsafe impl AllocatorCore for FlakyAllocator {
2822 fn allocate(
2823 &self,
2824 layout: std::alloc::Layout,
2825 ) -> Result<std::ptr::NonNull<[u8]>, AllocatorError> {
2826 if self.have_allocated.swap(true, Ordering::Relaxed) {
2827 Err(AllocatorError)
2828 } else {
2829 GlobalAllocator.allocate(layout)
2830 }
2831 }
2832
2833 unsafe fn deallocate(&self, ptr: std::ptr::NonNull<[u8]>, layout: std::alloc::Layout) {
2834 unsafe { GlobalAllocator.deallocate(ptr, layout) }
2836 }
2837 }
2838
2839 fn test_plan_panic_boundary<const NBITS: usize>(v: &Impl<NBITS>)
2840 where
2841 Impl<NBITS>: Quantizer,
2842 {
2843 let have_allocated = Arc::new(AtomicBool::new(false));
2845 let _: AllocatorError = v
2846 .serialize(FlakyAllocator::new(have_allocated.clone()))
2847 .unwrap_err();
2848 assert!(have_allocated.load(Ordering::Relaxed));
2849 }
2850
2851 #[test]
2852 fn test_plan_1bit_l2() {
2853 let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2854 test_plan_panic_boundary(&plan);
2855 test_plan_serialization(&plan, 1, data.as_view());
2856 }
2857
2858 #[test]
2859 fn test_plan_1bit_ip() {
2860 let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2861 test_plan_panic_boundary(&plan);
2862 test_plan_serialization(&plan, 1, data.as_view());
2863 }
2864
2865 #[test]
2866 fn test_plan_2bit_l2() {
2867 let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2868 test_plan_panic_boundary(&plan);
2869 test_plan_serialization(&plan, 2, data.as_view());
2870 }
2871
2872 #[test]
2873 fn test_plan_2bit_ip() {
2874 let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2875 test_plan_panic_boundary(&plan);
2876 test_plan_serialization(&plan, 2, data.as_view());
2877 }
2878
2879 #[test]
2880 fn test_plan_4bit_l2() {
2881 let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2882 test_plan_panic_boundary(&plan);
2883 test_plan_serialization(&plan, 4, data.as_view());
2884 }
2885
2886 #[test]
2887 fn test_plan_4bit_ip() {
2888 let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2889 test_plan_panic_boundary(&plan);
2890 test_plan_serialization(&plan, 4, data.as_view());
2891 }
2892
2893 #[test]
2894 fn test_plan_8bit_l2() {
2895 let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2896 test_plan_panic_boundary(&plan);
2897 test_plan_serialization(&plan, 8, data.as_view());
2898 }
2899
2900 #[test]
2901 fn test_plan_8bit_ip() {
2902 let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2903 test_plan_panic_boundary(&plan);
2904 test_plan_serialization(&plan, 8, data.as_view());
2905 }
2906
2907 #[test]
2908 fn test_plan_1bit_cosine() {
2909 let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2910 test_plan_panic_boundary(&plan);
2911 test_plan_serialization(&plan, 1, data.as_view());
2912 }
2913
2914 #[test]
2915 fn test_plan_2bit_cosine() {
2916 let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2917 test_plan_panic_boundary(&plan);
2918 test_plan_serialization(&plan, 2, data.as_view());
2919 }
2920
2921 #[test]
2922 fn test_plan_4bit_cosine() {
2923 let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2924 test_plan_panic_boundary(&plan);
2925 test_plan_serialization(&plan, 4, data.as_view());
2926 }
2927
2928 #[test]
2929 fn test_plan_8bit_cosine() {
2930 let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2931 test_plan_panic_boundary(&plan);
2932 test_plan_serialization(&plan, 8, data.as_view());
2933 }
2934
2935 #[test]
2936 fn test_allocation_order() {
2937 let (plan, _) = make_impl::<1>(SupportedMetric::SquaredL2);
2938 let buf = plan.serialize(GlobalAllocator).unwrap();
2939
2940 let allocator = BumpAllocator::new(8192, PowerOfTwo::new(64).unwrap()).unwrap();
2941 let deserialized =
2942 try_deserialize::<GlobalAllocator, _>(&buf, allocator.clone()).unwrap();
2943 assert_eq!(
2944 Poly::as_ptr(&deserialized).cast::<u8>(),
2945 allocator.as_ptr(),
2946 "expected the returned box to be allocated first",
2947 );
2948 }
2949 }
2950
2951 #[cfg(feature = "flatbuffers")]
2963 mod compatibility {
2964 use std::path::PathBuf;
2965
2966 use serde::{Deserialize, Serialize};
2967
2968 use super::*;
2969
2970 use crate::test_util::Check;
2971
2972 const TRAINING_SEED: u64 = 0x7d535118722ff197;
2973
2974 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2979 #[serde(rename_all = "snake_case")]
2980 enum Metric {
2981 SquaredL2,
2982 InnerProduct,
2983 Cosine,
2984 }
2985
2986 impl std::fmt::Display for Metric {
2987 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2988 let s = match self {
2989 Self::SquaredL2 => "squared_l2",
2990 Self::InnerProduct => "inner_product",
2991 Self::Cosine => "cosine",
2992 };
2993 write!(f, "{}", s)
2994 }
2995 }
2996
2997 impl From<SupportedMetric> for Metric {
2998 fn from(m: SupportedMetric) -> Self {
2999 match m {
3000 SupportedMetric::SquaredL2 => Self::SquaredL2,
3001 SupportedMetric::InnerProduct => Self::InnerProduct,
3002 SupportedMetric::Cosine => Self::Cosine,
3003 }
3004 }
3005 }
3006
3007 impl From<Metric> for SupportedMetric {
3008 fn from(m: Metric) -> Self {
3009 match m {
3010 Metric::SquaredL2 => Self::SquaredL2,
3011 Metric::InnerProduct => Self::InnerProduct,
3012 Metric::Cosine => Self::Cosine,
3013 }
3014 }
3015 }
3016
3017 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3018 #[serde(rename_all = "snake_case")]
3019 enum DataTransform {
3020 PaddingHadamard,
3021 DoubleHadamard,
3022 Null,
3023 }
3024
3025 impl std::fmt::Display for DataTransform {
3026 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3027 let s = match self {
3028 Self::PaddingHadamard => "padding_hadamard",
3029 Self::DoubleHadamard => "double_hadamard",
3030 Self::Null => "null",
3031 };
3032 write!(f, "{}", s)
3033 }
3034 }
3035
3036 impl DataTransform {
3037 fn to_transform_kind(self) -> TransformKind {
3038 match self {
3039 Self::PaddingHadamard => TransformKind::PaddingHadamard {
3040 target_dim: TargetDim::Natural,
3041 },
3042 Self::DoubleHadamard => TransformKind::DoubleHadamard {
3043 target_dim: TargetDim::Natural,
3044 },
3045 Self::Null => TransformKind::Null,
3046 }
3047 }
3048 }
3049
3050 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3051 #[serde(rename_all = "snake_case")]
3052 enum Layout {
3053 SameAsData,
3054 FourBitTransposed,
3055 ScalarQuantized,
3056 FullPrecision,
3057 }
3058
3059 impl From<QueryLayout> for Layout {
3060 fn from(l: QueryLayout) -> Self {
3061 match l {
3062 QueryLayout::SameAsData => Self::SameAsData,
3063 QueryLayout::FourBitTransposed => Self::FourBitTransposed,
3064 QueryLayout::ScalarQuantized => Self::ScalarQuantized,
3065 QueryLayout::FullPrecision => Self::FullPrecision,
3066 }
3067 }
3068 }
3069
3070 impl From<Layout> for QueryLayout {
3071 fn from(l: Layout) -> Self {
3072 match l {
3073 Layout::SameAsData => Self::SameAsData,
3074 Layout::FourBitTransposed => Self::FourBitTransposed,
3075 Layout::ScalarQuantized => Self::ScalarQuantized,
3076 Layout::FullPrecision => Self::FullPrecision,
3077 }
3078 }
3079 }
3080
3081 #[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
3082 #[serde(rename_all = "snake_case")]
3083 enum ScaleConfig {
3084 #[default]
3085 None,
3086 ReciprocalMeanNorm,
3087 }
3088
3089 impl ScaleConfig {
3090 fn try_as_str(self) -> Option<&'static str> {
3091 match self {
3092 Self::None => Option::None,
3093 Self::ReciprocalMeanNorm => Some("rmn"),
3094 }
3095 }
3096
3097 fn to_prescale(self) -> PreScale {
3098 match self {
3099 Self::None => PreScale::None,
3100 Self::ReciprocalMeanNorm => PreScale::ReciprocalMeanNorm,
3101 }
3102 }
3103 }
3104
3105 #[derive(Debug, Clone, Serialize, Deserialize)]
3112 struct Baseline {
3113 nbits: usize,
3115 metric: Metric,
3117 transform: DataTransform,
3119 pre_scale: ScaleConfig,
3121 dim: usize,
3123 full_dim: usize,
3125 training_seed: u64,
3127 serialized_quantizer: Vec<u8>,
3129 compressed_vectors: Vec<Vec<u8>>,
3131 data_distances: Vec<f32>,
3134 query_distances: Vec<LayoutDistances>,
3136 #[serde(default, skip_serializing_if = "Option::is_none")]
3139 rescaled_query_distances: Option<Vec<LayoutDistances>>,
3140 }
3141
3142 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3144 struct LayoutDistances {
3145 layout: Layout,
3146 distances: Vec<Vec<f32>>,
3148 }
3149
3150 fn compress_dataset(
3155 quantizer: &dyn Quantizer,
3156 dataset: rowmajor::Ref<f32>,
3157 ) -> Vec<Vec<u8>> {
3158 let scoped_global = ScopedAllocator::global();
3159 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
3160 dataset
3161 .rows()
3162 .map(|row| {
3163 let mut buf = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
3164 quantizer
3165 .compress(row, OpaqueMut::new(&mut buf), scoped_global)
3166 .unwrap();
3167 buf.to_vec()
3168 })
3169 .collect()
3170 }
3171
3172 fn compute_layout_distances(
3173 quantizer: &dyn Quantizer,
3174 dataset: rowmajor::Ref<f32>,
3175 compressed: &[Vec<u8>],
3176 allow_rescale: bool,
3177 ) -> Vec<LayoutDistances> {
3178 let scoped_global = ScopedAllocator::global();
3179 QueryLayout::all()
3180 .into_iter()
3181 .filter(|&layout| quantizer.is_supported(layout))
3182 .map(|layout| {
3183 let distances = dataset
3184 .rows()
3185 .map(|query_row| {
3186 let computer = quantizer
3187 .fused_query_computer(
3188 query_row,
3189 layout,
3190 allow_rescale,
3191 GlobalAllocator,
3192 scoped_global,
3193 )
3194 .unwrap();
3195 compressed
3196 .iter()
3197 .map(|d| computer.evaluate_similarity(Opaque::new(d)).unwrap())
3198 .collect()
3199 })
3200 .collect();
3201 LayoutDistances {
3202 layout: layout.into(),
3203 distances,
3204 }
3205 })
3206 .collect()
3207 }
3208
3209 const TOLERANCE: Check = Check::absrel(1e-6, 0.0);
3210
3211 fn assert_layout_distances(
3214 quantizer: &dyn Quantizer,
3215 dataset: rowmajor::Ref<f32>,
3216 compressed: &[Vec<u8>],
3217 expected: &[LayoutDistances],
3218 allow_rescale: bool,
3219 label: &str,
3220 ) {
3221 let scoped_global = ScopedAllocator::global();
3222 for layout_distances in expected {
3223 let layout: QueryLayout = layout_distances.layout.into();
3224 assert!(
3225 quantizer.is_supported(layout),
3226 "{label}: unsupported layout {layout:?}"
3227 );
3228
3229 for (qi, (query_row, expected_distances)) in
3230 std::iter::zip(dataset.rows(), layout_distances.distances.iter()).enumerate()
3231 {
3232 let computer = quantizer
3233 .fused_query_computer(
3234 query_row,
3235 layout,
3236 allow_rescale,
3237 GlobalAllocator,
3238 scoped_global,
3239 )
3240 .unwrap();
3241
3242 for (di, (compressed, expected)) in
3243 std::iter::zip(compressed.iter(), expected_distances.iter()).enumerate()
3244 {
3245 let distance = computer
3246 .evaluate_similarity(Opaque::new(compressed))
3247 .unwrap();
3248
3249 if let Err(err) = TOLERANCE.check(distance, *expected) {
3250 panic!("{label}: layout = {layout:?}, query={qi}, data={di}\n{err}")
3251 }
3252 }
3253 }
3254 }
3255 }
3256
3257 fn generate_baseline(
3262 quantizer: &dyn Quantizer,
3263 transform: DataTransform,
3264 pre_scale: ScaleConfig,
3265 dataset: rowmajor::Ref<f32>,
3266 ) -> Baseline {
3267 let compressed_vectors = compress_dataset(quantizer, dataset);
3268
3269 let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3271 let mut data_distances = Vec::new();
3272 for (i, a) in compressed_vectors.iter().enumerate() {
3273 for b in compressed_vectors.iter().skip(i) {
3274 data_distances.push(
3275 f.evaluate_similarity(Opaque::new(a), Opaque::new(b))
3276 .unwrap(),
3277 );
3278 }
3279 }
3280
3281 let query_distances =
3282 compute_layout_distances(quantizer, dataset, &compressed_vectors, false);
3283
3284 let is_ip = quantizer.metric() == SupportedMetric::InnerProduct;
3285 let rescaled = compute_layout_distances(quantizer, dataset, &compressed_vectors, true);
3286
3287 if !is_ip {
3288 assert_eq!(
3290 query_distances,
3291 rescaled,
3292 "allow_rescale should not affect {:?} distances",
3293 quantizer.metric(),
3294 );
3295 }
3296
3297 let rescaled_query_distances = if is_ip { Some(rescaled) } else { None };
3298
3299 let serialized = quantizer.serialize(GlobalAllocator).unwrap();
3300
3301 Baseline {
3302 nbits: quantizer.nbits(),
3303 metric: quantizer.metric().into(),
3304 transform,
3305 pre_scale,
3306 dim: quantizer.dim(),
3307 full_dim: quantizer.full_dim(),
3308 training_seed: TRAINING_SEED,
3309 serialized_quantizer: serialized.to_vec(),
3310 compressed_vectors,
3311 data_distances,
3312 query_distances,
3313 rescaled_query_distances,
3314 }
3315 }
3316
3317 fn baseline_path(
3322 nbits: usize,
3323 metric: Metric,
3324 transform: DataTransform,
3325 pre_scale: ScaleConfig,
3326 ) -> PathBuf {
3327 let manifest_dir = env!("CARGO_MANIFEST_DIR");
3328 let mut name = format!("{}bit_{}_{}", nbits, metric, transform);
3329 if let Some(mangled) = pre_scale.try_as_str() {
3330 name.push('_');
3331 name.push_str(mangled);
3332 }
3333 name.push_str(".json");
3334 PathBuf::from(manifest_dir)
3335 .join("tests")
3336 .join("generated")
3337 .join("spherical")
3338 .join(name)
3339 }
3340
3341 fn should_overwrite() -> bool {
3342 std::env::var("DISKANN_TEST")
3343 .map(|v| v == "overwrite")
3344 .unwrap_or(false)
3345 }
3346
3347 fn save_baseline(baseline: &Baseline) {
3348 let path = baseline_path(
3349 baseline.nbits,
3350 baseline.metric,
3351 baseline.transform,
3352 baseline.pre_scale,
3353 );
3354 if let Some(parent) = path.parent() {
3355 std::fs::create_dir_all(parent).unwrap();
3356 }
3357 let json = serde_json::to_string_pretty(baseline).unwrap();
3358 std::fs::write(&path, json).unwrap();
3359 }
3360
3361 fn load_baseline(
3362 nbits: usize,
3363 metric: Metric,
3364 transform: DataTransform,
3365 pre_scale: ScaleConfig,
3366 ) -> Baseline {
3367 let path = baseline_path(nbits, metric, transform, pre_scale);
3368 let json = std::fs::read_to_string(&path).unwrap_or_else(|e| {
3369 panic!(
3370 "Failed to load baseline from {}: {e}\n\
3371 Run with DISKANN_TEST=overwrite to generate baseline files.",
3372 path.display()
3373 );
3374 });
3375 serde_json::from_str(&json).unwrap()
3376 }
3377
3378 fn check_baseline(
3383 baseline: &Baseline,
3384 dataset: rowmajor::Ref<f32>,
3385 expected_transform: DataTransform,
3386 expected_pre_scale: ScaleConfig,
3387 ) {
3388 assert_eq!(
3392 baseline.training_seed, TRAINING_SEED,
3393 "baseline was generated with a different training seed"
3394 );
3395 assert_eq!(
3396 baseline.transform, expected_transform,
3397 "baseline transform does not match expected transform"
3398 );
3399 assert_eq!(
3400 baseline.pre_scale, expected_pre_scale,
3401 "baseline pre_scale does not match expected pre_scale"
3402 );
3403
3404 let quantizer = try_deserialize::<GlobalAllocator, _>(
3406 &baseline.serialized_quantizer,
3407 GlobalAllocator,
3408 )
3409 .unwrap();
3410
3411 assert_eq!(quantizer.nbits(), baseline.nbits, "nbits mismatch");
3413 assert_eq!(quantizer.dim(), baseline.dim, "dim mismatch");
3414 assert_eq!(quantizer.full_dim(), baseline.full_dim, "full_dim mismatch");
3415 assert_eq!(
3416 quantizer.metric(),
3417 SupportedMetric::from(baseline.metric),
3418 "metric mismatch"
3419 );
3420
3421 let compressed = compress_dataset(&*quantizer, dataset);
3423 assert_eq!(
3424 compressed, baseline.compressed_vectors,
3425 "compressed vectors do not match baseline"
3426 );
3427
3428 let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3430 let n = baseline.compressed_vectors.len();
3431 let expected_len = n * (n + 1) / 2;
3432
3433 assert_eq!(
3434 baseline.data_distances.len(),
3435 expected_len,
3436 "baseline.data_distances has length {}, expected {} for {} compressed vectors",
3437 baseline.data_distances.len(),
3438 expected_len,
3439 n,
3440 );
3441 let mut k = 0;
3442 for (i, a) in baseline.compressed_vectors.iter().enumerate() {
3443 for (j, b) in baseline.compressed_vectors.iter().enumerate().skip(i) {
3444 let distance = f
3445 .evaluate_similarity(Opaque::new(a), Opaque::new(b))
3446 .unwrap();
3447
3448 if let Err(err) = TOLERANCE.check(distance, baseline.data_distances[k]) {
3449 panic!("data distance mismatch at pair ({i}, {j})\n{err}");
3450 }
3451 k += 1;
3452 }
3453 }
3454
3455 assert_layout_distances(
3457 &*quantizer,
3458 dataset,
3459 &baseline.compressed_vectors,
3460 &baseline.query_distances,
3461 false,
3462 "query distance mismatch",
3463 );
3464
3465 if let Some(rescaled) = &baseline.rescaled_query_distances {
3468 assert_eq!(
3469 SupportedMetric::from(baseline.metric),
3470 SupportedMetric::InnerProduct,
3471 "rescaled_query_distances should only be present \
3472 for InnerProduct"
3473 );
3474 assert_layout_distances(
3475 &*quantizer,
3476 dataset,
3477 &baseline.compressed_vectors,
3478 rescaled,
3479 true,
3480 "rescaled query distance mismatch",
3481 );
3482 } else {
3483 assert_layout_distances(
3484 &*quantizer,
3485 dataset,
3486 &baseline.compressed_vectors,
3487 &baseline.query_distances,
3488 true,
3489 "allow_rescale should not affect non-IP distances",
3490 );
3491 }
3492 }
3493
3494 fn make_impl_with<const NBITS: usize>(
3499 metric: SupportedMetric,
3500 transform: DataTransform,
3501 pre_scale: ScaleConfig,
3502 ) -> (Poly<dyn Quantizer>, rowmajor::Owned<f32>)
3503 where
3504 Impl<NBITS>: Constructible + Quantizer,
3505 {
3506 let data = test_dataset();
3507 let mut rng = StdRng::seed_from_u64(TRAINING_SEED);
3508
3509 let quantizer = SphericalQuantizer::train(
3510 data.as_view(),
3511 transform.to_transform_kind(),
3512 metric,
3513 pre_scale.to_prescale(),
3514 &mut rng,
3515 GlobalAllocator,
3516 )
3517 .unwrap();
3518
3519 (quantizer.as_quantizer::<NBITS>().unwrap(), data)
3520 }
3521
3522 fn run_compatibility_test<const NBITS: usize>(
3523 metric: SupportedMetric,
3524 transform: DataTransform,
3525 pre_scale: ScaleConfig,
3526 ) where
3527 Impl<NBITS>: Constructible + Quantizer,
3528 {
3529 let (quantizer, data) = make_impl_with::<NBITS>(metric, transform, pre_scale);
3530 let dataset = data.as_view();
3531
3532 let baseline = if should_overwrite() {
3533 let baseline = generate_baseline(&*quantizer, transform, pre_scale, dataset);
3534 save_baseline(&baseline);
3535 baseline
3536 } else {
3537 load_baseline(NBITS, metric.into(), transform, pre_scale)
3538 };
3539 check_baseline(&baseline, dataset, transform, pre_scale);
3540 }
3541
3542 #[test]
3547 fn compat_1bit_l2() {
3548 run_compatibility_test::<1>(
3549 SupportedMetric::SquaredL2,
3550 DataTransform::DoubleHadamard,
3551 ScaleConfig::None,
3552 );
3553 }
3554
3555 #[test]
3556 fn compat_1bit_ip() {
3557 run_compatibility_test::<1>(
3558 SupportedMetric::InnerProduct,
3559 DataTransform::DoubleHadamard,
3560 ScaleConfig::None,
3561 );
3562 }
3563
3564 #[test]
3565 fn compat_1bit_cosine() {
3566 run_compatibility_test::<1>(
3567 SupportedMetric::Cosine,
3568 DataTransform::DoubleHadamard,
3569 ScaleConfig::None,
3570 );
3571 }
3572
3573 #[test]
3574 fn compat_2bit_l2() {
3575 run_compatibility_test::<2>(
3576 SupportedMetric::SquaredL2,
3577 DataTransform::DoubleHadamard,
3578 ScaleConfig::None,
3579 );
3580 }
3581
3582 #[test]
3583 fn compat_2bit_ip() {
3584 run_compatibility_test::<2>(
3585 SupportedMetric::InnerProduct,
3586 DataTransform::DoubleHadamard,
3587 ScaleConfig::None,
3588 );
3589 }
3590
3591 #[test]
3592 fn compat_2bit_cosine() {
3593 run_compatibility_test::<2>(
3594 SupportedMetric::Cosine,
3595 DataTransform::DoubleHadamard,
3596 ScaleConfig::None,
3597 );
3598 }
3599
3600 #[test]
3601 fn compat_4bit_l2() {
3602 run_compatibility_test::<4>(
3603 SupportedMetric::SquaredL2,
3604 DataTransform::DoubleHadamard,
3605 ScaleConfig::None,
3606 );
3607 }
3608
3609 #[test]
3610 fn compat_4bit_ip() {
3611 run_compatibility_test::<4>(
3612 SupportedMetric::InnerProduct,
3613 DataTransform::DoubleHadamard,
3614 ScaleConfig::None,
3615 );
3616 }
3617
3618 #[test]
3619 fn compat_4bit_cosine() {
3620 run_compatibility_test::<4>(
3621 SupportedMetric::Cosine,
3622 DataTransform::DoubleHadamard,
3623 ScaleConfig::None,
3624 );
3625 }
3626
3627 #[test]
3628 fn compat_8bit_l2() {
3629 run_compatibility_test::<8>(
3630 SupportedMetric::SquaredL2,
3631 DataTransform::DoubleHadamard,
3632 ScaleConfig::None,
3633 );
3634 }
3635
3636 #[test]
3637 fn compat_8bit_ip() {
3638 run_compatibility_test::<8>(
3639 SupportedMetric::InnerProduct,
3640 DataTransform::DoubleHadamard,
3641 ScaleConfig::None,
3642 );
3643 }
3644
3645 #[test]
3646 fn compat_8bit_cosine() {
3647 run_compatibility_test::<8>(
3648 SupportedMetric::Cosine,
3649 DataTransform::DoubleHadamard,
3650 ScaleConfig::None,
3651 );
3652 }
3653
3654 #[test]
3656 fn compat_4bit_l2_null() {
3657 run_compatibility_test::<4>(
3658 SupportedMetric::SquaredL2,
3659 DataTransform::Null,
3660 ScaleConfig::None,
3661 );
3662 }
3663
3664 #[test]
3665 fn compat_4bit_l2_padding_hadamard() {
3666 run_compatibility_test::<4>(
3667 SupportedMetric::SquaredL2,
3668 DataTransform::PaddingHadamard,
3669 ScaleConfig::None,
3670 );
3671 }
3672
3673 #[test]
3675 fn compat_4bit_l2_prescale_rmn() {
3676 run_compatibility_test::<4>(
3677 SupportedMetric::SquaredL2,
3678 DataTransform::DoubleHadamard,
3679 ScaleConfig::ReciprocalMeanNorm,
3680 );
3681 }
3682 }
3683}