Skip to content

Commit a5aec74

Browse files
committed
fix(sqlite-vec.c): square NEON int8 L2 differences in 32-bit lanes
l2_sqr_int8_neon() squared int8 differences in 16-bit lanes, which overflow once |a - b| >= 182, so NEON builds (including the published darwin-arm64 binary) returned too-small or NULL L2 distances between int8 vectors. Also make the NEON code compile with GCC, which rejects the implicit signed/unsigned vector conversions in l1_int8_neon(), and include arm64_neon.h under MSVC on ARM64.
1 parent 94eee2b commit a5aec74

3 files changed

Lines changed: 43 additions & 21 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Fixed
6+
7+
- Fixed L2 distance between `int8` vectors in NEON builds, which include the published darwin-arm64 binary. The NEON kernel squared each element difference in a 16-bit lane, and any difference of 182 or more (for example 127 against −128) overflows it, so `vec_distance_l2()` and `int8` KNN queries using L2 distance returned distances that were too small, or NULL when the wrapped sum went negative. Squares are now widened to 32 bits.
8+
- The NEON code now compiles with GCC, which rejects implicit conversions between signed and unsigned vector types, and includes `arm64_neon.h` under MSVC on ARM64. Release builds still enable NEON only on darwin-arm64.
9+
310
## [2.0.1] - 2026-08-27
411

512
No functional changes. The extension source, bindings, and packaged files are identical to 2.0.0 — this release only changes how the package is built and published.

