Skip to main content

diskann_quantization/bits/
distances.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! # Low-level functions
7//!
8//! The methods here are meant to be primitives used by the distance functions for the
9//! various scalar-quantized-like quantizers.
10//!
11//! As such, they typically return integer distance results since they largely operate over
12//! raw bit-slices.
13//!
14//! ## Micro-architecture Mapping
15//!
16//! There are two interfaces for interacting with the distance primitives:
17//!
18//! * [`diskann_wide::arch::Target2`]: A micro-architecture aware interface where the target
19//!   micro-architecture is provided as an explicit argument.
20//!
21//!   This can be used in conjunction with [`diskann_wide::Architecture::run2`] to apply the
22//!   necessary target-features to opt-into newer architecture code generation when
23//!   compiling the whole binary for an older architecture.
24//!
25//!   This interface is also composable with micro-architecture dispatching done higher in
26//!   the callstack, and so should be preferred when incorporating into quantizer distance
27//!   computations.
28//!
29//! * [`diskann_vector::PureDistanceFunction`]: If micro-architecture awareness is not needed,
30//!   this provides a simple interface targeting [`diskann_wide::ARCH`] (the current compilation
31//!   architecture).
32//!
33//!   This interface will always yield a binary compatible with the compilation architecture
34//!   target, but will not enable faster code-paths when compiling for older architectures.
35//!
36//! The following table summarizes the implementation status of kernels. All kernels have
37//! `diskann_wide::arch::Scalar` implementation fallbacks.
38//!
39//! Implementation Kind:
40//!
41//! * "Fallback": A fallback implementation using scalar indexing.
42//!
43//! * "Optimized": A better implementation than "fallback" that does not contain
44//!   target-depeendent code, instead relying on compiler optimizations.
45//!
46//!   Micro-architecture dispatch is still relevant as it allows the compiler to generate
47//!   better code for newer machines.
48//!
49//! * "Yes": Architecture specific SIMD implementation exists.
50//!
51//! * "No": Architecture specific implementation does not exist - the next most-specific
52//!   implementation is used. For example, if a `x86-64-v3` implementation does not exist,
53//!   then the "scalar" implementation will be used instead.
54//!
55//! Type Aliases
56//!
57//! * `USlice<N>`: `BitSlice<N, Unsigned, Dense>`
58//! * `TSlice<N>`: `BitSlice<N, Unsigned, BitTranspose>`
59//! * `BSlice`: `BitSlice<1, Binary, Dense>`
60//!
61//! * `MV<T>`: [`diskann_vector::MathematicalValue<T>`]
62//!
63//! ### Inner Product
64//!
65//! | LHS           | RHS           | Result    | Scalar    | x86-64-v3     | x86-64-v4 | Neon      |
66//! |---------------|---------------|-----------|-----------|---------------|-----------|-----------|
67//! | `USlice<1>`   | `USlice<1>`   | `MV<u32>` | Optimized | Optimized     | Uses V3   | Optimized |
68//! | `USlice<2>`   | `USlice<2>`   | `MV<u32>` | Fallback  | Yes           | Yes       | Fallback  |
69//! | `USlice<3>`   | `USlice<3>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
70//! | `USlice<4>`   | `USlice<4>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
71//! | `USlice<5>`   | `USlice<5>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
72//! | `USlice<6>`   | `USlice<6>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
73//! | `USlice<7>`   | `USlice<7>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
74//! | `USlice<8>`   | `USlice<8>`   | `MV<u32>` | Yes       | Yes           | Yes       | Fallback  |
75//! |               |               |           |           |               |           |           |
76//! | `USlice<8>`   | `USlice<4>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
77//! | `USlice<8>`   | `USlice<2>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
78//! | `USlice<8>`   | `USlice<1>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
79//! |               |               | `       ` |           |               |           |           |
80//! | `TSlice<4>`   | `USlice<1>`   | `MV<u32>` | Optimized | Optimized     | Optimized | Optimized |
81//! |               |               | `       ` |           |               |           |           |
82//! | `&[f32]`      | `USlice<1>`   | `MV<f32>` | Fallback  | Yes           | Uses V3   | Fallback  |
83//! | `&[f32]`      | `USlice<2>`   | `MV<f32>` | Fallback  | Yes           | Uses V3   | Fallback  |
84//! | `&[f32]`      | `USlice<3>`   | `MV<f32>` | Fallback  | No            | Uses V3   | Fallback  |
85//! | `&[f32]`      | `USlice<4>`   | `MV<f32>` | Fallback  | Yes           | Uses V3   | Fallback  |
86//! | `&[f32]`      | `USlice<5>`   | `MV<f32>` | Fallback  | No            | Uses V3   | Fallback  |
87//! | `&[f32]`      | `USlice<6>`   | `MV<f32>` | Fallback  | No            | Uses V3   | Fallback  |
88//! | `&[f32]`      | `USlice<7>`   | `MV<f32>` | Fallback  | No            | Uses V3   | Fallback  |
89//! | `&[f32]`      | `USlice<8>`   | `MV<f32>` | Fallback  | No            | Uses V3   | Fallback  |
90//!
91//! ### Squared L2
92//!
93//! | LHS           | RHS           | Result    | Scalar    | x86-64-v3     | x86-64-v4 | Neon      |
94//! |---------------|---------------|-----------|-----------|---------------|-----------|-----------|
95//! | `USlice<1>`   | `USlice<1>`   | `MV<u32>` | Optimized | Optimized     | Uses V3   | Optimized |
96//! | `USlice<2>`   | `USlice<2>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
97//! | `USlice<3>`   | `USlice<3>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
98//! | `USlice<4>`   | `USlice<4>`   | `MV<u32>` | Fallback  | Yes           | Uses V3   | Fallback  |
99//! | `USlice<5>`   | `USlice<5>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
100//! | `USlice<6>`   | `USlice<6>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
101//! | `USlice<7>`   | `USlice<7>`   | `MV<u32>` | Fallback  | No            | Uses V3   | Fallback  |
102//! | `USlice<8>`   | `USlice<8>`   | `MV<u32>` | Yes       | Yes           | Yes       | Fallback  |
103//!
104//! ### Hamming
105//!
106//! | LHS           | RHS           | Result    | Scalar    | x86-64-v3     | x86-64-v4 | Neon      |
107//! |---------------|---------------|-----------|-----------|---------------|-----------|-----------|
108//! | `BSlice`      | `BSlice`      | `MV<u32>` | Optimized | Optimized     | Uses V3   | Optimized |
109
110use diskann_vector::PureDistanceFunction;
111use diskann_wide::{ARCH, Architecture, arch::Target2};
112#[cfg(target_arch = "x86_64")]
113use diskann_wide::{
114    SIMDCast, SIMDDotProduct, SIMDMulAdd, SIMDReinterpret, SIMDSumTree, SIMDVector,
115};
116
117use super::{Binary, BitSlice, BitTranspose, Dense, Representation, Unsigned};
118use crate::distances::{Hamming, InnerProduct, MV, MathematicalResult, SquaredL2, check_lengths};
119
120// Convenience alias.
121type USlice<'a, const N: usize, Perm = Dense> = BitSlice<'a, N, Unsigned, Perm>;
122
123/// Retarget the architectures via the `retarget` inherent method.
124///
125/// * [`diskann_wide::arch::x86_64::V3`] -> [`diskann_wide::arch::Scalar`]
126/// * [`diskann_wide::arch::x86_64::V4`] -> [`diskann_wide::arch::x86_64::V3`]
127/// * [`diskann_wide::arch::aarch64::Neon`] -> [`diskann_wide::arch::Scalar`]
128macro_rules! retarget {
129    ($arch:path, $op:ty, ($N:literal, $M:literal)) => {
130        impl Target2<
131            $arch,
132            MathematicalResult<u32>,
133            USlice<'_, $N>,
134            USlice<'_, $M>,
135        > for $op {
136            #[inline(always)]
137            fn run(
138                self,
139                arch: $arch,
140                x: USlice<'_, $N>,
141                y: USlice<'_, $M>
142            ) -> MathematicalResult<u32> {
143                self.run(arch.retarget(), x, y)
144            }
145        }
146    };
147    ($arch:path, $op:ty, $N:literal) => {
148        retarget!($arch, $op, ($N, $N));
149    };
150    ($arch:path, $op:ty, $($args:tt),+ $(,)?) => {
151        $(retarget!($arch, $op, $args);)+
152    };
153}
154
155/// Impledment [`diskann_vector::PureDistanceFunction`] using the current compilation architecture
156macro_rules! dispatch_pure {
157    ($op:ty, ($N:literal, $M:literal)) => {
158        impl PureDistanceFunction<USlice<'_, $N>, USlice<'_, $M>, MathematicalResult<u32>> for $op {
159            #[inline(always)]
160            fn evaluate(x: USlice<'_, $N>, y: USlice<'_, $M>) -> MathematicalResult<u32> {
161                (diskann_wide::ARCH).run2(Self, x, y)
162            }
163        }
164    };
165    ($op:ty, $N:literal) => {
166        dispatch_pure!($op, ($N, $N));
167    };
168    ($op:ty, $($args:tt),+ $(,)?) => {
169        $(dispatch_pure!($op, $args);)+
170    }
171}
172
173/// Load 1 byte beginning at `ptr` and invoke `f` with that byte.
174///
175/// # Safety
176///
177/// * The memory range `[ptr, ptr + 1)` (in bytes) must be dereferencable.
178/// * `ptr` does not need to be aligned.
179#[cfg(target_arch = "x86_64")]
180unsafe fn load_one<F, R>(ptr: *const u32, mut f: F) -> R
181where
182    F: FnMut(u32) -> R,
183{
184    // SAFETY: Caller asserts that one byte is readable.
185    f(unsafe { ptr.cast::<u8>().read_unaligned() }.into())
186}
187
188/// Load 2 bytes beginning at `ptr` and invoke `f` with the value.
189///
190/// # Safety
191///
192/// * The memory range `[ptr, ptr + 2)` (in bytes) must be dereferencable.
193/// * `ptr` does not need to be aligned.
194#[cfg(target_arch = "x86_64")]
195unsafe fn load_two<F, R>(ptr: *const u32, mut f: F) -> R
196where
197    F: FnMut(u32) -> R,
198{
199    // SAFETY: Caller asserts that two bytes are readable.
200    f(unsafe { ptr.cast::<u16>().read_unaligned() }.into())
201}
202
203/// Load 3 bytes beginning at `ptr` and invoke `f` with the value.
204///
205/// # Safety
206///
207/// * The memory range `[ptr, ptr + 3)` (in bytes) must be dereferencable.
208/// * `ptr` does not need to be aligned.
209#[cfg(target_arch = "x86_64")]
210unsafe fn load_three<F, R>(ptr: *const u32, mut f: F) -> R
211where
212    F: FnMut(u32) -> R,
213{
214    // SAFETY: Caller asserts that three bytes are readable. This loads the first two.
215    let lo: u32 = unsafe { ptr.cast::<u16>().read_unaligned() }.into();
216    // SAFETY: Caller asserts that three bytes are readable. This loads the third.
217    let hi: u32 = unsafe { ptr.cast::<u8>().add(2).read_unaligned() }.into();
218    f(lo | hi << 16)
219}
220
221/// Load 4 bytes beginning at `ptr` and invoke `f` with the value.
222///
223/// # Safety
224///
225/// * The memory range `[ptr, ptr + 4)` (in bytes) must be dereferencable.
226/// * `ptr` does not need to be aligned.
227#[cfg(target_arch = "x86_64")]
228unsafe fn load_four<F, R>(ptr: *const u32, mut f: F) -> R
229where
230    F: FnMut(u32) -> R,
231{
232    // SAFETY: Caller asserts that four bytes are readable.
233    f(unsafe { ptr.read_unaligned() })
234}
235
236////////////////////////////
237// Distances on BitSlices //
238////////////////////////////
239
240/// Operations to apply to 1-bit encodings.
241///
242/// The general structure of 1-bit vector operations is the same, but the element wise
243/// operator is different. This trait encapsulates the differences in behavior required
244/// for different distance function.
245///
246/// The exact operations to apply depending on the representation of the bit encoding.
247trait BitVectorOp<Repr>
248where
249    Repr: Representation<1>,
250{
251    /// Apply the op to all bits in the 64-bit arguments.
252    fn on_u64(x: u64, y: u64) -> u32;
253
254    /// Apply the op to all bits in the 8-bit arguments.
255    ///
256    /// NOTE: Implementations must have the correct behavior when the upper bits of `x`
257    /// and `y` are set to 0 when handling epilogues.
258    fn on_u8(x: u8, y: u8) -> u32;
259}
260
261/// Computing Squared-L2 amounts to evaluating the pop-count of a bitwise `xor`.
262impl BitVectorOp<Unsigned> for SquaredL2 {
263    #[inline(always)]
264    fn on_u64(x: u64, y: u64) -> u32 {
265        (x ^ y).count_ones()
266    }
267    #[inline(always)]
268    fn on_u8(x: u8, y: u8) -> u32 {
269        (x ^ y).count_ones()
270    }
271}
272
273/// Computing Squared-L2 amounts to evaluating the pop-count of a bitwise `xor`.
274impl BitVectorOp<Binary> for Hamming {
275    #[inline(always)]
276    fn on_u64(x: u64, y: u64) -> u32 {
277        (x ^ y).count_ones()
278    }
279    #[inline(always)]
280    fn on_u8(x: u8, y: u8) -> u32 {
281        (x ^ y).count_ones()
282    }
283}
284
285/// The implementation as `and` is not straight-forward.
286///
287/// Recall that scalar quantization encodings are unsigned, so "0" is zero and "1" is some
288/// non-zero value.
289///
290/// When computing the inner product, `0 * x == 0` for all `x` and only `x * x` has a
291/// non-zero value. Therefore, the elementwise op is an `and` and not `xnor`.
292impl BitVectorOp<Unsigned> for InnerProduct {
293    #[inline(always)]
294    fn on_u64(x: u64, y: u64) -> u32 {
295        (x & y).count_ones()
296    }
297    #[inline(always)]
298    fn on_u8(x: u8, y: u8) -> u32 {
299        (x & y).count_ones()
300    }
301}
302
303/// A general algorithm for applying a bitwise operand to two dense bit vectors of equal
304/// but arbitrary length.
305///
306/// NOTE: The `inline(always)` attribute is required to inheret the caller's target-features.
307#[inline(always)]
308fn bitvector_op<Op, Repr>(
309    x: BitSlice<'_, 1, Repr>,
310    y: BitSlice<'_, 1, Repr>,
311) -> MathematicalResult<u32>
312where
313    Repr: Representation<1>,
314    Op: BitVectorOp<Repr>,
315{
316    let len = check_lengths!(x, y)?;
317
318    let px: *const u64 = x.as_ptr().cast();
319    let py: *const u64 = y.as_ptr().cast();
320
321    let mut i = 0;
322    let mut s: u32 = 0;
323
324    // Work in groups of 64
325    let blocks = len / 64;
326    while i < blocks {
327        // SAFETY: We know at least 64-bits (8-bytes) are valid from this offset (by
328        // guarantee of the `BitSlice`). All bit-patterns of a `u64` are valid, `u64: Copy`,
329        // and an `unaligned` read is used.
330        let vx = unsafe { px.add(i).read_unaligned() };
331
332        // SAFETY: The same logic applies to `y` because:
333        // 1. It has the same type as `x`.
334        // 2. We've verified that it has the same length as `x`.
335        let vy = unsafe { py.add(i).read_unaligned() };
336
337        s += Op::on_u64(vx, vy);
338        i += 1;
339    }
340
341    // Work in groups of 8
342    i *= 8;
343    let px: *const u8 = x.as_ptr();
344    let py: *const u8 = y.as_ptr();
345
346    let blocks = len / 8;
347    while i < blocks {
348        // SAFETY: The underlying pointer is a `*const u8` and we have checked that this
349        // offset is within the bounds of the slice underlying the bitslice.
350        let vx = unsafe { px.add(i).read_unaligned() };
351
352        // SAFETY: The same logic applies to `y` because:
353        // 1. It has the same type as `x`.
354        // 2. We've verified that it has the same length as `x`.
355        let vy = unsafe { py.add(i).read_unaligned() };
356        s += Op::on_u8(vx, vy);
357        i += 1;
358    }
359
360    if i * 8 != len {
361        // SAFETY: The underlying slice is readable in the range
362        // `[px, px + floor(len / 8) + 1)`. This accesses `px + floor(len / 8)`.
363        let vx = unsafe { px.add(i).read_unaligned() };
364
365        // SAFETY: Same as above.
366        let vy = unsafe { py.add(i).read_unaligned() };
367        let m = (0x01u8 << (len - 8 * i)) - 1;
368
369        s += Op::on_u8(vx & m, vy & m)
370    }
371    Ok(MV::new(s))
372}
373
374/// Compute the hamming distance between `x` and `y`.
375///
376/// Returns an error if the arguments have different lengths.
377impl PureDistanceFunction<BitSlice<'_, 1, Binary>, BitSlice<'_, 1, Binary>, MathematicalResult<u32>>
378    for Hamming
379{
380    fn evaluate(x: BitSlice<'_, 1, Binary>, y: BitSlice<'_, 1, Binary>) -> MathematicalResult<u32> {
381        bitvector_op::<Hamming, Binary>(x, y)
382    }
383}
384
385///////////////
386// SquaredL2 //
387///////////////
388
389/// Compute the squared L2 distance between `x` and `y`.
390///
391/// Returns an error if the arguments have different lengths.
392///
393/// # Implementation Notes
394///
395/// This can directly invoke the methods implemented in `vector` because
396/// `BitSlice<'_, 8, Unsigned>` is isomorphic to `&[u8]`.
397impl<A> Target2<A, MathematicalResult<u32>, USlice<'_, 8>, USlice<'_, 8>> for SquaredL2
398where
399    A: Architecture,
400    diskann_vector::distance::SquaredL2: for<'a> Target2<A, MV<f32>, &'a [u8], &'a [u8]>,
401{
402    #[inline(always)]
403    fn run(self, arch: A, x: USlice<'_, 8>, y: USlice<'_, 8>) -> MathematicalResult<u32> {
404        check_lengths!(x, y)?;
405
406        let r: MV<f32> = <_ as Target2<_, _, _, _>>::run(
407            diskann_vector::distance::SquaredL2 {},
408            arch,
409            x.as_slice(),
410            y.as_slice(),
411        );
412
413        Ok(MV::new(r.into_inner() as u32))
414    }
415}
416
417/// Compute the squared L2 distance between `x` and `y`.
418///
419/// Returns an error if the arguments have different lengths.
420///
421/// # Implementation Notes
422///
423/// This implementation is optimized around x86 with the AVX2 vector extension.
424/// Specifically, we try to hit `Wide::<i32, 8> as SIMDDotProduct<Wide<i16, 8>>` so we can
425/// hit the `_mm256_madd_epi16` intrinsic.
426///
427/// Also note that AVX2 does not have 16-bit integer bit-shift instructions. Instead, we
428/// have to use 32-bit integer shifts and then bit-cast to 16-bit intrinsics.
429/// This works because we need to apply the same shift to all lanes.
430#[cfg(target_arch = "x86_64")]
431impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 4>, USlice<'_, 4>>
432    for SquaredL2
433{
434    #[inline(always)]
435    fn run(
436        self,
437        arch: diskann_wide::arch::x86_64::V3,
438        x: USlice<'_, 4>,
439        y: USlice<'_, 4>,
440    ) -> MathematicalResult<u32> {
441        let len = check_lengths!(x, y)?;
442
443        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
444        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
445        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
446
447        let px_u32: *const u32 = x.as_ptr().cast();
448        let py_u32: *const u32 = y.as_ptr().cast();
449
450        let mut i = 0;
451        let mut s: u32 = 0;
452
453        // The number of 32-bit blocks over the underlying slice.
454        let blocks = len / 8;
455        if i < blocks {
456            let mut s0 = i32s::default(arch);
457            let mut s1 = i32s::default(arch);
458            let mut s2 = i32s::default(arch);
459            let mut s3 = i32s::default(arch);
460            let mask = u32s::splat(arch, 0x000f000f);
461            while i + 8 < blocks {
462                // SAFETY: We have checked that `i + 8 < blocks` which means the address
463                // range `[px_u32 + i, px_u32 + i + 8 * std::mem::size_of::<u32>())` is valid.
464                //
465                // The load has no alignment requirements.
466                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
467
468                // SAFETY: The same logic applies to `y` because:
469                // 1. It has the same type as `x`.
470                // 2. We've verified that it has the same length as `x`.
471                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
472
473                let wx: i16s = (vx & mask).reinterpret_simd();
474                let wy: i16s = (vy & mask).reinterpret_simd();
475                let d = wx - wy;
476                s0 = s0.dot_simd(d, d);
477
478                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
479                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
480                let d = wx - wy;
481                s1 = s1.dot_simd(d, d);
482
483                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
484                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
485                let d = wx - wy;
486                s2 = s2.dot_simd(d, d);
487
488                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
489                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
490                let d = wx - wy;
491                s3 = s3.dot_simd(d, d);
492
493                i += 8;
494            }
495
496            let remainder = blocks - i;
497
498            // SAFETY: At least one value of type `u32` is valid for an unaligned starting
499            // at offset `i`. The exact number is computed as `remainder`.
500            //
501            // The predicated load is guaranteed not to access memory after `remainder` and
502            // has no alignment requirements.
503            let vx = unsafe { u32s::load_simd_first(arch, px_u32.add(i), remainder) };
504
505            // SAFETY: The same logic applies to `y` because:
506            // 1. It has the same type as `x`.
507            // 2. We've verified that it has the same length as `x`.
508            let vy = unsafe { u32s::load_simd_first(arch, py_u32.add(i), remainder) };
509
510            let wx: i16s = (vx & mask).reinterpret_simd();
511            let wy: i16s = (vy & mask).reinterpret_simd();
512            let d = wx - wy;
513            s0 = s0.dot_simd(d, d);
514
515            let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
516            let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
517            let d = wx - wy;
518            s1 = s1.dot_simd(d, d);
519
520            let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
521            let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
522            let d = wx - wy;
523            s2 = s2.dot_simd(d, d);
524
525            let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
526            let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
527            let d = wx - wy;
528            s3 = s3.dot_simd(d, d);
529
530            i += remainder;
531
532            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
533        }
534
535        // Convert blocks to indexes.
536        i *= 8;
537
538        // Deal with the remainder the slow way.
539        if i != len {
540            // Outline the fallback routine to keep code-generation at this level cleaner.
541            #[inline(never)]
542            fn fallback(x: USlice<'_, 4>, y: USlice<'_, 4>, from: usize) -> u32 {
543                let mut s: i32 = 0;
544                for i in from..x.len() {
545                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
546                    let ix = unsafe { x.get_unchecked(i) } as i32;
547                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
548                    let iy = unsafe { y.get_unchecked(i) } as i32;
549                    let d = ix - iy;
550                    s += d * d;
551                }
552                s as u32
553            }
554            s += fallback(x, y, i);
555        }
556
557        Ok(MV::new(s))
558    }
559}
560
561/// Compute the squared L2 distance between `x` and `y`.
562///
563/// Returns an error if the arguments have different lengths.
564///
565/// # Implementation Notes
566///
567/// This implementation is optimized for x86 with the AVX-512 vector extension.
568///
569/// We load data as `u32x16`, shift and mask to extract 4-bit nibbles at 16-bit granularity
570/// (`0x000f000f` mask), reinterpret as `i16x32`, compute differences, and use
571/// `_mm512_madd_epi16` via `dot_simd` to accumulate squared differences into 4 independent
572/// `i32x16` accumulators. Reinterpreting after a shift is well-defined because the same
573/// shift amount is applied uniformly to all lanes.
574#[cfg(target_arch = "x86_64")]
575impl Target2<diskann_wide::arch::x86_64::V4, MathematicalResult<u32>, USlice<'_, 4>, USlice<'_, 4>>
576    for SquaredL2
577{
578    #[inline(always)]
579    fn run(
580        self,
581        arch: diskann_wide::arch::x86_64::V4,
582        x: USlice<'_, 4>,
583        y: USlice<'_, 4>,
584    ) -> MathematicalResult<u32> {
585        let len = check_lengths!(x, y)?;
586
587        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V4>::i32x16);
588        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V4>::u32x16);
589        diskann_wide::alias!(u8s = <diskann_wide::arch::x86_64::V4>::u8x64);
590        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V4>::i16x32);
591
592        let px_u32: *const u32 = x.as_ptr().cast();
593        let py_u32: *const u32 = y.as_ptr().cast();
594
595        let mut i = 0;
596        let mut s: u32 = 0;
597
598        // Number of u32 blocks (rounded up). Each u32 holds 8 nibbles = 8 four-bit values.
599        // We use `div_ceil` so a partial trailing byte (1..=7 stray nibbles) is still
600        // covered by the predicated `u8` load below; the scalar fallback only ever needs to
601        // handle at most a single dangling nibble.
602        let blocks = len.div_ceil(8);
603        if i < blocks {
604            let mut s0 = i32s::default(arch);
605            let mut s1 = i32s::default(arch);
606            let mut s2 = i32s::default(arch);
607            let mut s3 = i32s::default(arch);
608            let mask = u32s::splat(arch, 0x000f000f);
609            while i + 16 < blocks {
610                // SAFETY: We have checked that `i + 16 < blocks` which means the
611                // 16-element range `px_u32.add(i)..px_u32.add(i + 16)` (in `u32` units)
612                // is dereferenceable.
613                //
614                // The load has no alignment requirements.
615                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
616
617                // SAFETY: The same logic applies to `y` because:
618                // 1. It has the same type as `x`.
619                // 2. We've verified that it has the same length as `x`.
620                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
621
622                let wx: i16s = (vx & mask).reinterpret_simd();
623                let wy: i16s = (vy & mask).reinterpret_simd();
624                let d = wx - wy;
625                s0 = s0.dot_simd(d, d);
626
627                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
628                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
629                let d = wx - wy;
630                s1 = s1.dot_simd(d, d);
631
632                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
633                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
634                let d = wx - wy;
635                s2 = s2.dot_simd(d, d);
636
637                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
638                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
639                let d = wx - wy;
640                s3 = s3.dot_simd(d, d);
641
642                i += 16;
643            }
644
645            // Compute the number of bytes still to process:
646            //   * `len / 2`  — total full bytes in the input (2 nibbles/byte).
647            //   * `4 * i`    — bytes already consumed (4 bytes per u32 × `i` u32s).
648            //
649            // The loop invariant `i + 16 < blocks` (with `blocks = len.div_ceil(8)`)
650            // guarantees `4 * i < len / 2 + small`, so this subtraction is safe.
651            let remainder = len / 2 - 4 * i;
652
653            if remainder > 0 {
654                // SAFETY: `remainder` bytes are dereferenceable starting at
655                // `px_u32.add(i).cast::<u8>()` (i.e. byte offset `4 * i` from `px_u32`).
656                //
657                // The predicated load is guaranteed not to access memory beyond the
658                // first `remainder` bytes and has no alignment requirements.
659                let vx =
660                    unsafe { u8s::load_simd_first(arch, px_u32.add(i).cast::<u8>(), remainder) };
661                let vx: u32s = vx.reinterpret_simd();
662
663                // SAFETY: The same logic applies to `y` because:
664                // 1. It has the same type as `x`.
665                // 2. We've verified that it has the same length as `x`.
666                let vy =
667                    unsafe { u8s::load_simd_first(arch, py_u32.add(i).cast::<u8>(), remainder) };
668                let vy: u32s = vy.reinterpret_simd();
669
670                let wx: i16s = (vx & mask).reinterpret_simd();
671                let wy: i16s = (vy & mask).reinterpret_simd();
672                let d = wx - wy;
673                s0 = s0.dot_simd(d, d);
674
675                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
676                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
677                let d = wx - wy;
678                s1 = s1.dot_simd(d, d);
679
680                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
681                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
682                let d = wx - wy;
683                s2 = s2.dot_simd(d, d);
684
685                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
686                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
687                let d = wx - wy;
688                s3 = s3.dot_simd(d, d);
689            }
690
691            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
692            i = (4 * i) + remainder;
693        }
694
695        // Convert bytes to nibble indexes.
696        i *= 2;
697
698        // Deal with the remainder the slow way (at most 1 element).
699        debug_assert!(len - i <= 1);
700        if i != len {
701            // SAFETY: `i` is guaranteed to be less than `x.len()`.
702            let ix = unsafe { x.get_unchecked(i) } as i32;
703            // SAFETY: `i` is guaranteed to be less than `y.len()`.
704            let iy = unsafe { y.get_unchecked(i) } as i32;
705            let d = ix - iy;
706            s += (d * d) as u32;
707        }
708
709        Ok(MV::new(s))
710    }
711}
712
713/// Compute the squared L2 distance between `x` and `y`.
714///
715/// Returns an error if the arguments have different lengths.
716///
717/// # Implementation Notes
718///
719/// This implementation is optimized around x86 with the AVX2 vector extension.
720/// Specifically, we try to hit `Wide::<i32, 8> as SIMDDotProduct<Wide<i16, 8>>` so we can
721/// hit the `_mm256_madd_epi16` intrinsic.
722///
723/// Also note that AVX2 does not have 16-bit integer bit-shift instructions. Instead, we
724/// have to use 32-bit integer shifts and then bit-cast to 16-bit intrinsics.
725/// This works because we need to apply the same shift to all lanes.
726#[cfg(target_arch = "x86_64")]
727impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 2>, USlice<'_, 2>>
728    for SquaredL2
729{
730    #[inline(always)]
731    fn run(
732        self,
733        arch: diskann_wide::arch::x86_64::V3,
734        x: USlice<'_, 2>,
735        y: USlice<'_, 2>,
736    ) -> MathematicalResult<u32> {
737        let len = check_lengths!(x, y)?;
738
739        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
740        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
741        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
742
743        let px_u32: *const u32 = x.as_ptr().cast();
744        let py_u32: *const u32 = y.as_ptr().cast();
745
746        let mut i = 0;
747        let mut s: u32 = 0;
748
749        // The number of 32-bit blocks over the underlying slice.
750        let blocks = len / 16;
751        if i < blocks {
752            let mut s0 = i32s::default(arch);
753            let mut s1 = i32s::default(arch);
754            let mut s2 = i32s::default(arch);
755            let mut s3 = i32s::default(arch);
756            let mask = u32s::splat(arch, 0x00030003);
757            while i + 8 < blocks {
758                // SAFETY: We have checked that `i + 8 < blocks` which means the address
759                // range `[px_u32 + i, px_u32 + i + 8 * std::mem::size_of::<u32>())` is valid.
760                //
761                // The load has no alignment requirements.
762                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
763
764                // SAFETY: The same logic applies to `y` because:
765                // 1. It has the same type as `x`.
766                // 2. We've verified that it has the same length as `x`.
767                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
768
769                let wx: i16s = (vx & mask).reinterpret_simd();
770                let wy: i16s = (vy & mask).reinterpret_simd();
771                let d = wx - wy;
772                s0 = s0.dot_simd(d, d);
773
774                let wx: i16s = (vx >> 2 & mask).reinterpret_simd();
775                let wy: i16s = (vy >> 2 & mask).reinterpret_simd();
776                let d = wx - wy;
777                s1 = s1.dot_simd(d, d);
778
779                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
780                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
781                let d = wx - wy;
782                s2 = s2.dot_simd(d, d);
783
784                let wx: i16s = (vx >> 6 & mask).reinterpret_simd();
785                let wy: i16s = (vy >> 6 & mask).reinterpret_simd();
786                let d = wx - wy;
787                s3 = s3.dot_simd(d, d);
788
789                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
790                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
791                let d = wx - wy;
792                s0 = s0.dot_simd(d, d);
793
794                let wx: i16s = (vx >> 10 & mask).reinterpret_simd();
795                let wy: i16s = (vy >> 10 & mask).reinterpret_simd();
796                let d = wx - wy;
797                s1 = s1.dot_simd(d, d);
798
799                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
800                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
801                let d = wx - wy;
802                s2 = s2.dot_simd(d, d);
803
804                let wx: i16s = (vx >> 14 & mask).reinterpret_simd();
805                let wy: i16s = (vy >> 14 & mask).reinterpret_simd();
806                let d = wx - wy;
807                s3 = s3.dot_simd(d, d);
808
809                i += 8;
810            }
811
812            let remainder = blocks - i;
813
814            // SAFETY: At least one value of type `u32` is valid for an unaligned starting
815            // at offset `i`. The exact number is computed as `remainder`.
816            //
817            // The predicated load is guaranteed not to access memory after `remainder` and
818            // has no alignment requirements.
819            let vx = unsafe { u32s::load_simd_first(arch, px_u32.add(i), remainder) };
820
821            // SAFETY: The same logic applies to `y` because:
822            // 1. It has the same type as `x`.
823            // 2. We've verified that it has the same length as `x`.
824            let vy = unsafe { u32s::load_simd_first(arch, py_u32.add(i), remainder) };
825            let wx: i16s = (vx & mask).reinterpret_simd();
826            let wy: i16s = (vy & mask).reinterpret_simd();
827            let d = wx - wy;
828            s0 = s0.dot_simd(d, d);
829
830            let wx: i16s = (vx >> 2 & mask).reinterpret_simd();
831            let wy: i16s = (vy >> 2 & mask).reinterpret_simd();
832            let d = wx - wy;
833            s1 = s1.dot_simd(d, d);
834
835            let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
836            let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
837            let d = wx - wy;
838            s2 = s2.dot_simd(d, d);
839
840            let wx: i16s = (vx >> 6 & mask).reinterpret_simd();
841            let wy: i16s = (vy >> 6 & mask).reinterpret_simd();
842            let d = wx - wy;
843            s3 = s3.dot_simd(d, d);
844
845            let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
846            let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
847            let d = wx - wy;
848            s0 = s0.dot_simd(d, d);
849
850            let wx: i16s = (vx >> 10 & mask).reinterpret_simd();
851            let wy: i16s = (vy >> 10 & mask).reinterpret_simd();
852            let d = wx - wy;
853            s1 = s1.dot_simd(d, d);
854
855            let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
856            let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
857            let d = wx - wy;
858            s2 = s2.dot_simd(d, d);
859
860            let wx: i16s = (vx >> 14 & mask).reinterpret_simd();
861            let wy: i16s = (vy >> 14 & mask).reinterpret_simd();
862            let d = wx - wy;
863            s3 = s3.dot_simd(d, d);
864
865            i += remainder;
866
867            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
868        }
869
870        // Convert blocks to indexes.
871        i *= 16;
872
873        // Deal with the remainder the slow way.
874        if i != len {
875            // Outline the fallback routine to keep code-generation at this level cleaner.
876            #[inline(never)]
877            fn fallback(x: USlice<'_, 2>, y: USlice<'_, 2>, from: usize) -> u32 {
878                let mut s: i32 = 0;
879                for i in from..x.len() {
880                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
881                    let ix = unsafe { x.get_unchecked(i) } as i32;
882                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
883                    let iy = unsafe { y.get_unchecked(i) } as i32;
884                    let d = ix - iy;
885                    s += d * d;
886                }
887                s as u32
888            }
889            s += fallback(x, y, i);
890        }
891
892        Ok(MV::new(s))
893    }
894}
895
896/// Compute the squared L2 distance between bitvectors `x` and `y`.
897///
898/// Returns an error if the arguments have different lengths.
899impl<A> Target2<A, MathematicalResult<u32>, USlice<'_, 1>, USlice<'_, 1>> for SquaredL2
900where
901    A: Architecture,
902{
903    fn run(self, _: A, x: USlice<'_, 1>, y: USlice<'_, 1>) -> MathematicalResult<u32> {
904        bitvector_op::<Self, Unsigned>(x, y)
905    }
906}
907
908/// An implementation for L2 distance that uses scalar indexing for the implementation.
909macro_rules! impl_fallback_l2 {
910    ($N:literal) => {
911        /// Compute the squared L2 distance between `x` and `y`.
912        ///
913        /// Returns an error if the arguments have different lengths.
914        ///
915        /// # Performance
916        ///
917        /// This function uses a generic implementation and therefore is not very fast.
918        impl Target2<diskann_wide::arch::Scalar, MathematicalResult<u32>, USlice<'_, $N>, USlice<'_, $N>> for SquaredL2 {
919            #[inline(never)]
920            fn run(
921                self,
922                _: diskann_wide::arch::Scalar,
923                x: USlice<'_, $N>,
924                y: USlice<'_, $N>
925            ) -> MathematicalResult<u32> {
926                let len = check_lengths!(x, y)?;
927
928                let mut accum: i32 = 0;
929                for i in 0..len {
930                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
931                    let ix: i32 = unsafe { x.get_unchecked(i) } as i32;
932                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
933                    let iy: i32 = unsafe { y.get_unchecked(i) } as i32;
934                    let diff = ix - iy;
935                    accum += diff * diff;
936                }
937                Ok(MV::new(accum as u32))
938            }
939        }
940    };
941    ($($N:literal),+ $(,)?) => {
942        $(impl_fallback_l2!($N);)+
943    };
944}
945
946impl_fallback_l2!(7, 6, 5, 4, 3, 2);
947
948#[cfg(target_arch = "x86_64")]
949retarget!(diskann_wide::arch::x86_64::V3, SquaredL2, 7, 6, 5, 3);
950
951#[cfg(target_arch = "x86_64")]
952retarget!(diskann_wide::arch::x86_64::V4, SquaredL2, 7, 6, 5, 3, 2);
953
954dispatch_pure!(SquaredL2, 1, 2, 3, 4, 5, 6, 7, 8);
955#[cfg(target_arch = "aarch64")]
956retarget!(
957    diskann_wide::arch::aarch64::Neon,
958    SquaredL2,
959    7,
960    6,
961    5,
962    4,
963    3,
964    2
965);
966
967///////////////////
968// Inner Product //
969///////////////////
970
971/// Compute the inner product between `x` and `y`.
972///
973/// Returns an error if the arguments have different lengths.
974///
975/// # Implementation Notes
976///
977/// This can directly invoke the methods implemented in `vector` because
978/// `BitSlice<'_, 8, Unsigned>` is isomorphic to `&[u8]`.
979impl<A> Target2<A, MathematicalResult<u32>, USlice<'_, 8>, USlice<'_, 8>> for InnerProduct
980where
981    A: Architecture,
982    diskann_vector::distance::InnerProduct: for<'a> Target2<A, MV<f32>, &'a [u8], &'a [u8]>,
983{
984    #[inline(always)]
985    fn run(self, arch: A, x: USlice<'_, 8>, y: USlice<'_, 8>) -> MathematicalResult<u32> {
986        check_lengths!(x, y)?;
987        let r: MV<f32> = <_ as Target2<_, _, _, _>>::run(
988            diskann_vector::distance::InnerProduct {},
989            arch,
990            x.as_slice(),
991            y.as_slice(),
992        );
993
994        Ok(MV::new(r.into_inner() as u32))
995    }
996}
997
998/// Compute the inner product between `x` and `y`.
999///
1000/// Returns an error if the arguments have different lengths.
1001///
1002/// # Implementation Notes
1003///
1004/// This is optimized around the `__mm512_dpbusd_epi32` VNNI instruction, which computes the
1005/// pairwise dot product between vectors of 8-bit integers and accumulates groups of 4 with
1006/// an `i32` accumulation vector.
1007///
1008/// One quirk of this instruction is that one argument must be unsigned and the other must
1009/// be signed. Since thie kernsl works on 2-bit integers, this is not a limitation. Just
1010/// something to be aware of.
1011///
1012/// Since AVX512 does not have an 8-bit shift instruction, we generally load data as
1013/// `u32x16` (which has a native shift) and bit-cast it to `u8x64` as needed.
1014#[cfg(target_arch = "x86_64")]
1015impl Target2<diskann_wide::arch::x86_64::V4, MathematicalResult<u32>, USlice<'_, 2>, USlice<'_, 2>>
1016    for InnerProduct
1017{
1018    #[expect(non_camel_case_types)]
1019    #[inline(always)]
1020    fn run(
1021        self,
1022        arch: diskann_wide::arch::x86_64::V4,
1023        x: USlice<'_, 2>,
1024        y: USlice<'_, 2>,
1025    ) -> MathematicalResult<u32> {
1026        let len = check_lengths!(x, y)?;
1027
1028        type i32s = <diskann_wide::arch::x86_64::V4 as Architecture>::i32x16;
1029        type u32s = <diskann_wide::arch::x86_64::V4 as Architecture>::u32x16;
1030        type u8s = <diskann_wide::arch::x86_64::V4 as Architecture>::u8x64;
1031        type i8s = <diskann_wide::arch::x86_64::V4 as Architecture>::i8x64;
1032
1033        let px_u32: *const u32 = x.as_ptr().cast();
1034        let py_u32: *const u32 = y.as_ptr().cast();
1035
1036        let mut i = 0;
1037        let mut s: u32 = 0;
1038
1039        // The number of 32-bit blocks over the underlying slice.
1040        let blocks = len.div_ceil(16);
1041        if i < blocks {
1042            let mut s0 = i32s::default(arch);
1043            let mut s1 = i32s::default(arch);
1044            let mut s2 = i32s::default(arch);
1045            let mut s3 = i32s::default(arch);
1046            let mask = u32s::splat(arch, 0x03030303);
1047            while i + 16 < blocks {
1048                // SAFETY: We have checked that `i + 16 < blocks` which means the address
1049                // range `[px_u32 + i, px_u32 + i + 16 * std::mem::size_of::<u32>())` is valid.
1050                //
1051                // The load has no alignment requirements.
1052                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
1053
1054                // SAFETY: The same logic applies to `y` because:
1055                // 1. It has the same type as `x`.
1056                // 2. We've verified that it has the same length as `x`.
1057                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
1058
1059                let wx: u8s = (vx & mask).reinterpret_simd();
1060                let wy: i8s = (vy & mask).reinterpret_simd();
1061                s0 = s0.dot_simd(wx, wy);
1062
1063                let wx: u8s = ((vx >> 2) & mask).reinterpret_simd();
1064                let wy: i8s = ((vy >> 2) & mask).reinterpret_simd();
1065                s1 = s1.dot_simd(wx, wy);
1066
1067                let wx: u8s = ((vx >> 4) & mask).reinterpret_simd();
1068                let wy: i8s = ((vy >> 4) & mask).reinterpret_simd();
1069                s2 = s2.dot_simd(wx, wy);
1070
1071                let wx: u8s = ((vx >> 6) & mask).reinterpret_simd();
1072                let wy: i8s = ((vy >> 6) & mask).reinterpret_simd();
1073                s3 = s3.dot_simd(wx, wy);
1074
1075                i += 16;
1076            }
1077
1078            // Here
1079            // * `len / 4` gives the number of full bytes
1080            // * `4 * i` gives the number of bytes processed.
1081            let remainder = len / 4 - 4 * i;
1082
1083            // SAFETY: At least `remainder` bytes are valid starting at an offset of `i`.
1084            //
1085            // The predicated load is guaranteed not to access memory after `remainder` and
1086            // has no alignment requirements.
1087            let vx = unsafe { u8s::load_simd_first(arch, px_u32.add(i).cast::<u8>(), remainder) };
1088            let vx: u32s = vx.reinterpret_simd();
1089
1090            // SAFETY: The same logic applies to `y` because:
1091            // 1. It has the same type as `x`.
1092            // 2. We've verified that it has the same length as `x`.
1093            let vy = unsafe { u8s::load_simd_first(arch, py_u32.add(i).cast::<u8>(), remainder) };
1094            let vy: u32s = vy.reinterpret_simd();
1095
1096            let wx: u8s = (vx & mask).reinterpret_simd();
1097            let wy: i8s = (vy & mask).reinterpret_simd();
1098            s0 = s0.dot_simd(wx, wy);
1099
1100            let wx: u8s = ((vx >> 2) & mask).reinterpret_simd();
1101            let wy: i8s = ((vy >> 2) & mask).reinterpret_simd();
1102            s1 = s1.dot_simd(wx, wy);
1103
1104            let wx: u8s = ((vx >> 4) & mask).reinterpret_simd();
1105            let wy: i8s = ((vy >> 4) & mask).reinterpret_simd();
1106            s2 = s2.dot_simd(wx, wy);
1107
1108            let wx: u8s = ((vx >> 6) & mask).reinterpret_simd();
1109            let wy: i8s = ((vy >> 6) & mask).reinterpret_simd();
1110            s3 = s3.dot_simd(wx, wy);
1111
1112            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
1113            i = (4 * i) + remainder;
1114        }
1115
1116        // Convert blocks to indexes.
1117        i *= 4;
1118
1119        // Deal with the remainder the slow way.
1120        debug_assert!(len - i <= 3);
1121        let rest = (len - i).min(3);
1122        if i != len {
1123            for j in 0..rest {
1124                // SAFETY: `i` is guaranteed to be less than `x.len()`.
1125                let ix = unsafe { x.get_unchecked(i + j) } as u32;
1126                // SAFETY: `i` is guaranteed to be less than `y.len()`.
1127                let iy = unsafe { y.get_unchecked(i + j) } as u32;
1128                s += ix * iy;
1129            }
1130        }
1131
1132        Ok(MV::new(s))
1133    }
1134}
1135
1136/// Compute the inner product between `x` and `y`.
1137///
1138/// Returns an error if the arguments have different lengths.
1139///
1140/// # Implementation Notes
1141///
1142/// This implementation is optimized around x86 with the AVX2 vector extension.
1143/// Specifically, we try to hit `Wide::<i32, 8> as SIMDDotProduct<Wide<i16, 8>>` so we can
1144/// hit the `_mm256_madd_epi16` intrinsic.
1145///
1146/// Also note that AVX2 does not have 16-bit integer bit-shift instructions. Instead, we
1147/// have to use 32-bit integer shifts and then bit-cast to 16-bit intrinsics.
1148/// This works because we need to apply the same shift to all lanes.
1149#[cfg(target_arch = "x86_64")]
1150impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 4>, USlice<'_, 4>>
1151    for InnerProduct
1152{
1153    #[inline(always)]
1154    fn run(
1155        self,
1156        arch: diskann_wide::arch::x86_64::V3,
1157        x: USlice<'_, 4>,
1158        y: USlice<'_, 4>,
1159    ) -> MathematicalResult<u32> {
1160        let len = check_lengths!(x, y)?;
1161
1162        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
1163        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
1164        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
1165
1166        let px_u32: *const u32 = x.as_ptr().cast();
1167        let py_u32: *const u32 = y.as_ptr().cast();
1168
1169        let mut i = 0;
1170        let mut s: u32 = 0;
1171
1172        let blocks = len / 8;
1173        if i < blocks {
1174            let mut s0 = i32s::default(arch);
1175            let mut s1 = i32s::default(arch);
1176            let mut s2 = i32s::default(arch);
1177            let mut s3 = i32s::default(arch);
1178            let mask = u32s::splat(arch, 0x000f000f);
1179            while i + 8 < blocks {
1180                // SAFETY: We have checked that `i + 8 < blocks` which means the address
1181                // range `[px_u32 + i, px_u32 + i + 8 * std::mem::size_of::<u32>())` is valid.
1182                //
1183                // The load has no alignment requirements.
1184                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
1185
1186                // SAFETY: The same logic applies to `y` because:
1187                // 1. It has the same type as `x`.
1188                // 2. We've verified that it has the same length as `x`.
1189                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
1190
1191                let wx: i16s = (vx & mask).reinterpret_simd();
1192                let wy: i16s = (vy & mask).reinterpret_simd();
1193                s0 = s0.dot_simd(wx, wy);
1194
1195                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
1196                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
1197                s1 = s1.dot_simd(wx, wy);
1198
1199                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
1200                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
1201                s2 = s2.dot_simd(wx, wy);
1202
1203                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
1204                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
1205                s3 = s3.dot_simd(wx, wy);
1206
1207                i += 8;
1208            }
1209
1210            let remainder = blocks - i;
1211
1212            // SAFETY: At least one value of type `u32` is valid for an unaligned starting
1213            // at offset `i`. The exact number is computed as `remainder`.
1214            //
1215            // The predicated load is guaranteed not to access memory after `remainder` and
1216            // has no alignment requirements.
1217            let vx = unsafe { u32s::load_simd_first(arch, px_u32.add(i), remainder) };
1218
1219            // SAFETY: The same logic applies to `y` because:
1220            // 1. It has the same type as `x`.
1221            // 2. We've verified that it has the same length as `x`.
1222            let vy = unsafe { u32s::load_simd_first(arch, py_u32.add(i), remainder) };
1223
1224            let wx: i16s = (vx & mask).reinterpret_simd();
1225            let wy: i16s = (vy & mask).reinterpret_simd();
1226            s0 = s0.dot_simd(wx, wy);
1227
1228            let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
1229            let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
1230            s1 = s1.dot_simd(wx, wy);
1231
1232            let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
1233            let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
1234            s2 = s2.dot_simd(wx, wy);
1235
1236            let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
1237            let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
1238            s3 = s3.dot_simd(wx, wy);
1239
1240            i += remainder;
1241
1242            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
1243        }
1244
1245        // Convert blocks to indexes.
1246        i *= 8;
1247
1248        // Deal with the remainder the slow way.
1249        if i != len {
1250            // Outline the fallback routine to keep code-generation at this level cleaner.
1251            #[inline(never)]
1252            fn fallback(x: USlice<'_, 4>, y: USlice<'_, 4>, from: usize) -> u32 {
1253                let mut s: u32 = 0;
1254                for i in from..x.len() {
1255                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
1256                    let ix = unsafe { x.get_unchecked(i) } as u32;
1257                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
1258                    let iy = unsafe { y.get_unchecked(i) } as u32;
1259                    s += ix * iy;
1260                }
1261                s
1262            }
1263            s += fallback(x, y, i);
1264        }
1265
1266        Ok(MV::new(s))
1267    }
1268}
1269
1270/// Compute the inner product between `x` and `y`.
1271///
1272/// Returns an error if the arguments have different lengths.
1273///
1274/// # Implementation Notes
1275///
1276/// This implementation is optimized around the AVX-512 VNNI `_mm512_dpbusd_epi32`
1277/// instruction, which computes the pairwise dot product between vectors of 8-bit integers
1278/// and accumulates groups of 4 into an `i32` accumulator.
1279///
1280/// For 4-bit values, each byte holds 2 nibbles. We load data as `u32x16`, mask with
1281/// `0x0f0f0f0f` to extract the low nibbles as bytes, and shift right by 4 then mask to
1282/// extract the high nibbles. This yields `u8x64` / `i8x64` operands for VNNI, requiring
1283/// only 2 shift positions.
1284///
1285/// Since AVX-512 does not have an 8-bit shift instruction, we load data as `u32x16`
1286/// (which has a native shift) and bit-cast to `u8x64` as needed.
1287#[cfg(target_arch = "x86_64")]
1288impl Target2<diskann_wide::arch::x86_64::V4, MathematicalResult<u32>, USlice<'_, 4>, USlice<'_, 4>>
1289    for InnerProduct
1290{
1291    #[inline(always)]
1292    fn run(
1293        self,
1294        arch: diskann_wide::arch::x86_64::V4,
1295        x: USlice<'_, 4>,
1296        y: USlice<'_, 4>,
1297    ) -> MathematicalResult<u32> {
1298        let len = check_lengths!(x, y)?;
1299
1300        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V4>::i32x16);
1301        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V4>::u32x16);
1302        diskann_wide::alias!(u8s = <diskann_wide::arch::x86_64::V4>::u8x64);
1303        diskann_wide::alias!(i8s = <diskann_wide::arch::x86_64::V4>::i8x64);
1304
1305        let px_u32: *const u32 = x.as_ptr().cast();
1306        let py_u32: *const u32 = y.as_ptr().cast();
1307
1308        let mut i = 0;
1309        let mut s: u32 = 0;
1310
1311        // Number of u32 blocks (rounded up). Each u32 holds 8 nibbles = 8 four-bit values.
1312        // We use `div_ceil` so a partial trailing byte (1..=7 stray nibbles) is still
1313        // covered by the predicated `u8` load below; the scalar fallback only ever needs to
1314        // handle at most a single dangling nibble.
1315        let blocks = len.div_ceil(8);
1316        if i < blocks {
1317            let mut s0 = i32s::default(arch);
1318            let mut s1 = i32s::default(arch);
1319            let mask = u32s::splat(arch, 0x0f0f0f0f);
1320            while i + 16 < blocks {
1321                // SAFETY: We have checked that `i + 16 < blocks` which means the
1322                // 16-element range `px_u32.add(i)..px_u32.add(i + 16)` (in `u32` units)
1323                // is dereferenceable.
1324                //
1325                // The load has no alignment requirements.
1326                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
1327
1328                // SAFETY: The same logic applies to `y` because:
1329                // 1. It has the same type as `x`.
1330                // 2. We've verified that it has the same length as `x`.
1331                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
1332
1333                // VNNI `vpdpbusd` requires (unsigned, signed) operands in this order;
1334                // `dot_simd(u8s, i8s)` lowers to that intrinsic on V4. 4-bit values fit in
1335                // the positive half of `i8`, so the unsigned->signed reinterpret is safe.
1336                let wx: u8s = (vx & mask).reinterpret_simd();
1337                let wy: i8s = (vy & mask).reinterpret_simd();
1338                s0 = s0.dot_simd(wx, wy);
1339
1340                let wx: u8s = ((vx >> 4) & mask).reinterpret_simd();
1341                let wy: i8s = ((vy >> 4) & mask).reinterpret_simd();
1342                s1 = s1.dot_simd(wx, wy);
1343
1344                i += 16;
1345            }
1346
1347            // Compute the number of bytes still to process:
1348            //   * `len / 2`  — total full bytes in the input (2 nibbles/byte).
1349            //   * `4 * i`    — bytes already consumed (4 bytes per u32 × `i` u32s).
1350            //
1351            // The loop invariant `i + 16 < blocks` (with `blocks = len.div_ceil(8)`)
1352            // guarantees `4 * i < len / 2 + small`, so this subtraction is safe.
1353            let remainder = len / 2 - 4 * i;
1354
1355            if remainder > 0 {
1356                // SAFETY: `remainder` bytes are dereferenceable starting at
1357                // `px_u32.add(i).cast::<u8>()` (i.e. byte offset `4 * i` from `px_u32`).
1358                //
1359                // The predicated load is guaranteed not to access memory beyond the
1360                // first `remainder` bytes and has no alignment requirements.
1361                let vx =
1362                    unsafe { u8s::load_simd_first(arch, px_u32.add(i).cast::<u8>(), remainder) };
1363                let vx: u32s = vx.reinterpret_simd();
1364
1365                // SAFETY: The same logic applies to `y` because:
1366                // 1. It has the same type as `x`.
1367                // 2. We've verified that it has the same length as `x`.
1368                let vy =
1369                    unsafe { u8s::load_simd_first(arch, py_u32.add(i).cast::<u8>(), remainder) };
1370                let vy: u32s = vy.reinterpret_simd();
1371
1372                let wx: u8s = (vx & mask).reinterpret_simd();
1373                let wy: i8s = (vy & mask).reinterpret_simd();
1374                s0 = s0.dot_simd(wx, wy);
1375
1376                let wx: u8s = ((vx >> 4) & mask).reinterpret_simd();
1377                let wy: i8s = ((vy >> 4) & mask).reinterpret_simd();
1378                s1 = s1.dot_simd(wx, wy);
1379            }
1380
1381            s = (s0 + s1).sum_tree() as u32;
1382            i = (4 * i) + remainder;
1383        }
1384
1385        // Convert bytes to nibble indexes.
1386        i *= 2;
1387
1388        // Deal with the remainder the slow way (at most 1 element).
1389        debug_assert!(len - i <= 1);
1390        if i != len {
1391            // SAFETY: `i` is guaranteed to be less than `x.len()`.
1392            let ix = unsafe { x.get_unchecked(i) } as u32;
1393            // SAFETY: `i` is guaranteed to be less than `y.len()`.
1394            let iy = unsafe { y.get_unchecked(i) } as u32;
1395            s += ix * iy;
1396        }
1397
1398        Ok(MV::new(s))
1399    }
1400}
1401
1402/// Compute the inner product between `x` and `y`.
1403///
1404/// Returns an error if the arguments have different lengths.
1405///
1406/// # Implementation Notes
1407///
1408/// This implementation is optimized around x86 with the AVX2 vector extension.
1409/// Specifically, we try to hit `Wide::<i32, 8> as SIMDDotProduct<Wide<i16, 8>>` so we can
1410/// hit the `_mm256_madd_epi16` intrinsic.
1411///
1412/// Also note that AVX2 does not have 16-bit integer bit-shift instructions. Instead, we
1413/// have to use 32-bit integer shifts and then bit-cast to 16-bit intrinsics.
1414/// This works because we need to apply the same shift to all lanes.
1415#[cfg(target_arch = "x86_64")]
1416impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 2>, USlice<'_, 2>>
1417    for InnerProduct
1418{
1419    #[inline(always)]
1420    fn run(
1421        self,
1422        arch: diskann_wide::arch::x86_64::V3,
1423        x: USlice<'_, 2>,
1424        y: USlice<'_, 2>,
1425    ) -> MathematicalResult<u32> {
1426        let len = check_lengths!(x, y)?;
1427
1428        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
1429        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
1430        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
1431
1432        let px_u32: *const u32 = x.as_ptr().cast();
1433        let py_u32: *const u32 = y.as_ptr().cast();
1434
1435        let mut i = 0;
1436        let mut s: u32 = 0;
1437
1438        // The number of 32-bit blocks over the underlying slice.
1439        let blocks = len / 16;
1440        if i < blocks {
1441            let mut s0 = i32s::default(arch);
1442            let mut s1 = i32s::default(arch);
1443            let mut s2 = i32s::default(arch);
1444            let mut s3 = i32s::default(arch);
1445            let mask = u32s::splat(arch, 0x00030003);
1446            while i + 8 < blocks {
1447                // SAFETY: We have checked that `i + 8 < blocks` which means the address
1448                // range `[px_u32 + i, px_u32 + i + 8 * std::mem::size_of::<u32>())` is valid.
1449                //
1450                // The load has no alignment requirements.
1451                let vx = unsafe { u32s::load_simd(arch, px_u32.add(i)) };
1452
1453                // SAFETY: The same logic applies to `y` because:
1454                // 1. It has the same type as `x`.
1455                // 2. We've verified that it has the same length as `x`.
1456                let vy = unsafe { u32s::load_simd(arch, py_u32.add(i)) };
1457
1458                let wx: i16s = (vx & mask).reinterpret_simd();
1459                let wy: i16s = (vy & mask).reinterpret_simd();
1460                s0 = s0.dot_simd(wx, wy);
1461
1462                let wx: i16s = (vx >> 2 & mask).reinterpret_simd();
1463                let wy: i16s = (vy >> 2 & mask).reinterpret_simd();
1464                s1 = s1.dot_simd(wx, wy);
1465
1466                let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
1467                let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
1468                s2 = s2.dot_simd(wx, wy);
1469
1470                let wx: i16s = (vx >> 6 & mask).reinterpret_simd();
1471                let wy: i16s = (vy >> 6 & mask).reinterpret_simd();
1472                s3 = s3.dot_simd(wx, wy);
1473
1474                let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
1475                let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
1476                s0 = s0.dot_simd(wx, wy);
1477
1478                let wx: i16s = (vx >> 10 & mask).reinterpret_simd();
1479                let wy: i16s = (vy >> 10 & mask).reinterpret_simd();
1480                s1 = s1.dot_simd(wx, wy);
1481
1482                let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
1483                let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
1484                s2 = s2.dot_simd(wx, wy);
1485
1486                let wx: i16s = (vx >> 14 & mask).reinterpret_simd();
1487                let wy: i16s = (vy >> 14 & mask).reinterpret_simd();
1488                s3 = s3.dot_simd(wx, wy);
1489
1490                i += 8;
1491            }
1492
1493            let remainder = blocks - i;
1494
1495            // SAFETY: At least one value of type `u32` is valid for an unaligned starting
1496            // at offset `i`. The exact number is computed as `remainder`.
1497            //
1498            // The predicated load is guaranteed not to access memory after `remainder` and
1499            // has no alignment requirements.
1500            let vx = unsafe { u32s::load_simd_first(arch, px_u32.add(i), remainder) };
1501
1502            // SAFETY: The same logic applies to `y` because:
1503            // 1. It has the same type as `x`.
1504            // 2. We've verified that it has the same length as `x`.
1505            let vy = unsafe { u32s::load_simd_first(arch, py_u32.add(i), remainder) };
1506            let wx: i16s = (vx & mask).reinterpret_simd();
1507            let wy: i16s = (vy & mask).reinterpret_simd();
1508            s0 = s0.dot_simd(wx, wy);
1509
1510            let wx: i16s = (vx >> 2 & mask).reinterpret_simd();
1511            let wy: i16s = (vy >> 2 & mask).reinterpret_simd();
1512            s1 = s1.dot_simd(wx, wy);
1513
1514            let wx: i16s = (vx >> 4 & mask).reinterpret_simd();
1515            let wy: i16s = (vy >> 4 & mask).reinterpret_simd();
1516            s2 = s2.dot_simd(wx, wy);
1517
1518            let wx: i16s = (vx >> 6 & mask).reinterpret_simd();
1519            let wy: i16s = (vy >> 6 & mask).reinterpret_simd();
1520            s3 = s3.dot_simd(wx, wy);
1521
1522            let wx: i16s = (vx >> 8 & mask).reinterpret_simd();
1523            let wy: i16s = (vy >> 8 & mask).reinterpret_simd();
1524            s0 = s0.dot_simd(wx, wy);
1525
1526            let wx: i16s = (vx >> 10 & mask).reinterpret_simd();
1527            let wy: i16s = (vy >> 10 & mask).reinterpret_simd();
1528            s1 = s1.dot_simd(wx, wy);
1529
1530            let wx: i16s = (vx >> 12 & mask).reinterpret_simd();
1531            let wy: i16s = (vy >> 12 & mask).reinterpret_simd();
1532            s2 = s2.dot_simd(wx, wy);
1533
1534            let wx: i16s = (vx >> 14 & mask).reinterpret_simd();
1535            let wy: i16s = (vy >> 14 & mask).reinterpret_simd();
1536            s3 = s3.dot_simd(wx, wy);
1537
1538            i += remainder;
1539
1540            s = ((s0 + s1) + (s2 + s3)).sum_tree() as u32;
1541        }
1542
1543        // Convert blocks to indexes.
1544        i *= 16;
1545
1546        // Deal with the remainder the slow way.
1547        if i != len {
1548            // Outline the fallback routine to keep code-generation at this level cleaner.
1549            #[inline(never)]
1550            fn fallback(x: USlice<'_, 2>, y: USlice<'_, 2>, from: usize) -> u32 {
1551                let mut s: u32 = 0;
1552                for i in from..x.len() {
1553                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
1554                    let ix = unsafe { x.get_unchecked(i) } as u32;
1555                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
1556                    let iy = unsafe { y.get_unchecked(i) } as u32;
1557                    s += ix * iy;
1558                }
1559                s
1560            }
1561            s += fallback(x, y, i);
1562        }
1563
1564        Ok(MV::new(s))
1565    }
1566}
1567
1568/// Compute the inner product between bitvectors `x` and `y`.
1569///
1570/// Returns an error if the arguments have different lengths.
1571impl<A> Target2<A, MathematicalResult<u32>, USlice<'_, 1>, USlice<'_, 1>> for InnerProduct
1572where
1573    A: Architecture,
1574{
1575    #[inline(always)]
1576    fn run(self, _: A, x: USlice<'_, 1>, y: USlice<'_, 1>) -> MathematicalResult<u32> {
1577        bitvector_op::<Self, Unsigned>(x, y)
1578    }
1579}
1580
1581/// An implementation for inner products that uses scalar indexing for the implementation.
1582macro_rules! impl_fallback_ip {
1583    (($N:literal, $M:literal)) => {
1584        /// Compute the inner product between `x` and `y`.
1585        ///
1586        /// Returns an error if the arguments have different lengths.
1587        ///
1588        /// # Performance
1589        ///
1590        /// This function uses a generic implementation and therefore is not very fast.
1591        impl Target2<diskann_wide::arch::Scalar, MathematicalResult<u32>, USlice<'_, $N>, USlice<'_, $M>> for InnerProduct {
1592            #[inline(never)]
1593            fn run(
1594                self,
1595                _: diskann_wide::arch::Scalar,
1596                x: USlice<'_, $N>,
1597                y: USlice<'_, $M>
1598            ) -> MathematicalResult<u32> {
1599                let len = check_lengths!(x, y)?;
1600
1601                let mut accum: u32 = 0;
1602                for i in 0..len {
1603                    // SAFETY: `i` is guaranteed to be less than `x.len()`.
1604                    let ix = unsafe { x.get_unchecked(i) } as u32;
1605                    // SAFETY: `i` is guaranteed to be less than `y.len()`.
1606                    let iy = unsafe { y.get_unchecked(i) } as u32;
1607                    accum += ix * iy;
1608                }
1609                Ok(MV::new(accum))
1610            }
1611        }
1612    };
1613    ($N:literal) => {
1614        impl_fallback_ip!(($N, $N));
1615    };
1616    ($($args:tt),+ $(,)?) => {
1617        $(impl_fallback_ip!($args);)+
1618    };
1619}
1620
1621impl_fallback_ip!(7, 6, 5, 4, 3, 2, (8, 4), (8, 2), (8, 1));
1622
1623#[cfg(target_arch = "x86_64")]
1624retarget!(diskann_wide::arch::x86_64::V3, InnerProduct, 7, 6, 5, 3);
1625
1626#[cfg(target_arch = "x86_64")]
1627retarget!(
1628    diskann_wide::arch::x86_64::V4,
1629    InnerProduct,
1630    7,
1631    6,
1632    5,
1633    3,
1634    (8, 4),
1635    (8, 2),
1636    (8, 1)
1637);
1638
1639dispatch_pure!(
1640    InnerProduct,
1641    1,
1642    2,
1643    3,
1644    4,
1645    5,
1646    6,
1647    7,
1648    (8, 8),
1649    (8, 4),
1650    (8, 2),
1651    (8, 1)
1652);
1653
1654//////////////////////////////////////////
1655// Heterogeneous USlice<8> × USlice<M> //
1656/////////////////////////////////////////
1657#[cfg(target_arch = "x86_64")]
1658impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 8>, USlice<'_, 4>>
1659    for InnerProduct
1660{
1661    /// Computes the inner product of 8-bit unsigned × 4-bit unsigned vectors using V3 intrinsics.
1662    ///
1663    /// # Strategy
1664    ///
1665    /// Unpack each 16-byte chunk of `y` into 32 nibble values via [`unpack_half_bytes`],
1666    /// then multiply with the corresponding 32 bytes of `x` using `_mm256_maddubs_epi16`
1667    /// (u8 × u8 → i16, pairwise horizontal add).
1668    ///
1669    /// The main loop is 4× unrolled: four i16 products are summed in i16 before a single
1670    /// `_mm256_madd_epi16(…, 1)` widens to i32. This is safe because
1671    /// 4 × (255 × 15 × 2) = 30_600 < i16::MAX.
1672    #[inline(always)]
1673    fn run(
1674        self,
1675        arch: diskann_wide::arch::x86_64::V3,
1676        x: USlice<'_, 8>,
1677        y: USlice<'_, 4>,
1678    ) -> MathematicalResult<u32> {
1679        use std::arch::x86_64::_mm256_maddubs_epi16;
1680
1681        let len = check_lengths!(x, y)?;
1682
1683        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
1684        diskann_wide::alias!(u8s_16 = <diskann_wide::arch::x86_64::V3>::u8x16);
1685        diskann_wide::alias!(u8s_32 = <diskann_wide::arch::x86_64::V3>::u8x32);
1686        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
1687
1688        let px: *const u8 = x.as_ptr();
1689        let py: *const u8 = y.as_ptr();
1690
1691        let mut i: usize = 0;
1692        let mut s: u32 = 0;
1693
1694        #[inline(always)]
1695        fn unpack_half(input: u8s_16) -> u8s_32 {
1696            let combined = diskann_wide::LoHi::new(input, input >> 4).zip::<u8s_32>();
1697            combined & u8s_32::splat(input.arch(), (1u8 << 4) - 1)
1698        }
1699
1700        // Each block processes 32 elements: 32 bytes from x, 16 packed bytes from y.
1701        let blocks = len / 32;
1702        if blocks > 0 {
1703            let mut acc = i32s::default(arch);
1704
1705            let products = |x: u8s_32, y: u8s_32| -> i16s {
1706                // SAFETY: `arch` is V3 (AVX2), which provides `_mm256_maddubs_epi16`.
1707                i16s::from_underlying(arch, unsafe {
1708                    _mm256_maddubs_epi16(x.to_underlying(), y.to_underlying())
1709                })
1710            };
1711
1712            let ones = i16s::splat(arch, 1);
1713
1714            // Main loop: 4× unrolled, processing 128 elements per iteration.
1715            //
1716            // `products` returns i16 lanes, each at most 255 × 15 × 2 = 7_650.
1717            // We sum 4 such values in i16 before widening to i32:
1718            //   4 × 7_650 = 30_600 < i16::MAX (32_767)
1719            while i + 4 <= blocks {
1720                // Block 0
1721                // SAFETY: `i + 4 <= blocks` guarantees at least 4 full blocks remain.
1722                // Each block needs 32 bytes from `px` and 16 bytes from `py`.
1723                let vx = unsafe { u8s_32::load_simd(arch, px.add(32 * i)) };
1724                // SAFETY: `i + 4 <= blocks` guarantees 16 bytes readable at `py.add(16 * i)`.
1725                let vy = unsafe { u8s_16::load_simd(arch, py.add(16 * i)) };
1726                let m0 = products(vx, unpack_half(vy));
1727
1728                // Block 1
1729                // SAFETY: `i + 1 < i + 4 <= blocks`.
1730                let vx = unsafe { u8s_32::load_simd(arch, px.add(32 * (i + 1))) };
1731                // SAFETY: same bound; 16 bytes readable at `py.add(16 * (i + 1))`.
1732                let vy = unsafe { u8s_16::load_simd(arch, py.add(16 * (i + 1))) };
1733                let m1 = products(vx, unpack_half(vy));
1734
1735                // Block 2
1736                // SAFETY: `i + 2 < i + 4 <= blocks`.
1737                let vx = unsafe { u8s_32::load_simd(arch, px.add(32 * (i + 2))) };
1738                // SAFETY: same bound; 16 bytes readable at `py.add(16 * (i + 2))`.
1739                let vy = unsafe { u8s_16::load_simd(arch, py.add(16 * (i + 2))) };
1740                let m2 = products(vx, unpack_half(vy));
1741
1742                // Block 3
1743                // SAFETY: `i + 3 < i + 4 <= blocks`.
1744                let vx = unsafe { u8s_32::load_simd(arch, px.add(32 * (i + 3))) };
1745                // SAFETY: same bound; 16 bytes readable at `py.add(16 * (i + 3))`.
1746                let vy = unsafe { u8s_16::load_simd(arch, py.add(16 * (i + 3))) };
1747                let m3 = products(vx, unpack_half(vy));
1748
1749                acc = acc.dot_simd(m0 + m1 + m2 + m3, ones);
1750                i += 4;
1751            }
1752
1753            // Drain remaining full 32-element blocks (0..3 iterations).
1754            while i < blocks {
1755                // SAFETY: `i < blocks` guarantees 32 bytes from `px` and 16 bytes
1756                // from `py` are readable at this offset.
1757                let vx = unsafe { u8s_32::load_simd(arch, px.add(32 * i)) };
1758                // SAFETY: `i < blocks` guarantees 16 bytes readable at `py.add(16 * i)`.
1759                let vy = unsafe { u8s_16::load_simd(arch, py.add(16 * i)) };
1760                acc = acc.dot_simd(products(vx, unpack_half(vy)), ones);
1761                i += 1;
1762            }
1763
1764            s = acc.sum_tree() as u32;
1765        }
1766
1767        // Convert block count to element index.
1768        i *= 32;
1769
1770        // Scalar fallback for the remaining < 32 elements.
1771        if i != len {
1772            #[inline(never)]
1773            fn fallback(x: USlice<'_, 8>, y: USlice<'_, 4>, from: usize) -> u32 {
1774                let mut s: u32 = 0;
1775                for i in from..x.len() {
1776                    // SAFETY: `i` is bounded by `x.len()`.
1777                    let ix = unsafe { x.get_unchecked(i) } as u32;
1778                    // SAFETY: `i` is bounded by `x.len()` which equals `y.len()`.
1779                    let iy = unsafe { y.get_unchecked(i) } as u32;
1780                    s += ix * iy;
1781                }
1782                s
1783            }
1784            s += fallback(x, y, i);
1785        }
1786
1787        Ok(MV::new(s))
1788    }
1789}
1790
1791#[cfg(target_arch = "x86_64")]
1792impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 8>, USlice<'_, 2>>
1793    for InnerProduct
1794{
1795    /// Computes the inner product of 8-bit unsigned × 2-bit unsigned vectors using AVX2.
1796    ///
1797    /// # Strategy
1798    ///
1799    /// Unpack each 16-byte chunk of `y` into 64 crumb values via a two-level cascade:
1800    /// first [`unpack_half_bytes`] splits bytes into nibbles, then a second pass splits
1801    /// nibbles into crumbs (masked with `0x03`). Each unpacked half is paired with 32
1802    /// bytes of `x` and multiplied via `_mm256_maddubs_epi16`.
1803    ///
1804    /// The main loop is 4× unrolled: eight i16 products (4 blocks × 2 halves) are summed
1805    /// in i16 before a single `_mm256_madd_epi16(…, 1)` widens to i32. This is safe
1806    /// because 8 × (255 × 3 × 2) = 12_240 < i16::MAX.
1807    #[inline(always)]
1808    fn run(
1809        self,
1810        arch: diskann_wide::arch::x86_64::V3,
1811        x: USlice<'_, 8>,
1812        y: USlice<'_, 2>,
1813    ) -> MathematicalResult<u32> {
1814        use diskann_wide::SplitJoin;
1815        use std::arch::x86_64::_mm256_maddubs_epi16;
1816
1817        let len = check_lengths!(x, y)?;
1818
1819        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
1820        diskann_wide::alias!(u8s_16 = <diskann_wide::arch::x86_64::V3>::u8x16);
1821        diskann_wide::alias!(u8s_32 = <diskann_wide::arch::x86_64::V3>::u8x32);
1822        diskann_wide::alias!(i16s = <diskann_wide::arch::x86_64::V3>::i16x16);
1823
1824        let px: *const u8 = x.as_ptr();
1825        let py: *const u8 = y.as_ptr();
1826
1827        let mut i: usize = 0;
1828        let mut s: u32 = 0;
1829
1830        // Each block processes 64 elements: 64 bytes from x, 16 packed bytes from y.
1831        let blocks = len / 64;
1832        if blocks > 0 {
1833            let mut acc = i32s::default(arch);
1834
1835            let products = |x: u8s_32, y: u8s_32| -> i16s {
1836                // SAFETY: `arch` is V3 (AVX2), which provides `_mm256_maddubs_epi16`.
1837                i16s::from_underlying(arch, unsafe {
1838                    _mm256_maddubs_epi16(x.to_underlying(), y.to_underlying())
1839                })
1840            };
1841
1842            #[inline(always)]
1843            fn unpack_sub<const N: u8>(input: u8s_16) -> u8s_32 {
1844                let combined = diskann_wide::LoHi::new(input, input >> N).zip::<u8s_32>();
1845                combined & u8s_32::splat(input.arch(), (1u8 << N) - 1)
1846            }
1847
1848            let unpack_crumbs = |x: u8s_16| -> (u8s_32, u8s_32) {
1849                // Level 1: nibble split → 32 × 4-bit values in a u8x32.
1850                let nibbles = unpack_sub::<4>(x);
1851
1852                // Split the u8x32 into two u8x16 halves (lo = elements 0..15,
1853                // hi = elements 16..31), then apply Level 2 crumb split to each.
1854                let diskann_wide::LoHi { lo, hi } = nibbles.split();
1855                let lower = unpack_sub::<2>(lo);
1856                let upper = unpack_sub::<2>(hi);
1857
1858                (lower, upper)
1859            };
1860
1861            let ones = i16s::splat(arch, 1);
1862
1863            // Main loop: 4× unrolled, processing 256 elements per iteration.
1864            //
1865            // `products` returns i16 lanes, each at most 255 × 3 × 2 = 1_530.
1866            // Each iteration produces 4 blocks × 2 products = 8 values.
1867            // We sum all 8 in i16 before widening to i32:
1868            //   8 × 1_530 = 12_240 < i16::MAX (32_767)
1869            while i + 4 <= blocks {
1870                // Block 0
1871                // SAFETY: `i + 4 <= blocks` guarantees at least 4 full blocks remain.
1872                // Each block needs 64 bytes from `px` and 16 bytes from `py`.
1873                let (vx0, vx1, (vy0, vy1)) = unsafe {
1874                    (
1875                        u8s_32::load_simd(arch, px.add(64 * i)),
1876                        u8s_32::load_simd(arch, px.add(64 * i + 32)),
1877                        unpack_crumbs(u8s_16::load_simd(arch, py.add(16 * i))),
1878                    )
1879                };
1880                let m0a = products(vx0, vy0);
1881                let m0b = products(vx1, vy1);
1882
1883                // Block 1
1884                // SAFETY: `i + 1 < i + 4 <= blocks`.
1885                let (vx0, vx1, (vy0, vy1)) = unsafe {
1886                    (
1887                        u8s_32::load_simd(arch, px.add(64 * (i + 1))),
1888                        u8s_32::load_simd(arch, px.add(64 * (i + 1) + 32)),
1889                        unpack_crumbs(u8s_16::load_simd(arch, py.add(16 * (i + 1)))),
1890                    )
1891                };
1892                let m1a = products(vx0, vy0);
1893                let m1b = products(vx1, vy1);
1894
1895                // Block 2
1896                // SAFETY: `i + 2 < i + 4 <= blocks`.
1897                let (vx0, vx1, (vy0, vy1)) = unsafe {
1898                    (
1899                        u8s_32::load_simd(arch, px.add(64 * (i + 2))),
1900                        u8s_32::load_simd(arch, px.add(64 * (i + 2) + 32)),
1901                        unpack_crumbs(u8s_16::load_simd(arch, py.add(16 * (i + 2)))),
1902                    )
1903                };
1904                let m2a = products(vx0, vy0);
1905                let m2b = products(vx1, vy1);
1906
1907                // Block 3
1908                // SAFETY: `i + 3 < i + 4 <= blocks`.
1909                let (vx0, vx1, (vy0, vy1)) = unsafe {
1910                    (
1911                        u8s_32::load_simd(arch, px.add(64 * (i + 3))),
1912                        u8s_32::load_simd(arch, px.add(64 * (i + 3) + 32)),
1913                        unpack_crumbs(u8s_16::load_simd(arch, py.add(16 * (i + 3)))),
1914                    )
1915                };
1916                let m3a = products(vx0, vy0);
1917                let m3b = products(vx1, vy1);
1918
1919                acc = acc.dot_simd((m0a + m0b + m1a + m1b) + (m2a + m2b + m3a + m3b), ones);
1920                i += 4;
1921            }
1922
1923            // Drain remaining full 64-element blocks (0..3 iterations).
1924            while i < blocks {
1925                // SAFETY: `i < blocks` guarantees 64 bytes from `px` and 16 bytes
1926                // from `py` are readable at this offset.
1927                let (vx0, vx1, (vy0, vy1)) = unsafe {
1928                    (
1929                        u8s_32::load_simd(arch, px.add(64 * i)),
1930                        u8s_32::load_simd(arch, px.add(64 * i + 32)),
1931                        unpack_crumbs(u8s_16::load_simd(arch, py.add(16 * i))),
1932                    )
1933                };
1934                acc = acc.dot_simd(products(vx0, vy0) + products(vx1, vy1), ones);
1935                i += 1;
1936            }
1937
1938            s = acc.sum_tree() as u32;
1939        }
1940
1941        // Convert block count to element index.
1942        i *= 64;
1943
1944        // Scalar fallback for the remaining < 64 elements.
1945        if i != len {
1946            #[inline(never)]
1947            fn fallback(x: USlice<'_, 8>, y: USlice<'_, 2>, from: usize) -> u32 {
1948                let mut s: u32 = 0;
1949                for i in from..x.len() {
1950                    // SAFETY: `i` is in `from..x.len()`, which equals `y.len()`.
1951                    let (ix, iy) =
1952                        unsafe { (x.get_unchecked(i) as u32, y.get_unchecked(i) as u32) };
1953                    s += ix * iy;
1954                }
1955                s
1956            }
1957            s += fallback(x, y, i);
1958        }
1959
1960        Ok(MV::new(s))
1961    }
1962}
1963
1964#[cfg(target_arch = "x86_64")]
1965impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_, 8>, USlice<'_, 1>>
1966    for InnerProduct
1967{
1968    /// Computes the inner product of 8-bit unsigned × 1-bit unsigned vectors using V3 intrinsics.
1969    ///
1970    /// For each 32-element block we load 32 bytes from `x` and 4 bytes (32 bits) from `y`.
1971    /// ANDing the data with the mask created from 4 bytes from `y` zeroes unselected lanes.
1972    /// Finally, `_mm256_sad_epu8` horizontally sums the masked bytes in groups of 8.
1973    ///
1974    /// The main loop is 4× unrolled, processing 128 elements per iteration.
1975    ///
1976    /// ## Overflow
1977    ///
1978    /// Each `sad` output lane holds at most `8 × 255 = 2_040`. Accumulated across `d/32`
1979    /// blocks, the per-lane max is `(d/32) × 2_040`. At dim = 3072: `96 × 2_040 = 195_840`,
1980    /// well within i32 range.
1981    #[inline(always)]
1982    fn run(
1983        self,
1984        arch: diskann_wide::arch::x86_64::V3,
1985        x: USlice<'_, 8>,
1986        y: USlice<'_, 1>,
1987    ) -> MathematicalResult<u32> {
1988        use diskann_wide::{FromInt, SIMDMask};
1989        use std::arch::x86_64::_mm256_sad_epu8;
1990
1991        let len = check_lengths!(x, y)?;
1992
1993        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
1994        diskann_wide::alias!(u8s_32 = <diskann_wide::arch::x86_64::V3>::u8x32);
1995
1996        type Mask32 = diskann_wide::BitMask<32, diskann_wide::arch::x86_64::V3>;
1997        type Mask8x32 = diskann_wide::arch::x86_64::v3::masks::mask8x32;
1998
1999        let px: *const u8 = x.as_ptr();
2000        let py: *const u8 = y.as_ptr();
2001
2002        let mut i: usize = 0;
2003        let mut s: u32 = 0;
2004
2005        // Each block processes 32 elements: 32 bytes from x, 4 bytes (32 bits) from y.
2006        let blocks = len / 32;
2007        if blocks > 0 {
2008            let mut acc = i32s::default(arch);
2009            let zero = u8s_32::default(arch);
2010
2011            // Expand 32 bits of `y` to a byte mask, AND with `x`, and horizontally sum
2012            // the masked bytes via `_mm256_sad_epu8`.
2013            //
2014            // Returns an `i32x8` where lanes 0, 2, 4, 6 hold group sums (each ≤ 2040)
2015            // and lanes 1, 3, 5, 7 are zero.
2016            let masked_sad = |vx: u8s_32, bits: u32| -> i32s {
2017                // Expand 32-bit mask → 32 bytes of 0xFF/0x00.
2018                let byte_mask: Mask8x32 = Mask32::from_int(arch, bits).into();
2019
2020                // AND data with mask: zeroes lanes where the bit is 0.
2021                let masked = vx & u8s_32::from_underlying(arch, byte_mask.to_underlying());
2022
2023                // Horizontal byte sum in groups of 8 → 4 × u64 partial sums.
2024                // SAFETY: `arch` is V3 (AVX2), `_mm256_sad_epu8` is available.
2025                i32s::from_underlying(arch, unsafe {
2026                    _mm256_sad_epu8(masked.to_underlying(), zero.to_underlying())
2027                })
2028            };
2029
2030            // Main loop: 4× unrolled, processing 128 elements per iteration.
2031            while i + 4 <= blocks {
2032                // SAFETY: `i + 4 <= blocks` guarantees at least 4 full blocks remain.
2033                // Each block needs 32 bytes from `px` and 4 bytes from `py`.
2034                let s0 = unsafe {
2035                    let vx = u8s_32::load_simd(arch, px.add(32 * i));
2036                    let bits = (py.add(4 * i) as *const u32).read_unaligned();
2037                    masked_sad(vx, bits)
2038                };
2039
2040                // SAFETY: `i + 1 < i + 4 <= blocks`.
2041                let s1 = unsafe {
2042                    let vx = u8s_32::load_simd(arch, px.add(32 * (i + 1)));
2043                    let bits = (py.add(4 * (i + 1)) as *const u32).read_unaligned();
2044                    masked_sad(vx, bits)
2045                };
2046
2047                // SAFETY: `i + 2 < i + 4 <= blocks`.
2048                let s2 = unsafe {
2049                    let vx = u8s_32::load_simd(arch, px.add(32 * (i + 2)));
2050                    let bits = (py.add(4 * (i + 2)) as *const u32).read_unaligned();
2051                    masked_sad(vx, bits)
2052                };
2053
2054                // SAFETY: `i + 3 < i + 4 <= blocks`.
2055                let s3 = unsafe {
2056                    let vx = u8s_32::load_simd(arch, px.add(32 * (i + 3)));
2057                    let bits = (py.add(4 * (i + 3)) as *const u32).read_unaligned();
2058                    masked_sad(vx, bits)
2059                };
2060
2061                acc = acc + s0 + s1 + s2 + s3;
2062                i += 4;
2063            }
2064
2065            // Drain remaining full 32-element blocks (0..3 iterations).
2066            while i < blocks {
2067                // SAFETY: `i < blocks` guarantees 32 bytes from `px` and 4 bytes from `py`
2068                // are readable at this offset.
2069                let si = unsafe {
2070                    let vx = u8s_32::load_simd(arch, px.add(32 * i));
2071                    let bits = (py.add(4 * i) as *const u32).read_unaligned();
2072                    masked_sad(vx, bits)
2073                };
2074                acc = acc + si;
2075                i += 1;
2076            }
2077
2078            s = acc.sum_tree() as u32;
2079        }
2080
2081        // Convert block count to element index.
2082        i *= 32;
2083
2084        // Scalar fallback for the remaining < 32 elements.
2085        if i != len {
2086            #[inline(never)]
2087            fn fallback(x: USlice<'_, 8>, y: USlice<'_, 1>, from: usize) -> u32 {
2088                let mut s: u32 = 0;
2089                for i in from..x.len() {
2090                    // SAFETY: `i` is in `from..x.len()`, which equals `y.len()`.
2091                    let (ix, iy) =
2092                        unsafe { (x.get_unchecked(i) as u32, y.get_unchecked(i) as u32) };
2093                    s += ix * iy;
2094                }
2095                s
2096            }
2097            s += fallback(x, y, i);
2098        }
2099
2100        Ok(MV::new(s))
2101    }
2102}
2103
2104#[cfg(target_arch = "aarch64")]
2105retarget!(
2106    diskann_wide::arch::aarch64::Neon,
2107    InnerProduct,
2108    7,
2109    6,
2110    5,
2111    4,
2112    3,
2113    2,
2114    (8, 4),
2115    (8, 2),
2116    (8, 1)
2117);
2118
2119//////////////////
2120// BitTranspose //
2121//////////////////
2122
2123/// The strategy is to compute the inner product `<x, y>` by decomposing the problem into
2124/// groups of 64-dimensions.
2125///
2126/// For each group, we load the 64-bits of `y` into a word `bits`. And the four 64-bit words
2127/// of the group in `x` in `b0`, `b1`, b2`, and `b3`.
2128///
2129/// Note that bit `i` in `b0` is bit-0 of the `i`-th value in ths group. Likewise, bit `i`
2130/// in `b1` is bit-1 of the same word.
2131///
2132/// This means that we can compute the partial inner product for this group as
2133/// ```math
2134/// (bits & b0).count_ones()                // Contribution of bit 0
2135///     + 2 * (bits & b1).count_ones()      // Contribution of bit 1
2136///     + 4 * (bits & b2).count_ones()      // Contribution of bit 2
2137///     + 8 * (bits & b3).count_ones()      // Contribution of bit 3
2138/// ```
2139/// We process as many full groups as we can.
2140///
2141/// To handle the remainder, we need to be careful about acessing `y` because `BitSlice`
2142/// only guarantees the validity of reads at the byte level. That is - we cannot assume that
2143/// a full 64-bit read is valid.
2144///
2145/// The bit-tranposed `x`, on the other hand, guarantees allocations in blocks of
2146/// 4 * 64-bits, so it can be treated as normal.
2147impl<A> Target2<A, MathematicalResult<u32>, USlice<'_, 4, BitTranspose>, USlice<'_, 1, Dense>>
2148    for InnerProduct
2149where
2150    A: Architecture,
2151{
2152    #[inline(always)]
2153    fn run(
2154        self,
2155        _: A,
2156        x: USlice<'_, 4, BitTranspose>,
2157        y: USlice<'_, 1, Dense>,
2158    ) -> MathematicalResult<u32> {
2159        let len = check_lengths!(x, y)?;
2160
2161        // We work in blocks of 64 element.
2162        //
2163        // The `BitTranspose` guarantees read are valid in blocks of 64 elements (32 byte).
2164        // However, the `Dense` representation only pads to bytes.
2165        // Our strategy for dealing with fewer than 64 remaining elements is to reconstruct
2166        // a 64-bit integer from bytes.
2167        let px: *const u64 = x.as_ptr().cast();
2168        let py: *const u64 = y.as_ptr().cast();
2169
2170        let mut i = 0;
2171        let mut s: u32 = 0;
2172
2173        let blocks = len / 64;
2174        while i < blocks {
2175            // SAFETY: `y` is valid for at least `blocks` 64-bit reads and `i < blocks`.
2176            let bits = unsafe { py.add(i).read_unaligned() };
2177
2178            // SAFETY: The layout for `x` is grouped into 32-byte blocks. We've ensured that
2179            // the lengths of the two vectors are the same, so we know that `x` has at least
2180            // `blocks` such regions.
2181            //
2182            // This loads the first 64-bits of block `i` where `i < blocks`.
2183            let b0 = unsafe { px.add(4 * i).read_unaligned() };
2184            s += (bits & b0).count_ones();
2185
2186            // SAFETY: This loads the second 64-bit word of block `i`.
2187            let b1 = unsafe { px.add(4 * i + 1).read_unaligned() };
2188            s += (bits & b1).count_ones() << 1;
2189
2190            // SAFETY: This loads the third 64-bit word of block `i`.
2191            let b2 = unsafe { px.add(4 * i + 2).read_unaligned() };
2192            s += (bits & b2).count_ones() << 2;
2193
2194            // SAFETY: This loads the fourth 64-bit word of block `i`.
2195            let b3 = unsafe { px.add(4 * i + 3).read_unaligned() };
2196            s += (bits & b3).count_ones() << 3;
2197
2198            i += 1;
2199        }
2200
2201        // If the input length is a multiple of 64 - then we're done.
2202        if 64 * i == len {
2203            return Ok(MV::new(s));
2204        }
2205
2206        // Convert blocks to bytes.
2207        let k = i * 8;
2208
2209        // Unpack the last elements from the bit-vector.
2210        //
2211        // SAFETY: The length of the 1-bit BitSlice is `ceil(len / 8)`. This computation
2212        // effectively computes `ceil((64 * floor(len / 64)) / 8)`, which is less.
2213        let py = unsafe { py.cast::<u8>().add(k) };
2214        let bytes_remaining = y.bytes() - k;
2215        let mut bits: u64 = 0;
2216
2217        // Code - generation: Applying `min(8)` gives a constant upper-bound to the
2218        // compiler, allowing better code-generation.
2219        for j in 0..bytes_remaining.min(8) {
2220            // SAFETY: Starting at `py`, there are `bytes_remaining` valid bytes. This
2221            // accesses all of them.
2222            bits += (unsafe { py.add(j).read() } as u64) << (8 * j);
2223        }
2224
2225        // Because the upper-bits of the last loaded byte can contain indeterminate bits,
2226        // we must mask out all out-of-bounds bits.
2227        bits &= (0x01u64 << (len - (64 * i))) - 1;
2228
2229        // Combine with the remainders.
2230        //
2231        // SAFETY: The `BitTranspose` permutation always allocates in granularies of blocks.
2232        // This loads the first 64-bit word of the last block.
2233        let b0 = unsafe { px.add(4 * i).read_unaligned() };
2234        s += (bits & b0).count_ones();
2235
2236        // SAFETY: This loads the second 64-bit word of the last block.
2237        let b1 = unsafe { px.add(4 * i + 1).read_unaligned() };
2238        s += (bits & b1).count_ones() << 1;
2239
2240        // SAFETY: This loads the third 64-bit word of the last block.
2241        let b2 = unsafe { px.add(4 * i + 2).read_unaligned() };
2242        s += (bits & b2).count_ones() << 2;
2243
2244        // SAFETY: This loads the fourth 64-bit word of the last block.
2245        let b3 = unsafe { px.add(4 * i + 3).read_unaligned() };
2246        s += (bits & b3).count_ones() << 3;
2247
2248        Ok(MV::new(s))
2249    }
2250}
2251
2252impl
2253    PureDistanceFunction<USlice<'_, 4, BitTranspose>, USlice<'_, 1, Dense>, MathematicalResult<u32>>
2254    for InnerProduct
2255{
2256    fn evaluate(
2257        x: USlice<'_, 4, BitTranspose>,
2258        y: USlice<'_, 1, Dense>,
2259    ) -> MathematicalResult<u32> {
2260        (diskann_wide::ARCH).run2(Self, x, y)
2261    }
2262}
2263
2264////////////////////
2265// Full Precision //
2266////////////////////
2267
2268/// The main trick here is avoiding explicit conversion from 1 bit integers to 32-bit
2269/// floating-point numbers by using `_mm256_permutevar_ps`, which performs a shuffle on two
2270/// independent 128-bit lanes of `f32` values in a register `A` using the lower 2-bits of
2271/// each 32-bit integer in a register `B`.
2272///
2273/// Importantly, this instruction only takes a single cycle and we can avoid any kind of
2274/// masking. Going the route of conversion would require and `AND` operation to isolate
2275/// bottom bits and a somewhat lengthy 32-bit integer to `f32` conversion instruction.
2276///
2277/// The overall strategy broadcasts a 32-bit integer (consisting of 32, 1-bit values) across
2278/// 8 lanes into a register `A`.
2279///
2280/// Each lane is then shifted by a different amount so:
2281///
2282/// * Lane 0 has value 0 as its least significant bit (LSB)
2283/// * Lane 1 has value 1 as its LSB.
2284/// * Lane 2 has value 2 as its LSB.
2285/// * etc.
2286///
2287/// These LSB's are used to power the shuffle function to convert to `f32` values (either
2288/// 0.0 or 1.0) and we can FMA as needed.
2289///
2290/// To process the next group of 8 values, we shift all lanes in `A` by 8-bits so lane 0
2291/// has value 8 as its LSB, lane 1 has value 9 etc.
2292///
2293/// A total of three shifts are applied to extract all 32 1-bit value as `f32` in order.
2294#[cfg(target_arch = "x86_64")]
2295impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<f32>, &[f32], USlice<'_, 1>>
2296    for InnerProduct
2297{
2298    #[inline(always)]
2299    fn run(
2300        self,
2301        arch: diskann_wide::arch::x86_64::V3,
2302        x: &[f32],
2303        y: USlice<'_, 1>,
2304    ) -> MathematicalResult<f32> {
2305        let len = check_lengths!(x, y)?;
2306
2307        use std::arch::x86_64::*;
2308
2309        diskann_wide::alias!(f32s = <diskann_wide::arch::x86_64::V3>::f32x8);
2310        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
2311
2312        // Replicate 0s and 1s so we effectively get a shuffle that only depends on the
2313        // bottom bit (instead of the lowest 2).
2314        let values = f32s::from_array(arch, [0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0]);
2315
2316        // Shifts required to offset each lane.
2317        let variable_shifts = u32s::from_array(arch, [0, 1, 2, 3, 4, 5, 6, 7]);
2318
2319        let px: *const f32 = x.as_ptr();
2320        let py: *const u32 = y.as_ptr().cast();
2321
2322        let mut i = 0;
2323        let mut s = f32s::default(arch);
2324
2325        let prep = |v: u32| -> u32s { u32s::splat(arch, v) >> variable_shifts };
2326        let to_f32 = |v: u32s| -> f32s {
2327            // SAFETY: The `_mm256_permutevar_ps` instruction requires the AVX extension,
2328            // which the presence of the `x86_64::V3` architecture guarantees is available.
2329            f32s::from_underlying(arch, unsafe {
2330                _mm256_permutevar_ps(values.to_underlying(), v.to_underlying())
2331            })
2332        };
2333
2334        // Data is processed in groups of 32 elements.
2335        let blocks = len / 32;
2336        if i < blocks {
2337            let mut s0 = f32s::default(arch);
2338            let mut s1 = f32s::default(arch);
2339
2340            while i < blocks {
2341                // SAFETY: `i < blocks` implies 32-bits are readable from this offset.
2342                let iy = prep(unsafe { py.add(i).read_unaligned() });
2343
2344                // SAFETY: `i < blocks` implies 32 f32 values are readable beginning at `32*i`.
2345                let ix0 = unsafe { f32s::load_simd(arch, px.add(32 * i)) };
2346                // SAFETY: See above.
2347                let ix1 = unsafe { f32s::load_simd(arch, px.add(32 * i + 8)) };
2348                // SAFETY: See above.
2349                let ix2 = unsafe { f32s::load_simd(arch, px.add(32 * i + 16)) };
2350                // SAFETY: See above.
2351                let ix3 = unsafe { f32s::load_simd(arch, px.add(32 * i + 24)) };
2352
2353                s0 = ix0.mul_add_simd(to_f32(iy), s0);
2354                s1 = ix1.mul_add_simd(to_f32(iy >> 8), s1);
2355                s0 = ix2.mul_add_simd(to_f32(iy >> 16), s0);
2356                s1 = ix3.mul_add_simd(to_f32(iy >> 24), s1);
2357
2358                i += 1;
2359            }
2360            s = s0 + s1;
2361        }
2362
2363        let remainder = len % 32;
2364        if remainder != 0 {
2365            let tail = if len % 8 == 0 { 8 } else { len % 8 };
2366
2367            // SAFETY: Because `remainder != 0`, there is valid memory beginning at the
2368            // offset `blocks`, so this addition remains within an allocated object.
2369            let py = unsafe { py.add(blocks) };
2370
2371            if remainder <= 8 {
2372                // SAFETY: Non-zero remainder implies at least one byte is readable for `py`.
2373                // The same logic applies to the SIMD loads.
2374                unsafe {
2375                    load_one(py, |iy| {
2376                        let iy = prep(iy);
2377                        let ix = f32s::load_simd_first(arch, px.add(32 * blocks), tail);
2378                        s = ix.mul_add_simd(to_f32(iy), s);
2379                    })
2380                }
2381            } else if remainder <= 16 {
2382                // SAFETY: At least two bytes are readable for `py`.
2383                // The same logic applies to the SIMD loads.
2384                unsafe {
2385                    load_two(py, |iy| {
2386                        let iy = prep(iy);
2387                        let ix0 = f32s::load_simd(arch, px.add(32 * blocks));
2388                        let ix1 = f32s::load_simd_first(arch, px.add(32 * blocks + 8), tail);
2389                        s = ix0.mul_add_simd(to_f32(iy), s);
2390                        s = ix1.mul_add_simd(to_f32(iy >> 8), s);
2391                    })
2392                }
2393            } else if remainder <= 24 {
2394                // SAFETY: At least three bytes are readable for `py`.
2395                // The same logic applies to the SIMD loads.
2396                unsafe {
2397                    load_three(py, |iy| {
2398                        let iy = prep(iy);
2399
2400                        let ix0 = f32s::load_simd(arch, px.add(32 * blocks));
2401                        let ix1 = f32s::load_simd(arch, px.add(32 * blocks + 8));
2402                        let ix2 = f32s::load_simd_first(arch, px.add(32 * blocks + 16), tail);
2403
2404                        s = ix0.mul_add_simd(to_f32(iy), s);
2405                        s = ix1.mul_add_simd(to_f32(iy >> 8), s);
2406                        s = ix2.mul_add_simd(to_f32(iy >> 16), s);
2407                    })
2408                }
2409            } else {
2410                // SAFETY: At least four bytes are readable for `py`.
2411                // The same logic applies to the SIMD loads.
2412                unsafe {
2413                    load_four(py, |iy| {
2414                        let iy = prep(iy);
2415
2416                        let ix0 = f32s::load_simd(arch, px.add(32 * blocks));
2417                        let ix1 = f32s::load_simd(arch, px.add(32 * blocks + 8));
2418                        let ix2 = f32s::load_simd(arch, px.add(32 * blocks + 16));
2419                        let ix3 = f32s::load_simd_first(arch, px.add(32 * blocks + 24), tail);
2420
2421                        s = ix0.mul_add_simd(to_f32(iy), s);
2422                        s = ix1.mul_add_simd(to_f32(iy >> 8), s);
2423                        s = ix2.mul_add_simd(to_f32(iy >> 16), s);
2424                        s = ix3.mul_add_simd(to_f32(iy >> 24), s);
2425                    })
2426                }
2427            }
2428        }
2429
2430        Ok(MV::new(s.sum_tree()))
2431    }
2432}
2433
2434/// The strategy used here is almost identical to that used for 1-bit distances. The main
2435/// difference is that now we use the full 2-bit shuffle capabilities of `_mm256_permutevar_ps`
2436/// and ths relatives sizes of the shifts are slightly different.
2437#[cfg(target_arch = "x86_64")]
2438impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<f32>, &[f32], USlice<'_, 2>>
2439    for InnerProduct
2440{
2441    #[inline(always)]
2442    fn run(
2443        self,
2444        arch: diskann_wide::arch::x86_64::V3,
2445        x: &[f32],
2446        y: USlice<'_, 2>,
2447    ) -> MathematicalResult<f32> {
2448        let len = check_lengths!(x, y)?;
2449
2450        use std::arch::x86_64::*;
2451
2452        diskann_wide::alias!(f32s = <diskann_wide::arch::x86_64::V3>::f32x8);
2453        diskann_wide::alias!(u32s = <diskann_wide::arch::x86_64::V3>::u32x8);
2454
2455        // This is the lookup table mapping 2-bit patterns to their equivalent `f32`
2456        // representation. The AVX2 shuffle only applies within each 128-bit group of the
2457        // full 256-bit register, so we replicate the contents.
2458        let values = f32s::from_array(arch, [0.0, 1.0, 2.0, 3.0, 0.0, 1.0, 2.0, 3.0]);
2459
2460        // Shifts required to get logical dimensions shifted to the lower 2-bits of each lane.
2461        let variable_shifts = u32s::from_array(arch, [0, 2, 4, 6, 8, 10, 12, 14]);
2462
2463        let px: *const f32 = x.as_ptr();
2464        let py: *const u32 = y.as_ptr().cast();
2465
2466        let mut i = 0;
2467        let mut s = f32s::default(arch);
2468
2469        let prep = |v: u32| -> u32s { u32s::splat(arch, v) >> variable_shifts };
2470        let to_f32 = |v: u32s| -> f32s {
2471            // SAFETY: The `_mm256_permutevar_ps` instruction requires the AVX extension,
2472            // which the presense of the `x86_64::V3` architecture guarantees is available.
2473            f32s::from_underlying(arch, unsafe {
2474                _mm256_permutevar_ps(values.to_underlying(), v.to_underlying())
2475            })
2476        };
2477
2478        let blocks = len / 16;
2479        if blocks != 0 {
2480            let mut s0 = f32s::default(arch);
2481            let mut s1 = f32s::default(arch);
2482
2483            // Process 32 elements.
2484            while i + 2 <= blocks {
2485                // SAFETY: `i + 2 <= blocks` implies `py.add(i)` is in-bounds and readable
2486                // for 4 unaligned bytes.
2487                let iy = prep(unsafe { py.add(i).read_unaligned() });
2488
2489                // SAFETY: Same logic as above, just applied to `f32` values instead of
2490                // packed bits.
2491                let (ix0, ix1) = unsafe {
2492                    (
2493                        f32s::load_simd(arch, px.add(16 * i)),
2494                        f32s::load_simd(arch, px.add(16 * i + 8)),
2495                    )
2496                };
2497
2498                s0 = ix0.mul_add_simd(to_f32(iy), s0);
2499                s1 = ix1.mul_add_simd(to_f32(iy >> 16), s1);
2500
2501                // SAFETY: `i + 2 <= blocks` implies `py.add(i + 1)` is in-bounds and readable
2502                // for 4 unaligned bytes.
2503                let iy = prep(unsafe { py.add(i + 1).read_unaligned() });
2504
2505                // SAFETY: Same logic as above.
2506                let (ix0, ix1) = unsafe {
2507                    (
2508                        f32s::load_simd(arch, px.add(16 * (i + 1))),
2509                        f32s::load_simd(arch, px.add(16 * (i + 1) + 8)),
2510                    )
2511                };
2512
2513                s0 = ix0.mul_add_simd(to_f32(iy), s0);
2514                s1 = ix1.mul_add_simd(to_f32(iy >> 16), s1);
2515
2516                i += 2;
2517            }
2518
2519            // Process 16 elements
2520            if i < blocks {
2521                // SAFETY: `i < blocks` implies `py.add(i)` is in-bounds and readable for
2522                // 4 unaligned bytes.
2523                let iy = prep(unsafe { py.add(i).read_unaligned() });
2524
2525                // SAFETY: Same logic as above.
2526                let (ix0, ix1) = unsafe {
2527                    (
2528                        f32s::load_simd(arch, px.add(16 * i)),
2529                        f32s::load_simd(arch, px.add(16 * i + 8)),
2530                    )
2531                };
2532
2533                s0 = ix0.mul_add_simd(to_f32(iy), s0);
2534                s1 = ix1.mul_add_simd(to_f32(iy >> 16), s1);
2535            }
2536
2537            s = s0 + s1;
2538        }
2539
2540        let remainder = len % 16;
2541        if remainder != 0 {
2542            let tail = if len % 8 == 0 { 8 } else { len % 8 };
2543            // SAFETY: Non-zero remainder implies there are readable bytes after the offset
2544            // `blocks`, so the addition is valid.
2545            let py = unsafe { py.add(blocks) };
2546
2547            if remainder <= 4 {
2548                // SAFETY: Non-zero remainder implies at least one byte is readable for `py`.
2549                // The same logic applies to the SIMD loads.
2550                unsafe {
2551                    load_one(py, |iy| {
2552                        let iy = prep(iy);
2553                        let ix = f32s::load_simd_first(arch, px.add(16 * blocks), tail);
2554                        s = ix.mul_add_simd(to_f32(iy), s);
2555                    });
2556                }
2557            } else if remainder <= 8 {
2558                // SAFETY: At least two bytes are readable for `py`.
2559                // The same logic applies to the SIMD loads.
2560                unsafe {
2561                    load_two(py, |iy| {
2562                        let iy = prep(iy);
2563                        let ix = f32s::load_simd_first(arch, px.add(16 * blocks), tail);
2564                        s = ix.mul_add_simd(to_f32(iy), s);
2565                    });
2566                }
2567            } else if remainder <= 12 {
2568                // SAFETY: At least three bytes are readable for `py`.
2569                // The same logic applies to the SIMD loads.
2570                unsafe {
2571                    load_three(py, |iy| {
2572                        let iy = prep(iy);
2573                        let ix0 = f32s::load_simd(arch, px.add(16 * blocks));
2574                        let ix1 = f32s::load_simd_first(arch, px.add(16 * blocks + 8), tail);
2575                        s = ix0.mul_add_simd(to_f32(iy), s);
2576                        s = ix1.mul_add_simd(to_f32(iy >> 16), s);
2577                    });
2578                }
2579            } else {
2580                // SAFETY: At least four bytes are readable for `py`.
2581                // The same logic applies to the SIMD loads.
2582                unsafe {
2583                    load_four(py, |iy| {
2584                        let iy = prep(iy);
2585                        let ix0 = f32s::load_simd(arch, px.add(16 * blocks));
2586                        let ix1 = f32s::load_simd_first(arch, px.add(16 * blocks + 8), tail);
2587                        s = ix0.mul_add_simd(to_f32(iy), s);
2588                        s = ix1.mul_add_simd(to_f32(iy >> 16), s);
2589                    });
2590                }
2591            }
2592        }
2593
2594        Ok(MV::new(s.sum_tree()))
2595    }
2596}
2597
2598/// The strategy here is similar to the 1 and 2-bit strategies. However, instead of using
2599/// `_mm256_permutevar_ps`, we now go directly for 32-bit integer to 32-bit floating point.
2600///
2601/// This is because the shuffle intrinsic only supports 2-bit shuffles.
2602#[cfg(target_arch = "x86_64")]
2603impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<f32>, &[f32], USlice<'_, 4>>
2604    for InnerProduct
2605{
2606    #[inline(always)]
2607    fn run(
2608        self,
2609        arch: diskann_wide::arch::x86_64::V3,
2610        x: &[f32],
2611        y: USlice<'_, 4>,
2612    ) -> MathematicalResult<f32> {
2613        let len = check_lengths!(x, y)?;
2614
2615        diskann_wide::alias!(f32s = <diskann_wide::arch::x86_64::V3>::f32x8);
2616        diskann_wide::alias!(i32s = <diskann_wide::arch::x86_64::V3>::i32x8);
2617
2618        let variable_shifts = i32s::from_array(arch, [0, 4, 8, 12, 16, 20, 24, 28]);
2619        let mask = i32s::splat(arch, 0x0f);
2620
2621        let to_f32 = |v: u32| -> f32s {
2622            ((i32s::splat(arch, v as i32) >> variable_shifts) & mask).simd_cast()
2623        };
2624
2625        let px: *const f32 = x.as_ptr();
2626        let py: *const u32 = y.as_ptr().cast();
2627
2628        let mut i = 0;
2629        let mut s = f32s::default(arch);
2630
2631        let blocks = len / 8;
2632        while i < blocks {
2633            // SAFETY: `i < blocks` implies that 8 `f32` values are readable from `8*i`.
2634            let ix = unsafe { f32s::load_simd(arch, px.add(8 * i)) };
2635            // SAFETY: Same logic as above - but applied to the packed bits.
2636            let iy = to_f32(unsafe { py.add(i).read_unaligned() });
2637            s = ix.mul_add_simd(iy, s);
2638
2639            i += 1;
2640        }
2641
2642        let remainder = len % 8;
2643        if remainder != 0 {
2644            let f = |iy| {
2645                // SAFETY: The epilogue handles at most 8 values. Since the remainder is
2646                // non-zero, the pointer arithmetic is in-bounds and `load_simd_first` will
2647                // avoid accessing the out-of-bounds elements.
2648                let ix = unsafe { f32s::load_simd_first(arch, px.add(8 * blocks), remainder) };
2649                s = ix.mul_add_simd(to_f32(iy), s);
2650            };
2651
2652            // SAFETY: Non-zero remainder means there are readable bytes from the offset
2653            // `blocks`.
2654            let py = unsafe { py.add(blocks) };
2655
2656            if remainder <= 2 {
2657                // SAFETY: Non-zero remainder less than 2 implies that one byte is readable.
2658                unsafe { load_one(py, f) };
2659            } else if remainder <= 4 {
2660                // SAFETY: At least two bytes are readable from `py`.
2661                unsafe { load_two(py, f) };
2662            } else if remainder <= 6 {
2663                // SAFETY: At least three bytes are readable from `py`.
2664                unsafe { load_three(py, f) };
2665            } else {
2666                // SAFETY: At least four bytes are readable from `py`.
2667                unsafe { load_four(py, f) };
2668            }
2669        }
2670
2671        Ok(MV::new(s.sum_tree()))
2672    }
2673}
2674
2675impl<const N: usize>
2676    Target2<diskann_wide::arch::Scalar, MathematicalResult<f32>, &[f32], USlice<'_, N>>
2677    for InnerProduct
2678where
2679    Unsigned: Representation<N>,
2680{
2681    /// A fallback implementation that uses scaler indexing to retrieve values from
2682    /// the corresponding `BitSlice`.
2683    #[inline(always)]
2684    fn run(
2685        self,
2686        _: diskann_wide::arch::Scalar,
2687        x: &[f32],
2688        y: USlice<'_, N>,
2689    ) -> MathematicalResult<f32> {
2690        check_lengths!(x, y)?;
2691
2692        let mut s = 0.0;
2693        for (i, x) in x.iter().enumerate() {
2694            // SAFETY: We've ensured that `x.len() == y.len()`, so this access is
2695            // always inbounds.
2696            let y = unsafe { y.get_unchecked(i) } as f32;
2697            s += x * y;
2698        }
2699
2700        Ok(MV::new(s))
2701    }
2702}
2703
2704/// Implement `Target2` for higher architecture in terms of the scalar fallback.
2705macro_rules! ip_retarget {
2706    ($arch:path, $N:literal) => {
2707        impl Target2<$arch, MathematicalResult<f32>, &[f32], USlice<'_, $N>>
2708            for InnerProduct
2709        {
2710            #[inline(always)]
2711            fn run(
2712                self,
2713                arch: $arch,
2714                x: &[f32],
2715                y: USlice<'_, $N>,
2716            ) -> MathematicalResult<f32> {
2717                self.run(arch.retarget(), x, y)
2718            }
2719        }
2720    };
2721    ($arch:path, $($Ns:literal),*) => {
2722        $(ip_retarget!($arch, $Ns);)*
2723    }
2724}
2725
2726#[cfg(target_arch = "x86_64")]
2727ip_retarget!(diskann_wide::arch::x86_64::V3, 3, 5, 6, 7, 8);
2728
2729#[cfg(target_arch = "x86_64")]
2730ip_retarget!(diskann_wide::arch::x86_64::V4, 1, 2, 3, 4, 5, 6, 7, 8);
2731
2732#[cfg(target_arch = "aarch64")]
2733ip_retarget!(diskann_wide::arch::aarch64::Neon, 1, 2, 3, 4, 5, 6, 7, 8);
2734
2735/// Delegate the implementation of `PureDistanceFunction` to `diskann_wide::arch::Target2`
2736/// with the current architectures.
2737macro_rules! dispatch_full_ip {
2738    ($N:literal) => {
2739        /// Compute the inner product between `x` and `y`.
2740        ///
2741        /// Returns an error if the arguments have different lengths.
2742        impl PureDistanceFunction<&[f32], USlice<'_, $N>, MathematicalResult<f32>>
2743            for InnerProduct
2744        {
2745            fn evaluate(x: &[f32], y: USlice<'_, $N>) -> MathematicalResult<f32> {
2746                Self.run(ARCH, x, y)
2747            }
2748        }
2749    };
2750    ($($Ns:literal),*) => {
2751        $(dispatch_full_ip!($Ns);)*
2752    }
2753}
2754
2755dispatch_full_ip!(1, 2, 3, 4, 5, 6, 7, 8);
2756
2757///////////
2758// Tests //
2759///////////
2760
2761#[cfg(test)]
2762mod tests {
2763    use std::{collections::HashMap, fmt::Display, sync::LazyLock};
2764
2765    use diskann_utils::{Reborrow, lazy_format};
2766    use rand::{
2767        Rng, SeedableRng,
2768        distr::{Distribution, Uniform},
2769        rngs::StdRng,
2770        seq::IndexedRandom,
2771    };
2772
2773    use super::*;
2774    use crate::bits::{BoxedBitSlice, Representation, Unsigned};
2775
2776    type MR = MathematicalResult<u32>;
2777
2778    #[inline(always)]
2779    fn should_check_this_dimension(dim: usize) -> bool {
2780        if cfg!(miri) {
2781            return dim.is_power_of_two()
2782                || (dim > 1 && (dim - 1).is_power_of_two())
2783                || (dim < 64 && (dim % 8 == 7));
2784        }
2785
2786        true
2787    }
2788
2789    /////////////////////////
2790    // Unsigned Bit Slices //
2791    /////////////////////////
2792
2793    // This test works by generating random integer codes for the compressed vectors,
2794    // then uses the functions implemented in `vector` to compute the expected result of
2795    // the computation in "full precision integer space".
2796    //
2797    // We verify that the exact same results are returned by each computation.
2798    fn test_bitslice_distances<const NBITS: usize, R>(
2799        dim_max: usize,
2800        trials_per_dim: usize,
2801        evaluate_l2: &dyn Fn(USlice<'_, NBITS>, USlice<'_, NBITS>) -> MR,
2802        evaluate_ip: &dyn Fn(USlice<'_, NBITS>, USlice<'_, NBITS>) -> MR,
2803        context: &str,
2804        rng: &mut R,
2805    ) where
2806        Unsigned: Representation<NBITS>,
2807        R: Rng,
2808    {
2809        let domain = Unsigned::domain_const::<NBITS>();
2810        let min: i64 = *domain.start();
2811        let max: i64 = *domain.end();
2812
2813        let dist = Uniform::new_inclusive(min, max).unwrap();
2814
2815        for dim in 0..dim_max {
2816            if !should_check_this_dimension(dim) {
2817                continue;
2818            }
2819
2820            let mut x_reference: Vec<u8> = vec![0; dim];
2821            let mut y_reference: Vec<u8> = vec![0; dim];
2822
2823            let mut x = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(dim);
2824            let mut y = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(dim);
2825
2826            for trial in 0..trials_per_dim {
2827                x_reference
2828                    .iter_mut()
2829                    .for_each(|i| *i = dist.sample(rng).try_into().unwrap());
2830                y_reference
2831                    .iter_mut()
2832                    .for_each(|i| *i = dist.sample(rng).try_into().unwrap());
2833
2834                // Fill the input slices with 1's so we can catch situations where we don't
2835                // correctly handle odd remaining elements.
2836                x.as_mut_slice().fill(u8::MAX);
2837                y.as_mut_slice().fill(u8::MAX);
2838
2839                for i in 0..dim {
2840                    x.set(i, x_reference[i].into()).unwrap();
2841                    y.set(i, y_reference[i].into()).unwrap();
2842                }
2843
2844                // Check L2
2845                let expected: MV<f32> =
2846                    diskann_vector::distance::SquaredL2::evaluate(&*x_reference, &*y_reference);
2847
2848                let got = evaluate_l2(x.reborrow(), y.reborrow()).unwrap();
2849
2850                // Integer computations should be exact.
2851                assert_eq!(
2852                    expected.into_inner(),
2853                    got.into_inner() as f32,
2854                    "failed SquaredL2 for NBITS = {}, dim = {}, trial = {} -- context {}",
2855                    NBITS,
2856                    dim,
2857                    trial,
2858                    context,
2859                );
2860
2861                // Check IP
2862                let expected: MV<f32> =
2863                    diskann_vector::distance::InnerProduct::evaluate(&*x_reference, &*y_reference);
2864
2865                let got = evaluate_ip(x.reborrow(), y.reborrow()).unwrap();
2866
2867                // Integer computations should be exact.
2868                assert_eq!(
2869                    expected.into_inner(),
2870                    got.into_inner() as f32,
2871                    "faild InnerProduct for NBITS = {}, dim = {}, trial = {} -- context {}",
2872                    NBITS,
2873                    dim,
2874                    trial,
2875                    context,
2876                );
2877            }
2878        }
2879
2880        // Test that we correctly return error types for length mismatches.
2881        let x = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(10);
2882        let y = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(11);
2883
2884        assert!(
2885            evaluate_l2(x.reborrow(), y.reborrow()).is_err(),
2886            "context: {}",
2887            context
2888        );
2889        assert!(
2890            evaluate_l2(y.reborrow(), x.reborrow()).is_err(),
2891            "context: {}",
2892            context
2893        );
2894
2895        assert!(
2896            evaluate_ip(x.reborrow(), y.reborrow()).is_err(),
2897            "context: {}",
2898            context
2899        );
2900        assert!(
2901            evaluate_ip(y.reborrow(), x.reborrow()).is_err(),
2902            "context: {}",
2903            context
2904        );
2905    }
2906
2907    cfg_if::cfg_if! {
2908        if #[cfg(miri)] {
2909            const MAX_DIM: usize = 132;
2910            const TRIALS_PER_DIM: usize = 1;
2911        } else {
2912            const MAX_DIM: usize = 256;
2913            const TRIALS_PER_DIM: usize = 20;
2914        }
2915    }
2916
2917    // For the bit-slice kernels, we want to use different maximum dimensions for the distance
2918    // test depending on the implementation of the kernel, and whether or not we are running
2919    // under Miri.
2920    //
2921    // For implementations that use the scalar fallback, we need not set very high bounds
2922    // (particularly when running under miri) because the implementations are quite simple.
2923    //
2924    // However, some SIMD kernels (especially for the lower bit widths), require higher bounds
2925    // to trigger all possible corner cases.
2926    static BITSLICE_TEST_BOUNDS: LazyLock<HashMap<Key, Bounds>> = LazyLock::new(|| {
2927        use ArchKey::{Neon, Scalar, X86_64_V3, X86_64_V4};
2928        [
2929            (Key::new(1, Scalar), Bounds::new(64, 64)),
2930            (Key::new(1, X86_64_V3), Bounds::new(256, 256)),
2931            (Key::new(1, X86_64_V4), Bounds::new(256, 256)),
2932            (Key::new(1, Neon), Bounds::new(64, 64)),
2933            (Key::new(2, Scalar), Bounds::new(64, 64)),
2934            // Need a higher miri-amount due to the larget block size
2935            (Key::new(2, X86_64_V3), Bounds::new(512, 300)),
2936            (Key::new(2, X86_64_V4), Bounds::new(768, 600)), // main loop processes 256 items
2937            (Key::new(2, Neon), Bounds::new(64, 64)),
2938            (Key::new(3, Scalar), Bounds::new(64, 64)),
2939            (Key::new(3, X86_64_V3), Bounds::new(256, 96)),
2940            (Key::new(3, X86_64_V4), Bounds::new(256, 96)),
2941            (Key::new(3, Neon), Bounds::new(64, 64)),
2942            (Key::new(4, Scalar), Bounds::new(64, 64)),
2943            // Need a higher miri-amount due to the larget block size
2944            (Key::new(4, X86_64_V3), Bounds::new(256, 150)),
2945            (Key::new(4, X86_64_V4), Bounds::new(512, 300)),
2946            (Key::new(4, Neon), Bounds::new(64, 64)),
2947            (Key::new(5, Scalar), Bounds::new(64, 64)),
2948            (Key::new(5, X86_64_V3), Bounds::new(256, 96)),
2949            (Key::new(5, X86_64_V4), Bounds::new(256, 96)),
2950            (Key::new(5, Neon), Bounds::new(64, 64)),
2951            (Key::new(6, Scalar), Bounds::new(64, 64)),
2952            (Key::new(6, X86_64_V3), Bounds::new(256, 96)),
2953            (Key::new(6, X86_64_V4), Bounds::new(256, 96)),
2954            (Key::new(6, Neon), Bounds::new(64, 64)),
2955            (Key::new(7, Scalar), Bounds::new(64, 64)),
2956            (Key::new(7, X86_64_V3), Bounds::new(256, 96)),
2957            (Key::new(7, X86_64_V4), Bounds::new(256, 96)),
2958            (Key::new(7, Neon), Bounds::new(64, 64)),
2959            (Key::new(8, Scalar), Bounds::new(64, 64)),
2960            (Key::new(8, X86_64_V3), Bounds::new(256, 96)),
2961            (Key::new(8, X86_64_V4), Bounds::new(256, 96)),
2962            (Key::new(8, Neon), Bounds::new(64, 64)),
2963        ]
2964        .into_iter()
2965        .collect()
2966    });
2967
2968    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
2969    enum ArchKey {
2970        Scalar,
2971        #[expect(non_camel_case_types)]
2972        X86_64_V3,
2973        #[expect(non_camel_case_types)]
2974        X86_64_V4,
2975        Neon,
2976    }
2977
2978    #[derive(Debug, Clone, PartialEq, Eq, Hash)]
2979    struct Key {
2980        nbits: usize,
2981        arch: ArchKey,
2982    }
2983
2984    impl Key {
2985        fn new(nbits: usize, arch: ArchKey) -> Self {
2986            Self { nbits, arch }
2987        }
2988    }
2989
2990    #[derive(Debug, Clone, Copy)]
2991    struct Bounds {
2992        standard: usize,
2993        miri: usize,
2994    }
2995
2996    impl Bounds {
2997        fn new(standard: usize, miri: usize) -> Self {
2998            Self { standard, miri }
2999        }
3000
3001        fn get(&self) -> usize {
3002            if cfg!(miri) { self.miri } else { self.standard }
3003        }
3004    }
3005
3006    macro_rules! test_bitslice {
3007        ($name:ident, $nbits:literal, $seed:literal) => {
3008            #[test]
3009            fn $name() {
3010                let mut rng = StdRng::seed_from_u64($seed);
3011
3012                let max_dim = BITSLICE_TEST_BOUNDS[&Key::new($nbits, ArchKey::Scalar)].get();
3013
3014                test_bitslice_distances::<$nbits, _>(
3015                    max_dim,
3016                    TRIALS_PER_DIM,
3017                    &|x, y| SquaredL2::evaluate(x, y),
3018                    &|x, y| InnerProduct::evaluate(x, y),
3019                    "pure distance function",
3020                    &mut rng,
3021                );
3022
3023                test_bitslice_distances::<$nbits, _>(
3024                    max_dim,
3025                    TRIALS_PER_DIM,
3026                    &|x, y| diskann_wide::arch::Scalar::new().run2(SquaredL2, x, y),
3027                    &|x, y| diskann_wide::arch::Scalar::new().run2(InnerProduct, x, y),
3028                    "scalar arch",
3029                    &mut rng,
3030                );
3031
3032                // Architecture Specific.
3033                #[cfg(target_arch = "x86_64")]
3034                if let Some(arch) = diskann_wide::arch::x86_64::V3::new_checked() {
3035                    let max_dim = BITSLICE_TEST_BOUNDS[&Key::new($nbits, ArchKey::X86_64_V3)].get();
3036                    test_bitslice_distances::<$nbits, _>(
3037                        max_dim,
3038                        TRIALS_PER_DIM,
3039                        &|x, y| arch.run2(SquaredL2, x, y),
3040                        &|x, y| arch.run2(InnerProduct, x, y),
3041                        "x86-64-v3",
3042                        &mut rng,
3043                    );
3044                }
3045
3046                #[cfg(target_arch = "x86_64")]
3047                if let Some(arch) = diskann_wide::arch::x86_64::V4::new_checked_miri() {
3048                    let max_dim = BITSLICE_TEST_BOUNDS[&Key::new($nbits, ArchKey::X86_64_V4)].get();
3049                    test_bitslice_distances::<$nbits, _>(
3050                        max_dim,
3051                        TRIALS_PER_DIM,
3052                        &|x, y| arch.run2(SquaredL2, x, y),
3053                        &|x, y| arch.run2(InnerProduct, x, y),
3054                        "x86-64-v4",
3055                        &mut rng,
3056                    );
3057                }
3058
3059                #[cfg(target_arch = "aarch64")]
3060                if let Some(arch) = diskann_wide::arch::aarch64::Neon::new_checked() {
3061                    let max_dim = BITSLICE_TEST_BOUNDS[&Key::new($nbits, ArchKey::Neon)].get();
3062                    test_bitslice_distances::<$nbits, _>(
3063                        max_dim,
3064                        TRIALS_PER_DIM,
3065                        &|x, y| arch.run2(SquaredL2, x, y),
3066                        &|x, y| arch.run2(InnerProduct, x, y),
3067                        "neon",
3068                        &mut rng,
3069                    );
3070                }
3071            }
3072        };
3073    }
3074
3075    test_bitslice!(test_bitslice_distances_8bit, 8, 0xf0330c6d880e08ff);
3076    test_bitslice!(test_bitslice_distances_7bit, 7, 0x98aa7f2d4c83844f);
3077    test_bitslice!(test_bitslice_distances_6bit, 6, 0xf2f7ad7a37764b4c);
3078    test_bitslice!(test_bitslice_distances_5bit, 5, 0xae878d14973fb43f);
3079    test_bitslice!(test_bitslice_distances_4bit, 4, 0x8d6dbb8a6b19a4f8);
3080    test_bitslice!(test_bitslice_distances_3bit, 3, 0x8f56767236e58da2);
3081    test_bitslice!(test_bitslice_distances_2bit, 2, 0xb04f741a257b61af);
3082    test_bitslice!(test_bitslice_distances_1bit, 1, 0x820ea031c379eab5);
3083
3084    ///////////////////////////
3085    // Hamming Bit Distances //
3086    ///////////////////////////
3087
3088    fn test_hamming_distances<R>(dim_max: usize, trials_per_dim: usize, rng: &mut R)
3089    where
3090        R: Rng,
3091    {
3092        let dist: [i8; 2] = [-1, 1];
3093
3094        for dim in 0..dim_max {
3095            if !should_check_this_dimension(dim) {
3096                continue;
3097            }
3098
3099            let mut x_reference: Vec<i8> = vec![1; dim];
3100            let mut y_reference: Vec<i8> = vec![1; dim];
3101
3102            let mut x = BoxedBitSlice::<1, Binary>::new_boxed(dim);
3103            let mut y = BoxedBitSlice::<1, Binary>::new_boxed(dim);
3104
3105            for _ in 0..trials_per_dim {
3106                x_reference
3107                    .iter_mut()
3108                    .for_each(|i| *i = *dist.choose(rng).unwrap());
3109                y_reference
3110                    .iter_mut()
3111                    .for_each(|i| *i = *dist.choose(rng).unwrap());
3112
3113                // Fill the input slices with 1's so we can catch situations where we don't
3114                // correctly handle odd remaining elements.
3115                x.as_mut_slice().fill(u8::MAX);
3116                y.as_mut_slice().fill(u8::MAX);
3117
3118                for i in 0..dim {
3119                    x.set(i, x_reference[i].into()).unwrap();
3120                    y.set(i, y_reference[i].into()).unwrap();
3121                }
3122
3123                // We can check equality by evaluating the L2 distance between the reference
3124                // vectors.
3125                //
3126                // This is proportional to the Hamming distance by a factor of 4 (since the
3127                // distance betwwen +1 and -1 is 2 - and 2^2 = 4.
3128                let expected: MV<f32> =
3129                    diskann_vector::distance::SquaredL2::evaluate(&*x_reference, &*y_reference);
3130                let got: MV<u32> = Hamming::evaluate(x.reborrow(), y.reborrow()).unwrap();
3131                assert_eq!(4.0 * (got.into_inner() as f32), expected.into_inner());
3132            }
3133        }
3134
3135        let x = BoxedBitSlice::<1, Binary>::new_boxed(10);
3136        let y = BoxedBitSlice::<1, Binary>::new_boxed(11);
3137        assert!(Hamming::evaluate(x.reborrow(), y.reborrow()).is_err());
3138        assert!(Hamming::evaluate(y.reborrow(), x.reborrow()).is_err());
3139    }
3140
3141    #[test]
3142    fn test_hamming_distance() {
3143        let mut rng = StdRng::seed_from_u64(0x2160419161246d97);
3144        test_hamming_distances(MAX_DIM, TRIALS_PER_DIM, &mut rng);
3145    }
3146
3147    ///////////////////
3148    // Heterogeneous //
3149    ///////////////////
3150
3151    fn test_bit_transpose_distances<R>(
3152        dim_max: usize,
3153        trials_per_dim: usize,
3154        evaluate_ip: &dyn Fn(USlice<'_, 4, BitTranspose>, USlice<'_, 1>) -> MR,
3155        context: &str,
3156        rng: &mut R,
3157    ) where
3158        R: Rng,
3159    {
3160        let dist_4bit = {
3161            let domain = Unsigned::domain_const::<4>();
3162            Uniform::new_inclusive(*domain.start(), *domain.end()).unwrap()
3163        };
3164
3165        let dist_1bit = {
3166            let domain = Unsigned::domain_const::<1>();
3167            Uniform::new_inclusive(*domain.start(), *domain.end()).unwrap()
3168        };
3169
3170        for dim in 0..dim_max {
3171            if !should_check_this_dimension(dim) {
3172                continue;
3173            }
3174
3175            let mut x_reference: Vec<u8> = vec![0; dim];
3176            let mut y_reference: Vec<u8> = vec![0; dim];
3177
3178            let mut x = BoxedBitSlice::<4, Unsigned, BitTranspose>::new_boxed(dim);
3179            let mut y = BoxedBitSlice::<1, Unsigned, Dense>::new_boxed(dim);
3180
3181            for trial in 0..trials_per_dim {
3182                x_reference
3183                    .iter_mut()
3184                    .for_each(|i| *i = dist_4bit.sample(rng).try_into().unwrap());
3185                y_reference
3186                    .iter_mut()
3187                    .for_each(|i| *i = dist_1bit.sample(rng).try_into().unwrap());
3188
3189                // First - pre-set all the values in the bit-slices to 1.
3190                x.as_mut_slice().fill(u8::MAX);
3191                y.as_mut_slice().fill(u8::MAX);
3192
3193                for i in 0..dim {
3194                    x.set(i, x_reference[i].into()).unwrap();
3195                    y.set(i, y_reference[i].into()).unwrap();
3196                }
3197
3198                // Check IP
3199                let expected: MV<f32> =
3200                    diskann_vector::distance::InnerProduct::evaluate(&*x_reference, &*y_reference);
3201
3202                let got = evaluate_ip(x.reborrow(), y.reborrow());
3203
3204                // Integer computations should be exact.
3205                assert_eq!(
3206                    expected.into_inner(),
3207                    got.unwrap().into_inner() as f32,
3208                    "faild InnerProduct for dim = {}, trial = {} -- context {}",
3209                    dim,
3210                    trial,
3211                    context,
3212                );
3213            }
3214        }
3215
3216        let x = BoxedBitSlice::<4, Unsigned, BitTranspose>::new_boxed(10);
3217        let y = BoxedBitSlice::<1, Unsigned>::new_boxed(11);
3218        assert!(
3219            evaluate_ip(x.reborrow(), y.reborrow()).is_err(),
3220            "context: {}",
3221            context
3222        );
3223
3224        let y = BoxedBitSlice::<1, Unsigned>::new_boxed(9);
3225        assert!(
3226            evaluate_ip(x.reborrow(), y.reborrow()).is_err(),
3227            "context: {}",
3228            context
3229        );
3230    }
3231
3232    #[test]
3233    fn test_bit_transpose_distance() {
3234        let mut rng = StdRng::seed_from_u64(0xe20e26e926d4b853);
3235
3236        test_bit_transpose_distances(
3237            MAX_DIM,
3238            TRIALS_PER_DIM,
3239            &|x, y| InnerProduct::evaluate(x, y),
3240            "pure distance function",
3241            &mut rng,
3242        );
3243
3244        test_bit_transpose_distances(
3245            MAX_DIM,
3246            TRIALS_PER_DIM,
3247            &|x, y| diskann_wide::arch::Scalar::new().run2(InnerProduct, x, y),
3248            "scalar",
3249            &mut rng,
3250        );
3251
3252        // Architecture Specific.
3253        #[cfg(target_arch = "x86_64")]
3254        if let Some(arch) = diskann_wide::arch::x86_64::V3::new_checked() {
3255            test_bit_transpose_distances(
3256                MAX_DIM,
3257                TRIALS_PER_DIM,
3258                &|x, y| arch.run2(InnerProduct, x, y),
3259                "x86-64-v3",
3260                &mut rng,
3261            );
3262        }
3263
3264        // Architecture Specific.
3265        #[cfg(target_arch = "x86_64")]
3266        if let Some(arch) = diskann_wide::arch::x86_64::V4::new_checked_miri() {
3267            test_bit_transpose_distances(
3268                MAX_DIM,
3269                TRIALS_PER_DIM,
3270                &|x, y| arch.run2(InnerProduct, x, y),
3271                "x86-64-v4",
3272                &mut rng,
3273            );
3274        }
3275
3276        // Architecture Specific.
3277        #[cfg(target_arch = "aarch64")]
3278        if let Some(arch) = diskann_wide::arch::aarch64::Neon::new_checked() {
3279            test_bit_transpose_distances(
3280                MAX_DIM,
3281                TRIALS_PER_DIM,
3282                &|x, y| arch.run2(InnerProduct, x, y),
3283                "neon",
3284                &mut rng,
3285            );
3286        }
3287    }
3288
3289    //////////
3290    // Full //
3291    //////////
3292
3293    fn test_full_distances<const NBITS: usize>(
3294        dim_max: usize,
3295        trials_per_dim: usize,
3296        evaluate_ip: &dyn Fn(&[f32], USlice<'_, NBITS>) -> MathematicalResult<f32>,
3297        context: &str,
3298        rng: &mut impl Rng,
3299    ) where
3300        Unsigned: Representation<NBITS>,
3301    {
3302        // let dist_float = Uniform::new_inclusive(-2.0f32, 2.0f32).unwrap();
3303        let dist_float = [-2.0, -1.0, 0.0, 1.0, 2.0];
3304        let dist_bit = {
3305            let domain = Unsigned::domain_const::<NBITS>();
3306            Uniform::new_inclusive(*domain.start(), *domain.end()).unwrap()
3307        };
3308
3309        for dim in 0..dim_max {
3310            if !should_check_this_dimension(dim) {
3311                continue;
3312            }
3313
3314            let mut x: Vec<f32> = vec![0.0; dim];
3315
3316            let mut y_reference: Vec<u8> = vec![0; dim];
3317            let mut y = BoxedBitSlice::<NBITS, Unsigned, Dense>::new_boxed(dim);
3318
3319            for trial in 0..trials_per_dim {
3320                x.iter_mut()
3321                    .for_each(|i| *i = *dist_float.choose(rng).unwrap());
3322                y_reference
3323                    .iter_mut()
3324                    .for_each(|i| *i = dist_bit.sample(rng).try_into().unwrap());
3325
3326                // First - pre-set all the values in the bit-slices to 1.
3327                y.as_mut_slice().fill(u8::MAX);
3328
3329                let mut expected = 0.0;
3330                for i in 0..dim {
3331                    y.set(i, y_reference[i].into()).unwrap();
3332                    expected += y_reference[i] as f32 * x[i];
3333                }
3334
3335                // Check IP
3336                let got = evaluate_ip(&x, y.reborrow()).unwrap();
3337
3338                // Integer computations should be exact.
3339                assert_eq!(
3340                    expected,
3341                    got.into_inner(),
3342                    "faild InnerProduct for dim = {}, trial = {} -- context {}",
3343                    dim,
3344                    trial,
3345                    context,
3346                );
3347
3348                // Ensure that using the `Scalar` architecture providers the same
3349                // results.
3350                let scalar: MV<f32> = InnerProduct
3351                    .run(diskann_wide::arch::Scalar, x.as_slice(), y.reborrow())
3352                    .unwrap();
3353                assert_eq!(got.into_inner(), scalar.into_inner());
3354            }
3355        }
3356
3357        // Error Checking
3358        let x = vec![0.0; 10];
3359        let y = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(11);
3360        assert!(
3361            evaluate_ip(x.as_slice(), y.reborrow()).is_err(),
3362            "context: {}",
3363            context
3364        );
3365
3366        let y = BoxedBitSlice::<NBITS, Unsigned>::new_boxed(9);
3367        assert!(
3368            evaluate_ip(x.as_slice(), y.reborrow()).is_err(),
3369            "context: {}",
3370            context
3371        );
3372    }
3373
3374    macro_rules! test_full {
3375        ($name:ident, $nbits:literal, $seed:literal) => {
3376            #[test]
3377            fn $name() {
3378                let mut rng = StdRng::seed_from_u64($seed);
3379
3380                test_full_distances::<$nbits>(
3381                    MAX_DIM,
3382                    TRIALS_PER_DIM,
3383                    &|x, y| InnerProduct::evaluate(x, y),
3384                    "pure distance function",
3385                    &mut rng,
3386                );
3387
3388                test_full_distances::<$nbits>(
3389                    MAX_DIM,
3390                    TRIALS_PER_DIM,
3391                    &|x, y| diskann_wide::arch::Scalar::new().run2(InnerProduct, x, y),
3392                    "scalar",
3393                    &mut rng,
3394                );
3395
3396                // Architecture Specific.
3397                #[cfg(target_arch = "x86_64")]
3398                if let Some(arch) = diskann_wide::arch::x86_64::V3::new_checked() {
3399                    test_full_distances::<$nbits>(
3400                        MAX_DIM,
3401                        TRIALS_PER_DIM,
3402                        &|x, y| arch.run2(InnerProduct, x, y),
3403                        "x86-64-v3",
3404                        &mut rng,
3405                    );
3406                }
3407
3408                #[cfg(target_arch = "x86_64")]
3409                if let Some(arch) = diskann_wide::arch::x86_64::V4::new_checked_miri() {
3410                    test_full_distances::<$nbits>(
3411                        MAX_DIM,
3412                        TRIALS_PER_DIM,
3413                        &|x, y| arch.run2(InnerProduct, x, y),
3414                        "x86-64-v4",
3415                        &mut rng,
3416                    );
3417                }
3418
3419                #[cfg(target_arch = "aarch64")]
3420                if let Some(arch) = diskann_wide::arch::aarch64::Neon::new_checked() {
3421                    test_full_distances::<$nbits>(
3422                        MAX_DIM,
3423                        TRIALS_PER_DIM,
3424                        &|x, y| arch.run2(InnerProduct, x, y),
3425                        "neon",
3426                        &mut rng,
3427                    );
3428                }
3429            }
3430        };
3431    }
3432
3433    test_full!(test_full_distance_1bit, 1, 0xe20e26e926d4b853);
3434    test_full!(test_full_distance_2bit, 2, 0xae9542700aecbf68);
3435    test_full!(test_full_distance_3bit, 3, 0xfffd04b26bb6068c);
3436    test_full!(test_full_distance_4bit, 4, 0x86db49fd1a1704ba);
3437    test_full!(test_full_distance_5bit, 5, 0x3a35dc7fa7931c41);
3438    test_full!(test_full_distance_6bit, 6, 0x1f69de79e418d336);
3439    test_full!(test_full_distance_7bit, 7, 0x3fcf17b82dadc5ab);
3440    test_full!(test_full_distance_8bit, 8, 0x85dcaf48b1399db2);
3441
3442    ///////////////////////////////////////////
3443    // Heterogeneous: USlice<8> × USlice<M> //
3444    ///////////////////////////////////////////
3445
3446    /// Helper that builds vectors from a fill function and asserts the
3447    /// inner product matches.
3448    struct HetCase<const M: usize> {
3449        x_vals: Vec<i64>,
3450        y_vals: Vec<i64>,
3451    }
3452
3453    impl<const M: usize> HetCase<M>
3454    where
3455        Unsigned: Representation<M>,
3456    {
3457        fn new(dim: usize, fill: impl FnMut(usize) -> (i64, i64)) -> Self {
3458            let (x_vals, y_vals) = (0..dim).map(fill).unzip();
3459            Self { x_vals, y_vals }
3460        }
3461
3462        fn check_with(
3463            &self,
3464            label: impl Display,
3465            evaluate: &dyn Fn(USlice<'_, 8>, USlice<'_, M>) -> MR,
3466        ) {
3467            let dim = self.x_vals.len();
3468            let mut x = BoxedBitSlice::<8, Unsigned>::new_boxed(dim);
3469            let mut y = BoxedBitSlice::<M, Unsigned>::new_boxed(dim);
3470            // Pre-fill with 0xFF to catch trailing-element bugs.
3471            x.as_mut_slice().fill(u8::MAX);
3472            y.as_mut_slice().fill(u8::MAX);
3473            for (i, (&xv, &yv)) in self.x_vals.iter().zip(&self.y_vals).enumerate() {
3474                x.set(i, xv).unwrap();
3475                y.set(i, yv).unwrap();
3476            }
3477            let expected: u32 = self
3478                .x_vals
3479                .iter()
3480                .zip(&self.y_vals)
3481                .map(|(&a, &b)| a as u32 * b as u32)
3482                .sum();
3483            let got = evaluate(x.reborrow(), y.reborrow()).unwrap().into_inner();
3484            assert_eq!(expected, got, "{} failed for dim = {}", label, dim);
3485        }
3486    }
3487
3488    /// Fuzz test helper: random vectors across many dimensions.
3489    fn fuzz_heterogeneous_ip<const M: usize>(
3490        dim_max: usize,
3491        trials_per_dim: usize,
3492        max_val: i64,
3493        evaluate_ip: &dyn Fn(USlice<'_, 8>, USlice<'_, M>) -> MR,
3494        context: &str,
3495        rng: &mut impl Rng,
3496    ) where
3497        Unsigned: Representation<M>,
3498    {
3499        let dist_8bit = Uniform::new_inclusive(0i64, 255i64).unwrap();
3500        let dist_mbit = Uniform::new_inclusive(0i64, max_val).unwrap();
3501
3502        for dim in 0..dim_max {
3503            for trial in 0..trials_per_dim {
3504                HetCase::<M>::new(dim, |_| {
3505                    (dist_8bit.sample(&mut *rng), dist_mbit.sample(&mut *rng))
3506                })
3507                .check_with(
3508                    lazy_format!("IP(8,{}) dim={dim}, trial={trial} -- {context}", M),
3509                    evaluate_ip,
3510                );
3511            }
3512
3513            // Length mismatch → error.
3514            let x = BoxedBitSlice::<8, Unsigned>::new_boxed(dim);
3515            let y = BoxedBitSlice::<M, Unsigned>::new_boxed(dim + 1);
3516            assert!(
3517                evaluate_ip(x.reborrow(), y.reborrow()).is_err(),
3518                "context: {}",
3519                context,
3520            );
3521        }
3522    }
3523
3524    /// All values at maximum: x[i] = 255, y[i] = max_val.
3525    /// Confirms no i16 saturation in vpmaddubsw.
3526    fn het_test_max_values<const M: usize>(
3527        max_val: i64,
3528        context: &str,
3529        evaluate: &dyn Fn(USlice<'_, 8>, USlice<'_, M>) -> MR,
3530    ) where
3531        Unsigned: Representation<M>,
3532    {
3533        let dims = [127, 128, 129, 255, 256, 512, 768, 896, 3072];
3534        for &dim in &dims {
3535            let case = HetCase::<M>::new(dim, |_| (255, max_val));
3536            case.check_with(lazy_format!("max-value {context} dim={dim}"), evaluate);
3537        }
3538    }
3539
3540    /// Known-answer tests to catch bit-ordering bugs.
3541    fn het_test_known_answers<const M: usize>(
3542        max_val: i64,
3543        evaluate: &dyn Fn(USlice<'_, 8>, USlice<'_, M>) -> MR,
3544    ) where
3545        Unsigned: Representation<M>,
3546    {
3547        // _mm256_addubs_epi8 unsigned treatment: x[i] = 200 (> 127), y[i] = max_val.
3548        // Correct: 200 × max_val per element.
3549        HetCase::<M>::new(64, |_| (200, max_val)).check_with("vpmaddubsw operand-order", evaluate);
3550
3551        // Ascending x, constant y.
3552        let y_val = (max_val / 2).max(1);
3553        HetCase::<M>::new(128, |i| ((i % 256) as i64, y_val))
3554            .check_with("ascending-x constant-y", evaluate);
3555
3556        // Single element (pure scalar fallback).
3557        HetCase::<M>::new(1, |_| (200, max_val)).check_with("single element", evaluate);
3558    }
3559
3560    /// Exhaustive edge-case coverage.
3561    fn het_test_edge_cases<const M: usize>(
3562        max_val: i64,
3563        block_size: usize,
3564        evaluate: &dyn Fn(USlice<'_, 8>, USlice<'_, M>) -> MR,
3565    ) where
3566        Unsigned: Representation<M>,
3567    {
3568        let y_half = (max_val / 2).max(1);
3569
3570        // One side zero.
3571        HetCase::<M>::new(64, |_| (0, max_val)).check_with("x-zero y-nonzero", evaluate);
3572        HetCase::<M>::new(64, |_| (255, 0)).check_with("y-zero x-nonzero", evaluate);
3573
3574        // Every dimension from 0..block_size+1 (scalar fallback boundary).
3575        for dim in 0..=(block_size + 1) {
3576            HetCase::<M>::new(dim, |_| (3, y_half)).check_with("uniform fill", evaluate);
3577        }
3578
3579        // Exact block boundaries.
3580        for &dim in &[block_size, 2 * block_size, 4 * block_size, 8 * block_size] {
3581            HetCase::<M>::new(dim, |_| (100, max_val)).check_with("exact block boundary", evaluate);
3582        }
3583
3584        // x varies, y constant.
3585        HetCase::<M>::new(300, |i| ((i % 256) as i64, 1))
3586            .check_with("x-varies y-constant", evaluate);
3587
3588        // x constant, y varies.
3589        HetCase::<M>::new(300, |i| (1, (i as i64) % (max_val + 1)))
3590            .check_with("x-constant y-varies", evaluate);
3591
3592        // Alternating pattern.
3593        HetCase::<M>::new(128, |i| if i % 2 == 0 { (255, max_val) } else { (0, 0) })
3594            .check_with("alternating pattern", evaluate);
3595
3596        // Opposite alternating pattern.
3597        HetCase::<M>::new(128, |i| if i % 2 == 0 { (0, 0) } else { (255, max_val) })
3598            .check_with("opposite alternating", evaluate);
3599
3600        // Large accumulation check for overflow.
3601        HetCase::<M>::new(1024, |_| (255, max_val)).check_with("large accumulation", evaluate);
3602
3603        // x > 127 sweep (vpmaddubsw unsigned treatment).
3604        for x_val in [128i64, 170, 200, 240, 255] {
3605            HetCase::<M>::new(block_size, move |_| (x_val, y_half))
3606                .check_with(lazy_format!("x > 127 (x_val={x_val})"), evaluate);
3607        }
3608
3609        // Dim = block_size - 1 (no full block, all scalar).
3610        HetCase::<M>::new(block_size - 1, |i| {
3611            (
3612                ((i * 7 + 3) % 256) as i64,
3613                ((i * 11 + 5) as i64) % (max_val + 1),
3614            )
3615        })
3616        .check_with("dim=block_size-1 (all scalar)", evaluate);
3617
3618        // 4× unroll boundary exercises.
3619        let unroll4 = 4 * block_size;
3620        for &dim in &[
3621            unroll4,
3622            unroll4 + 1,
3623            unroll4 + block_size,
3624            unroll4 + block_size + 1,
3625        ] {
3626            HetCase::<M>::new(dim, |i| {
3627                (((i + 1) % 256) as i64, ((i + 1) as i64) % (max_val + 1))
3628            })
3629            .check_with("unroll boundary", evaluate);
3630        }
3631    }
3632
3633    macro_rules! heterogeneous_ip_tests_8xM {
3634        (
3635            mod_name: $mod:ident,
3636            M: $M:literal,
3637            max_val: $max_val:literal,
3638            block_size: $block_size:literal,
3639            seed_fuzz: $seed_fuzz:literal,
3640        ) => {
3641            mod $mod {
3642                use super::*;
3643
3644                #[test]
3645                fn all_ip_dispatches() {
3646                    let mut rng = StdRng::seed_from_u64($seed_fuzz);
3647
3648                    fuzz_heterogeneous_ip::<$M>(
3649                        MAX_DIM,
3650                        TRIALS_PER_DIM,
3651                        $max_val,
3652                        &|x, y| InnerProduct::evaluate(x, y),
3653                        "pure distance function",
3654                        &mut rng,
3655                    );
3656                    fuzz_heterogeneous_ip::<$M>(
3657                        MAX_DIM,
3658                        TRIALS_PER_DIM,
3659                        $max_val,
3660                        &|x, y| diskann_wide::arch::Scalar::new().run2(InnerProduct, x, y),
3661                        "scalar arch",
3662                        &mut rng,
3663                    );
3664                    #[cfg(target_arch = "x86_64")]
3665                    if let Some(arch) = diskann_wide::arch::x86_64::V3::new_checked() {
3666                        fuzz_heterogeneous_ip::<$M>(
3667                            MAX_DIM,
3668                            TRIALS_PER_DIM,
3669                            $max_val,
3670                            &|x, y| arch.run2(InnerProduct, x, y),
3671                            "x86-64-v3",
3672                            &mut rng,
3673                        );
3674                    }
3675                    #[cfg(target_arch = "x86_64")]
3676                    if let Some(arch) = diskann_wide::arch::x86_64::V4::new_checked_miri() {
3677                        fuzz_heterogeneous_ip::<$M>(
3678                            MAX_DIM,
3679                            TRIALS_PER_DIM,
3680                            $max_val,
3681                            &|x, y| arch.run2(InnerProduct, x, y),
3682                            "x86-64-v4",
3683                            &mut rng,
3684                        );
3685                    }
3686                }
3687
3688                #[test]
3689                fn max_values() {
3690                    het_test_max_values::<$M>($max_val, "dispatch", &|x, y| {
3691                        InnerProduct::evaluate(x, y)
3692                    });
3693                    #[cfg(target_arch = "x86_64")]
3694                    if let Some(arch) = diskann_wide::arch::x86_64::V3::new_checked() {
3695                        het_test_max_values::<$M>($max_val, "V3", &|x, y| {
3696                            arch.run2(InnerProduct, x, y)
3697                        });
3698                    }
3699                }
3700
3701                #[test]
3702                fn known_answers() {
3703                    het_test_known_answers::<$M>($max_val, &|x, y| InnerProduct::evaluate(x, y));
3704                }
3705
3706                #[test]
3707                fn edge_cases() {
3708                    het_test_edge_cases::<$M>($max_val, $block_size, &|x, y| {
3709                        InnerProduct::evaluate(x, y)
3710                    });
3711                }
3712            }
3713        };
3714    }
3715
3716    heterogeneous_ip_tests_8xM! {
3717        mod_name: heterogeneous_ip_8x4,
3718        M: 4,
3719        max_val: 15,
3720        block_size: 32,
3721        seed_fuzz: 0xd3a7f1c09b2e4856,
3722    }
3723
3724    heterogeneous_ip_tests_8xM! {
3725        mod_name: heterogeneous_ip_8x2,
3726        M: 2,
3727        max_val: 3,
3728        block_size: 64,
3729        seed_fuzz: 0x82c4a6e809f1d3b5,
3730    }
3731
3732    heterogeneous_ip_tests_8xM! {
3733        mod_name: heterogeneous_ip_8x1,
3734        M: 1,
3735        max_val: 1,
3736        block_size: 32,
3737        seed_fuzz: 0x1b17_a5e7c2d0f839,
3738    }
3739}