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