Skip to main content

diskann_quantization/spherical/
iface.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! The main export of this module is the dyn compatible [`Quantizer`] trait, which provides
7//! a common interface for interacting with bit-width specific [`SphericalQuantizer`]s,
8//! compressing vectors, and computing distances between compressed vectors.
9//!
10//! This is offered as a convenience interface for interacting with the myriad of generics
11//! associated with the [`SphericalQuantizer`]. Better performance can be achieved by using
12//! the generic types directly if desired.
13//!
14//! The [`Quantizer`] uses the [`Opaque`] and [`OpaqueMut`] types for its compressed data
15//! representations. These are thin wrappers around raw byte slices.
16//!
17//! Distance computation is performed using [`DistanceComputer`] and [`QueryComputer`]
18//!
19//! Concrete implementations of [`Quantizer`] are available via the generic struct [`Impl`].
20//!
21//! ## Compatibility Table
22//!
23//! Multiple [`QueryLayout`]s are supported when constructing a [`QueryComputer`], but not
24//! all layouts are supported for each back end. This table lists the valid instantiations
25//! of [`Impl`] (parameterized by data vector bit-width) and their supported query layouts.
26//!
27//! | Bits | Same As Data | Full Precision | Four-Bit Transposed | Scalar Quantized |
28//! |------|--------------|----------------|---------------------|------------------|
29//! |    1 |     Yes      |      Yes       |         Yes         |       No         |
30//! |    2 |     Yes      |      Yes       |          No         |       Yes        |
31//! |    4 |     Yes      |      Yes       |          No         |       Yes        |
32//! |    8 |     Yes      |      Yes       |          No         |       Yes        |
33//!
34//! # Example
35//!
36//! ```
37//! use diskann_quantization::{
38//!     alloc::{Poly, ScopedAllocator, AlignedAllocator, GlobalAllocator},
39//!     algorithms::TransformKind,
40//!     spherical::{iface, SupportedMetric, SphericalQuantizer, PreScale},
41//!     num::PowerOfTwo,
42//! };
43//! use diskann_utils::views::rowmajor::{self, Matrix};
44//!
45//! // For illustration purposes, the dataset consists of just a single vector.
46//! let mut data = rowmajor::Owned::from_element(1, 4, 1.0);
47//! let quantizer = SphericalQuantizer::train(
48//!     data.as_view(),
49//!     TransformKind::Null,
50//!     SupportedMetric::SquaredL2,
51//!     PreScale::None,
52//!     &mut rand::rng(),
53//!     GlobalAllocator
54//! )
55//! .unwrap()
56//! .as_quantizer::<1>()
57//! .unwrap();
58//!
59//!
60//! let alloc = AlignedAllocator::new(PowerOfTwo::new(1).unwrap());
61//! let mut buf = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
62//!
63//! quantizer.compress(
64//!     data.row(0),
65//!     iface::OpaqueMut::new(&mut buf),
66//!     ScopedAllocator::new(&alloc),
67//! ).unwrap();
68//!
69//! assert!(quantizer.is_supported(iface::QueryLayout::FullPrecision));
70//! assert!(!quantizer.is_supported(iface::QueryLayout::ScalarQuantized));
71//! ```
72
73/// # DevDocs
74///
75/// This section provides developer documentation for the structure of the structs and
76/// traits inside this file. The main goal of the [`DistanceComputer`] and [`QueryComputer`]
77/// implementations are to introduce just a single level of indirection.
78///
79/// ## Distance Computer Philosophy
80///
81/// The goal of this code (and in large part the reason for the somewhat spaghetti nature)
82/// is to do all the the dispatches:
83///
84/// * Number of bits.
85/// * Query layout.
86/// * Distance Type (L2, Inner Product, Cosine).
87/// * Micro-architecture specific code-generation
88///
89/// Behind a **single** level of dynamic dispatch. This means we need to bake all of this
90/// information into a single type, facilitated through a combination of the `Reify` and
91/// `Curried` private structs.
92///
93/// ## Anatomy of the [`DistanceComputer`]
94///
95/// To provide a single level of indirection for all distance function implementations,
96/// the [`DistanceComputer`] is a thin wrapper around the [`DynDistanceComputer`] trait.
97///
98/// Concrete implementations of this trait consist of a base distance function like
99/// [`CompensatedIP`] or [`CompensatedSquaredL2`]. Because the data is passed through the
100/// [`Opaque`] type, these distance functions are embedded inside a [`Reify`] which first
101/// converts the [`Opaque`] to the appropriate fully-typed object before calling the inner
102/// distance function.
103///
104/// This full typing is supplied by the private [`FromOpaque`] helper trait.
105///
106/// When returned from the [`Quantizer::distance_computer`] or
107/// [`Quantizer::distance_computer_ref`] traits, the resulting computer will be specialized
108/// to work solely on data vectors compressed through [`Quantizer::compress`].
109///
110/// When returned from [`Quantizer::query_computer`], the expected type of the query will
111/// depend on the [`QueryLayout`] supplied to that method.
112///
113/// The method [`QueryComputer::layout`] is provided to inspect at run time the query layout
114/// the object is meant for.
115///
116/// If at all possible, the [`QueryComputer`] should be preferred as it removes the
117/// possibility of providing an incorrect query layout and will be slightly faster since
118/// it does not require argument reification.
119///
120/// ## Anatomy of the [`QueryComputer`]
121///
122/// This is similar to the [`DistanceComputer`] but has the extra duty of supporting
123/// multiple different [`iface::QueryLayouts`] (compressions for the query). To that end,
124/// the stack of types used to implement the underlying [`DynDistanceComputer`] trait is:
125///
126/// * Base [`DistanceFunction`] (e.g. [`CompensatedIP`]).
127///
128/// * Embedded inside [`Curried`] - which also contains a heap-allocated representation of
129///   the query using the selected layout. For example, this could be one of.
130///
131///   - [`diskann_quantization::spherical::Query`]
132///   - [`diskann_quantization::spherical::FullQuery`]
133///   - [`diskann_quantization::sphericasl::Data`]
134///
135/// * Embedded inside [`Reify`] to convert [`Opaque`] to the correct type.
136use std::{fmt::Debug, marker::PhantomData};
137
138use diskann_utils::{Reborrow, ReborrowMut};
139use diskann_vector::{DistanceFunction, PreprocessedDistanceFunction};
140use diskann_wide::{
141    Architecture,
142    arch::{Scalar, Target1, Target2},
143};
144#[cfg(feature = "flatbuffers")]
145use flatbuffers::FlatBufferBuilder;
146use thiserror::Error;
147
148#[cfg(target_arch = "x86_64")]
149use diskann_wide::arch::x86_64::{V3, V4};
150
151#[cfg(target_arch = "aarch64")]
152use diskann_wide::arch::aarch64::Neon;
153
154use super::{
155    CompensatedCosine, CompensatedIP, CompensatedSquaredL2, Data, DataMut, DataRef, FullQuery,
156    FullQueryMut, FullQueryRef, Query, QueryMut, QueryRef, SphericalQuantizer, SupportedMetric,
157    quantizer,
158};
159use crate::{
160    AsFunctor, CompressIntoWith,
161    alloc::{
162        Allocator, AllocatorCore, AllocatorError, GlobalAllocator, Poly, ScopedAllocator, TryClone,
163    },
164    bits::{self, Representation, Unsigned},
165    distances::{self, UnequalLengths},
166    error::InlineError,
167    meta,
168    num::PowerOfTwo,
169    poly,
170};
171#[cfg(feature = "flatbuffers")]
172use crate::{alloc::CompoundError, flatbuffers as fb};
173
174// A convenience definition to shorten the extensive where-clauses present in this file.
175type Rf32 = distances::Result<f32>;
176
177///////////////
178// Quantizer //
179///////////////
180
181/// A description of the buffer size (in bytes) and alignment required for a compressed query.
182#[derive(Debug, Clone)]
183pub struct QueryBufferDescription {
184    size: usize,
185    align: PowerOfTwo,
186}
187
188impl QueryBufferDescription {
189    /// Construct a new [`QueryBufferDescription`]
190    pub fn new(size: usize, align: PowerOfTwo) -> Self {
191        Self { size, align }
192    }
193
194    /// Return the number of bytes needed in a buffer for a compressed query.
195    pub fn bytes(&self) -> usize {
196        self.size
197    }
198
199    /// Return the necessary alignment of the base pointer for a query buffer.
200    pub fn align(&self) -> PowerOfTwo {
201        self.align
202    }
203}
204
205/// A dyn-compatible trait providing a common interface for a bit-width specific
206/// [`SphericalQuantizer`].
207///
208/// This allows us to have a single [`dyn Quantizer`] type without generics while still
209/// supporting the range of bit-widths and query strategies we wish to support.
210///
211/// A level of indirection for each distance computation, unfortunately, is required to
212/// support this. But we try to structure the code so there is only a single level of
213/// indirection.
214///
215/// # Allocator
216///
217/// The quantizer is parameterized by the allocator provided used to acquire any necessary
218/// memory for returned data structures.  The contract is as follows:
219///
220/// 1. Any allocation made as part of a returned data structure from a function will be
221///    performed through the allocator given to that function.
222///
223/// 2. If dynamic memory allocation for scratch space is required, a separate `scratch`
224///    allocator will be required and all scratch space allocations will go through that
225///    allocator.
226pub trait Quantizer<A = GlobalAllocator>: Send + Sync + Debug
227where
228    A: Allocator + std::panic::UnwindSafe + Send + Sync + 'static,
229{
230    /// The effective number of bits in the encoding.
231    fn nbits(&self) -> usize;
232
233    /// The number of bytes occupied by each compressed vector.
234    fn bytes(&self) -> usize;
235
236    /// The effective dimensionality of each compressed vector.
237    fn dim(&self) -> usize;
238
239    /// The dimensionality of the full-precision input vectors.
240    fn full_dim(&self) -> usize;
241
242    /// Return a distance computer capable on operating on validly initialized [`Opaque`]
243    /// slices of length [`Self::bytes`].
244    ///
245    /// These slices should be initialized by [`Self::compress`].
246    ///
247    /// The query layout associated with this computer will always be
248    /// [`QueryLayout::SameAsData`].
249    fn distance_computer(&self, allocator: A) -> Result<DistanceComputer<A>, AllocatorError>;
250
251    /// Return a scoped distance computer capable on operating on validly initialized
252    /// [`Opaque`] slices of length [`Self::bytes`].
253    ///
254    /// These slices should be initialized by [`Self::compress`].
255    fn distance_computer_ref(&self) -> &dyn DynDistanceComputer;
256
257    /// A stand alone distance computer specialized for the specified query layout.
258    ///
259    /// Only layouts for which [`Self::is_supported`] returns `true` are supported.
260    ///
261    /// # Note
262    ///
263    /// The returned object will **only** be compatible with queries compressed using
264    /// [`Self::compress_query`] using the same layout. If possible, the API
265    /// [`Self::fused_query_computer`] should be used to avoid this ambiguity.
266    fn query_computer(
267        &self,
268        layout: QueryLayout,
269        allocator: A,
270    ) -> Result<DistanceComputer<A>, DistanceComputerError>;
271
272    /// Return the number of bytes and alignment of a buffer used to contain a compressed
273    /// query with the provided layout.
274    ///
275    /// Only layouts for which [`Self::is_supported`] returns `true` are supported.
276    fn query_buffer_description(
277        &self,
278        layout: QueryLayout,
279    ) -> Result<QueryBufferDescription, UnsupportedQueryLayout>;
280
281    /// Compress the query using the specified layout into `buffer`.
282    ///
283    /// This requires that buffer have the exact size and alignment as that returned from
284    /// `query_buffer_description`.
285    ///
286    /// Only layouts for which [`Self::is_supported`] returns `true` are supported.
287    fn compress_query(
288        &self,
289        x: &[f32],
290        layout: QueryLayout,
291        allow_rescale: bool,
292        buffer: OpaqueMut<'_>,
293        scratch: ScopedAllocator<'_>,
294    ) -> Result<(), QueryCompressionError>;
295
296    /// Return a query for the argument `x` capable on operating on validly initialized
297    /// [`Opaque`] slices of length [`Self::bytes`].
298    ///
299    /// These slices should be initialized by [`Self::compress`].
300    ///
301    /// Note: Only layouts for which [`Self::is_supported`] returns `true` are supported.
302    fn fused_query_computer(
303        &self,
304        x: &[f32],
305        layout: QueryLayout,
306        allow_rescale: bool,
307        allocator: A,
308        scratch: ScopedAllocator<'_>,
309    ) -> Result<QueryComputer<A>, QueryComputerError>;
310
311    /// Return whether or not this plan supports the given [`QueryLayout`].
312    fn is_supported(&self, layout: QueryLayout) -> bool;
313
314    /// Compress the vector `x` into the opaque slice.
315    ///
316    /// # Note
317    ///
318    /// This requires the length of the slice to be exactly [`Self::bytes`]. There is no
319    /// alignment restriction on the base pointer.
320    fn compress(
321        &self,
322        x: &[f32],
323        into: OpaqueMut<'_>,
324        scratch: ScopedAllocator<'_>,
325    ) -> Result<(), CompressionError>;
326
327    /// Return the metric this plan was created with.
328    fn metric(&self) -> SupportedMetric;
329
330    /// Clone the backing object.
331    fn try_clone_into(&self, allocator: A) -> Result<Poly<dyn Quantizer<A>, A>, AllocatorError>;
332
333    crate::utils::features! {
334        #![feature = "flatbuffers"]
335        /// Serialize `self` into a flatbuffer, returning the flatbuffer. The function
336        /// [`try_deserialize`] should undo this operation.
337        fn serialize(&self, allocator: A) -> Result<Poly<[u8], A>, AllocatorError>;
338    }
339}
340
341#[derive(Debug, Error)]
342#[error("Layout {layout} is not supported for {desc}")]
343pub struct UnsupportedQueryLayout {
344    layout: QueryLayout,
345    desc: &'static str,
346}
347
348impl UnsupportedQueryLayout {
349    fn new(layout: QueryLayout, desc: &'static str) -> Self {
350        Self { layout, desc }
351    }
352}
353
354#[derive(Debug, Error)]
355#[non_exhaustive]
356pub enum DistanceComputerError {
357    #[error(transparent)]
358    UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
359    #[error(transparent)]
360    AllocatorError(#[from] AllocatorError),
361}
362
363#[derive(Debug, Error)]
364#[non_exhaustive]
365pub enum QueryCompressionError {
366    #[error(transparent)]
367    UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
368    #[error(transparent)]
369    CompressionError(#[from] CompressionError),
370    #[error(transparent)]
371    NotCanonical(#[from] NotCanonical),
372    #[error(transparent)]
373    AllocatorError(#[from] AllocatorError),
374}
375
376#[derive(Debug, Error)]
377#[non_exhaustive]
378pub enum QueryComputerError {
379    #[error(transparent)]
380    UnsupportedQueryLayout(#[from] UnsupportedQueryLayout),
381    #[error(transparent)]
382    CompressionError(#[from] CompressionError),
383    #[error(transparent)]
384    AllocatorError(#[from] AllocatorError),
385}
386
387/// Errors that can occur during data compression
388#[derive(Debug, Error)]
389#[error("Error occured during query compression")]
390pub enum CompressionError {
391    /// The input buffer did not have the expected layout. This is an input error.
392    NotCanonical(#[source] InlineError<16>),
393
394    /// Forward any error that occurs during the compression process.
395    ///
396    /// See [`quantizer::CompressionError`] for the complete list.
397    CompressionError(#[source] quantizer::CompressionError),
398}
399
400impl CompressionError {
401    fn not_canonical<E>(error: E) -> Self
402    where
403        E: std::error::Error + Send + Sync + 'static,
404    {
405        Self::NotCanonical(InlineError::new(error))
406    }
407}
408
409#[derive(Debug, Error)]
410#[error("An opaque argument did not have the required alignment or length")]
411pub struct NotCanonical {
412    source: Box<dyn std::error::Error + Send + Sync>,
413}
414
415impl NotCanonical {
416    fn new<E>(err: E) -> Self
417    where
418        E: std::error::Error + Send + Sync + 'static,
419    {
420        Self {
421            source: Box::new(err),
422        }
423    }
424}
425
426////////////
427// Opaque //
428////////////
429
430/// A type-erased slice wrapper used to hide the implementation of spherically quantized
431/// vectors. This allows multiple bit-width implementations to share the same type.
432#[derive(Debug, Clone, Copy)]
433#[repr(transparent)]
434pub struct Opaque<'a>(&'a [u8]);
435
436impl<'a> Opaque<'a> {
437    /// Construct a new `Opaque` referencing `slice`.
438    pub fn new(slice: &'a [u8]) -> Self {
439        Self(slice)
440    }
441
442    /// Consume `self`, returning the wrapped slice.
443    pub fn into_inner(self) -> &'a [u8] {
444        self.0
445    }
446}
447
448impl std::ops::Deref for Opaque<'_> {
449    type Target = [u8];
450    fn deref(&self) -> &[u8] {
451        self.0
452    }
453}
454impl<'short> Reborrow<'short> for Opaque<'_> {
455    type Target = Opaque<'short>;
456    fn reborrow(&'short self) -> Self::Target {
457        *self
458    }
459}
460
461/// A type-erased slice wrapper used to hide the implementation of spherically quantized
462/// vectors. This allows multiple bit-width implementations to share the same type.
463#[derive(Debug)]
464#[repr(transparent)]
465pub struct OpaqueMut<'a>(&'a mut [u8]);
466
467impl<'a> OpaqueMut<'a> {
468    /// Construct a new `OpaqueMut` referencing `slice`.
469    pub fn new(slice: &'a mut [u8]) -> Self {
470        Self(slice)
471    }
472
473    /// Inspect the referenced slice.
474    pub fn inspect(&mut self) -> &mut [u8] {
475        self.0
476    }
477}
478
479impl std::ops::Deref for OpaqueMut<'_> {
480    type Target = [u8];
481    fn deref(&self) -> &[u8] {
482        self.0
483    }
484}
485
486impl std::ops::DerefMut for OpaqueMut<'_> {
487    fn deref_mut(&mut self) -> &mut [u8] {
488        self.0
489    }
490}
491
492//////////////////
493// Query Layout //
494//////////////////
495
496/// The layout to use for the query in [`DistanceComputer`] and [`QueryComputer`].
497#[derive(Debug, Clone, Copy, PartialEq, Eq)]
498pub enum QueryLayout {
499    /// Use the same compression strategy as the data vectors.
500    ///
501    /// This may result in slow compression if high bit-widths are used.
502    SameAsData,
503
504    /// Use 4-bits for the query vector using a bitwise transpose layout.
505    FourBitTransposed,
506
507    /// Use scalar quantization for the query using the same number of bits per dimension
508    /// as the dataset.
509    ScalarQuantized,
510
511    /// Use `f32` to encode the query.
512    FullPrecision,
513}
514
515impl QueryLayout {
516    #[cfg(test)]
517    fn all() -> [Self; 4] {
518        [
519            Self::SameAsData,
520            Self::FourBitTransposed,
521            Self::ScalarQuantized,
522            Self::FullPrecision,
523        ]
524    }
525}
526
527impl std::fmt::Display for QueryLayout {
528    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
529        <Self as Debug>::fmt(self, fmt)
530    }
531}
532
533//////////////////////
534// Layout Reporting //
535//////////////////////
536
537/// Because dynamic dispatch is used heavily in the implementation, it can be easy to lose
538/// track of the actual layout used for the [`DistanceComputer`] and [`QueryComputer`].
539///
540/// This trait provides a mechanism by which we ensure the correct runtime layout is always
541/// reported without requiring manual tracking.
542trait ReportQueryLayout {
543    fn report_query_layout(&self) -> QueryLayout;
544}
545
546impl<T, M, L, R> ReportQueryLayout for Reify<T, M, L, R>
547where
548    T: ReportQueryLayout,
549{
550    fn report_query_layout(&self) -> QueryLayout {
551        self.inner.report_query_layout()
552    }
553}
554
555impl<D, Q> ReportQueryLayout for Curried<D, Q>
556where
557    Q: ReportQueryLayout,
558{
559    fn report_query_layout(&self) -> QueryLayout {
560        self.query.report_query_layout()
561    }
562}
563
564impl<const NBITS: usize, A> ReportQueryLayout for Data<NBITS, A>
565where
566    Unsigned: Representation<NBITS>,
567    A: AllocatorCore,
568{
569    fn report_query_layout(&self) -> QueryLayout {
570        QueryLayout::SameAsData
571    }
572}
573
574impl<const NBITS: usize, A> ReportQueryLayout for Query<NBITS, bits::Dense, A>
575where
576    Unsigned: Representation<NBITS>,
577    A: AllocatorCore,
578{
579    fn report_query_layout(&self) -> QueryLayout {
580        QueryLayout::ScalarQuantized
581    }
582}
583
584impl<A> ReportQueryLayout for Query<4, bits::BitTranspose, A>
585where
586    A: AllocatorCore,
587{
588    fn report_query_layout(&self) -> QueryLayout {
589        QueryLayout::FourBitTransposed
590    }
591}
592
593impl<A> ReportQueryLayout for FullQuery<A>
594where
595    A: AllocatorCore,
596{
597    fn report_query_layout(&self) -> QueryLayout {
598        QueryLayout::FullPrecision
599    }
600}
601
602//-----------------------//
603// Reification Utilities //
604//-----------------------//
605
606/// An adaptor trait defining how to go from an `Opaque` slice to a fully reified type.
607///
608/// THis is the building block for building distance computers with the reificiation code
609/// inlined into the callsite.
610trait FromOpaque: 'static + Send + Sync + Debug {
611    type Target<'a>;
612    type Error: std::error::Error + Send + Sync + 'static;
613
614    fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error>;
615}
616
617/// Reify as full-precision.
618#[derive(Debug, Default)]
619pub(super) struct AsFull;
620
621/// Reify as data.
622#[derive(Debug, Default)]
623pub(super) struct AsData<const NBITS: usize>;
624
625/// Reify as scalar quantized query.
626#[derive(Debug)]
627pub(super) struct AsQuery<const NBITS: usize, Perm = bits::Dense> {
628    _marker: PhantomData<Perm>,
629}
630
631// This impelmentation works around the `derive` impl requiring `Perm: Default`.
632impl<const NBITS: usize, Perm> Default for AsQuery<NBITS, Perm> {
633    fn default() -> Self {
634        Self {
635            _marker: PhantomData,
636        }
637    }
638}
639
640impl FromOpaque for AsFull {
641    type Target<'a> = FullQueryRef<'a>;
642    type Error = meta::slice::NotCanonical;
643
644    fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
645        Self::Target::from_canonical(query.into_inner(), dim)
646    }
647}
648
649impl ReportQueryLayout for AsFull {
650    fn report_query_layout(&self) -> QueryLayout {
651        QueryLayout::FullPrecision
652    }
653}
654
655impl<const NBITS: usize> FromOpaque for AsData<NBITS>
656where
657    Unsigned: Representation<NBITS>,
658{
659    type Target<'a> = DataRef<'a, NBITS>;
660    type Error = meta::NotCanonical;
661
662    fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
663        Self::Target::from_canonical_back(query.into_inner(), dim)
664    }
665}
666
667impl<const NBITS: usize> ReportQueryLayout for AsData<NBITS> {
668    fn report_query_layout(&self) -> QueryLayout {
669        QueryLayout::SameAsData
670    }
671}
672
673impl<const NBITS: usize, Perm> FromOpaque for AsQuery<NBITS, Perm>
674where
675    Unsigned: Representation<NBITS>,
676    Perm: bits::PermutationStrategy<NBITS> + Debug + Send + Sync + 'static,
677{
678    type Target<'a> = QueryRef<'a, NBITS, Perm>;
679    type Error = meta::NotCanonical;
680
681    fn from_opaque<'a>(query: Opaque<'a>, dim: usize) -> Result<Self::Target<'a>, Self::Error> {
682        Self::Target::from_canonical_back(query.into_inner(), dim)
683    }
684}
685
686impl<const NBITS: usize> ReportQueryLayout for AsQuery<NBITS, bits::Dense> {
687    fn report_query_layout(&self) -> QueryLayout {
688        QueryLayout::ScalarQuantized
689    }
690}
691
692impl<const NBITS: usize> ReportQueryLayout for AsQuery<NBITS, bits::BitTranspose> {
693    fn report_query_layout(&self) -> QueryLayout {
694        QueryLayout::FourBitTransposed
695    }
696}
697
698//-------//
699// Reify //
700//-------//
701
702/// Helper struct to convert an [`Opaque`] to a fully-typed [`DataRef`].
703#[derive(Debug)]
704pub(super) struct Reify<T, M, L, R> {
705    inner: T,
706    dim: usize,
707    arch: M,
708    _markers: PhantomData<(L, R)>,
709}
710
711impl<T, M, L, R> Reify<T, M, L, R> {
712    pub(super) fn new(inner: T, dim: usize, arch: M) -> Self {
713        Self {
714            inner,
715            dim,
716            arch,
717            _markers: PhantomData,
718        }
719    }
720}
721
722impl<M, T, R> DynQueryComputer for Reify<T, M, (), R>
723where
724    M: Architecture,
725    R: FromOpaque,
726    T: ReportQueryLayout + Debug + Send + Sync,
727    for<'a> &'a T: Target1<M, Rf32, R::Target<'a>>,
728{
729    fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
730        self.arch.run2(
731            |this: &Self, x| {
732                let x = R::from_opaque(x, this.dim)
733                    .map_err(|err| QueryDistanceError::XReify(InlineError::new(err)))?;
734                this.arch
735                    .run1(&this.inner, x)
736                    .map_err(QueryDistanceError::UnequalLengths)
737            },
738            self,
739            x,
740        )
741    }
742
743    fn layout(&self) -> QueryLayout {
744        self.inner.report_query_layout()
745    }
746}
747
748impl<T, M, Q, R> DynDistanceComputer for Reify<T, M, Q, R>
749where
750    M: Architecture,
751    Q: FromOpaque + Default + ReportQueryLayout,
752    R: FromOpaque,
753    T: for<'a> Target2<M, Rf32, Q::Target<'a>, R::Target<'a>> + Copy + Debug + Send + Sync,
754{
755    fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
756        self.arch.run3(
757            |this: &Self, query, x| {
758                let query = Q::from_opaque(query, this.dim)
759                    .map_err(|err| DistanceError::QueryReify(InlineError::<24>::new(err)))?;
760
761                let x = R::from_opaque(x, this.dim)
762                    .map_err(|err| DistanceError::XReify(InlineError::<16>::new(err)))?;
763
764                this.arch
765                    .run2_inline(this.inner, query, x)
766                    .map_err(DistanceError::UnequalLengths)
767            },
768            self,
769            query,
770            x,
771        )
772    }
773
774    fn layout(&self) -> QueryLayout {
775        Q::default().report_query_layout()
776    }
777}
778
779///////////////////////
780// Query Computation //
781///////////////////////
782
783/// Errors that can occur while perfoming distance cacluations on opaque vectors.
784#[derive(Debug, Error)]
785pub enum QueryDistanceError {
786    /// The right-hand data argument appears to be malformed.
787    #[error("trouble trying to reify the argument")]
788    XReify(#[source] InlineError<16>),
789
790    /// Distance computation failed because the logical lengths of the two vectors differ.
791    #[error("encountered while trying to compute distances")]
792    UnequalLengths(#[source] UnequalLengths),
793}
794
795pub trait DynQueryComputer: Send + Sync + Debug {
796    fn evaluate(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError>;
797    fn layout(&self) -> QueryLayout;
798}
799
800/// An opaque [`PreprocessedDistanceFunction`] for the [`Quantizer`] trait object.
801///
802/// # Note
803///
804/// This is only valid to call on [`Opaque`] slices compressed by the same [`Quantizer`] that
805/// created the computer.
806///
807/// Otherwise, distance computations may return garbage values or panic.
808#[derive(Debug)]
809pub struct QueryComputer<A = GlobalAllocator>
810where
811    A: AllocatorCore,
812{
813    inner: Poly<dyn DynQueryComputer, A>,
814}
815
816impl<A> QueryComputer<A>
817where
818    A: AllocatorCore,
819{
820    fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
821    where
822        T: DynQueryComputer + 'static,
823    {
824        let inner = Poly::new(inner, allocator)?;
825        Ok(Self {
826            inner: poly!(DynQueryComputer, inner),
827        })
828    }
829
830    /// Report the layout used by the query computer.
831    pub fn layout(&self) -> QueryLayout {
832        self.inner.layout()
833    }
834
835    /// This is a temporary function until custom allocator support fully comes on line.
836    pub fn into_inner(self) -> Poly<dyn DynQueryComputer, A> {
837        self.inner
838    }
839}
840
841impl<A> PreprocessedDistanceFunction<Opaque<'_>, Result<f32, QueryDistanceError>>
842    for QueryComputer<A>
843where
844    A: AllocatorCore,
845{
846    fn evaluate_similarity(&self, x: Opaque<'_>) -> Result<f32, QueryDistanceError> {
847        self.inner.evaluate(x)
848    }
849}
850
851/// To handle multiple query bit-widths, we use type erasure on the actual distance
852/// function implementation.
853///
854/// This struct represents the partial application of the `inner` distance function with
855/// `query` in a generic way so we only have one level of dynamic dispatch when computing
856/// distances.
857#[derive(Debug)]
858pub(super) struct Curried<D, Q> {
859    inner: D,
860    query: Q,
861}
862
863impl<D, Q> Curried<D, Q> {
864    pub(super) fn new(inner: D, query: Q) -> Self {
865        Self { inner, query }
866    }
867}
868
869impl<A, D, Q, T, R> Target1<A, R, T> for &Curried<D, Q>
870where
871    A: Architecture,
872    Q: for<'a> Reborrow<'a>,
873    D: for<'a> Target2<A, R, <Q as Reborrow<'a>>::Target, T> + Copy,
874{
875    fn run(self, arch: A, x: T) -> R {
876        self.inner.run(arch, self.query.reborrow(), x)
877    }
878}
879
880///////////////////////
881// Distance Computer //
882///////////////////////
883
884/// Errors that can occur while perfoming distance cacluations on opaque vectors.
885#[derive(Debug, Error)]
886pub enum DistanceError {
887    /// The left-hand data argument appears to be malformed.
888    #[error("trouble trying to reify the left-hand argument")]
889    QueryReify(InlineError<24>),
890
891    /// The right-hand data argument appears to be malformed.
892    #[error("trouble trying to reify the right-hand argument")]
893    XReify(InlineError<16>),
894
895    /// Distance computation failed because the logical lengths of the two vectors differ.
896    ///
897    /// If vector reificiation occurs successfully, then this should not be returned.
898    #[error("encountered while trying to compute distances")]
899    UnequalLengths(UnequalLengths),
900}
901
902pub trait DynDistanceComputer: Send + Sync + Debug {
903    fn evaluate(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError>;
904    fn layout(&self) -> QueryLayout;
905}
906
907/// An opaque [`DistanceFunction`] for the [`Quantizer`] trait object.
908///
909/// # Note
910///
911/// Left-hand arguments must be [`Opaque`] slices compressed using
912/// [`Quantizer::compress_query`] using [`Self::layout`].
913///
914/// Right-hand arguments must be [`Opaque`] slices compressed using [`Quantizer::compress`].
915///
916/// Otherwise, distance computations may return garbage values or panic.
917#[derive(Debug)]
918pub struct DistanceComputer<A = GlobalAllocator>
919where
920    A: AllocatorCore,
921{
922    inner: Poly<dyn DynDistanceComputer, A>,
923}
924
925impl<A> DistanceComputer<A>
926where
927    A: AllocatorCore,
928{
929    pub(super) fn new<T>(inner: T, allocator: A) -> Result<Self, AllocatorError>
930    where
931        T: DynDistanceComputer + 'static,
932    {
933        let inner = Poly::new(inner, allocator)?;
934        Ok(Self {
935            inner: poly!(DynDistanceComputer, inner),
936        })
937    }
938
939    /// Report the layout used by the query computer.
940    pub fn layout(&self) -> QueryLayout {
941        self.inner.layout()
942    }
943
944    pub fn into_inner(self) -> Poly<dyn DynDistanceComputer, A> {
945        self.inner
946    }
947}
948
949impl<A> DistanceFunction<Opaque<'_>, Opaque<'_>, Result<f32, DistanceError>> for DistanceComputer<A>
950where
951    A: AllocatorCore,
952{
953    fn evaluate_similarity(&self, query: Opaque<'_>, x: Opaque<'_>) -> Result<f32, DistanceError> {
954        self.inner.evaluate(query, x)
955    }
956}
957
958//////////
959// Impl //
960//////////
961
962/// The base number of bytes to allocate when attempting to serialize a quantizer.
963#[cfg(all(not(test), feature = "flatbuffers"))]
964const DEFAULT_SERIALIZED_BYTES: usize = 1024;
965
966// When testing, use a small value so we trigger the reallocation logic.
967#[cfg(all(test, feature = "flatbuffers"))]
968const DEFAULT_SERIALIZED_BYTES: usize = 1;
969
970/// Implementation for [`Quantizer`] specializing on the number of bits used for data
971/// compression.
972#[derive(Debug)]
973pub struct Impl<const NBITS: usize, A = GlobalAllocator>
974where
975    A: Allocator,
976{
977    quantizer: SphericalQuantizer<A>,
978    distance: Poly<dyn DynDistanceComputer, A>,
979}
980
981/// Pre-dispatch distance functions between compressed data vectors from `quantizer`
982/// specialized for the current run-time mciro architecture.
983pub trait Constructible<A = GlobalAllocator>
984where
985    A: Allocator,
986{
987    fn dispatch_distance(
988        quantizer: &SphericalQuantizer<A>,
989    ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError>;
990}
991
992impl<const NBITS: usize, A: Allocator> Constructible<A> for Impl<NBITS, A>
993where
994    A: Allocator,
995    AsData<NBITS>: FromOpaque,
996    SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
997{
998    fn dispatch_distance(
999        quantizer: &SphericalQuantizer<A>,
1000    ) -> Result<Poly<dyn DynDistanceComputer, A>, AllocatorError> {
1001        diskann_wide::arch::dispatch2_no_features(
1002            ComputerDispatcher::<AsData<NBITS>, NBITS>::new(),
1003            quantizer,
1004            quantizer.allocator().clone(),
1005        )
1006        .map(|obj| obj.inner)
1007    }
1008}
1009
1010impl<const NBITS: usize, A> TryClone for Impl<NBITS, A>
1011where
1012    A: Allocator,
1013    AsData<NBITS>: FromOpaque,
1014    SphericalQuantizer<A>: Dispatchable<AsData<NBITS>, NBITS>,
1015{
1016    fn try_clone(&self) -> Result<Self, AllocatorError> {
1017        Self::new(self.quantizer.try_clone()?)
1018    }
1019}
1020
1021impl<const NBITS: usize, A: Allocator> Impl<NBITS, A> {
1022    /// Construct a new plan around `quantizer` providing distance computers for `metric`.
1023    pub fn new(quantizer: SphericalQuantizer<A>) -> Result<Self, AllocatorError>
1024    where
1025        Self: Constructible<A>,
1026    {
1027        let distance = Self::dispatch_distance(&quantizer)?;
1028        Ok(Self {
1029            quantizer,
1030            distance,
1031        })
1032    }
1033
1034    /// Return the underlying [`SphericalQuantizer`].
1035    pub fn quantizer(&self) -> &SphericalQuantizer<A> {
1036        &self.quantizer
1037    }
1038
1039    /// Return `true` if this plan supports `layout` for query computers.
1040    ///
1041    /// Otherwise, return `false`.
1042    pub fn supports(layout: QueryLayout) -> bool {
1043        if const { NBITS == 1 } {
1044            [
1045                QueryLayout::SameAsData,
1046                QueryLayout::FourBitTransposed,
1047                QueryLayout::FullPrecision,
1048            ]
1049            .contains(&layout)
1050        } else {
1051            [
1052                QueryLayout::SameAsData,
1053                QueryLayout::ScalarQuantized,
1054                QueryLayout::FullPrecision,
1055            ]
1056            .contains(&layout)
1057        }
1058    }
1059
1060    /// Return a [`DistanceComputer`] that is specialized for the most specific runtime
1061    /// architecture.
1062    fn query_computer<Q, B>(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError>
1063    where
1064        Q: FromOpaque,
1065        B: AllocatorCore,
1066        SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1067    {
1068        diskann_wide::arch::dispatch2_no_features(
1069            ComputerDispatcher::<Q, NBITS>::new(),
1070            &self.quantizer,
1071            allocator,
1072        )
1073    }
1074
1075    fn compress_query<'a, T>(
1076        &self,
1077        query: &'a [f32],
1078        storage: T,
1079        scratch: ScopedAllocator<'a>,
1080    ) -> Result<(), QueryCompressionError>
1081    where
1082        SphericalQuantizer<A>: CompressIntoWith<&'a [f32], T, ScopedAllocator<'a>, Error = quantizer::CompressionError>,
1083    {
1084        self.quantizer
1085            .compress_into_with(query, storage, scratch)
1086            .map_err(|err| CompressionError::CompressionError(err).into())
1087    }
1088
1089    /// Return a [`QueryComputer`] that is specialized for the most specific runtime
1090    /// architecture.
1091    fn fused_query_computer<Q, T, B>(
1092        &self,
1093        query: &[f32],
1094        mut storage: T,
1095        allocator: B,
1096        scratch: ScopedAllocator<'_>,
1097    ) -> Result<QueryComputer<B>, QueryComputerError>
1098    where
1099        Q: FromOpaque,
1100        T: for<'a> ReborrowMut<'a>
1101            + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1102            + ReportQueryLayout
1103            + Debug
1104            + Send
1105            + Sync
1106            + 'static,
1107        B: AllocatorCore,
1108        SphericalQuantizer<A>: for<'a> CompressIntoWith<
1109                &'a [f32],
1110                <T as ReborrowMut<'a>>::Target,
1111                ScopedAllocator<'a>,
1112                Error = quantizer::CompressionError,
1113            >,
1114        SphericalQuantizer<A>: Dispatchable<Q, NBITS>,
1115    {
1116        if let Err(err) = self
1117            .quantizer
1118            .compress_into_with(query, storage.reborrow_mut(), scratch)
1119        {
1120            return Err(CompressionError::CompressionError(err).into());
1121        }
1122
1123        diskann_wide::arch::dispatch3_no_features(
1124            ComputerDispatcher::<Q, NBITS>::new(),
1125            &self.quantizer,
1126            storage,
1127            allocator,
1128        )
1129        .map_err(|e| e.into())
1130    }
1131
1132    /// Attempt to deserialize a FlatBuffer `fb::spherical::Quantizer` (as produced by [`Quantizer::serialize`]) into [`Impl`].
1133    #[cfg(feature = "flatbuffers")]
1134    #[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
1135    pub fn try_deserialize(data: &[u8], alloc: A) -> Result<Self, DeserializationError>
1136    where
1137        Self: Constructible<A>,
1138    {
1139        // Check that this is one of the known identifiers.
1140        if !fb::spherical::quantizer_buffer_has_identifier(data) {
1141            return Err(DeserializationError::InvalidIdentifier);
1142        }
1143
1144        // Match as much as we can without allocating.
1145        //
1146        // Then, we branch on the number of bits.
1147        let root = fb::spherical::root_as_quantizer(data)?;
1148        let nbits = root.nbits();
1149        let proto = root.quantizer();
1150
1151        if nbits as usize == NBITS {
1152            Self::try_deserialize_from(proto, alloc)
1153        } else {
1154            Err(DeserializationError::UnsupportedBitWidth(nbits))
1155        }
1156    }
1157
1158    #[cfg(feature = "flatbuffers")]
1159    fn try_deserialize_from(
1160        proto: fb::spherical::SphericalQuantizer<'_>,
1161        alloc: A,
1162    ) -> Result<Self, DeserializationError>
1163    where
1164        Self: Constructible<A>,
1165    {
1166        let quantizer = SphericalQuantizer::try_unpack(alloc, proto)?;
1167        Ok(Self::new(quantizer)?)
1168    }
1169
1170    #[cfg(feature = "flatbuffers")]
1171    fn serialize<B>(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError>
1172    where
1173        B: Allocator + std::panic::UnwindSafe,
1174        A: std::panic::RefUnwindSafe,
1175    {
1176        let mut buf = FlatBufferBuilder::new_in(Poly::broadcast(
1177            0u8,
1178            DEFAULT_SERIALIZED_BYTES,
1179            allocator.clone(),
1180        )?);
1181
1182        let quantizer = &self.quantizer;
1183
1184        let (root, mut buf) = match std::panic::catch_unwind(move || {
1185            let offset = quantizer.pack(&mut buf);
1186
1187            let root = fb::spherical::Quantizer::create(
1188                &mut buf,
1189                &fb::spherical::QuantizerArgs {
1190                    quantizer: Some(offset),
1191                    nbits: NBITS as u32,
1192                },
1193            );
1194            (root, buf)
1195        }) {
1196            Ok(ret) => ret,
1197            Err(err) => match err.downcast_ref::<String>() {
1198                Some(msg) => {
1199                    if msg.contains("AllocatorError") {
1200                        return Err(AllocatorError);
1201                    } else {
1202                        std::panic::resume_unwind(err);
1203                    }
1204                }
1205                None => std::panic::resume_unwind(err),
1206            },
1207        };
1208
1209        // Finish serializing and then copy out the finished data into a newly allocated buffer.
1210        fb::spherical::finish_quantizer_buffer(&mut buf, root);
1211        Poly::from_iter(buf.finished_data().iter().copied(), allocator)
1212    }
1213}
1214
1215//----------------------//
1216// Distance Dispatching //
1217//----------------------//
1218
1219/// This trait and [`ComputerDispatcher`] are the glue for pre-dispatching
1220/// micro-architecture compatibility of distance computers.
1221///
1222/// This trait takes
1223///
1224/// * `M`: The target micro-architecture.
1225/// * `Q`: The target query type
1226///
1227/// And generates a specialized `DistanceComputer` and `QueryComputer`.
1228///
1229/// The [`ComputerDispatcher`] struct implements the [`diskann_wide::arch::Target2`] and
1230/// [`diskann_wide::arch::Target3`] traits to do the architecture-dispatching.
1231trait BuildComputer<M, Q, const N: usize>
1232where
1233    M: Architecture,
1234    Q: FromOpaque,
1235{
1236    /// Build a [`DistanceComputer`] targeting the micro-architecture `M`.
1237    ///
1238    /// The resulting object should implement distance calculations using just a single
1239    /// level of indirection.
1240    fn build_computer<A>(
1241        &self,
1242        arch: M,
1243        allocator: A,
1244    ) -> Result<DistanceComputer<A>, AllocatorError>
1245    where
1246        A: AllocatorCore;
1247
1248    /// Build a [`DistanceComputer`] with `query` targeting the micro-architecture `M`.
1249    ///
1250    /// The resulting object should implement distance calculations using just a single
1251    /// level of indirection.
1252    fn build_fused_computer<R, A>(
1253        &self,
1254        arch: M,
1255        query: R,
1256        allocator: A,
1257    ) -> Result<QueryComputer<A>, AllocatorError>
1258    where
1259        R: ReportQueryLayout
1260            + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1261            + Debug
1262            + Send
1263            + Sync
1264            + 'static,
1265        A: AllocatorCore;
1266}
1267
1268fn identity<T>(x: T) -> T {
1269    x
1270}
1271
1272macro_rules! dispatch_map {
1273    ($N:literal, $Q:ty, $arch:ty) => {
1274        dispatch_map!($N, $Q, $arch, identity);
1275    };
1276    ($N:literal, $Q:ty, $arch:ty, $op:ident) => {
1277        impl<A> BuildComputer<$arch, $Q, $N> for SphericalQuantizer<A>
1278        where
1279            A: Allocator,
1280        {
1281            fn build_computer<B>(
1282                &self,
1283                input_arch: $arch,
1284                allocator: B,
1285            ) -> Result<DistanceComputer<B>, AllocatorError>
1286            where
1287                B: AllocatorCore,
1288            {
1289                type D = AsData<$N>;
1290
1291                // Perform any architecture down-casting.
1292                let arch = ($op)(input_arch);
1293                let dim = self.output_dim();
1294                match self.metric() {
1295                    SupportedMetric::SquaredL2 => {
1296                        let reify = Reify::<CompensatedSquaredL2, _, $Q, D>::new(
1297                            self.as_functor(),
1298                            dim,
1299                            arch,
1300                        );
1301                        DistanceComputer::new(reify, allocator)
1302                    }
1303                    SupportedMetric::InnerProduct => {
1304                        let reify =
1305                            Reify::<CompensatedIP, _, $Q, D>::new(self.as_functor(), dim, arch);
1306                        DistanceComputer::new(reify, allocator)
1307                    }
1308                    SupportedMetric::Cosine => {
1309                        let reify =
1310                            Reify::<CompensatedCosine, _, $Q, D>::new(self.as_functor(), dim, arch);
1311                        DistanceComputer::new(reify, allocator)
1312                    }
1313                }
1314            }
1315
1316            fn build_fused_computer<R, B>(
1317                &self,
1318                input_arch: $arch,
1319                query: R,
1320                allocator: B,
1321            ) -> Result<QueryComputer<B>, AllocatorError>
1322            where
1323                R: ReportQueryLayout
1324                    + for<'a> Reborrow<'a, Target = <$Q as FromOpaque>::Target<'a>>
1325                    + Debug
1326                    + Send
1327                    + Sync
1328                    + 'static,
1329                B: AllocatorCore,
1330            {
1331                type D = AsData<$N>;
1332                let arch = ($op)(input_arch);
1333                let dim = self.output_dim();
1334                match self.metric() {
1335                    SupportedMetric::SquaredL2 => {
1336                        let computer: CompensatedSquaredL2 = self.as_functor();
1337                        let curried = Curried::new(computer, query);
1338                        let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1339                        Ok(QueryComputer::new(reify, allocator)?)
1340                    }
1341                    SupportedMetric::InnerProduct => {
1342                        let computer: CompensatedIP = self.as_functor();
1343                        let curried = Curried::new(computer, query);
1344                        let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1345                        Ok(QueryComputer::new(reify, allocator)?)
1346                    }
1347                    SupportedMetric::Cosine => {
1348                        let computer: CompensatedCosine = self.as_functor();
1349                        let curried = Curried::new(computer, query);
1350                        let reify = Reify::<_, _, (), D>::new(curried, dim, arch);
1351                        Ok(QueryComputer::new(reify, allocator)?)
1352                    }
1353                }
1354            }
1355        }
1356    };
1357}
1358
1359dispatch_map!(1, AsFull, Scalar);
1360dispatch_map!(2, AsFull, Scalar);
1361dispatch_map!(4, AsFull, Scalar);
1362dispatch_map!(8, AsFull, Scalar);
1363
1364dispatch_map!(1, AsData<1>, Scalar);
1365dispatch_map!(2, AsData<2>, Scalar);
1366dispatch_map!(4, AsData<4>, Scalar);
1367dispatch_map!(8, AsData<8>, Scalar);
1368
1369// Special Cases
1370dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Scalar);
1371dispatch_map!(2, AsQuery<2>, Scalar);
1372dispatch_map!(4, AsQuery<4>, Scalar);
1373dispatch_map!(8, AsQuery<8>, Scalar);
1374
1375cfg_if::cfg_if! {
1376    if #[cfg(target_arch = "x86_64")] {
1377        fn downcast_to_v3(arch: V4) -> V3 {
1378            arch.into()
1379        }
1380
1381        // V3
1382        dispatch_map!(1, AsFull, V3);
1383        dispatch_map!(2, AsFull, V3);
1384        dispatch_map!(4, AsFull, V3);
1385        dispatch_map!(8, AsFull, V3);
1386
1387        dispatch_map!(1, AsData<1>, V3);
1388        dispatch_map!(2, AsData<2>, V3);
1389        dispatch_map!(4, AsData<4>, V3);
1390        dispatch_map!(8, AsData<8>, V3);
1391
1392        dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V3);
1393        dispatch_map!(2, AsQuery<2>, V3);
1394        dispatch_map!(4, AsQuery<4>, V3);
1395        dispatch_map!(8, AsQuery<8>, V3);
1396
1397        // V4
1398        dispatch_map!(1, AsFull, V4, downcast_to_v3);
1399        dispatch_map!(2, AsFull, V4, downcast_to_v3);
1400        dispatch_map!(4, AsFull, V4, downcast_to_v3);
1401        dispatch_map!(8, AsFull, V4, downcast_to_v3);
1402
1403        dispatch_map!(1, AsData<1>, V4, downcast_to_v3);
1404        dispatch_map!(2, AsData<2>, V4); // specialized
1405        dispatch_map!(4, AsData<4>, V4); // specialized
1406        dispatch_map!(8, AsData<8>, V4, downcast_to_v3);
1407
1408        dispatch_map!(1, AsQuery<4, bits::BitTranspose>, V4, downcast_to_v3);
1409        dispatch_map!(2, AsQuery<2>, V4); // specialized
1410        dispatch_map!(4, AsQuery<4>, V4); // specialized
1411        dispatch_map!(8, AsQuery<8>, V4, downcast_to_v3);
1412    } else if #[cfg(target_arch = "aarch64")] {
1413        fn downcast(arch: Neon) -> Scalar {
1414            arch.retarget()
1415        }
1416
1417        dispatch_map!(1, AsFull, Neon, downcast);
1418        dispatch_map!(2, AsFull, Neon, downcast);
1419        dispatch_map!(4, AsFull, Neon, downcast);
1420        dispatch_map!(8, AsFull, Neon, downcast);
1421
1422        dispatch_map!(1, AsData<1>, Neon, downcast);
1423        dispatch_map!(2, AsData<2>, Neon);
1424        dispatch_map!(4, AsData<4>, Neon);
1425        dispatch_map!(8, AsData<8>, Neon, downcast);
1426
1427        dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Neon, downcast);
1428        dispatch_map!(2, AsQuery<2>, Neon);
1429        dispatch_map!(4, AsQuery<4>, Neon);
1430        dispatch_map!(8, AsQuery<8>, Neon, downcast);
1431    }
1432}
1433
1434/// This struct and the [`BuildComputer`] trait are the glue for pre-dispatching
1435/// micro-architecture compatibility of distance computers.
1436///
1437/// This trait takes
1438///
1439/// * `Q`: The target query type.
1440/// * `N`: The nubmer of data bits to target.
1441///
1442/// This struct implements [`diskann_wide::arch::Target2`] and
1443/// [`diskann_wide::arch::Target3`] traits to do the architecture-dispatching, relying on
1444/// `Impl<N, A> as BuildQueryComputer` for the implementation.
1445#[derive(Debug, Clone, Copy)]
1446struct ComputerDispatcher<Q, const N: usize> {
1447    _query_type: std::marker::PhantomData<Q>,
1448}
1449
1450impl<Q, const N: usize> ComputerDispatcher<Q, N> {
1451    fn new() -> Self {
1452        Self {
1453            _query_type: std::marker::PhantomData,
1454        }
1455    }
1456}
1457
1458impl<M, const N: usize, A, B, Q>
1459    diskann_wide::arch::Target2<
1460        M,
1461        Result<DistanceComputer<B>, AllocatorError>,
1462        &SphericalQuantizer<A>,
1463        B,
1464    > for ComputerDispatcher<Q, N>
1465where
1466    M: Architecture,
1467    A: Allocator,
1468    B: AllocatorCore,
1469    Q: FromOpaque,
1470    SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1471{
1472    fn run(
1473        self,
1474        arch: M,
1475        quantizer: &SphericalQuantizer<A>,
1476        allocator: B,
1477    ) -> Result<DistanceComputer<B>, AllocatorError> {
1478        quantizer.build_computer(arch, allocator)
1479    }
1480}
1481
1482impl<M, const N: usize, A, R, B, Q>
1483    diskann_wide::arch::Target3<
1484        M,
1485        Result<QueryComputer<B>, AllocatorError>,
1486        &SphericalQuantizer<A>,
1487        R,
1488        B,
1489    > for ComputerDispatcher<Q, N>
1490where
1491    M: Architecture,
1492    A: Allocator,
1493    B: AllocatorCore,
1494    Q: FromOpaque,
1495    R: ReportQueryLayout
1496        + for<'a> Reborrow<'a, Target = Q::Target<'a>>
1497        + Debug
1498        + Send
1499        + Sync
1500        + 'static,
1501    SphericalQuantizer<A>: BuildComputer<M, Q, N>,
1502{
1503    fn run(
1504        self,
1505        arch: M,
1506        quantizer: &SphericalQuantizer<A>,
1507        query: R,
1508        allocator: B,
1509    ) -> Result<QueryComputer<B>, AllocatorError> {
1510        quantizer.build_fused_computer(arch, query, allocator)
1511    }
1512}
1513
1514#[cfg(target_arch = "x86_64")]
1515trait Dispatchable<Q, const N: usize>:
1516    BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>
1517where
1518    Q: FromOpaque,
1519{
1520}
1521
1522#[cfg(target_arch = "x86_64")]
1523impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1524where
1525    Q: FromOpaque,
1526    T: BuildComputer<Scalar, Q, N> + BuildComputer<V3, Q, N> + BuildComputer<V4, Q, N>,
1527{
1528}
1529
1530#[cfg(target_arch = "aarch64")]
1531trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>
1532where
1533    Q: FromOpaque,
1534{
1535}
1536
1537#[cfg(target_arch = "aarch64")]
1538impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1539where
1540    Q: FromOpaque,
1541    T: BuildComputer<Scalar, Q, N> + BuildComputer<Neon, Q, N>,
1542{
1543}
1544
1545#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1546trait Dispatchable<Q, const N: usize>: BuildComputer<Scalar, Q, N>
1547where
1548    Q: FromOpaque,
1549{
1550}
1551
1552#[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
1553impl<Q, const N: usize, T> Dispatchable<Q, N> for T
1554where
1555    Q: FromOpaque,
1556    T: BuildComputer<Scalar, Q, N>,
1557{
1558}
1559
1560//---------------------------//
1561// Quantizer Implementations //
1562//---------------------------//
1563
1564impl<A, B> Quantizer<B> for Impl<1, A>
1565where
1566    A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
1567    B: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
1568{
1569    fn nbits(&self) -> usize {
1570        1
1571    }
1572
1573    fn dim(&self) -> usize {
1574        self.quantizer.output_dim()
1575    }
1576
1577    fn full_dim(&self) -> usize {
1578        self.quantizer.input_dim()
1579    }
1580
1581    fn bytes(&self) -> usize {
1582        DataRef::<1>::canonical_bytes(self.quantizer.output_dim())
1583    }
1584
1585    fn distance_computer(&self, allocator: B) -> Result<DistanceComputer<B>, AllocatorError> {
1586        self.query_computer::<AsData<1>, _>(allocator)
1587    }
1588
1589    fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1590        &*self.distance
1591    }
1592
1593    fn query_computer(
1594        &self,
1595        layout: QueryLayout,
1596        allocator: B,
1597    ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1598        match layout {
1599            QueryLayout::SameAsData => Ok(self.query_computer::<AsData<1>, _>(allocator)?),
1600            QueryLayout::FourBitTransposed => {
1601                Ok(self.query_computer::<AsQuery<4, bits::BitTranspose>, _>(allocator)?)
1602            }
1603            QueryLayout::ScalarQuantized => {
1604                Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1605            }
1606            QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1607        }
1608    }
1609
1610    fn query_buffer_description(
1611        &self,
1612        layout: QueryLayout,
1613    ) -> Result<QueryBufferDescription, UnsupportedQueryLayout> {
1614        let dim = <Self as Quantizer<B>>::dim(self);
1615        match layout {
1616            QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1617                DataRef::<1>::canonical_bytes(dim),
1618                PowerOfTwo::alignment_of::<u8>(),
1619            )),
1620            QueryLayout::FourBitTransposed => Ok(QueryBufferDescription::new(
1621                QueryRef::<4, bits::BitTranspose>::canonical_bytes(dim),
1622                PowerOfTwo::alignment_of::<u8>(),
1623            )),
1624            QueryLayout::ScalarQuantized => {
1625                Err(UnsupportedQueryLayout::new(layout, "1-bit compression"))
1626            }
1627            QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1628                FullQueryRef::canonical_bytes(dim),
1629                FullQueryRef::canonical_align(),
1630            )),
1631        }
1632    }
1633
1634    fn compress_query(
1635        &self,
1636        x: &[f32],
1637        layout: QueryLayout,
1638        allow_rescale: bool,
1639        mut buffer: OpaqueMut<'_>,
1640        scratch: ScopedAllocator<'_>,
1641    ) -> Result<(), QueryCompressionError> {
1642        let dim = <Self as Quantizer<B>>::dim(self);
1643        let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1644            match layout {
1645                QueryLayout::SameAsData => self.compress_query(
1646                    v,
1647                    DataMut::<1>::from_canonical_back_mut(&mut buffer, dim)
1648                        .map_err(NotCanonical::new)?,
1649                    scratch,
1650                ),
1651                QueryLayout::FourBitTransposed => self.compress_query(
1652                    v,
1653                    QueryMut::<4, bits::BitTranspose>::from_canonical_back_mut(&mut buffer, dim)
1654                        .map_err(NotCanonical::new)?,
1655                    scratch,
1656                ),
1657                QueryLayout::ScalarQuantized => {
1658                    Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1659                }
1660                QueryLayout::FullPrecision => self.compress_query(
1661                    v,
1662                    FullQueryMut::from_canonical_mut(&mut buffer, dim)
1663                        .map_err(NotCanonical::new)?,
1664                    scratch,
1665                ),
1666            }
1667        };
1668
1669        if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1670            let mut copy = x.to_owned();
1671            self.quantizer.rescale(&mut copy);
1672            finish(&copy)
1673        } else {
1674            finish(x)
1675        }
1676    }
1677
1678    fn fused_query_computer(
1679        &self,
1680        x: &[f32],
1681        layout: QueryLayout,
1682        allow_rescale: bool,
1683        allocator: B,
1684        scratch: ScopedAllocator<'_>,
1685    ) -> Result<QueryComputer<B>, QueryComputerError> {
1686        let dim = <Self as Quantizer<B>>::dim(self);
1687        let finish = |v: &[f32], allocator: B| -> Result<QueryComputer<B>, QueryComputerError> {
1688            match layout {
1689                    QueryLayout::SameAsData => self.fused_query_computer::<AsData<1>, Data<1, _>, _>(
1690                        v,
1691                        Data::new_in(dim, allocator.clone())?,
1692                        allocator,
1693                        scratch,
1694                    ),
1695                    QueryLayout::FourBitTransposed => self
1696                        .fused_query_computer::<AsQuery<4, bits::BitTranspose>, Query<4, bits::BitTranspose, _>, _>(
1697                            v,
1698                            Query::new_in(dim, allocator.clone())?,
1699                            allocator,
1700                            scratch,
1701                        ),
1702                    QueryLayout::ScalarQuantized => {
1703                        Err(UnsupportedQueryLayout::new(layout, "1-bit compression").into())
1704                    }
1705                    QueryLayout::FullPrecision => self.fused_query_computer::<AsFull, FullQuery<_>, _>(
1706                        v,
1707                        FullQuery::empty(dim, allocator.clone())?,
1708                        allocator,
1709                        scratch,
1710                    ),
1711                }
1712        };
1713
1714        if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1715            let mut copy = x.to_owned();
1716            self.quantizer.rescale(&mut copy);
1717            finish(&copy, allocator)
1718        } else {
1719            finish(x, allocator)
1720        }
1721    }
1722
1723    fn is_supported(&self, layout: QueryLayout) -> bool {
1724        Self::supports(layout)
1725    }
1726
1727    fn compress(
1728        &self,
1729        x: &[f32],
1730        mut into: OpaqueMut<'_>,
1731        scratch: ScopedAllocator<'_>,
1732    ) -> Result<(), CompressionError> {
1733        let dim = <Self as Quantizer<B>>::dim(self);
1734        let into = DataMut::<1>::from_canonical_back_mut(into.inspect(), dim)
1735            .map_err(CompressionError::not_canonical)?;
1736        self.quantizer
1737            .compress_into_with(x, into, scratch)
1738            .map_err(CompressionError::CompressionError)
1739    }
1740
1741    fn metric(&self) -> SupportedMetric {
1742        self.quantizer.metric()
1743    }
1744
1745    fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1746        let clone = (*self).try_clone()?;
1747        poly!({ Quantizer<B> }, clone, allocator)
1748    }
1749
1750    #[cfg(feature = "flatbuffers")]
1751    fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1752        Impl::<1, A>::serialize(self, allocator)
1753    }
1754}
1755
1756macro_rules! plan {
1757    ($N:literal) => {
1758        impl<A, B> Quantizer<B> for Impl<$N, A>
1759        where
1760            A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
1761            B: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
1762        {
1763            fn nbits(&self) -> usize {
1764                $N
1765            }
1766
1767            fn dim(&self) -> usize {
1768                self.quantizer.output_dim()
1769            }
1770
1771            fn full_dim(&self) -> usize {
1772                self.quantizer.input_dim()
1773            }
1774
1775            fn bytes(&self) -> usize {
1776                DataRef::<$N>::canonical_bytes(<Self as Quantizer<B>>::dim(self))
1777            }
1778
1779            fn distance_computer(
1780                &self,
1781                allocator: B
1782            ) -> Result<DistanceComputer<B>, AllocatorError> {
1783                self.query_computer::<AsData<$N>, _>(allocator)
1784            }
1785
1786            fn distance_computer_ref(&self) -> &dyn DynDistanceComputer {
1787                &*self.distance
1788            }
1789
1790            fn query_computer(
1791                &self,
1792                layout: QueryLayout,
1793                allocator: B,
1794            ) -> Result<DistanceComputer<B>, DistanceComputerError> {
1795                match layout {
1796                    QueryLayout::SameAsData => Ok(self.query_computer::<AsData<$N>, _>(allocator)?)
1797                    ,
1798                    QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout::new(
1799                        layout,
1800                        concat!($N, "-bit compression"),
1801                    ).into()),
1802                    QueryLayout::ScalarQuantized => {
1803                        Ok(self.query_computer::<AsQuery<$N, bits::Dense>, _>(allocator)?)
1804                    },
1805                    QueryLayout::FullPrecision => Ok(self.query_computer::<AsFull, _>(allocator)?),
1806
1807                }
1808            }
1809
1810            fn query_buffer_description(
1811                &self,
1812                layout: QueryLayout
1813            ) -> Result<QueryBufferDescription, UnsupportedQueryLayout>
1814            {
1815                let dim = <Self as Quantizer<B>>::dim(self);
1816                match layout {
1817                    QueryLayout::SameAsData => Ok(QueryBufferDescription::new(
1818                        DataRef::<$N>::canonical_bytes(dim),
1819                        PowerOfTwo::alignment_of::<u8>(),
1820                    )),
1821                    QueryLayout::FourBitTransposed => Err(UnsupportedQueryLayout {
1822                        layout,
1823                        desc: concat!($N, "-bit compression"),
1824                    }),
1825                    QueryLayout::ScalarQuantized => Ok(QueryBufferDescription::new(
1826                        QueryRef::<$N, bits::Dense>::canonical_bytes(dim),
1827                        PowerOfTwo::alignment_of::<u8>(),
1828                    )),
1829                    QueryLayout::FullPrecision => Ok(QueryBufferDescription::new(
1830                        FullQueryRef::canonical_bytes(dim),
1831                        FullQueryRef::canonical_align(),
1832                    )),
1833                }
1834            }
1835
1836            fn compress_query(
1837                &self,
1838                x: &[f32],
1839                layout: QueryLayout,
1840                allow_rescale: bool,
1841                mut buffer: OpaqueMut<'_>,
1842                scratch: ScopedAllocator<'_>,
1843            ) -> Result<(), QueryCompressionError> {
1844                let dim = <Self as Quantizer<B>>::dim(self);
1845                let mut finish = |v: &[f32]| -> Result<(), QueryCompressionError> {
1846                    match layout {
1847                        QueryLayout::SameAsData => self.compress_query(
1848                            v,
1849                            DataMut::<$N>::from_canonical_back_mut(
1850                                &mut buffer,
1851                                dim,
1852                            ).map_err(NotCanonical::new)?,
1853                            scratch,
1854                        ),
1855                        QueryLayout::FourBitTransposed => {
1856                            Err(UnsupportedQueryLayout::new(
1857                                layout,
1858                                concat!($N, "-bit compression"),
1859                            ).into())
1860                        },
1861                        QueryLayout::ScalarQuantized => self.compress_query(
1862                            v,
1863                            QueryMut::<$N, bits::Dense>::from_canonical_back_mut(
1864                                &mut buffer,
1865                                dim,
1866                            ).map_err(NotCanonical::new)?,
1867                            scratch,
1868                        ),
1869                        QueryLayout::FullPrecision => self.compress_query(
1870                            v,
1871                            FullQueryMut::from_canonical_mut(
1872                                &mut buffer,
1873                                dim,
1874                            ).map_err(NotCanonical::new)?,
1875                            scratch,
1876                        ),
1877                    }
1878                };
1879
1880                if allow_rescale && self.quantizer.metric() == SupportedMetric::InnerProduct {
1881                    let mut copy = x.to_owned();
1882                    self.quantizer.rescale(&mut copy);
1883                    finish(&copy)
1884                } else {
1885                    finish(x)
1886                }
1887            }
1888
1889            fn fused_query_computer(
1890                &self,
1891                x: &[f32],
1892                layout: QueryLayout,
1893                allow_rescale: bool,
1894                allocator: B,
1895                scratch: ScopedAllocator<'_>,
1896            ) -> Result<QueryComputer<B>, QueryComputerError>
1897            {
1898                let dim = <Self as Quantizer<B>>::dim(self);
1899                let finish = |v: &[f32]| -> Result<QueryComputer<B>, QueryComputerError> {
1900                    match layout {
1901                        QueryLayout::SameAsData => {
1902                            self.fused_query_computer::<AsData<$N>, Data<$N, _>, B>(
1903                                v,
1904                                Data::new_in(dim, allocator.clone())?,
1905                                allocator,
1906                                scratch,
1907                            )
1908                        },
1909                        QueryLayout::FourBitTransposed => {
1910                            Err(UnsupportedQueryLayout::new(
1911                                layout,
1912                                concat!($N, "-bit compression"),
1913                            ).into())
1914                        },
1915                        QueryLayout::ScalarQuantized => {
1916                            self.fused_query_computer::<AsQuery<$N, bits::Dense>, Query<$N, bits::Dense, _>, B>(
1917                                v,
1918                                Query::new_in(dim, allocator.clone())?,
1919                                allocator,
1920                                scratch,
1921                            )
1922                        },
1923                        QueryLayout::FullPrecision => {
1924                            self.fused_query_computer::<AsFull, FullQuery<_>, B>(
1925                                v,
1926                                FullQuery::empty(dim, allocator.clone())?,
1927                                allocator,
1928                                scratch,
1929                            )
1930                        },
1931                    }
1932                };
1933
1934                let metric = <Self as Quantizer<B>>::metric(self);
1935                if allow_rescale && metric == SupportedMetric::InnerProduct {
1936                    let mut copy = x.to_owned();
1937                    self.quantizer.rescale(&mut copy);
1938                    finish(&copy)
1939                } else {
1940                    finish(x)
1941                }
1942            }
1943
1944            fn is_supported(&self, layout: QueryLayout) -> bool {
1945                Self::supports(layout)
1946            }
1947
1948            fn compress(
1949                &self,
1950                x: &[f32],
1951                mut into: OpaqueMut<'_>,
1952                scratch: ScopedAllocator<'_>,
1953            ) -> Result<(), CompressionError> {
1954                let dim = <Self as Quantizer<B>>::dim(self);
1955                let into = DataMut::<$N>::from_canonical_back_mut(into.inspect(), dim)
1956                    .map_err(CompressionError::not_canonical)?;
1957
1958                self.quantizer.compress_into_with(x, into, scratch)
1959                    .map_err(CompressionError::CompressionError)
1960            }
1961
1962            fn metric(&self) -> SupportedMetric {
1963                self.quantizer.metric()
1964            }
1965
1966            fn try_clone_into(&self, allocator: B) -> Result<Poly<dyn Quantizer<B>, B>, AllocatorError> {
1967                let clone = (&*self).try_clone()?;
1968                poly!({ Quantizer<B> }, clone, allocator)
1969            }
1970
1971            #[cfg(feature = "flatbuffers")]
1972            fn serialize(&self, allocator: B) -> Result<Poly<[u8], B>, AllocatorError> {
1973                Impl::<$N, A>::serialize(self, allocator)
1974            }
1975        }
1976    };
1977    ($N:literal, $($Ns:literal),*) => {
1978        plan!($N);
1979        $(plan!($Ns);)*
1980    }
1981}
1982
1983plan!(2, 4, 8);
1984
1985////////////////
1986// Flatbuffer //
1987////////////////
1988
1989#[cfg(feature = "flatbuffers")]
1990#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
1991#[derive(Debug, Clone, Error)]
1992#[non_exhaustive]
1993pub enum DeserializationError {
1994    #[error("unhandled file identifier in flatbuffer")]
1995    InvalidIdentifier,
1996
1997    #[error("unsupported number of bits ({0})")]
1998    UnsupportedBitWidth(u32),
1999
2000    #[error(transparent)]
2001    InvalidQuantizer(#[from] super::quantizer::DeserializationError),
2002
2003    #[error(transparent)]
2004    InvalidFlatBuffer(#[from] flatbuffers::InvalidFlatbuffer),
2005
2006    #[error(transparent)]
2007    AllocatorError(#[from] AllocatorError),
2008}
2009
2010/// Attempt to deserialize a `spherical::Quantizer` flatbuffer into one of the concrete
2011/// implementations of `Quantizer`.
2012///
2013/// This function guarantees that the returned `Poly` is the first object allocated through
2014/// `alloc`.
2015#[cfg(feature = "flatbuffers")]
2016#[cfg_attr(docsrs, doc(cfg(feature = "flatbuffers")))]
2017pub fn try_deserialize<O, A>(
2018    data: &[u8],
2019    alloc: A,
2020) -> Result<Poly<dyn Quantizer<O>, A>, DeserializationError>
2021where
2022    O: Allocator + Debug + std::panic::UnwindSafe + Send + Sync + 'static,
2023    A: Allocator + Debug + std::panic::RefUnwindSafe + Send + Sync + 'static,
2024{
2025    // An inner impl is used to ensure that the returned `Poly` is allocated before any of
2026    // the allocations needed by the members.
2027    //
2028    // This ensures that if a bump allocator is used, the root object appears first.
2029    fn unpack_bits<'a, const NBITS: usize, O, A>(
2030        proto: fb::spherical::SphericalQuantizer<'_>,
2031        alloc: A,
2032    ) -> Result<Poly<dyn Quantizer<O> + 'a, A>, DeserializationError>
2033    where
2034        O: Allocator + Send + Sync + std::panic::UnwindSafe + 'static,
2035        A: Allocator + Send + Sync + 'a,
2036        Impl<NBITS, A>: Quantizer<O> + Constructible<A>,
2037    {
2038        let imp = match Poly::new_with(
2039            |alloc| Impl::<NBITS, A>::try_deserialize_from(proto, alloc),
2040            alloc,
2041        ) {
2042            Ok(imp) => imp,
2043            Err(CompoundError::Allocator(err)) => {
2044                return Err(err.into());
2045            }
2046            Err(CompoundError::Constructor(err)) => {
2047                return Err(err);
2048            }
2049        };
2050        Ok(poly!({ Quantizer<O> }, imp))
2051    }
2052
2053    // Check that this is one of the known identifiers.
2054    if !fb::spherical::quantizer_buffer_has_identifier(data) {
2055        return Err(DeserializationError::InvalidIdentifier);
2056    }
2057
2058    // Match as much as we can without allocating.
2059    //
2060    // Then, we branch on the number of bits.
2061    let root = fb::spherical::root_as_quantizer(data)?;
2062    let nbits = root.nbits();
2063    let proto = root.quantizer();
2064
2065    match nbits {
2066        1 => unpack_bits::<1, _, _>(proto, alloc),
2067        2 => unpack_bits::<2, _, _>(proto, alloc),
2068        4 => unpack_bits::<4, _, _>(proto, alloc),
2069        8 => unpack_bits::<8, _, _>(proto, alloc),
2070        n => Err(DeserializationError::UnsupportedBitWidth(n)),
2071    }
2072}
2073
2074///////////
2075// Tests //
2076///////////
2077
2078#[cfg(test)]
2079mod tests {
2080    use diskann_utils::views::rowmajor::{self, Matrix};
2081    use rand::{SeedableRng, rngs::StdRng};
2082
2083    use super::*;
2084    use crate::{
2085        algorithms::{TransformKind, transforms::TargetDim},
2086        alloc::{AlignedAllocator, GlobalAllocator, Poly},
2087        num::PowerOfTwo,
2088        spherical::PreScale,
2089    };
2090
2091    ////////////////////
2092    // Test Quantizer //
2093    ////////////////////
2094
2095    fn test_plan_1_bit(plan: &dyn Quantizer) {
2096        assert_eq!(
2097            plan.nbits(),
2098            1,
2099            "this test only applies to 1-bit quantization"
2100        );
2101
2102        // Check Layouts.
2103        for layout in QueryLayout::all() {
2104            match layout {
2105                QueryLayout::SameAsData
2106                | QueryLayout::FourBitTransposed
2107                | QueryLayout::FullPrecision => assert!(
2108                    plan.is_supported(layout),
2109                    "expected {} to be supported",
2110                    layout
2111                ),
2112                QueryLayout::ScalarQuantized => assert!(
2113                    !plan.is_supported(layout),
2114                    "expected {} to not be supported",
2115                    layout
2116                ),
2117            }
2118        }
2119    }
2120
2121    fn test_plan_n_bit(plan: &dyn Quantizer, nbits: usize) {
2122        assert_ne!(nbits, 1, "there is another test for 1-bit quantizers");
2123        assert_eq!(
2124            plan.nbits(),
2125            nbits,
2126            "this test only applies to 1-bit quantization"
2127        );
2128
2129        // Check Layouts.
2130        for layout in QueryLayout::all() {
2131            match layout {
2132                QueryLayout::SameAsData
2133                | QueryLayout::ScalarQuantized
2134                | QueryLayout::FullPrecision => assert!(
2135                    plan.is_supported(layout),
2136                    "expected {} to be supported",
2137                    layout
2138                ),
2139                QueryLayout::FourBitTransposed => assert!(
2140                    !plan.is_supported(layout),
2141                    "expected {} to not be supported",
2142                    layout
2143                ),
2144            }
2145        }
2146    }
2147
2148    #[inline(never)]
2149    fn test_plan(plan: &dyn Quantizer, nbits: usize, dataset: rowmajor::Ref<f32>) {
2150        // Perform the bit-specific test.
2151        if nbits == 1 {
2152            test_plan_1_bit(plan);
2153        } else {
2154            test_plan_n_bit(plan, nbits);
2155        }
2156
2157        // Run bit-width agnostic tests.
2158        assert_eq!(plan.full_dim(), dataset.ncols());
2159
2160        // Use the correct alignment for the base pointers.
2161        let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2162        let mut a = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2163        let mut b = Poly::broadcast(u8::default(), plan.bytes(), alloc).unwrap();
2164        let scoped_global = ScopedAllocator::global();
2165
2166        plan.compress(dataset.row(0), OpaqueMut::new(&mut a), scoped_global)
2167            .unwrap();
2168        plan.compress(dataset.row(1), OpaqueMut::new(&mut b), scoped_global)
2169            .unwrap();
2170
2171        let f = plan.distance_computer(GlobalAllocator).unwrap();
2172        let _: f32 = f
2173            .evaluate_similarity(Opaque::new(&a), Opaque::new(&b))
2174            .unwrap();
2175
2176        let test_errors = |f: &dyn DynDistanceComputer| {
2177            // `a` too short
2178            let err = f
2179                .evaluate(Opaque::new(&a[..a.len() - 1]), Opaque::new(&b))
2180                .unwrap_err();
2181            assert!(matches!(err, DistanceError::QueryReify(_)));
2182
2183            // `a` too long
2184            let err = f
2185                .evaluate(Opaque::new(&vec![0u8; a.len() + 1]), Opaque::new(&b))
2186                .unwrap_err();
2187            assert!(matches!(err, DistanceError::QueryReify(_)));
2188
2189            // `b` too short
2190            let err = f
2191                .evaluate(Opaque::new(&a), Opaque::new(&b[..b.len() - 1]))
2192                .unwrap_err();
2193            assert!(matches!(err, DistanceError::XReify(_)));
2194
2195            // `a` too long
2196            let err = f
2197                .evaluate(Opaque::new(&a), Opaque::new(&vec![0u8; b.len() + 1]))
2198                .unwrap_err();
2199            assert!(matches!(err, DistanceError::XReify(_)));
2200        };
2201
2202        test_errors(&*f.inner);
2203
2204        let f = plan.distance_computer_ref();
2205        let _: f32 = f.evaluate(Opaque::new(&a), Opaque::new(&b)).unwrap();
2206        test_errors(f);
2207
2208        // Test all supported flavors of `QueryComputer`.
2209        for layout in QueryLayout::all() {
2210            if !plan.is_supported(layout) {
2211                let check_message = |msg: &str| {
2212                    assert!(
2213                        msg.contains(&(layout.to_string())),
2214                        "error message ({}) should contain the layout \"{}\"",
2215                        msg,
2216                        layout
2217                    );
2218                    assert!(
2219                        msg.contains(&format!("{}", nbits)),
2220                        "error message ({}) should contain the number of bits \"{}\"",
2221                        msg,
2222                        nbits
2223                    );
2224                };
2225
2226                // Error for query computer
2227                {
2228                    let err = plan
2229                        .fused_query_computer(
2230                            dataset.row(1),
2231                            layout,
2232                            false,
2233                            GlobalAllocator,
2234                            scoped_global,
2235                        )
2236                        .unwrap_err();
2237
2238                    let msg = err.to_string();
2239                    check_message(&msg);
2240                }
2241
2242                // Query buffer
2243                {
2244                    let err = plan.query_buffer_description(layout).unwrap_err();
2245                    let msg = err.to_string();
2246                    check_message(&msg);
2247                }
2248
2249                // Compresss Query Into
2250                {
2251                    let buffer = &mut [];
2252                    let err = plan
2253                        .compress_query(
2254                            dataset.row(1),
2255                            layout,
2256                            true,
2257                            OpaqueMut::new(buffer),
2258                            scoped_global,
2259                        )
2260                        .unwrap_err();
2261                    let msg = err.to_string();
2262                    check_message(&msg);
2263                }
2264
2265                // Standalone Query Computer
2266                {
2267                    let err = plan.query_computer(layout, GlobalAllocator).unwrap_err();
2268                    let msg = err.to_string();
2269                    check_message(&msg);
2270                }
2271
2272                continue;
2273            }
2274
2275            let g = plan
2276                .fused_query_computer(
2277                    dataset.row(1),
2278                    layout,
2279                    false,
2280                    GlobalAllocator,
2281                    scoped_global,
2282                )
2283                .unwrap();
2284            assert_eq!(
2285                g.layout(),
2286                layout,
2287                "the query computer should faithfully preserve the requested layout"
2288            );
2289
2290            let direct: f32 = g.evaluate_similarity(Opaque(&a)).unwrap();
2291
2292            // Check that the fused computer correctly returns errors for invalid inputs.
2293            {
2294                let err = g
2295                    .evaluate_similarity(Opaque::new(&a[..a.len() - 1]))
2296                    .unwrap_err();
2297                assert!(matches!(err, QueryDistanceError::XReify(_)));
2298
2299                let err = g
2300                    .evaluate_similarity(Opaque::new(&vec![0u8; a.len() + 1]))
2301                    .unwrap_err();
2302                assert!(matches!(err, QueryDistanceError::XReify(_)));
2303            }
2304
2305            let sizes = plan.query_buffer_description(layout).unwrap();
2306            let mut buf =
2307                Poly::broadcast(0u8, sizes.bytes(), AlignedAllocator::new(sizes.align())).unwrap();
2308
2309            plan.compress_query(
2310                dataset.row(1),
2311                layout,
2312                false,
2313                OpaqueMut::new(&mut buf),
2314                scoped_global,
2315            )
2316            .unwrap();
2317
2318            let standalone = plan.query_computer(layout, GlobalAllocator).unwrap();
2319
2320            assert_eq!(
2321                standalone.layout(),
2322                layout,
2323                "the standalone computer did not preserve the requested layout",
2324            );
2325
2326            let indirect: f32 = standalone
2327                .evaluate_similarity(Opaque(&buf), Opaque(&a))
2328                .unwrap();
2329
2330            assert_eq!(
2331                direct, indirect,
2332                "the two different query computation APIs did not return the same result"
2333            );
2334
2335            // Errors
2336            let too_small = &dataset.row(0)[..dataset.ncols() - 1];
2337            assert!(
2338                plan.fused_query_computer(too_small, layout, false, GlobalAllocator, scoped_global)
2339                    .is_err()
2340            );
2341        }
2342
2343        // Errors
2344        {
2345            let mut too_small = vec![u8::default(); plan.bytes() - 1];
2346            assert!(
2347                plan.compress(dataset.row(0), OpaqueMut(&mut too_small), scoped_global)
2348                    .is_err()
2349            );
2350
2351            let mut too_big = vec![u8::default(); plan.bytes() + 1];
2352            assert!(
2353                plan.compress(dataset.row(0), OpaqueMut(&mut too_big), scoped_global)
2354                    .is_err()
2355            );
2356
2357            let mut just_right = vec![u8::default(); plan.bytes()];
2358            assert!(
2359                plan.compress(
2360                    &dataset.row(0)[..dataset.ncols() - 1],
2361                    OpaqueMut(&mut just_right),
2362                    scoped_global
2363                )
2364                .is_err()
2365            );
2366        }
2367    }
2368
2369    fn make_impl<const NBITS: usize>(metric: SupportedMetric) -> (Impl<NBITS>, rowmajor::Owned<f32>)
2370    where
2371        Impl<NBITS>: Constructible,
2372    {
2373        let data = test_dataset();
2374        let mut rng = StdRng::seed_from_u64(0x7d535118722ff197);
2375
2376        let quantizer = SphericalQuantizer::train(
2377            data.as_view(),
2378            TransformKind::PaddingHadamard {
2379                target_dim: TargetDim::Natural,
2380            },
2381            metric,
2382            PreScale::None,
2383            &mut rng,
2384            GlobalAllocator,
2385        )
2386        .unwrap();
2387
2388        (Impl::new(quantizer).unwrap(), data)
2389    }
2390
2391    #[test]
2392    fn test_plan_1bit_l2() {
2393        let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2394        test_plan(&plan, 1, data.as_view());
2395    }
2396
2397    #[test]
2398    fn test_plan_1bit_ip() {
2399        let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2400        test_plan(&plan, 1, data.as_view());
2401    }
2402
2403    #[test]
2404    fn test_plan_1bit_cosine() {
2405        let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2406        test_plan(&plan, 1, data.as_view());
2407    }
2408
2409    #[test]
2410    fn test_plan_2bit_l2() {
2411        let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2412        test_plan(&plan, 2, data.as_view());
2413    }
2414
2415    #[test]
2416    fn test_plan_2bit_ip() {
2417        let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2418        test_plan(&plan, 2, data.as_view());
2419    }
2420
2421    #[test]
2422    fn test_plan_2bit_cosine() {
2423        let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2424        test_plan(&plan, 2, data.as_view());
2425    }
2426
2427    #[test]
2428    fn test_plan_4bit_l2() {
2429        let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2430        test_plan(&plan, 4, data.as_view());
2431    }
2432
2433    #[test]
2434    fn test_plan_4bit_ip() {
2435        let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2436        test_plan(&plan, 4, data.as_view());
2437    }
2438
2439    #[test]
2440    fn test_plan_4bit_cosine() {
2441        let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2442        test_plan(&plan, 4, data.as_view());
2443    }
2444
2445    #[test]
2446    fn test_plan_8bit_l2() {
2447        let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2448        test_plan(&plan, 8, data.as_view());
2449    }
2450
2451    #[test]
2452    fn test_plan_8bit_ip() {
2453        let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2454        test_plan(&plan, 8, data.as_view());
2455    }
2456
2457    #[test]
2458    fn test_plan_8bit_cosine() {
2459        let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2460        test_plan(&plan, 8, data.as_view());
2461    }
2462
2463    fn test_dataset() -> rowmajor::Owned<f32> {
2464        let data = vec![
2465            0.28657,
2466            -0.0318168,
2467            0.0666847,
2468            0.0329265,
2469            -0.00829283,
2470            0.168735,
2471            -0.000846311,
2472            -0.360779, // row 0
2473            -0.0968938,
2474            0.161921,
2475            -0.0979579,
2476            0.102228,
2477            -0.259928,
2478            -0.139634,
2479            0.165384,
2480            -0.293443, // row 1
2481            0.130205,
2482            0.265737,
2483            0.401816,
2484            -0.407552,
2485            0.13012,
2486            -0.0475244,
2487            0.511723,
2488            -0.4372, // row 2
2489            -0.0979126,
2490            0.135861,
2491            -0.0154144,
2492            -0.14047,
2493            -0.0250029,
2494            -0.190279,
2495            0.407283,
2496            -0.389184, // row 3
2497            -0.264153,
2498            0.0696822,
2499            -0.145585,
2500            0.370284,
2501            0.186825,
2502            -0.140736,
2503            0.274703,
2504            -0.334563, // row 4
2505            0.247613,
2506            0.513165,
2507            -0.0845867,
2508            0.0532264,
2509            -0.00480601,
2510            -0.122408,
2511            0.47227,
2512            -0.268301, // row 5
2513            0.103198,
2514            0.30756,
2515            -0.316293,
2516            -0.0686877,
2517            -0.330729,
2518            -0.461997,
2519            0.550857,
2520            -0.240851, // row 6
2521            0.128258,
2522            0.786291,
2523            -0.0268103,
2524            0.111763,
2525            -0.308962,
2526            -0.17407,
2527            0.437154,
2528            -0.159879, // row 7
2529            0.00374063,
2530            0.490301,
2531            0.0327826,
2532            -0.0340962,
2533            -0.118605,
2534            0.163879,
2535            0.2737,
2536            -0.299942, // row 8
2537            -0.284077,
2538            0.249377,
2539            -0.0307734,
2540            -0.0661631,
2541            0.233854,
2542            0.427987,
2543            0.614132,
2544            -0.288649, // row 9
2545            -0.109492,
2546            0.203939,
2547            -0.73956,
2548            -0.130748,
2549            0.22072,
2550            0.0647836,
2551            0.328726,
2552            -0.374602, // row 10
2553            -0.223114,
2554            0.0243489,
2555            0.109195,
2556            -0.416914,
2557            0.0201052,
2558            -0.0190542,
2559            0.947078,
2560            -0.333229, // row 11
2561            -0.165869,
2562            -0.00296729,
2563            -0.414378,
2564            0.231321,
2565            0.205365,
2566            0.161761,
2567            0.148608,
2568            -0.395063, // row 12
2569            -0.0498255,
2570            0.193279,
2571            -0.110946,
2572            -0.181174,
2573            -0.274578,
2574            -0.227511,
2575            0.190208,
2576            -0.256174, // row 13
2577            -0.188106,
2578            -0.0292958,
2579            0.0930939,
2580            0.0558456,
2581            0.257437,
2582            0.685481,
2583            0.307922,
2584            -0.320006, // row 14
2585            0.250035,
2586            0.275942,
2587            -0.0856306,
2588            -0.352027,
2589            -0.103509,
2590            -0.00890859,
2591            0.276121,
2592            -0.324718, // row 15
2593        ];
2594
2595        rowmajor::Owned::try_from_data(data.into(), 16, 8).unwrap()
2596    }
2597
2598    #[cfg(feature = "flatbuffers")]
2599    mod serialization {
2600        use std::sync::{
2601            Arc,
2602            atomic::{AtomicBool, Ordering},
2603        };
2604
2605        use super::*;
2606        use crate::alloc::{BumpAllocator, GlobalAllocator};
2607
2608        fn test_deserialization_inner(
2609            quantizer: &dyn Quantizer,
2610            deserialized: &dyn Quantizer,
2611            nbits: usize,
2612            dataset: rowmajor::Ref<'_, f32>,
2613        ) {
2614            let scoped_global = ScopedAllocator::global();
2615
2616            assert_eq!(quantizer.nbits(), nbits);
2617            assert_eq!(deserialized.nbits(), nbits);
2618            assert_eq!(deserialized.bytes(), quantizer.bytes());
2619            assert_eq!(deserialized.dim(), quantizer.dim());
2620            assert_eq!(deserialized.full_dim(), quantizer.full_dim());
2621            assert_eq!(deserialized.metric(), quantizer.metric());
2622
2623            for layout in QueryLayout::all() {
2624                assert_eq!(
2625                    deserialized.is_supported(layout),
2626                    quantizer.is_supported(layout)
2627                );
2628            }
2629
2630            // Use the correct alignment for the base pointers.
2631            let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
2632            {
2633                let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2634                let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2635
2636                for row in dataset.rows() {
2637                    quantizer
2638                        .compress(row, OpaqueMut::new(&mut a), scoped_global)
2639                        .unwrap();
2640                    deserialized
2641                        .compress(row, OpaqueMut::new(&mut b), scoped_global)
2642                        .unwrap();
2643
2644                    // Compressed representation should be identical.
2645                    assert_eq!(a, b);
2646                }
2647            }
2648
2649            // Distance Computer
2650            {
2651                let mut a0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2652                let mut a1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2653                let mut b0 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2654                let mut b1 = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2655
2656                let q_computer = quantizer.distance_computer(GlobalAllocator).unwrap();
2657                let q_computer_ref = quantizer.distance_computer_ref();
2658                let d_computer = deserialized.distance_computer(GlobalAllocator).unwrap();
2659                let d_computer_ref = deserialized.distance_computer_ref();
2660
2661                for r0 in dataset.rows() {
2662                    quantizer
2663                        .compress(r0, OpaqueMut::new(&mut a0), scoped_global)
2664                        .unwrap();
2665                    deserialized
2666                        .compress(r0, OpaqueMut::new(&mut b0), scoped_global)
2667                        .unwrap();
2668                    for r1 in dataset.rows() {
2669                        quantizer
2670                            .compress(r1, OpaqueMut::new(&mut a1), scoped_global)
2671                            .unwrap();
2672                        deserialized
2673                            .compress(r1, OpaqueMut::new(&mut b1), scoped_global)
2674                            .unwrap();
2675
2676                        let a0 = Opaque::new(&a0);
2677                        let a1 = Opaque::new(&a1);
2678
2679                        let q_computer_dist = q_computer.evaluate_similarity(a0, a1).unwrap();
2680                        let d_computer_dist = d_computer.evaluate_similarity(a0, a1).unwrap();
2681
2682                        assert_eq!(q_computer_dist, d_computer_dist);
2683
2684                        let q_computer_ref_dist = q_computer_ref.evaluate(a0, a1).unwrap();
2685
2686                        assert_eq!(q_computer_dist, q_computer_ref_dist);
2687
2688                        let d_computer_ref_dist = d_computer_ref.evaluate(a0, a1).unwrap();
2689                        assert_eq!(d_computer_dist, d_computer_ref_dist);
2690                    }
2691                }
2692            }
2693
2694            // Query Computer
2695            {
2696                let mut a = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2697                let mut b = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
2698
2699                for layout in QueryLayout::all() {
2700                    if !quantizer.is_supported(layout) {
2701                        continue;
2702                    }
2703
2704                    for r in dataset.rows() {
2705                        let q_computer = quantizer
2706                            .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2707                            .unwrap();
2708                        let d_computer = deserialized
2709                            .fused_query_computer(r, layout, false, GlobalAllocator, scoped_global)
2710                            .unwrap();
2711
2712                        for u in dataset.rows() {
2713                            quantizer
2714                                .compress(u, OpaqueMut::new(&mut a), scoped_global)
2715                                .unwrap();
2716                            deserialized
2717                                .compress(u, OpaqueMut::new(&mut b), scoped_global)
2718                                .unwrap();
2719
2720                            assert_eq!(
2721                                q_computer.evaluate_similarity(Opaque::new(&a)).unwrap(),
2722                                d_computer.evaluate_similarity(Opaque::new(&b)).unwrap(),
2723                            );
2724                        }
2725                    }
2726                }
2727            }
2728        }
2729
2730        #[inline(never)]
2731        fn test_plan_serialization(
2732            quantizer: &dyn Quantizer,
2733            nbits: usize,
2734            dataset: rowmajor::Ref<f32>,
2735        ) {
2736            let global = GlobalAllocator;
2737
2738            // Run bit-width agnostic tests.
2739            assert_eq!(quantizer.full_dim(), dataset.ncols());
2740
2741            let serialized = quantizer.serialize(global).unwrap();
2742            let deserialized = try_deserialize::<GlobalAllocator, _>(&serialized, global).unwrap();
2743
2744            test_deserialization_inner(quantizer, &*deserialized, nbits, dataset);
2745
2746            // Go through the direct deserialization interface.
2747            match nbits {
2748                1 => {
2749                    test_deserialization_inner(
2750                        quantizer,
2751                        &Impl::<1, _>::try_deserialize(&serialized, global).unwrap(),
2752                        nbits,
2753                        dataset,
2754                    );
2755
2756                    // Verify that we can't reload with a different bit-width.
2757                    assert!(matches!(
2758                        Impl::<2, _>::try_deserialize(&serialized, global),
2759                        Err(DeserializationError::UnsupportedBitWidth(1)),
2760                    ));
2761                }
2762                2 => {
2763                    test_deserialization_inner(
2764                        quantizer,
2765                        &Impl::<2, _>::try_deserialize(&serialized, global).unwrap(),
2766                        nbits,
2767                        dataset,
2768                    );
2769
2770                    // Verify that we can't reload with a different bit-width.
2771                    assert!(matches!(
2772                        Impl::<1, _>::try_deserialize(&serialized, global),
2773                        Err(DeserializationError::UnsupportedBitWidth(2)),
2774                    ));
2775                }
2776                4 => {
2777                    test_deserialization_inner(
2778                        quantizer,
2779                        &Impl::<4, _>::try_deserialize(&serialized, global).unwrap(),
2780                        nbits,
2781                        dataset,
2782                    );
2783
2784                    // Verify that we can't reload with a different bit-width.
2785                    assert!(matches!(
2786                        Impl::<8, _>::try_deserialize(&serialized, global),
2787                        Err(DeserializationError::UnsupportedBitWidth(4)),
2788                    ));
2789                }
2790                8 => {
2791                    test_deserialization_inner(
2792                        quantizer,
2793                        &Impl::<8, _>::try_deserialize(&serialized, global).unwrap(),
2794                        nbits,
2795                        dataset,
2796                    );
2797
2798                    // Verify that we can't reload with a different bit-width.
2799                    assert!(matches!(
2800                        Impl::<4, _>::try_deserialize(&serialized, global),
2801                        Err(DeserializationError::UnsupportedBitWidth(8)),
2802                    ));
2803                }
2804                _ => panic!("bit width {} is not supported", nbits),
2805            }
2806        }
2807
2808        // An allocator that succeeds on its first allocation but fails on its second.
2809        #[derive(Debug, Clone)]
2810        struct FlakyAllocator {
2811            have_allocated: Arc<AtomicBool>,
2812        }
2813
2814        impl FlakyAllocator {
2815            fn new(have_allocated: Arc<AtomicBool>) -> Self {
2816                Self { have_allocated }
2817            }
2818        }
2819
2820        // SAFETY: This is a wrapper around GlobalAllocator that only succeeds once.
2821        unsafe impl AllocatorCore for FlakyAllocator {
2822            fn allocate(
2823                &self,
2824                layout: std::alloc::Layout,
2825            ) -> Result<std::ptr::NonNull<[u8]>, AllocatorError> {
2826                if self.have_allocated.swap(true, Ordering::Relaxed) {
2827                    Err(AllocatorError)
2828                } else {
2829                    GlobalAllocator.allocate(layout)
2830                }
2831            }
2832
2833            unsafe fn deallocate(&self, ptr: std::ptr::NonNull<[u8]>, layout: std::alloc::Layout) {
2834                // SAFETY: Inherited from caller.
2835                unsafe { GlobalAllocator.deallocate(ptr, layout) }
2836            }
2837        }
2838
2839        fn test_plan_panic_boundary<const NBITS: usize>(v: &Impl<NBITS>)
2840        where
2841            Impl<NBITS>: Quantizer,
2842        {
2843            // Ensure that we do not panic if reallocation returns an error.
2844            let have_allocated = Arc::new(AtomicBool::new(false));
2845            let _: AllocatorError = v
2846                .serialize(FlakyAllocator::new(have_allocated.clone()))
2847                .unwrap_err();
2848            assert!(have_allocated.load(Ordering::Relaxed));
2849        }
2850
2851        #[test]
2852        fn test_plan_1bit_l2() {
2853            let (plan, data) = make_impl::<1>(SupportedMetric::SquaredL2);
2854            test_plan_panic_boundary(&plan);
2855            test_plan_serialization(&plan, 1, data.as_view());
2856        }
2857
2858        #[test]
2859        fn test_plan_1bit_ip() {
2860            let (plan, data) = make_impl::<1>(SupportedMetric::InnerProduct);
2861            test_plan_panic_boundary(&plan);
2862            test_plan_serialization(&plan, 1, data.as_view());
2863        }
2864
2865        #[test]
2866        fn test_plan_2bit_l2() {
2867            let (plan, data) = make_impl::<2>(SupportedMetric::SquaredL2);
2868            test_plan_panic_boundary(&plan);
2869            test_plan_serialization(&plan, 2, data.as_view());
2870        }
2871
2872        #[test]
2873        fn test_plan_2bit_ip() {
2874            let (plan, data) = make_impl::<2>(SupportedMetric::InnerProduct);
2875            test_plan_panic_boundary(&plan);
2876            test_plan_serialization(&plan, 2, data.as_view());
2877        }
2878
2879        #[test]
2880        fn test_plan_4bit_l2() {
2881            let (plan, data) = make_impl::<4>(SupportedMetric::SquaredL2);
2882            test_plan_panic_boundary(&plan);
2883            test_plan_serialization(&plan, 4, data.as_view());
2884        }
2885
2886        #[test]
2887        fn test_plan_4bit_ip() {
2888            let (plan, data) = make_impl::<4>(SupportedMetric::InnerProduct);
2889            test_plan_panic_boundary(&plan);
2890            test_plan_serialization(&plan, 4, data.as_view());
2891        }
2892
2893        #[test]
2894        fn test_plan_8bit_l2() {
2895            let (plan, data) = make_impl::<8>(SupportedMetric::SquaredL2);
2896            test_plan_panic_boundary(&plan);
2897            test_plan_serialization(&plan, 8, data.as_view());
2898        }
2899
2900        #[test]
2901        fn test_plan_8bit_ip() {
2902            let (plan, data) = make_impl::<8>(SupportedMetric::InnerProduct);
2903            test_plan_panic_boundary(&plan);
2904            test_plan_serialization(&plan, 8, data.as_view());
2905        }
2906
2907        #[test]
2908        fn test_plan_1bit_cosine() {
2909            let (plan, data) = make_impl::<1>(SupportedMetric::Cosine);
2910            test_plan_panic_boundary(&plan);
2911            test_plan_serialization(&plan, 1, data.as_view());
2912        }
2913
2914        #[test]
2915        fn test_plan_2bit_cosine() {
2916            let (plan, data) = make_impl::<2>(SupportedMetric::Cosine);
2917            test_plan_panic_boundary(&plan);
2918            test_plan_serialization(&plan, 2, data.as_view());
2919        }
2920
2921        #[test]
2922        fn test_plan_4bit_cosine() {
2923            let (plan, data) = make_impl::<4>(SupportedMetric::Cosine);
2924            test_plan_panic_boundary(&plan);
2925            test_plan_serialization(&plan, 4, data.as_view());
2926        }
2927
2928        #[test]
2929        fn test_plan_8bit_cosine() {
2930            let (plan, data) = make_impl::<8>(SupportedMetric::Cosine);
2931            test_plan_panic_boundary(&plan);
2932            test_plan_serialization(&plan, 8, data.as_view());
2933        }
2934
2935        #[test]
2936        fn test_allocation_order() {
2937            let (plan, _) = make_impl::<1>(SupportedMetric::SquaredL2);
2938            let buf = plan.serialize(GlobalAllocator).unwrap();
2939
2940            let allocator = BumpAllocator::new(8192, PowerOfTwo::new(64).unwrap()).unwrap();
2941            let deserialized =
2942                try_deserialize::<GlobalAllocator, _>(&buf, allocator.clone()).unwrap();
2943            assert_eq!(
2944                Poly::as_ptr(&deserialized).cast::<u8>(),
2945                allocator.as_ptr(),
2946                "expected the returned box to be allocated first",
2947            );
2948        }
2949    }
2950
2951    /// Backwards-compatibility baseline tests.
2952    ///
2953    /// These tests persist serialized quantizers, compressed vectors, and pre-computed
2954    /// distances as JSON golden files. On subsequent runs, the stored serialized bytes
2955    /// are deserialized and the resulting quantizer is verified to produce identical
2956    /// compression and distance results.
2957    ///
2958    /// To generate or regenerate baseline files:
2959    /// ```sh
2960    /// DISKANN_TEST=overwrite cargo test -p diskann-quantization --features flatbuffers
2961    /// ```
2962    #[cfg(feature = "flatbuffers")]
2963    mod compatibility {
2964        use std::path::PathBuf;
2965
2966        use serde::{Deserialize, Serialize};
2967
2968        use super::*;
2969
2970        use crate::test_util::Check;
2971
2972        const TRAINING_SEED: u64 = 0x7d535118722ff197;
2973
2974        ///////////////////////
2975        // Serde Mirror Types //
2976        ///////////////////////
2977
2978        #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
2979        #[serde(rename_all = "snake_case")]
2980        enum Metric {
2981            SquaredL2,
2982            InnerProduct,
2983            Cosine,
2984        }
2985
2986        impl std::fmt::Display for Metric {
2987            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2988                let s = match self {
2989                    Self::SquaredL2 => "squared_l2",
2990                    Self::InnerProduct => "inner_product",
2991                    Self::Cosine => "cosine",
2992                };
2993                write!(f, "{}", s)
2994            }
2995        }
2996
2997        impl From<SupportedMetric> for Metric {
2998            fn from(m: SupportedMetric) -> Self {
2999                match m {
3000                    SupportedMetric::SquaredL2 => Self::SquaredL2,
3001                    SupportedMetric::InnerProduct => Self::InnerProduct,
3002                    SupportedMetric::Cosine => Self::Cosine,
3003                }
3004            }
3005        }
3006
3007        impl From<Metric> for SupportedMetric {
3008            fn from(m: Metric) -> Self {
3009                match m {
3010                    Metric::SquaredL2 => Self::SquaredL2,
3011                    Metric::InnerProduct => Self::InnerProduct,
3012                    Metric::Cosine => Self::Cosine,
3013                }
3014            }
3015        }
3016
3017        #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3018        #[serde(rename_all = "snake_case")]
3019        enum DataTransform {
3020            PaddingHadamard,
3021            DoubleHadamard,
3022            Null,
3023        }
3024
3025        impl std::fmt::Display for DataTransform {
3026            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3027                let s = match self {
3028                    Self::PaddingHadamard => "padding_hadamard",
3029                    Self::DoubleHadamard => "double_hadamard",
3030                    Self::Null => "null",
3031                };
3032                write!(f, "{}", s)
3033            }
3034        }
3035
3036        impl DataTransform {
3037            fn to_transform_kind(self) -> TransformKind {
3038                match self {
3039                    Self::PaddingHadamard => TransformKind::PaddingHadamard {
3040                        target_dim: TargetDim::Natural,
3041                    },
3042                    Self::DoubleHadamard => TransformKind::DoubleHadamard {
3043                        target_dim: TargetDim::Natural,
3044                    },
3045                    Self::Null => TransformKind::Null,
3046                }
3047            }
3048        }
3049
3050        #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
3051        #[serde(rename_all = "snake_case")]
3052        enum Layout {
3053            SameAsData,
3054            FourBitTransposed,
3055            ScalarQuantized,
3056            FullPrecision,
3057        }
3058
3059        impl From<QueryLayout> for Layout {
3060            fn from(l: QueryLayout) -> Self {
3061                match l {
3062                    QueryLayout::SameAsData => Self::SameAsData,
3063                    QueryLayout::FourBitTransposed => Self::FourBitTransposed,
3064                    QueryLayout::ScalarQuantized => Self::ScalarQuantized,
3065                    QueryLayout::FullPrecision => Self::FullPrecision,
3066                }
3067            }
3068        }
3069
3070        impl From<Layout> for QueryLayout {
3071            fn from(l: Layout) -> Self {
3072                match l {
3073                    Layout::SameAsData => Self::SameAsData,
3074                    Layout::FourBitTransposed => Self::FourBitTransposed,
3075                    Layout::ScalarQuantized => Self::ScalarQuantized,
3076                    Layout::FullPrecision => Self::FullPrecision,
3077                }
3078            }
3079        }
3080
3081        #[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
3082        #[serde(rename_all = "snake_case")]
3083        enum ScaleConfig {
3084            #[default]
3085            None,
3086            ReciprocalMeanNorm,
3087        }
3088
3089        impl ScaleConfig {
3090            fn try_as_str(self) -> Option<&'static str> {
3091                match self {
3092                    Self::None => Option::None,
3093                    Self::ReciprocalMeanNorm => Some("rmn"),
3094                }
3095            }
3096
3097            fn to_prescale(self) -> PreScale {
3098                match self {
3099                    Self::None => PreScale::None,
3100                    Self::ReciprocalMeanNorm => PreScale::ReciprocalMeanNorm,
3101                }
3102            }
3103        }
3104
3105        //////////////
3106        // Baseline //
3107        //////////////
3108
3109        /// A complete snapshot of a spherical quantizer's serialized form and expected
3110        /// behavior on the local test dataset.
3111        #[derive(Debug, Clone, Serialize, Deserialize)]
3112        struct Baseline {
3113            /// Number of bits per quantized dimension.
3114            nbits: usize,
3115            /// The distance metric.
3116            metric: Metric,
3117            /// The data transform used for pre-conditioning.
3118            transform: DataTransform,
3119            /// Pre-scaling configuration.
3120            pre_scale: ScaleConfig,
3121            /// Effective dimensionality of compressed vectors.
3122            dim: usize,
3123            /// Dimensionality of full-precision input vectors.
3124            full_dim: usize,
3125            /// Seed used to train the quantizer.
3126            training_seed: u64,
3127            /// The raw flatbuffer bytes from `Quantizer::serialize`.
3128            serialized_quantizer: Vec<u8>,
3129            /// One compressed vector per row of the test dataset.
3130            compressed_vectors: Vec<Vec<u8>>,
3131            /// Upper-triangle pairwise data distances:
3132            /// (0,0), (0,1), …, (0,N-1), (1,1), (1,2), …
3133            data_distances: Vec<f32>,
3134            /// Query-to-data distances for each supported query layout.
3135            query_distances: Vec<LayoutDistances>,
3136            /// Query-to-data distances with `allow_rescale=true`, present only
3137            /// for `InnerProduct` (where rescaling changes the result).
3138            #[serde(default, skip_serializing_if = "Option::is_none")]
3139            rescaled_query_distances: Option<Vec<LayoutDistances>>,
3140        }
3141
3142        /// Distances between every (query, data) pair for a single query layout.
3143        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
3144        struct LayoutDistances {
3145            layout: Layout,
3146            /// `distances[query_index][data_index]`
3147            distances: Vec<Vec<f32>>,
3148        }
3149
3150        /////////////
3151        // Helpers //
3152        /////////////
3153
3154        fn compress_dataset(
3155            quantizer: &dyn Quantizer,
3156            dataset: rowmajor::Ref<f32>,
3157        ) -> Vec<Vec<u8>> {
3158            let scoped_global = ScopedAllocator::global();
3159            let alloc = AlignedAllocator::new(PowerOfTwo::new(4).unwrap());
3160            dataset
3161                .rows()
3162                .map(|row| {
3163                    let mut buf = Poly::broadcast(u8::default(), quantizer.bytes(), alloc).unwrap();
3164                    quantizer
3165                        .compress(row, OpaqueMut::new(&mut buf), scoped_global)
3166                        .unwrap();
3167                    buf.to_vec()
3168                })
3169                .collect()
3170        }
3171
3172        fn compute_layout_distances(
3173            quantizer: &dyn Quantizer,
3174            dataset: rowmajor::Ref<f32>,
3175            compressed: &[Vec<u8>],
3176            allow_rescale: bool,
3177        ) -> Vec<LayoutDistances> {
3178            let scoped_global = ScopedAllocator::global();
3179            QueryLayout::all()
3180                .into_iter()
3181                .filter(|&layout| quantizer.is_supported(layout))
3182                .map(|layout| {
3183                    let distances = dataset
3184                        .rows()
3185                        .map(|query_row| {
3186                            let computer = quantizer
3187                                .fused_query_computer(
3188                                    query_row,
3189                                    layout,
3190                                    allow_rescale,
3191                                    GlobalAllocator,
3192                                    scoped_global,
3193                                )
3194                                .unwrap();
3195                            compressed
3196                                .iter()
3197                                .map(|d| computer.evaluate_similarity(Opaque::new(d)).unwrap())
3198                                .collect()
3199                        })
3200                        .collect();
3201                    LayoutDistances {
3202                        layout: layout.into(),
3203                        distances,
3204                    }
3205                })
3206                .collect()
3207        }
3208
3209        const TOLERANCE: Check = Check::absrel(1e-6, 0.0);
3210
3211        /// Assert that the deserialized quantizer produces the expected
3212        /// per-layout query distances.
3213        fn assert_layout_distances(
3214            quantizer: &dyn Quantizer,
3215            dataset: rowmajor::Ref<f32>,
3216            compressed: &[Vec<u8>],
3217            expected: &[LayoutDistances],
3218            allow_rescale: bool,
3219            label: &str,
3220        ) {
3221            let scoped_global = ScopedAllocator::global();
3222            for layout_distances in expected {
3223                let layout: QueryLayout = layout_distances.layout.into();
3224                assert!(
3225                    quantizer.is_supported(layout),
3226                    "{label}: unsupported layout {layout:?}"
3227                );
3228
3229                for (qi, (query_row, expected_distances)) in
3230                    std::iter::zip(dataset.rows(), layout_distances.distances.iter()).enumerate()
3231                {
3232                    let computer = quantizer
3233                        .fused_query_computer(
3234                            query_row,
3235                            layout,
3236                            allow_rescale,
3237                            GlobalAllocator,
3238                            scoped_global,
3239                        )
3240                        .unwrap();
3241
3242                    for (di, (compressed, expected)) in
3243                        std::iter::zip(compressed.iter(), expected_distances.iter()).enumerate()
3244                    {
3245                        let distance = computer
3246                            .evaluate_similarity(Opaque::new(compressed))
3247                            .unwrap();
3248
3249                        if let Err(err) = TOLERANCE.check(distance, *expected) {
3250                            panic!("{label}: layout = {layout:?}, query={qi}, data={di}\n{err}")
3251                        }
3252                    }
3253                }
3254            }
3255        }
3256
3257        ////////////////
3258        // Generation //
3259        ////////////////
3260
3261        fn generate_baseline(
3262            quantizer: &dyn Quantizer,
3263            transform: DataTransform,
3264            pre_scale: ScaleConfig,
3265            dataset: rowmajor::Ref<f32>,
3266        ) -> Baseline {
3267            let compressed_vectors = compress_dataset(quantizer, dataset);
3268
3269            // Pairwise data distances (upper triangle including diagonal).
3270            let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3271            let mut data_distances = Vec::new();
3272            for (i, a) in compressed_vectors.iter().enumerate() {
3273                for b in compressed_vectors.iter().skip(i) {
3274                    data_distances.push(
3275                        f.evaluate_similarity(Opaque::new(a), Opaque::new(b))
3276                            .unwrap(),
3277                    );
3278                }
3279            }
3280
3281            let query_distances =
3282                compute_layout_distances(quantizer, dataset, &compressed_vectors, false);
3283
3284            let is_ip = quantizer.metric() == SupportedMetric::InnerProduct;
3285            let rescaled = compute_layout_distances(quantizer, dataset, &compressed_vectors, true);
3286
3287            if !is_ip {
3288                // For non-IP metrics, allow_rescale must be a no-op.
3289                assert_eq!(
3290                    query_distances,
3291                    rescaled,
3292                    "allow_rescale should not affect {:?} distances",
3293                    quantizer.metric(),
3294                );
3295            }
3296
3297            let rescaled_query_distances = if is_ip { Some(rescaled) } else { None };
3298
3299            let serialized = quantizer.serialize(GlobalAllocator).unwrap();
3300
3301            Baseline {
3302                nbits: quantizer.nbits(),
3303                metric: quantizer.metric().into(),
3304                transform,
3305                pre_scale,
3306                dim: quantizer.dim(),
3307                full_dim: quantizer.full_dim(),
3308                training_seed: TRAINING_SEED,
3309                serialized_quantizer: serialized.to_vec(),
3310                compressed_vectors,
3311                data_distances,
3312                query_distances,
3313                rescaled_query_distances,
3314            }
3315        }
3316
3317        //////////////////
3318        // Save / Load  //
3319        //////////////////
3320
3321        fn baseline_path(
3322            nbits: usize,
3323            metric: Metric,
3324            transform: DataTransform,
3325            pre_scale: ScaleConfig,
3326        ) -> PathBuf {
3327            let manifest_dir = env!("CARGO_MANIFEST_DIR");
3328            let mut name = format!("{}bit_{}_{}", nbits, metric, transform);
3329            if let Some(mangled) = pre_scale.try_as_str() {
3330                name.push('_');
3331                name.push_str(mangled);
3332            }
3333            name.push_str(".json");
3334            PathBuf::from(manifest_dir)
3335                .join("tests")
3336                .join("generated")
3337                .join("spherical")
3338                .join(name)
3339        }
3340
3341        fn should_overwrite() -> bool {
3342            std::env::var("DISKANN_TEST")
3343                .map(|v| v == "overwrite")
3344                .unwrap_or(false)
3345        }
3346
3347        fn save_baseline(baseline: &Baseline) {
3348            let path = baseline_path(
3349                baseline.nbits,
3350                baseline.metric,
3351                baseline.transform,
3352                baseline.pre_scale,
3353            );
3354            if let Some(parent) = path.parent() {
3355                std::fs::create_dir_all(parent).unwrap();
3356            }
3357            let json = serde_json::to_string_pretty(baseline).unwrap();
3358            std::fs::write(&path, json).unwrap();
3359        }
3360
3361        fn load_baseline(
3362            nbits: usize,
3363            metric: Metric,
3364            transform: DataTransform,
3365            pre_scale: ScaleConfig,
3366        ) -> Baseline {
3367            let path = baseline_path(nbits, metric, transform, pre_scale);
3368            let json = std::fs::read_to_string(&path).unwrap_or_else(|e| {
3369                panic!(
3370                    "Failed to load baseline from {}: {e}\n\
3371                     Run with DISKANN_TEST=overwrite to generate baseline files.",
3372                    path.display()
3373                );
3374            });
3375            serde_json::from_str(&json).unwrap()
3376        }
3377
3378        //////////////
3379        // Checking //
3380        //////////////
3381
3382        fn check_baseline(
3383            baseline: &Baseline,
3384            dataset: rowmajor::Ref<f32>,
3385            expected_transform: DataTransform,
3386            expected_pre_scale: ScaleConfig,
3387        ) {
3388            // Verify baseline-level metadata that isn't part of the
3389            // deserialized quantizer but must stay consistent with the
3390            // golden file.
3391            assert_eq!(
3392                baseline.training_seed, TRAINING_SEED,
3393                "baseline was generated with a different training seed"
3394            );
3395            assert_eq!(
3396                baseline.transform, expected_transform,
3397                "baseline transform does not match expected transform"
3398            );
3399            assert_eq!(
3400                baseline.pre_scale, expected_pre_scale,
3401                "baseline pre_scale does not match expected pre_scale"
3402            );
3403
3404            // Deserialize the quantizer from the stored bytes.
3405            let quantizer = try_deserialize::<GlobalAllocator, _>(
3406                &baseline.serialized_quantizer,
3407                GlobalAllocator,
3408            )
3409            .unwrap();
3410
3411            // Verify quantizer metadata.
3412            assert_eq!(quantizer.nbits(), baseline.nbits, "nbits mismatch");
3413            assert_eq!(quantizer.dim(), baseline.dim, "dim mismatch");
3414            assert_eq!(quantizer.full_dim(), baseline.full_dim, "full_dim mismatch");
3415            assert_eq!(
3416                quantizer.metric(),
3417                SupportedMetric::from(baseline.metric),
3418                "metric mismatch"
3419            );
3420
3421            // Verify compressed vectors.
3422            let compressed = compress_dataset(&*quantizer, dataset);
3423            assert_eq!(
3424                compressed, baseline.compressed_vectors,
3425                "compressed vectors do not match baseline"
3426            );
3427
3428            // Verify pairwise data distances.
3429            let f = quantizer.distance_computer(GlobalAllocator).unwrap();
3430            let n = baseline.compressed_vectors.len();
3431            let expected_len = n * (n + 1) / 2;
3432
3433            assert_eq!(
3434                baseline.data_distances.len(),
3435                expected_len,
3436                "baseline.data_distances has length {}, expected {} for {} compressed vectors",
3437                baseline.data_distances.len(),
3438                expected_len,
3439                n,
3440            );
3441            let mut k = 0;
3442            for (i, a) in baseline.compressed_vectors.iter().enumerate() {
3443                for (j, b) in baseline.compressed_vectors.iter().enumerate().skip(i) {
3444                    let distance = f
3445                        .evaluate_similarity(Opaque::new(a), Opaque::new(b))
3446                        .unwrap();
3447
3448                    if let Err(err) = TOLERANCE.check(distance, baseline.data_distances[k]) {
3449                        panic!("data distance mismatch at pair ({i}, {j})\n{err}");
3450                    }
3451                    k += 1;
3452                }
3453            }
3454
3455            // Verify query distances (allow_rescale=false).
3456            assert_layout_distances(
3457                &*quantizer,
3458                dataset,
3459                &baseline.compressed_vectors,
3460                &baseline.query_distances,
3461                false,
3462                "query distance mismatch",
3463            );
3464
3465            // Verify rescaled query distances for InnerProduct, or
3466            // assert that allow_rescale is a no-op for other metrics.
3467            if let Some(rescaled) = &baseline.rescaled_query_distances {
3468                assert_eq!(
3469                    SupportedMetric::from(baseline.metric),
3470                    SupportedMetric::InnerProduct,
3471                    "rescaled_query_distances should only be present \
3472                     for InnerProduct"
3473                );
3474                assert_layout_distances(
3475                    &*quantizer,
3476                    dataset,
3477                    &baseline.compressed_vectors,
3478                    rescaled,
3479                    true,
3480                    "rescaled query distance mismatch",
3481                );
3482            } else {
3483                assert_layout_distances(
3484                    &*quantizer,
3485                    dataset,
3486                    &baseline.compressed_vectors,
3487                    &baseline.query_distances,
3488                    true,
3489                    "allow_rescale should not affect non-IP distances",
3490                );
3491            }
3492        }
3493
3494        /////////////////////
3495        // Test Entrypoint //
3496        /////////////////////
3497
3498        fn make_impl_with<const NBITS: usize>(
3499            metric: SupportedMetric,
3500            transform: DataTransform,
3501            pre_scale: ScaleConfig,
3502        ) -> (Poly<dyn Quantizer>, rowmajor::Owned<f32>)
3503        where
3504            Impl<NBITS>: Constructible + Quantizer,
3505        {
3506            let data = test_dataset();
3507            let mut rng = StdRng::seed_from_u64(TRAINING_SEED);
3508
3509            let quantizer = SphericalQuantizer::train(
3510                data.as_view(),
3511                transform.to_transform_kind(),
3512                metric,
3513                pre_scale.to_prescale(),
3514                &mut rng,
3515                GlobalAllocator,
3516            )
3517            .unwrap();
3518
3519            (quantizer.as_quantizer::<NBITS>().unwrap(), data)
3520        }
3521
3522        fn run_compatibility_test<const NBITS: usize>(
3523            metric: SupportedMetric,
3524            transform: DataTransform,
3525            pre_scale: ScaleConfig,
3526        ) where
3527            Impl<NBITS>: Constructible + Quantizer,
3528        {
3529            let (quantizer, data) = make_impl_with::<NBITS>(metric, transform, pre_scale);
3530            let dataset = data.as_view();
3531
3532            let baseline = if should_overwrite() {
3533                let baseline = generate_baseline(&*quantizer, transform, pre_scale, dataset);
3534                save_baseline(&baseline);
3535                baseline
3536            } else {
3537                load_baseline(NBITS, metric.into(), transform, pre_scale)
3538            };
3539            check_baseline(&baseline, dataset, transform, pre_scale);
3540        }
3541
3542        ///////////////////////////////////////////
3543        // DoubleHadamard × all bits × metrics //
3544        ///////////////////////////////////////////
3545
3546        #[test]
3547        fn compat_1bit_l2() {
3548            run_compatibility_test::<1>(
3549                SupportedMetric::SquaredL2,
3550                DataTransform::DoubleHadamard,
3551                ScaleConfig::None,
3552            );
3553        }
3554
3555        #[test]
3556        fn compat_1bit_ip() {
3557            run_compatibility_test::<1>(
3558                SupportedMetric::InnerProduct,
3559                DataTransform::DoubleHadamard,
3560                ScaleConfig::None,
3561            );
3562        }
3563
3564        #[test]
3565        fn compat_1bit_cosine() {
3566            run_compatibility_test::<1>(
3567                SupportedMetric::Cosine,
3568                DataTransform::DoubleHadamard,
3569                ScaleConfig::None,
3570            );
3571        }
3572
3573        #[test]
3574        fn compat_2bit_l2() {
3575            run_compatibility_test::<2>(
3576                SupportedMetric::SquaredL2,
3577                DataTransform::DoubleHadamard,
3578                ScaleConfig::None,
3579            );
3580        }
3581
3582        #[test]
3583        fn compat_2bit_ip() {
3584            run_compatibility_test::<2>(
3585                SupportedMetric::InnerProduct,
3586                DataTransform::DoubleHadamard,
3587                ScaleConfig::None,
3588            );
3589        }
3590
3591        #[test]
3592        fn compat_2bit_cosine() {
3593            run_compatibility_test::<2>(
3594                SupportedMetric::Cosine,
3595                DataTransform::DoubleHadamard,
3596                ScaleConfig::None,
3597            );
3598        }
3599
3600        #[test]
3601        fn compat_4bit_l2() {
3602            run_compatibility_test::<4>(
3603                SupportedMetric::SquaredL2,
3604                DataTransform::DoubleHadamard,
3605                ScaleConfig::None,
3606            );
3607        }
3608
3609        #[test]
3610        fn compat_4bit_ip() {
3611            run_compatibility_test::<4>(
3612                SupportedMetric::InnerProduct,
3613                DataTransform::DoubleHadamard,
3614                ScaleConfig::None,
3615            );
3616        }
3617
3618        #[test]
3619        fn compat_4bit_cosine() {
3620            run_compatibility_test::<4>(
3621                SupportedMetric::Cosine,
3622                DataTransform::DoubleHadamard,
3623                ScaleConfig::None,
3624            );
3625        }
3626
3627        #[test]
3628        fn compat_8bit_l2() {
3629            run_compatibility_test::<8>(
3630                SupportedMetric::SquaredL2,
3631                DataTransform::DoubleHadamard,
3632                ScaleConfig::None,
3633            );
3634        }
3635
3636        #[test]
3637        fn compat_8bit_ip() {
3638            run_compatibility_test::<8>(
3639                SupportedMetric::InnerProduct,
3640                DataTransform::DoubleHadamard,
3641                ScaleConfig::None,
3642            );
3643        }
3644
3645        #[test]
3646        fn compat_8bit_cosine() {
3647            run_compatibility_test::<8>(
3648                SupportedMetric::Cosine,
3649                DataTransform::DoubleHadamard,
3650                ScaleConfig::None,
3651            );
3652        }
3653
3654        // Different transforms
3655        #[test]
3656        fn compat_4bit_l2_null() {
3657            run_compatibility_test::<4>(
3658                SupportedMetric::SquaredL2,
3659                DataTransform::Null,
3660                ScaleConfig::None,
3661            );
3662        }
3663
3664        #[test]
3665        fn compat_4bit_l2_padding_hadamard() {
3666            run_compatibility_test::<4>(
3667                SupportedMetric::SquaredL2,
3668                DataTransform::PaddingHadamard,
3669                ScaleConfig::None,
3670            );
3671        }
3672
3673        // PreScale
3674        #[test]
3675        fn compat_4bit_l2_prescale_rmn() {
3676            run_compatibility_test::<4>(
3677                SupportedMetric::SquaredL2,
3678                DataTransform::DoubleHadamard,
3679                ScaleConfig::ReciprocalMeanNorm,
3680            );
3681        }
3682    }
3683}