‎sqlite-vec.c‎

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,11 @@ static f32 l2_sqr_float_avx(const void *pVect1v, const void *pVect2v,
239239
#endif
240240

241241
#ifdef SQLITE_VEC_ENABLE_NEON
242+
#if defined(_MSC_VER) && !defined(__clang__) && defined(_M_ARM64)
243+
#include <arm64_neon.h>
244+
#else
242245
#include <arm_neon.h>
246+
#endif
243247

244248
#define PORTABLE_ALIGN32 __attribute__((aligned(32)))
245249

@@ -309,7 +313,8 @@ static f32 l2_sqr_int8_neon(const void *pVect1v, const void *pVect2v,
309313
size_t qty = *((size_t *)qty_ptr);
310314

311315
const i8 *pEnd1 = pVect1 + qty;
312-
i32 sum_scalar = 0;
316+
int32x4_t acc_low = vdupq_n_s32(0);
317+
int32x4_t acc_high = vdupq_n_s32(0);
313318

314319
while (pVect1 < pEnd1 - 7) {
315320
// loading 8 at a time
@@ -318,18 +323,15 @@ static f32 l2_sqr_int8_neon(const void *pVect1v, const void *pVect2v,
318323
pVect1 += 8;
319324
pVect2 += 8;
320325

321-
// widen to protect against overflow
322-
int16x8_t v1_wide = vmovl_s8(v1);
323-
int16x8_t v2_wide = vmovl_s8(v2);
324-
325-
int16x8_t diff = vsubq_s16(v1_wide, v2_wide);
326-
int16x8_t squared_diff = vmulq_s16(diff, diff);
327-
int32x4_t sum = vpaddlq_s16(squared_diff);
328-
329-
sum_scalar += vgetq_lane_s32(sum, 0) + vgetq_lane_s32(sum, 1) +
330-
vgetq_lane_s32(sum, 2) + vgetq_lane_s32(sum, 3);
326+
// the difference (up to ±255) fits in 16 bits, but its square (up to
327+
// 65025) does not, so square with a widening multiply into 32 bits
328+
int16x8_t diff = vsubq_s16(vmovl_s8(v1), vmovl_s8(v2));
329+
acc_low = vmlal_s16(acc_low, vget_low_s16(diff), vget_low_s16(diff));
330+
acc_high = vmlal_high_s16(acc_high, diff, diff);
331331
}
332332

333+
i32 sum_scalar = vaddvq_s32(vaddq_s32(acc_low, acc_high));
334+
333335
// handle leftovers
334336
while (pVect1 < pEnd1) {
335337
i16 diff = (i16)*pVect1 - (i16)*pVect2;
@@ -354,26 +356,32 @@ static i32 l1_int8_neon(const void *pVect1v, const void *pVect2v,
354356
int32x4_t acc3 = vdupq_n_s32(0);
355357
int32x4_t acc4 = vdupq_n_s32(0);
356358

359+
// vabdq_s8 returns |a - b| (up to 255) in signed lanes, so each difference
360+
// is reinterpreted as unsigned before the widening pairwise adds
357361
while (pVect1 < pEnd1 - 63) {
358362
int8x16_t v1 = vld1q_s8(pVect1);
359363
int8x16_t v2 = vld1q_s8(pVect2);
360-
int8x16_t diff1 = vabdq_s8(v1, v2);
361-
acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff1)));
364+
uint8x16_t diff1 = vreinterpretq_u8_s8(vabdq_s8(v1, v2));
365+
acc1 =
366+
vaddq_s32(acc1, vreinterpretq_s32_u32(vpaddlq_u16(vpaddlq_u8(diff1))));
362367

363368
v1 = vld1q_s8(pVect1 + 16);
364369
v2 = vld1q_s8(pVect2 + 16);
365-
int8x16_t diff2 = vabdq_s8(v1, v2);
366-
acc2 = vaddq_s32(acc2, vpaddlq_u16(vpaddlq_u8(diff2)));
370+
uint8x16_t diff2 = vreinterpretq_u8_s8(vabdq_s8(v1, v2));
371+
acc2 =
372+
vaddq_s32(acc2, vreinterpretq_s32_u32(vpaddlq_u16(vpaddlq_u8(diff2))));
367373

368374
v1 = vld1q_s8(pVect1 + 32);
369375
v2 = vld1q_s8(pVect2 + 32);
370-
int8x16_t diff3 = vabdq_s8(v1, v2);
371-
acc3 = vaddq_s32(acc3, vpaddlq_u16(vpaddlq_u8(diff3)));
376+
uint8x16_t diff3 = vreinterpretq_u8_s8(vabdq_s8(v1, v2));
377+
acc3 =
378+
vaddq_s32(acc3, vreinterpretq_s32_u32(vpaddlq_u16(vpaddlq_u8(diff3))));
372379

373380
v1 = vld1q_s8(pVect1 + 48);
374381
v2 = vld1q_s8(pVect2 + 48);
375-
int8x16_t diff4 = vabdq_s8(v1, v2);
376-
acc4 = vaddq_s32(acc4, vpaddlq_u16(vpaddlq_u8(diff4)));
382+
uint8x16_t diff4 = vreinterpretq_u8_s8(vabdq_s8(v1, v2));
383+
acc4 =
384+
vaddq_s32(acc4, vreinterpretq_s32_u32(vpaddlq_u16(vpaddlq_u8(diff4))));
377385

378386
pVect1 += 64;
379387
pVect2 += 64;
@@ -382,8 +390,9 @@ static i32 l1_int8_neon(const void *pVect1v, const void *pVect2v,
382390
while (pVect1 < pEnd1 - 15) {
383391
int8x16_t v1 = vld1q_s8(pVect1);
384392
int8x16_t v2 = vld1q_s8(pVect2);
385-
int8x16_t diff = vabdq_s8(v1, v2);
386-
acc1 = vaddq_s32(acc1, vpaddlq_u16(vpaddlq_u8(diff)));
393+
uint8x16_t diff = vreinterpretq_u8_s8(vabdq_s8(v1, v2));
394+
acc1 =
395+
vaddq_s32(acc1, vreinterpretq_s32_u32(vpaddlq_u16(vpaddlq_u8(diff))));
387396
pVect1 += 16;
388397
pVect2 += 16;
389398
}

‎tests/test-loadable.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,12 @@ def check(a, b, dtype=np.float32):
617617
check([1.2, 0.1], [0.4, -0.4])
618618
check([-1.2, -0.1], [-0.4, 0.4])
619619
check([1, 2, 3], [-9, -8, -7], dtype=np.int8)
620+
# check overflow in neon elements: |a - b| >= 182 squares past INT16_MAX
621+
check([127] * 16, [-128] * 16, dtype=np.int8)
622+
check([-128, 127] * 8, [127, -128] * 8, dtype=np.int8)
623+
check([91] * 16, [-91] * 16, dtype=np.int8)
624+
# overflow in neon elements, plus a leftover element
625+
check([127] * 25, [-128] * 25, dtype=np.int8)
620626

621627

622628
def test_vec_length():

0 commit comments

Comments
 (0)