Skip to main content

diskann_quantization/product/tables/
padded.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! A PQ table optimized for computing distances between quantized vectors.
7//!
8//! During similarity search index construction, it is not uncommon to compute distances
9//! among quantized elements within a table. The [`PaddedTable`] is designed to facilitate
10//! such distances.
11//!
12//! ```
13//! use diskann_quantization::{views::ChunkOffsets, product::tables};
14//! use diskann_utils::views::rowmajor::{Owned, Matrix, MatrixMut};
15//!
16//! // We're creating the following pivot table.
17//! //
18//! // | chunk 0 | chunk 1 |
19//! // |   0  0  |   1  1  | pivot 0
20//! // |   1  1  |   2  2  | pivot 1
21//! // |   2  2  |   3  3  | pivot 2
22//!
23//! let mut pivots = Owned::from_element(3, 4, 0.0f32);
24//! pivots.row_mut(0).copy_from_slice(&[0.0, 0.0, 1.0, 1.0]);
25//! pivots.row_mut(1).copy_from_slice(&[1.0, 1.0, 2.0, 2.0]);
26//! pivots.row_mut(2).copy_from_slice(&[2.0, 2.0, 3.0, 3.0]);
27//!
28//! let offsets = ChunkOffsets::new(Box::new([0, 2, 4])).unwrap();
29//!
30//! let basic = tables::BasicTable::new(pivots, offsets).unwrap();
31//! let padded = tables::PaddedTable::from_basic(basic.as_view());
32//!
33//! // Distances are provided through a v-table.
34//! let vtable = padded.vtable(tables::padded::Metric::SquaredL2);
35//!
36//! // Compute the distance between [1, 1, 1, 1] and the chunk defined by [2, 1], which
37//! // should translate to the compressed vector [2, 2, 2, 2].
38//! let distance = vtable.distance(&padded, &[1.0, 1.0, 1.0, 1.0], &[2, 1]).unwrap();
39//! assert_eq!(distance, 4.0);
40//!
41//! // Compute the distance between the two compressed vectors encoded by [0, 2] and [2, 0].
42//! let distance = vtable.self_distance(&padded, &[0, 2], &[2, 0]).unwrap();
43//! assert_eq!(distance, 16.0);
44//! ```
45
46use std::{marker::PhantomData, num::NonZeroUsize};
47
48use diskann_utils::views::rowmajor::{self, Matrix, MatrixMut};
49use diskann_vector::distance::Metric as VectorMetric;
50use diskann_wide::{
51    SIMDFloat, SIMDSumTree, SIMDVector,
52    arch::{Architecture, Dispatched3, FTarget3, Scalar, Target, dispatch_no_features},
53    lifetime::Ref,
54};
55use thiserror::Error;
56
57#[cfg(target_arch = "x86_64")]
58use diskann_wide::arch::x86_64::{V3, V4};
59
60#[cfg(target_arch = "aarch64")]
61use diskann_wide::arch::aarch64::Neon;
62
63use crate::{product::tables::BasicTableView, views::ChunkOffsets};
64
65/// A PQ table that stores pivots grouped by chunk in the following dense, row-major form:
66/// ```text
67///            | -- pivot 0 --    | -- pivot 1 --    | .... | -- pivot K-1 --    |
68///            +------------------+------------------+------+--------------------+
69///  chunk 0   | c000 c001 ... 0X | c010 c011 ... 0X | .... | c0K0 c0K1 ...  0X |
70///  chunk 1   | c100 c101 ... 0X | c110 c111 ... 0X | .... | c1K0 c1K1 ...  0X |
71///    ...     |       ...        |       ...        | .... |       ...         |
72///  chunk N-1 | cN00 cN01 ... 0X | cN10 cN11 ... 0X | .... | cNK0 cNK1 ...  0X |
73/// ```
74/// where `cCPD` is dimension `D` of pivot `P` in chunk `C`, and trailing `0X`s denote
75/// potential zero-padding to the SIMD-aligned pivot width.
76///
77/// The member `offsets` describes the number of *unpadded* dimensions of each chunk.
78///
79/// Importantly, though, the storage for each pivot is rounded up to a multiple of the
80/// runtime system's preferred SIMD width and all pivots are padded to the same length.
81/// This makes distance computations between pivots very fast when computing distances
82/// between two product-quantized vectors.
83#[derive(Debug, Clone)]
84pub struct PaddedTable {
85    /// Invariants:
86    /// * `pivots.ncols()` is at least as large as the largest chunk in [`Self::offsets`]
87    ///   and is always a multiple of `simd_width`.
88    /// * `pivots.nrows() == offsets.len() * pivots_per_chunk`.
89    pivots: rowmajor::Owned<f32>,
90    offsets: ChunkOffsets,
91    pivots_per_chunk: usize,
92    simd_width: SIMDWidth,
93    arch: RuntimeArch,
94}
95
96impl PaddedTable {
97    /// Construct a [`PaddedTable`] with the same contents as `basic`.
98    pub fn from_basic(basic: BasicTableView<'_>) -> Self {
99        let arch = RuntimeArch::new();
100        Self::from_basic_with(basic, arch)
101    }
102
103    fn from_basic_with(basic: BasicTableView<'_>, arch: RuntimeArch) -> Self {
104        let pivots = basic.view_pivots();
105        let offsets = basic.view_offsets();
106
107        let pivots_per_chunk = pivots.nrows();
108
109        // Compute the padded dimension of the pivots.
110        //
111        // There exists a corner case where `pivots` barely fits within the `isize` limit
112        // of an allocation and padding will put us beyond that threshold, but that is
113        // exceedingly unlikely for typical data.
114        let max_chunk_dim = offsets.max_chunk_dim();
115        let simd_width = arch.select_simd_width(max_chunk_dim);
116        let padded_dim = max_chunk_dim
117            .get()
118            .next_multiple_of(simd_width.as_nonzero().get());
119
120        let rows = pivots_per_chunk * offsets.len();
121        let mut padded = rowmajor::Owned::from_element(rows, padded_dim, 0.0);
122        let mut row = 0;
123
124        // Since we padded, we use a custom "copy_from_slice" that allows `dst` to shrink.
125        fn copy_from_slice_subset(dst: &mut [f32], src: &[f32]) {
126            dst[..src.len()].copy_from_slice(src)
127        }
128
129        // Copy the pivots.
130        (0..offsets.len()).for_each(|i| {
131            #[expect(clippy::expect_used, reason = "`i` should be in-bounds")]
132            let view = basic.pivots_for(i).expect("`i` should be in-bounds");
133            view.rows().for_each(|src| {
134                copy_from_slice_subset(padded.row_mut(row), src);
135                row += 1;
136            });
137        });
138
139        Self {
140            pivots: padded,
141            offsets: offsets.to_owned(),
142            pivots_per_chunk,
143            simd_width,
144            arch,
145        }
146    }
147
148    /// Return the distance [`VTable`] for the requested [`Metric`].
149    ///
150    /// Note that [`VTable`]s are generally specific to the [`PaddedTable`] that generated
151    /// them and cannot be reliably shared with different tables.
152    ///
153    /// While this is not a safety issue, incorrect [`VTable`]s may yield errors due to
154    /// mismatched SIMD widths.
155    pub fn vtable(&self, metric: Metric) -> VTable {
156        self.arch.dispatch(self.simd_width, metric)
157    }
158
159    /// Return the number of PQ centers per chunk.
160    pub fn ncenters(&self) -> usize {
161        self.pivots_per_chunk
162    }
163
164    /// Return the number of PQ chunks.
165    pub fn nchunks(&self) -> usize {
166        self.offsets.len()
167    }
168
169    /// Return the full-precision dimension expected by this table.
170    pub fn dim(&self) -> usize {
171        self.offsets.dim()
172    }
173}
174
175/// Distance metrics used by [`PaddedTable`].
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum Metric {
178    SquaredL2,
179    InnerProduct,
180    Cosine,
181}
182
183impl From<VectorMetric> for Metric {
184    fn from(metric: VectorMetric) -> Self {
185        match metric {
186            VectorMetric::L2 => Self::SquaredL2,
187            VectorMetric::InnerProduct => Self::InnerProduct,
188            VectorMetric::Cosine => Self::Cosine,
189            VectorMetric::CosineNormalized => Self::Cosine,
190        }
191    }
192}
193
194type Distance = Dispatched3<Result<f32, DistanceError>, Ref<PaddedTable>, Ref<[f32]>, Ref<[u8]>>;
195type SelfDistance =
196    Dispatched3<Result<f32, SelfDistanceError>, Ref<PaddedTable>, Ref<[u8]>, Ref<[u8]>>;
197
198/// A distance [`VTable`] for a [`PaddedTable`].
199///
200/// See: [`PaddedTable::vtable`].
201#[derive(Debug, Clone, Copy)]
202pub struct VTable {
203    distance: Distance,
204    self_distance: SelfDistance,
205}
206
207impl VTable {
208    fn new<O>(arch: O::Arch) -> Self
209    where
210        O: Op,
211    {
212        Self {
213            distance: arch.dispatch3::<
214                OpTarget<O>,
215                Result<f32, DistanceError>,
216                Ref<PaddedTable>,
217                Ref<[f32]>,
218                Ref<[u8]>,
219            >(),
220            self_distance: arch.dispatch3::<
221                OpTarget<O>,
222                Result<f32, SelfDistanceError>,
223                Ref<PaddedTable>,
224                Ref<[u8]>,
225                Ref<[u8]>,
226            >(),
227        }
228    }
229
230    /// Compute the distance between `vector` and the PQ vector encoded by `codes`.
231    ///
232    /// # Errors
233    ///
234    /// Returns an error if:
235    ///
236    /// * `vector.len() != padded.dim()`
237    /// * `codes.len() != padded.nchunks()`
238    /// * Any element in `codes` is equal to or greater than `padded.ncenters()`.
239    ///
240    /// In addition, an error may be returned if `self` was created for a different
241    /// [`PaddedTable`]. Mixing [`VTable`]s in this way is not a safety issue, but is also
242    /// not guaranteed to work.
243    #[inline]
244    pub fn distance(
245        &self,
246        padded: &PaddedTable,
247        vector: &[f32],
248        codes: &[u8],
249    ) -> Result<f32, DistanceError> {
250        (self.distance).call(padded, vector, codes)
251    }
252
253    /// Compute the distance between two compressed vectors.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if:
258    ///
259    /// * `a.len() != padded.nchunks()`
260    /// * `b.len() != padded.nchunks()`
261    /// * Any element in `a` or `b` is equal to or greater than `padded.ncenters()`.
262    ///
263    /// In addition, an error may be returned if `self` was created for a different
264    /// [`PaddedTable`]. Mixing [`VTable`]s in this way is not a safety issue, but is also
265    /// not guaranteed to work.
266    #[inline]
267    pub fn self_distance(
268        &self,
269        padded: &PaddedTable,
270        a: &[u8],
271        b: &[u8],
272    ) -> Result<f32, SelfDistanceError> {
273        (self.self_distance).call(padded, a, b)
274    }
275}
276
277//-----------------//
278// Width Selection //
279//-----------------//
280
281const FOUR: NonZeroUsize = NonZeroUsize::new(4).unwrap();
282
283#[cfg(target_arch = "x86_64")]
284const EIGHT: NonZeroUsize = NonZeroUsize::new(8).unwrap();
285
286#[derive(Debug, Clone, Copy)]
287enum SIMDWidth {
288    Four,
289    #[cfg(target_arch = "x86_64")]
290    Eight,
291}
292
293impl SIMDWidth {
294    fn as_nonzero(self) -> NonZeroUsize {
295        match self {
296            Self::Four => FOUR,
297            #[cfg(target_arch = "x86_64")]
298            Self::Eight => EIGHT,
299        }
300    }
301}
302
303/// The runtime architecture.
304#[derive(Debug, Clone, Copy)]
305enum RuntimeArch {
306    Scalar(Scalar),
307    #[cfg(target_arch = "x86_64")]
308    V3(V3),
309    #[cfg(target_arch = "aarch64")]
310    Neon(Neon),
311}
312
313impl RuntimeArch {
314    fn new() -> Self {
315        dispatch_no_features(GetArch)
316    }
317
318    #[cfg_attr(
319        not(target_arch = "x86_64"),
320        expect(
321            unused_variables,
322            reason = "the same result is returned regardless of the actual chunk size"
323        )
324    )]
325    fn select_simd_width(&self, max_chunk_size: NonZeroUsize) -> SIMDWidth {
326        match self {
327            Self::Scalar(_) => SIMDWidth::Four,
328            // Use a smaller width if available.
329            #[cfg(target_arch = "x86_64")]
330            Self::V3(_) => match max_chunk_size.get() {
331                0..=4 => SIMDWidth::Four,
332                _ => SIMDWidth::Eight,
333            },
334            #[cfg(target_arch = "aarch64")]
335            Self::Neon(_) => SIMDWidth::Four,
336        }
337    }
338
339    fn dispatch(&self, simd_width: SIMDWidth, metric: Metric) -> VTable {
340        diskann_wide::alias!(f32x4<A> = f32x4);
341        diskann_wide::alias!(f32x8<A> = f32x8);
342
343        match (*self, simd_width, metric) {
344            (Self::Scalar(a), _, Metric::SquaredL2) => VTable::new::<SquaredL2<f32x4<Scalar>>>(a),
345            (Self::Scalar(a), _, Metric::InnerProduct) => {
346                VTable::new::<InnerProduct<f32x4<Scalar>>>(a)
347            }
348            (Self::Scalar(a), _, Metric::Cosine) => VTable::new::<Cosine<f32x4<Scalar>>>(a),
349
350            // V3 //
351            #[cfg(target_arch = "x86_64")]
352            (Self::V3(a), SIMDWidth::Four, Metric::SquaredL2) => {
353                VTable::new::<SquaredL2<f32x4<V3>>>(a)
354            }
355            #[cfg(target_arch = "x86_64")]
356            (Self::V3(a), SIMDWidth::Four, Metric::InnerProduct) => {
357                VTable::new::<InnerProduct<f32x4<V3>>>(a)
358            }
359            #[cfg(target_arch = "x86_64")]
360            (Self::V3(a), SIMDWidth::Four, Metric::Cosine) => VTable::new::<Cosine<f32x4<V3>>>(a),
361
362            #[cfg(target_arch = "x86_64")]
363            (Self::V3(a), SIMDWidth::Eight, Metric::SquaredL2) => {
364                VTable::new::<SquaredL2<f32x8<V3>>>(a)
365            }
366            #[cfg(target_arch = "x86_64")]
367            (Self::V3(a), SIMDWidth::Eight, Metric::InnerProduct) => {
368                VTable::new::<InnerProduct<f32x8<V3>>>(a)
369            }
370            #[cfg(target_arch = "x86_64")]
371            (Self::V3(a), SIMDWidth::Eight, Metric::Cosine) => VTable::new::<Cosine<f32x8<V3>>>(a),
372
373            // Neon //
374            #[cfg(target_arch = "aarch64")]
375            (Self::Neon(a), _, Metric::SquaredL2) => VTable::new::<SquaredL2<f32x4<Neon>>>(a),
376            #[cfg(target_arch = "aarch64")]
377            (Self::Neon(a), _, Metric::InnerProduct) => VTable::new::<InnerProduct<f32x4<Neon>>>(a),
378            #[cfg(target_arch = "aarch64")]
379            (Self::Neon(a), _, Metric::Cosine) => VTable::new::<Cosine<f32x4<Neon>>>(a),
380        }
381    }
382}
383
384#[derive(Debug, Clone, Copy)]
385struct GetArch;
386
387impl Target<Scalar, RuntimeArch> for GetArch {
388    #[inline(always)]
389    fn run(self, arch: Scalar) -> RuntimeArch {
390        RuntimeArch::Scalar(arch)
391    }
392}
393
394#[cfg(target_arch = "x86_64")]
395impl Target<V3, RuntimeArch> for GetArch {
396    #[inline(always)]
397    fn run(self, arch: V3) -> RuntimeArch {
398        RuntimeArch::V3(arch)
399    }
400}
401
402#[cfg(target_arch = "x86_64")]
403impl Target<V4, RuntimeArch> for GetArch {
404    #[inline(always)]
405    fn run(self, arch: V4) -> RuntimeArch {
406        RuntimeArch::V3(arch.retarget())
407    }
408}
409
410#[cfg(target_arch = "aarch64")]
411impl Target<Neon, RuntimeArch> for GetArch {
412    #[inline(always)]
413    fn run(self, arch: Neon) -> RuntimeArch {
414        RuntimeArch::Neon(arch)
415    }
416}
417
418//----------//
419// SIMD Ops //
420//----------//
421
422trait Op {
423    type Arch: Architecture;
424    type Accum;
425    type Vector: SIMDVector<Scalar = f32, Arch = Self::Arch>;
426
427    fn init(arch: Self::Arch) -> Self::Accum;
428    fn accum(acc: Self::Accum, x: Self::Vector, y: Self::Vector) -> Self::Accum;
429    fn reduce_pair(a: Self::Accum, b: Self::Accum) -> f32;
430}
431
432#[derive(Debug)]
433struct SquaredL2<V>(PhantomData<V>);
434
435impl<V> Op for SquaredL2<V>
436where
437    V: SIMDFloat<Scalar = f32> + SIMDSumTree,
438    V::Arch: Architecture,
439{
440    type Arch = V::Arch;
441    type Accum = V;
442    type Vector = V;
443
444    fn init(arch: Self::Arch) -> V {
445        V::default(arch)
446    }
447
448    fn accum(acc: V, x: V, y: V) -> V {
449        let d = x - y;
450        d.mul_add_simd(d, acc)
451    }
452
453    fn reduce_pair(a: V, b: V) -> f32 {
454        (a + b).sum_tree()
455    }
456}
457
458#[derive(Debug)]
459struct InnerProduct<V>(PhantomData<V>);
460
461impl<V> Op for InnerProduct<V>
462where
463    V: SIMDFloat<Scalar = f32> + SIMDSumTree,
464    V::Arch: Architecture,
465{
466    type Arch = V::Arch;
467    type Accum = V;
468    type Vector = V;
469
470    fn init(arch: Self::Arch) -> V {
471        V::default(arch)
472    }
473
474    fn accum(acc: V, x: V, y: V) -> V {
475        x.mul_add_simd(y, acc)
476    }
477
478    fn reduce_pair(a: V, b: V) -> f32 {
479        -(a + b).sum_tree()
480    }
481}
482
483#[derive(Debug)]
484struct Cosine<V>(PhantomData<V>);
485
486#[derive(Debug)]
487struct CosineAccumulator<V> {
488    xy: V,
489    xnorm: V,
490    ynorm: V,
491}
492
493fn finish_cosine(xy: f32, xnorm: f32, ynorm: f32) -> f32 {
494    if xnorm < f32::MIN_POSITIVE || ynorm < f32::MIN_POSITIVE {
495        1.0
496    } else {
497        let v = xy / (xnorm.sqrt() * ynorm.sqrt());
498        1.0 - (-1.0f32).max(1.0f32.min(v))
499    }
500}
501
502impl<V> Op for Cosine<V>
503where
504    V: SIMDFloat<Scalar = f32> + SIMDSumTree,
505    V::Arch: Architecture,
506{
507    type Arch = V::Arch;
508    type Accum = CosineAccumulator<V>;
509    type Vector = V;
510
511    fn init(arch: Self::Arch) -> Self::Accum {
512        CosineAccumulator {
513            xy: V::default(arch),
514            xnorm: V::default(arch),
515            ynorm: V::default(arch),
516        }
517    }
518
519    fn accum(acc: Self::Accum, x: V, y: V) -> Self::Accum {
520        CosineAccumulator {
521            xy: x.mul_add_simd(y, acc.xy),
522            xnorm: x.mul_add_simd(x, acc.xnorm),
523            ynorm: y.mul_add_simd(y, acc.ynorm),
524        }
525    }
526
527    fn reduce_pair(a: Self::Accum, b: Self::Accum) -> f32 {
528        let xy = (a.xy + b.xy).sum_tree();
529        let xnorm = (a.xnorm + b.xnorm).sum_tree();
530        let ynorm = (a.ynorm + b.ynorm).sum_tree();
531        finish_cosine(xy, xnorm, ynorm)
532    }
533}
534
535#[derive(Debug, Clone, Copy)]
536struct OpTarget<O>(PhantomData<O>);
537
538impl<O> FTarget3<O::Arch, Result<f32, DistanceError>, &PaddedTable, &[f32], &[u8]> for OpTarget<O>
539where
540    O: Op,
541{
542    #[inline(always)]
543    fn run(arch: O::Arch, table: &PaddedTable, a: &[f32], b: &[u8]) -> Result<f32, DistanceError> {
544        distance::<O>(arch, table, a, b)
545    }
546}
547
548impl<O> FTarget3<O::Arch, Result<f32, SelfDistanceError>, &PaddedTable, &[u8], &[u8]>
549    for OpTarget<O>
550where
551    O: Op,
552{
553    #[inline(always)]
554    fn run(
555        arch: O::Arch,
556        table: &PaddedTable,
557        a: &[u8],
558        b: &[u8],
559    ) -> Result<f32, SelfDistanceError> {
560        self_distance::<O>(arch, table, a, b)
561    }
562}
563
564//--------------------------------//
565// Full Precision-Quant Distances //
566//--------------------------------//
567
568/// Errors for [`VTable::distance`].
569#[derive(Debug, Error)]
570#[non_exhaustive]
571pub enum DistanceError {
572    #[error("table has dimension {} but full vector has length {}", dim, alen)]
573    ALen { dim: usize, alen: usize },
574    #[error("table has {} chunks but codes slice has length {}", chunks, blen)]
575    BLen { chunks: usize, blen: usize },
576    #[error("codes have a value that exceeds the number of pivots {}", ncenters)]
577    OutOfBounds { ncenters: usize },
578    #[error("An invalid SIMD width was chosen - ensure the correct vtable is used")]
579    InvalidSimd,
580}
581
582#[inline(always)]
583fn distance<O>(
584    arch: O::Arch,
585    table: &PaddedTable,
586    a: &[f32],
587    b: &[u8],
588) -> Result<f32, DistanceError>
589where
590    O: Op,
591{
592    // Check 1
593    if !table
594        .simd_width
595        .as_nonzero()
596        .get()
597        .is_multiple_of(O::Vector::LANES)
598    {
599        return Err(DistanceError::InvalidSimd);
600    }
601
602    // Check 2
603    if a.len() != table.dim() {
604        return Err(DistanceError::ALen {
605            dim: table.dim(),
606            alen: a.len(),
607        });
608    }
609
610    // Check 3
611    if b.len() != table.nchunks() {
612        return Err(DistanceError::BLen {
613            chunks: table.nchunks(),
614            blen: b.len(),
615        });
616    }
617
618    // Check 4
619    if let Ok(ncenters) = u8::try_from(table.ncenters())
620        && let Some(max) = b.iter().max()
621        && *max >= ncenters
622    {
623        return Err(DistanceError::OutOfBounds {
624            ncenters: table.ncenters(),
625        });
626    }
627
628    // All checks passed - we're good to go!
629
630    let pivots = &table.pivots;
631
632    let pivot_stride = pivots.ncols();
633    let chunk_stride = table.pivots_per_chunk * pivot_stride;
634
635    let nchunks = b.len();
636    let mut d0 = O::init(arch);
637    let mut d1 = O::init(arch);
638
639    let mut i = 0;
640    let mut p = pivots.as_ptr();
641
642    let offsets = table.offsets.as_slice();
643    let aptr = a.as_ptr();
644
645    let lanes = O::Vector::LANES;
646
647    // Here is our conundrum. Unrolling is nice, but there is an issue if two adjacent chunks
648    // have wildly different lengths.
649    //
650    // So here, we process two chunks at a time. We first tackle the common full-width prefix.
651    // Then we process the tails independently.
652    //
653    // SAFETY INVARIANTS (SI)
654    //
655    // 1. `offsets.len() == nchunks + 1`. Offsets are strictly increasing with
656    //    `offsets[0] == 0` and `offsets[nchunks] == a.len()` by the table invariant and
657    //    Check 2.
658    //
659    // 2. At the start of each outer iteration, `p` points to the beginning of pivot storage
660    //    for chunk `i`.
661    //
662    // 3. Pivot chunk `i` begins at `pivots.as_ptr().add(i * chunk_stride)`. Within a chunk,
663    //    pivot `j` begins at `chunk_base.add(j * pivot_stride)`. Each chunk and pivot row
664    //    contains `chunk_stride` and `pivot_stride` initialized elements, respectively.
665    //
666    // 4. Each code selects an existing pivot. Check 4 establishes this when the number of
667    //    pivots fits in `u8`; otherwise every possible `u8` is less than
668    //    `pivots_per_chunk`.
669    //
670    // 5. `pivot_stride` is a multiple of the table's SIMD width, and Check 1 establishes
671    //    that width is a multiple of `lanes`. Each pivot row is at least as long as every
672    //    unpadded chunk rounded up to `lanes`.
673    while i + 2 <= nchunks {
674        // SAFETY: `i + 2 <= nchunks`, so offsets `i` through `i + 2` exist by SI-1.
675        let (o0, o1, o2) = unsafe {
676            (
677                *offsets.get_unchecked(i),
678                *offsets.get_unchecked(i + 1),
679                *offsets.get_unchecked(i + 2),
680            )
681        };
682
683        // SAFETY: `o0` and `o1` are in-bounds chunk starts in `a` by SI-1.
684        let (a0, a1) = unsafe { (aptr.add(o0), aptr.add(o1)) };
685
686        // SAFETY: By SI-(2,3,4), `p` begins chunk `i` and the code selects an in-bounds
687        // pivot row in that chunk.
688        let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
689
690        // SAFETY: `i + 1 < nchunks`; by SI-(2,3,4), this selects an in-bounds pivot row in
691        // the following chunk.
692        let b1 = unsafe { p.add(chunk_stride + pivot_stride * (*b.get_unchecked(i + 1) as usize)) };
693
694        let full0 = (o1 - o0) / lanes;
695        let full1 = (o2 - o1) / lanes;
696
697        let common = full0.min(full1);
698        for j in 0..common {
699            // SAFETY: `j < full0` and `j < full1`, so both query loads end at or before
700            // their respective chunk boundaries. By SI-(3,4,5), the corresponding pivot
701            // loads remain within their initialized padded rows.
702            unsafe {
703                let va = O::Vector::load_simd(arch, a0.add(lanes * j));
704                let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
705                d0 = O::accum(d0, va, vb);
706
707                let va = O::Vector::load_simd(arch, a1.add(lanes * j));
708                let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
709                d1 = O::accum(d1, va, vb);
710            }
711        }
712
713        // Handle whatever happens to be left of `a0`.
714        for j in common..full0 {
715            // SAFETY: `j < full0`, so the query load remains within chunk 0. By SI-(3,4,5),
716            // the corresponding pivot load remains within its padded row.
717            unsafe {
718                let va = O::Vector::load_simd(arch, a0.add(lanes * j));
719                let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
720                d0 = O::accum(d0, va, vb);
721            }
722        }
723
724        let a0_remaining = (o1 - o0) - full0 * lanes;
725        if a0_remaining != 0 {
726            // SAFETY: `full0 * lanes + a0_remaining == o1 - o0` and `a0_remaining < lanes`,
727            // so this reads exactly the initialized remainder.
728            let va =
729                unsafe { O::Vector::load_simd_first(arch, a0.add(lanes * full0), a0_remaining) };
730            // SAFETY: `(full0 + 1) * lanes` is the chunk length rounded up to `lanes`,
731            // which is at most `pivot_stride` by SI-5.
732            let vb = unsafe { O::Vector::load_simd(arch, b0.add(lanes * full0)) };
733            d0 = O::accum(d0, va, vb);
734        }
735
736        // Handle whatever happens to be left of `a1`.
737        for j in common..full1 {
738            // SAFETY: Same proof as the remaining full-vector loop for chunk 0.
739            unsafe {
740                let va = O::Vector::load_simd(arch, a1.add(lanes * j));
741                let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
742                d1 = O::accum(d1, va, vb);
743            }
744        }
745
746        let a1_remaining = (o2 - o1) - full1 * lanes;
747        if a1_remaining != 0 {
748            // SAFETY: Same remainder proof as chunk 0.
749            let va =
750                unsafe { O::Vector::load_simd_first(arch, a1.add(lanes * full1), a1_remaining) };
751            // SAFETY: Same padded-row proof as chunk 0.
752            let vb = unsafe { O::Vector::load_simd(arch, b1.add(lanes * full1)) };
753            d1 = O::accum(d1, va, vb);
754        }
755
756        i += 2;
757
758        // SAFETY: Before incrementing, `p` began chunk `i - 2` by SI-2. The loop bound
759        // established `i <= nchunks`, so advancing two chunk strides reaches chunk `i` or
760        // one-past the final chunk, preserving SI-2.
761        p = unsafe { p.add(2 * chunk_stride) };
762    }
763
764    if i < nchunks {
765        debug_assert!(i + 1 == nchunks);
766
767        // SAFETY: `i < nchunks`, so offsets `i` and `i + 1` exist by SI-1.
768        let (o0, o1) = unsafe { (*offsets.get_unchecked(i), *offsets.get_unchecked(i + 1)) };
769
770        // SAFETY: `o0` is an in-bounds chunk start in `a` by SI-1.
771        let a0 = unsafe { aptr.add(o0) };
772
773        // SAFETY: By SI-(2,3,4), this selects an in-bounds pivot row in chunk `i`.
774        let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
775
776        // The number of unprocessed elements.
777        let full = (o1 - o0) / lanes;
778        for j in 0..full {
779            // SAFETY: `j < full`, so the query load remains within the chunk. By SI-(3,4,5),
780            // the corresponding pivot load remains within its padded row.
781            unsafe {
782                let va = O::Vector::load_simd(arch, a0.add(lanes * j));
783                let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
784                d0 = O::accum(d0, va, vb);
785            }
786        }
787
788        let remaining = (o1 - o0) - full * lanes;
789        if remaining != 0 {
790            // SAFETY: `full * lanes + remaining == o1 - o0` and `remaining < lanes`, so
791            // this reads exactly the initialized remainder.
792            let va = unsafe { O::Vector::load_simd_first(arch, a0.add(lanes * full), remaining) };
793            // SAFETY: `(full + 1) * lanes` is at most `pivot_stride` by SI-5.
794            let vb = unsafe { O::Vector::load_simd(arch, b0.add(lanes * full)) };
795            d0 = O::accum(d0, va, vb);
796        }
797    }
798
799    Ok(O::reduce_pair(d0, d1))
800}
801
802//-----------------------//
803// Quant-Quant Distances //
804//-----------------------//
805
806/// Errors for [`VTable::self_distance`].
807#[derive(Debug, Error)]
808#[non_exhaustive]
809pub enum SelfDistanceError {
810    #[error("table has {} chunks but codes slice has length {}", chunks, len)]
811    Len { chunks: usize, len: usize },
812    #[error("codes have a value that exceeds the number of pivots {}", ncenters)]
813    OutOfBounds { ncenters: usize },
814    #[error("An invalid SIMD width was chosen - ensure the correct vtable is used")]
815    InvalidSimd,
816}
817
818#[inline(always)]
819fn self_distance<O>(
820    arch: O::Arch,
821    table: &PaddedTable,
822    a: &[u8],
823    b: &[u8],
824) -> Result<f32, SelfDistanceError>
825where
826    O: Op,
827{
828    // Check 1
829    if !table
830        .simd_width
831        .as_nonzero()
832        .get()
833        .is_multiple_of(O::Vector::LANES)
834    {
835        return Err(SelfDistanceError::InvalidSimd);
836    }
837
838    let chunks = table.nchunks();
839    // Check 2
840    if a.len() != chunks {
841        return Err(SelfDistanceError::Len {
842            chunks,
843            len: a.len(),
844        });
845    }
846
847    // Check 3
848    if b.len() != chunks {
849        return Err(SelfDistanceError::Len {
850            chunks,
851            len: b.len(),
852        });
853    }
854
855    // Check 4
856    if let Ok(ncenters) = u8::try_from(table.ncenters()) {
857        if let Some(max) = a.iter().max()
858            && *max >= ncenters
859        {
860            return Err(SelfDistanceError::OutOfBounds {
861                ncenters: ncenters.into(),
862            });
863        }
864
865        if let Some(max) = b.iter().max()
866            && *max >= ncenters
867        {
868            return Err(SelfDistanceError::OutOfBounds {
869                ncenters: ncenters.into(),
870            });
871        }
872    }
873
874    // All checks passed - we're good to go!
875
876    let pivots = &table.pivots;
877
878    // The number of SIMD steps to process for each pivot.
879    let steps = pivots.ncols() / O::Vector::LANES;
880
881    let pivot_stride = pivots.ncols();
882    let chunk_stride = table.pivots_per_chunk * pivot_stride;
883
884    let len = a.len();
885    let mut d0 = O::init(arch);
886    let mut d1 = O::init(arch);
887
888    let mut p = pivots.as_ptr();
889
890    let lanes = O::Vector::LANES;
891    let mut i = 0;
892
893    // SAFETY INVARIANTS (SI)
894    //
895    // 1. Both code slices contain exactly `len == table.nchunks()` entries by Checks 2
896    //    and 3.
897    //
898    // 2. At the start of each outer iteration, `p` points to the beginning of pivot storage
899    //    for chunk `i`.
900    //
901    // 3. Pivot chunk `i` begins at `pivots.as_ptr().add(i * chunk_stride)`. Within a chunk,
902    //    pivot `j` begins at `chunk_base.add(j * pivot_stride)`. Each chunk and pivot row
903    //    contains `chunk_stride` and `pivot_stride` initialized elements, respectively.
904    //
905    // 4. Every code selects an existing pivot. Check 4 establishes this when the number of
906    //    pivots fits in `u8`; otherwise every possible `u8` is in bounds.
907    //
908    // 5. `pivot_stride` is a multiple of the table's SIMD width, and Check 1 establishes
909    //    that width is a multiple of `lanes`. Therefore
910    //    `steps * lanes == pivot_stride`.
911    while i + 2 <= len {
912        // SAFETY: `i + 2 <= len`; by SI-(1,2,3,4), `p` begins chunk `i` and this code
913        // selects an in-bounds pivot row in that chunk.
914        let a0 = unsafe { p.add(pivot_stride * (*a.get_unchecked(i) as usize)) };
915
916        // SAFETY: By the same invariants, this selects an in-bounds pivot row in chunk
917        // `i + 1`.
918        let a1 = unsafe { p.add(chunk_stride + pivot_stride * (*a.get_unchecked(i + 1) as usize)) };
919
920        // SAFETY: Same proof as `a0`; SI-1 establishes that `b[i]` exists.
921        let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
922        // SAFETY: Same proof as `a1`.
923        let b1 = unsafe { p.add(chunk_stride + pivot_stride * (*b.get_unchecked(i + 1) as usize)) };
924
925        for j in 0..steps {
926            // SAFETY: `j < steps` and `steps * lanes == pivot_stride` by SI-5, so every
927            // full-vector load remains within its selected initialized pivot row.
928            unsafe {
929                // Unroll 0
930                let va = O::Vector::load_simd(arch, a0.add(lanes * j));
931                let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
932                d0 = O::accum(d0, va, vb);
933
934                // Unroll 1
935                let va = O::Vector::load_simd(arch, a1.add(lanes * j));
936                let vb = O::Vector::load_simd(arch, b1.add(lanes * j));
937                d1 = O::accum(d1, va, vb);
938            }
939        }
940
941        i += 2;
942
943        // SAFETY: Before incrementing, `p` began chunk `i - 2` by SI-2. The loop bound
944        // established `i <= len`, so advancing two chunk strides reaches chunk `i` or
945        // one-past the final chunk, preserving SI-2.
946        p = unsafe { p.add(2 * chunk_stride) };
947    }
948
949    if i < len {
950        debug_assert!(i + 1 == len);
951
952        // SAFETY: By SI-(1,2,3,4), `p` begins chunk `i` and this selects an in-bounds
953        // pivot row.
954        let a0 = unsafe { p.add(pivot_stride * (*a.get_unchecked(i) as usize)) };
955        // SAFETY: Same proof for `b[i]`.
956        let b0 = unsafe { p.add(pivot_stride * (*b.get_unchecked(i) as usize)) };
957
958        for j in 0..steps {
959            // SAFETY: `j < steps`, so both loads remain within their initialized pivot rows
960            // by SI-5.
961            unsafe {
962                let va = O::Vector::load_simd(arch, a0.add(lanes * j));
963                let vb = O::Vector::load_simd(arch, b0.add(lanes * j));
964                d0 = O::accum(d0, va, vb);
965            }
966        }
967    }
968
969    Ok(O::reduce_pair(d0, d1))
970}
971
972///////////
973// Tests //
974///////////
975
976#[cfg(test)]
977mod tests {
978    use super::*;
979
980    use diskann_utils::assert_contains;
981
982    use crate::{
983        product::tables::test::{self as table_test, DistanceTestTable, QueryLike, SelfLike},
984        test_util::Check,
985    };
986
987    fn cases() -> &'static [Case] {
988        const CASES: &[Case] = &[
989            // chunk dimensions, pivots, start
990            Case::new(&[1], 1, 0.0, Check::exact()),
991            Case::new(&[1, 1, 1], 2, -1.0, Check::exact()),
992            Case::new(&[3, 2, 2], 15, -7.0, Check::exact()),
993            Case::new(&[2, 2, 2, 2], 16, -8.0, Check::exact()),
994            Case::new(&[3, 3, 3, 2, 2], 17, -11.0, Check::exact()),
995            Case::new(&[8, 8, 8, 8], 32, -16.0, Check::exact()),
996            Case::new(&[6, 6, 5, 5, 5, 5, 5], 33, -20.0, Check::exact()),
997            Case::new(&[13, 12, 12], 33, -20.0, Check::exact()),
998            Case::new(&[4, 4, 3, 3, 3], 256, -128.0, Check::exact()),
999            // Exercise sharply imbalanced chunks in either position of an unrolled pair,
1000            // followed by an odd chunk that must use the remainder loop.
1001            Case::new(&[1, 23, 23], 17, -9.0, Check::exact()),
1002            Case::new(&[23, 1, 23], 17, -9.0, Check::exact()),
1003            Case::new(&[1, 17, 2, 9, 3], 17, -9.0, Check::exact()),
1004        ];
1005        CASES
1006    }
1007
1008    #[derive(Debug, Clone, Copy)]
1009    struct Case {
1010        chunk_dims: &'static [usize],
1011        pivots: usize,
1012        start: f32,
1013        check: Check,
1014    }
1015
1016    impl Case {
1017        const fn new(
1018            chunk_dims: &'static [usize],
1019            pivots: usize,
1020            start: f32,
1021            check: Check,
1022        ) -> Self {
1023            Self {
1024                chunk_dims,
1025                pivots,
1026                start,
1027                check,
1028            }
1029        }
1030    }
1031
1032    fn run_test(
1033        cases: &[Case],
1034        metric: Metric,
1035        arch: Option<RuntimeArch>,
1036        reference: &dyn Fn(&[f32], &[f32]) -> f32,
1037        ctx: &dyn std::fmt::Display,
1038    ) {
1039        let (num_queries, num_trials) = if cfg!(miri) {
1040            // The driver will run some directed tests even if there are no regular random
1041            // trials.
1042            (1, 0)
1043        } else {
1044            (10, 10)
1045        };
1046
1047        #[derive(Debug)]
1048        struct Dut<'a> {
1049            table: &'a PaddedTable,
1050            query: Vec<f32>,
1051            vtable: VTable,
1052        }
1053
1054        impl QueryLike for Dut<'_> {
1055            fn preprocess(&mut self, query: &[f32]) {
1056                self.query.clear();
1057                self.query.extend_from_slice(query);
1058            }
1059
1060            fn evaluate(&mut self, code: &[u8]) -> f32 {
1061                self.vtable.distance(self.table, &self.query, code).unwrap()
1062            }
1063        }
1064
1065        impl SelfLike for Dut<'_> {
1066            fn evaluate(&mut self, a: &[u8], b: &[u8]) -> f32 {
1067                self.vtable.self_distance(self.table, a, b).unwrap()
1068            }
1069        }
1070
1071        for Case {
1072            chunk_dims,
1073            pivots,
1074            start,
1075            check,
1076        } in cases.iter().copied()
1077        {
1078            let driver = DistanceTestTable::from_chunk_dims(chunk_dims, pivots, start);
1079            let dim = driver.dim();
1080            let chunks = driver.chunks();
1081            let basic = driver.basic_table();
1082
1083            let table = match arch {
1084                Some(arch) => PaddedTable::from_basic_with(basic.as_view(), arch),
1085                None => PaddedTable::from_basic(basic.as_view()),
1086            };
1087
1088            let vtable = table.vtable(metric);
1089
1090            let mut dut = Dut {
1091                table: &table,
1092                query: Vec::new(),
1093                vtable,
1094            };
1095
1096            driver.drive_query_like(
1097                num_queries,
1098                num_trials,
1099                &mut driver.rng(0xc0ffee),
1100                check,
1101                reference,
1102                &mut dut,
1103                format_args!(
1104                    "[{}] padded table - dim = {}, chunks = {}, pivots = {}",
1105                    ctx, dim, chunks, pivots
1106                ),
1107            );
1108
1109            driver.drive_self_like(
1110                num_trials,
1111                &mut driver.rng(0xc0ffee),
1112                check,
1113                reference,
1114                &mut dut,
1115                format_args!(
1116                    "[{}] padded table - dim = {}, chunks = {}, pivots = {}",
1117                    ctx, dim, chunks, pivots
1118                ),
1119            );
1120        }
1121    }
1122
1123    // L2 - query-like
1124    #[test]
1125    fn test_l2_query_like() {
1126        run_test(
1127            cases(),
1128            Metric::SquaredL2,
1129            None,
1130            &table_test::squared_l2,
1131            &"squared l2 full x quant - auto-detect",
1132        );
1133
1134        run_test(
1135            cases(),
1136            Metric::SquaredL2,
1137            Some(RuntimeArch::Scalar(Scalar::new())),
1138            &table_test::squared_l2,
1139            &"squared l2 full x quant - scalar",
1140        );
1141
1142        #[cfg(target_arch = "x86_64")]
1143        if let Some(arch) = V3::new_checked() {
1144            run_test(
1145                cases(),
1146                Metric::SquaredL2,
1147                Some(RuntimeArch::V3(arch)),
1148                &table_test::squared_l2,
1149                &"squared l2 full x quant - V3",
1150            );
1151        }
1152
1153        #[cfg(target_arch = "aarch64")]
1154        if let Some(arch) = Neon::new_checked() {
1155            run_test(
1156                cases(),
1157                Metric::SquaredL2,
1158                Some(RuntimeArch::Neon(arch)),
1159                &table_test::squared_l2,
1160                &"squared l2 full x quant - V3",
1161            );
1162        }
1163    }
1164
1165    // Inner Product - query-like
1166    #[test]
1167    fn test_inner_product_query_like() {
1168        run_test(
1169            cases(),
1170            Metric::InnerProduct,
1171            None,
1172            &table_test::inner_product,
1173            &"inner-product full x quant - auto-detect",
1174        );
1175
1176        run_test(
1177            cases(),
1178            Metric::InnerProduct,
1179            Some(RuntimeArch::Scalar(Scalar::new())),
1180            &table_test::inner_product,
1181            &"inner-product full x quant - scalar",
1182        );
1183
1184        #[cfg(target_arch = "x86_64")]
1185        if let Some(arch) = V3::new_checked() {
1186            run_test(
1187                cases(),
1188                Metric::InnerProduct,
1189                Some(RuntimeArch::V3(arch)),
1190                &table_test::inner_product,
1191                &"inner-product full x quant - V3",
1192            );
1193        }
1194
1195        #[cfg(target_arch = "aarch64")]
1196        if let Some(arch) = Neon::new_checked() {
1197            run_test(
1198                cases(),
1199                Metric::InnerProduct,
1200                Some(RuntimeArch::Neon(arch)),
1201                &table_test::inner_product,
1202                &"inner-product full x quant - V3",
1203            );
1204        }
1205    }
1206
1207    // Cosine - query-like
1208    #[test]
1209    fn test_cosine_query_like() {
1210        run_test(
1211            cases(),
1212            Metric::Cosine,
1213            None,
1214            &table_test::cosine,
1215            &"cosine full x quant - auto-detect",
1216        );
1217
1218        run_test(
1219            cases(),
1220            Metric::Cosine,
1221            Some(RuntimeArch::Scalar(Scalar::new())),
1222            &table_test::cosine,
1223            &"cosine full x quant - scalar",
1224        );
1225
1226        #[cfg(target_arch = "x86_64")]
1227        if let Some(arch) = V3::new_checked() {
1228            run_test(
1229                cases(),
1230                Metric::Cosine,
1231                Some(RuntimeArch::V3(arch)),
1232                &table_test::cosine,
1233                &"cosine full x quant - V3",
1234            );
1235        }
1236
1237        #[cfg(target_arch = "aarch64")]
1238        if let Some(arch) = Neon::new_checked() {
1239            run_test(
1240                cases(),
1241                Metric::Cosine,
1242                Some(RuntimeArch::Neon(arch)),
1243                &table_test::cosine,
1244                &"cosine full x quant - V3",
1245            );
1246        }
1247    }
1248
1249    ////////////
1250    // Errors //
1251    ////////////
1252
1253    /// A table with dimension 7, 3 chunks, and 3 pivots per chunk.
1254    fn error_table() -> (PaddedTable, VTable) {
1255        let driver = DistanceTestTable::new(7, 3, 3, 0.0);
1256        let basic = driver.basic_table();
1257        let table =
1258            PaddedTable::from_basic_with(basic.as_view(), RuntimeArch::Scalar(Scalar::new()));
1259        let vtable = table.vtable(Metric::SquaredL2);
1260        (table, vtable)
1261    }
1262
1263    #[test]
1264    fn test_distance_errors() {
1265        let (table, vtable) = error_table();
1266        let vector = vec![0.0; table.dim()];
1267        let codes = vec![0; table.nchunks()];
1268
1269        for len in [table.dim() - 1, table.dim() + 1] {
1270            let invalid = vec![0.0; len];
1271            let err = vtable.distance(&table, &invalid, &codes).unwrap_err();
1272            assert_contains!(
1273                err.to_string(),
1274                format!(
1275                    "table has dimension {} but full vector has length {len}",
1276                    table.dim()
1277                )
1278            );
1279        }
1280
1281        for len in [table.nchunks() - 1, table.nchunks() + 1] {
1282            let invalid = vec![0; len];
1283            let err = vtable.distance(&table, &vector, &invalid).unwrap_err();
1284            assert_contains!(
1285                err.to_string(),
1286                format!(
1287                    "table has {} chunks but codes slice has length {len}",
1288                    table.nchunks()
1289                )
1290            );
1291        }
1292
1293        let mut invalid = codes;
1294        invalid[1] = u8::try_from(table.ncenters()).unwrap();
1295        let err = vtable.distance(&table, &vector, &invalid).unwrap_err();
1296        assert_contains!(
1297            err.to_string(),
1298            format!(
1299                "codes have a value that exceeds the number of pivots {}",
1300                table.ncenters()
1301            )
1302        );
1303    }
1304
1305    #[test]
1306    fn test_self_distance_errors() {
1307        let (table, vtable) = error_table();
1308        let codes = vec![0; table.nchunks()];
1309
1310        for len in [table.nchunks() - 1, table.nchunks() + 1] {
1311            let invalid = vec![0; len];
1312
1313            let err = vtable.self_distance(&table, &invalid, &codes).unwrap_err();
1314            assert_contains!(
1315                err.to_string(),
1316                format!(
1317                    "table has {} chunks but codes slice has length {len}",
1318                    table.nchunks()
1319                )
1320            );
1321
1322            let err = vtable.self_distance(&table, &codes, &invalid).unwrap_err();
1323            assert_contains!(
1324                err.to_string(),
1325                format!(
1326                    "table has {} chunks but codes slice has length {len}",
1327                    table.nchunks()
1328                )
1329            );
1330        }
1331
1332        for invalid_operand in 0..2 {
1333            let mut a = codes.clone();
1334            let mut b = codes.clone();
1335            let invalid = if invalid_operand == 0 { &mut a } else { &mut b };
1336            invalid[1] = u8::try_from(table.ncenters()).unwrap();
1337
1338            let err = vtable.self_distance(&table, &a, &b).unwrap_err();
1339            assert_contains!(
1340                err.to_string(),
1341                format!(
1342                    "codes have a value that exceeds the number of pivots {}",
1343                    table.ncenters()
1344                )
1345            );
1346        }
1347    }
1348
1349    #[test]
1350    fn test_mismatched_simd_width_errors() {
1351        diskann_wide::alias!(f32x8<A> = f32x8);
1352
1353        let (table, _) = error_table();
1354        let invalid_vtable = VTable::new::<SquaredL2<f32x8<Scalar>>>(Scalar::new());
1355        let vector = vec![0.0; table.dim()];
1356        let codes = vec![0; table.nchunks()];
1357
1358        let err = invalid_vtable
1359            .distance(&table, &vector, &codes)
1360            .unwrap_err();
1361        assert_contains!(err.to_string(), "An invalid SIMD width was chosen");
1362
1363        let err = invalid_vtable
1364            .self_distance(&table, &codes, &codes)
1365            .unwrap_err();
1366        assert_contains!(err.to_string(), "An invalid SIMD width was chosen");
1367    }
1368}