Skip to content

Commit aef66ea

Browse files
committed
Use NEON for cosine distance
distance_cosine_float() and distance_cosine_int8() had no NEON kernel, so cosine KNN queries in NEON builds, which include the macOS arm64 build, ran the scalar loop. asg017/sqlite-vec 0de765f added cosine_float_neon() and cosine_int8_neon(), dispatched above 16 and 15 dimensions. This ports them with two changes. asg017's float32 kernel sums in float32, the loss d75c245 fixed in the scalar loop for asg017#324: vectors with elements below about 3e-23 or above about 2e19 get wrong distances. Here each float32x4 load is widened to two float64x2 halves before it is multiplied and added, in asg017's 16-element blocks with four accumulators per sum. And the int8 kernel finishes in double, as distance_cosine_int8() does, where asg017's finishes in float; both sum the products exactly in 32-bit lanes. Neither kernel checks for a zero vector: vec_distance_cosine() returns NULL for one, and vec0 refuses them in cosine columns, before any distance is computed. Built with -DSQLITE_VEC_ENABLE_NEON on Linux arm64 (Clang 19, -O3), a KNN query on a 100,000-row float[768] table takes 39.2 ms instead of 102.8 ms for k = 10 and 46.2 ms instead of 126.2 ms for k = 100 (medians of 20 queries), returning the same rows and distances. The float32 distances match the scalar loop's on 12,000 random pairs of 16 to 1,536 dimensions, except that a vector's distance to itself, 0 or about 2e-16 in either, can change between the two. The int8 distances match up to 1,024 dimensions, where every sum is exact in float, and past that the kernel's are closer to the exact value for vectors whose squares sum past 2^24 (a mean error of 2.3e-8 instead of 2.5e-7 at 2,048 dimensions). The cosine tests used vectors of 2 and 3 elements, below both kernels' thresholds, and pass against asg017's float32 kernel as it is. test_vec_distance_cosine now checks lengths of 17 to 768, with and without a remainder past the 16-element blocks, against a reference computed in float64, since numpy sums int8 products in int8, and the extreme-magnitude test repeats at 40 dimensions, which asg017's float32 kernel fails. test-loadable.py passes on the NEON build and on the scalar one, and make test-unit, which builds without SIMD, passes too.
1 parent e67755f commit aef66ea

2 files changed

Lines changed: 255 additions & 13 deletions

File tree

