1use std::num::NonZeroUsize;
7
8use diskann_utils::{
9 ReborrowMut,
10 views::rowmajor::{self, Matrix},
11};
12use diskann_vector::{
13 MathematicalValue, Norm, PureDistanceFunction, distance::InnerProduct, norm::FastL2Norm,
14};
15#[cfg(feature = "flatbuffers")]
16use flatbuffers::{FlatBufferBuilder, WIPOffset};
17use rand::{Rng, RngCore};
18use thiserror::Error;
19
20use super::{
21 CompensatedCosine, CompensatedIP, CompensatedSquaredL2, DataMeta, DataMetaError, DataMut,
22 FullQueryMeta, FullQueryMut, QueryMeta, QueryMut, SupportedMetric, iface,
23};
24use crate::{
25 AsFunctor, CompressIntoWith,
26 algorithms::{
27 heap::SliceHeap,
28 transforms::{NewTransformError, Transform, TransformFailed, TransformKind},
29 },
30 alloc::{Allocator, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone},
31 bits::{PermutationStrategy, Representation, Unsigned},
32 num::Positive,
33 utils::{CannotBeEmpty, compute_means_and_average_norm, compute_normalized_means},
34};
35#[cfg(feature = "flatbuffers")]
36use crate::{
37 algorithms::transforms::TransformError, flatbuffers::spherical, spherical::InvalidMetric,
38};
39
40#[derive(Debug)]
45#[cfg_attr(test, derive(PartialEq))]
46pub struct SphericalQuantizer<A = GlobalAllocator>
47where
48 A: Allocator,
49{
50 shift: Poly<[f32], A>,
52
53 transform: Transform<A>,
61
62 metric: SupportedMetric,
64
65 mean_norm: Positive<f32>,
74
75 pre_scale: Positive<f32>,
81}
82
83impl<A> TryClone for SphericalQuantizer<A>
84where
85 A: Allocator,
86{
87 fn try_clone(&self) -> Result<Self, AllocatorError> {
88 SphericalQuantizer::try_clone(self)
89 }
90}
91
92#[derive(Debug, Clone, Copy, Error)]
93#[non_exhaustive]
94pub enum TrainError {
95 #[error("data dim cannot be zero")]
96 DimCannotBeZero,
97 #[error("data cannot be empty")]
98 DataCannotBeEmpty,
99 #[error("pre-scale must be positive")]
100 PrescaleNotPositive,
101 #[error("norm must be positive")]
102 NormNotPositive,
103 #[error("computed norm contains infinity or NaN")]
104 NormNotFinite,
105 #[error("reciprocal norm contains infinity or NaN")]
106 ReciprocalNormNotFinite,
107 #[error(transparent)]
108 AllocatorError(#[from] AllocatorError),
109}
110
111impl<A> SphericalQuantizer<A>
112where
113 A: Allocator,
114{
115 pub fn input_dim(&self) -> usize {
117 self.shift.len()
118 }
119
120 pub fn output_dim(&self) -> usize {
125 self.transform.output_dim()
126 }
127
128 pub fn shift(&self) -> &[f32] {
135 &self.shift
136 }
137
138 pub fn mean_norm(&self) -> Positive<f32> {
140 self.mean_norm
141 }
142
143 pub fn pre_scale(&self) -> Positive<f32> {
148 self.pre_scale
149 }
150
151 pub fn allocator(&self) -> &A {
153 self.shift.allocator()
154 }
155
156 pub fn try_clone(&self) -> Result<Self, AllocatorError> {
158 Ok(Self {
159 shift: self.shift.try_clone()?,
160 transform: self.transform.try_clone()?,
161 metric: self.metric,
162 mean_norm: self.mean_norm,
163 pre_scale: self.pre_scale,
164 })
165 }
166
167 pub fn generate(
169 mut centroid: Poly<[f32], A>,
170 mean_norm: f32,
171 transform: TransformKind,
172 metric: SupportedMetric,
173 pre_scale: Option<f32>,
174 rng: &mut dyn RngCore,
175 allocator: A,
176 ) -> Result<Self, TrainError> {
177 let pre_scale = match pre_scale {
178 Some(v) => Positive::new(v).map_err(|_| TrainError::PrescaleNotPositive)?,
179 None => crate::num::POSITIVE_ONE_F32,
180 };
181
182 let dim = match NonZeroUsize::new(centroid.len()) {
183 Some(dim) => dim,
184 None => {
185 return Err(TrainError::DimCannotBeZero);
186 }
187 };
188
189 let mean_norm = Positive::new(mean_norm).map_err(|_| TrainError::NormNotPositive)?;
190
191 let transform = match Transform::new(transform, dim, Some(rng), allocator.clone()) {
193 Ok(v) => v,
194 Err(NewTransformError::RngMissing(_)) => unreachable!("An Rng was provided"),
195 Err(NewTransformError::AllocatorError(err)) => {
196 return Err(TrainError::AllocatorError(err));
197 }
198 };
199
200 centroid
202 .iter_mut()
203 .for_each(|v| *v *= pre_scale.into_inner());
204
205 Ok(SphericalQuantizer {
206 shift: centroid,
207 transform,
208 metric,
209 mean_norm,
210 pre_scale,
211 })
212 }
213
214 pub fn metric(&self) -> SupportedMetric {
216 self.metric
217 }
218
219 pub fn train<T, R>(
234 data: rowmajor::Ref<T>,
235 transform: TransformKind,
236 metric: SupportedMetric,
237 pre_scale: PreScale,
238 rng: &mut R,
239 allocator: A,
240 ) -> Result<Self, TrainError>
241 where
242 T: Copy + Into<f64> + Into<f32>,
243 R: Rng,
244 {
245 #[inline(never)]
248 fn train<T, A>(
249 data: rowmajor::Ref<T>,
250 transform: TransformKind,
251 metric: SupportedMetric,
252 pre_scale: PreScale,
253 rng: &mut dyn RngCore,
254 allocator: A,
255 ) -> Result<SphericalQuantizer<A>, TrainError>
256 where
257 T: Copy + Into<f64> + Into<f32>,
258 A: Allocator,
259 {
260 if data.ncols() == 0 {
263 return Err(TrainError::DimCannotBeZero);
264 }
265
266 let (centroid, mean_norm) = match metric {
267 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
268 compute_means_and_average_norm(data)
269 }
270 SupportedMetric::Cosine => (
271 compute_normalized_means(data)
272 .map_err(|_: CannotBeEmpty| TrainError::DataCannotBeEmpty)?,
273 1.0,
274 ),
275 };
276
277 let mean_norm = mean_norm as f32;
278
279 if mean_norm <= 0.0 {
280 return Err(TrainError::NormNotPositive);
281 }
282
283 if !mean_norm.is_finite() {
284 return Err(TrainError::NormNotFinite);
285 }
286
287 let pre_scale: Positive<f32> = match pre_scale {
289 PreScale::None => crate::num::POSITIVE_ONE_F32,
290 PreScale::Some(v) => v,
291 PreScale::ReciprocalMeanNorm => {
292 let pre_scale = Positive::new(1.0 / mean_norm)
301 .map_err(|_| TrainError::ReciprocalNormNotFinite)?;
302
303 if !pre_scale.into_inner().is_finite() {
304 return Err(TrainError::ReciprocalNormNotFinite);
305 }
306
307 pre_scale
308 }
309 };
310
311 let centroid =
313 Poly::from_iter(centroid.into_iter().map(|i| i as f32), allocator.clone())?;
314
315 SphericalQuantizer::generate(
316 centroid,
317 mean_norm,
318 transform,
319 metric,
320 Some(pre_scale.into_inner()),
321 rng,
322 allocator,
323 )
324 }
325
326 train(data, transform, metric, pre_scale, rng, allocator)
327 }
328
329 pub fn rescale(&self, v: &mut [f32]) {
331 let norm = FastL2Norm.evaluate(&*v);
332 let m = self.mean_norm.into_inner() / norm;
333 v.iter_mut().for_each(|i| *i *= m);
334 }
335
336 fn preprocess<'a>(
342 &self,
343 data: &[f32],
344 allocator: ScopedAllocator<'a>,
345 ) -> Result<Preprocessed<'a>, CompressionError> {
346 assert_eq!(data.len(), self.input_dim(), "Data dimension is incorrect.");
347
348 let scale = self.pre_scale.into_inner();
353 let mul: f32 = match self.metric {
354 SupportedMetric::Cosine => {
355 let norm: f32 = (FastL2Norm).evaluate(data);
356 if norm == 0.0 { 1.0 } else { 1.0 / norm }
357 }
358 SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => scale,
359 };
360
361 let shifted = Poly::from_iter(
363 std::iter::zip(data.iter(), self.shift.iter()).map(|(&f, &s)| mul * f - s),
364 allocator,
365 )?;
366
367 let shifted_norm = FastL2Norm.evaluate(&*shifted);
368 if !shifted_norm.is_finite() {
369 return Err(CompressionError::InputContainsNaN);
370 }
371 let inner_product_with_centroid = match self.metric {
372 SupportedMetric::SquaredL2 => None,
373 SupportedMetric::InnerProduct | SupportedMetric::Cosine => {
374 let ip: MathematicalValue<f32> = InnerProduct::evaluate(&*shifted, &*self.shift);
375 Some(ip.into_inner())
376 }
377 };
378
379 Ok(Preprocessed {
380 shifted,
381 shifted_norm,
382 inner_product_with_centroid,
383 })
384 }
385
386 pub fn as_quantizer<const NBITS: usize>(
388 self,
389 ) -> Result<Poly<dyn iface::Quantizer>, AllocatorError>
390 where
391 A: 'static,
392 iface::Impl<NBITS, A>: iface::Constructible<A> + iface::Quantizer,
393 {
394 let iface = iface::Impl::<NBITS, A>::new(self)?;
395 crate::poly!({ iface::Quantizer }, iface, GlobalAllocator)
396 }
397}
398
399#[derive(Debug, Clone, Copy)]
403pub enum PreScale {
404 None,
406 Some(Positive<f32>),
408 ReciprocalMeanNorm,
411}
412
413#[cfg(feature = "flatbuffers")]
414#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
415#[derive(Debug, Clone, Error, PartialEq)]
416#[non_exhaustive]
417pub enum DeserializationError {
418 #[error(transparent)]
419 TransformError(#[from] TransformError),
420 #[error("unrecognized flatbuffer identifier")]
421 UnrecognizedIdentifier,
422 #[error("transform length not equal to centroid")]
423 DimMismatch,
424 #[error("norm is missing or is not positive")]
425 MissingNorm,
426 #[error("pre-scale is missing or is not positive")]
427 PreScaleNotPositive,
428
429 #[error(transparent)]
430 InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
431
432 #[error(transparent)]
433 InvalidMetric(#[from] InvalidMetric),
434
435 #[error(transparent)]
436 AllocatorError(#[from] AllocatorError),
437}
438
439#[cfg(feature = "flatbuffers")]
440#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
441impl<A> SphericalQuantizer<A>
442where
443 A: Allocator + Clone,
444{
445 pub(crate) fn pack<'a, FA>(
448 &self,
449 buf: &mut FlatBufferBuilder<'a, FA>,
450 ) -> WIPOffset<spherical::SphericalQuantizer<'a>>
451 where
452 FA: flatbuffers::Allocator + 'a,
453 {
454 let centroid = buf.create_vector(&self.shift);
456
457 let transform = self.transform.pack(buf);
459
460 spherical::SphericalQuantizer::create(
462 buf,
463 &spherical::SphericalQuantizerArgs {
464 centroid: Some(centroid),
465 transform: Some(transform),
466 metric: self.metric.into(),
467 mean_norm: self.mean_norm.into_inner(),
468 pre_scale: self.pre_scale.into_inner(),
469 },
470 )
471 }
472
473 pub(crate) fn try_unpack(
476 alloc: A,
477 proto: spherical::SphericalQuantizer<'_>,
478 ) -> Result<Self, DeserializationError> {
479 let metric: SupportedMetric = proto.metric().try_into()?;
480
481 let shift = Poly::from_iter(proto.centroid().into_iter(), alloc.clone())?;
483
484 let transform = Transform::try_unpack(alloc, proto.transform())?;
486
487 if shift.len() != transform.input_dim() {
489 return Err(DeserializationError::DimMismatch);
490 }
491
492 let mean_norm =
494 Positive::new(proto.mean_norm()).map_err(|_| DeserializationError::MissingNorm)?;
495
496 let pre_scale = Positive::new(proto.pre_scale())
497 .map_err(|_| DeserializationError::PreScaleNotPositive)?;
498
499 Ok(Self {
500 shift,
501 transform,
502 metric,
503 mean_norm,
504 pre_scale,
505 })
506 }
507}
508
509struct Preprocessed<'a> {
510 shifted: Poly<[f32], ScopedAllocator<'a>>,
511 shifted_norm: f32,
512 inner_product_with_centroid: Option<f32>,
513}
514
515impl Preprocessed<'_> {
516 fn metric_specific(&self) -> f32 {
521 match self.inner_product_with_centroid {
522 Some(ip) => ip,
523 None => self.shifted_norm * self.shifted_norm,
524 }
525 }
526}
527
528impl<A> AsFunctor<CompensatedSquaredL2> for SphericalQuantizer<A>
533where
534 A: Allocator,
535{
536 fn as_functor(&self) -> CompensatedSquaredL2 {
537 CompensatedSquaredL2::new(self.output_dim())
538 }
539}
540
541impl<A> AsFunctor<CompensatedIP> for SphericalQuantizer<A>
542where
543 A: Allocator,
544{
545 fn as_functor(&self) -> CompensatedIP {
546 CompensatedIP::new(&self.shift, self.output_dim())
547 }
548}
549
550impl<A> AsFunctor<CompensatedCosine> for SphericalQuantizer<A>
551where
552 A: Allocator,
553{
554 fn as_functor(&self) -> CompensatedCosine {
555 CompensatedCosine::new(self.as_functor())
556 }
557}
558
559#[derive(Debug, Error, Clone, Copy, PartialEq)]
564#[non_exhaustive]
565pub enum CompressionError {
566 #[error("input contains NaN")]
567 InputContainsNaN,
568
569 #[error("expected source vector to have length {expected}")]
570 SourceDimensionMismatch { expected: usize },
571
572 #[error("expected destination vector to have length {expected}")]
573 DestinationDimensionMismatch { expected: usize },
574
575 #[error(
576 "encoding error - you may need to scale the entire dataset to reduce its dynamic range"
577 )]
578 EncodingError(#[from] DataMetaError),
579
580 #[error(transparent)]
581 AllocatorError(#[from] AllocatorError),
582}
583
584fn check_dims(
585 input: usize,
586 output: usize,
587 from: usize,
588 into: usize,
589) -> Result<(), CompressionError> {
590 if from != input {
591 return Err(CompressionError::SourceDimensionMismatch { expected: input });
592 }
593 if into != output {
594 return Err(CompressionError::DestinationDimensionMismatch { expected: output });
595 }
596 Ok(())
597}
598
599trait FinishCompressing {
602 fn finish_compressing(
603 &mut self,
604 preprocessed: &Preprocessed<'_>,
605 transformed: &[f32],
606 transformed_norm: f32,
607 allocator: ScopedAllocator<'_>,
608 ) -> Result<(), CompressionError>;
609}
610
611impl FinishCompressing for DataMut<'_, 1> {
612 fn finish_compressing(
613 &mut self,
614 preprocessed: &Preprocessed<'_>,
615 transformed: &[f32],
616 transformed_norm: f32,
617 _: ScopedAllocator<'_>,
618 ) -> Result<(), CompressionError> {
619 let mut quant_raw_inner_product = 0.0f32;
622 let mut bit_sum = 0u32;
623 transformed.iter().enumerate().for_each(|(i, &r)| {
624 let bit: u8 = if r > 0.0 { 1 } else { 0 };
625
626 quant_raw_inner_product += r.abs();
627 bit_sum += <u8 as Into<u32>>::into(bit);
628
629 unsafe { self.vector_mut().set_unchecked(i, bit) };
631 });
632
633 let inner_product_correction =
654 2.0 * transformed_norm * preprocessed.shifted_norm / quant_raw_inner_product;
655 self.set_meta(DataMeta::new(
656 inner_product_correction,
657 preprocessed.metric_specific(),
658 bit_sum,
659 )?);
660 Ok(())
661 }
662}
663
664impl FinishCompressing for DataMut<'_, 2> {
665 fn finish_compressing(
666 &mut self,
667 preprocessed: &Preprocessed<'_>,
668 transformed: &[f32],
669 transformed_norm: f32,
670 allocator: ScopedAllocator<'_>,
671 ) -> Result<(), CompressionError> {
672 compress_via_maximum_cosine(
673 self.reborrow_mut(),
674 preprocessed,
675 transformed,
676 transformed_norm,
677 allocator,
678 )
679 }
680}
681
682impl FinishCompressing for DataMut<'_, 4> {
683 fn finish_compressing(
684 &mut self,
685 preprocessed: &Preprocessed<'_>,
686 transformed: &[f32],
687 transformed_norm: f32,
688 allocator: ScopedAllocator<'_>,
689 ) -> Result<(), CompressionError> {
690 compress_via_maximum_cosine(
691 self.reborrow_mut(),
692 preprocessed,
693 transformed,
694 transformed_norm,
695 allocator,
696 )
697 }
698}
699
700impl FinishCompressing for DataMut<'_, 8> {
701 fn finish_compressing(
702 &mut self,
703 preprocessed: &Preprocessed<'_>,
704 transformed: &[f32],
705 transformed_norm: f32,
706 allocator: ScopedAllocator<'_>,
707 ) -> Result<(), CompressionError> {
708 compress_via_maximum_cosine(
709 self.reborrow_mut(),
710 preprocessed,
711 transformed,
712 transformed_norm,
713 allocator,
714 )
715 }
716}
717
718impl<A> CompressIntoWith<&[f32], FullQueryMut<'_>, ScopedAllocator<'_>> for SphericalQuantizer<A>
723where
724 A: Allocator,
725{
726 type Error = CompressionError;
727
728 fn compress_into_with(
741 &self,
742 from: &[f32],
743 mut into: FullQueryMut<'_>,
744 allocator: ScopedAllocator<'_>,
745 ) -> Result<(), Self::Error> {
746 let input_dim = self.shift.len();
747 let output_dim = self.output_dim();
748 check_dims(input_dim, output_dim, from.len(), into.len())?;
749
750 let mut preprocessed = self.preprocess(from, allocator)?;
751
752 if preprocessed.shifted_norm == 0.0 {
755 into.vector_mut().fill(0.0);
756 *into.meta_mut() = Default::default();
757 return Ok(());
758 }
759
760 preprocessed
761 .shifted
762 .iter_mut()
763 .for_each(|v| *v /= preprocessed.shifted_norm);
764
765 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
770 match self
771 .transform
772 .transform_into(into.vector_mut(), &preprocessed.shifted, allocator)
773 {
774 Ok(()) => {}
775 Err(TransformFailed::AllocatorError(err)) => {
776 return Err(CompressionError::AllocatorError(err));
777 }
778 Err(TransformFailed::SourceMismatch { .. })
779 | Err(TransformFailed::DestinationMismatch { .. }) => {
780 panic!(
781 "The sizes of these arrays should already be checked - this is a logic error"
782 );
783 }
784 #[cfg(feature = "linalg")]
785 Err(TransformFailed::SgemmError(_)) => {
786 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
787 }
788 }
789
790 *into.meta_mut() = FullQueryMeta {
791 sum: into.vector().iter().sum::<f32>(),
792 shifted_norm: preprocessed.shifted_norm,
793 metric_specific: preprocessed.metric_specific(),
794 };
795 Ok(())
796 }
797}
798
799impl<const NBITS: usize, A> CompressIntoWith<&[f32], DataMut<'_, NBITS>, ScopedAllocator<'_>>
800 for SphericalQuantizer<A>
801where
802 A: Allocator,
803 Unsigned: Representation<NBITS>,
804 for<'a> DataMut<'a, NBITS>: FinishCompressing,
805{
806 type Error = CompressionError;
807
808 fn compress_into_with(
821 &self,
822 from: &[f32],
823 mut into: DataMut<'_, NBITS>,
824 allocator: ScopedAllocator<'_>,
825 ) -> Result<(), Self::Error> {
826 let input_dim = self.shift.len();
827 let output_dim = self.output_dim();
828 check_dims(input_dim, output_dim, from.len(), into.len())?;
829
830 let mut preprocessed = self.preprocess(from, allocator)?;
831
832 if preprocessed.shifted_norm == 0.0 {
833 into.set_meta(DataMeta::default());
834 return Ok(());
835 }
836
837 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
838 preprocessed
839 .shifted
840 .iter_mut()
841 .for_each(|v| *v /= preprocessed.shifted_norm);
842
843 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
848 match self
849 .transform
850 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
851 {
852 Ok(()) => {}
853 Err(TransformFailed::AllocatorError(err)) => {
854 return Err(CompressionError::AllocatorError(err));
855 }
856 Err(TransformFailed::SourceMismatch { .. })
857 | Err(TransformFailed::DestinationMismatch { .. }) => {
858 panic!(
859 "The sizes of these arrays should already be checked - this is a logic error"
860 );
861 }
862 #[cfg(feature = "linalg")]
863 Err(TransformFailed::SgemmError(_)) => {
864 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
865 }
866 }
867
868 let transformed_norm = if self.transform.preserves_norms() {
869 1.0
870 } else {
871 (FastL2Norm).evaluate(&*transformed)
872 };
873
874 into.finish_compressing(&preprocessed, &transformed, transformed_norm, allocator)?;
875 Ok(())
876 }
877}
878
879struct AsNonZero<const NBITS: usize>;
880impl<const NBITS: usize> AsNonZero<NBITS> {
881 const NON_ZERO: NonZeroUsize = NonZeroUsize::new(NBITS).unwrap();
882}
883
884fn compress_via_maximum_cosine<const NBITS: usize>(
885 mut data: DataMut<'_, NBITS>,
886 preprocessed: &Preprocessed<'_>,
887 transformed: &[f32],
888 transformed_norm: f32,
889 allocator: ScopedAllocator<'_>,
890) -> Result<(), CompressionError>
891where
892 Unsigned: Representation<NBITS>,
893{
894 assert_eq!(data.len(), transformed.len());
895
896 let optimal_scale =
899 maximize_cosine_similarity(transformed, AsNonZero::<NBITS>::NON_ZERO, allocator)?;
900
901 let domain = Unsigned::domain_const::<NBITS>();
902 let min = *domain.start() as f32;
903 let max = *domain.end() as f32;
904 let offset = max / 2.0;
905
906 let mut self_inner_product = 0.0f32;
907 let mut bit_sum = 0u32;
908 for (i, t) in transformed.iter().enumerate() {
909 let v = (*t * optimal_scale + offset).clamp(min, max).round();
910 let dv = v - offset;
911 self_inner_product = dv.mul_add(*t, self_inner_product);
912
913 let v = v as u8;
914 bit_sum += <u8 as Into<u32>>::into(v);
915
916 unsafe { data.vector_mut().set_unchecked(i, v) };
921 }
922
923 let shifted_norm = preprocessed.shifted_norm;
924 let inner_product_correction = (transformed_norm * shifted_norm) / self_inner_product;
925 data.set_meta(DataMeta::new(
926 inner_product_correction,
927 preprocessed.metric_specific(),
928 bit_sum,
929 )?);
930 Ok(())
931}
932
933#[derive(Debug, Clone, Copy)]
947struct Pair {
948 value: f32,
949 position: u32,
950}
951
952impl PartialEq for Pair {
953 fn eq(&self, other: &Self) -> bool {
954 self.value.eq(&other.value)
955 }
956}
957
958impl Eq for Pair {}
959impl PartialOrd for Pair {
960 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
961 Some(self.cmp(other))
962 }
963}
964impl Ord for Pair {
965 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
966 other
967 .value
968 .partial_cmp(&self.value)
969 .unwrap_or(std::cmp::Ordering::Equal)
970 }
971}
972
973fn maximize_cosine_similarity(
1005 v: &[f32],
1006 num_bits: NonZeroUsize,
1007 allocator: ScopedAllocator<'_>,
1008) -> Result<f32, AllocatorError> {
1009 let mut current_ip = 0.5 * v.iter().map(|i| i.abs() as f64).sum::<f64>();
1014 let mut current_square_norm = 0.25 * (v.len() as f64);
1015
1016 let mut rounded = Poly::broadcast(1u16, v.len(), allocator)?;
1020
1021 let eps = 0.0001f32;
1027 let one_and_change = 1.0 + eps;
1028 let mut base = Poly::from_iter(
1029 v.iter().enumerate().map(|(position, value)| {
1030 let value = one_and_change / value.abs();
1031 Pair {
1032 value,
1033 position: position as u32,
1034 }
1035 }),
1036 allocator,
1037 )?;
1038
1039 #[expect(clippy::expect_used)]
1042 let mut critical_values =
1043 SliceHeap::new(&mut base).expect("calling code should not allow the slice to be empty");
1044
1045 let mut max_similarity = f64::NEG_INFINITY;
1046 let mut optimal_scale = f32::default();
1047 let stop = (2usize).pow(num_bits.get() as u32 - 1) as u16;
1048
1049 loop {
1050 let mut should_break = false;
1051 critical_values.update_root(|pair| {
1052 let Pair { value, position } = *pair;
1053 if value == f32::MAX {
1054 should_break = true;
1055 return;
1056 }
1057
1058 let r = &mut rounded[position as usize];
1059 let vp = &v[position as usize];
1060
1061 let old_r = *r;
1062 *r += 1;
1065
1066 current_ip += vp.abs() as f64;
1071
1072 current_square_norm += (2 * old_r) as f64;
1086
1087 let similarity = current_ip / current_square_norm.sqrt();
1089 if similarity > max_similarity {
1090 max_similarity = similarity;
1091 optimal_scale = value;
1092 }
1093
1094 if *r < stop {
1096 *pair = Pair {
1097 value: (*r as f32 + eps) / vp.abs(),
1098 position,
1099 };
1100 } else {
1101 *pair = Pair {
1102 value: f32::MAX,
1103 position,
1104 };
1105 }
1106 });
1107 if should_break {
1108 break;
1109 }
1110 }
1111
1112 Ok(optimal_scale)
1113}
1114
1115impl<const NBITS: usize, Perm, A>
1120 CompressIntoWith<&[f32], QueryMut<'_, NBITS, Perm>, ScopedAllocator<'_>>
1121 for SphericalQuantizer<A>
1122where
1123 Unsigned: Representation<NBITS>,
1124 Perm: PermutationStrategy<NBITS>,
1125 A: Allocator,
1126{
1127 type Error = CompressionError;
1128
1129 fn compress_into_with(
1142 &self,
1143 from: &[f32],
1144 mut into: QueryMut<'_, NBITS, Perm>,
1145 allocator: ScopedAllocator<'_>,
1146 ) -> Result<(), Self::Error> {
1147 let input_dim = self.shift.len();
1148 let output_dim = self.output_dim();
1149 check_dims(input_dim, output_dim, from.len(), into.len())?;
1150
1151 let mut preprocessed = self.preprocess(from, allocator)?;
1152
1153 if preprocessed.shifted_norm == 0.0 {
1154 into.set_meta(QueryMeta::default());
1155 return Ok(());
1156 }
1157
1158 preprocessed
1159 .shifted
1160 .iter_mut()
1161 .for_each(|v| *v /= preprocessed.shifted_norm);
1162
1163 let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
1164
1165 #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
1170 match self
1171 .transform
1172 .transform_into(&mut transformed, &preprocessed.shifted, allocator)
1173 {
1174 Ok(()) => {}
1175 Err(TransformFailed::AllocatorError(err)) => {
1176 return Err(CompressionError::AllocatorError(err));
1177 }
1178 Err(TransformFailed::SourceMismatch { .. })
1179 | Err(TransformFailed::DestinationMismatch { .. }) => {
1180 panic!(
1181 "The sizes of these arrays should already be checked - this is a logic error"
1182 );
1183 }
1184 #[cfg(feature = "linalg")]
1185 Err(TransformFailed::SgemmError(_)) => {
1186 panic!("SGEMM should not fail with valid dimensions - this is a logic error");
1187 }
1188 }
1189
1190 let (min, max) = transformed
1192 .iter()
1193 .fold((f32::MAX, f32::MIN), |(min, max), i| {
1194 (i.min(min), i.max(max))
1195 });
1196
1197 let domain = Unsigned::domain_const::<NBITS>();
1198 let lo = (*domain.start()) as f32;
1199 let hi = (*domain.end()) as f32;
1200
1201 let scale = (max - min) / hi;
1202 let mut bit_sum: f32 = 0.0;
1203 transformed.iter().enumerate().for_each(|(i, v)| {
1204 let c = ((v - min) / scale).round().clamp(lo, hi);
1205 bit_sum += c;
1206
1207 #[expect(clippy::unwrap_used)]
1213 into.vector_mut().set(i, c as i64).unwrap();
1214 });
1215
1216 into.set_meta(QueryMeta {
1218 inner_product_correction: preprocessed.shifted_norm * scale,
1219 bit_sum,
1220 offset: min / scale,
1221 metric_specific: preprocessed.metric_specific(),
1222 });
1223
1224 Ok(())
1225 }
1226}
1227
1228#[cfg(not(miri))]
1233#[cfg(test)]
1234mod tests {
1235 use super::*;
1236
1237 use std::fmt::Display;
1238
1239 use diskann_utils::{ReborrowMut, lazy_format, views::rowmajor::MatrixMut};
1240 use diskann_vector::{PureDistanceFunction, norm::FastL2NormSquared};
1241 use diskann_wide::ARCH;
1242 use rand::{
1243 SeedableRng,
1244 distr::{Distribution, Uniform},
1245 rngs::StdRng,
1246 };
1247 use rand_distr::StandardNormal;
1248
1249 use crate::{
1250 algorithms::transforms::TargetDim,
1251 alloc::GlobalAllocator,
1252 bits::{BitTranspose, Dense},
1253 spherical::{Data, DataMetaF32, FullQuery, Query},
1254 test_util,
1255 };
1256
1257 #[test]
1259 fn test_cosine_similarity_maximizer() {
1260 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
1261 let num_trials = 10000;
1262 let num_bits = NonZeroUsize::new(3).unwrap();
1263
1264 let scale_distribution = Uniform::new(0.5f32, 10.0f32).unwrap();
1265
1266 let run_test = |target: [f32; 4]| {
1267 let scale =
1268 maximize_cosine_similarity(&target, num_bits, ScopedAllocator::global()).unwrap();
1269
1270 let mut best: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
1271 let mut best_similarity: f32 = f32::NEG_INFINITY;
1272
1273 let min = -3.5;
1276 for i0 in 0..8 {
1277 for i1 in 0..8 {
1278 for i2 in 0..8 {
1279 for i3 in 0..8 {
1280 let p: [f32; 4] = [
1281 min + (i0 as f32),
1282 min + (i1 as f32),
1283 min + (i2 as f32),
1284 min + (i3 as f32),
1285 ];
1286
1287 let sim: MathematicalValue<f32> =
1288 diskann_vector::distance::Cosine::evaluate(&p, &target);
1289 let sim = sim.into_inner();
1290 if sim > best_similarity {
1291 best_similarity = sim;
1292 best = p.map(|i| i - min);
1294 }
1295 }
1296 }
1297 }
1298 }
1299
1300 let clamped = target.map(|i| (i * scale - min).round().clamp(0.0, 7.0));
1303 let clamped_cosine: MathematicalValue<f32> =
1304 diskann_vector::distance::Cosine::evaluate(&clamped.map(|i| i + min), &target);
1305
1306 let passed = if best == clamped {
1309 true
1310 } else {
1311 let ratio: Vec<f32> = std::iter::zip(best, clamped)
1312 .map(|(b, c)| {
1313 let ratio = (b + min) / (c + min);
1314 assert_ne!(
1315 ratio, 0.0,
1316 "ratio should never be zero because `b` is an integer and \
1317 `min` is not"
1318 );
1319 ratio
1320 })
1321 .collect();
1322
1323 ratio.iter().all(|i| *i == ratio[0])
1324 };
1325
1326 if !passed {
1327 panic!(
1328 "failed for input {:?}.\
1329 Best = {:?}, Found = {:?}\
1330 Best similarity = {}, similarity with clamped = {}",
1331 target,
1332 best,
1333 clamped,
1334 best_similarity,
1335 clamped_cosine.into_inner()
1336 );
1337 }
1338 };
1339
1340 let min = -3.5;
1342 for i0 in (0..8).step_by(2) {
1343 for i1 in (1..9).step_by(2) {
1344 for i2 in (0..8).step_by(2) {
1345 for i3 in (1..9).step_by(2) {
1346 let p: [f32; 4] = [
1347 min + (i0 as f32),
1348 min + (i1 as f32),
1349 min + (i2 as f32),
1350 min + (i3 as f32),
1351 ];
1352 run_test(p)
1353 }
1354 }
1355 }
1356 }
1357
1358 for _ in 0..num_trials {
1359 let this_scale: f32 = scale_distribution.sample(&mut rng);
1360 let v: [f32; 4] = [(); 4].map(|_| {
1361 let v: f32 = StandardNormal {}.sample(&mut rng);
1362 this_scale * v
1363 });
1364 run_test(v);
1365 }
1366 }
1367
1368 #[test]
1369 #[should_panic(expected = "calling code should not allow the slice to be empty")]
1370 fn empty_slice_panics() {
1371 maximize_cosine_similarity(
1372 &[],
1373 NonZeroUsize::new(4).unwrap(),
1374 ScopedAllocator::global(),
1375 )
1376 .unwrap();
1377 }
1378
1379 struct Setup {
1380 transform: TransformKind,
1381 nrows: usize,
1382 ncols: usize,
1383 num_trials: usize,
1384 }
1385
1386 fn get_scale(scale: PreScale, quantizer: &SphericalQuantizer) -> f32 {
1387 match scale {
1388 PreScale::None => 1.0,
1389 PreScale::Some(v) => v.into_inner(),
1390 PreScale::ReciprocalMeanNorm => 1.0 / quantizer.mean_norm().into_inner(),
1391 }
1392 }
1393
1394 fn test_l2<const Q: usize, const D: usize, Perm>(
1395 setup: &Setup,
1396 problem: &test_util::TestProblem,
1397 computed_means: &[f32],
1398 pre_scale: PreScale,
1399 rng: &mut StdRng,
1400 ) where
1401 Unsigned: Representation<Q>,
1402 Unsigned: Representation<D>,
1403 Perm: PermutationStrategy<Q>,
1404 for<'a> SphericalQuantizer:
1405 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1406 for<'a> SphericalQuantizer:
1407 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1408 {
1409 assert_eq!(setup.nrows, problem.data.nrows());
1410 assert_eq!(setup.ncols, problem.data.ncols());
1411
1412 let scoped_global = ScopedAllocator::global();
1413 let distribution = Uniform::new(0, setup.nrows).unwrap();
1414 let quantizer = SphericalQuantizer::train(
1415 problem.data.as_view(),
1416 setup.transform,
1417 SupportedMetric::SquaredL2,
1418 pre_scale,
1419 rng,
1420 GlobalAllocator,
1421 )
1422 .unwrap();
1423
1424 let scale = get_scale(pre_scale, &quantizer);
1425
1426 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1427 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1428 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1429
1430 assert_eq!(
1431 quantizer.mean_norm.into_inner(),
1432 problem.mean_norm as f32,
1433 "computed mean norm should not apply scale"
1434 );
1435 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1436 assert_eq!(&*scaled_means, quantizer.shift());
1437
1438 let l2: CompensatedSquaredL2 = quantizer.as_functor();
1439 assert_eq!(l2.dim, quantizer.output_dim() as f32);
1440
1441 for _ in 0..setup.num_trials {
1442 let i = distribution.sample(rng);
1443 let v = problem.data.row(i);
1444
1445 quantizer
1446 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1447 .unwrap();
1448 quantizer
1449 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1450 .unwrap();
1451 quantizer
1452 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1453 .unwrap();
1454
1455 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1456 .map(|(a, b)| scale * a - b)
1457 .collect();
1458
1459 {
1461 let DataMetaF32 {
1462 inner_product_correction,
1463 bit_sum,
1464 metric_specific,
1465 } = b.meta().to_full(ARCH);
1466
1467 let shifted_square_norm = metric_specific;
1468
1469 let bv = b.vector();
1471 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1472 assert_eq!(s, bit_sum as usize);
1473
1474 {
1476 let expected = FastL2NormSquared.evaluate(&*shifted);
1477 let err = (shifted_square_norm - expected).abs() / expected.abs();
1478 assert!(
1479 err < 5.0e-4, "failed diff check, got {}, expected {} - relative error = {}",
1481 shifted_square_norm,
1482 expected,
1483 err
1484 );
1485 }
1486
1487 if const { D == 1 } {
1490 let self_inner_product = 2.0 * shifted_square_norm.sqrt()
1491 / (inner_product_correction * (bv.len() as f32).sqrt());
1492 assert!(
1493 (self_inner_product - 0.8).abs() < 0.13,
1494 "self inner-product should be close to 0.8. Instead, it's {}",
1495 self_inner_product
1496 );
1497 }
1498 }
1499
1500 {
1501 let QueryMeta {
1502 inner_product_correction,
1503 bit_sum,
1504 offset,
1505 metric_specific,
1506 } = q.meta();
1507
1508 let shifted_square_norm = metric_specific;
1509 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1510 preprocessed
1511 .shifted
1512 .iter_mut()
1513 .for_each(|i| *i /= preprocessed.shifted_norm);
1514
1515 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1516 quantizer
1517 .transform
1518 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1519 .unwrap();
1520
1521 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1522 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1523
1524 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1525
1526 {
1528 let expected = FastL2NormSquared.evaluate(&*shifted);
1529 let err = (shifted_square_norm - expected).abs() / expected.abs();
1530 assert!(
1531 err < 2e-7,
1532 "failed diff check, got {}, expected {} - relative error = {}",
1533 shifted_square_norm,
1534 expected,
1535 err
1536 );
1537 }
1538
1539 {
1541 let expected = shifted_square_norm.sqrt() * scale;
1542 let got = inner_product_correction;
1543
1544 let err = (expected - got).abs();
1545 assert!(
1546 err < 1.0e-7,
1547 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1548 expected,
1549 got,
1550 err
1551 );
1552 }
1553
1554 {
1556 let expected = min / scale;
1557 let got = offset;
1558
1559 let err = (expected - got).abs();
1560 assert!(
1561 err < 1.0e-7,
1562 "\"sum_scale\": expected {}, got {}, error = {}",
1563 expected,
1564 got,
1565 err
1566 );
1567 }
1568
1569 {
1571 let expected = (0..q.len())
1572 .map(|i| q.vector().get(i).unwrap())
1573 .sum::<i64>() as f32;
1574
1575 let got = bit_sum;
1576
1577 let err = (expected - got).abs();
1578 assert!(
1579 err < 1.0e-7,
1580 "\"offset\": expected {}, got {}, error = {}",
1581 expected,
1582 got,
1583 err
1584 );
1585 }
1586 }
1587
1588 {
1590 let s: f32 = f.data.iter().sum::<f32>();
1592 assert_eq!(s, f.meta.sum);
1593
1594 {
1596 let expected = FastL2Norm.evaluate(&*shifted);
1597 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1598 assert!(
1599 err < 2e-7,
1600 "failed diff check, got {}, expected {} - relative error = {}",
1601 f.meta.shifted_norm,
1602 expected,
1603 err
1604 );
1605 }
1606
1607 assert_eq!(
1608 f.meta.metric_specific,
1609 f.meta.shifted_norm * f.meta.shifted_norm,
1610 "metric specific data for squared l2 is the square shifted norm",
1611 );
1612 }
1613 }
1614
1615 quantizer
1618 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1619 .unwrap();
1620 assert_eq!(b.meta(), DataMeta::default());
1621
1622 quantizer
1623 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1624 .unwrap();
1625 assert_eq!(q.meta(), QueryMeta::default());
1626
1627 f.data.fill(f32::INFINITY);
1628 quantizer
1629 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1630 .unwrap();
1631 assert!(f.data.iter().all(|&i| i == 0.0));
1632 assert_eq!(f.meta.sum, 0.0);
1633 assert_eq!(f.meta.metric_specific, 0.0);
1634 }
1635
1636 fn test_ip<const Q: usize, const D: usize, Perm>(
1637 setup: &Setup,
1638 problem: &test_util::TestProblem,
1639 computed_means: &[f32],
1640 pre_scale: PreScale,
1641 rng: &mut StdRng,
1642 ctx: &dyn Display,
1643 ) where
1644 Unsigned: Representation<Q>,
1645 Unsigned: Representation<D>,
1646 Perm: PermutationStrategy<Q>,
1647 for<'a> SphericalQuantizer:
1648 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1649 for<'a> SphericalQuantizer:
1650 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1651 {
1652 assert_eq!(setup.nrows, problem.data.nrows());
1653 assert_eq!(setup.ncols, problem.data.ncols());
1654
1655 let scoped_global = ScopedAllocator::global();
1656 let distribution = Uniform::new(0, setup.nrows).unwrap();
1657 let quantizer = SphericalQuantizer::train(
1658 problem.data.as_view(),
1659 setup.transform,
1660 SupportedMetric::InnerProduct,
1661 pre_scale,
1662 rng,
1663 GlobalAllocator,
1664 )
1665 .unwrap();
1666
1667 let scale = get_scale(pre_scale, &quantizer);
1668
1669 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1670 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1671 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1672
1673 assert_eq!(
1674 quantizer.mean_norm.into_inner(),
1675 problem.mean_norm as f32,
1676 "computed mean norm should not apply scale"
1677 );
1678 let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1679 assert_eq!(&*scaled_means, quantizer.shift());
1680
1681 let ip: CompensatedIP = quantizer.as_functor();
1682
1683 assert_eq!(ip.dim, quantizer.output_dim() as f32);
1684 assert_eq!(
1685 ip.squared_shift_norm,
1686 FastL2NormSquared.evaluate(quantizer.shift())
1687 );
1688
1689 for _ in 0..setup.num_trials {
1690 let i = distribution.sample(rng);
1691 let v = problem.data.row(i);
1692
1693 quantizer
1694 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1695 .unwrap();
1696 quantizer
1697 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1698 .unwrap();
1699 quantizer
1700 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1701 .unwrap();
1702
1703 let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1704 .map(|(a, b)| scale * a - b)
1705 .collect();
1706
1707 {
1709 let DataMetaF32 {
1710 inner_product_correction,
1711 bit_sum,
1712 metric_specific,
1713 } = b.meta().to_full(ARCH);
1714
1715 let inner_product_with_centroid = metric_specific;
1716
1717 let bv = b.vector();
1719 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1720 assert_eq!(s, bit_sum as usize);
1721
1722 let inner_product: MathematicalValue<f32> =
1724 InnerProduct::evaluate(&*shifted, quantizer.shift());
1725
1726 let diff = (inner_product.into_inner() - inner_product_with_centroid).abs();
1727 assert!(
1728 diff < 1.53e-5,
1729 "got a diff of {}. Expected = {}, got = {} -- context: {}",
1730 diff,
1731 inner_product.into_inner(),
1732 inner_product_with_centroid,
1733 ctx,
1734 );
1735
1736 if const { D == 1 } {
1739 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1740 / (inner_product_correction * (bv.len() as f32).sqrt());
1741 assert!(
1742 (self_inner_product - 0.8).abs() < 0.12,
1743 "self inner-product should be close to 0.8. Instead, it's {}",
1744 self_inner_product
1745 );
1746 }
1747 }
1748
1749 {
1750 let QueryMeta {
1751 inner_product_correction,
1752 bit_sum,
1753 offset,
1754 metric_specific,
1755 } = q.meta();
1756
1757 let inner_product_with_centroid = metric_specific;
1758 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1759 preprocessed
1760 .shifted
1761 .iter_mut()
1762 .for_each(|i| *i /= preprocessed.shifted_norm);
1763
1764 let mut transformed = vec![0.0f32; quantizer.output_dim()];
1765 quantizer
1766 .transform
1767 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1768 .unwrap();
1769
1770 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1771 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1772
1773 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1774
1775 {
1777 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
1778 let got = inner_product_correction;
1779
1780 let err = (expected - got).abs();
1781 assert!(
1782 err < 1.0e-7,
1783 "\"innerproduct_scale\": expected {}, got {}, error = {}",
1784 expected,
1785 got,
1786 err
1787 );
1788 }
1789
1790 {
1792 let expected = min / scale;
1793 let got = offset;
1794
1795 let err = (expected - got).abs();
1796 assert!(
1797 err < 1.0e-7,
1798 "\"sum_scale\": expected {}, got {}, error = {}",
1799 expected,
1800 got,
1801 err
1802 );
1803 }
1804
1805 {
1807 let expected = (0..q.len())
1808 .map(|i| q.vector().get(i).unwrap())
1809 .sum::<i64>() as f32;
1810
1811 let got = bit_sum;
1812
1813 let err = (expected - got).abs();
1814 assert!(
1815 err < 1.0e-7,
1816 "\"offset\": expected {}, got {}, error = {}",
1817 expected,
1818 got,
1819 err
1820 );
1821 }
1822
1823 {
1825 let inner_product: MathematicalValue<f32> =
1827 InnerProduct::evaluate(&*shifted, quantizer.shift());
1828 assert_eq!(inner_product.into_inner(), inner_product_with_centroid);
1829 }
1830 }
1831
1832 {
1834 let s: f32 = f.data.iter().sum::<f32>();
1836 assert_eq!(s, f.meta.sum);
1837
1838 {
1840 let expected = FastL2Norm.evaluate(&*shifted);
1841 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1842 assert!(
1843 err < 2e-7,
1844 "failed diff check, got {}, expected {} - relative error = {}",
1845 f.meta.shifted_norm,
1846 expected,
1847 err
1848 );
1849 }
1850
1851 let inner_product: MathematicalValue<f32> =
1853 InnerProduct::evaluate(&*shifted, quantizer.shift());
1854 assert_eq!(inner_product.into_inner(), f.meta.metric_specific,);
1855 }
1856 }
1857
1858 quantizer
1861 .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1862 .unwrap();
1863 assert_eq!(b.meta(), DataMeta::default());
1864
1865 quantizer
1866 .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1867 .unwrap();
1868 assert_eq!(q.meta(), QueryMeta::default());
1869
1870 f.data.fill(f32::INFINITY);
1871 quantizer
1872 .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1873 .unwrap();
1874 assert!(f.data.iter().all(|&i| i == 0.0));
1875 assert_eq!(f.meta.sum, 0.0);
1876 assert_eq!(f.meta.metric_specific, 0.0);
1877 }
1878
1879 fn test_cosine<const Q: usize, const D: usize, Perm>(
1880 setup: &Setup,
1881 problem: &test_util::TestProblem,
1882 pre_scale: PreScale,
1883 rng: &mut StdRng,
1884 ) where
1885 Unsigned: Representation<Q>,
1886 Unsigned: Representation<D>,
1887 Perm: PermutationStrategy<Q>,
1888 for<'a> SphericalQuantizer:
1889 CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1890 for<'a> SphericalQuantizer:
1891 CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1892 {
1893 assert_eq!(setup.nrows, problem.data.nrows());
1894 assert_eq!(setup.ncols, problem.data.ncols());
1895
1896 let scoped_global = ScopedAllocator::global();
1897 let distribution = Uniform::new(0, setup.nrows).unwrap();
1898 let quantizer = SphericalQuantizer::train(
1899 problem.data.as_view(),
1900 setup.transform,
1901 SupportedMetric::Cosine,
1902 pre_scale,
1903 rng,
1904 GlobalAllocator,
1905 )
1906 .unwrap();
1907
1908 let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1909 let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1910 let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1911
1912 let cosine: CompensatedCosine = quantizer.as_functor();
1913
1914 assert_eq!(cosine.inner.dim, quantizer.output_dim() as f32);
1915 assert_eq!(
1916 cosine.inner.squared_shift_norm,
1917 FastL2NormSquared.evaluate(quantizer.shift())
1918 );
1919
1920 const IP_BOUND: f32 = 2.6e-3;
1921
1922 let mut test_row = |v: &[f32]| {
1923 let vnorm = (FastL2Norm).evaluate(v);
1924 let v_normalized: Vec<f32> = v
1925 .iter()
1926 .map(|i| if vnorm == 0.0 { 0.0 } else { *i / vnorm })
1927 .collect();
1928
1929 quantizer
1930 .compress_into_with(v, b.reborrow_mut(), scoped_global)
1931 .unwrap();
1932
1933 quantizer
1934 .compress_into_with(v, q.reborrow_mut(), scoped_global)
1935 .unwrap();
1936
1937 quantizer
1938 .compress_into_with(v, f.reborrow_mut(), scoped_global)
1939 .unwrap();
1940
1941 let shifted: Vec<f32> = std::iter::zip(v_normalized.iter(), quantizer.shift().iter())
1942 .map(|(a, b)| a - b)
1943 .collect();
1944
1945 {
1947 let DataMetaF32 {
1948 inner_product_correction,
1949 bit_sum,
1950 metric_specific,
1951 } = b.meta().to_full(ARCH);
1952
1953 let inner_product_with_centroid = metric_specific;
1954
1955 let bv = b.vector();
1957 let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1958 assert_eq!(s, bit_sum as usize);
1959
1960 let inner_product: MathematicalValue<f32> =
1963 InnerProduct::evaluate(&*shifted, quantizer.shift());
1964
1965 let abs = (inner_product.into_inner() - inner_product_with_centroid).abs();
1966 let relative = abs / inner_product.into_inner().abs();
1967
1968 assert!(
1969 abs < 1e-7 || relative < IP_BOUND,
1970 "got an abs/rel of {}/{} with a bound of {}/{}",
1971 abs,
1972 relative,
1973 1e-7,
1974 IP_BOUND
1975 );
1976
1977 if const { D == 1 } {
1980 let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1981 / (inner_product_correction * (bv.len() as f32).sqrt());
1982 assert!(
1983 (self_inner_product - 0.8).abs() < 0.11,
1984 "self inner-product should be close to 0.8. Instead, it's {}",
1985 self_inner_product
1986 );
1987 }
1988 }
1989
1990 {
1991 let QueryMeta {
1992 inner_product_correction,
1993 bit_sum,
1994 offset,
1995 metric_specific,
1996 } = q.meta();
1997
1998 let inner_product_with_centroid = metric_specific;
1999 let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
2000 preprocessed
2001 .shifted
2002 .iter_mut()
2003 .for_each(|i| *i /= preprocessed.shifted_norm);
2004
2005 let mut transformed = vec![0.0f32; quantizer.output_dim()];
2006 quantizer
2007 .transform
2008 .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
2009 .unwrap();
2010
2011 let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
2012 let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
2013
2014 let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
2015
2016 {
2018 let expected = (FastL2Norm).evaluate(&*shifted) * scale;
2019 let got = inner_product_correction;
2020
2021 let err = (expected - got).abs();
2022 assert!(
2023 err < 1.0e-7,
2024 "\"innerproduct_scale\": expected {}, got {}, error = {}",
2025 expected,
2026 got,
2027 err
2028 );
2029 }
2030
2031 {
2033 let expected = min / scale;
2034 let got = offset;
2035
2036 let err = (expected - got).abs();
2037 assert!(
2038 err < 1.0e-7,
2039 "\"sum_scale\": expected {}, got {}, error = {}",
2040 expected,
2041 got,
2042 err
2043 );
2044 }
2045
2046 {
2048 let expected = (0..q.len())
2049 .map(|i| q.vector().get(i).unwrap())
2050 .sum::<i64>() as f32;
2051
2052 let got = bit_sum;
2053
2054 let err = (expected - got).abs();
2055 assert!(
2056 err < 1.0e-7,
2057 "\"offset\": expected {}, got {}, error = {}",
2058 expected,
2059 got,
2060 err
2061 );
2062 }
2063
2064 {
2066 let inner_product: MathematicalValue<f32> =
2068 InnerProduct::evaluate(&*shifted, quantizer.shift());
2069
2070 let err = (inner_product.into_inner() - inner_product_with_centroid).abs()
2071 / inner_product.into_inner().abs();
2072 assert!(
2073 err < IP_BOUND,
2074 "\"offset\": expected {}, got {}, error = {}",
2075 inner_product.into_inner(),
2076 inner_product_with_centroid,
2077 err
2078 );
2079 }
2080 }
2081
2082 {
2084 let s: f32 = f.data.iter().sum::<f32>();
2086 assert_eq!(s, f.meta.sum);
2087
2088 {
2090 let expected = FastL2Norm.evaluate(&*shifted);
2091 let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
2092 assert!(
2093 err < 2e-7,
2094 "failed diff check, got {}, expected {} - relative error = {}",
2095 f.meta.shifted_norm,
2096 expected,
2097 err
2098 );
2099 }
2100
2101 let inner_product: MathematicalValue<f32> =
2103 InnerProduct::evaluate(&*shifted, quantizer.shift());
2104 let err = (inner_product.into_inner() - f.meta.metric_specific).abs()
2105 / inner_product.into_inner().abs();
2106 assert!(
2107 err < IP_BOUND,
2108 "\"offset\": expected {}, got {}, error = {}",
2109 inner_product.into_inner(),
2110 f.meta.metric_specific,
2111 err
2112 );
2113 }
2114 };
2115
2116 for _ in 0..setup.num_trials {
2117 let i = distribution.sample(rng);
2118 let v = problem.data.row(i);
2119 test_row(v);
2120 }
2121
2122 let zero = vec![0.0f32; quantizer.input_dim()];
2124 test_row(&zero);
2125 }
2126
2127 fn _test_oom_resiliance<T>(quantizer: &SphericalQuantizer, data: &[f32], dst: &mut T)
2128 where
2129 for<'a> T: ReborrowMut<'a>,
2130 for<'a> SphericalQuantizer: CompressIntoWith<
2131 &'a [f32],
2132 <T as ReborrowMut<'a>>::Target,
2133 ScopedAllocator<'a>,
2134 Error = CompressionError,
2135 >,
2136 {
2137 let mut succeeded = false;
2138 let mut failed = false;
2139 for max_allocations in 0..10 {
2140 match quantizer.compress_into_with(
2141 data,
2142 dst.reborrow_mut(),
2143 ScopedAllocator::new(&test_util::LimitedAllocator::new(max_allocations)),
2144 ) {
2145 Ok(()) => {
2146 succeeded = true;
2147 }
2148 Err(CompressionError::AllocatorError(_)) => {
2149 failed = true;
2150 }
2151 Err(other) => {
2152 panic!("received an unexpected error: {:?}", other);
2153 }
2154 }
2155 }
2156 assert!(succeeded);
2157 assert!(failed);
2158 }
2159
2160 fn test_oom_resiliance<const Q: usize, const D: usize, Perm>(
2161 setup: &Setup,
2162 problem: &test_util::TestProblem,
2163 pre_scale: PreScale,
2164 rng: &mut StdRng,
2165 ) where
2166 Unsigned: Representation<Q>,
2167 Unsigned: Representation<D>,
2168 Perm: PermutationStrategy<Q>,
2169 for<'a> SphericalQuantizer: CompressIntoWith<
2170 &'a [f32],
2171 DataMut<'a, D>,
2172 ScopedAllocator<'a>,
2173 Error = CompressionError,
2174 >,
2175 for<'a> SphericalQuantizer: CompressIntoWith<
2176 &'a [f32],
2177 QueryMut<'a, Q, Perm>,
2178 ScopedAllocator<'a>,
2179 Error = CompressionError,
2180 >,
2181 {
2182 assert_eq!(setup.nrows, problem.data.nrows());
2183 assert_eq!(setup.ncols, problem.data.ncols());
2184
2185 let quantizer = SphericalQuantizer::train(
2186 problem.data.as_view(),
2187 setup.transform,
2188 SupportedMetric::SquaredL2,
2189 pre_scale,
2190 rng,
2191 GlobalAllocator,
2192 )
2193 .unwrap();
2194
2195 let data = problem.data.row(0);
2197 _test_oom_resiliance::<Data<D, _>>(
2198 &quantizer,
2199 data,
2200 &mut Data::new_boxed(quantizer.output_dim()),
2201 );
2202 _test_oom_resiliance::<Query<Q, Perm, _>>(
2203 &quantizer,
2204 data,
2205 &mut Query::new_boxed(quantizer.output_dim()),
2206 );
2207 _test_oom_resiliance::<FullQuery<_>>(
2208 &quantizer,
2209 data,
2210 &mut FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap(),
2211 );
2212 }
2213
2214 fn test_quantizer<const Q: usize, const D: usize, Perm>(setup: &Setup, rng: &mut StdRng)
2215 where
2216 Unsigned: Representation<Q>,
2217 Unsigned: Representation<D>,
2218 Perm: PermutationStrategy<Q>,
2219 for<'a> SphericalQuantizer: CompressIntoWith<
2220 &'a [f32],
2221 DataMut<'a, D>,
2222 ScopedAllocator<'a>,
2223 Error = CompressionError,
2224 >,
2225 for<'a> SphericalQuantizer: CompressIntoWith<
2226 &'a [f32],
2227 QueryMut<'a, Q, Perm>,
2228 ScopedAllocator<'a>,
2229 Error = CompressionError,
2230 >,
2231 {
2232 let problem = test_util::create_test_problem(setup.nrows, setup.ncols, rng);
2233 let computed_means_f32: Vec<_> = problem.means.iter().map(|i| *i as f32).collect();
2234
2235 let scales = [
2236 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2237 PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2238 PreScale::ReciprocalMeanNorm,
2239 ];
2240
2241 for scale in scales {
2242 let ctx = &lazy_format!("dim = {}, scale = {:?}", setup.ncols, scale);
2243
2244 test_l2::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng);
2245 test_ip::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng, ctx);
2246 test_cosine::<Q, D, Perm>(setup, &problem, scale, rng);
2247 }
2248
2249 test_oom_resiliance::<Q, D, Perm>(setup, &problem, PreScale::ReciprocalMeanNorm, rng);
2250 }
2251
2252 #[test]
2253 fn test_spherical_quantizer() {
2254 let mut rng = StdRng::seed_from_u64(0xab516aef1ce61640);
2255 for dim in [56, 72, 128, 255] {
2256 let setup = Setup {
2257 transform: TransformKind::PaddingHadamard {
2258 target_dim: TargetDim::Same,
2259 },
2260 nrows: 64,
2261 ncols: dim,
2262 num_trials: 10,
2263 };
2264
2265 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2266 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2267 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2268 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2269
2270 let setup = Setup {
2271 transform: TransformKind::DoubleHadamard {
2272 target_dim: TargetDim::Same,
2273 },
2274 nrows: 64,
2275 ncols: dim,
2276 num_trials: 10,
2277 };
2278 test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2279 test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2280 test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2281 test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2282 }
2283 }
2284
2285 #[test]
2290 fn err_dim_cannot_be_zero() {
2291 let data = rowmajor::Owned::from_element(10, 0, 0.0f32);
2292 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2293 let err = SphericalQuantizer::train(
2294 data.as_view(),
2295 TransformKind::DoubleHadamard {
2296 target_dim: TargetDim::Same,
2297 },
2298 SupportedMetric::SquaredL2,
2299 PreScale::None,
2300 &mut rng,
2301 GlobalAllocator,
2302 )
2303 .unwrap_err();
2304 assert_eq!(err.to_string(), "data dim cannot be zero");
2305 }
2306
2307 #[test]
2308 fn err_norm_must_be_positive() {
2309 let data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2310 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2311 let err = SphericalQuantizer::train(
2312 data.as_view(),
2313 TransformKind::DoubleHadamard {
2314 target_dim: TargetDim::Same,
2315 },
2316 SupportedMetric::SquaredL2,
2317 PreScale::None,
2318 &mut rng,
2319 GlobalAllocator,
2320 )
2321 .unwrap_err();
2322 assert_eq!(err.to_string(), "norm must be positive");
2323 }
2324
2325 #[test]
2326 fn err_norm_cannot_be_infinity() {
2327 let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2328 *data.element_mut(2, 5) = f32::INFINITY;
2329
2330 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2331 let err = SphericalQuantizer::train(
2332 data.as_view(),
2333 TransformKind::DoubleHadamard {
2334 target_dim: TargetDim::Same,
2335 },
2336 SupportedMetric::SquaredL2,
2337 PreScale::None,
2338 &mut rng,
2339 GlobalAllocator,
2340 )
2341 .unwrap_err();
2342 assert_eq!(err.to_string(), "computed norm contains infinity or NaN");
2343 }
2344
2345 #[test]
2346 fn err_reciprocal_norm_cannot_be_infinity() {
2347 let mut data = rowmajor::Owned::from_element(10, 10, 0.0f32);
2348 *data.element_mut(2, 5) = 2.93863e-39;
2349
2350 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2351 let err = SphericalQuantizer::train(
2352 data.as_view(),
2353 TransformKind::DoubleHadamard {
2354 target_dim: TargetDim::Same,
2355 },
2356 SupportedMetric::SquaredL2,
2357 PreScale::ReciprocalMeanNorm,
2358 &mut rng,
2359 GlobalAllocator,
2360 )
2361 .unwrap_err();
2362 assert_eq!(err.to_string(), "reciprocal norm contains infinity or NaN");
2363 }
2364
2365 #[test]
2366 fn err_mean_norm_cannot_be_zero_generate() {
2367 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2368 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2369 let err = SphericalQuantizer::generate(
2370 centroid,
2371 0.0,
2372 TransformKind::DoubleHadamard {
2373 target_dim: TargetDim::Same,
2374 },
2375 SupportedMetric::SquaredL2,
2376 None,
2377 &mut rng,
2378 GlobalAllocator,
2379 )
2380 .unwrap_err();
2381 assert_eq!(err.to_string(), "norm must be positive");
2382 }
2383
2384 #[test]
2385 fn err_scale_cannot_be_zero_generate() {
2386 let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2387 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2388 let err = SphericalQuantizer::generate(
2389 centroid,
2390 1.0,
2391 TransformKind::DoubleHadamard {
2392 target_dim: TargetDim::Same,
2393 },
2394 SupportedMetric::SquaredL2,
2395 Some(0.0),
2396 &mut rng,
2397 GlobalAllocator,
2398 )
2399 .unwrap_err();
2400 assert_eq!(err.to_string(), "pre-scale must be positive");
2401 }
2402
2403 #[test]
2404 fn compression_errors_data() {
2405 let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2406 let data = rowmajor::Owned::<f32>::from_fn(16, 12, |_| StandardNormal {}.sample(&mut rng));
2407
2408 let quantizer = SphericalQuantizer::train(
2409 data.as_view(),
2410 TransformKind::PaddingHadamard {
2411 target_dim: TargetDim::Same,
2412 },
2413 SupportedMetric::SquaredL2,
2414 PreScale::None,
2415 &mut rng,
2416 GlobalAllocator,
2417 )
2418 .unwrap();
2419
2420 let scoped_global = ScopedAllocator::global();
2421
2422 {
2424 let mut query: Vec<f32> = quantizer.shift().to_vec();
2425 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2426 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2427
2428 for i in 0..query.len() {
2429 let last = query[i];
2430 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2431 query[i] = v;
2432
2433 let err = quantizer
2434 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2435 .unwrap_err();
2436
2437 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2438
2439 let err = quantizer
2440 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2441 .unwrap_err();
2442
2443 assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2444 }
2445 query[i] = last;
2446 }
2447 }
2448
2449 {
2451 let query: Vec<f32> = vec![1000000.0; quantizer.input_dim()];
2452 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2453
2454 let err = quantizer
2455 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2456 .unwrap_err();
2457
2458 let expected = "encoding error - you may need to scale the entire dataset to reduce its dynamic range";
2459
2460 assert_eq!(err.to_string(), expected, "failed for {:?}", query);
2461 }
2462
2463 for len in [quantizer.input_dim() - 1, quantizer.input_dim() + 1] {
2465 let query = vec![0.0f32; len];
2466 let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2467 let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2468
2469 let err = quantizer
2470 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2471 .unwrap_err();
2472 assert_eq!(
2473 err,
2474 CompressionError::SourceDimensionMismatch {
2475 expected: quantizer.input_dim(),
2476 }
2477 );
2478
2479 let err = quantizer
2480 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2481 .unwrap_err();
2482 assert_eq!(
2483 err,
2484 CompressionError::SourceDimensionMismatch {
2485 expected: quantizer.input_dim(),
2486 }
2487 );
2488 }
2489
2490 for len in [quantizer.output_dim() - 1, quantizer.output_dim() + 1] {
2491 let query = vec![0.0f32; quantizer.input_dim()];
2492 let mut d = Data::<1, _>::new_boxed(len);
2493 let mut q = Query::<4, BitTranspose, _>::new_boxed(len);
2494
2495 let err = quantizer
2496 .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2497 .unwrap_err();
2498 assert_eq!(
2499 err,
2500 CompressionError::DestinationDimensionMismatch {
2501 expected: quantizer.output_dim(),
2502 }
2503 );
2504
2505 let err = quantizer
2506 .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2507 .unwrap_err();
2508 assert_eq!(
2509 err,
2510 CompressionError::DestinationDimensionMismatch {
2511 expected: quantizer.output_dim(),
2512 }
2513 );
2514 }
2515 }
2516
2517 #[test]
2518 fn centroid_scaling_happens_in_generate() {
2519 let centroid = Poly::from_iter(
2520 [1088.6732f32, 1393.32, 1547.877].into_iter(),
2521 GlobalAllocator,
2522 )
2523 .unwrap();
2524 let mean_norm = 2359.27;
2525 let pre_scale = 1.0 / mean_norm;
2526
2527 let quantizer = SphericalQuantizer::generate(
2528 centroid,
2529 mean_norm,
2530 TransformKind::Null,
2531 SupportedMetric::InnerProduct,
2532 Some(pre_scale),
2533 &mut StdRng::seed_from_u64(10),
2534 GlobalAllocator,
2535 )
2536 .unwrap();
2537
2538 let mut v = Data::<4, _>::new_boxed(quantizer.input_dim());
2539 let data: &[f32] = &[1000.34, 1456.32, 1234.5446];
2540 assert!(
2541 quantizer
2542 .compress_into_with(data, v.reborrow_mut(), ScopedAllocator::global())
2543 .is_ok(),
2544 "if this failed, the likely culprit is exceeding the value of the 16-bit correction terms"
2545 );
2546 }
2547}
2548
2549#[cfg(feature = "flatbuffers")]
2550#[cfg(test)]
2551mod test_serialization {
2552 use rand::{SeedableRng, rngs::StdRng};
2553
2554 use super::*;
2555 use crate::{
2556 algorithms::transforms::TargetDim,
2557 flatbuffers::{self as fb, to_flatbuffer},
2558 poly, test_util,
2559 };
2560
2561 #[test]
2562 fn test_serialization_happy_path() {
2563 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2564 let problem = test_util::create_test_problem(10, 128, &mut rng);
2565
2566 let low = NonZeroUsize::new(100).unwrap();
2567 let high = NonZeroUsize::new(150).unwrap();
2568
2569 let kinds = [
2570 TransformKind::Null,
2572 TransformKind::DoubleHadamard {
2574 target_dim: TargetDim::Same,
2575 },
2576 TransformKind::DoubleHadamard {
2577 target_dim: TargetDim::Natural,
2578 },
2579 TransformKind::DoubleHadamard {
2580 target_dim: TargetDim::Override(low),
2581 },
2582 TransformKind::DoubleHadamard {
2583 target_dim: TargetDim::Override(high),
2584 },
2585 TransformKind::PaddingHadamard {
2587 target_dim: TargetDim::Same,
2588 },
2589 TransformKind::PaddingHadamard {
2590 target_dim: TargetDim::Natural,
2591 },
2592 TransformKind::PaddingHadamard {
2593 target_dim: TargetDim::Override(low),
2594 },
2595 TransformKind::PaddingHadamard {
2596 target_dim: TargetDim::Override(high),
2597 },
2598 #[cfg(all(not(miri), feature = "linalg"))]
2600 TransformKind::RandomRotation {
2601 target_dim: TargetDim::Same,
2602 },
2603 #[cfg(all(not(miri), feature = "linalg"))]
2604 TransformKind::RandomRotation {
2605 target_dim: TargetDim::Natural,
2606 },
2607 #[cfg(all(not(miri), feature = "linalg"))]
2608 TransformKind::RandomRotation {
2609 target_dim: TargetDim::Override(low),
2610 },
2611 #[cfg(all(not(miri), feature = "linalg"))]
2612 TransformKind::RandomRotation {
2613 target_dim: TargetDim::Override(high),
2614 },
2615 ];
2616
2617 let pre_scales = [
2618 PreScale::None,
2619 PreScale::Some(Positive::new(0.5).unwrap()),
2620 PreScale::Some(Positive::new(1.0).unwrap()),
2621 PreScale::Some(Positive::new(1.5).unwrap()),
2622 PreScale::ReciprocalMeanNorm,
2623 ];
2624
2625 let alloc = GlobalAllocator;
2626 for kind in kinds.into_iter() {
2627 for metric in SupportedMetric::all() {
2628 for pre_scale in pre_scales {
2629 let quantizer = SphericalQuantizer::train(
2630 problem.data.as_view(),
2631 kind,
2632 metric,
2633 pre_scale,
2634 &mut rng,
2635 alloc,
2636 )
2637 .unwrap();
2638
2639 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2640 let proto =
2641 flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2642 let reloaded = SphericalQuantizer::try_unpack(alloc, proto).unwrap();
2643 assert_eq!(quantizer, reloaded, "failed on transform {:?}", kind);
2644 }
2645 }
2646 }
2647 }
2648
2649 #[test]
2650 fn test_error_checking() {
2651 let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2652 let problem = test_util::create_test_problem(10, 128, &mut rng);
2653
2654 let transform = TransformKind::DoubleHadamard {
2655 target_dim: TargetDim::Same,
2656 };
2657
2658 let alloc = GlobalAllocator;
2659 let mut make_quantizer = || {
2660 SphericalQuantizer::train(
2661 problem.data.as_view(),
2662 transform,
2663 SupportedMetric::SquaredL2,
2664 PreScale::None,
2665 &mut rng,
2666 alloc,
2667 )
2668 .unwrap()
2669 };
2670
2671 type E = DeserializationError;
2672
2673 {
2675 let mut quantizer = make_quantizer();
2676 quantizer.mean_norm = unsafe { Positive::new_unchecked(0.0) };
2680
2681 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2682 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2683 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2684 assert_eq!(err, E::MissingNorm);
2685 }
2686
2687 {
2689 let mut quantizer = make_quantizer();
2690
2691 quantizer.mean_norm = unsafe { Positive::new_unchecked(-1.0) };
2695
2696 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2697 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2698 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2699 assert_eq!(err, E::MissingNorm);
2700 }
2701
2702 {
2704 let mut quantizer = make_quantizer();
2705
2706 quantizer.pre_scale = unsafe { Positive::new_unchecked(0.0) };
2709
2710 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2711 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2712 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2713 assert_eq!(err, E::PreScaleNotPositive);
2714 }
2715
2716 {
2718 let mut quantizer = make_quantizer();
2719 quantizer.shift = poly!([1.0, 2.0, 3.0], alloc).unwrap();
2720
2721 let data = to_flatbuffer(|buf| quantizer.pack(buf));
2722 let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2723 let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2724 assert_eq!(err, E::DimMismatch);
2725 }
2726 }
2727}