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