Skip to main content

diskann_quantization/spherical/
iface.rs

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