‎sqlite-vec.c‎

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,226 @@ static double l1_f32_neon(const void *pVect1v, const void *pVect2v,
400400

401401
return vaddvq_f64(acc) + sum;
402402
}
403+
404+
// asg017/sqlite-vec's cosine_float_neon, with its sums kept in double as
405+
// distance_cosine_float keeps them: each float32x4 load is widened to two
406+
// float64x2 halves before it is multiplied and added.
407+
static f32 cosine_float_neon(const void *pVect1v, const void *pVect2v,
408+
const void *qty_ptr) {
409+
f32 *pVect1 = (f32 *)pVect1v;
410+
f32 *pVect2 = (f32 *)pVect2v;
411+
size_t qty = *((size_t *)qty_ptr);
412+
size_t qty16 = qty >> 4;
413+
const f32 *pEnd1 = pVect1 + (qty16 << 4);
414+
415+
float64x2_t dot0 = vdupq_n_f64(0), dot1 = vdupq_n_f64(0);
416+
float64x2_t dot2 = vdupq_n_f64(0), dot3 = vdupq_n_f64(0);
417+
float64x2_t amag0 = vdupq_n_f64(0), amag1 = vdupq_n_f64(0);
418+
float64x2_t amag2 = vdupq_n_f64(0), amag3 = vdupq_n_f64(0);
419+
float64x2_t bmag0 = vdupq_n_f64(0), bmag1 = vdupq_n_f64(0);
420+
float64x2_t bmag2 = vdupq_n_f64(0), bmag3 = vdupq_n_f64(0);
421+
422+
while (pVect1 < pEnd1) {
423+
float32x4_t v1, v2;
424+
float64x2_t a, b;
425+
426+
v1 = vld1q_f32(pVect1);
427+
pVect1 += 4;
428+
v2 = vld1q_f32(pVect2);
429+
pVect2 += 4;
430+
a = vcvt_f64_f32(vget_low_f32(v1));
431+
b = vcvt_f64_f32(vget_low_f32(v2));
432+
dot0 = vfmaq_f64(dot0, a, b);
433+
amag0 = vfmaq_f64(amag0, a, a);
434+
bmag0 = vfmaq_f64(bmag0, b, b);
435+
a = vcvt_high_f64_f32(v1);
436+
b = vcvt_high_f64_f32(v2);
437+
dot0 = vfmaq_f64(dot0, a, b);
438+
amag0 = vfmaq_f64(amag0, a, a);
439+
bmag0 = vfmaq_f64(bmag0, b, b);
440+
441+
v1 = vld1q_f32(pVect1);
442+
pVect1 += 4;
443+
v2 = vld1q_f32(pVect2);
444+
pVect2 += 4;
445+
a = vcvt_f64_f32(vget_low_f32(v1));
446+
b = vcvt_f64_f32(vget_low_f32(v2));
447+
dot1 = vfmaq_f64(dot1, a, b);
448+
amag1 = vfmaq_f64(amag1, a, a);
449+
bmag1 = vfmaq_f64(bmag1, b, b);
450+
a = vcvt_high_f64_f32(v1);
451+
b = vcvt_high_f64_f32(v2);
452+
dot1 = vfmaq_f64(dot1, a, b);
453+
amag1 = vfmaq_f64(amag1, a, a);
454+
bmag1 = vfmaq_f64(bmag1, b, b);
455+
456+
v1 = vld1q_f32(pVect1);
457+
pVect1 += 4;
458+
v2 = vld1q_f32(pVect2);
459+
pVect2 += 4;
460+
a = vcvt_f64_f32(vget_low_f32(v1));
461+
b = vcvt_f64_f32(vget_low_f32(v2));
462+
dot2 = vfmaq_f64(dot2, a, b);
463+
amag2 = vfmaq_f64(amag2, a, a);
464+
bmag2 = vfmaq_f64(bmag2, b, b);
465+
a = vcvt_high_f64_f32(v1);
466+
b = vcvt_high_f64_f32(v2);
467+
dot2 = vfmaq_f64(dot2, a, b);
468+
amag2 = vfmaq_f64(amag2, a, a);
469+
bmag2 = vfmaq_f64(bmag2, b, b);
470+
471+
v1 = vld1q_f32(pVect1);
472+
pVect1 += 4;
473+
v2 = vld1q_f32(pVect2);
474+
pVect2 += 4;
475+
a = vcvt_f64_f32(vget_low_f32(v1));
476+
b = vcvt_f64_f32(vget_low_f32(v2));
477+
dot3 = vfmaq_f64(dot3, a, b);
478+
amag3 = vfmaq_f64(amag3, a, a);
479+
bmag3 = vfmaq_f64(bmag3, b, b);
480+
a = vcvt_high_f64_f32(v1);
481+
b = vcvt_high_f64_f32(v2);
482+
dot3 = vfmaq_f64(dot3, a, b);
483+
amag3 = vfmaq_f64(amag3, a, a);
484+
bmag3 = vfmaq_f64(bmag3, b, b);
485+
}
486+
487+
double dot =
488+
vaddvq_f64(vaddq_f64(vaddq_f64(dot0, dot1), vaddq_f64(dot2, dot3)));
489+
double aMag =
490+
vaddvq_f64(vaddq_f64(vaddq_f64(amag0, amag1), vaddq_f64(amag2, amag3)));
491+
double bMag =
492+
vaddvq_f64(vaddq_f64(vaddq_f64(bmag0, bmag1), vaddq_f64(bmag2, bmag3)));
493+
494+
const f32 *pEnd2 = pVect1 + (qty - (qty16 << 4));
495+
while (pVect1 < pEnd2) {
496+
dot += (double)*pVect1 * (double)*pVect2;
497+
aMag += (double)*pVect1 * (double)*pVect1;
498+
bMag += (double)*pVect2 * (double)*pVect2;
499+
pVect1++;
500+
pVect2++;
501+
}
502+
503+
return 1 - (dot / (sqrt(aMag) * sqrt(bMag)));
504+
}
505+
506+
// asg017/sqlite-vec's cosine_int8_neon, finishing as distance_cosine_int8
507+
// does. Products of int8 elements are summed exactly in 32-bit lanes.
508+
static f32 cosine_int8_neon(const void *pA, const void *pB, const void *pD) {
509+
const i8 *a = (const i8 *)pA;
510+
const i8 *b = (const i8 *)pB;
511+
size_t d = *((const size_t *)pD);
512+
const i8 *aEnd = a + d;
513+
514+
int32x4_t dot_acc1 = vdupq_n_s32(0);
515+
int32x4_t dot_acc2 = vdupq_n_s32(0);
516+
int32x4_t aMag_acc1 = vdupq_n_s32(0);
517+
int32x4_t aMag_acc2 = vdupq_n_s32(0);
518+
int32x4_t bMag_acc1 = vdupq_n_s32(0);
519+
int32x4_t bMag_acc2 = vdupq_n_s32(0);
520+
521+
while (a < aEnd - 31) {
522+
int8x16_t va1 = vld1q_s8(a);
523+
int8x16_t vb1 = vld1q_s8(b);
524+
int16x8_t a1_lo = vmovl_s8(vget_low_s8(va1));
525+
int16x8_t a1_hi = vmovl_s8(vget_high_s8(va1));
526+
int16x8_t b1_lo = vmovl_s8(vget_low_s8(vb1));
527+
int16x8_t b1_hi = vmovl_s8(vget_high_s8(vb1));
528+
529+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a1_lo), vget_low_s16(b1_lo));
530+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a1_lo), vget_high_s16(b1_lo));
531+
dot_acc2 = vmlal_s16(dot_acc2, vget_low_s16(a1_hi), vget_low_s16(b1_hi));
532+
dot_acc2 = vmlal_s16(dot_acc2, vget_high_s16(a1_hi), vget_high_s16(b1_hi));
533+
534+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a1_lo), vget_low_s16(a1_lo));
535+
aMag_acc1 =
536+
vmlal_s16(aMag_acc1, vget_high_s16(a1_lo), vget_high_s16(a1_lo));
537+
aMag_acc2 = vmlal_s16(aMag_acc2, vget_low_s16(a1_hi), vget_low_s16(a1_hi));
538+
aMag_acc2 =
539+
vmlal_s16(aMag_acc2, vget_high_s16(a1_hi), vget_high_s16(a1_hi));
540+
541+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b1_lo), vget_low_s16(b1_lo));
542+
bMag_acc1 =
543+
vmlal_s16(bMag_acc1, vget_high_s16(b1_lo), vget_high_s16(b1_lo));
544+
bMag_acc2 = vmlal_s16(bMag_acc2, vget_low_s16(b1_hi), vget_low_s16(b1_hi));
545+
bMag_acc2 =
546+
vmlal_s16(bMag_acc2, vget_high_s16(b1_hi), vget_high_s16(b1_hi));
547+
548+
int8x16_t va2 = vld1q_s8(a + 16);
549+
int8x16_t vb2 = vld1q_s8(b + 16);
550+
int16x8_t a2_lo = vmovl_s8(vget_low_s8(va2));
551+
int16x8_t a2_hi = vmovl_s8(vget_high_s8(va2));
552+
int16x8_t b2_lo = vmovl_s8(vget_low_s8(vb2));
553+
int16x8_t b2_hi = vmovl_s8(vget_high_s8(vb2));
554+
555+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a2_lo), vget_low_s16(b2_lo));
556+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a2_lo), vget_high_s16(b2_lo));
557+
dot_acc2 = vmlal_s16(dot_acc2, vget_low_s16(a2_hi), vget_low_s16(b2_hi));
558+
dot_acc2 = vmlal_s16(dot_acc2, vget_high_s16(a2_hi), vget_high_s16(b2_hi));
559+
560+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a2_lo), vget_low_s16(a2_lo));
561+
aMag_acc1 =
562+
vmlal_s16(aMag_acc1, vget_high_s16(a2_lo), vget_high_s16(a2_lo));
563+
aMag_acc2 = vmlal_s16(aMag_acc2, vget_low_s16(a2_hi), vget_low_s16(a2_hi));
564+
aMag_acc2 =
565+
vmlal_s16(aMag_acc2, vget_high_s16(a2_hi), vget_high_s16(a2_hi));
566+
567+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b2_lo), vget_low_s16(b2_lo));
568+
bMag_acc1 =
569+
vmlal_s16(bMag_acc1, vget_high_s16(b2_lo), vget_high_s16(b2_lo));
570+
bMag_acc2 = vmlal_s16(bMag_acc2, vget_low_s16(b2_hi), vget_low_s16(b2_hi));
571+
bMag_acc2 =
572+
vmlal_s16(bMag_acc2, vget_high_s16(b2_hi), vget_high_s16(b2_hi));
573+
574+
a += 32;
575+
b += 32;
576+
}
577+
578+
while (a < aEnd - 15) {
579+
int8x16_t va = vld1q_s8(a);
580+
int8x16_t vb = vld1q_s8(b);
581+
int16x8_t a_lo = vmovl_s8(vget_low_s8(va));
582+
int16x8_t a_hi = vmovl_s8(vget_high_s8(va));
583+
int16x8_t b_lo = vmovl_s8(vget_low_s8(vb));
584+
int16x8_t b_hi = vmovl_s8(vget_high_s8(vb));
585+
586+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a_lo), vget_low_s16(b_lo));
587+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a_lo), vget_high_s16(b_lo));
588+
dot_acc1 = vmlal_s16(dot_acc1, vget_low_s16(a_hi), vget_low_s16(b_hi));
589+
dot_acc1 = vmlal_s16(dot_acc1, vget_high_s16(a_hi), vget_high_s16(b_hi));
590+
591+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a_lo), vget_low_s16(a_lo));
592+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_high_s16(a_lo), vget_high_s16(a_lo));
593+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_low_s16(a_hi), vget_low_s16(a_hi));
594+
aMag_acc1 = vmlal_s16(aMag_acc1, vget_high_s16(a_hi), vget_high_s16(a_hi));
595+
596+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b_lo), vget_low_s16(b_lo));
597+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_high_s16(b_lo), vget_high_s16(b_lo));
598+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_low_s16(b_hi), vget_low_s16(b_hi));
599+
bMag_acc1 = vmlal_s16(bMag_acc1, vget_high_s16(b_hi), vget_high_s16(b_hi));
600+
601+
a += 16;
602+
b += 16;
603+
}
604+
605+
int32x4_t dot_sum = vaddq_s32(dot_acc1, dot_acc2);
606+
int32x4_t aMag_sum = vaddq_s32(aMag_acc1, aMag_acc2);
607+
int32x4_t bMag_sum = vaddq_s32(bMag_acc1, bMag_acc2);
608+
609+
i32 dot = vaddvq_s32(dot_sum);
610+
i32 aMag = vaddvq_s32(aMag_sum);
611+
i32 bMag = vaddvq_s32(bMag_sum);
612+
613+
while (a < aEnd) {
614+
dot += (i32)*a * (i32)*b;
615+
aMag += (i32)*a * (i32)*a;
616+
bMag += (i32)*b * (i32)*b;
617+
a++;
618+
b++;
619+
}
620+
621+
return 1 - ((f32)dot / (sqrt((f32)aMag) * sqrt((f32)bMag)));
622+
}
403623
#endif
404624

