Skip to main content

diskann_quantization/spherical/
quantizer.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::num::NonZeroUsize;
7
8use diskann_utils::{ReborrowMut, views::MatrixView};
9use diskann_vector::{
10    MathematicalValue, Norm, PureDistanceFunction, distance::InnerProduct, norm::FastL2Norm,
11};
12#[cfg(feature = "flatbuffers")]
13use flatbuffers::{FlatBufferBuilder, WIPOffset};
14use rand::{Rng, RngCore};
15use thiserror::Error;
16
17use super::{
18    CompensatedCosine, CompensatedIP, CompensatedSquaredL2, DataMeta, DataMetaError, DataMut,
19    FullQueryMeta, FullQueryMut, QueryMeta, QueryMut, SupportedMetric,
20};
21use crate::{
22    AsFunctor, CompressIntoWith,
23    algorithms::{
24        heap::SliceHeap,
25        transforms::{NewTransformError, Transform, TransformFailed, TransformKind},
26    },
27    alloc::{Allocator, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone},
28    bits::{PermutationStrategy, Representation, Unsigned},
29    num::Positive,
30    utils::{CannotBeEmpty, compute_means_and_average_norm, compute_normalized_means},
31};
32#[cfg(feature = "flatbuffers")]
33use crate::{
34    algorithms::transforms::TransformError, flatbuffers::spherical, spherical::InvalidMetric,
35};
36
37///////////////
38// Quantizer //
39///////////////
40
41#[derive(Debug)]
42#[cfg_attr(test, derive(PartialEq))]
43pub struct SphericalQuantizer<A = GlobalAllocator>
44where
45    A: Allocator,
46{
47    /// The offset to apply to each vector.
48    shift: Poly<[f32], A>,
49
50    /// The [`SphericalQuantizer`] supports several different strategies for performing the
51    /// distance-preserving transformation on dataset vectors, which may be applicable in
52    /// different scenarios.
53    ///
54    /// The different transformations may have restrictions on the number of supported dimensions.
55    /// While we will accept all non-zero input dimensions, the output dimension of a transform
56    /// may be higher or lower, depending on the configuration.
57    transform: Transform<A>,
58
59    /// The metric meant to be used by the quantizer.
60    metric: SupportedMetric,
61
62    /// When processing queries, it may be beneficial to modify the query norm to match the
63    /// dataset norm.
64    ///
65    /// This is only applicable when `InnerProduct` and `Cosine` are used, but serves to
66    /// move the query into the dynamic range of the quantization.
67    ///
68    /// You would think that the normalization step in RabitQ would mitigate this, but
69    /// that is not always right since range-adjustment happens before centering.
70    mean_norm: Positive<f32>,
71
72    /// To support 16-bit constants which have a limited dynamic range, we allow a
73    /// pre-scaling parameter that is multiplied to each value in compressed vectors.
74    ///
75    /// This allows to transparent handling of compressing integral data, which can
76    /// otherwise easily overflow `f16`.
77    pre_scale: Positive<f32>,
78}
79
80impl<A> TryClone for SphericalQuantizer<A>
81where
82    A: Allocator,
83{
84    fn try_clone(&self) -> Result<Self, AllocatorError> {
85        SphericalQuantizer::try_clone(self)
86    }
87}
88
89#[derive(Debug, Clone, Copy, Error)]
90#[non_exhaustive]
91pub enum TrainError {
92    #[error("data dim cannot be zero")]
93    DimCannotBeZero,
94    #[error("data cannot be empty")]
95    DataCannotBeEmpty,
96    #[error("pre-scale must be positive")]
97    PrescaleNotPositive,
98    #[error("norm must be positive")]
99    NormNotPositive,
100    #[error("computed norm contains infinity or NaN")]
101    NormNotFinite,
102    #[error("reciprocal norm contains infinity or NaN")]
103    ReciprocalNormNotFinite,
104    #[error(transparent)]
105    AllocatorError(#[from] AllocatorError),
106}
107
108impl<A> SphericalQuantizer<A>
109where
110    A: Allocator,
111{
112    /// Return the number dimensions this quantizer has been trained for.
113    pub fn input_dim(&self) -> usize {
114        self.shift.len()
115    }
116
117    /// Return the dimension of the post-transformed vector.
118    ///
119    /// Output storage vectors should use this dimension instead of `self.dim()` because
120    /// in general, the output dim **may** be different from the input dimension.
121    pub fn output_dim(&self) -> usize {
122        self.transform.output_dim()
123    }
124
125    /// Return the per-dimension shift vector.
126    ///
127    /// This vector is meant to accomplish two goals:
128    ///
129    /// 1. Centers the data around the training dataset mean.
130    /// 2. Offsets each dimension into a range that can be encoded in unsigned values.
131    pub fn shift(&self) -> &[f32] {
132        &self.shift
133    }
134
135    /// Return the approximate mean norm of the training data.
136    pub fn mean_norm(&self) -> Positive<f32> {
137        self.mean_norm
138    }
139
140    /// Return the pre-scaling parameter for data. This value is multiplied to every
141    /// compressed vector to adjust its dynamic range.
142    ///
143    /// A value of 1.0 means that no scaling is occurring.
144    pub fn pre_scale(&self) -> Positive<f32> {
145        self.pre_scale
146    }
147
148    /// Return a reference to the allocator used by this data structure.
149    pub fn allocator(&self) -> &A {
150        self.shift.allocator()
151    }
152
153    /// Return an independently allocated copy of this quantizer.
154    pub fn try_clone(&self) -> Result<Self, AllocatorError> {
155        Ok(Self {
156            shift: self.shift.try_clone()?,
157            transform: self.transform.try_clone()?,
158            metric: self.metric,
159            mean_norm: self.mean_norm,
160            pre_scale: self.pre_scale,
161        })
162    }
163
164    /// A lower-level constructor that accepts a centroid, mean norm, and pre-scale directly.
165    pub fn generate(
166        mut centroid: Poly<[f32], A>,
167        mean_norm: f32,
168        transform: TransformKind,
169        metric: SupportedMetric,
170        pre_scale: Option<f32>,
171        rng: &mut dyn RngCore,
172        allocator: A,
173    ) -> Result<Self, TrainError> {
174        let pre_scale = match pre_scale {
175            Some(v) => Positive::new(v).map_err(|_| TrainError::PrescaleNotPositive)?,
176            None => crate::num::POSITIVE_ONE_F32,
177        };
178
179        let dim = match NonZeroUsize::new(centroid.len()) {
180            Some(dim) => dim,
181            None => {
182                return Err(TrainError::DimCannotBeZero);
183            }
184        };
185
186        let mean_norm = Positive::new(mean_norm).map_err(|_| TrainError::NormNotPositive)?;
187
188        // We passed in a 'rng' so `Transform::new` will not fail.
189        let transform = match Transform::new(transform, dim, Some(rng), allocator.clone()) {
190            Ok(v) => v,
191            Err(NewTransformError::RngMissing(_)) => unreachable!("An Rng was provided"),
192            Err(NewTransformError::AllocatorError(err)) => {
193                return Err(TrainError::AllocatorError(err));
194            }
195        };
196
197        // Transform the centroid by the pre-scale.
198        centroid
199            .iter_mut()
200            .for_each(|v| *v *= pre_scale.into_inner());
201
202        Ok(SphericalQuantizer {
203            shift: centroid,
204            transform,
205            metric,
206            mean_norm,
207            pre_scale,
208        })
209    }
210
211    /// Return the metric used by this quantizer.
212    pub fn metric(&self) -> SupportedMetric {
213        self.metric
214    }
215
216    /// Construct a quantizer for vectors in the distribution of `data`.
217    ///
218    /// The type of distance-preserving transform to use is selected by the [`TransformKind`].
219    ///
220    /// Vectors compressed with this quantizer will be **metric specific** and optimized for
221    /// distance computations rather than reconstruction. This means that vectors compressed
222    /// targeting the inner-product distance will not return meaningful results if used for
223    /// L2 distance computations.
224    ///
225    /// Additionally, vectors compressed when using the [`SupportedMetric::Cosine`] distance
226    /// will be implicitly normalized before being compressed to enable better compression.
227    ///
228    /// If argument `pre_scale` is given, then all vectors compressed by this quantizer will
229    /// first be scaled by this value. Note that if given, `pre_scale` **must** be positive.
230    pub fn train<T, R>(
231        data: MatrixView<T>,
232        transform: TransformKind,
233        metric: SupportedMetric,
234        pre_scale: PreScale,
235        rng: &mut R,
236        allocator: A,
237    ) -> Result<Self, TrainError>
238    where
239        T: Copy + Into<f64> + Into<f32>,
240        R: Rng,
241    {
242        // An inner implementation erasing the type of the random number generator to
243        // cut down on excess monomorphization.
244        #[inline(never)]
245        fn train<T, A>(
246            data: MatrixView<T>,
247            transform: TransformKind,
248            metric: SupportedMetric,
249            pre_scale: PreScale,
250            rng: &mut dyn RngCore,
251            allocator: A,
252        ) -> Result<SphericalQuantizer<A>, TrainError>
253        where
254            T: Copy + Into<f64> + Into<f32>,
255            A: Allocator,
256        {
257            // This check is repeated in `Self::generate`, but we prefer to bail as early
258            // as possible if we detect an error.
259            if data.ncols() == 0 {
260                return Err(TrainError::DimCannotBeZero);
261            }
262
263            let (centroid, mean_norm) = match metric {
264                SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => {
265                    compute_means_and_average_norm(data)
266                }
267                SupportedMetric::Cosine => (
268                    compute_normalized_means(data)
269                        .map_err(|_: CannotBeEmpty| TrainError::DataCannotBeEmpty)?,
270                    1.0,
271                ),
272            };
273
274            let mean_norm = mean_norm as f32;
275
276            if mean_norm <= 0.0 {
277                return Err(TrainError::NormNotPositive);
278            }
279
280            if !mean_norm.is_finite() {
281                return Err(TrainError::NormNotFinite);
282            }
283
284            // Determining if (and how) the pre-scaling term will be calculated.
285            let pre_scale: Positive<f32> = match pre_scale {
286                PreScale::None => crate::num::POSITIVE_ONE_F32,
287                PreScale::Some(v) => v,
288                PreScale::ReciprocalMeanNorm => {
289                    // We've checked that `mean_norm` is both positive and finite.
290                    //
291                    // It's possible that when converted to `f32`,
292                    //
293                    // Taking the reciprocal is well defined. However, since the norms
294                    // and scales in `compute_means_and_average_norm` are done using `f64`,
295                    // it's possible that the computed `mean_norm` is subnormal, leading
296                    // to the reciprocal being infinity.
297                    let pre_scale = Positive::new(1.0 / mean_norm)
298                        .map_err(|_| TrainError::ReciprocalNormNotFinite)?;
299
300                    if !pre_scale.into_inner().is_finite() {
301                        return Err(TrainError::ReciprocalNormNotFinite);
302                    }
303
304                    pre_scale
305                }
306            };
307
308            // Allow the pre-scaling to take place inside `Self::generate`.
309            let centroid =
310                Poly::from_iter(centroid.into_iter().map(|i| i as f32), allocator.clone())?;
311
312            SphericalQuantizer::generate(
313                centroid,
314                mean_norm,
315                transform,
316                metric,
317                Some(pre_scale.into_inner()),
318                rng,
319                allocator,
320            )
321        }
322
323        train(data, transform, metric, pre_scale, rng, allocator)
324    }
325
326    /// Rescale the argument `v` to be in the rough dynamic range of the training dataset.
327    pub fn rescale(&self, v: &mut [f32]) {
328        let norm = FastL2Norm.evaluate(&*v);
329        let m = self.mean_norm.into_inner() / norm;
330        v.iter_mut().for_each(|i| *i *= m);
331    }
332
333    /// Private helper function to do common data pre-processing.
334    ///
335    /// # Panics
336    ///
337    /// Panics if `data.len() != self.dim()`.
338    fn preprocess<'a>(
339        &self,
340        data: &[f32],
341        allocator: ScopedAllocator<'a>,
342    ) -> Result<Preprocessed<'a>, CompressionError> {
343        assert_eq!(data.len(), self.input_dim(), "Data dimension is incorrect.");
344
345        // Fold in pre-scaling with the potential norm corretion for cosine.
346        //
347        // NOTE: When we're computing Cosine Similarity, we normalize the vector. As such,
348        // the `pre_scale` parameter become irrelvant since it just gets normalized away.
349        let scale = self.pre_scale.into_inner();
350        let mul: f32 = match self.metric {
351            SupportedMetric::Cosine => {
352                let norm: f32 = (FastL2Norm).evaluate(data);
353                if norm == 0.0 { 1.0 } else { 1.0 / norm }
354            }
355            SupportedMetric::SquaredL2 | SupportedMetric::InnerProduct => scale,
356        };
357
358        // Center the vector and compute the squared norm of the shifted vector.
359        let shifted = Poly::from_iter(
360            std::iter::zip(data.iter(), self.shift.iter()).map(|(&f, &s)| mul * f - s),
361            allocator,
362        )?;
363
364        let shifted_norm = FastL2Norm.evaluate(&*shifted);
365        if !shifted_norm.is_finite() {
366            return Err(CompressionError::InputContainsNaN);
367        }
368        let inner_product_with_centroid = match self.metric {
369            SupportedMetric::SquaredL2 => None,
370            SupportedMetric::InnerProduct | SupportedMetric::Cosine => {
371                let ip: MathematicalValue<f32> = InnerProduct::evaluate(&*shifted, &*self.shift);
372                Some(ip.into_inner())
373            }
374        };
375
376        Ok(Preprocessed {
377            shifted,
378            shifted_norm,
379            inner_product_with_centroid,
380        })
381    }
382}
383
384/// Pre-scaling selector for spherical quantization training. Pre-scaling adjusts the
385/// dynamic range of the data (usually decreasing it uniformly) to keep the correction terms
386/// within the range expressible by 16-bit floating point numbers.
387#[derive(Debug, Clone, Copy)]
388pub enum PreScale {
389    /// Do not use any pre-scaling.
390    None,
391    /// Pre-scale all data by the specified amount.
392    Some(Positive<f32>),
393    /// Heuristically estimate a pre-scaling parameter by using the inverse approximate
394    /// mean norm. This will nearly normalize in-distribution vectors.
395    ReciprocalMeanNorm,
396}
397
398#[cfg(feature = "flatbuffers")]
399#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
400#[derive(Debug, Clone, Error, PartialEq)]
401#[non_exhaustive]
402pub enum DeserializationError {
403    #[error(transparent)]
404    TransformError(#[from] TransformError),
405    #[error("unrecognized flatbuffer identifier")]
406    UnrecognizedIdentifier,
407    #[error("transform length not equal to centroid")]
408    DimMismatch,
409    #[error("norm is missing or is not positive")]
410    MissingNorm,
411    #[error("pre-scale is missing or is not positive")]
412    PreScaleNotPositive,
413
414    #[error(transparent)]
415    InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
416
417    #[error(transparent)]
418    InvalidMetric(#[from] InvalidMetric),
419
420    #[error(transparent)]
421    AllocatorError(#[from] AllocatorError),
422}
423
424#[cfg(feature = "flatbuffers")]
425#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
426impl<A> SphericalQuantizer<A>
427where
428    A: Allocator + Clone,
429{
430    /// Pack `self` into `buf` using the [`spherical::SphericalQuantizer`] serialized
431    /// representation.
432    pub(crate) fn pack<'a, FA>(
433        &self,
434        buf: &mut FlatBufferBuilder<'a, FA>,
435    ) -> WIPOffset<spherical::SphericalQuantizer<'a>>
436    where
437        FA: flatbuffers::Allocator + 'a,
438    {
439        // Save the centroid vector.
440        let centroid = buf.create_vector(&self.shift);
441
442        // Save the transform.
443        let transform = self.transform.pack(buf);
444
445        // Finish up.
446        spherical::SphericalQuantizer::create(
447            buf,
448            &spherical::SphericalQuantizerArgs {
449                centroid: Some(centroid),
450                transform: Some(transform),
451                metric: self.metric.into(),
452                mean_norm: self.mean_norm.into_inner(),
453                pre_scale: self.pre_scale.into_inner(),
454            },
455        )
456    }
457
458    /// Attempt to unpack `self` from a serialized [`spherical::SphericalQuantizer`]
459    /// serialized representation, returning any encountered error.
460    pub(crate) fn try_unpack(
461        alloc: A,
462        proto: spherical::SphericalQuantizer<'_>,
463    ) -> Result<Self, DeserializationError> {
464        let metric: SupportedMetric = proto.metric().try_into()?;
465
466        // Unpack the centroid.
467        let shift = Poly::from_iter(proto.centroid().into_iter(), alloc.clone())?;
468
469        // Unpack the transform.
470        let transform = Transform::try_unpack(alloc, proto.transform())?;
471
472        // Ensure consistency between the shift dimensions and the transform.
473        if shift.len() != transform.input_dim() {
474            return Err(DeserializationError::DimMismatch);
475        }
476
477        // Make sure we get a sane value for the mean norm.
478        let mean_norm =
479            Positive::new(proto.mean_norm()).map_err(|_| DeserializationError::MissingNorm)?;
480
481        let pre_scale = Positive::new(proto.pre_scale())
482            .map_err(|_| DeserializationError::PreScaleNotPositive)?;
483
484        Ok(Self {
485            shift,
486            transform,
487            metric,
488            mean_norm,
489            pre_scale,
490        })
491    }
492}
493
494struct Preprocessed<'a> {
495    shifted: Poly<[f32], ScopedAllocator<'a>>,
496    shifted_norm: f32,
497    inner_product_with_centroid: Option<f32>,
498}
499
500impl Preprocessed<'_> {
501    /// Return the metric specific correction term as sumamrized below:
502    ///
503    /// * Inner Product: The inner product between the shifted vector and the centroid.
504    /// * Squared L2: The squared norm of the shifted vector.
505    fn metric_specific(&self) -> f32 {
506        match self.inner_product_with_centroid {
507            Some(ip) => ip,
508            None => self.shifted_norm * self.shifted_norm,
509        }
510    }
511}
512
513///////////////////////
514// Distance Functors //
515///////////////////////
516
517impl<A> AsFunctor<CompensatedSquaredL2> for SphericalQuantizer<A>
518where
519    A: Allocator,
520{
521    fn as_functor(&self) -> CompensatedSquaredL2 {
522        CompensatedSquaredL2::new(self.output_dim())
523    }
524}
525
526impl<A> AsFunctor<CompensatedIP> for SphericalQuantizer<A>
527where
528    A: Allocator,
529{
530    fn as_functor(&self) -> CompensatedIP {
531        CompensatedIP::new(&self.shift, self.output_dim())
532    }
533}
534
535impl<A> AsFunctor<CompensatedCosine> for SphericalQuantizer<A>
536where
537    A: Allocator,
538{
539    fn as_functor(&self) -> CompensatedCosine {
540        CompensatedCosine::new(self.as_functor())
541    }
542}
543
544/////////////////
545// Compression //
546/////////////////
547
548#[derive(Debug, Error, Clone, Copy, PartialEq)]
549#[non_exhaustive]
550pub enum CompressionError {
551    #[error("input contains NaN")]
552    InputContainsNaN,
553
554    #[error("expected source vector to have length {expected}")]
555    SourceDimensionMismatch { expected: usize },
556
557    #[error("expected destination vector to have length {expected}")]
558    DestinationDimensionMismatch { expected: usize },
559
560    #[error(
561        "encoding error - you may need to scale the entire dataset to reduce its dynamic range"
562    )]
563    EncodingError(#[from] DataMetaError),
564
565    #[error(transparent)]
566    AllocatorError(#[from] AllocatorError),
567}
568
569fn check_dims(
570    input: usize,
571    output: usize,
572    from: usize,
573    into: usize,
574) -> Result<(), CompressionError> {
575    if from != input {
576        return Err(CompressionError::SourceDimensionMismatch { expected: input });
577    }
578    if into != output {
579        return Err(CompressionError::DestinationDimensionMismatch { expected: output });
580    }
581    Ok(())
582}
583
584/// Helper trait to dispatch to a faster 1-bit implementation and use the slower
585/// maximum-cosine algorithm when more than 1 bit is used.
586trait FinishCompressing {
587    fn finish_compressing(
588        &mut self,
589        preprocessed: &Preprocessed<'_>,
590        transformed: &[f32],
591        transformed_norm: f32,
592        allocator: ScopedAllocator<'_>,
593    ) -> Result<(), CompressionError>;
594}
595
596impl FinishCompressing for DataMut<'_, 1> {
597    fn finish_compressing(
598        &mut self,
599        preprocessed: &Preprocessed<'_>,
600        transformed: &[f32],
601        transformed_norm: f32,
602        _: ScopedAllocator<'_>,
603    ) -> Result<(), CompressionError> {
604        // Compute signed quantized vector (-1 or 1)
605        // and also populate the unsigned bit representation in `into` output vector.
606        let mut quant_raw_inner_product = 0.0f32;
607        let mut bit_sum = 0u32;
608        transformed.iter().enumerate().for_each(|(i, &r)| {
609            let bit: u8 = if r > 0.0 { 1 } else { 0 };
610
611            quant_raw_inner_product += r.abs();
612            bit_sum += <u8 as Into<u32>>::into(bit);
613
614            // SAFETY: From check 1, we know that `i < into.len()`.
615            unsafe { self.vector_mut().set_unchecked(i, bit) };
616        });
617
618        // The value we just computed for `quant_raw_inner_product` is:
619        // ```
620        // Y = <x', x> * sqrt(D)                        [1]
621        // ```
622        // The inner product correction term is
623        // ```
624        //       2 |X|
625        // -----------------                            [2]
626        // <x', x> * sqrt(D)
627        // ```
628        // [1] substitutes directly into [2] and we get
629        // ```
630        // 2 |X|
631        // -----
632        //   Y
633        // ```
634        // Therefore, the inner product correction term is
635        // ```
636        // 2.0 * shifted_norm / quant_raw_inner_product
637        // ```
638        let inner_product_correction =
639            2.0 * transformed_norm * preprocessed.shifted_norm / quant_raw_inner_product;
640        self.set_meta(DataMeta::new(
641            inner_product_correction,
642            preprocessed.metric_specific(),
643            bit_sum,
644        )?);
645        Ok(())
646    }
647}
648
649impl FinishCompressing for DataMut<'_, 2> {
650    fn finish_compressing(
651        &mut self,
652        preprocessed: &Preprocessed<'_>,
653        transformed: &[f32],
654        transformed_norm: f32,
655        allocator: ScopedAllocator<'_>,
656    ) -> Result<(), CompressionError> {
657        compress_via_maximum_cosine(
658            self.reborrow_mut(),
659            preprocessed,
660            transformed,
661            transformed_norm,
662            allocator,
663        )
664    }
665}
666
667impl FinishCompressing for DataMut<'_, 4> {
668    fn finish_compressing(
669        &mut self,
670        preprocessed: &Preprocessed<'_>,
671        transformed: &[f32],
672        transformed_norm: f32,
673        allocator: ScopedAllocator<'_>,
674    ) -> Result<(), CompressionError> {
675        compress_via_maximum_cosine(
676            self.reborrow_mut(),
677            preprocessed,
678            transformed,
679            transformed_norm,
680            allocator,
681        )
682    }
683}
684
685impl FinishCompressing for DataMut<'_, 8> {
686    fn finish_compressing(
687        &mut self,
688        preprocessed: &Preprocessed<'_>,
689        transformed: &[f32],
690        transformed_norm: f32,
691        allocator: ScopedAllocator<'_>,
692    ) -> Result<(), CompressionError> {
693        compress_via_maximum_cosine(
694            self.reborrow_mut(),
695            preprocessed,
696            transformed,
697            transformed_norm,
698            allocator,
699        )
700    }
701}
702
703//////////////////////
704// Data Compression //
705//////////////////////
706
707impl<A> CompressIntoWith<&[f32], FullQueryMut<'_>, ScopedAllocator<'_>> for SphericalQuantizer<A>
708where
709    A: Allocator,
710{
711    type Error = CompressionError;
712
713    /// Compress the input vector `from` into the bitslice `into`.
714    ///
715    /// # Error
716    ///
717    /// Returns an error if
718    /// * The input contains `NaN`.
719    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
720    ///   dimensionality as the quantizer.
721    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
722    ///   dimensionality as the output of the distance-preserving transform. Importantely,
723    ///   this **may** be different than `self.dim()` and should be retrieved from
724    ///   `self.output_dim()`.
725    fn compress_into_with(
726        &self,
727        from: &[f32],
728        mut into: FullQueryMut<'_>,
729        allocator: ScopedAllocator<'_>,
730    ) -> Result<(), Self::Error> {
731        let input_dim = self.shift.len();
732        let output_dim = self.output_dim();
733        check_dims(input_dim, output_dim, from.len(), into.len())?;
734
735        let mut preprocessed = self.preprocess(from, allocator)?;
736
737        // If the preprocessed norm is zero, then we tried to compress the center directly.
738        // In this case, we can get the correct behavior by setting `into` to all zeros.
739        if preprocessed.shifted_norm == 0.0 {
740            into.vector_mut().fill(0.0);
741            *into.meta_mut() = Default::default();
742            return Ok(());
743        }
744
745        preprocessed
746            .shifted
747            .iter_mut()
748            .for_each(|v| *v /= preprocessed.shifted_norm);
749
750        // Transformation can fail due to OOM - we want to handle that gracefully.
751        //
752        // If the transformation fails because we provided the wrong sizes, that is a hard
753        // program bug.
754        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
755        match self
756            .transform
757            .transform_into(into.vector_mut(), &preprocessed.shifted, allocator)
758        {
759            Ok(()) => {}
760            Err(TransformFailed::AllocatorError(err)) => {
761                return Err(CompressionError::AllocatorError(err));
762            }
763            Err(TransformFailed::SourceMismatch { .. })
764            | Err(TransformFailed::DestinationMismatch { .. }) => {
765                panic!(
766                    "The sizes of these arrays should already be checked - this is a logic error"
767                );
768            }
769            #[cfg(feature = "linalg")]
770            Err(TransformFailed::SgemmError(_)) => {
771                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
772            }
773        }
774
775        *into.meta_mut() = FullQueryMeta {
776            sum: into.vector().iter().sum::<f32>(),
777            shifted_norm: preprocessed.shifted_norm,
778            metric_specific: preprocessed.metric_specific(),
779        };
780        Ok(())
781    }
782}
783
784impl<const NBITS: usize, A> CompressIntoWith<&[f32], DataMut<'_, NBITS>, ScopedAllocator<'_>>
785    for SphericalQuantizer<A>
786where
787    A: Allocator,
788    Unsigned: Representation<NBITS>,
789    for<'a> DataMut<'a, NBITS>: FinishCompressing,
790{
791    type Error = CompressionError;
792
793    /// Compress the input vector `from` into the bitslice `into`.
794    ///
795    /// # Error
796    ///
797    /// Returns an error if
798    /// * The input contains `NaN`.
799    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
800    ///   dimensionality as the quantizer.
801    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
802    ///   dimensionality as the output of the distance-preserving transform. Importantely,
803    ///   this **may** be different than `self.dim()` and should be retrieved from
804    ///   `self.output_dim()`.
805    fn compress_into_with(
806        &self,
807        from: &[f32],
808        mut into: DataMut<'_, NBITS>,
809        allocator: ScopedAllocator<'_>,
810    ) -> Result<(), Self::Error> {
811        let input_dim = self.shift.len();
812        let output_dim = self.output_dim();
813        check_dims(input_dim, output_dim, from.len(), into.len())?;
814
815        let mut preprocessed = self.preprocess(from, allocator)?;
816
817        if preprocessed.shifted_norm == 0.0 {
818            into.set_meta(DataMeta::default());
819            return Ok(());
820        }
821
822        let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
823        preprocessed
824            .shifted
825            .iter_mut()
826            .for_each(|v| *v /= preprocessed.shifted_norm);
827
828        // Transformation can fail due to OOM - we want to handle that gracefully.
829        //
830        // If the transformation fails because we provided the wrong sizes, that is a hard
831        // program bug.
832        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
833        match self
834            .transform
835            .transform_into(&mut transformed, &preprocessed.shifted, allocator)
836        {
837            Ok(()) => {}
838            Err(TransformFailed::AllocatorError(err)) => {
839                return Err(CompressionError::AllocatorError(err));
840            }
841            Err(TransformFailed::SourceMismatch { .. })
842            | Err(TransformFailed::DestinationMismatch { .. }) => {
843                panic!(
844                    "The sizes of these arrays should already be checked - this is a logic error"
845                );
846            }
847            #[cfg(feature = "linalg")]
848            Err(TransformFailed::SgemmError(_)) => {
849                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
850            }
851        }
852
853        let transformed_norm = if self.transform.preserves_norms() {
854            1.0
855        } else {
856            (FastL2Norm).evaluate(&*transformed)
857        };
858
859        into.finish_compressing(&preprocessed, &transformed, transformed_norm, allocator)?;
860        Ok(())
861    }
862}
863
864struct AsNonZero<const NBITS: usize>;
865impl<const NBITS: usize> AsNonZero<NBITS> {
866    // Lint: Unwrap is being used in a const-context.
867    #[allow(clippy::unwrap_used)]
868    const NON_ZERO: NonZeroUsize = NonZeroUsize::new(NBITS).unwrap();
869}
870
871fn compress_via_maximum_cosine<const NBITS: usize>(
872    mut data: DataMut<'_, NBITS>,
873    preprocessed: &Preprocessed<'_>,
874    transformed: &[f32],
875    transformed_norm: f32,
876    allocator: ScopedAllocator<'_>,
877) -> Result<(), CompressionError>
878where
879    Unsigned: Representation<NBITS>,
880{
881    assert_eq!(data.len(), transformed.len());
882
883    // Find the value we will use to multiply `transformed` to round it to the lattice
884    // element that has the maximum cosine-similarity.
885    let optimal_scale =
886        maximize_cosine_similarity(transformed, AsNonZero::<NBITS>::NON_ZERO, allocator)?;
887
888    let domain = Unsigned::domain_const::<NBITS>();
889    let min = *domain.start() as f32;
890    let max = *domain.end() as f32;
891    let offset = max / 2.0;
892
893    let mut self_inner_product = 0.0f32;
894    let mut bit_sum = 0u32;
895    for (i, t) in transformed.iter().enumerate() {
896        let v = (*t * optimal_scale + offset).clamp(min, max).round();
897        let dv = v - offset;
898        self_inner_product = dv.mul_add(*t, self_inner_product);
899
900        let v = v as u8;
901        bit_sum += <u8 as Into<u32>>::into(v);
902
903        // SAFETY: We have checked that `data.len() == transformed.len()`, so this access
904        // is in-bounds.
905        //
906        // Further, by construction, `v` is encodable by the `Unsigned`.
907        unsafe { data.vector_mut().set_unchecked(i, v) };
908    }
909
910    let shifted_norm = preprocessed.shifted_norm;
911    let inner_product_correction = (transformed_norm * shifted_norm) / self_inner_product;
912    data.set_meta(DataMeta::new(
913        inner_product_correction,
914        preprocessed.metric_specific(),
915        bit_sum,
916    )?);
917    Ok(())
918}
919
920// This struct does 2 things:
921//
922// 1. Records the index in `v` and `rounded` and the scaling parameter so that
923//   `value * v[position]` gets rounded to `rounded[position] + 1` while
924//   `(value - epsilon) * v[position]` is rounded to `rounded[position]` for some small
925//   epsilon.
926//
927//   Informally, "what's the smallest scaling factor so `v[position]` gets rounded to
928//   the next value.
929//
930// 2. Imposes a total ordering on `f32` values so it can be used in a `BinaryHeap`.
931//   Additionally, ordering is reverse so that `BinaryHeap` models a min-heap instead
932//   of a max-heap.
933#[derive(Debug, Clone, Copy)]
934struct Pair {
935    value: f32,
936    position: u32,
937}
938
939impl PartialEq for Pair {
940    fn eq(&self, other: &Self) -> bool {
941        self.value.eq(&other.value)
942    }
943}
944
945impl Eq for Pair {}
946impl PartialOrd for Pair {
947    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
948        Some(self.cmp(other))
949    }
950}
951impl Ord for Pair {
952    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
953        other
954            .value
955            .partial_cmp(&self.value)
956            .unwrap_or(std::cmp::Ordering::Equal)
957    }
958}
959
960/// This is a tricky function - please read carefully.
961///
962/// Given a vector `v` compute the scaling factor `s` such that the cosine similarity
963/// betwen `v` and `r` is maximized where `r` is defined as
964/// ```math
965/// let offset = (2^(num_bits) - 1) / 2;
966/// let r = (s * v + offset).round().clamp(0, 2^num_bits - 1) - offset
967/// ```
968///
969/// More informally, maximize the inner product between `v` and the points in a square
970/// lattice with 2^num_bits values in each dimension, centered around zero. This latice
971/// takes the values (+0.5, -0.5, +1.5, -1.5 ...) to give equal weight above and below zero.
972///
973/// It works by slowly increasing the factor `s` such that the rounding of only one
974/// dimension in `v` is changed at a time. A running tally of the cosine similarity is
975/// computed for each scaling factor until we've processed `D * 2^(num_bits - 1)` possible
976/// scaling factors, where `D` is the length of `v`.
977///
978/// The best scaling factor is returned.
979///
980/// Refer to algorithm 1 in <https://arxiv.org/pdf/2409.09913>.
981///
982/// # Panics
983///
984/// Panics is `v.is_empty()`.
985///
986/// # Implementation details
987///
988/// We work with the absolute value of the elements in the vector `v`.
989/// This does not affect the final result as the scaling works the same in both the
990/// positive and negative directions but simplifies the book keeping.
991fn maximize_cosine_similarity(
992    v: &[f32],
993    num_bits: NonZeroUsize,
994    allocator: ScopedAllocator<'_>,
995) -> Result<f32, AllocatorError> {
996    // Initially, the lattice element has the value `0.5` for all dimensions.
997    // This means the initial inner product between `v` and the rounded term is simply
998    // `0.5 * sum(abs.(v))`. The absolute value is used because the latice element is
999    // always in the direction of the components in `v`.
1000    let mut current_ip = 0.5 * v.iter().map(|i| i.abs() as f64).sum::<f64>();
1001    let mut current_square_norm = 0.25 * (v.len() as f64);
1002
1003    // Book keeping for the current value of the rounded vector.
1004    // The true numeric value is 0.5 less than this (in the direction of `v`), but we use
1005    // integers for a smaller memory footprint.
1006    let mut rounded = Poly::broadcast(1u16, v.len(), allocator)?;
1007
1008    // Compute the critical values and store them on a heap.
1009    //
1010    // The binary heap will keep track of the minimum critical value. Multiplying `v` by the
1011    // minimum critical value `s` means that `s * v` will only change `rounded` from its
1012    // current value at a single index (the position associated with `s`).
1013    let eps = 0.0001f32;
1014    let one_and_change = 1.0 + eps;
1015    let mut base = Poly::from_iter(
1016        v.iter().enumerate().map(|(position, value)| {
1017            let value = one_and_change / value.abs();
1018            Pair {
1019                value,
1020                position: position as u32,
1021            }
1022        }),
1023        allocator,
1024    )?;
1025
1026    // Lint: This is a private method and all the callers have an invariant that they check
1027    // for non-empty inputs.
1028    #[allow(clippy::expect_used)]
1029    let mut critical_values =
1030        SliceHeap::new(&mut base).expect("calling code should not allow the slice to be empty");
1031
1032    let mut max_similarity = f64::NEG_INFINITY;
1033    let mut optimal_scale = f32::default();
1034    let stop = (2usize).pow(num_bits.get() as u32 - 1) as u16;
1035
1036    loop {
1037        let mut should_break = false;
1038        critical_values.update_root(|pair| {
1039            let Pair { value, position } = *pair;
1040            if value == f32::MAX {
1041                should_break = true;
1042                return;
1043            }
1044
1045            let r = &mut rounded[position as usize];
1046            let vp = &v[position as usize];
1047
1048            let old_r = *r;
1049            // By the nature of cricital values, only `r` will change in `rounded` when
1050            // multiplying by `value`. And that change will be to increase by 1.
1051            *r += 1;
1052
1053            // The inner product estimate simply increases by `vp.abs()` because:
1054            //
1055            // * `r` is the only value in `rounded` that changes.
1056            // * `r` is increased by 1.
1057            current_ip += vp.abs() as f64;
1058
1059            // This uses the formula
1060            // ```math
1061            // (x + 1)^2 - x^2 = x^2 + 2x + 1 - x^2
1062            //                 = 2x + 1
1063            // ```
1064            // substitute `x = y - 1/2` to obtain the true value associated with rounded and
1065            // we get
1066            // ```math
1067            // 2 ( y - 1/2 ) + 1 = 2y - 1 + 1
1068            //                   = 2y
1069            // ```
1070            // Therefore, the change in the estimate for the square norm of `rounded` is
1071            // `2 * old_r`.
1072            current_square_norm += (2 * old_r) as f64;
1073
1074            // Compute the current cosine similarity and update max if needed.
1075            let similarity = current_ip / current_square_norm.sqrt();
1076            if similarity > max_similarity {
1077                max_similarity = similarity;
1078                optimal_scale = value;
1079            }
1080
1081            // Compute the scaling factor that will change this dimension to the next value.
1082            if *r < stop {
1083                *pair = Pair {
1084                    value: (*r as f32 + eps) / vp.abs(),
1085                    position,
1086                };
1087            } else {
1088                *pair = Pair {
1089                    value: f32::MAX,
1090                    position,
1091                };
1092            }
1093        });
1094        if should_break {
1095            break;
1096        }
1097    }
1098
1099    Ok(optimal_scale)
1100}
1101
1102///////////////////////
1103// Query Compression //
1104///////////////////////
1105
1106impl<const NBITS: usize, Perm, A>
1107    CompressIntoWith<&[f32], QueryMut<'_, NBITS, Perm>, ScopedAllocator<'_>>
1108    for SphericalQuantizer<A>
1109where
1110    Unsigned: Representation<NBITS>,
1111    Perm: PermutationStrategy<NBITS>,
1112    A: Allocator,
1113{
1114    type Error = CompressionError;
1115
1116    /// Compress the input vector `from` into the bitslice `into`.
1117    ///
1118    /// # Error
1119    ///
1120    /// Returns an error if
1121    /// * The input contains `NaN`.
1122    /// * `from.len() != self.dim()`: Vector to be compressed must have the same
1123    ///   dimensionality as the quantizer.
1124    /// * `into.len() != self.output_dim()`: Compressed vector must have the same
1125    ///   dimensionality as the output of the distance-preserving transform. Importantely,
1126    ///   this **may** be different than `self.dim()` and should be retrieved from
1127    ///   `self.output_dim()`.
1128    fn compress_into_with(
1129        &self,
1130        from: &[f32],
1131        mut into: QueryMut<'_, NBITS, Perm>,
1132        allocator: ScopedAllocator<'_>,
1133    ) -> Result<(), Self::Error> {
1134        let input_dim = self.shift.len();
1135        let output_dim = self.output_dim();
1136        check_dims(input_dim, output_dim, from.len(), into.len())?;
1137
1138        let mut preprocessed = self.preprocess(from, allocator)?;
1139
1140        if preprocessed.shifted_norm == 0.0 {
1141            into.set_meta(QueryMeta::default());
1142            return Ok(());
1143        }
1144
1145        preprocessed
1146            .shifted
1147            .iter_mut()
1148            .for_each(|v| *v /= preprocessed.shifted_norm);
1149
1150        let mut transformed = Poly::broadcast(0.0f32, output_dim, allocator)?;
1151
1152        // Transformation can fail due to OOM - we want to handle that gracefully.
1153        //
1154        // If the transformation fails because we provided the wrong sizes, that is a hard
1155        // program bug.
1156        #[expect(clippy::panic, reason = "the dimensions should already be as expected")]
1157        match self
1158            .transform
1159            .transform_into(&mut transformed, &preprocessed.shifted, allocator)
1160        {
1161            Ok(()) => {}
1162            Err(TransformFailed::AllocatorError(err)) => {
1163                return Err(CompressionError::AllocatorError(err));
1164            }
1165            Err(TransformFailed::SourceMismatch { .. })
1166            | Err(TransformFailed::DestinationMismatch { .. }) => {
1167                panic!(
1168                    "The sizes of these arrays should already be checked - this is a logic error"
1169                );
1170            }
1171            #[cfg(feature = "linalg")]
1172            Err(TransformFailed::SgemmError(_)) => {
1173                panic!("SGEMM should not fail with valid dimensions - this is a logic error");
1174            }
1175        }
1176
1177        // Compute the minimum and maximum values of the transformed vector.
1178        let (min, max) = transformed
1179            .iter()
1180            .fold((f32::MAX, f32::MIN), |(min, max), i| {
1181                (i.min(min), i.max(max))
1182            });
1183
1184        let domain = Unsigned::domain_const::<NBITS>();
1185        let lo = (*domain.start()) as f32;
1186        let hi = (*domain.end()) as f32;
1187
1188        let scale = (max - min) / hi;
1189        let mut bit_sum: f32 = 0.0;
1190        transformed.iter().enumerate().for_each(|(i, v)| {
1191            let c = ((v - min) / scale).round().clamp(lo, hi);
1192            bit_sum += c;
1193
1194            // Lint: We have verified that `into.len() == transformed.len()`, so the index
1195            // `i` is in bounds.
1196            //
1197            // Further, `c` has beem clamped to `[0, 2^NBITS - 1]` and is thus encodable
1198            // with the NBITS-bit unsigned representation.
1199            #[allow(clippy::unwrap_used)]
1200            into.vector_mut().set(i, c as i64).unwrap();
1201        });
1202
1203        // Finish up the compensation terms.
1204        into.set_meta(QueryMeta {
1205            inner_product_correction: preprocessed.shifted_norm * scale,
1206            bit_sum,
1207            offset: min / scale,
1208            metric_specific: preprocessed.metric_specific(),
1209        });
1210
1211        Ok(())
1212    }
1213}
1214
1215///////////
1216// Tests //
1217///////////
1218
1219#[cfg(not(miri))]
1220#[cfg(test)]
1221mod tests {
1222    use super::*;
1223
1224    use std::fmt::Display;
1225
1226    use diskann_utils::{
1227        ReborrowMut, lazy_format,
1228        views::{self, Matrix},
1229    };
1230    use diskann_vector::{PureDistanceFunction, norm::FastL2NormSquared};
1231    use diskann_wide::ARCH;
1232    use rand::{
1233        SeedableRng,
1234        distr::{Distribution, Uniform},
1235        rngs::StdRng,
1236    };
1237    use rand_distr::StandardNormal;
1238
1239    use crate::{
1240        algorithms::transforms::TargetDim,
1241        alloc::GlobalAllocator,
1242        bits::{BitTranspose, Dense},
1243        spherical::{Data, DataMetaF32, FullQuery, Query},
1244        test_util,
1245    };
1246
1247    // Test cosine-similarity maximizer
1248    #[test]
1249    fn test_cosine_similarity_maximizer() {
1250        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
1251        let num_trials = 10000;
1252        let num_bits = NonZeroUsize::new(3).unwrap();
1253
1254        let scale_distribution = Uniform::new(0.5f32, 10.0f32).unwrap();
1255
1256        let run_test = |target: [f32; 4]| {
1257            let scale =
1258                maximize_cosine_similarity(&target, num_bits, ScopedAllocator::global()).unwrap();
1259
1260            let mut best: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
1261            let mut best_similarity: f32 = f32::NEG_INFINITY;
1262
1263            // This crazy series of nested loops performs an exhaustive search over the
1264            // encoding space.
1265            let min = -3.5;
1266            for i0 in 0..8 {
1267                for i1 in 0..8 {
1268                    for i2 in 0..8 {
1269                        for i3 in 0..8 {
1270                            let p: [f32; 4] = [
1271                                min + (i0 as f32),
1272                                min + (i1 as f32),
1273                                min + (i2 as f32),
1274                                min + (i3 as f32),
1275                            ];
1276
1277                            let sim: MathematicalValue<f32> =
1278                                diskann_vector::distance::Cosine::evaluate(&p, &target);
1279                            let sim = sim.into_inner();
1280                            if sim > best_similarity {
1281                                best_similarity = sim;
1282                                // Transform into an integer starting at zero.
1283                                best = p.map(|i| i - min);
1284                            }
1285                        }
1286                    }
1287                }
1288            }
1289
1290            // Now, rescale the input vector, clamp, and round.
1291            // Check if they agree.
1292            let clamped = target.map(|i| (i * scale - min).round().clamp(0.0, 7.0));
1293            let clamped_cosine: MathematicalValue<f32> =
1294                diskann_vector::distance::Cosine::evaluate(&clamped.map(|i| i + min), &target);
1295
1296            // We expect to either get the best value found via exhaustive search, or some
1297            // scalar multiple of it (since that will have the same cosine similarity).
1298            let passed = if best == clamped {
1299                true
1300            } else {
1301                let ratio: Vec<f32> = std::iter::zip(best, clamped)
1302                    .map(|(b, c)| {
1303                        let ratio = (b + min) / (c + min);
1304                        assert_ne!(
1305                            ratio, 0.0,
1306                            "ratio should never be zero because `b` is an integer and \
1307                             `min` is not"
1308                        );
1309                        ratio
1310                    })
1311                    .collect();
1312
1313                ratio.iter().all(|i| *i == ratio[0])
1314            };
1315
1316            if !passed {
1317                panic!(
1318                    "failed for input {:?}.\
1319                     Best = {:?}, Found = {:?}\
1320                     Best similarity = {}, similarity with clamped = {}",
1321                    target,
1322                    best,
1323                    clamped,
1324                    best_similarity,
1325                    clamped_cosine.into_inner()
1326                );
1327            }
1328        };
1329
1330        // Run targeted tests.
1331        let min = -3.5;
1332        for i0 in (0..8).step_by(2) {
1333            for i1 in (1..9).step_by(2) {
1334                for i2 in (0..8).step_by(2) {
1335                    for i3 in (1..9).step_by(2) {
1336                        let p: [f32; 4] = [
1337                            min + (i0 as f32),
1338                            min + (i1 as f32),
1339                            min + (i2 as f32),
1340                            min + (i3 as f32),
1341                        ];
1342                        run_test(p)
1343                    }
1344                }
1345            }
1346        }
1347
1348        for _ in 0..num_trials {
1349            let this_scale: f32 = scale_distribution.sample(&mut rng);
1350            let v: [f32; 4] = [(); 4].map(|_| {
1351                let v: f32 = StandardNormal {}.sample(&mut rng);
1352                this_scale * v
1353            });
1354            run_test(v);
1355        }
1356    }
1357
1358    #[test]
1359    #[should_panic(expected = "calling code should not allow the slice to be empty")]
1360    fn empty_slice_panics() {
1361        maximize_cosine_similarity(
1362            &[],
1363            NonZeroUsize::new(4).unwrap(),
1364            ScopedAllocator::global(),
1365        )
1366        .unwrap();
1367    }
1368
1369    struct Setup {
1370        transform: TransformKind,
1371        nrows: usize,
1372        ncols: usize,
1373        num_trials: usize,
1374    }
1375
1376    fn get_scale(scale: PreScale, quantizer: &SphericalQuantizer) -> f32 {
1377        match scale {
1378            PreScale::None => 1.0,
1379            PreScale::Some(v) => v.into_inner(),
1380            PreScale::ReciprocalMeanNorm => 1.0 / quantizer.mean_norm().into_inner(),
1381        }
1382    }
1383
1384    fn test_l2<const Q: usize, const D: usize, Perm>(
1385        setup: &Setup,
1386        problem: &test_util::TestProblem,
1387        computed_means: &[f32],
1388        pre_scale: PreScale,
1389        rng: &mut StdRng,
1390    ) where
1391        Unsigned: Representation<Q>,
1392        Unsigned: Representation<D>,
1393        Perm: PermutationStrategy<Q>,
1394        for<'a> SphericalQuantizer:
1395            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1396        for<'a> SphericalQuantizer:
1397            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1398    {
1399        assert_eq!(setup.nrows, problem.data.nrows());
1400        assert_eq!(setup.ncols, problem.data.ncols());
1401
1402        let scoped_global = ScopedAllocator::global();
1403        let distribution = Uniform::new(0, setup.nrows).unwrap();
1404        let quantizer = SphericalQuantizer::train(
1405            problem.data.as_view(),
1406            setup.transform,
1407            SupportedMetric::SquaredL2,
1408            pre_scale,
1409            rng,
1410            GlobalAllocator,
1411        )
1412        .unwrap();
1413
1414        let scale = get_scale(pre_scale, &quantizer);
1415
1416        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1417        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1418        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1419
1420        assert_eq!(
1421            quantizer.mean_norm.into_inner(),
1422            problem.mean_norm as f32,
1423            "computed mean norm should not apply scale"
1424        );
1425        let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1426        assert_eq!(&*scaled_means, quantizer.shift());
1427
1428        let l2: CompensatedSquaredL2 = quantizer.as_functor();
1429        assert_eq!(l2.dim, quantizer.output_dim() as f32);
1430
1431        for _ in 0..setup.num_trials {
1432            let i = distribution.sample(rng);
1433            let v = problem.data.row(i);
1434
1435            quantizer
1436                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1437                .unwrap();
1438            quantizer
1439                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1440                .unwrap();
1441            quantizer
1442                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1443                .unwrap();
1444
1445            let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1446                .map(|(a, b)| scale * a - b)
1447                .collect();
1448
1449            // Check that the compensation coefficient were chosen correctly.
1450            {
1451                let DataMetaF32 {
1452                    inner_product_correction,
1453                    bit_sum,
1454                    metric_specific,
1455                } = b.meta().to_full(ARCH);
1456
1457                let shifted_square_norm = metric_specific;
1458
1459                // Check that the bit-count is correct. let bv = b.vector();
1460                let bv = b.vector();
1461                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1462                assert_eq!(s, bit_sum as usize);
1463
1464                // Check that the shifted norm is correct.
1465                {
1466                    let expected = FastL2NormSquared.evaluate(&*shifted);
1467                    let err = (shifted_square_norm - expected).abs() / expected.abs();
1468                    assert!(
1469                        err < 5.0e-4, // twice the minimum normal f16 value.
1470                        "failed diff check, got {}, expected {} - relative error = {}",
1471                        shifted_square_norm,
1472                        expected,
1473                        err
1474                    );
1475                }
1476
1477                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1478                // the RaBitQ paper suggests.
1479                if const { D == 1 } {
1480                    let self_inner_product = 2.0 * shifted_square_norm.sqrt()
1481                        / (inner_product_correction * (bv.len() as f32).sqrt());
1482                    assert!(
1483                        (self_inner_product - 0.8).abs() < 0.13,
1484                        "self inner-product should be close to 0.8. Instead, it's {}",
1485                        self_inner_product
1486                    );
1487                }
1488            }
1489
1490            {
1491                let QueryMeta {
1492                    inner_product_correction,
1493                    bit_sum,
1494                    offset,
1495                    metric_specific,
1496                } = q.meta();
1497
1498                let shifted_square_norm = metric_specific;
1499                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1500                preprocessed
1501                    .shifted
1502                    .iter_mut()
1503                    .for_each(|i| *i /= preprocessed.shifted_norm);
1504
1505                let mut transformed = vec![0.0f32; quantizer.output_dim()];
1506                quantizer
1507                    .transform
1508                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1509                    .unwrap();
1510
1511                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1512                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1513
1514                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1515
1516                // Shifted Norm
1517                {
1518                    let expected = FastL2NormSquared.evaluate(&*shifted);
1519                    let err = (shifted_square_norm - expected).abs() / expected.abs();
1520                    assert!(
1521                        err < 2e-7,
1522                        "failed diff check, got {}, expected {} - relative error = {}",
1523                        shifted_square_norm,
1524                        expected,
1525                        err
1526                    );
1527                }
1528
1529                // Inner product correction
1530                {
1531                    let expected = shifted_square_norm.sqrt() * scale;
1532                    let got = inner_product_correction;
1533
1534                    let err = (expected - got).abs();
1535                    assert!(
1536                        err < 1.0e-7,
1537                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
1538                        expected,
1539                        got,
1540                        err
1541                    );
1542                }
1543
1544                // Offset
1545                {
1546                    let expected = min / scale;
1547                    let got = offset;
1548
1549                    let err = (expected - got).abs();
1550                    assert!(
1551                        err < 1.0e-7,
1552                        "\"sum_scale\": expected {}, got {}, error = {}",
1553                        expected,
1554                        got,
1555                        err
1556                    );
1557                }
1558
1559                // Bit Sum
1560                {
1561                    let expected = (0..q.len())
1562                        .map(|i| q.vector().get(i).unwrap())
1563                        .sum::<i64>() as f32;
1564
1565                    let got = bit_sum;
1566
1567                    let err = (expected - got).abs();
1568                    assert!(
1569                        err < 1.0e-7,
1570                        "\"offset\": expected {}, got {}, error = {}",
1571                        expected,
1572                        got,
1573                        err
1574                    );
1575                }
1576            }
1577
1578            // Check that the compensation coefficient were chosen correctly.
1579            {
1580                // Check that the bit-count is correct.
1581                let s: f32 = f.data.iter().sum::<f32>();
1582                assert_eq!(s, f.meta.sum);
1583
1584                // Check that the shifted norm is correct.
1585                {
1586                    let expected = FastL2Norm.evaluate(&*shifted);
1587                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1588                    assert!(
1589                        err < 2e-7,
1590                        "failed diff check, got {}, expected {} - relative error = {}",
1591                        f.meta.shifted_norm,
1592                        expected,
1593                        err
1594                    );
1595                }
1596
1597                assert_eq!(
1598                    f.meta.metric_specific,
1599                    f.meta.shifted_norm * f.meta.shifted_norm,
1600                    "metric specific data for squared l2 is the square shifted norm",
1601                );
1602            }
1603        }
1604
1605        // Finally - test that if we compress the centroid, the metadata coefficients get
1606        // zeroed correctly.
1607        quantizer
1608            .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1609            .unwrap();
1610        assert_eq!(b.meta(), DataMeta::default());
1611
1612        quantizer
1613            .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1614            .unwrap();
1615        assert_eq!(q.meta(), QueryMeta::default());
1616
1617        f.data.fill(f32::INFINITY);
1618        quantizer
1619            .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1620            .unwrap();
1621        assert!(f.data.iter().all(|&i| i == 0.0));
1622        assert_eq!(f.meta.sum, 0.0);
1623        assert_eq!(f.meta.metric_specific, 0.0);
1624    }
1625
1626    fn test_ip<const Q: usize, const D: usize, Perm>(
1627        setup: &Setup,
1628        problem: &test_util::TestProblem,
1629        computed_means: &[f32],
1630        pre_scale: PreScale,
1631        rng: &mut StdRng,
1632        ctx: &dyn Display,
1633    ) where
1634        Unsigned: Representation<Q>,
1635        Unsigned: Representation<D>,
1636        Perm: PermutationStrategy<Q>,
1637        for<'a> SphericalQuantizer:
1638            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1639        for<'a> SphericalQuantizer:
1640            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1641    {
1642        assert_eq!(setup.nrows, problem.data.nrows());
1643        assert_eq!(setup.ncols, problem.data.ncols());
1644
1645        let scoped_global = ScopedAllocator::global();
1646        let distribution = Uniform::new(0, setup.nrows).unwrap();
1647        let quantizer = SphericalQuantizer::train(
1648            problem.data.as_view(),
1649            setup.transform,
1650            SupportedMetric::InnerProduct,
1651            pre_scale,
1652            rng,
1653            GlobalAllocator,
1654        )
1655        .unwrap();
1656
1657        let scale = get_scale(pre_scale, &quantizer);
1658
1659        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1660        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1661        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1662
1663        assert_eq!(
1664            quantizer.mean_norm.into_inner(),
1665            problem.mean_norm as f32,
1666            "computed mean norm should not apply scale"
1667        );
1668        let scaled_means: Vec<_> = computed_means.iter().map(|i| scale * i).collect();
1669        assert_eq!(&*scaled_means, quantizer.shift());
1670
1671        let ip: CompensatedIP = quantizer.as_functor();
1672
1673        assert_eq!(ip.dim, quantizer.output_dim() as f32);
1674        assert_eq!(
1675            ip.squared_shift_norm,
1676            FastL2NormSquared.evaluate(quantizer.shift())
1677        );
1678
1679        for _ in 0..setup.num_trials {
1680            let i = distribution.sample(rng);
1681            let v = problem.data.row(i);
1682
1683            quantizer
1684                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1685                .unwrap();
1686            quantizer
1687                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1688                .unwrap();
1689            quantizer
1690                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1691                .unwrap();
1692
1693            let shifted: Vec<f32> = std::iter::zip(v.iter(), quantizer.shift().iter())
1694                .map(|(a, b)| scale * a - b)
1695                .collect();
1696
1697            // Check that the compensation coefficient were chosen correctly.
1698            {
1699                let DataMetaF32 {
1700                    inner_product_correction,
1701                    bit_sum,
1702                    metric_specific,
1703                } = b.meta().to_full(ARCH);
1704
1705                let inner_product_with_centroid = metric_specific;
1706
1707                // Check that the bit-count is correct.
1708                let bv = b.vector();
1709                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1710                assert_eq!(s, bit_sum as usize);
1711
1712                // Check that the shifted norm is correct.
1713                let inner_product: MathematicalValue<f32> =
1714                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1715
1716                let diff = (inner_product.into_inner() - inner_product_with_centroid).abs();
1717                assert!(
1718                    diff < 1.53e-5,
1719                    "got a diff of {}. Expected = {}, got = {} -- context: {}",
1720                    diff,
1721                    inner_product.into_inner(),
1722                    inner_product_with_centroid,
1723                    ctx,
1724                );
1725
1726                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1727                // the RaBitQ paper suggests.
1728                if const { D == 1 } {
1729                    let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1730                        / (inner_product_correction * (bv.len() as f32).sqrt());
1731                    assert!(
1732                        (self_inner_product - 0.8).abs() < 0.12,
1733                        "self inner-product should be close to 0.8. Instead, it's {}",
1734                        self_inner_product
1735                    );
1736                }
1737            }
1738
1739            {
1740                let QueryMeta {
1741                    inner_product_correction,
1742                    bit_sum,
1743                    offset,
1744                    metric_specific,
1745                } = q.meta();
1746
1747                let inner_product_with_centroid = metric_specific;
1748                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1749                preprocessed
1750                    .shifted
1751                    .iter_mut()
1752                    .for_each(|i| *i /= preprocessed.shifted_norm);
1753
1754                let mut transformed = vec![0.0f32; quantizer.output_dim()];
1755                quantizer
1756                    .transform
1757                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1758                    .unwrap();
1759
1760                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
1761                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
1762
1763                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
1764
1765                // Inner product correction
1766                {
1767                    let expected = (FastL2Norm).evaluate(&*shifted) * scale;
1768                    let got = inner_product_correction;
1769
1770                    let err = (expected - got).abs();
1771                    assert!(
1772                        err < 1.0e-7,
1773                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
1774                        expected,
1775                        got,
1776                        err
1777                    );
1778                }
1779
1780                // Offset
1781                {
1782                    let expected = min / scale;
1783                    let got = offset;
1784
1785                    let err = (expected - got).abs();
1786                    assert!(
1787                        err < 1.0e-7,
1788                        "\"sum_scale\": expected {}, got {}, error = {}",
1789                        expected,
1790                        got,
1791                        err
1792                    );
1793                }
1794
1795                // Bit Sum
1796                {
1797                    let expected = (0..q.len())
1798                        .map(|i| q.vector().get(i).unwrap())
1799                        .sum::<i64>() as f32;
1800
1801                    let got = bit_sum;
1802
1803                    let err = (expected - got).abs();
1804                    assert!(
1805                        err < 1.0e-7,
1806                        "\"offset\": expected {}, got {}, error = {}",
1807                        expected,
1808                        got,
1809                        err
1810                    );
1811                }
1812
1813                // Inner Product with Centroid
1814                {
1815                    // Check that the shifted norm is correct.
1816                    let inner_product: MathematicalValue<f32> =
1817                        InnerProduct::evaluate(&*shifted, quantizer.shift());
1818                    assert_eq!(inner_product.into_inner(), inner_product_with_centroid);
1819                }
1820            }
1821
1822            // Check that the compensation coefficient were chosen correctly.
1823            {
1824                // Check that the bit-count is correct.
1825                let s: f32 = f.data.iter().sum::<f32>();
1826                assert_eq!(s, f.meta.sum);
1827
1828                // Check that the shifted norm is correct.
1829                {
1830                    let expected = FastL2Norm.evaluate(&*shifted);
1831                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
1832                    assert!(
1833                        err < 2e-7,
1834                        "failed diff check, got {}, expected {} - relative error = {}",
1835                        f.meta.shifted_norm,
1836                        expected,
1837                        err
1838                    );
1839                }
1840
1841                // Check that the shifted norm is correct. s
1842                let inner_product: MathematicalValue<f32> =
1843                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1844                assert_eq!(inner_product.into_inner(), f.meta.metric_specific,);
1845            }
1846        }
1847
1848        // Finally - test that if we compress the centroid, the metadata coefficients get
1849        // zeroed correctly.
1850        quantizer
1851            .compress_into_with(computed_means, b.reborrow_mut(), scoped_global)
1852            .unwrap();
1853        assert_eq!(b.meta(), DataMeta::default());
1854
1855        quantizer
1856            .compress_into_with(computed_means, q.reborrow_mut(), scoped_global)
1857            .unwrap();
1858        assert_eq!(q.meta(), QueryMeta::default());
1859
1860        f.data.fill(f32::INFINITY);
1861        quantizer
1862            .compress_into_with(computed_means, f.reborrow_mut(), scoped_global)
1863            .unwrap();
1864        assert!(f.data.iter().all(|&i| i == 0.0));
1865        assert_eq!(f.meta.sum, 0.0);
1866        assert_eq!(f.meta.metric_specific, 0.0);
1867    }
1868
1869    fn test_cosine<const Q: usize, const D: usize, Perm>(
1870        setup: &Setup,
1871        problem: &test_util::TestProblem,
1872        pre_scale: PreScale,
1873        rng: &mut StdRng,
1874    ) where
1875        Unsigned: Representation<Q>,
1876        Unsigned: Representation<D>,
1877        Perm: PermutationStrategy<Q>,
1878        for<'a> SphericalQuantizer:
1879            CompressIntoWith<&'a [f32], DataMut<'a, D>, ScopedAllocator<'a>>,
1880        for<'a> SphericalQuantizer:
1881            CompressIntoWith<&'a [f32], QueryMut<'a, Q, Perm>, ScopedAllocator<'a>>,
1882    {
1883        assert_eq!(setup.nrows, problem.data.nrows());
1884        assert_eq!(setup.ncols, problem.data.ncols());
1885
1886        let scoped_global = ScopedAllocator::global();
1887        let distribution = Uniform::new(0, setup.nrows).unwrap();
1888        let quantizer = SphericalQuantizer::train(
1889            problem.data.as_view(),
1890            setup.transform,
1891            SupportedMetric::Cosine,
1892            pre_scale,
1893            rng,
1894            GlobalAllocator,
1895        )
1896        .unwrap();
1897
1898        let mut b = Data::<D, _>::new_boxed(quantizer.output_dim());
1899        let mut q = Query::<Q, Perm, _>::new_boxed(quantizer.output_dim());
1900        let mut f = FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap();
1901
1902        let cosine: CompensatedCosine = quantizer.as_functor();
1903
1904        assert_eq!(cosine.inner.dim, quantizer.output_dim() as f32);
1905        assert_eq!(
1906            cosine.inner.squared_shift_norm,
1907            FastL2NormSquared.evaluate(quantizer.shift())
1908        );
1909
1910        const IP_BOUND: f32 = 2.6e-3;
1911
1912        let mut test_row = |v: &[f32]| {
1913            let vnorm = (FastL2Norm).evaluate(v);
1914            let v_normalized: Vec<f32> = v
1915                .iter()
1916                .map(|i| if vnorm == 0.0 { 0.0 } else { *i / vnorm })
1917                .collect();
1918
1919            quantizer
1920                .compress_into_with(v, b.reborrow_mut(), scoped_global)
1921                .unwrap();
1922
1923            quantizer
1924                .compress_into_with(v, q.reborrow_mut(), scoped_global)
1925                .unwrap();
1926
1927            quantizer
1928                .compress_into_with(v, f.reborrow_mut(), scoped_global)
1929                .unwrap();
1930
1931            let shifted: Vec<f32> = std::iter::zip(v_normalized.iter(), quantizer.shift().iter())
1932                .map(|(a, b)| a - b)
1933                .collect();
1934
1935            // Check that the compensation coefficient were chosen correctly.
1936            {
1937                let DataMetaF32 {
1938                    inner_product_correction,
1939                    bit_sum,
1940                    metric_specific,
1941                } = b.meta().to_full(ARCH);
1942
1943                let inner_product_with_centroid = metric_specific;
1944
1945                // Check that the bit-count is correct.
1946                let bv = b.vector();
1947                let s: usize = (0..bv.len()).map(|i| bv.get(i).unwrap() as usize).sum();
1948                assert_eq!(s, bit_sum as usize);
1949
1950                // Check that the shifted norm is correct. Since they are computed slightly
1951                // differnetly, allow a small amount of error.
1952                let inner_product: MathematicalValue<f32> =
1953                    InnerProduct::evaluate(&*shifted, quantizer.shift());
1954
1955                let abs = (inner_product.into_inner() - inner_product_with_centroid).abs();
1956                let relative = abs / inner_product.into_inner().abs();
1957
1958                assert!(
1959                    abs < 1e-7 || relative < IP_BOUND,
1960                    "got an abs/rel of {}/{} with a bound of {}/{}",
1961                    abs,
1962                    relative,
1963                    1e-7,
1964                    IP_BOUND
1965                );
1966
1967                // Finaly, verify that the self-inner-product is clustered around 0.8 as
1968                // the RaBitQ paper suggests.
1969                if const { D == 1 } {
1970                    let self_inner_product = 2.0 * (FastL2Norm).evaluate(&*shifted)
1971                        / (inner_product_correction * (bv.len() as f32).sqrt());
1972                    assert!(
1973                        (self_inner_product - 0.8).abs() < 0.11,
1974                        "self inner-product should be close to 0.8. Instead, it's {}",
1975                        self_inner_product
1976                    );
1977                }
1978            }
1979
1980            {
1981                let QueryMeta {
1982                    inner_product_correction,
1983                    bit_sum,
1984                    offset,
1985                    metric_specific,
1986                } = q.meta();
1987
1988                let inner_product_with_centroid = metric_specific;
1989                let mut preprocessed = quantizer.preprocess(v, scoped_global).unwrap();
1990                preprocessed
1991                    .shifted
1992                    .iter_mut()
1993                    .for_each(|i| *i /= preprocessed.shifted_norm);
1994
1995                let mut transformed = vec![0.0f32; quantizer.output_dim()];
1996                quantizer
1997                    .transform
1998                    .transform_into(&mut transformed, &preprocessed.shifted, scoped_global)
1999                    .unwrap();
2000
2001                let min = transformed.iter().fold(f32::MAX, |min, &i| min.min(i));
2002                let max = transformed.iter().fold(f32::MIN, |max, &i| max.max(i));
2003
2004                let scale = (max - min) / ((2usize.pow(Q as u32) - 1) as f32);
2005
2006                // Inner product correction
2007                {
2008                    let expected = (FastL2Norm).evaluate(&*shifted) * scale;
2009                    let got = inner_product_correction;
2010
2011                    let err = (expected - got).abs();
2012                    assert!(
2013                        err < 1.0e-7,
2014                        "\"innerproduct_scale\": expected {}, got {}, error = {}",
2015                        expected,
2016                        got,
2017                        err
2018                    );
2019                }
2020
2021                // Offset
2022                {
2023                    let expected = min / scale;
2024                    let got = offset;
2025
2026                    let err = (expected - got).abs();
2027                    assert!(
2028                        err < 1.0e-7,
2029                        "\"sum_scale\": expected {}, got {}, error = {}",
2030                        expected,
2031                        got,
2032                        err
2033                    );
2034                }
2035
2036                // Bit Sum
2037                {
2038                    let expected = (0..q.len())
2039                        .map(|i| q.vector().get(i).unwrap())
2040                        .sum::<i64>() as f32;
2041
2042                    let got = bit_sum;
2043
2044                    let err = (expected - got).abs();
2045                    assert!(
2046                        err < 1.0e-7,
2047                        "\"offset\": expected {}, got {}, error = {}",
2048                        expected,
2049                        got,
2050                        err
2051                    );
2052                }
2053
2054                // Inner Product with Centroid
2055                {
2056                    // Check that the shifted norm is correct.
2057                    let inner_product: MathematicalValue<f32> =
2058                        InnerProduct::evaluate(&*shifted, quantizer.shift());
2059
2060                    let err = (inner_product.into_inner() - inner_product_with_centroid).abs()
2061                        / inner_product.into_inner().abs();
2062                    assert!(
2063                        err < IP_BOUND,
2064                        "\"offset\": expected {}, got {}, error = {}",
2065                        inner_product.into_inner(),
2066                        inner_product_with_centroid,
2067                        err
2068                    );
2069                }
2070            }
2071
2072            // Check that the compensation coefficient were chosen correctly.
2073            {
2074                // Check that the bit-count is correct.
2075                let s: f32 = f.data.iter().sum::<f32>();
2076                assert_eq!(s, f.meta.sum);
2077
2078                // Check that the shifted norm is correct.
2079                {
2080                    let expected = FastL2Norm.evaluate(&*shifted);
2081                    let err = (f.meta.shifted_norm - expected).abs() / expected.abs();
2082                    assert!(
2083                        err < 2e-7,
2084                        "failed diff check, got {}, expected {} - relative error = {}",
2085                        f.meta.shifted_norm,
2086                        expected,
2087                        err
2088                    );
2089                }
2090
2091                // Check that the shifted norm is correct. s
2092                let inner_product: MathematicalValue<f32> =
2093                    InnerProduct::evaluate(&*shifted, quantizer.shift());
2094                let err = (inner_product.into_inner() - f.meta.metric_specific).abs()
2095                    / inner_product.into_inner().abs();
2096                assert!(
2097                    err < IP_BOUND,
2098                    "\"offset\": expected {}, got {}, error = {}",
2099                    inner_product.into_inner(),
2100                    f.meta.metric_specific,
2101                    err
2102                );
2103            }
2104        };
2105
2106        for _ in 0..setup.num_trials {
2107            let i = distribution.sample(rng);
2108            let v = problem.data.row(i);
2109            test_row(v);
2110        }
2111
2112        // Ensure that if a zero vector is provided that we do not divide by zero.
2113        let zero = vec![0.0f32; quantizer.input_dim()];
2114        test_row(&zero);
2115    }
2116
2117    fn _test_oom_resiliance<T>(quantizer: &SphericalQuantizer, data: &[f32], dst: &mut T)
2118    where
2119        for<'a> T: ReborrowMut<'a>,
2120        for<'a> SphericalQuantizer: CompressIntoWith<
2121                &'a [f32],
2122                <T as ReborrowMut<'a>>::Target,
2123                ScopedAllocator<'a>,
2124                Error = CompressionError,
2125            >,
2126    {
2127        let mut succeeded = false;
2128        let mut failed = false;
2129        for max_allocations in 0..10 {
2130            match quantizer.compress_into_with(
2131                data,
2132                dst.reborrow_mut(),
2133                ScopedAllocator::new(&test_util::LimitedAllocator::new(max_allocations)),
2134            ) {
2135                Ok(()) => {
2136                    succeeded = true;
2137                }
2138                Err(CompressionError::AllocatorError(_)) => {
2139                    failed = true;
2140                }
2141                Err(other) => {
2142                    panic!("received an unexpected error: {:?}", other);
2143                }
2144            }
2145        }
2146        assert!(succeeded);
2147        assert!(failed);
2148    }
2149
2150    fn test_oom_resiliance<const Q: usize, const D: usize, Perm>(
2151        setup: &Setup,
2152        problem: &test_util::TestProblem,
2153        pre_scale: PreScale,
2154        rng: &mut StdRng,
2155    ) where
2156        Unsigned: Representation<Q>,
2157        Unsigned: Representation<D>,
2158        Perm: PermutationStrategy<Q>,
2159        for<'a> SphericalQuantizer: CompressIntoWith<
2160                &'a [f32],
2161                DataMut<'a, D>,
2162                ScopedAllocator<'a>,
2163                Error = CompressionError,
2164            >,
2165        for<'a> SphericalQuantizer: CompressIntoWith<
2166                &'a [f32],
2167                QueryMut<'a, Q, Perm>,
2168                ScopedAllocator<'a>,
2169                Error = CompressionError,
2170            >,
2171    {
2172        assert_eq!(setup.nrows, problem.data.nrows());
2173        assert_eq!(setup.ncols, problem.data.ncols());
2174
2175        let quantizer = SphericalQuantizer::train(
2176            problem.data.as_view(),
2177            setup.transform,
2178            SupportedMetric::SquaredL2,
2179            pre_scale,
2180            rng,
2181            GlobalAllocator,
2182        )
2183        .unwrap();
2184
2185        // Data.
2186        let data = problem.data.row(0);
2187        _test_oom_resiliance::<Data<D, _>>(
2188            &quantizer,
2189            data,
2190            &mut Data::new_boxed(quantizer.output_dim()),
2191        );
2192        _test_oom_resiliance::<Query<Q, Perm, _>>(
2193            &quantizer,
2194            data,
2195            &mut Query::new_boxed(quantizer.output_dim()),
2196        );
2197        _test_oom_resiliance::<FullQuery<_>>(
2198            &quantizer,
2199            data,
2200            &mut FullQuery::empty(quantizer.output_dim(), GlobalAllocator).unwrap(),
2201        );
2202    }
2203
2204    fn test_quantizer<const Q: usize, const D: usize, Perm>(setup: &Setup, rng: &mut StdRng)
2205    where
2206        Unsigned: Representation<Q>,
2207        Unsigned: Representation<D>,
2208        Perm: PermutationStrategy<Q>,
2209        for<'a> SphericalQuantizer: CompressIntoWith<
2210                &'a [f32],
2211                DataMut<'a, D>,
2212                ScopedAllocator<'a>,
2213                Error = CompressionError,
2214            >,
2215        for<'a> SphericalQuantizer: CompressIntoWith<
2216                &'a [f32],
2217                QueryMut<'a, Q, Perm>,
2218                ScopedAllocator<'a>,
2219                Error = CompressionError,
2220            >,
2221    {
2222        let problem = test_util::create_test_problem(setup.nrows, setup.ncols, rng);
2223        let computed_means_f32: Vec<_> = problem.means.iter().map(|i| *i as f32).collect();
2224
2225        let scales = [
2226            PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2227            PreScale::Some(Positive::new(1.0 / 1024.0).unwrap()),
2228            PreScale::ReciprocalMeanNorm,
2229        ];
2230
2231        for scale in scales {
2232            let ctx = &lazy_format!("dim = {}, scale = {:?}", setup.ncols, scale);
2233
2234            test_l2::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng);
2235            test_ip::<Q, D, Perm>(setup, &problem, &computed_means_f32, scale, rng, ctx);
2236            test_cosine::<Q, D, Perm>(setup, &problem, scale, rng);
2237        }
2238
2239        test_oom_resiliance::<Q, D, Perm>(setup, &problem, PreScale::ReciprocalMeanNorm, rng);
2240    }
2241
2242    #[test]
2243    fn test_spherical_quantizer() {
2244        let mut rng = StdRng::seed_from_u64(0xab516aef1ce61640);
2245        for dim in [56, 72, 128, 255] {
2246            let setup = Setup {
2247                transform: TransformKind::PaddingHadamard {
2248                    target_dim: TargetDim::Same,
2249                },
2250                nrows: 64,
2251                ncols: dim,
2252                num_trials: 10,
2253            };
2254
2255            test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2256            test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2257            test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2258            test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2259
2260            let setup = Setup {
2261                transform: TransformKind::DoubleHadamard {
2262                    target_dim: TargetDim::Same,
2263                },
2264                nrows: 64,
2265                ncols: dim,
2266                num_trials: 10,
2267            };
2268            test_quantizer::<4, 1, BitTranspose>(&setup, &mut rng);
2269            test_quantizer::<2, 2, Dense>(&setup, &mut rng);
2270            test_quantizer::<4, 4, Dense>(&setup, &mut rng);
2271            test_quantizer::<8, 8, Dense>(&setup, &mut rng);
2272        }
2273    }
2274
2275    ////////////
2276    // Errors //
2277    ////////////
2278
2279    #[test]
2280    fn err_dim_cannot_be_zero() {
2281        let data = Matrix::new(0.0f32, 10, 0);
2282        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2283        let err = SphericalQuantizer::train(
2284            data.as_view(),
2285            TransformKind::DoubleHadamard {
2286                target_dim: TargetDim::Same,
2287            },
2288            SupportedMetric::SquaredL2,
2289            PreScale::None,
2290            &mut rng,
2291            GlobalAllocator,
2292        )
2293        .unwrap_err();
2294        assert_eq!(err.to_string(), "data dim cannot be zero");
2295    }
2296
2297    #[test]
2298    fn err_norm_must_be_positive() {
2299        let data = Matrix::new(0.0f32, 10, 10);
2300        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2301        let err = SphericalQuantizer::train(
2302            data.as_view(),
2303            TransformKind::DoubleHadamard {
2304                target_dim: TargetDim::Same,
2305            },
2306            SupportedMetric::SquaredL2,
2307            PreScale::None,
2308            &mut rng,
2309            GlobalAllocator,
2310        )
2311        .unwrap_err();
2312        assert_eq!(err.to_string(), "norm must be positive");
2313    }
2314
2315    #[test]
2316    fn err_norm_cannot_be_infinity() {
2317        let mut data = Matrix::new(0.0f32, 10, 10);
2318        data[(2, 5)] = f32::INFINITY;
2319
2320        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2321        let err = SphericalQuantizer::train(
2322            data.as_view(),
2323            TransformKind::DoubleHadamard {
2324                target_dim: TargetDim::Same,
2325            },
2326            SupportedMetric::SquaredL2,
2327            PreScale::None,
2328            &mut rng,
2329            GlobalAllocator,
2330        )
2331        .unwrap_err();
2332        assert_eq!(err.to_string(), "computed norm contains infinity or NaN");
2333    }
2334
2335    #[test]
2336    fn err_reciprocal_norm_cannot_be_infinity() {
2337        let mut data = Matrix::new(0.0f32, 10, 10);
2338        data[(2, 5)] = 2.93863e-39;
2339
2340        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2341        let err = SphericalQuantizer::train(
2342            data.as_view(),
2343            TransformKind::DoubleHadamard {
2344                target_dim: TargetDim::Same,
2345            },
2346            SupportedMetric::SquaredL2,
2347            PreScale::ReciprocalMeanNorm,
2348            &mut rng,
2349            GlobalAllocator,
2350        )
2351        .unwrap_err();
2352        assert_eq!(err.to_string(), "reciprocal norm contains infinity or NaN");
2353    }
2354
2355    #[test]
2356    fn err_mean_norm_cannot_be_zero_generate() {
2357        let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2358        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2359        let err = SphericalQuantizer::generate(
2360            centroid,
2361            0.0,
2362            TransformKind::DoubleHadamard {
2363                target_dim: TargetDim::Same,
2364            },
2365            SupportedMetric::SquaredL2,
2366            None,
2367            &mut rng,
2368            GlobalAllocator,
2369        )
2370        .unwrap_err();
2371        assert_eq!(err.to_string(), "norm must be positive");
2372    }
2373
2374    #[test]
2375    fn err_scale_cannot_be_zero_generate() {
2376        let centroid = Poly::broadcast(0.0f32, 10, GlobalAllocator).unwrap();
2377        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2378        let err = SphericalQuantizer::generate(
2379            centroid,
2380            1.0,
2381            TransformKind::DoubleHadamard {
2382                target_dim: TargetDim::Same,
2383            },
2384            SupportedMetric::SquaredL2,
2385            Some(0.0),
2386            &mut rng,
2387            GlobalAllocator,
2388        )
2389        .unwrap_err();
2390        assert_eq!(err.to_string(), "pre-scale must be positive");
2391    }
2392
2393    #[test]
2394    fn compression_errors_data() {
2395        let mut rng = StdRng::seed_from_u64(0xe3e9f42ed9f15883);
2396        let data = Matrix::<f32>::new(views::Init(|| StandardNormal {}.sample(&mut rng)), 16, 12);
2397
2398        let quantizer = SphericalQuantizer::train(
2399            data.as_view(),
2400            TransformKind::PaddingHadamard {
2401                target_dim: TargetDim::Same,
2402            },
2403            SupportedMetric::SquaredL2,
2404            PreScale::None,
2405            &mut rng,
2406            GlobalAllocator,
2407        )
2408        .unwrap();
2409
2410        let scoped_global = ScopedAllocator::global();
2411
2412        // Input contains NaN.
2413        {
2414            let mut query: Vec<f32> = quantizer.shift().to_vec();
2415            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2416            let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2417
2418            for i in 0..query.len() {
2419                let last = query[i];
2420                for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2421                    query[i] = v;
2422
2423                    let err = quantizer
2424                        .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2425                        .unwrap_err();
2426
2427                    assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2428
2429                    let err = quantizer
2430                        .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2431                        .unwrap_err();
2432
2433                    assert_eq!(err.to_string(), "input contains NaN", "failed for {}", v);
2434                }
2435                query[i] = last;
2436            }
2437        }
2438
2439        // Input has a large value.
2440        {
2441            let query: Vec<f32> = vec![1000000.0; quantizer.input_dim()];
2442            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2443
2444            let err = quantizer
2445                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2446                .unwrap_err();
2447
2448            let expected = "encoding error - you may need to scale the entire dataset to reduce its dynamic range";
2449
2450            assert_eq!(err.to_string(), expected, "failed for {:?}", query);
2451        }
2452
2453        // Input length
2454        for len in [quantizer.input_dim() - 1, quantizer.input_dim() + 1] {
2455            let query = vec![0.0f32; len];
2456            let mut d = Data::<1, _>::new_boxed(quantizer.output_dim());
2457            let mut q = Query::<4, BitTranspose, _>::new_boxed(quantizer.output_dim());
2458
2459            let err = quantizer
2460                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2461                .unwrap_err();
2462            assert_eq!(
2463                err,
2464                CompressionError::SourceDimensionMismatch {
2465                    expected: quantizer.input_dim(),
2466                }
2467            );
2468
2469            let err = quantizer
2470                .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2471                .unwrap_err();
2472            assert_eq!(
2473                err,
2474                CompressionError::SourceDimensionMismatch {
2475                    expected: quantizer.input_dim(),
2476                }
2477            );
2478        }
2479
2480        for len in [quantizer.output_dim() - 1, quantizer.output_dim() + 1] {
2481            let query = vec![0.0f32; quantizer.input_dim()];
2482            let mut d = Data::<1, _>::new_boxed(len);
2483            let mut q = Query::<4, BitTranspose, _>::new_boxed(len);
2484
2485            let err = quantizer
2486                .compress_into_with(&*query, d.reborrow_mut(), scoped_global)
2487                .unwrap_err();
2488            assert_eq!(
2489                err,
2490                CompressionError::DestinationDimensionMismatch {
2491                    expected: quantizer.output_dim(),
2492                }
2493            );
2494
2495            let err = quantizer
2496                .compress_into_with(&*query, q.reborrow_mut(), scoped_global)
2497                .unwrap_err();
2498            assert_eq!(
2499                err,
2500                CompressionError::DestinationDimensionMismatch {
2501                    expected: quantizer.output_dim(),
2502                }
2503            );
2504        }
2505    }
2506
2507    #[test]
2508    fn centroid_scaling_happens_in_generate() {
2509        let centroid = Poly::from_iter(
2510            [1088.6732f32, 1393.32, 1547.877].into_iter(),
2511            GlobalAllocator,
2512        )
2513        .unwrap();
2514        let mean_norm = 2359.27;
2515        let pre_scale = 1.0 / mean_norm;
2516
2517        let quantizer = SphericalQuantizer::generate(
2518            centroid,
2519            mean_norm,
2520            TransformKind::Null,
2521            SupportedMetric::InnerProduct,
2522            Some(pre_scale),
2523            &mut StdRng::seed_from_u64(10),
2524            GlobalAllocator,
2525        )
2526        .unwrap();
2527
2528        let mut v = Data::<4, _>::new_boxed(quantizer.input_dim());
2529        let data: &[f32] = &[1000.34, 1456.32, 1234.5446];
2530        assert!(
2531            quantizer
2532                .compress_into_with(data, v.reborrow_mut(), ScopedAllocator::global())
2533                .is_ok(),
2534            "if this failed, the likely culprit is exceeding the value of the 16-bit correction terms"
2535        );
2536    }
2537}
2538
2539#[cfg(feature = "flatbuffers")]
2540#[cfg(test)]
2541mod test_serialization {
2542    use rand::{SeedableRng, rngs::StdRng};
2543
2544    use super::*;
2545    use crate::{
2546        algorithms::transforms::TargetDim,
2547        flatbuffers::{self as fb, to_flatbuffer},
2548        poly, test_util,
2549    };
2550
2551    #[test]
2552    fn test_serialization_happy_path() {
2553        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2554        let problem = test_util::create_test_problem(10, 128, &mut rng);
2555
2556        let low = NonZeroUsize::new(100).unwrap();
2557        let high = NonZeroUsize::new(150).unwrap();
2558
2559        let kinds = [
2560            // Null
2561            TransformKind::Null,
2562            // Double Hadamard
2563            TransformKind::DoubleHadamard {
2564                target_dim: TargetDim::Same,
2565            },
2566            TransformKind::DoubleHadamard {
2567                target_dim: TargetDim::Natural,
2568            },
2569            TransformKind::DoubleHadamard {
2570                target_dim: TargetDim::Override(low),
2571            },
2572            TransformKind::DoubleHadamard {
2573                target_dim: TargetDim::Override(high),
2574            },
2575            // Padding Hadamard
2576            TransformKind::PaddingHadamard {
2577                target_dim: TargetDim::Same,
2578            },
2579            TransformKind::PaddingHadamard {
2580                target_dim: TargetDim::Natural,
2581            },
2582            TransformKind::PaddingHadamard {
2583                target_dim: TargetDim::Override(low),
2584            },
2585            TransformKind::PaddingHadamard {
2586                target_dim: TargetDim::Override(high),
2587            },
2588            // Random Rotation
2589            #[cfg(all(not(miri), feature = "linalg"))]
2590            TransformKind::RandomRotation {
2591                target_dim: TargetDim::Same,
2592            },
2593            #[cfg(all(not(miri), feature = "linalg"))]
2594            TransformKind::RandomRotation {
2595                target_dim: TargetDim::Natural,
2596            },
2597            #[cfg(all(not(miri), feature = "linalg"))]
2598            TransformKind::RandomRotation {
2599                target_dim: TargetDim::Override(low),
2600            },
2601            #[cfg(all(not(miri), feature = "linalg"))]
2602            TransformKind::RandomRotation {
2603                target_dim: TargetDim::Override(high),
2604            },
2605        ];
2606
2607        let pre_scales = [
2608            PreScale::None,
2609            PreScale::Some(Positive::new(0.5).unwrap()),
2610            PreScale::Some(Positive::new(1.0).unwrap()),
2611            PreScale::Some(Positive::new(1.5).unwrap()),
2612            PreScale::ReciprocalMeanNorm,
2613        ];
2614
2615        let alloc = GlobalAllocator;
2616        for kind in kinds.into_iter() {
2617            for metric in SupportedMetric::all() {
2618                for pre_scale in pre_scales {
2619                    let quantizer = SphericalQuantizer::train(
2620                        problem.data.as_view(),
2621                        kind,
2622                        metric,
2623                        pre_scale,
2624                        &mut rng,
2625                        alloc,
2626                    )
2627                    .unwrap();
2628
2629                    let data = to_flatbuffer(|buf| quantizer.pack(buf));
2630                    let proto =
2631                        flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2632                    let reloaded = SphericalQuantizer::try_unpack(alloc, proto).unwrap();
2633                    assert_eq!(quantizer, reloaded, "failed on transform {:?}", kind);
2634                }
2635            }
2636        }
2637    }
2638
2639    #[test]
2640    fn test_error_checking() {
2641        let mut rng = StdRng::seed_from_u64(0x070d9ff8cf5e0f8c);
2642        let problem = test_util::create_test_problem(10, 128, &mut rng);
2643
2644        let transform = TransformKind::DoubleHadamard {
2645            target_dim: TargetDim::Same,
2646        };
2647
2648        let alloc = GlobalAllocator;
2649        let mut make_quantizer = || {
2650            SphericalQuantizer::train(
2651                problem.data.as_view(),
2652                transform,
2653                SupportedMetric::SquaredL2,
2654                PreScale::None,
2655                &mut rng,
2656                alloc,
2657            )
2658            .unwrap()
2659        };
2660
2661        type E = DeserializationError;
2662
2663        // Missing norm: 0.0
2664        {
2665            let mut quantizer = make_quantizer();
2666            // SAFETY: We do not do anything with the created value and the compiler
2667            // does not know about the layout of `Positive`, so we don't need to worry
2668            // about violating layout restrictions.
2669            quantizer.mean_norm = unsafe { Positive::new_unchecked(0.0) };
2670
2671            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2672            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2673            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2674            assert_eq!(err, E::MissingNorm);
2675        }
2676
2677        // Missing norm: negative
2678        {
2679            let mut quantizer = make_quantizer();
2680
2681            // SAFETY: We do not do anything with the created value and the compiler
2682            // does not know about the layout of `Positive`, so we don't need to worry
2683            // about violating layout restrictions.
2684            quantizer.mean_norm = unsafe { Positive::new_unchecked(-1.0) };
2685
2686            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2687            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2688            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2689            assert_eq!(err, E::MissingNorm);
2690        }
2691
2692        // PreScaleNotPositive
2693        {
2694            let mut quantizer = make_quantizer();
2695
2696            // SAFETY: This really isn't safe, but we are not using the improper value in a
2697            // way that will trigger undefined behavior.
2698            quantizer.pre_scale = unsafe { Positive::new_unchecked(0.0) };
2699
2700            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2701            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2702            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2703            assert_eq!(err, E::PreScaleNotPositive);
2704        }
2705
2706        // Dim Mismatch.
2707        {
2708            let mut quantizer = make_quantizer();
2709            quantizer.shift = poly!([1.0, 2.0, 3.0], alloc).unwrap();
2710
2711            let data = to_flatbuffer(|buf| quantizer.pack(buf));
2712            let proto = flatbuffers::root::<fb::spherical::SphericalQuantizer>(&data).unwrap();
2713            let err = SphericalQuantizer::try_unpack(alloc, proto).unwrap_err();
2714            assert_eq!(err, E::DimMismatch);
2715        }
2716    }
2717}