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