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