1use std::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
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 std::fmt::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 {
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> + 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
698pub(super) struct Reify<T, M, L, R> {
704 inner: T,
705 dim: usize,
706 arch: M,
707 _markers: PhantomData<(L, R)>,
708}
709
710impl<T, M, L, R> Reify<T, M, L, R> {
711 pub(super) fn new(inner: T, dim: usize, arch: M) -> Self {
712 Self {
713 inner,
714 dim,
715 arch,
716 _markers: PhantomData,
717 }
718 }
719}
720
721impl<M, T, R> DynQueryComputer for Reify<T, M, (), R>
722where
723 M: Architecture,
724 R: FromOpaque,
725 T: ReportQueryLayout + Send + Sync,
726 for<'a> &'a T: Target1<M, Rf32, R::Target<'a>>,
727{
728 fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
729 self.arch.run2(
730 |this: &Self, x| {
731 let x = R::from_opaque(x, this.dim)
732 .map_err(|err| QueryDistanceError::XReify(InlineError::new(err)))?;
733 this.arch
734 .run1(&this.inner, x)
735 .map_err(QueryDistanceError::UnequalLengths)
736 },
737 self,
738 x,
739 )
740 }
741
742 fn layout(&self) -> QueryLayout {
743 self.inner.report_query_layout()
744 }
745}
746
747impl<T, M, Q, R> DynDistanceComputer for Reify<T, M, Q, R>
748where
749 M: Architecture,
750 Q: FromOpaque + Default + ReportQueryLayout,
751 R: FromOpaque,
752 T: for<'a> Target2<M, Rf32, Q::Target<'a>, R::Target<'a>> + Copy + Send + Sync,
753{
754 fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
755 self.arch.run3(
756 |this: &Self, query, x| {
757 let query = Q::from_opaque(query, this.dim)
758 .map_err(|err| DistanceError::QueryReify(InlineError::<24>::new(err)))?;
759
760 let x = R::from_opaque(x, this.dim)
761 .map_err(|err| DistanceError::XReify(InlineError::<16>::new(err)))?;
762
763 this.arch
764 .run2_inline(this.inner, query, x)
765 .map_err(DistanceError::UnequalLengths)
766 },
767 self,
768 query,
769 x,
770 )
771 }
772
773 fn layout(&self) -> QueryLayout {
774 Q::default().report_query_layout()
775 }
776}
777
778#[derive(Debug, Error)]
784pub enum QueryDistanceError {
785 #[error("trouble trying to reify the argument")]
787 XReify(#[source] InlineError<16>),
788
789 #[error("encountered while trying to compute distances")]
791 UnequalLengths(#[source] UnequalLengths),
792}
793
794pub trait DynQueryComputer: Send + Sync {
795 fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError>;
796 fn layout(&self) -> QueryLayout;
797}
798
799pub struct QueryComputer<A = GlobalAllocator>
808where
809 A: AllocatorCore,
810{
811 inner: Poly<dyn DynQueryComputer, A>,
812}
813
814impl<A> QueryComputer<A>
815where
816 A: AllocatorCore,
817{
818 fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
819 where
820 T: DynQueryComputer + 'static,
821 {
822 let inner = Poly::new(inner, allocator)?;
823 Ok(Self {
824 inner: poly!(DynQueryComputer, inner),
825 })
826 }
827
828 pub fn layout(&self) -> QueryLayout {
830 self.inner.layout()
831 }
832
833 pub fn into_inner(self) -> Poly<dyn DynQueryComputer, A> {
835 self.inner
836 }
837}
838
839impl<A> std::fmt::Debug for QueryComputer<A>
840where
841 A: AllocatorCore,
842{
843 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
844 write!(
845 f,
846 "dynamic fused query computer with layout \"{}\"",
847 self.layout()
848 )
849 }
850}
851
852impl<A> PreprocessedDistanceFunction<Opaque<'_>, Result<f32, QueryDistanceError>>
853 for QueryComputer<A>
854where
855 A: AllocatorCore,
856{
857 fn evaluate_similarity(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
858 self.inner.evaluate(x)
859 }
860}
861
862pub(super) struct Curried<D, Q> {
869 inner: D,
870 query: Q,
871}
872
873impl<D, Q> Curried<D, Q> {
874 pub(super) fn new(inner: D, query: Q) -> Self {
875 Self { inner, query }
876 }
877}
878
879impl<A, D, Q, T, R> Target1<A, R, T> for &Curried<D, Q>
880where
881 A: Architecture,
882 Q: for<'a> Reborrow<'a>,
883 D: for<'a> Target2<A, R, <Q as Reborrow<'a>>::Target, T> + Copy,
884{
885 fn run(self, arch: A, x: T) -> R {
886 self.inner.run(arch, self.query.reborrow(), x)
887 }
888}
889
890#[derive(Debug, Error)]
896pub enum DistanceError {
897 #[error("trouble trying to reify the left-hand argument")]
899 QueryReify(InlineError<24>),
900
901 #[error("trouble trying to reify the right-hand argument")]
903 XReify(InlineError<16>),
904
905 #[error("encountered while trying to compute distances")]
909 UnequalLengths(UnequalLengths),
910}
911
912pub trait DynDistanceComputer: Send + Sync {
913 fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError>;
914 fn layout(&self) -> QueryLayout;
915}
916
917pub struct DistanceComputer<A = GlobalAllocator>
928where
929 A: AllocatorCore,
930{
931 inner: Poly<dyn DynDistanceComputer, A>,
932}
933
934impl<A> DistanceComputer<A>
935where
936 A: AllocatorCore,
937{
938 pub(super) fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
939 where
940 T: DynDistanceComputer + 'static,
941 {
942 let inner = Poly::new(inner, allocator)?;
943 Ok(Self {
944 inner: poly!(DynDistanceComputer, inner),
945 })
946 }
947
948 pub fn layout(&self) -> QueryLayout {
950 self.inner.layout()
951 }
952
953 pub fn into_inner(self) -> Poly<dyn DynDistanceComputer, A> {
954 self.inner
955 }
956}
957
958impl<A> std::fmt::Debug for DistanceComputer<A>
959where
960 A: AllocatorCore,
961{
962 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
963 write!(
964 f,
965 "dynamic distance computer with layout \"{}\"",
966 self.layout()
967 )
968 }
969}
970
971impl<A> DistanceFunction<Opaque<'_>, Opaque<'_>, Result<f32, DistanceError>> for DistanceComputer<A>
972where
973 A: AllocatorCore,
974{
975 fn evaluate_similarity(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
976 self.inner.evaluate(query, x)
977 }
978}
979
980#[cfg(all(not(test), feature = "flatbuffers"))]
986const DEFAULT_SERIALIZED_BYTES: usize = 1024;
987
988#[cfg(all(test, feature = "flatbuffers"))]
990const DEFAULT_SERIALIZED_BYTES: usize = 1;
991
992pub struct Impl<const NBITS: usize, A = GlobalAllocator>
995where
996 A: Allocator,
997{
998 quantizer: SphericalQuantizer<A>,
999 distance: Poly<dyn DynDistanceComputer, A>,
1000}
1001
1002pub trait Constructible<A = GlobalAllocator>
1005where
1006 A: Allocator,
1007{
1008 fn dispatch_distance(
1009 quantizer: &SphericalQuantizer<A>,
1010 ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError>;
1011}
1012
1013impl<const NBITS: usize, A: Allocator> Constructible<A> for Impl<NBITS, A>
1014where
1015 A: Allocator,
1016 AsData<NBITS>: FromOpaque,
1017 SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
1018{
1019 fn dispatch_distance(
1020 quantizer: &SphericalQuantizer<A>,
1021 ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError> {
1022 diskann_wide::arch::dispatch2_no_features(
1023 ComputerDispatcher::<AsData<NBITS>, NBITS>::new(),
1024 quantizer,
1025 quantizer.allocator().clone(),
1026 )
1027 .map(|obj| obj.inner)
1028 }
1029}
1030
1031impl<const NBITS: usize, A> TryClone for Impl<NBITS, A>
1032where
1033 A: Allocator,
1034 AsData<NBITS>: FromOpaque,
1035 SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
1036{
1037 fn try_clone(&self) -> Result<Self, AllocatorError> {
1038 Self::new(self.quantizer.try_clone()?)
1039 }
1040}
1041
1042impl<const NBITS: usize, A: Allocator> Impl<NBITS, A> {
1043 pub fn new(quantizer: SphericalQuantizer<A>) -> Result<Self, AllocatorError>
1045 where
1046 Self: Constructible<A>,
1047 {
1048 let distance = Self::dispatch_distance(&quantizer)?;
1049 Ok(Self {
1050 quantizer,
1051 distance,
1052 })
1053 }
1054
1055 pub fn quantizer(&self) -> &SphericalQuantizer<A> {
1057 &self.quantizer
1058 }
1059
1060 pub fn supports(layout: QueryLayout) -> bool {
1064 if const { NBITS == 1 } {
1065 [
1066 QueryLayout::SameAsData,
1067 QueryLayout::FourBitTransposed,
1068 QueryLayout::FullPrecision,
1069 ]
1070 .contains(&layout)
1071 } else {
1072 [
1073 QueryLayout::SameAsData,
1074 QueryLayout::ScalarQuantized,
1075 QueryLayout::FullPrecision,
1076 ]
1077 .contains(&layout)
1078 }
1079 }
1080
1081 fn query_computer<Q, B>(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError>
1084 where
1085 Q: FromOpaque,
1086 B: AllocatorCore,
1087 SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1088 {
1089 diskann_wide::arch::dispatch2_no_features(
1090 ComputerDispatcher::<Q, NBITS>::new(),
1091 &self.quantizer,
1092 allocator,
1093 )
1094 }
1095
1096 fn compress_query<'a, T>(
1097 &self,
1098 query: &'a [f32],
1099 storage: T,
1100 scratch: ScopedAllocator<'a>,
1101 ) -> Result<(), QueryCompressionError>
1102 where
1103 SphericalQuantizer<A>: CompressIntoWith<&'a [f32], T, ScopedAllocator<'a>, Error = quantizer::CompressionError>,
1104 {
1105 self.quantizer
1106 .compress_into_with(query, storage, scratch)
1107 .map_err(|err| CompressionError::CompressionError(err).into())
1108 }
1109
1110 fn fused_query_computer<Q, T, B>(
1113 &self,
1114 query: &[f32],
1115 mut storage: T,
1116 allocator: B,
1117 scratch: ScopedAllocator<'_>,
1118 ) -> Result<QueryComputer<B>, QueryComputerError>
1119 where
1120 Q: FromOpaque,
1121 T: for<'a> ReborrowMut<'a>
1122 + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1123 + ReportQueryLayout
1124 + Send
1125 + Sync
1126 + 'static,
1127 B: AllocatorCore,
1128 SphericalQuantizer<A>: for<'a> CompressIntoWith<
1129 &'a [f32],
1130 <T as ReborrowMut<'a>>::Target,
1131 ScopedAllocator<'a>,
1132 Error = quantizer::CompressionError,
1133 >,
1134 SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1135 {
1136 if let Err(err) = self
1137 .quantizer
1138 .compress_into_with(query, storage.reborrow_mut(), scratch)
1139 {
1140 return Err(CompressionError::CompressionError(err).into());
1141 }
1142
1143 diskann_wide::arch::dispatch3_no_features(
1144 ComputerDispatcher::<Q, NBITS>::new(),
1145 &self.quantizer,
1146 storage,
1147 allocator,
1148 )
1149 .map_err(|e| e.into())
1150 }
1151
1152 #[cfg(feature = "flatbuffers")]
1154 #[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
1155 pub fn try_deserialize(data: &[u8], alloc: A) -> Result<Self, DeserializationError>
1156 where
1157 Self: Constructible<A>,
1158 {
1159 if !fb::spherical::quantizer_buffer_has_identifier(data) {
1161 return Err(DeserializationError::InvalidIdentifier);
1162 }
1163
1164 let root = fb::spherical::root_as_quantizer(data)?;
1168 let nbits = root.nbits();
1169 let proto = root.quantizer();
1170
1171 if nbits as usize == NBITS {
1172 Self::try_deserialize_from(proto, alloc)
1173 } else {
1174 Err(DeserializationError::UnsupportedBitWidth(nbits))
1175 }
1176 }
1177
1178 #[cfg(feature = "flatbuffers")]
1179 fn try_deserialize_from(
1180 proto: fb::spherical::SphericalQuantizer<'_>,
1181 alloc: A,
1182 ) -> Result<Self, DeserializationError>
1183 where
1184 Self: Constructible<A>,
1185 {
1186 let quantizer = SphericalQuantizer::try_unpack(alloc, proto)?;
1187 Ok(Self::new(quantizer)?)
1188 }
1189
1190 #[cfg(feature = "flatbuffers")]
1191 fn serialize<B>(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError>
1192 where
1193 B: Allocator + std::panic::UnwindSafe,
1194 A: std::panic::RefUnwindSafe,
1195 {
1196 let mut buf = FlatBufferBuilder::new_in(Poly::broadcast(
1197 0u8,
1198 DEFAULT_SERIALIZED_BYTES,
1199 allocator.clone(),
1200 )?);
1201
1202 let quantizer = &self.quantizer;
1203
1204 let (root, mut buf) = match std::panic::catch_unwind(move || {
1205 let offset = quantizer.pack(&mut buf);
1206
1207 let root = fb::spherical::Quantizer::create(
1208 &mut buf,
1209 &fb::spherical::QuantizerArgs {
1210 quantizer: Some(offset),
1211 nbits: NBITS as u32,
1212 },
1213 );
1214 (root, buf)
1215 }) {
1216 Ok(ret) => ret,
1217 Err(err) => match err.downcast_ref::<String>() {
1218 Some(msg) => {
1219 if msg.contains("AllocatorError") {
1220 return Err(AllocatorError);
1221 } else {
1222 std::panic::resume_unwind(err);
1223 }
1224 }
1225 None => std::panic::resume_unwind(err),
1226 },
1227 };
1228
1229 fb::spherical::finish_quantizer_buffer(&mut buf, root);
1231 Poly::from_iter(buf.finished_data().iter().copied(), allocator)
1232 }
1233}
1234
1235trait BuildComputer<M, Q, const N: usize>
1252where
1253 M: Architecture,
1254 Q: FromOpaque,
1255{
1256 fn build_computer<A>(
1261 &self,
1262 arch: M,
1263 allocator: A,
1264 ) -> Result<DistanceComputer<A>, AllocatorError>
1265 where
1266 A: AllocatorCore;
1267
1268 fn build_fused_computer<R, A>(
1273 &self,
1274 arch: M,
1275 query: R,
1276 allocator: A,
1277 ) -> Result<QueryComputer<A>, AllocatorError>
1278 where
1279 R: ReportQueryLayout + for<'a> Reborrow<'a, Target = Q::Target<'a>> + Send + Sync + 'static,
1280 A: AllocatorCore;
1281}
1282
1283fn identity<T>(x: T) -> T {
1284 x
1285}
1286
1287macro_rules! dispatch_map {
1288 ($N:literal, $Q:ty, $arch:ty) => {
1289 dispatch_map!($N, $Q, $arch, identity);
1290 };
1291 ($N:literal, $Q:ty, $arch:ty, $op:ident) => {
1292 impl<A> BuildComputer<$arch, $Q, $N> for SphericalQuantizer<A>
1293 where
1294 A: Allocator,
1295 {
1296 fn build_computer<B>(
1297 &self,
1298 input_arch: $arch,
1299 allocator: B,
1300 ) -> Result<DistanceComputer<B>, AllocatorError>
1301 where
1302 B: AllocatorCore,
1303 {
1304 type D = AsData<$N>;
1305
1306 let arch = ($op)(input_arch);
1308 let dim = self.output_dim();
1309 match self.metric() {
1310 SupportedMetric::SquaredL2 => {
1311 let reify = Reify::<CompensatedSquaredL2, _, $Q, D>::new(
1312 self.as_functor(),
1313 dim,
1314 arch,
1315 );
1316 DistanceComputer::new(reify, allocator)
1317 }
1318 SupportedMetric::InnerProduct => {
1319 let reify =
1320 Reify::<CompensatedIP, _, $Q, D>::new(self.as_functor(), dim, arch);
1321 DistanceComputer::new(reify, allocator)
1322 }
1323 SupportedMetric::Cosine => {
1324 let reify =
1325 Reify::<CompensatedCosine, _, $Q, D>::new(self.as_functor(), dim, arch);
1326 DistanceComputer::new(reify, allocator)
1327 }
1328 }
1329 }
1330
1331 fn build_fused_computer<R, B>(
1332 &self,
1333 input_arch: $arch,
1334 query: R,
1335 allocator: B,
1336 ) -> Result<QueryComputer<B>, AllocatorError>
1337 where
1338 R: ReportQueryLayout
1339 + for<'a> Reborrow<'a, Target = <$Q as FromOpaque>::Target<'a>>
1340 + Send
1341 + Sync
1342 + 'static,
1343 B: AllocatorCore,
1344 {
1345 type D = AsData<$N>;
1346 let arch = ($op)(input_arch);
1347 let dim = self.output_dim();
1348 match self.metric() {
1349 SupportedMetric::SquaredL2 => {
1350 let computer: CompensatedSquaredL2 = self.as_functor();
1351 let curried = Curried::new(computer, query);
1352 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1353 Ok(QueryComputer::new(reify, allocator)?)
1354 }
1355 SupportedMetric::InnerProduct => {
1356 let computer: CompensatedIP = self.as_functor();
1357 let curried = Curried::new(computer, query);
1358 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1359 Ok(QueryComputer::new(reify, allocator)?)
1360 }
1361 SupportedMetric::Cosine => {
1362 let computer: CompensatedCosine = self.as_functor();
1363 let curried = Curried::new(computer, query);
1364 let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1365 Ok(QueryComputer::new(reify, allocator)?)
1366 }
1367 }
1368 }
1369 }
1370 };
1371}
1372
1373dispatch_map!(1, AsFull, Scalar);
1374dispatch_map!(2, AsFull, Scalar);
1375dispatch_map!(4, AsFull, Scalar);
1376dispatch_map!(8, AsFull, Scalar);
1377
1378dispatch_map!(1, AsData<1>, Scalar);
1379dispatch_map!(2, AsData<2>, Scalar);
1380dispatch_map!(4, AsData<4>, Scalar);
1381dispatch_map!(8, AsData<8>, Scalar);
1382
1383dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Scalar);
1385dispatch_map!(2, AsQuery<2>, Scalar);
1386dispatch_map!(4, AsQuery<4>, Scalar);
1387dispatch_map!(8, AsQuery<8>, Scalar);
1388
1389cfg_if::cfg_if! {
1390 if #[cfg(target_arch = "x86_64")] {
1391 fn downcast_to_v3(arch: V4) -> V3 {
1392 arch.into()
1393 }
1394
1395 dispatch_map!(1, AsFull, V3);
1397 dispatch_map!(2, AsFull, V3);
1398 dispatch_map!(4, AsFull, V3);
1399 dispatch_map!(8, AsFull, V3);
1400
1401 dispatch_map!(1, AsData<1>, V3);
1402 dispatch_map!(2, AsData<2>, V3);
1403 dispatch_map!(4, AsData<4>, V3);
1404 dispatch_map!(8, AsData<8>, V3);
1405
1406 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V3);
1407 dispatch_map!(2, AsQuery<2>, V3);
1408 dispatch_map!(4, AsQuery<4>, V3);
1409 dispatch_map!(8, AsQuery<8>, V3);
1410
1411 dispatch_map!(1, AsFull, V4, downcast_to_v3);
1413 dispatch_map!(2, AsFull, V4, downcast_to_v3);
1414 dispatch_map!(4, AsFull, V4, downcast_to_v3);
1415 dispatch_map!(8, AsFull, V4, downcast_to_v3);
1416
1417 dispatch_map!(1, AsData<1>, V4, downcast_to_v3);
1418 dispatch_map!(2, AsData<2>, V4); dispatch_map!(4, AsData<4>, V4); dispatch_map!(8, AsData<8>, V4, downcast_to_v3);
1421
1422 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V4, downcast_to_v3);
1423 dispatch_map!(2, AsQuery<2>, V4); dispatch_map!(4, AsQuery<4>, V4); dispatch_map!(8, AsQuery<8>, V4, downcast_to_v3);
1426 } else if #[cfg(target_arch = "aarch64")] {
1427 fn downcast(arch: Neon) -> Scalar {
1428 arch.retarget()
1429 }
1430
1431 dispatch_map!(1, AsFull, Neon, downcast);
1432 dispatch_map!(2, AsFull, Neon, downcast);
1433 dispatch_map!(4, AsFull, Neon, downcast);
1434 dispatch_map!(8, AsFull, Neon, downcast);
1435
1436 dispatch_map!(1, AsData<1>, Neon, downcast);
1437 dispatch_map!(2, AsData<2>, Neon, downcast);
1438 dispatch_map!(4, AsData<4>, Neon, downcast);
1439 dispatch_map!(8, AsData<8>, Neon, downcast);
1440
1441 dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Neon, downcast);
1442 dispatch_map!(2, AsQuery<2>, Neon, downcast);
1443 dispatch_map!(4, AsQuery<4>, Neon, downcast);
1444 dispatch_map!(8, AsQuery<8>, Neon, downcast);
1445 }
1446}
1447
1448#[derive(Debug, Clone, Copy)]
1460struct ComputerDispatcher<Q, const N: usize> {
1461 _query_type: std::marker::PhantomData<Q>,
1462}
1463
1464impl<Q, const N: usize> ComputerDispatcher<Q, N> {
1465 fn new() -> Self {
1466 Self {
1467 _query_type: std::marker::PhantomData,
1468 }
1469 }
1470}
1471
1472impl<M, const N: usize, A, B, Q>
1473 diskann_wide::arch::Target2<
1474 M,
1475 Result<DistanceComputer<B>, AllocatorError>,
1476 &SphericalQuantizer<A>,
1477 B,
1478 > for ComputerDispatcher<Q, N>
1479where
1480 M: Architecture,
1481 A: Allocator,
1482 B: AllocatorCore,
1483 Q: FromOpaque,
1484 SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1485{
1486 fn run(
1487 self,
1488 arch: M,
1489 quantizer: &SphericalQuantizer<A>,
1490 allocator: B,
1491 ) -> Result<DistanceComputer<B>, AllocatorError> {
1492 quantizer.build_computer(arch, allocator)
1493 }
1494}
1495
1496impl<M, const N: usize, A, R, B, Q>
1497 diskann_wide::arch::Target3<
1498 M,
1499 Result<QueryComputer<B>, AllocatorError>,
1500 &SphericalQuantizer<A>,
1501 R,
1502 B,
1503 > for ComputerDispatcher<Q, N>
1504where
1505 M: Architecture,
1506 A: Allocator,
1507 B: AllocatorCore,
1508 Q: FromOpaque,
1509 R: ReportQueryLayout + for<'a> Reborrow<'a, Target = Q::Target<'a>> + Send + Sync + 'static,
1510 SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1511{
1512 fn run(
1513 self,
1514 arch: M,
1515 quantizer: &SphericalQuantizer<A>,
1516 query: R,
1517 allocator: B,
1518 ) -> Result<QueryComputer<B>, AllocatorError> {
1519 quantizer.build_fused_computer(arch, query, allocator)
1520 }
1521}
1522
1523#[cfg(target_arch = "x86_64")]
1524trait Dispatchable<Q, const N: usize>:
1525 BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>
1526where
1527 Q: FromOpaque,
1528{
1529}
1530
1531#[cfg(target_arch = "x86_64")]
1532impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1533where
1534 Q: FromOpaque,
1535 T: BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>,
1536{
1537}
1538
1539#[cfg(target_arch = "aarch64")]
1540trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>
1541where
1542 Q: FromOpaque,
1543{
1544}
1545
1546#[cfg(target_arch = "aarch64")]
1547impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1548where
1549 Q: FromOpaque,
1550 T: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>,
1551{
1552}
1553
1554#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1555trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N>
1556where
1557 Q: FromOpaque,
1558{
1559}
1560
1561#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1562impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1563where
1564 Q: FromOpaque,
1565 T: BuildComputer<Scalar, Q, N>,
1566{
1567}
1568
1569impl<A, B> Quantizer<B> for Impl<1, A>
1574where
1575 A: Allocator + std::panic::RefUnwindSafe + Send + Sync + 'static,
1576 B: Allocator + std::panic::UnwindSafe + Send + Sync + 'static,
1577{
1578 fn nbits(&self) -> usize {
1579 1
1580 }
1581
1582 fn dim(&self) -> usize {
1583 self.quantizer.output_dim()
1584 }
1585
1586 fn full_dim(&self) -> usize {
1587 self.quantizer.input_dim()
1588 }
1589
1590 fn bytes(&self) -> usize {
1591 DataRef::<1>::canonical_bytes(self.quantizer.output_dim())
1592 }
1593
1594 fn distance_computer(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError> {
1595 self.query_computer::<AsData<1>, _>(allocator)
1596 }
1597
1598 fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1599 &*self.distance
1600 }
1601
1602 fn query_computer(
1603 &self,
1604 layout: QueryLayout,
1605 allocator: B,
1606 ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1607 match layout {
1608 QueryLayout::SameAsData => Ok(self.query_computer::<AsData<1>, _>(allocator)?),
1609 QueryLayout::FourBitTransposed => {
1610 Ok(self.query_computer::<AsQuery<4, bits::BitTranspose>, _>(allocator)?)
1611 }
1612 QueryLayout::ScalarQuantized => {
1613 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1614 }
1615 QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1616 }
1617 }
1618
1619 fn query_buffer_description(
1620 &self,
1621 layout: QueryLayout,
1622 ) -> Result<QueryBufferDescription, UnsupportedQueryLayout> {
1623 let dim = <Self as Quantizer<B>>::dim(self);
1624 match layout {
1625 QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1626 DataRef::<1>::canonical_bytes(dim),
1627 PowerOfTwo::alignment_of::<u8>(),
1628 )),
1629 QueryLayout::FourBitTransposed => Ok(QueryBufferDescription::new(
1630 QueryRef::<4, bits::BitTranspose>::canonical_bytes(dim),
1631 PowerOfTwo::alignment_of::<u8>(),
1632 )),
1633 QueryLayout::ScalarQuantized => {
1634 Err(UnsupportedQueryLayout::new(layout, "1-bit compression"))
1635 }
1636 QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1637 FullQueryRef::canonical_bytes(dim),
1638 FullQueryRef::canonical_align(),
1639 )),
1640 }
1641 }
1642
1643 fn compress_query(
1644 &self,
1645 x: &[f32],
1646 layout: QueryLayout,
1647 allow_rescale: bool,
1648 mut buffer: OpaqueMut<'_>,
1649 scratch: ScopedAllocator<'_>,
1650 ) -> Result<(), QueryCompressionError> {
1651 let dim = <Self as Quantizer<B>>::dim(self);
1652 let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1653 match layout {
1654 QueryLayout::SameAsData => self.compress_query(
1655 v,
1656 DataMut::<1>::from_canonical_back_mut(&mut buffer, dim)
1657 .map_err(NotCanonical::new)?,
1658 scratch,
1659 ),
1660 QueryLayout::FourBitTransposed => self.compress_query(
1661 v,
1662 QueryMut::<4, bits::BitTranspose>::from_canonical_back_mut(&mut buffer, dim)
1663 .map_err(NotCanonical::new)?,
1664 scratch,
1665 ),
1666 QueryLayout::ScalarQuantized => {
1667 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1668 }
1669 QueryLayout::FullPrecision => self.compress_query(
1670 v,
1671 FullQueryMut::from_canonical_mut(&mut buffer, dim)
1672 .map_err(NotCanonical::new)?,
1673 scratch,
1674 ),
1675 }
1676 };
1677
1678 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1679 let mut copy = x.to_owned();
1680 self.quantizer.rescale(&mut copy);
1681 finish(©)
1682 } else {
1683 finish(x)
1684 }
1685 }
1686
1687 fn fused_query_computer(
1688 &self,
1689 x: &[f32],
1690 layout: QueryLayout,
1691 allow_rescale: bool,
1692 allocator: B,
1693 scratch: ScopedAllocator<'_>,
1694 ) -> Result<QueryComputer<B>, QueryComputerError> {
1695 let dim = <Self as Quantizer<B>>::dim(self);
1696 let finish = |v: &[f32], allocator: B| -> Result<QueryComputer<B>, QueryComputerError> {
1697 match layout {
1698 QueryLayout::SameAsData => self.fused_query_computer::<AsData<1>, Data<1, _>, _>(
1699 v,
1700 Data::new_in(dim, allocator.clone())?,
1701 allocator,
1702 scratch,
1703 ),
1704 QueryLayout::FourBitTransposed => self
1705 .fused_query_computer::<AsQuery<4, bits::BitTranspose>, Query<4, bits::BitTranspose, _>, _>(
1706 v,
1707 Query::new_in(dim, allocator.clone())?,
1708 allocator,
1709 scratch,
1710 ),
1711 QueryLayout::ScalarQuantized => {
1712 Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1713 }
1714 QueryLayout::FullPrecision => self.fused_query_computer::<AsFull, FullQuery<_>, _>(
1715 v,
1716 FullQuery::empty(dim, allocator.clone())?,
1717 allocator,
1718 scratch,
1719 ),
1720 }
1721 };
1722
1723 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1724 let mut copy = x.to_owned();
1725 self.quantizer.rescale(&mut copy);
1726 finish(©, allocator)
1727 } else {
1728 finish(x, allocator)
1729 }
1730 }
1731
1732 fn is_supported(&self, layout: QueryLayout) -> bool {
1733 Self::supports(layout)
1734 }
1735
1736 fn compress(
1737 &self,
1738 x: &[f32],
1739 mut into: OpaqueMut<'_>,
1740 scratch: ScopedAllocator<'_>,
1741 ) -> Result<(), CompressionError> {
1742 let dim = <Self as Quantizer<B>>::dim(self);
1743 let into = DataMut::<1>::from_canonical_back_mut(into.inspect(), dim)
1744 .map_err(CompressionError::not_canonical)?;
1745 self.quantizer
1746 .compress_into_with(x, into, scratch)
1747 .map_err(CompressionError::CompressionError)
1748 }
1749
1750 fn metric(&self) -> SupportedMetric {
1751 self.quantizer.metric()
1752 }
1753
1754 fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1755 let clone = (*self).try_clone()?;
1756 poly!({ Quantizer<B> }, clone, allocator)
1757 }
1758
1759 #[cfg(feature = "flatbuffers")]
1760 fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1761 Impl::<1, A>::serialize(self, allocator)
1762 }
1763}
1764
1765macro_rules! plan {
1766 ($N:literal) => {
1767 impl<A, B> Quantizer<B> for Impl<$N, A>
1768 where
1769 A: Allocator + std::panic::RefUnwindSafe + Send + Sync + 'static,
1770 B: Allocator + std::panic::UnwindSafe + Send + Sync + 'static,
1771 {
1772 fn nbits(&self) -> usize {
1773 $N
1774 }
1775
1776 fn dim(&self) -> usize {
1777 self.quantizer.output_dim()
1778 }
1779
1780 fn full_dim(&self) -> usize {
1781 self.quantizer.input_dim()
1782 }
1783
1784 fn bytes(&self) -> usize {
1785 DataRef::<$N>::canonical_bytes(<Self as Quantizer<B>>::dim(self))
1786 }
1787
1788 fn distance_computer(
1789 &self,
1790 allocator: B
1791 ) -> Result<DistanceComputer<B>, AllocatorError> {
1792 self.query_computer::<AsData<$N>, _>(allocator)
1793 }
1794
1795 fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1796 &*self.distance
1797 }
1798
1799 fn query_computer(
1800 &self,
1801 layout: QueryLayout,
1802 allocator: B,
1803 ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1804 match layout {
1805 QueryLayout::SameAsData => Ok(self.query_computer::<AsData<$N>, _>(allocator)?)
1806 ,
1807 QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout::new(
1808 layout,
1809 concat!($N, "-bit compression"),
1810 ).into()),
1811 QueryLayout::ScalarQuantized => {
1812 Ok(self.query_computer::<AsQuery<$N, bits::Dense>, _>(allocator)?)
1813 },
1814 QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1815
1816 }
1817 }
1818
1819 fn query_buffer_description(
1820 &self,
1821 layout: QueryLayout
1822 ) -> Result<QueryBufferDescription, UnsupportedQueryLayout>
1823 {
1824 let dim = <Self as Quantizer<B>>::dim(self);
1825 match layout {
1826 QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1827 DataRef::<$N>::canonical_bytes(dim),
1828 PowerOfTwo::alignment_of::<u8>(),
1829 )),
1830 QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout {
1831 layout,
1832 desc: concat!($N, "-bit compression"),
1833 }),
1834 QueryLayout::ScalarQuantized => Ok(QueryBufferDescription::new(
1835 QueryRef::<$N, bits::Dense>::canonical_bytes(dim),
1836 PowerOfTwo::alignment_of::<u8>(),
1837 )),
1838 QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1839 FullQueryRef::canonical_bytes(dim),
1840 FullQueryRef::canonical_align(),
1841 )),
1842 }
1843 }
1844
1845 fn compress_query(
1846 &self,
1847 x: &[f32],
1848 layout: QueryLayout,
1849 allow_rescale: bool,
1850 mut buffer: OpaqueMut<'_>,
1851 scratch: ScopedAllocator<'_>,
1852 ) -> Result<(), QueryCompressionError> {
1853 let dim = <Self as Quantizer<B>>::dim(self);
1854 let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1855 match layout {
1856 QueryLayout::SameAsData => self.compress_query(
1857 v,
1858 DataMut::<$N>::from_canonical_back_mut(
1859 &mut buffer,
1860 dim,
1861 ).map_err(NotCanonical::new)?,
1862 scratch,
1863 ),
1864 QueryLayout::FourBitTransposed => {
1865 Err(UnsupportedQueryLayout::new(
1866 layout,
1867 concat!($N, "-bit compression"),
1868 ).into())
1869 },
1870 QueryLayout::ScalarQuantized => self.compress_query(
1871 v,
1872 QueryMut::<$N, bits::Dense>::from_canonical_back_mut(
1873 &mut buffer,
1874 dim,
1875 ).map_err(NotCanonical::new)?,
1876 scratch,
1877 ),
1878 QueryLayout::FullPrecision => self.compress_query(
1879 v,
1880 FullQueryMut::from_canonical_mut(
1881 &mut buffer,
1882 dim,
1883 ).map_err(NotCanonical::new)?,
1884 scratch,
1885 ),
1886 }
1887 };
1888
1889 if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1890 let mut copy = x.to_owned();
1891 self.quantizer.rescale(&mut copy);
1892 finish(©)
1893 } else {
1894 finish(x)
1895 }
1896 }
1897
1898 fn fused_query_computer(
1899 &self,
1900 x: &[f32],
1901 layout: QueryLayout,
1902 allow_rescale: bool,
1903 allocator: B,
1904 scratch: ScopedAllocator<'_>,
1905 ) -> Result<QueryComputer<B>, QueryComputerError>
1906 {
1907 let dim = <Self as Quantizer<B>>::dim(self);
1908 let finish = |v: &[f32]| -> Result<QueryComputer<B>, QueryComputerError> {
1909 match layout {
1910 QueryLayout::SameAsData => {
1911 self.fused_query_computer::<AsData<$N>, Data<$N, _>, B>(
1912 v,
1913 Data::new_in(dim, allocator.clone())?,
1914 allocator,
1915 scratch,
1916 )
1917 },
1918 QueryLayout::FourBitTransposed => {
1919 Err(UnsupportedQueryLayout::new(
1920 layout,
1921 concat!($N, "-bit compression"),
1922 ).into())
1923 },
1924 QueryLayout::ScalarQuantized => {
1925 self.fused_query_computer::<AsQuery<$N, bits::Dense>, Query<$N, bits::Dense, _>, B>(
1926 v,
1927 Query::new_in(dim, allocator.clone())?,
1928 allocator,
1929 scratch,
1930 )
1931 },
1932 QueryLayout::FullPrecision => {
1933 self.fused_query_computer::<AsFull, FullQuery<_>, B>(
1934 v,
1935 FullQuery::empty(dim, allocator.clone())?,
1936 allocator,
1937 scratch,
1938 )
1939 },
1940 }
1941 };
1942
1943 let metric = <Self as Quantizer<B>>::metric(self);
1944 if allow_rescale && metric == SupportedMetric::InnerProduct {
1945 let mut copy = x.to_owned();
1946 self.quantizer.rescale(&mut copy);
1947 finish(©)
1948 } else {
1949 finish(x)
1950 }
1951 }
1952
1953 fn is_supported(&self, layout: QueryLayout) -> bool {
1954 Self::supports(layout)
1955 }
1956
1957 fn compress(
1958 &self,
1959 x: &[f32],
1960 mut into: OpaqueMut<'_>,
1961 scratch: ScopedAllocator<'_>,
1962 ) -> Result<(), CompressionError> {
1963 let dim = <Self as Quantizer<B>>::dim(self);
1964 let into = DataMut::<$N>::from_canonical_back_mut(into.inspect(), dim)
1965 .map_err(CompressionError::not_canonical)?;
1966
1967 self.quantizer.compress_into_with(x, into, scratch)
1968 .map_err(CompressionError::CompressionError)
1969 }
1970
1971 fn metric(&self) -> SupportedMetric {
1972 self.quantizer.metric()
1973 }
1974
1975 fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1976 let clone = (&*self).try_clone()?;
1977 poly!({ Quantizer<B> }, clone, allocator)
1978 }
1979
1980 #[cfg(feature = "flatbuffers")]
1981 fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1982 Impl::<$N, A>::serialize(self, allocator)
1983 }
1984 }
1985 };
1986 ($N:literal, $($Ns:literal),*) => {
1987 plan!($N);
1988 $(plan!($Ns);)*
1989 }
1990}
1991
1992plan!(2, 4, 8);
1993
1994#[cfg(feature = "flatbuffers")]
1999#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
2000#[derive(Debug, Clone, Error)]
2001#[non_exhaustive]
2002pub enum DeserializationError {
2003 #[error("unhandled file identifier in flatbuffer")]
2004 InvalidIdentifier,
2005
2006 #[error("unsupported number of bits ({0})")]
2007 UnsupportedBitWidth(u32),
2008
2009 #[error(transparent)]
2010 InvalidQuantizer(#[from] super::quantizer::DeserializationError),
2011
2012 #[error(transparent)]
2013 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
2014
2015 #[error(transparent)]
2016 AllocatorError(#[from] AllocatorError),
2017}
2018
2019#[cfg(feature = "flatbuffers")]
2025#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
2026pub fn try_deserialize<O, A>(
2027 data: &[u8],
2028 alloc: A,
2029) -> Result<Poly<dyn Quantizer<O>, A>, DeserializationError>
2030where
2031 O: Allocator + std::panic::UnwindSafe + Send + Sync + 'static,
2032 A: Allocator + std::panic::RefUnwindSafe + Send + Sync + 'static,
2033{
2034 fn unpack_bits<'a, const NBITS: usize, O, A>(
2039 proto: fb::spherical::SphericalQuantizer<'_>,
2040 alloc: A,
2041 ) -> Result<Poly<dyn Quantizer<O> + 'a, A>, DeserializationError>
2042 where
2043 O: Allocator + Send + Sync + std::panic::UnwindSafe + 'static,
2044 A: Allocator + Send + Sync + 'a,
2045 Impl<NBITS, A>: Quantizer<O> + Constructible<A>,
2046 {
2047 let imp = match Poly::new_with(
2048 |alloc| Impl::<NBITS, A>::try_deserialize_from(proto, alloc),
2049 alloc,
2050 ) {
2051 Ok(imp) => imp,
2052 Err(CompoundError::Allocator(err)) => {
2053 return Err(err.into());
2054 }
2055 Err(CompoundError::Constructor(err)) => {
2056 return Err(err);
2057 }
2058 };
2059 Ok(poly!({ Quantizer<O> }, imp))
2060 }
2061
2062 if !fb::spherical::quantizer_buffer_has_identifier(data) {
2064 return Err(DeserializationError::InvalidIdentifier);
2065 }
2066
2067 let root = fb::spherical::root_as_quantizer(data)?;
2071 let nbits = root.nbits();
2072 let proto = root.quantizer();
2073
2074 match nbits {
2075 1 => unpack_bits::<1, _, _>(proto, alloc),
2076 2 => unpack_bits::<2, _, _>(proto, alloc),
2077 4 => unpack_bits::<4, _, _>(proto, alloc),
2078 8 => unpack_bits::<8, _, _>(proto, alloc),
2079 n => Err(DeserializationError::UnsupportedBitWidth(n)),
2080 }
2081}
2082
2083#[cfg(test)]
2088mod tests {
2089 use diskann_utils::views::{Matrix, MatrixView};
2090 use rand::{SeedableRng, rngs::StdRng};
2091
2092 use super::*;
2093 use crate::{
2094 algorithms::{TransformKind, transforms::TargetDim},
2095 alloc::{AlignedAllocator, GlobalAllocator, Poly},
2096 num::PowerOfTwo,
2097 spherical::PreScale,
2098 };
2099
2100 fn test_plan_1_bit(plan: &dyn Quantizer) {
2105 assert_eq!(
2106 plan.nbits(),
2107 1,
2108 "this test only applies to 1-bit quantization"
2109 );
2110
2111 for layout in QueryLayout::all() {
2113 match layout {
2114 QueryLayout::SameAsData
2115 | QueryLayout::FourBitTransposed
2116 | QueryLayout::FullPrecision => assert!(
2117 plan.is_supported(layout),
2118 "expected {} to be supported",
2119 layout
2120 ),
2121 QueryLayout::ScalarQuantized => assert!(
2122 !plan.is_supported(layout),
2123 "expected {} to not be supported",
2124 layout
2125 ),
2126 }
2127 }
2128 }
2129
2130 fn test_plan_n_bit(plan: &dyn Quantizer, nbits: usize) {
2131 assert_ne!(nbits, 1, "there is another test for 1-bit quantizers");
2132 assert_eq!(
2133 plan.nbits(),
2134 nbits,
2135 "this test only applies to 1-bit quantization"
2136 );
2137
2138 for layout in QueryLayout::all() {
2140 match layout {
2141 QueryLayout::SameAsData
2142 | QueryLayout::ScalarQuantized
2143 | QueryLayout::FullPrecision => assert!(
2144 plan.is_supported(layout),
2145 "expected {} to be supported",
2146 layout
2147 ),
2148 QueryLayout::FourBitTransposed => assert!(
2149 !plan.is_supported(layout),
2150 "expected {} to not be supported",
2151 layout
2152 ),
2153 }
2154 }
2155 }
2156
2157 #[inline(never)]
2158 fn test_plan(plan: &dyn Quantizer, nbits: usize, dataset: MatrixView<f32>) {
2159 if nbits == 1 {
2161 test_plan_1_bit(plan);
2162 } else {
2163 test_plan_n_bit(plan, nbits);
2164 }
2165
2166 assert_eq!(plan.full_dim(), dataset.ncols());
2168
2169 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2171 let mut a = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2172 let mut b = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2173 let scoped_global = ScopedAllocator::global();
2174
2175 plan.compress(dataset.row(0), OpaqueMut::new(&mut a), scoped_global)
2176 .unwrap();
2177 plan.compress(dataset.row(1), OpaqueMut::new(&mut b), scoped_global)
2178 .unwrap();
2179
2180 let f = plan.distance_computer(GlobalAllocator).unwrap();
2181 let _: f32 = f
2182 .evaluate_similarity(Opaque::new(&a), Opaque::new(&b))
2183 .unwrap();
2184
2185 let test_errors = |f: &dyn DynDistanceComputer| {
2186 let err = f
2188 .evaluate(Opaque::new(&a[..a.len() - 1]), Opaque::new(&b))
2189 .unwrap_err();
2190 assert!(matches!(err, DistanceError::QueryReify(_)));
2191
2192 let err = f
2194 .evaluate(Opaque::new(&vec![0u8; a.len() + 1]), Opaque::new(&b))
2195 .unwrap_err();
2196 assert!(matches!(err, DistanceError::QueryReify(_)));
2197
2198 let err = f
2200 .evaluate(Opaque::new(&a), Opaque::new(&b[..b.len() - 1]))
2201 .unwrap_err();
2202 assert!(matches!(err, DistanceError::XReify(_)));
2203
2204 let err = f
2206 .evaluate(Opaque::new(&a), Opaque::new(&vec![0u8; b.len() + 1]))
2207 .unwrap_err();
2208 assert!(matches!(err, DistanceError::XReify(_)));
2209 };
2210
2211 test_errors(&*f.inner);
2212
2213 let f = plan.distance_computer_ref();
2214 let _: f32 = f.evaluate(Opaque::new(&a), Opaque::new(&b)).unwrap();
2215 test_errors(f);
2216
2217 for layout in QueryLayout::all() {
2219 if !plan.is_supported(layout) {
2220 let check_message = |msg: &str| {
2221 assert!(
2222 msg.contains(&(layout.to_string())),
2223 "error message ({}) should contain the layout \"{}\"",
2224 msg,
2225 layout
2226 );
2227 assert!(
2228 msg.contains(&format!("{}", nbits)),
2229 "error message ({}) should contain the number of bits \"{}\"",
2230 msg,
2231 nbits
2232 );
2233 };
2234
2235 {
2237 let err = plan
2238 .fused_query_computer(
2239 dataset.row(1),
2240 layout,
2241 false,
2242 GlobalAllocator,
2243 scoped_global,
2244 )
2245 .unwrap_err();
2246
2247 let msg = err.to_string();
2248 check_message(&msg);
2249 }
2250
2251 {
2253 let err = plan.query_buffer_description(layout).unwrap_err();
2254 let msg = err.to_string();
2255 check_message(&msg);
2256 }
2257
2258 {
2260 let buffer = &mut [];
2261 let err = plan
2262 .compress_query(
2263 dataset.row(1),
2264 layout,
2265 true,
2266 OpaqueMut::new(buffer),
2267 scoped_global,
2268 )
2269 .unwrap_err();
2270 let msg = err.to_string();
2271 check_message(&msg);
2272 }
2273
2274 {
2276 let err = plan.query_computer(layout, GlobalAllocator).unwrap_err();
2277 let msg = err.to_string();
2278 check_message(&msg);
2279 }
2280
2281 continue;
2282 }
2283
2284 let g = plan
2285 .fused_query_computer(
2286 dataset.row(1),
2287 layout,
2288 false,
2289 GlobalAllocator,
2290 scoped_global,
2291 )
2292 .unwrap();
2293 assert_eq!(
2294 g.layout(),
2295 layout,
2296 "the query computer should faithfully preserve the requested layout"
2297 );
2298
2299 let direct: f32 = g.evaluate_similarity(Opaque(&a)).unwrap();
2300
2301 {
2303 let err = g
2304 .evaluate_similarity(Opaque::new(&a[..a.len() - 1]))
2305 .unwrap_err();
2306 assert!(matches!(err, QueryDistanceError::XReify(_)));
2307
2308 let err = g
2309 .evaluate_similarity(Opaque::new(&vec![0u8; a.len() + 1]))
2310 .unwrap_err();
2311 assert!(matches!(err, QueryDistanceError::XReify(_)));
2312 }
2313
2314 let sizes = plan.query_buffer_description(layout).unwrap();
2315 let mut buf =
2316 Poly::broadcast(0u8, sizes.bytes(), AlignedAllocator::new(sizes.align())).unwrap();
2317
2318 plan.compress_query(
2319 dataset.row(1),
2320 layout,
2321 false,
2322 OpaqueMut::new(&mut buf),
2323 scoped_global,
2324 )
2325 .unwrap();
2326
2327 let standalone = plan.query_computer(layout, GlobalAllocator).unwrap();
2328
2329 assert_eq!(
2330 standalone.layout(),
2331 layout,
2332 "the standalone computer did not preserve the requested layout",
2333 );
2334
2335 let indirect: f32 = standalone
2336 .evaluate_similarity(Opaque(&buf), Opaque(&a))
2337 .unwrap();
2338
2339 assert_eq!(
2340 direct, indirect,
2341 "the two different query computation APIs did not return the same result"
2342 );
2343
2344 let too_small = &dataset.row(0)[..dataset.ncols() - 1];
2346 assert!(
2347 plan.fused_query_computer(too_small, layout, false, GlobalAllocator, scoped_global)
2348 .is_err()
2349 );
2350 }
2351
2352 {
2354 let mut too_small = vec![u8::default(); plan.bytes() - 1];
2355 assert!(
2356 plan.compress(dataset.row(0), OpaqueMut(&mut too_small), scoped_global)
2357 .is_err()
2358 );
2359
2360 let mut too_big = vec![u8::default(); plan.bytes() + 1];
2361 assert!(
2362 plan.compress(dataset.row(0), OpaqueMut(&mut too_big), scoped_global)
2363 .is_err()
2364 );
2365
2366 let mut just_right = vec![u8::default(); plan.bytes()];
2367 assert!(
2368 plan.compress(
2369 &dataset.row(0)[..dataset.ncols() - 1],
2370 OpaqueMut(&mut just_right),
2371 scoped_global
2372 )
2373 .is_err()
2374 );
2375 }
2376 }
2377
2378 fn make_impl<const NBITS: usize>(metric: SupportedMetric) -> (Impl<NBITS>, Matrix<f32>)
2379 where
2380 Impl<NBITS>: Constructible,
2381 {
2382 let data = test_dataset();
2383 let mut rng = StdRng::seed_from_u64(0x7d535118722ff197);
2384
2385 let quantizer = SphericalQuantizer::train(
2386 data.as_view(),
2387 TransformKind::PaddingHadamard {
2388 target_dim: TargetDim::Natural,
2389 },
2390 metric,
2391 PreScale::None,
2392 &mut rng,
2393 GlobalAllocator,
2394 )
2395 .unwrap();
2396
2397 (Impl::<NBITS>::new(quantizer).unwrap(), data)
2398 }
2399
2400 #[test]
2401 fn test_plan_1bit_l2() {
2402 let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2403 test_plan(&plan, 1, data.as_view());
2404 }
2405
2406 #[test]
2407 fn test_plan_1bit_ip() {
2408 let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2409 test_plan(&plan, 1, data.as_view());
2410 }
2411
2412 #[test]
2413 fn test_plan_1bit_cosine() {
2414 let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2415 test_plan(&plan, 1, data.as_view());
2416 }
2417
2418 #[test]
2419 fn test_plan_2bit_l2() {
2420 let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2421 test_plan(&plan, 2, data.as_view());
2422 }
2423
2424 #[test]
2425 fn test_plan_2bit_ip() {
2426 let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2427 test_plan(&plan, 2, data.as_view());
2428 }
2429
2430 #[test]
2431 fn test_plan_2bit_cosine() {
2432 let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2433 test_plan(&plan, 2, data.as_view());
2434 }
2435
2436 #[test]
2437 fn test_plan_4bit_l2() {
2438 let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2439 test_plan(&plan, 4, data.as_view());
2440 }
2441
2442 #[test]
2443 fn test_plan_4bit_ip() {
2444 let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2445 test_plan(&plan, 4, data.as_view());
2446 }
2447
2448 #[test]
2449 fn test_plan_4bit_cosine() {
2450 let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2451 test_plan(&plan, 4, data.as_view());
2452 }
2453
2454 #[test]
2455 fn test_plan_8bit_l2() {
2456 let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2457 test_plan(&plan, 8, data.as_view());
2458 }
2459
2460 #[test]
2461 fn test_plan_8bit_ip() {
2462 let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2463 test_plan(&plan, 8, data.as_view());
2464 }
2465
2466 #[test]
2467 fn test_plan_8bit_cosine() {
2468 let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2469 test_plan(&plan, 8, data.as_view());
2470 }
2471
2472 fn test_dataset() -> Matrix<f32> {
2473 let data = vec![
2474 0.28657,
2475 -0.0318168,
2476 0.0666847,
2477 0.0329265,
2478 -0.00829283,
2479 0.168735,
2480 -0.000846311,
2481 -0.360779, -0.0968938,
2483 0.161921,
2484 -0.0979579,
2485 0.102228,
2486 -0.259928,
2487 -0.139634,
2488 0.165384,
2489 -0.293443, 0.130205,
2491 0.265737,
2492 0.401816,
2493 -0.407552,
2494 0.13012,
2495 -0.0475244,
2496 0.511723,
2497 -0.4372, -0.0979126,
2499 0.135861,
2500 -0.0154144,
2501 -0.14047,
2502 -0.0250029,
2503 -0.190279,
2504 0.407283,
2505 -0.389184, -0.264153,
2507 0.0696822,
2508 -0.145585,
2509 0.370284,
2510 0.186825,
2511 -0.140736,
2512 0.274703,
2513 -0.334563, 0.247613,
2515 0.513165,
2516 -0.0845867,
2517 0.0532264,
2518 -0.00480601,
2519 -0.122408,
2520 0.47227,
2521 -0.268301, 0.103198,
2523 0.30756,
2524 -0.316293,
2525 -0.0686877,
2526 -0.330729,
2527 -0.461997,
2528 0.550857,
2529 -0.240851, 0.128258,
2531 0.786291,
2532 -0.0268103,
2533 0.111763,
2534 -0.308962,
2535 -0.17407,
2536 0.437154,
2537 -0.159879, 0.00374063,
2539 0.490301,
2540 0.0327826,
2541 -0.0340962,
2542 -0.118605,
2543 0.163879,
2544 0.2737,
2545 -0.299942, -0.284077,
2547 0.249377,
2548 -0.0307734,
2549 -0.0661631,
2550 0.233854,
2551 0.427987,
2552 0.614132,
2553 -0.288649, -0.109492,
2555 0.203939,
2556 -0.73956,
2557 -0.130748,
2558 0.22072,
2559 0.0647836,
2560 0.328726,
2561 -0.374602, -0.223114,
2563 0.0243489,
2564 0.109195,
2565 -0.416914,
2566 0.0201052,
2567 -0.0190542,
2568 0.947078,
2569 -0.333229, -0.165869,
2571 -0.00296729,
2572 -0.414378,
2573 0.231321,
2574 0.205365,
2575 0.161761,
2576 0.148608,
2577 -0.395063, -0.0498255,
2579 0.193279,
2580 -0.110946,
2581 -0.181174,
2582 -0.274578,
2583 -0.227511,
2584 0.190208,
2585 -0.256174, -0.188106,
2587 -0.0292958,
2588 0.0930939,
2589 0.0558456,
2590 0.257437,
2591 0.685481,
2592 0.307922,
2593 -0.320006, 0.250035,
2595 0.275942,
2596 -0.0856306,
2597 -0.352027,
2598 -0.103509,
2599 -0.00890859,
2600 0.276121,
2601 -0.324718, ];
2603
2604 Matrix::try_from(data.into(), 16, 8).unwrap()
2605 }
2606
2607 #[cfg(feature = "flatbuffers")]
2608 mod serialization {
2609 use std::sync::{
2610 Arc,
2611 atomic::{AtomicBool, Ordering},
2612 };
2613
2614 use super::*;
2615 use crate::alloc::{BumpAllocator, GlobalAllocator};
2616
2617 fn test_deserialization_inner(
2618 quantizer: &dyn Quantizer,
2619 deserialized: &dyn Quantizer,
2620 nbits: usize,
2621 dataset: MatrixView<'_, f32>,
2622 ) {
2623 let scoped_global = ScopedAllocator::global();
2624
2625 assert_eq!(quantizer.nbits(), nbits);
2626 assert_eq!(deserialized.nbits(), nbits);
2627 assert_eq!(deserialized.bytes(), quantizer.bytes());
2628 assert_eq!(deserialized.dim(), quantizer.dim());
2629 assert_eq!(deserialized.full_dim(), quantizer.full_dim());
2630 assert_eq!(deserialized.metric(), quantizer.metric());
2631
2632 for layout in QueryLayout::all() {
2633 assert_eq!(
2634 deserialized.is_supported(layout),
2635 quantizer.is_supported(layout)
2636 );
2637 }
2638
2639 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2641 {
2642 let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2643 let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2644
2645 for row in dataset.row_iter() {
2646 quantizer
2647 .compress(row, OpaqueMut::new(&mut a), scoped_global)
2648 .unwrap();
2649 deserialized
2650 .compress(row, OpaqueMut::new(&mut b), scoped_global)
2651 .unwrap();
2652
2653 assert_eq!(a, b);
2655 }
2656 }
2657
2658 {
2660 let mut a0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2661 let mut a1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2662 let mut b0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2663 let mut b1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2664
2665 let q_computer = quantizer.distance_computer(GlobalAllocator).unwrap();
2666 let q_computer_ref = quantizer.distance_computer_ref();
2667 let d_computer = deserialized.distance_computer(GlobalAllocator).unwrap();
2668 let d_computer_ref = deserialized.distance_computer_ref();
2669
2670 for r0 in dataset.row_iter() {
2671 quantizer
2672 .compress(r0, OpaqueMut::new(&mut a0), scoped_global)
2673 .unwrap();
2674 deserialized
2675 .compress(r0, OpaqueMut::new(&mut b0), scoped_global)
2676 .unwrap();
2677 for r1 in dataset.row_iter() {
2678 quantizer
2679 .compress(r1, OpaqueMut::new(&mut a1), scoped_global)
2680 .unwrap();
2681 deserialized
2682 .compress(r1, OpaqueMut::new(&mut b1), scoped_global)
2683 .unwrap();
2684
2685 let a0 = Opaque::new(&a0);
2686 let a1 = Opaque::new(&a1);
2687
2688 let q_computer_dist = q_computer.evaluate_similarity(a0, a1).unwrap();
2689 let d_computer_dist = d_computer.evaluate_similarity(a0, a1).unwrap();
2690
2691 assert_eq!(q_computer_dist, d_computer_dist);
2692
2693 let q_computer_ref_dist = q_computer_ref.evaluate(a0, a1).unwrap();
2694
2695 assert_eq!(q_computer_dist, q_computer_ref_dist);
2696
2697 let d_computer_ref_dist = d_computer_ref.evaluate(a0, a1).unwrap();
2698 assert_eq!(d_computer_dist, d_computer_ref_dist);
2699 }
2700 }
2701 }
2702
2703 {
2705 let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2706 let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2707
2708 for layout in QueryLayout::all() {
2709 if !quantizer.is_supported(layout) {
2710 continue;
2711 }
2712
2713 for r in dataset.row_iter() {
2714 let q_computer = quantizer
2715 .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2716 .unwrap();
2717 let d_computer = deserialized
2718 .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2719 .unwrap();
2720
2721 for u in dataset.row_iter() {
2722 quantizer
2723 .compress(u, OpaqueMut::new(&mut a), scoped_global)
2724 .unwrap();
2725 deserialized
2726 .compress(u, OpaqueMut::new(&mut b), scoped_global)
2727 .unwrap();
2728
2729 assert_eq!(
2730 q_computer.evaluate_similarity(Opaque::new(&a)).unwrap(),
2731 d_computer.evaluate_similarity(Opaque::new(&b)).unwrap(),
2732 );
2733 }
2734 }
2735 }
2736 }
2737 }
2738
2739 #[inline(never)]
2740 fn test_plan_serialization(
2741 quantizer: &dyn Quantizer,
2742 nbits: usize,
2743 dataset: MatrixView<f32>,
2744 ) {
2745 let global = GlobalAllocator;
2746
2747 assert_eq!(quantizer.full_dim(), dataset.ncols());
2749
2750 let serialized = quantizer.serialize(global).unwrap();
2751 let deserialized = try_deserialize::<GlobalAllocator, _>(&serialized, global).unwrap();
2752
2753 test_deserialization_inner(quantizer, &*deserialized, nbits, dataset);
2754
2755 match nbits {
2757 1 => {
2758 test_deserialization_inner(
2759 quantizer,
2760 &Impl::<1, _>::try_deserialize(&serialized, global).unwrap(),
2761 nbits,
2762 dataset,
2763 );
2764
2765 assert!(matches!(
2767 Impl::<2, _>::try_deserialize(&serialized, global),
2768 Err(DeserializationError::UnsupportedBitWidth(1)),
2769 ));
2770 }
2771 2 => {
2772 test_deserialization_inner(
2773 quantizer,
2774 &Impl::<2, _>::try_deserialize(&serialized, global).unwrap(),
2775 nbits,
2776 dataset,
2777 );
2778
2779 assert!(matches!(
2781 Impl::<1, _>::try_deserialize(&serialized, global),
2782 Err(DeserializationError::UnsupportedBitWidth(2)),
2783 ));
2784 }
2785 4 => {
2786 test_deserialization_inner(
2787 quantizer,
2788 &Impl::<4, _>::try_deserialize(&serialized, global).unwrap(),
2789 nbits,
2790 dataset,
2791 );
2792
2793 assert!(matches!(
2795 Impl::<8, _>::try_deserialize(&serialized, global),
2796 Err(DeserializationError::UnsupportedBitWidth(4)),
2797 ));
2798 }
2799 8 => {
2800 test_deserialization_inner(
2801 quantizer,
2802 &Impl::<8, _>::try_deserialize(&serialized, global).unwrap(),
2803 nbits,
2804 dataset,
2805 );
2806
2807 assert!(matches!(
2809 Impl::<4, _>::try_deserialize(&serialized, global),
2810 Err(DeserializationError::UnsupportedBitWidth(8)),
2811 ));
2812 }
2813 _ => panic!("bit width {} is not supported", nbits),
2814 }
2815 }
2816
2817 #[derive(Debug, Clone)]
2819 struct FlakyAllocator {
2820 have_allocated: Arc<AtomicBool>,
2821 }
2822
2823 impl FlakyAllocator {
2824 fn new(have_allocated: Arc<AtomicBool>) -> Self {
2825 Self { have_allocated }
2826 }
2827 }
2828
2829 unsafe impl AllocatorCore for FlakyAllocator {
2831 fn allocate(
2832 &self,
2833 layout: std::alloc::Layout,
2834 ) -> Result<std::ptr::NonNull<[u8]>, AllocatorError> {
2835 if self.have_allocated.swap(true, Ordering::Relaxed) {
2836 Err(AllocatorError)
2837 } else {
2838 GlobalAllocator.allocate(layout)
2839 }
2840 }
2841
2842 unsafe fn deallocate(&self, ptr: std::ptr::NonNull<[u8]>, layout: std::alloc::Layout) {
2843 unsafe { GlobalAllocator.deallocate(ptr, layout) }
2845 }
2846 }
2847
2848 fn test_plan_panic_boundary<const NBITS: usize>(v: &Impl<NBITS>)
2849 where
2850 Impl<NBITS>: Quantizer,
2851 {
2852 let have_allocated = Arc::new(AtomicBool::new(false));
2854 let _: AllocatorError = v
2855 .serialize(FlakyAllocator::new(have_allocated.clone()))
2856 .unwrap_err();
2857 assert!(have_allocated.load(Ordering::Relaxed));
2858 }
2859
2860 #[test]
2861 fn test_plan_1bit_l2() {
2862 let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2863 test_plan_panic_boundary(&plan);
2864 test_plan_serialization(&plan, 1, data.as_view());
2865 }
2866
2867 #[test]
2868 fn test_plan_1bit_ip() {
2869 let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2870 test_plan_panic_boundary(&plan);
2871 test_plan_serialization(&plan, 1, data.as_view());
2872 }
2873
2874 #[test]
2875 fn test_plan_2bit_l2() {
2876 let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2877 test_plan_panic_boundary(&plan);
2878 test_plan_serialization(&plan, 2, data.as_view());
2879 }
2880
2881 #[test]
2882 fn test_plan_2bit_ip() {
2883 let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2884 test_plan_panic_boundary(&plan);
2885 test_plan_serialization(&plan, 2, data.as_view());
2886 }
2887
2888 #[test]
2889 fn test_plan_4bit_l2() {
2890 let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2891 test_plan_panic_boundary(&plan);
2892 test_plan_serialization(&plan, 4, data.as_view());
2893 }
2894
2895 #[test]
2896 fn test_plan_4bit_ip() {
2897 let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2898 test_plan_panic_boundary(&plan);
2899 test_plan_serialization(&plan, 4, data.as_view());
2900 }
2901
2902 #[test]
2903 fn test_plan_8bit_l2() {
2904 let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2905 test_plan_panic_boundary(&plan);
2906 test_plan_serialization(&plan, 8, data.as_view());
2907 }
2908
2909 #[test]
2910 fn test_plan_8bit_ip() {
2911 let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2912 test_plan_panic_boundary(&plan);
2913 test_plan_serialization(&plan, 8, data.as_view());
2914 }
2915
2916 #[test]
2917 fn test_plan_1bit_cosine() {
2918 let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2919 test_plan_panic_boundary(&plan);
2920 test_plan_serialization(&plan, 1, data.as_view());
2921 }
2922
2923 #[test]
2924 fn test_plan_2bit_cosine() {
2925 let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2926 test_plan_panic_boundary(&plan);
2927 test_plan_serialization(&plan, 2, data.as_view());
2928 }
2929
2930 #[test]
2931 fn test_plan_4bit_cosine() {
2932 let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2933 test_plan_panic_boundary(&plan);
2934 test_plan_serialization(&plan, 4, data.as_view());
2935 }
2936
2937 #[test]
2938 fn test_plan_8bit_cosine() {
2939 let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2940 test_plan_panic_boundary(&plan);
2941 test_plan_serialization(&plan, 8, data.as_view());
2942 }
2943
2944 #[test]
2945 fn test_allocation_order() {
2946 let (plan, _) = make_impl::<1>(SupportedMetric::SquaredL2);
2947 let buf = plan.serialize(GlobalAllocator).unwrap();
2948
2949 let allocator = BumpAllocator::new(8192, PowerOfTwo::new(64).unwrap()).unwrap();
2950 let deserialized =
2951 try_deserialize::<GlobalAllocator, _>(&buf, allocator.clone()).unwrap();
2952 assert_eq!(
2953 Poly::as_ptr(&deserialized).cast::<u8>(),
2954 allocator.as_ptr(),
2955 "expected the returned box to be allocated first",
2956 );
2957 }
2958 }
2959
2960 #[cfg(feature = "flatbuffers")]
2972 mod compatibility {
2973 use std::path::PathBuf;
2974
2975 use serde::{Deserialize, Serialize};
2976
2977 use super::*;
2978
2979 use crate::test_util::Check;
2980
2981 const TRAINING_SEED: u64 = 0x7d535118722ff197;
2982
2983 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2988 #[serde(rename_all = "snake_case")]
2989 enum Metric {
2990 SquaredL2,
2991 InnerProduct,
2992 Cosine,
2993 }
2994
2995 impl std::fmt::Display for Metric {
2996 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2997 let s = match self {
2998 Self::SquaredL2 => "squared_l2",
2999 Self::InnerProduct => "inner_product",
3000 Self::Cosine => "cosine",
3001 };
3002 write!(f, "{}", s)
3003 }
3004 }
3005
3006 impl From<SupportedMetric> for Metric {
3007 fn from(m: SupportedMetric) -> Self {
3008 match m {
3009 SupportedMetric::SquaredL2 => Self::SquaredL2,
3010 SupportedMetric::InnerProduct => Self::InnerProduct,
3011 SupportedMetric::Cosine => Self::Cosine,
3012 }
3013 }
3014 }
3015
3016 impl From<Metric> for SupportedMetric {
3017 fn from(m: Metric) -> Self {
3018 match m {
3019 Metric::SquaredL2 => Self::SquaredL2,
3020 Metric::InnerProduct => Self::InnerProduct,
3021 Metric::Cosine => Self::Cosine,
3022 }
3023 }
3024 }
3025
3026 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3027 #[serde(rename_all = "snake_case")]
3028 enum DataTransform {
3029 PaddingHadamard,
3030 DoubleHadamard,
3031 Null,
3032 }
3033
3034 impl std::fmt::Display for DataTransform {
3035 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3036 let s = match self {
3037 Self::PaddingHadamard => "padding_hadamard",
3038 Self::DoubleHadamard => "double_hadamard",
3039 Self::Null => "null",
3040 };
3041 write!(f, "{}", s)
3042 }
3043 }
3044
3045 impl DataTransform {
3046 fn to_transform_kind(self) -> TransformKind {
3047 match self {
3048 Self::PaddingHadamard => TransformKind::PaddingHadamard {
3049 target_dim: TargetDim::Natural,
3050 },
3051 Self::DoubleHadamard => TransformKind::DoubleHadamard {
3052 target_dim: TargetDim::Natural,
3053 },
3054 Self::Null => TransformKind::Null,
3055 }
3056 }
3057 }
3058
3059 #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3060 #[serde(rename_all = "snake_case")]
3061 enum Layout {
3062 SameAsData,
3063 FourBitTransposed,
3064 ScalarQuantized,
3065 FullPrecision,
3066 }
3067
3068 impl From<QueryLayout> for Layout {
3069 fn from(l: QueryLayout) -> Self {
3070 match l {
3071 QueryLayout::SameAsData => Self::SameAsData,
3072 QueryLayout::FourBitTransposed => Self::FourBitTransposed,
3073 QueryLayout::ScalarQuantized => Self::ScalarQuantized,
3074 QueryLayout::FullPrecision => Self::FullPrecision,
3075 }
3076 }
3077 }
3078
3079 impl From<Layout> for QueryLayout {
3080 fn from(l: Layout) -> Self {
3081 match l {
3082 Layout::SameAsData => Self::SameAsData,
3083 Layout::FourBitTransposed => Self::FourBitTransposed,
3084 Layout::ScalarQuantized => Self::ScalarQuantized,
3085 Layout::FullPrecision => Self::FullPrecision,
3086 }
3087 }
3088 }
3089
3090 #[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
3091 #[serde(rename_all = "snake_case")]
3092 enum ScaleConfig {
3093 #[default]
3094 None,
3095 ReciprocalMeanNorm,
3096 }
3097
3098 impl ScaleConfig {
3099 fn try_as_str(self) -> Option<&'static str> {
3100 match self {
3101 Self::None => Option::None,
3102 Self::ReciprocalMeanNorm => Some("rmn"),
3103 }
3104 }
3105
3106 fn to_prescale(self) -> PreScale {
3107 match self {
3108 Self::None => PreScale::None,
3109 Self::ReciprocalMeanNorm => PreScale::ReciprocalMeanNorm,
3110 }
3111 }
3112 }
3113
3114 #[derive(Debug, Clone, Serialize, Deserialize)]
3121 struct Baseline {
3122 nbits: usize,
3124 metric: Metric,
3126 transform: DataTransform,
3128 pre_scale: ScaleConfig,
3130 dim: usize,
3132 full_dim: usize,
3134 training_seed: u64,
3136 serialized_quantizer: Vec<u8>,
3138 compressed_vectors: Vec<Vec<u8>>,
3140 data_distances: Vec<f32>,
3143 query_distances: Vec<LayoutDistances>,
3145 #[serde(default, skip_serializing_if = "Option::is_none")]
3148 rescaled_query_distances: Option<Vec<LayoutDistances>>,
3149 }
3150
3151 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3153 struct LayoutDistances {
3154 layout: Layout,
3155 distances: Vec<Vec<f32>>,
3157 }
3158
3159 fn compress_dataset(quantizer: &dyn Quantizer, dataset: MatrixView<f32>) -> Vec<Vec<u8>> {
3164 let scoped_global = ScopedAllocator::global();
3165 let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
3166 dataset
3167 .row_iter()
3168 .map(|row| {
3169 let mut buf = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
3170 quantizer
3171 .compress(row, OpaqueMut::new(&mut buf), scoped_global)
3172 .unwrap();
3173 buf.to_vec()
3174 })
3175 .collect()
3176 }
3177
3178 fn compute_layout_distances(
3179 quantizer: &dyn Quantizer,
3180 dataset: MatrixView<f32>,
3181 compressed: &[Vec<u8>],
3182 allow_rescale: bool,
3183 ) -> Vec<LayoutDistances> {
3184 let scoped_global = ScopedAllocator::global();
3185 QueryLayout::all()
3186 .into_iter()
3187 .filter(|&layout| quantizer.is_supported(layout))
3188 .map(|layout| {
3189 let distances = dataset
3190 .row_iter()
3191 .map(|query_row| {
3192 let computer = quantizer
3193 .fused_query_computer(
3194 query_row,
3195 layout,
3196 allow_rescale,
3197 GlobalAllocator,
3198 scoped_global,
3199 )
3200 .unwrap();
3201 compressed
3202 .iter()
3203 .map(|d| computer.evaluate_similarity(Opaque::new(d)).unwrap())
3204 .collect()
3205 })
3206 .collect();
3207 LayoutDistances {
3208 layout: layout.into(),
3209 distances,
3210 }
3211 })
3212 .collect()
3213 }
3214
3215 const TOLERANCE: Check = Check::absrel(1e-6, 0.0);
3216
3217 fn assert_layout_distances(
3220 quantizer: &dyn Quantizer,
3221 dataset: MatrixView<f32>,
3222 compressed: &[Vec<u8>],
3223 expected: &[LayoutDistances],
3224 allow_rescale: bool,
3225 label: &str,
3226 ) {
3227 let scoped_global = ScopedAllocator::global();
3228 for layout_distances in expected {
3229 let layout: QueryLayout = layout_distances.layout.into();
3230 assert!(
3231 quantizer.is_supported(layout),
3232 "{label}: unsupported layout {layout:?}"
3233 );
3234
3235 for (qi, (query_row, expected_distances)) in
3236 std::iter::zip(dataset.row_iter(), layout_distances.distances.iter())
3237 .enumerate()
3238 {
3239 let computer = quantizer
3240 .fused_query_computer(
3241 query_row,
3242 layout,
3243 allow_rescale,
3244 GlobalAllocator,
3245 scoped_global,
3246 )
3247 .unwrap();
3248
3249 for (di, (compressed, expected)) in
3250 std::iter::zip(compressed.iter(), expected_distances.iter()).enumerate()
3251 {
3252 let distance = computer
3253 .evaluate_similarity(Opaque::new(compressed))
3254 .unwrap();
3255
3256 if let Err(err) = TOLERANCE.check(distance, *expected) {
3257 panic!("{label}: layout = {layout:?}, query={qi}, data={di}\n{err}")
3258 }
3259 }
3260 }
3261 }
3262 }
3263
3264 fn generate_baseline(
3269 quantizer: &dyn Quantizer,
3270 transform: DataTransform,
3271 pre_scale: ScaleConfig,
3272 dataset: MatrixView<f32>,
3273 ) -> Baseline {
3274 let compressed_vectors = compress_dataset(quantizer, dataset);
3275
3276 let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3278 let mut data_distances = Vec::new();
3279 for (i, a) in compressed_vectors.iter().enumerate() {
3280 for b in compressed_vectors.iter().skip(i) {
3281 data_distances.push(
3282 f.evaluate_similarity(Opaque::new(a), Opaque::new(b))
3283 .unwrap(),
3284 );
3285 }
3286 }
3287
3288 let query_distances =
3289 compute_layout_distances(quantizer, dataset, &compressed_vectors, false);
3290
3291 let is_ip = quantizer.metric() == SupportedMetric::InnerProduct;
3292 let rescaled = compute_layout_distances(quantizer, dataset, &compressed_vectors, true);
3293
3294 if !is_ip {
3295 assert_eq!(
3297 query_distances,
3298 rescaled,
3299 "allow_rescale should not affect {:?} distances",
3300 quantizer.metric(),
3301 );
3302 }
3303
3304 let rescaled_query_distances = if is_ip { Some(rescaled) } else { None };
3305
3306 let serialized = quantizer.serialize(GlobalAllocator).unwrap();
3307
3308 Baseline {
3309 nbits: quantizer.nbits(),
3310 metric: quantizer.metric().into(),
3311 transform,
3312 pre_scale,
3313 dim: quantizer.dim(),
3314 full_dim: quantizer.full_dim(),
3315 training_seed: TRAINING_SEED,
3316 serialized_quantizer: serialized.to_vec(),
3317 compressed_vectors,
3318 data_distances,
3319 query_distances,
3320 rescaled_query_distances,
3321 }
3322 }
3323
3324 fn baseline_path(
3329 nbits: usize,
3330 metric: Metric,
3331 transform: DataTransform,
3332 pre_scale: ScaleConfig,
3333 ) -> PathBuf {
3334 let manifest_dir = env!("CARGO_MANIFEST_DIR");
3335 let mut name = format!("{}bit_{}_{}", nbits, metric, transform);
3336 if let Some(mangled) = pre_scale.try_as_str() {
3337 name.push('_');
3338 name.push_str(mangled);
3339 }
3340 name.push_str(".json");
3341 PathBuf::from(manifest_dir)
3342 .join("test")
3343 .join("generated")
3344 .join("spherical")
3345 .join(name)
3346 }
3347
3348 fn should_overwrite() -> bool {
3349 std::env::var("DISKANN_TEST")
3350 .map(|v| v == "overwrite")
3351 .unwrap_or(false)
3352 }
3353
3354 fn save_baseline(baseline: &Baseline) {
3355 let path = baseline_path(
3356 baseline.nbits,
3357 baseline.metric,
3358 baseline.transform,
3359 baseline.pre_scale,
3360 );
3361 if let Some(parent) = path.parent() {
3362 std::fs::create_dir_all(parent).unwrap();
3363 }
3364 let json = serde_json::to_string_pretty(baseline).unwrap();
3365 std::fs::write(&path, json).unwrap();
3366 }
3367
3368 fn load_baseline(
3369 nbits: usize,
3370 metric: Metric,
3371 transform: DataTransform,
3372 pre_scale: ScaleConfig,
3373 ) -> Baseline {
3374 let path = baseline_path(nbits, metric, transform, pre_scale);
3375 let json = std::fs::read_to_string(&path).unwrap_or_else(|e| {
3376 panic!(
3377 "Failed to load baseline from {}: {e}\n\
3378 Run with DISKANN_TEST=overwrite to generate baseline files.",
3379 path.display()
3380 );
3381 });
3382 serde_json::from_str(&json).unwrap()
3383 }
3384
3385 fn check_baseline(
3390 baseline: &Baseline,
3391 dataset: MatrixView<f32>,
3392 expected_transform: DataTransform,
3393 expected_pre_scale: ScaleConfig,
3394 ) {
3395 assert_eq!(
3399 baseline.training_seed, TRAINING_SEED,
3400 "baseline was generated with a different training seed"
3401 );
3402 assert_eq!(
3403 baseline.transform, expected_transform,
3404 "baseline transform does not match expected transform"
3405 );
3406 assert_eq!(
3407 baseline.pre_scale, expected_pre_scale,
3408 "baseline pre_scale does not match expected pre_scale"
3409 );
3410
3411 let quantizer = try_deserialize::<GlobalAllocator, _>(
3413 &baseline.serialized_quantizer,
3414 GlobalAllocator,
3415 )
3416 .unwrap();
3417
3418 assert_eq!(quantizer.nbits(), baseline.nbits, "nbits mismatch");
3420 assert_eq!(quantizer.dim(), baseline.dim, "dim mismatch");
3421 assert_eq!(quantizer.full_dim(), baseline.full_dim, "full_dim mismatch");
3422 assert_eq!(
3423 quantizer.metric(),
3424 SupportedMetric::from(baseline.metric),
3425 "metric mismatch"
3426 );
3427
3428 let compressed = compress_dataset(&*quantizer, dataset);
3430 assert_eq!(
3431 compressed, baseline.compressed_vectors,
3432 "compressed vectors do not match baseline"
3433 );
3434
3435 let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3437 let n = baseline.compressed_vectors.len();
3438 let expected_len = n * (n + 1) / 2;
3439
3440 assert_eq!(
3441 baseline.data_distances.len(),
3442 expected_len,
3443 "baseline.data_distances has length {}, expected {} for {} compressed vectors",
3444 baseline.data_distances.len(),
3445 expected_len,
3446 n,
3447 );
3448 let mut k = 0;
3449 for (i, a) in baseline.compressed_vectors.iter().enumerate() {
3450 for (j, b) in baseline.compressed_vectors.iter().enumerate().skip(i) {
3451 let distance = f
3452 .evaluate_similarity(Opaque::new(a), Opaque::new(b))
3453 .unwrap();
3454
3455 if let Err(err) = TOLERANCE.check(distance, baseline.data_distances[k]) {
3456 panic!("data distance mismatch at pair ({i}, {j})\n{err}");
3457 }
3458 k += 1;
3459 }
3460 }
3461
3462 assert_layout_distances(
3464 &*quantizer,
3465 dataset,
3466 &baseline.compressed_vectors,
3467 &baseline.query_distances,
3468 false,
3469 "query distance mismatch",
3470 );
3471
3472 if let Some(rescaled) = &baseline.rescaled_query_distances {
3475 assert_eq!(
3476 SupportedMetric::from(baseline.metric),
3477 SupportedMetric::InnerProduct,
3478 "rescaled_query_distances should only be present \
3479 for InnerProduct"
3480 );
3481 assert_layout_distances(
3482 &*quantizer,
3483 dataset,
3484 &baseline.compressed_vectors,
3485 rescaled,
3486 true,
3487 "rescaled query distance mismatch",
3488 );
3489 } else {
3490 assert_layout_distances(
3491 &*quantizer,
3492 dataset,
3493 &baseline.compressed_vectors,
3494 &baseline.query_distances,
3495 true,
3496 "allow_rescale should not affect non-IP distances",
3497 );
3498 }
3499 }
3500
3501 fn make_impl_with<const NBITS: usize>(
3506 metric: SupportedMetric,
3507 transform: DataTransform,
3508 pre_scale: ScaleConfig,
3509 ) -> (Impl<NBITS>, Matrix<f32>)
3510 where
3511 Impl<NBITS>: Constructible,
3512 {
3513 let data = test_dataset();
3514 let mut rng = StdRng::seed_from_u64(TRAINING_SEED);
3515
3516 let quantizer = SphericalQuantizer::train(
3517 data.as_view(),
3518 transform.to_transform_kind(),
3519 metric,
3520 pre_scale.to_prescale(),
3521 &mut rng,
3522 GlobalAllocator,
3523 )
3524 .unwrap();
3525
3526 (Impl::<NBITS>::new(quantizer).unwrap(), data)
3527 }
3528
3529 fn run_compatibility_test<const NBITS: usize>(
3530 metric: SupportedMetric,
3531 transform: DataTransform,
3532 pre_scale: ScaleConfig,
3533 ) where
3534 Impl<NBITS>: Constructible + Quantizer,
3535 {
3536 let (quantizer, data) = make_impl_with::<NBITS>(metric, transform, pre_scale);
3537 let dataset = data.as_view();
3538
3539 let baseline = if should_overwrite() {
3540 let baseline = generate_baseline(&quantizer, transform, pre_scale, dataset);
3541 save_baseline(&baseline);
3542 baseline
3543 } else {
3544 load_baseline(NBITS, metric.into(), transform, pre_scale)
3545 };
3546 check_baseline(&baseline, dataset, transform, pre_scale);
3547 }
3548
3549 #[test]
3554 fn compat_1bit_l2() {
3555 run_compatibility_test::<1>(
3556 SupportedMetric::SquaredL2,
3557 DataTransform::DoubleHadamard,
3558 ScaleConfig::None,
3559 );
3560 }
3561
3562 #[test]
3563 fn compat_1bit_ip() {
3564 run_compatibility_test::<1>(
3565 SupportedMetric::InnerProduct,
3566 DataTransform::DoubleHadamard,
3567 ScaleConfig::None,
3568 );
3569 }
3570
3571 #[test]
3572 fn compat_1bit_cosine() {
3573 run_compatibility_test::<1>(
3574 SupportedMetric::Cosine,
3575 DataTransform::DoubleHadamard,
3576 ScaleConfig::None,
3577 );
3578 }
3579
3580 #[test]
3581 fn compat_2bit_l2() {
3582 run_compatibility_test::<2>(
3583 SupportedMetric::SquaredL2,
3584 DataTransform::DoubleHadamard,
3585 ScaleConfig::None,
3586 );
3587 }
3588
3589 #[test]
3590 fn compat_2bit_ip() {
3591 run_compatibility_test::<2>(
3592 SupportedMetric::InnerProduct,
3593 DataTransform::DoubleHadamard,
3594 ScaleConfig::None,
3595 );
3596 }
3597
3598 #[test]
3599 fn compat_2bit_cosine() {
3600 run_compatibility_test::<2>(
3601 SupportedMetric::Cosine,
3602 DataTransform::DoubleHadamard,
3603 ScaleConfig::None,
3604 );
3605 }
3606
3607 #[test]
3608 fn compat_4bit_l2() {
3609 run_compatibility_test::<4>(
3610 SupportedMetric::SquaredL2,
3611 DataTransform::DoubleHadamard,
3612 ScaleConfig::None,
3613 );
3614 }
3615
3616 #[test]
3617 fn compat_4bit_ip() {
3618 run_compatibility_test::<4>(
3619 SupportedMetric::InnerProduct,
3620 DataTransform::DoubleHadamard,
3621 ScaleConfig::None,
3622 );
3623 }
3624
3625 #[test]
3626 fn compat_4bit_cosine() {
3627 run_compatibility_test::<4>(
3628 SupportedMetric::Cosine,
3629 DataTransform::DoubleHadamard,
3630 ScaleConfig::None,
3631 );
3632 }
3633
3634 #[test]
3635 fn compat_8bit_l2() {
3636 run_compatibility_test::<8>(
3637 SupportedMetric::SquaredL2,
3638 DataTransform::DoubleHadamard,
3639 ScaleConfig::None,
3640 );
3641 }
3642
3643 #[test]
3644 fn compat_8bit_ip() {
3645 run_compatibility_test::<8>(
3646 SupportedMetric::InnerProduct,
3647 DataTransform::DoubleHadamard,
3648 ScaleConfig::None,
3649 );
3650 }
3651
3652 #[test]
3653 fn compat_8bit_cosine() {
3654 run_compatibility_test::<8>(
3655 SupportedMetric::Cosine,
3656 DataTransform::DoubleHadamard,
3657 ScaleConfig::None,
3658 );
3659 }
3660
3661 #[test]
3663 fn compat_4bit_l2_null() {
3664 run_compatibility_test::<4>(
3665 SupportedMetric::SquaredL2,
3666 DataTransform::Null,
3667 ScaleConfig::None,
3668 );
3669 }
3670
3671 #[test]
3672 fn compat_4bit_l2_padding_hadamard() {
3673 run_compatibility_test::<4>(
3674 SupportedMetric::SquaredL2,
3675 DataTransform::PaddingHadamard,
3676 ScaleConfig::None,
3677 );
3678 }
3679
3680 #[test]
3682 fn compat_4bit_l2_prescale_rmn() {
3683 run_compatibility_test::<4>(
3684 SupportedMetric::SquaredL2,
3685 DataTransform::DoubleHadamard,
3686 ScaleConfig::ReciprocalMeanNorm,
3687 );
3688 }
3689 }
3690}