Skip to main content

diskann_providers/model/graph/provider/async_/inmem/
scalar.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::{future::Future, sync::Mutex};
7
8use crate::storage::{StorageReadProvider, StorageWriteProvider};
9use diskann::{
10    ANNError, ANNResult, default_post_processor,
11    graph::{
12        glue::{
13            self, DefaultPostProcessor, ExpandBeam, FilterStartPoints, InsertStrategy, Pipeline,
14            PruneStrategy, SearchExt, SearchStrategy,
15        },
16        workingset,
17    },
18    provider::{
19        Accessor, BuildDistanceComputer, BuildQueryComputer, DelegateNeighbor, ExecutionContext,
20        HasId,
21    },
22    utils::{IntoUsize, VectorRepr},
23};
24use diskann_quantization::{
25    AsFunctor, CompressInto,
26    bits::{Representation, Unsigned},
27    meta::NotCanonical,
28    scalar::{
29        CompensatedCosineNormalized, CompensatedIP, CompensatedSquaredL2, CompensatedVector,
30        CompensatedVectorRef, InputContainsNaN, MeanNormMissing, MutCompensatedVectorRef,
31        ScalarQuantizer,
32    },
33};
34use diskann_utils::{Reborrow, ReborrowMut, future::AsyncFriendly};
35use diskann_vector::{DistanceFunction, PreprocessedDistanceFunction, distance::Metric};
36use thiserror::Error;
37
38use super::{DefaultProvider, GetFullPrecision, PassThrough, Rerank};
39use crate::{
40    common::IgnoreLockPoison,
41    model::graph::provider::async_::{
42        FastMemoryVectorProviderAsync, SimpleNeighborProviderAsync,
43        common::{
44            AlignedMemoryVectorStore, CreateVectorStore, NoStore, Quantized, SetElementHelper,
45            TestCallCount, VectorStore,
46        },
47        inmem::{FullPrecisionProvider, FullPrecisionStore},
48        postprocess::{AsDeletionCheck, DeletionCheck, RemoveDeletedIdsAndCopy},
49    },
50    storage::{self, AsyncIndexMetadata, AsyncQuantLoadContext, LoadWith, SaveWith},
51};
52
53type CVRef<'a, const NBITS: usize> = CompensatedVectorRef<'a, NBITS>;
54
55/// A thin wrapper around [`ScalarQuantizer`] that encodes the number of bits desired for
56/// the [`SQStore`] derived from the quantizer.
57///
58/// This is meant to be used in conjunction with [`CreateQuantProvider`] to serve as a
59/// precursor for [`DefaultProvider::new_empty`].
60#[derive(Clone)]
61pub struct WithBits<const NBITS: usize> {
62    quantizer: ScalarQuantizer,
63}
64
65impl<const NBITS: usize> WithBits<NBITS> {
66    pub fn new(quantizer: ScalarQuantizer) -> Self {
67        Self { quantizer }
68    }
69}
70
71//////////////
72// Provider //
73//////////////
74
75/// This controls how many vectors share a write lock.
76///
77/// With the default value of 16, vector IDs 0-15 will share write lock 0,
78/// 16-31 will share write lock 1, etc.
79const WRITE_LOCK_GRANULARITY: usize = 16;
80
81/// The default prefetch lookahead to use if not configured externally.
82const PREFETCH_DEFAULT: usize = 8;
83
84pub struct SQStore<const NBITS: usize> {
85    data: AlignedMemoryVectorStore<u8>,
86    quantizer: ScalarQuantizer,
87    metric: Metric,
88
89    // We keep only write locks as reads are unsynchronized. Since there are
90    // only writers, we use Mutex here. Note that sync::Mutex is ok here
91    // because the Mutex is never held across an await.
92    write_locks: Vec<Mutex<()>>,
93
94    /// Prefetching for scalar bulk operations.
95    prefetch_lookahead: usize,
96
97    num_get_calls: TestCallCount,
98}
99
100impl<const NBITS: usize> SQStore<NBITS>
101where
102    Unsigned: Representation<NBITS>,
103{
104    pub(super) fn new(
105        quantizer: ScalarQuantizer,
106        num_vectors: usize,
107        metric: Metric,
108        prefetch_lookahead: Option<usize>,
109    ) -> Self {
110        let write_locks = (0..num_vectors.div_ceil(WRITE_LOCK_GRANULARITY))
111            .map(|_| Mutex::new(()))
112            .collect::<Vec<_>>();
113        // Compute the number of bytes needed to hold the data and the compensation
114        // coefficient.
115        let bytes = CVRef::<NBITS>::canonical_bytes(quantizer.dim());
116        Self {
117            data: AlignedMemoryVectorStore::with_capacity(num_vectors, bytes),
118            quantizer,
119            metric,
120            write_locks,
121            num_get_calls: TestCallCount::default(),
122            prefetch_lookahead: prefetch_lookahead.unwrap_or(PREFETCH_DEFAULT),
123        }
124    }
125
126    /// Prefetch the first few cache lines of the data for vector `i`.
127    ///
128    /// # Panics
129    ///
130    /// Panics if `i >= self.size()`.
131    pub(crate) fn prefetch_hint(&self, i: usize) {
132        // SAFETY: Racing on the underlying data is okay because we are dispatching to
133        // an architectural primitive for prefetching that doesn't care about the data
134        // itself, just its address.
135        let data = unsafe { self.data.get_slice(i) };
136        diskann_vector::prefetch_hint_max::<4, _>(data);
137    }
138
139    pub(super) fn dim(&self) -> usize {
140        self.quantizer.dim()
141    }
142
143    pub(super) fn get_vector(&self, i: usize) -> Result<CVRef<'_, NBITS>, SQError> {
144        self.num_get_calls.increment();
145        Ok(CVRef::from_canonical_front(
146            unsafe { self.data.get_slice(i) },
147            self.dim(),
148        )?)
149    }
150
151    pub(super) fn set_vector<T>(&self, i: usize, v: &[T]) -> Result<(), SQError>
152    where
153        T: VectorRepr,
154    {
155        let vf32: &[f32] =
156            &T::as_f32(v).map_err(|e| SQError::FullPrecisionConversionErr(format!("{:?}", e)))?;
157
158        debug_assert!(
159            vf32.len() == self.dim(),
160            "vector f32 dimension {} does not match dimension {}",
161            vf32.len(),
162            self.dim()
163        );
164
165        let lock_id = i / WRITE_LOCK_GRANULARITY;
166        let _guard = self.write_locks[lock_id].lock_or_panic();
167
168        self.quantizer.compress_into(
169            vf32,
170            MutCompensatedVectorRef::<NBITS>::from_canonical_front_mut(
171                unsafe { self.data.get_mut_slice(i) },
172                self.dim(),
173            )?,
174        )?;
175        Ok(())
176    }
177
178    /// Store the compressed SQ vector directly at position `i`.
179    ///
180    /// Panic if:
181    ///
182    /// * `i >= self.total()`: `i` must be inbounds.
183    /// * `v.len() != self.dim()`: `v` must have the right length.
184    ///
185    /// # Safety
186    ///
187    /// This function guarantees mutual exclusion of **writers** to the underlying data,
188    /// but does not guarantee the mutual exclusion of aliased readers to the same data.
189    ///
190    /// It is the caller's responsibility to either:
191    ///
192    /// 1. Use this method in a way that ensures mutual exclusion with mutable references to
193    ///    the same ID.
194    ///
195    /// 2. Be okay with racey data.
196    pub(crate) unsafe fn set_quant_vector(&self, i: usize, v: &[u8]) -> ANNResult<()> {
197        let expected_quant_len = CVRef::<NBITS>::canonical_bytes(self.dim());
198        debug_assert!(
199            v.len() == expected_quant_len,
200            "vector length {} does not match dimension {}",
201            v.len(),
202            expected_quant_len
203        );
204
205        let lock_id = i / WRITE_LOCK_GRANULARITY;
206        let _guard = self.write_locks[lock_id].lock_or_panic();
207        // SAFETY: `get_mut_slice` guarantees it is safe to access the memory,
208        // and but it may be a torn read. As we are trading off synchronization
209        // for speed, this is okay.
210        unsafe { self.data.get_mut_slice(i) }.copy_from_slice(v);
211        Ok(())
212    }
213
214    // Return a distance computer.
215    pub(super) fn distance_computer(&self) -> Result<DistanceComputer, SQError> {
216        Ok(match self.metric {
217            Metric::L2 => DistanceComputer::SquaredL2(self.quantizer.as_functor()),
218            Metric::InnerProduct => DistanceComputer::InnerProduct(self.quantizer.as_functor()),
219            Metric::CosineNormalized => {
220                DistanceComputer::CosineNormalized(self.quantizer.as_functor())
221            }
222            unsupported_metric => {
223                return Err(SQError::UnsupportedDistanceMetric(unsupported_metric));
224            }
225        })
226    }
227
228    pub(super) fn query_computer<T>(
229        &self,
230        query: &[T],
231        allow_rescale: bool,
232    ) -> Result<QueryComputer<NBITS>, SQError>
233    where
234        T: VectorRepr,
235    {
236        let mut boxed = CompensatedVector::new_boxed(self.dim());
237        let q = T::as_f32(query)
238            .map_err(|e| SQError::FullPrecisionConversionErr(format!("{:?}", e)))?;
239
240        if allow_rescale && !matches!(self.metric, Metric::L2 | Metric::CosineNormalized) {
241            let mut query: Box<[f32]> = q.as_ref().into();
242            self.quantizer.rescale(&mut query)?;
243            self.quantizer
244                .compress_into(&*query, boxed.reborrow_mut())?;
245        } else {
246            self.quantizer
247                .compress_into(q.as_ref(), boxed.reborrow_mut())?;
248        }
249
250        Ok(QueryComputer {
251            inner: self.distance_computer()?,
252            query: boxed,
253        })
254    }
255
256    pub fn prefetch_lookahead(&self) -> usize {
257        self.prefetch_lookahead
258    }
259}
260
261#[derive(Debug)]
262pub enum DistanceComputer {
263    SquaredL2(CompensatedSquaredL2),
264    InnerProduct(CompensatedIP),
265    CosineNormalized(CompensatedCosineNormalized),
266}
267
268impl<const NBITS: usize> DistanceFunction<CVRef<'_, NBITS>, CVRef<'_, NBITS>, f32>
269    for DistanceComputer
270where
271    Unsigned: Representation<NBITS>,
272    CompensatedSquaredL2: for<'a, 'b> DistanceFunction<
273            CVRef<'a, NBITS>,
274            CVRef<'b, NBITS>,
275            diskann_quantization::distances::Result<f32>,
276        >,
277    CompensatedIP: for<'a, 'b> DistanceFunction<
278            CVRef<'a, NBITS>,
279            CVRef<'b, NBITS>,
280            diskann_quantization::distances::Result<f32>,
281        >,
282    CompensatedCosineNormalized: for<'a, 'b> DistanceFunction<
283            CVRef<'a, NBITS>,
284            CVRef<'b, NBITS>,
285            diskann_quantization::distances::Result<f32>,
286        >,
287{
288    #[inline(always)]
289    fn evaluate_similarity(&self, left: CVRef<'_, NBITS>, right: CVRef<'_, NBITS>) -> f32 {
290        let r = match self {
291            DistanceComputer::SquaredL2(f) => f.evaluate_similarity(left, right),
292            DistanceComputer::InnerProduct(f) => f.evaluate_similarity(left, right),
293            DistanceComputer::CosineNormalized(f) => f.evaluate_similarity(left, right),
294        };
295
296        r.map_err(|err| err.panic(left.len(), right.len())).unwrap()
297    }
298}
299
300pub struct QueryComputer<const NBITS: usize>
301where
302    Unsigned: Representation<NBITS>,
303{
304    inner: DistanceComputer,
305    query: CompensatedVector<NBITS>,
306}
307
308impl<const NBITS: usize> PreprocessedDistanceFunction<CVRef<'_, NBITS>, f32>
309    for QueryComputer<NBITS>
310where
311    Unsigned: Representation<NBITS>,
312    DistanceComputer: for<'a, 'b> DistanceFunction<CVRef<'a, NBITS>, CVRef<'b, NBITS>, f32>,
313{
314    fn evaluate_similarity(&self, changing: CVRef<'_, NBITS>) -> f32 {
315        self.inner
316            .evaluate_similarity(self.query.reborrow(), changing)
317    }
318}
319
320///////////////////
321// Data Provider //
322///////////////////
323
324impl<const NBITS: usize> CreateVectorStore for WithBits<NBITS>
325where
326    Unsigned: Representation<NBITS>,
327{
328    type Target = SQStore<NBITS>;
329
330    /// Create a quant provider capable of tracking `max_points`.
331    fn create(
332        self,
333        max_points: usize,
334        metric: Metric,
335        prefetch_lookahead: Option<usize>,
336    ) -> Self::Target {
337        SQStore::new(self.quantizer, max_points, metric, prefetch_lookahead)
338    }
339}
340
341impl<const NBITS: usize> VectorStore for SQStore<NBITS> {
342    fn total(&self) -> usize {
343        self.data.max_vectors()
344    }
345
346    fn count_for_get_vector(&self) -> usize {
347        self.num_get_calls.get()
348    }
349}
350
351////////////////
352// SetElement //
353////////////////
354
355/// Assign to SQ vector store.
356impl<T, const NBITS: usize> SetElementHelper<T> for SQStore<NBITS>
357where
358    T: VectorRepr,
359    Unsigned: Representation<NBITS>,
360{
361    fn set_element(&self, id: &u32, element: &[T]) -> ANNResult<()> {
362        self.set_vector(id.into_usize(), element)?;
363        Ok(())
364    }
365}
366
367//////////////
368// Accessor //
369//////////////
370
371/// The accessor for SQ.
372pub struct QuantAccessor<'a, const NBITS: usize, V, D, Ctx> {
373    provider: &'a DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
374    id_buffer: Vec<u32>,
375    is_search: bool,
376}
377
378impl<T, const NBITS: usize, D, Ctx> GetFullPrecision
379    for QuantAccessor<'_, NBITS, FullPrecisionStore<T>, D, Ctx>
380where
381    T: VectorRepr,
382{
383    type Repr = T;
384    fn as_full_precision(&self) -> &FastMemoryVectorProviderAsync<T> {
385        &self.provider.base_vectors
386    }
387}
388
389impl<const NBITS: usize, V, D, Ctx> HasId for QuantAccessor<'_, NBITS, V, D, Ctx> {
390    type Id = u32;
391}
392
393impl<const NBITS: usize, V, D, Ctx> SearchExt for QuantAccessor<'_, NBITS, V, D, Ctx>
394where
395    V: AsyncFriendly,
396    D: AsyncFriendly,
397    Ctx: ExecutionContext,
398    Unsigned: Representation<NBITS>,
399{
400    fn starting_points(&self) -> impl Future<Output = ANNResult<Vec<u32>>> {
401        std::future::ready(self.provider.starting_points())
402    }
403}
404
405impl<'a, const NBITS: usize, V, D, Ctx> QuantAccessor<'a, NBITS, V, D, Ctx>
406where
407    V: AsyncFriendly,
408    D: AsyncFriendly,
409    Ctx: ExecutionContext,
410{
411    pub(crate) fn new(
412        provider: &'a DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
413        is_search: bool,
414    ) -> Self {
415        Self {
416            provider,
417            id_buffer: Vec::with_capacity(32),
418            is_search,
419        }
420    }
421}
422
423impl<const NBITS: usize, V, D, Ctx> Accessor for QuantAccessor<'_, NBITS, V, D, Ctx>
424where
425    V: AsyncFriendly,
426    D: AsyncFriendly,
427    Ctx: ExecutionContext,
428    Unsigned: Representation<NBITS>,
429{
430    /// This accessor returns raw slices. There *is* a chance of racing when the fast
431    /// providers are used. We just have to live with it.
432    type Element<'a>
433        = CVRef<'a, NBITS>
434    where
435        Self: 'a;
436
437    /// `ElementRef` has an arbitrarily short lifetime.
438    type ElementRef<'a> = CVRef<'a, NBITS>;
439
440    /// Choose to panic on an out-of-bounds access rather than propagate an error.
441    type GetError = ANNError;
442
443    /// Return the quantized vector stored at index `i`.
444    ///
445    /// This function always completes synchronously.
446    fn get_element(
447        &mut self,
448        id: Self::Id,
449    ) -> impl Future<Output = Result<Self::Element<'_>, Self::GetError>> + Send {
450        // SAFETY: We've decided to live with UB that can result from potentially mixing
451        // unsynchronized reads and writes on the underlying memory.
452        std::future::ready(
453            match self.provider.aux_vectors.get_vector(id.into_usize()) {
454                Ok(v) => Ok(v),
455                Err(err) => Err(err.into()),
456            },
457        )
458    }
459
460    /// Perform a bulk operation.
461    ///
462    /// This implementation uses prefetching.
463    fn on_elements_unordered<Itr, F>(
464        &mut self,
465        itr: Itr,
466        mut f: F,
467    ) -> impl Future<Output = Result<(), Self::GetError>> + Send
468    where
469        Self: Sync,
470        Itr: Iterator<Item = Self::Id> + Send,
471        F: Send + for<'b> FnMut(Self::ElementRef<'b>, Self::Id),
472    {
473        // Reuse the internal buffer to collect the results and give us random access
474        // capabilities.
475        let id_buffer = &mut self.id_buffer;
476        id_buffer.clear();
477        id_buffer.extend(itr);
478
479        let len = id_buffer.len();
480        let lookahead = self.provider.aux_vectors.prefetch_lookahead();
481
482        // Prefetch the first few vectors.
483        for id in id_buffer.iter().take(lookahead) {
484            self.provider.aux_vectors.prefetch_hint(id.into_usize());
485        }
486
487        for (i, id) in id_buffer.iter().enumerate() {
488            // Prefetch `lookahead` iterations ahead as long as it is safe.
489            if lookahead > 0 && i + lookahead < len {
490                self.provider
491                    .aux_vectors
492                    .prefetch_hint(id_buffer[i + lookahead].into_usize());
493            }
494
495            let vector = match self.provider.aux_vectors.get_vector(id.into_usize()) {
496                Ok(v) => v,
497                Err(e) => return std::future::ready(Err(e.into())),
498            };
499
500            // Invoke the passed closure on the vector.
501            //
502            // SAFETY: We're accepting the consequences of potential unsynchronized,
503            // concurrent mutation.
504            f(vector, *id)
505        }
506
507        std::future::ready(Ok(()))
508    }
509}
510
511impl<'a, const NBITS: usize, V, D, Ctx> DelegateNeighbor<'a> for QuantAccessor<'_, NBITS, V, D, Ctx>
512where
513    V: AsyncFriendly,
514    D: AsyncFriendly,
515    Ctx: ExecutionContext,
516{
517    type Delegate = &'a SimpleNeighborProviderAsync<u32>;
518    fn delegate_neighbor(&'a mut self) -> Self::Delegate {
519        self.provider.neighbors()
520    }
521}
522
523impl<const NBITS: usize, V, D, Ctx, T> BuildQueryComputer<&[T]>
524    for QuantAccessor<'_, NBITS, V, D, Ctx>
525where
526    T: VectorRepr,
527    V: AsyncFriendly,
528    D: AsyncFriendly,
529    Ctx: ExecutionContext,
530    Unsigned: Representation<NBITS>,
531    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
532{
533    type QueryComputerError = ANNError;
534    type QueryComputer = QueryComputer<NBITS>;
535
536    fn build_query_computer(
537        &self,
538        from: &[T],
539    ) -> Result<Self::QueryComputer, Self::QueryComputerError> {
540        // Allow rescaling if this is search.
541        Ok(self
542            .provider
543            .aux_vectors
544            .query_computer(from, self.is_search)?)
545    }
546}
547
548impl<const NBITS: usize, V, D, Ctx, T> ExpandBeam<&[T]> for QuantAccessor<'_, NBITS, V, D, Ctx>
549where
550    T: VectorRepr,
551    V: AsyncFriendly,
552    D: AsyncFriendly,
553    Ctx: ExecutionContext,
554    Unsigned: Representation<NBITS>,
555    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
556{
557}
558
559impl<const NBITS: usize, V, D, Ctx> BuildDistanceComputer for QuantAccessor<'_, NBITS, V, D, Ctx>
560where
561    V: AsyncFriendly,
562    D: AsyncFriendly,
563    Ctx: ExecutionContext,
564    Unsigned: Representation<NBITS>,
565    DistanceComputer: for<'a, 'b> DistanceFunction<CVRef<'a, NBITS>, CVRef<'b, NBITS>, f32>,
566{
567    type DistanceComputerError = ANNError;
568    type DistanceComputer = DistanceComputer;
569
570    fn build_distance_computer(
571        &self,
572    ) -> Result<Self::DistanceComputer, Self::DistanceComputerError> {
573        Ok(self.provider.aux_vectors.distance_computer()?)
574    }
575}
576
577impl<const NBITS: usize, V, D, Ctx> AsDeletionCheck for QuantAccessor<'_, NBITS, V, D, Ctx>
578where
579    V: AsyncFriendly,
580    D: AsyncFriendly + DeletionCheck,
581    Ctx: ExecutionContext,
582    Unsigned: Representation<NBITS>,
583{
584    type Checker = D;
585    fn as_deletion_check(&self) -> &D {
586        &self.provider.deleted
587    }
588}
589
590////////////////
591// Strategies //
592////////////////
593
594/// SearchStrategy for quantized search when a full-precision store exists alongside
595/// the quantized store. This allows reranking using original vectors after
596/// approximate search, so the post-processing step includes a [`Rerank`] stage.
597impl<const NBITS: usize, D, Ctx, T>
598    SearchStrategy<FullPrecisionProvider<T, SQStore<NBITS>, D, Ctx>, &[T]> for Quantized
599where
600    T: VectorRepr,
601    D: AsyncFriendly + DeletionCheck,
602    Ctx: ExecutionContext,
603    Unsigned: Representation<NBITS>,
604    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
605{
606    type QueryComputer = QueryComputer<NBITS>;
607    type SearchAccessor<'a> = QuantAccessor<'a, NBITS, FullPrecisionStore<T>, D, Ctx>;
608    type SearchAccessorError = ANNError;
609
610    fn search_accessor<'a>(
611        &'a self,
612        provider: &'a FullPrecisionProvider<T, SQStore<NBITS>, D, Ctx>,
613        _context: &'a Ctx,
614    ) -> Result<Self::SearchAccessor<'a>, Self::SearchAccessorError> {
615        Ok(QuantAccessor::new(provider, true))
616    }
617}
618
619impl<const NBITS: usize, D, Ctx, T>
620    DefaultPostProcessor<FullPrecisionProvider<T, SQStore<NBITS>, D, Ctx>, &[T]> for Quantized
621where
622    T: VectorRepr,
623    D: AsyncFriendly + DeletionCheck,
624    Ctx: ExecutionContext,
625    Unsigned: Representation<NBITS>,
626    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
627{
628    default_post_processor!(Pipeline<FilterStartPoints, Rerank>);
629}
630
631/// SearchStrategy for quantized search when only the quantized store is present.
632/// Since no full-precision vectors exist, reranking is not possible and the
633/// post-processing step just copies candidate IDs forward via [`RemoveDeletedIdsAndCopy`].
634impl<const NBITS: usize, D, Ctx, T>
635    SearchStrategy<DefaultProvider<NoStore, SQStore<NBITS>, D, Ctx>, &[T]> for Quantized
636where
637    T: VectorRepr,
638    D: AsyncFriendly + DeletionCheck,
639    Ctx: ExecutionContext,
640    Unsigned: Representation<NBITS>,
641    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
642{
643    type QueryComputer = QueryComputer<NBITS>;
644    type SearchAccessor<'a> = QuantAccessor<'a, NBITS, NoStore, D, Ctx>;
645    type SearchAccessorError = ANNError;
646
647    fn search_accessor<'a>(
648        &'a self,
649        provider: &'a DefaultProvider<NoStore, SQStore<NBITS>, D, Ctx>,
650        _context: &'a Ctx,
651    ) -> Result<Self::SearchAccessor<'a>, Self::SearchAccessorError> {
652        Ok(QuantAccessor::new(provider, true))
653    }
654}
655
656impl<const NBITS: usize, D, Ctx, T>
657    DefaultPostProcessor<DefaultProvider<NoStore, SQStore<NBITS>, D, Ctx>, &[T]> for Quantized
658where
659    T: VectorRepr,
660    D: AsyncFriendly + DeletionCheck,
661    Ctx: ExecutionContext,
662    Unsigned: Representation<NBITS>,
663    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
664{
665    default_post_processor!(Pipeline<FilterStartPoints, RemoveDeletedIdsAndCopy>);
666}
667
668impl<const NBITS: usize, V, D, Ctx> PruneStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>>
669    for Quantized
670where
671    V: AsyncFriendly,
672    D: AsyncFriendly + DeletionCheck,
673    Ctx: ExecutionContext,
674    Unsigned: Representation<NBITS>,
675    DistanceComputer: for<'a, 'b> DistanceFunction<CVRef<'a, NBITS>, CVRef<'b, NBITS>, f32>,
676{
677    type DistanceComputer<'a> = DistanceComputer;
678    type PruneAccessor<'a> = QuantAccessor<'a, NBITS, V, D, Ctx>;
679    type PruneAccessorError = diskann::error::Infallible;
680    type WorkingSet = PassThrough;
681
682    fn create_working_set(&self, _capacity: usize) -> Self::WorkingSet {
683        PassThrough
684    }
685
686    fn prune_accessor<'a>(
687        &'a self,
688        provider: &'a DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
689        _context: &'a Ctx,
690    ) -> Result<Self::PruneAccessor<'a>, Self::PruneAccessorError> {
691        Ok(QuantAccessor::new(provider, false))
692    }
693}
694
695// Pass-through fill — returns `&Self` which directly accesses the underlying provider.
696impl<const NBITS: usize, V, D, Ctx> workingset::Fill<PassThrough>
697    for QuantAccessor<'_, NBITS, V, D, Ctx>
698where
699    V: AsyncFriendly,
700    D: AsyncFriendly + DeletionCheck,
701    Ctx: ExecutionContext,
702    Unsigned: Representation<NBITS>,
703{
704    type Error = std::convert::Infallible;
705    type View<'a>
706        = &'a Self
707    where
708        Self: 'a;
709
710    async fn fill<'a, Itr>(
711        &'a mut self,
712        _state: &'a mut PassThrough,
713        _itr: Itr,
714    ) -> Result<Self::View<'a>, Self::Error>
715    where
716        Itr: ExactSizeIterator<Item = Self::Id> + Clone + Send + Sync,
717        Self: 'a,
718    {
719        Ok(self)
720    }
721}
722
723// Pass-through view — reads scalar-quantized vectors directly from the provider.
724impl<const NBITS: usize, V, D, Ctx> workingset::View<u32> for &QuantAccessor<'_, NBITS, V, D, Ctx>
725where
726    V: AsyncFriendly,
727    D: AsyncFriendly + DeletionCheck,
728    Ctx: ExecutionContext,
729    Unsigned: Representation<NBITS>,
730{
731    type ElementRef<'a> = CVRef<'a, NBITS>;
732    type Element<'a>
733        = CVRef<'a, NBITS>
734    where
735        Self: 'a;
736
737    fn get(&self, id: u32) -> Option<Self::Element<'_>> {
738        self.provider.aux_vectors.get_vector(id.into_usize()).ok()
739    }
740}
741
742impl<const NBITS: usize, V, D, Ctx, T>
743    InsertStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>, &[T]> for Quantized
744where
745    T: VectorRepr,
746    V: AsyncFriendly,
747    D: AsyncFriendly,
748    D: AsyncFriendly + DeletionCheck,
749    Ctx: ExecutionContext,
750    Unsigned: Representation<NBITS>,
751    QueryComputer<NBITS>: for<'a> PreprocessedDistanceFunction<CVRef<'a, NBITS>, f32>,
752    DistanceComputer: for<'a, 'b> DistanceFunction<CVRef<'a, NBITS>, CVRef<'b, NBITS>, f32>,
753    Quantized: for<'a> SearchStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>, &'a [T]>,
754{
755    type PruneStrategy = Self;
756
757    fn prune_strategy(&self) -> Self::PruneStrategy {
758        *self
759    }
760}
761
762impl<const NBITS: usize, V, D, Ctx, B>
763    glue::MultiInsertStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>, B> for Quantized
764where
765    V: AsyncFriendly,
766    D: AsyncFriendly + DeletionCheck,
767    Ctx: ExecutionContext,
768    B: glue::Batch,
769    Self: PruneStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>, WorkingSet = PassThrough>
770        + for<'a> InsertStrategy<
771            DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
772            B::Element<'a>,
773            PruneStrategy = Self,
774        >,
775{
776    type WorkingSet = PassThrough;
777    type Seed = PassThrough;
778    type FinishError = diskann::error::Infallible;
779    type InsertStrategy = Self;
780
781    fn insert_strategy(&self) -> Self::InsertStrategy {
782        *self
783    }
784
785    fn finish<Itr>(
786        &self,
787        _provider: &DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
788        _ctx: &Ctx,
789        _batch: &std::sync::Arc<B>,
790        _ids: Itr,
791    ) -> impl std::future::Future<Output = Result<Self::Seed, Self::FinishError>> + Send
792    where
793        Itr: ExactSizeIterator<Item = u32> + Send,
794    {
795        std::future::ready(Ok(PassThrough))
796    }
797}
798
799////////////////////////////
800// SaveWith  and LoadWith //
801////////////////////////////
802
803impl<const NBITS: usize> SaveWith<AsyncIndexMetadata> for SQStore<NBITS> {
804    type Ok = usize;
805    type Error = ANNError;
806
807    async fn save_with<P>(
808        &self,
809        write_provider: &P,
810        metadata: &AsyncIndexMetadata,
811    ) -> Result<Self::Ok, Self::Error>
812    where
813        P: StorageWriteProvider,
814    {
815        let sq_storage = storage::SQStorage::new(metadata.prefix());
816        let bytes_written =
817            storage::bin::save_to_bin(self, write_provider, sq_storage.compressed_data_path())?;
818        let quantizer_bytes_written = sq_storage.save_quantizer(&self.quantizer, write_provider)?;
819        Ok(bytes_written + quantizer_bytes_written)
820    }
821}
822
823impl<const NBITS: usize> LoadWith<AsyncQuantLoadContext> for SQStore<NBITS>
824where
825    Unsigned: Representation<NBITS>,
826{
827    type Error = ANNError;
828
829    async fn load_with<P>(read_provider: &P, ctx: &AsyncQuantLoadContext) -> ANNResult<Self>
830    where
831        P: StorageReadProvider,
832    {
833        let sq_storage = storage::SQStorage::new(ctx.metadata.prefix());
834        let quantizer = sq_storage.load_quantizer(read_provider)?;
835
836        storage::bin::load_from_bin(
837            read_provider,
838            sq_storage.compressed_data_path(),
839            |num_points, _pq_bytes| {
840                Ok(SQStore::<NBITS>::new(
841                    quantizer,
842                    num_points,
843                    ctx.metric,
844                    ctx.prefetch_lookahead,
845                ))
846            },
847        )
848    }
849}
850
851/// Hook into [`storage::bin::load_from_bin`] by implementing [`storage::bin::SetData`].
852impl<const NBITS: usize> storage::bin::SetData for SQStore<NBITS>
853where
854    Unsigned: Representation<NBITS>,
855{
856    type Item = u8;
857
858    fn set_data(&mut self, i: usize, element: &[Self::Item]) -> ANNResult<()> {
859        // SAFETY: No race can happen because we have a mutable reference to `self`.
860        unsafe { self.set_quant_vector(i, element) }
861    }
862}
863
864/// Hook into [`storage::bin::save_to_bin`] by implementing [`storage::bin::GetData`].
865impl<const NBITS: usize> storage::bin::GetData for SQStore<NBITS> {
866    type Element = u8;
867    type Item<'a> = &'a [u8];
868
869    fn get_data(&self, i: usize) -> ANNResult<Self::Item<'_>> {
870        // SAFETY: We aren't full protected against races on the underlying data, but at
871        // least `&self` will keep the data alive.
872        Ok(unsafe { self.data.get_slice(i) })
873    }
874
875    /// Return the total number of points, including frozen points.
876    fn total(&self) -> usize {
877        self.data.max_vectors()
878    }
879
880    fn dim(&self) -> usize {
881        self.data.dim()
882    }
883}
884
885#[derive(Debug, Error)]
886pub enum SQError {
887    #[error("Issue with canonical layout of data: {0:?}")]
888    CanonicalLayoutError(#[from] NotCanonical),
889
890    #[error("Input contains NaN values.")]
891    InputContainsNaN(#[from] InputContainsNaN),
892
893    #[error("Input full-precision conversion error : {0}")]
894    FullPrecisionConversionErr(String),
895
896    #[error("Mean Norm is missing in the quantizer.")]
897    MeanNormMissing(#[from] MeanNormMissing),
898
899    #[error("Unsupported distance metric: {0:?}")]
900    UnsupportedDistanceMetric(Metric),
901
902    #[error("Error while loading quantizer proto struct from file: {0:?}")]
903    ProtoStorageError(#[from] crate::storage::protos::ProtoStorageError),
904
905    #[error("Error while converting proto struct to Scalar Qunatizer: {0:?}")]
906    QuantizerDecodeError(#[from] crate::storage::protos::ProtoConversionError),
907}
908
909impl From<SQError> for ANNError {
910    #[cold]
911    fn from(err: SQError) -> Self {
912        ANNError::log_sq_error(err)
913    }
914}
915
916#[cfg(test)]
917mod tests {
918    use crate::storage::VirtualStorageProvider;
919    use diskann::utils::ONE;
920    use diskann_quantization::scalar::train::ScalarQuantizationParameters;
921    use diskann_utils::views::MatrixView;
922    use diskann_vector::distance::Metric;
923    use rstest::rstest;
924
925    use super::*;
926
927    const NBITS: usize = 1;
928    const DIM: usize = 4;
929    const NPTS: usize = 5;
930    const DATA: [f32; 20] = [
931        0.286541, -0.079761, 0.373634, 0.878595, -0.131049, -0.131040, 0.883841, 0.429512,
932        -0.482576, 0.557701, -0.476350, -0.478727, 0.091383, -0.722600, -0.651460, -0.212363,
933        -0.510018, 0.158241, -0.457242, -0.711176,
934    ];
935    const V: [f32; DIM] = [DATA[0], DATA[1], DATA[2], DATA[3]];
936
937    fn make_store(metric: Metric) -> SQStore<NBITS> {
938        let quantizer = ScalarQuantizationParameters::default()
939            .train(MatrixView::try_from(&DATA, NPTS, DIM).unwrap());
940        SQStore::new(quantizer, /* capacity */ 5, metric, None)
941    }
942
943    #[test]
944    fn test_dim() {
945        let store = make_store(Metric::L2);
946        assert_eq!(store.dim(), DIM);
947    }
948
949    #[test]
950    fn test_set_and_get_vector() {
951        let store = make_store(Metric::L2);
952        store.set_vector(0, &V).unwrap();
953        store.get_vector(0).unwrap();
954        // `set` and `get` should not panic
955    }
956
957    #[test]
958    #[should_panic]
959    fn test_set_vector_wrong_dim_panic_in_debug() {
960        let store = make_store(Metric::L2);
961        let _: Result<_, SQError> = store.set_vector(0, &[1.0f32; DIM + 1]);
962    }
963
964    #[test]
965    #[should_panic]
966    fn test_get_vector_oob() {
967        let store = make_store(Metric::L2);
968        let _: Result<_, SQError> = store.get_vector(NPTS);
969    }
970
971    #[test]
972    fn test_prefetch_hint_ok() {
973        let store = make_store(Metric::L2);
974        store.prefetch_hint(NPTS - 1);
975    }
976
977    #[test]
978    #[should_panic]
979    fn test_prefetch_hint_oob() {
980        let store = make_store(Metric::L2);
981        store.prefetch_hint(NPTS);
982    }
983
984    #[test]
985    fn test_distance_computer_variants() {
986        let dc_l2 = make_store(Metric::L2).distance_computer().unwrap();
987        match dc_l2 {
988            DistanceComputer::SquaredL2(_) => {}
989            _ => panic!("expected SquaredL2 variant"),
990        }
991
992        let dc_ip = make_store(Metric::InnerProduct)
993            .distance_computer()
994            .unwrap();
995        match dc_ip {
996            DistanceComputer::InnerProduct(_) => {}
997            _ => panic!("expected InnerProduct variant"),
998        }
999
1000        let dc_cosine_normalized = make_store(Metric::CosineNormalized)
1001            .distance_computer()
1002            .unwrap();
1003        match dc_cosine_normalized {
1004            DistanceComputer::CosineNormalized(_) => {}
1005            _ => panic!("expected CosineNormalized variant"),
1006        }
1007
1008        let dc_unsupported = make_store(Metric::Cosine).distance_computer().unwrap_err();
1009        match dc_unsupported {
1010            SQError::UnsupportedDistanceMetric(Metric::Cosine) => {}
1011            _ => panic!("expected UnsupportedDistanceMetric error"),
1012        }
1013    }
1014
1015    #[rstest]
1016    fn test_query_computer(
1017        #[values(Metric::L2, Metric::InnerProduct, Metric::CosineNormalized)] metric: Metric,
1018        #[values(false, true)] allow_rescale: bool,
1019    ) {
1020        let store = make_store(metric);
1021        let q = [1.0_f32; DIM];
1022        let result = store.query_computer(&q, allow_rescale);
1023        assert!(
1024            result.is_ok(),
1025            "query_computer() failed for metric {:?} with allow_rescale={}",
1026            metric,
1027            allow_rescale
1028        );
1029    }
1030
1031    #[test]
1032    fn test_set_quant_vector() {
1033        let store = make_store(Metric::L2);
1034        let compressed_vec_len = CVRef::<NBITS>::canonical_bytes(DIM);
1035        let raw = vec![1u8; compressed_vec_len];
1036
1037        unsafe {
1038            store.set_quant_vector(0, &raw).unwrap();
1039        }
1040
1041        // read back the same bytes
1042        let slice = unsafe { store.data.get_slice(0) };
1043        assert_eq!(slice, raw.as_slice());
1044    }
1045
1046    #[test]
1047    #[should_panic]
1048    fn test_set_quant_vector_with_wrong_dim_panics() {
1049        let store = make_store(Metric::L2);
1050        let wrong_compressed_vec_len = CVRef::<NBITS>::canonical_bytes(DIM) + 1;
1051        let raw = vec![1u8; wrong_compressed_vec_len];
1052
1053        unsafe {
1054            store.set_quant_vector(0, &raw).unwrap();
1055        }
1056    }
1057
1058    #[rstest]
1059    fn test_distance_computer_cosine_normalized(
1060        #[values(Metric::L2, Metric::InnerProduct, Metric::CosineNormalized)] metric: Metric,
1061    ) {
1062        let store = make_store(metric);
1063        // Set two vectors
1064        let v1 = [0.1, 0.2, 0.3, 0.4];
1065        let v2 = [0.4, 0.3, 0.2, 0.1];
1066        store.set_vector(0, &v1).unwrap();
1067        store.set_vector(1, &v2).unwrap();
1068
1069        let dc = store.distance_computer().unwrap();
1070        let x = store.get_vector(0).unwrap();
1071        let y = store.get_vector(1).unwrap();
1072
1073        // This will exercise the CosineNormalized match arm
1074        let _ = dc.evaluate_similarity(x, y);
1075    }
1076
1077    #[tokio::test]
1078    async fn test_save_with_and_load_with() {
1079        let storage_provider = VirtualStorageProvider::new_memory();
1080        let store = make_store(Metric::InnerProduct);
1081
1082        // Save to our memory provider
1083        let prefix = "/test";
1084        let metadata = AsyncIndexMetadata::new(prefix.to_string());
1085        let bytes_written = store.save_with(&storage_provider, &metadata).await.unwrap();
1086        let sq_storage = storage::SQStorage::new(prefix);
1087        assert!(bytes_written > 0);
1088        assert!(storage_provider.exists(sq_storage.compressed_data_path()),);
1089        assert!(storage_provider.exists(sq_storage.quantizer_path()));
1090
1091        // Load back from the same provider
1092        let ctx = AsyncQuantLoadContext {
1093            metadata,
1094            num_frozen_points: ONE,
1095            metric: Metric::InnerProduct,
1096            prefetch_lookahead: None,
1097            is_disk_index: false,
1098            prefetch_cache_line_level: None,
1099        };
1100
1101        let loaded = SQStore::<NBITS>::load_with(&storage_provider, &ctx)
1102            .await
1103            .unwrap();
1104
1105        // verify dimension is preserved
1106        assert_eq!(loaded.dim(), store.dim());
1107        // verify the raw bytes round-trip correctly
1108        for i in 0..NPTS {
1109            let original = unsafe { store.data.get_slice(i) };
1110            let loaded = unsafe { loaded.data.get_slice(i) };
1111            assert_eq!(original, loaded);
1112        }
1113    }
1114}