405625
static f32 l2_sqr_float(const void *pVect1v, const void *pVect2v,
@@ -582,6 +802,11 @@ static f32 distance_cosine_bit(const void *pA, const void *pB,
582802

583803
static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v,
584804
const void *qty_ptr) {
805+
#ifdef SQLITE_VEC_ENABLE_NEON
806+
if ((*(const size_t *)qty_ptr) > 16) {
807+
return cosine_float_neon(pVect1v, pVect2v, qty_ptr);
808+
}
809+
#endif
585810
f32 *pVect1 = (f32 *)pVect1v;
586811
f32 *pVect2 = (f32 *)pVect2v;
587812
size_t qty = *((size_t *)qty_ptr);
@@ -604,6 +829,11 @@ static f32 distance_cosine_float(const void *pVect1v, const void *pVect2v,
604829
}
605830
static f32 distance_cosine_int8(const void *pA, const void *pB,
606831
const void *pD) {
832+
#ifdef SQLITE_VEC_ENABLE_NEON
833+
if ((*(const size_t *)pD) > 15) {
834+
return cosine_int8_neon(pA, pB, pD);
835+
}
836+
#endif
607837
i8 *a = (i8 *)pA;
608838
i8 *b = (i8 *)pB;
609839
size_t d = *((size_t *)pD);

‎tests/test-loadable.py‎

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,22 @@ def check(a, b, dtype: type = np.float32):
417417
b = np.array(b, dtype=dtype)
418418

419419
x = vec_distance_cosine(a, b, a=transform, b=transform)
420-
y = npy_cosine(a, b)
420+
# the reference in float64: numpy sums int8 products in int8
421+
y = npy_cosine(a.astype(np.float64), b.astype(np.float64))
421422
assert isclose(x, y, abs_tol=1e-6)
422423

423424
check([1.2, 0.1], [0.4, -0.4])
424425
check([-1.2, -0.1], [-0.4, 0.4])
425426
check([1, 2, 3], [-9, -8, -7], dtype=np.int8)
426427
assert vec_distance_cosine("[1.1, 1.0]", "[1.2, 1.2]") == 0.0011318627512082458
427428

429+
# lengths past the SIMD kernels' thresholds, with and without a remainder
430+
# past their 16-element blocks
431+
rng = np.random.default_rng(324)
432+
for n in [17, 31, 32, 40, 127, 768]:
433+
check(rng.uniform(-1, 1, n), rng.uniform(-1, 1, n))
434+
check(rng.integers(-128, 128, n), rng.integers(-128, 128, n), dtype=np.int8)
435+
428436
vec_distance_cosine_bit = lambda *args: db.execute(
429437
"select vec_distance_cosine(vec_bit(?), vec_bit(?))", args
430438
).fetchone()[0]
@@ -477,18 +485,22 @@ def test_vec_distance_cosine_extreme_magnitudes():
477485
"select vec_distance_cosine(?, ?)", [_f32(a), _f32(b)]
478486
).fetchone()[0]
479487

480-
for scale in [1e-45, 1e-30, 1e-25, 1e-20, 1.0, 1e20, 3e38]:
481-
# cosine distance is invariant under positive scaling
482-
assert vec_distance_cosine([1, 0], [scale, 0]) == 0.0
483-
assert vec_distance_cosine([scale, 0], [1, 0]) == 0.0
484-
assert vec_distance_cosine([scale, 0], [scale, 0]) == 0.0
485-
assert vec_distance_cosine([1, 0], [-scale, 0]) == 2.0
486-
assert vec_distance_cosine([1, 0], [0, scale]) == 1.0
487-
assert isclose(
488-
vec_distance_cosine([1, 0], [scale, scale]),
489-
1 - 1 / np.sqrt(2),
490-
abs_tol=1e-6,
491-
)
488+
# the same 2-element vectors padded to 40 elements, past the SIMD kernels'
489+
# threshold, with a remainder past their 16-element blocks
490+
for pad in [0, 38]:
491+
z = [0] * pad
492+
for scale in [1e-45, 1e-30, 1e-25, 1e-20, 1.0, 1e20, 3e38]:
493+
# cosine distance is invariant under positive scaling
494+
assert vec_distance_cosine([1, 0] + z, [scale, 0] + z) == 0.0
495+
assert vec_distance_cosine([scale, 0] + z, [1, 0] + z) == 0.0
496+
assert vec_distance_cosine([scale, 0] + z, [scale, 0] + z) == 0.0
497+
assert vec_distance_cosine([1, 0] + z, [-scale, 0] + z) == 2.0
498+
assert vec_distance_cosine([1, 0] + z, [0, scale] + z) == 1.0
499+
assert isclose(
500+
vec_distance_cosine([1, 0] + z, [scale, scale] + z),
501+
1 - 1 / np.sqrt(2),
502+
abs_tol=1e-6,
503+
)
492504

493505

494506
def test_ensure_vector_match_cleanup_on_second_vector_error():

0 commit comments

Comments
 (0)