Skip to content

Commit fabcb9b

Browse files
Add Neon Inner product u4*u4 kernel (#1353)
Exclusive aarch64 USlice4 * USlice 4 Inner Product kernel using Neon and dotprod. We also add quantization instantiation for spherical quantization. We use dot_simd() heavily in this kernel, for performance we rely on dotprod feature. Co-authored-by: Mustafa Idris <mustafa.idris@arm.com>
1 parent e480e02 commit fabcb9b

4 files changed

Lines changed: 159 additions & 6 deletions

File tree

diskann-quantization/src/bits/distances.rs

Lines changed: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
//! | `USlice<1>` | `USlice<1>` | `MV<u32>` | Optimized | Optimized | Uses V3 | Optimized |
6868
//! | `USlice<2>` | `USlice<2>` | `MV<u32>` | Fallback | Yes | Yes | Fallback |
6969
//! | `USlice<3>` | `USlice<3>` | `MV<u32>` | Fallback | No | Uses V3 | Fallback |
70-
//! | `USlice<4>` | `USlice<4>` | `MV<u32>` | Fallback | Yes | Uses V3 | Fallback |
70+
//! | `USlice<4>` | `USlice<4>` | `MV<u32>` | Fallback | Yes | Uses V3 | Optimized |
7171
//! | `USlice<5>` | `USlice<5>` | `MV<u32>` | Fallback | No | Uses V3 | Fallback |
7272
//! | `USlice<6>` | `USlice<6>` | `MV<u32>` | Fallback | No | Uses V3 | Fallback |
7373
//! | `USlice<7>` | `USlice<7>` | `MV<u32>` | Fallback | No | Uses V3 | Fallback |
@@ -114,6 +114,9 @@ use diskann_wide::{
114114
SIMDCast, SIMDDotProduct, SIMDMulAdd, SIMDReinterpret, SIMDSumTree, SIMDVector,
115115
};
116116

117+
#[cfg(target_arch = "aarch64")]
118+
use diskann_wide::{SIMDDotProduct, SIMDSumTree, SIMDVector};
119+
117120
use super::{Binary, BitSlice, BitTranspose, Dense, Representation, Unsigned};
118121
use crate::distances::{Hamming, InnerProduct, MV, MathematicalResult, SquaredL2, check_lengths};
119122

@@ -1565,6 +1568,103 @@ impl Target2<diskann_wide::arch::x86_64::V3, MathematicalResult<u32>, USlice<'_,
15651568
}
15661569
}
15671570

1571+
#[cfg(target_arch = "aarch64")]
1572+
impl
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+
15681668
/// Compute the inner product between bitvectors `x` and `y`.
15691669
///
15701670
/// Returns an error if the arguments have different lengths.
@@ -2108,7 +2208,6 @@ retarget!(
21082208
7,
21092209
6,
21102210
5,
2111-
4,
21122211
3,
21132212
2,
21142213
(8, 4),
@@ -2943,7 +3042,7 @@ mod tests {
29433042
// Need a higher miri-amount due to the larget block size
29443043
(Key::new(4, X86_64_V3), Bounds::new(256, 150)),
29453044
(Key::new(4, X86_64_V4), Bounds::new(512, 300)),
2946-
(Key::new(4, Neon), Bounds::new(64, 64)),
3045+
(Key::new(4, Neon), Bounds::new(128, 128)),
29473046
(Key::new(5, Scalar), Bounds::new(64, 64)),
29483047
(Key::new(5, X86_64_V3), Bounds::new(256, 96)),
29493048
(Key::new(5, X86_64_V4), Bounds::new(256, 96)),
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright (c) Microsoft Corporation.
3+
* Licensed under the MIT license.
4+
*/
5+
6+
//! Explicitly instantiate the AArch64 Neon spherical inner-product paths.
7+
use diskann_wide::arch::aarch64::Neon;
8+
9+
use crate::{
10+
alloc::{AllocatorError, GlobalAllocator},
11+
spherical::{
12+
iface::{AsData, AsQuery, DistanceComputer, Reify},
13+
vectors,
14+
},
15+
};
16+
17+
/// Instantiate the Neon inner-product implementation for
18+
/// `USlice<'_, 4> × USlice<'_, 4>` in the data-to-data path.
19+
#[inline(never)]
20+
pub fn fourbit_neon_ip_data_data(
21+
arch: Neon,
22+
shift: &[f32],
23+
dim: usize,
24+
) -> Result<DistanceComputer, AllocatorError> {
25+
let reify = Reify::<_, _, AsData<4>, AsData<4>>::new(
26+
vectors::CompensatedIP::new(shift, dim),
27+
dim,
28+
arch,
29+
);
30+
31+
DistanceComputer::new(reify, GlobalAllocator)
32+
}
33+
34+
/// Instantiate the Neon inner-product implementation for the four-bit
35+
/// query-to-data path.
36+
///
37+
/// `dispatch_map!(4, AsQuery<4>, Neon);`
38+
#[inline(never)]
39+
pub fn fourbit_neon_ip_query_data(
40+
arch: Neon,
41+
shift: &[f32],
42+
dim: usize,
43+
) -> Result<DistanceComputer, AllocatorError> {
44+
let reify = Reify::<_, _, AsQuery<4>, AsData<4>>::new(
45+
vectors::CompensatedIP::new(shift, dim),
46+
dim,
47+
arch,
48+
);
49+
50+
DistanceComputer::new(reify, GlobalAllocator)
51+
}

diskann-quantization/src/spherical/__codegen/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
* Licensed under the MIT license.
44
*/
55

6-
//! Intantiations for codegen inspection.
6+
//! Instantiations for codegen inspection.
77
//!
88
//! These methods are **not** part of the public API.
99
1010
#[cfg(target_arch = "x86_64")]
1111
pub mod x86_64;
12+
13+
#[cfg(target_arch = "aarch64")]
14+
pub mod aarch64;

diskann-quantization/src/spherical/iface.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1435,12 +1435,12 @@ cfg_if::cfg_if! {
14351435

14361436
dispatch_map!(1, AsData<1>, Neon, downcast);
14371437
dispatch_map!(2, AsData<2>, Neon, downcast);
1438-
dispatch_map!(4, AsData<4>, Neon, downcast);
1438+
dispatch_map!(4, AsData<4>, Neon);
14391439
dispatch_map!(8, AsData<8>, Neon, downcast);
14401440

14411441
dispatch_map!(1, AsQuery<4, bits::BitTranspose>, Neon, downcast);
14421442
dispatch_map!(2, AsQuery<2>, Neon, downcast);
1443-
dispatch_map!(4, AsQuery<4>, Neon, downcast);
1443+
dispatch_map!(4, AsQuery<4>, Neon);
14441444
dispatch_map!(8, AsQuery<8>, Neon, downcast);
14451445
}
14461446
}

0 commit comments

Comments
 (0)