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,
704    D: AsyncFriendly + DeletionCheck,
705    Ctx: ExecutionContext,
706    Unsigned: Representation<NBITS>,
707    QueryComputer<NBITS>: for<'x> PreprocessedDistanceFunction<CVRef<'x, NBITS>, f32>,
708    DistanceComputer: for<'x, 'y> DistanceFunction<CVRef<'x, NBITS>, CVRef<'y, NBITS>, f32>,
709    Quantized: SearchStrategy<'a, DefaultProvider<V, SQStore<NBITS>, D, Ctx>, &'a [T]>,
710{
711    type PruneStrategy = Self;
712
713    fn prune_strategy(&self) -> Self::PruneStrategy {
714        *self
715    }
716}
717
718impl<const NBITS: usize, V, D, Ctx, B>
719    glue::MultiInsertStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>, B> for Quantized
720where
721    V: AsyncFriendly,
722    D: AsyncFriendly + DeletionCheck,
723    Ctx: ExecutionContext,
724    B: glue::Batch,
725    Self: PruneStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>>
726        + for<'a> InsertStrategy<
727            'a,
728            DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
729            B::Element<'a>,
730            PruneStrategy = Self,
731        >,
732{
733    type Seed = ();
734    type FinishError = diskann::error::Infallible;
735    type PruneStrategy = Self;
736    type InsertStrategy = Self;
737
738    fn insert_strategy(&self) -> Self::InsertStrategy {
739        *self
740    }
741
742    fn finish<Itr>(
743        &self,
744        _provider: &DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
745        _ctx: &Ctx,
746        _batch: &std::sync::Arc<B>,
747        _ids: Itr,
748    ) -> impl std::future::Future<Output = Result<Self::Seed, Self::FinishError>> + Send
749    where
750        Itr: ExactSizeIterator<Item = u32> + Send,
751    {
752        std::future::ready(Ok(()))
753    }
754
755    fn seeded_prune_accessor<'a>(
756        &'a self,
757        provider: &'a DefaultProvider<V, SQStore<NBITS>, D, Ctx>,
758        context: &'a Ctx,
759        _seed: &'a (),
760        capacity: usize,
761    ) -> ANNResult<
762        <Self as PruneStrategy<DefaultProvider<V, SQStore<NBITS>, D, Ctx>>>::PruneAccessor<'a>,
763    > {
764        self.prune_accessor(provider, context, capacity)
765            .into_ann_result()
766    }
767}
768
769////////////////////////////
770// SaveWith  and LoadWith //
771////////////////////////////
772
773impl<const NBITS: usize> SaveWith<AsyncIndexMetadata> for SQStore<NBITS> {
774    type Ok = usize;
775    type Error = ANNError;
776
777    async fn save_with<P>(
778        &self,
779        write_provider: &P,
780        metadata: &AsyncIndexMetadata,
781    ) -> Result<Self::Ok, Self::Error>
782    where
783        P: StorageWriteProvider,
784    {
785        let sq_storage = storage::SQStorage::new(metadata.prefix());
786        let bytes_written =
787            storage::bin::save_to_bin(self, write_provider, sq_storage.compressed_data_path())?;
788        let quantizer_bytes_written = sq_storage.save_quantizer(&self.quantizer, write_provider)?;
789        Ok(bytes_written + quantizer_bytes_written)
790    }
791}
792
793impl<const NBITS: usize> LoadWith<AsyncQuantLoadContext> for SQStore<NBITS>
794where
795    Unsigned: Representation<NBITS>,
796{
797    type Error = ANNError;
798
799    async fn load_with<P>(read_provider: &P, ctx: &AsyncQuantLoadContext) -> ANNResult<Self>
800    where
801        P: StorageReadProvider,
802    {
803        let sq_storage = storage::SQStorage::new(ctx.metadata.prefix());
804        let quantizer = sq_storage.load_quantizer(read_provider)?;
805
806        storage::bin::load_from_bin(
807            read_provider,
808            sq_storage.compressed_data_path(),
809            |num_points, _pq_bytes| {
810                Ok(SQStore::<NBITS>::new(
811                    quantizer,
812                    num_points,
813                    ctx.metric,
814                    ctx.prefetch_lookahead,
815                ))
816            },
817        )
818    }
819}
820
821/// Hook into [`storage::bin::load_from_bin`] by implementing [`storage::bin::SetData`].
822impl<const NBITS: usize> storage::bin::SetData for SQStore<NBITS>
823where
824    Unsigned: Representation<NBITS>,
825{
826    type Item = u8;
827
828    fn set_data(&mut self, i: usize, element: &[Self::Item]) -> ANNResult<()> {
829        // SAFETY: No race can happen because we have a mutable reference to `self`.
830        unsafe { self.set_quant_vector(i, element) }
831    }
832}
833
834/// Hook into [`storage::bin::save_to_bin`] by implementing [`storage::bin::GetData`].
835impl<const NBITS: usize> storage::bin::GetData for SQStore<NBITS> {
836    type Element = u8;
837    type Item<'a> = &'a [u8];
838
839    fn get_data(&self, i: usize) -> ANNResult<Self::Item<'_>> {
840        // SAFETY: We aren't full protected against races on the underlying data, but at
841        // least `&self` will keep the data alive.
842        Ok(unsafe { self.data.get_slice(i) })
843    }
844
845    /// Return the total number of points, including frozen points.
846    fn total(&self) -> usize {
847        self.data.max_vectors()
848    }
849
850    fn dim(&self) -> usize {
851        self.data.dim()
852    }
853}
854
855#[derive(Debug, Error)]
856pub enum SQError {
857    #[error("Issue with canonical layout of data: {0:?}")]
858    CanonicalLayoutError(#[from] NotCanonical),
859
860    #[error("Input contains NaN values.")]
861    InputContainsNaN(#[from] InputContainsNaN),
862
863    #[error("Input full-precision conversion error : {0}")]
864    FullPrecisionConversionErr(String),
865
866    #[error("Mean Norm is missing in the quantizer.")]
867    MeanNormMissing(#[from] MeanNormMissing),
868
869    #[error("Unsupported distance metric: {0:?}")]
870    UnsupportedDistanceMetric(Metric),
871
872    #[error("Error while loading quantizer proto struct from file: {0:?}")]
873    ProtoStorageError(#[from] crate::storage::protos::ProtoStorageError),
874
875    #[error("Error while converting proto struct to Scalar Qunatizer: {0:?}")]
876    QuantizerDecodeError(#[from] crate::storage::protos::ProtoConversionError),
877}
878
879impl From<SQError> for ANNError {
880    #[cold]
881    fn from(err: SQError) -> Self {
882        ANNError::log_sq_error(err)
883    }
884}
885
886#[cfg(test)]
887mod tests {
888    use crate::storage::VirtualStorageProvider;
889    use diskann::utils::ONE;
890    use diskann_quantization::scalar::train::ScalarQuantizationParameters;
891    use diskann_utils::views::MatrixView;
892    use diskann_vector::distance::Metric;
893    use rstest::rstest;
894
895    use super::*;
896
897    const NBITS: usize = 1;
898    const DIM: usize = 4;
899    const NPTS: usize = 5;
900    const DATA: [f32; 20] = [
901        0.286541, -0.079761, 0.373634, 0.878595, -0.131049, -0.131040, 0.883841, 0.429512,
902        -0.482576, 0.557701, -0.476350, -0.478727, 0.091383, -0.722600, -0.651460, -0.212363,
903        -0.510018, 0.158241, -0.457242, -0.711176,
904    ];
905    const V: [f32; DIM] = [DATA[0], DATA[1], DATA[2], DATA[3]];
906
907    fn make_store(metric: Metric) -> SQStore<NBITS> {
908        let quantizer = ScalarQuantizationParameters::default()
909            .train(MatrixView::try_from(&DATA, NPTS, DIM).unwrap());
910        SQStore::new(quantizer, /* capacity */ 5, metric, None)
911    }
912
913    #[test]
914    fn test_dim() {
915        let store = make_store(Metric::L2);
916        assert_eq!(store.dim(), DIM);
917    }
918
919    #[test]
920    fn test_set_and_get_vector() {
921        let store = make_store(Metric::L2);
922        store.set_vector(0, &V).unwrap();
923        store.get_vector(0).unwrap();
924        // `set` and `get` should not panic
925    }
926
927    #[test]
928    #[should_panic]
929    fn test_set_vector_wrong_dim_panic_in_debug() {
930        let store = make_store(Metric::L2);
931        let _: Result<_, SQError> = store.set_vector(0, &[1.0f32; DIM + 1]);
932    }
933
934    #[test]
935    #[should_panic]
936    fn test_get_vector_oob() {
937        let store = make_store(Metric::L2);
938        let _: Result<_, SQError> = store.get_vector(NPTS);
939    }
940
941    #[test]
942    fn test_prefetch_hint_ok() {
943        let store = make_store(Metric::L2);
944        store.prefetch_hint(NPTS - 1);
945    }
946
947    #[test]
948    #[should_panic]
949    fn test_prefetch_hint_oob() {
950        let store = make_store(Metric::L2);
951        store.prefetch_hint(NPTS);
952    }
953
954    #[test]
955    fn test_distance_computer_variants() {
956        let dc_l2 = make_store(Metric::L2).distance_computer().unwrap();
957        match dc_l2 {
958            DistanceComputer::SquaredL2(_) => {}
959            _ => panic!("expected SquaredL2 variant"),
960        }
961
962        let dc_ip = make_store(Metric::InnerProduct)
963            .distance_computer()
964            .unwrap();
965        match dc_ip {
966            DistanceComputer::InnerProduct(_) => {}
967            _ => panic!("expected InnerProduct variant"),
968        }
969
970        let dc_cosine_normalized = make_store(Metric::CosineNormalized)
971            .distance_computer()
972            .unwrap();
973        match dc_cosine_normalized {
974            DistanceComputer::CosineNormalized(_) => {}
975            _ => panic!("expected CosineNormalized variant"),
976        }
977
978        let dc_unsupported = make_store(Metric::Cosine).distance_computer().unwrap_err();
979        match dc_unsupported {
980            SQError::UnsupportedDistanceMetric(Metric::Cosine) => {}
981            _ => panic!("expected UnsupportedDistanceMetric error"),
982        }
983    }
984
985    #[rstest]
986    fn test_query_computer(
987        #[values(Metric::L2, Metric::InnerProduct, Metric::CosineNormalized)] metric: Metric,
988        #[values(false, true)] allow_rescale: bool,
989    ) {
990        let store = make_store(metric);
991        let q = [1.0_f32; DIM];
992        let result = store.query_computer(&q, allow_rescale);
993        assert!(
994            result.is_ok(),
995            "query_computer() failed for metric {:?} with allow_rescale={}",
996            metric,
997            allow_rescale
998        );
999    }
1000
1001    #[test]
1002    fn test_set_quant_vector() {
1003        let store = make_store(Metric::L2);
1004        let compressed_vec_len = CVRef::<NBITS>::canonical_bytes(DIM);
1005        let raw = vec![1u8; compressed_vec_len];
1006
1007        unsafe {
1008            store.set_quant_vector(0, &raw).unwrap();
1009        }
1010
1011        // read back the same bytes
1012        let slice = unsafe { store.data.get_slice(0) };
1013        assert_eq!(slice, raw.as_slice());
1014    }
1015
1016    #[test]
1017    #[should_panic]
1018    fn test_set_quant_vector_with_wrong_dim_panics() {
1019        let store = make_store(Metric::L2);
1020        let wrong_compressed_vec_len = CVRef::<NBITS>::canonical_bytes(DIM) + 1;
1021        let raw = vec![1u8; wrong_compressed_vec_len];
1022
1023        unsafe {
1024            store.set_quant_vector(0, &raw).unwrap();
1025        }
1026    }
1027
1028    #[rstest]
1029    fn test_distance_computer_cosine_normalized(
1030        #[values(Metric::L2, Metric::InnerProduct, Metric::CosineNormalized)] metric: Metric,
1031    ) {
1032        let store = make_store(metric);
1033        // Set two vectors
1034        let v1 = [0.1, 0.2, 0.3, 0.4];
1035        let v2 = [0.4, 0.3, 0.2, 0.1];
1036        store.set_vector(0, &v1).unwrap();
1037        store.set_vector(1, &v2).unwrap();
1038
1039        let dc = store.distance_computer().unwrap();
1040        let x = store.get_vector(0).unwrap();
1041        let y = store.get_vector(1).unwrap();
1042
1043        // This will exercise the CosineNormalized match arm
1044        let _ = dc.evaluate_similarity(x, y);
1045    }
1046
1047    #[tokio::test]
1048    async fn test_save_with_and_load_with() {
1049        let storage_provider = VirtualStorageProvider::new_memory();
1050        let store = make_store(Metric::InnerProduct);
1051
1052        // Save to our memory provider
1053        let prefix = "/test";
1054        let metadata = AsyncIndexMetadata::new(prefix.to_string());
1055        let bytes_written = store.save_with(&storage_provider, &metadata).await.unwrap();
1056        let sq_storage = storage::SQStorage::new(prefix);
1057        assert!(bytes_written > 0);
1058        assert!(storage_provider.exists(sq_storage.compressed_data_path()),);
1059        assert!(storage_provider.exists(sq_storage.quantizer_path()));
1060
1061        // Load back from the same provider
1062        let ctx = AsyncQuantLoadContext {
1063            metadata,
1064            num_frozen_points: ONE,
1065            metric: Metric::InnerProduct,
1066            prefetch_lookahead: None,
1067            is_disk_index: false,
1068            prefetch_cache_line_level: None,
1069        };
1070
1071        let loaded = SQStore::<NBITS>::load_with(&storage_provider, &ctx)
1072            .await
1073            .unwrap();
1074
1075        // verify dimension is preserved
1076        assert_eq!(loaded.dim(), store.dim());
1077        // verify the raw bytes round-trip correctly
1078        for i in 0..NPTS {
1079            let original = unsafe { store.data.get_slice(i) };
1080            let loaded = unsafe { loaded.data.get_slice(i) };
1081            assert_eq!(original, loaded);
1082        }
1083    }
1084}