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