Skip to content

Add an AVX cosine distance for float vectors - #364

Open
rich-murphey wants to merge 1 commit into
asg017:mainfrom
rich-murphey:avx-cosine
Open

rich-murphey wants to merge 1 commit into
asg017:mainfrom
rich-murphey:avx-cosine

Conversation

@rich-murphey

Copy link
Copy Markdown

On x86 builds with SQLITE_VEC_ENABLE_AVX, L2 distance uses l2_sqr_float_avx() but cosine distance still runs the scalar loop in distance_cosine_float(). NEON has cosine_float_neon(); AVX had no counterpart. For a cosine vec0 table, this makes cosine queries the slow path on x86.

What changed

  • cosine_float_avx(): 16 floats per step into two independent sets of dot, |a|² and |b|² accumulators, so the loop is not bound by FP-add latency. It uses _mm256_fmadd_ps when the build has __FMA__ and mul+add otherwise, so the existing -mavx -mavx2 flags in the Makefile build it unchanged. The tail (fewer than 16 elements) is finished in scalar code, so every dimension count is handled.
  • distance_cosine_float() dispatches to it for 16 or more dimensions, behind the existing SQLITE_VEC_ENABLE_AVX flag. The function signature and the scalar path are unchanged.
  • test_distance_cosine_float() in tests/test-unit.c checks lengths 15, 16, 17, 31, 33, 47, 384 and 389 against a double-precision reference, within 1e-5. Dropping the tail loop makes it fail.

Tests

Benchmark

AMD EPYC 7351P (Zen 1), GCC, -O3 -mavx -mavx2 (Makefile defaults), pinned to one core with its SMT sibling idle. 304,971 random unit vectors, float[384], k = 400, median of 15 queries through the Python sqlite3 module. The host had other load, so the end-to-end rows vary about ±10% between runs.

main this PR
kernel only: distance_cosine_float() over all rows, in memory 126.5 ms 35.9 ms 3.5×
same, cache-resident (2,000 rows), per row 407 ns 90 ns 4.5×
vec0 cosine KNN, k=400 571 ms 466 ms 1.2×
vec0 cosine KNN, k=400, with #337's heap 298–328 ms 198–234 ms 1.4–1.5×
vec_distance_cosine() … ORDER BY … LIMIT 400 636 ms 513 ms 1.2×

With -mfma the kernel row is 34.2 ms. KNN results are identical to main (recall@400 = 1.000 on every build).

On main, vec0 KNN time is mostly top-k selection, which is why the kernel's 3.5× shows up as 1.2× there. In an earlier measurement on the same machine and data shape inside an application, with #337's heap top-k, exact vec0 KNN went from 194 ms to 87 ms (2.2×) and the vec_distance_cosine scan from 635 ms to 344 ms (1.8×); vec0 with the existing AVX L2 was 92 ms there.

Related

distance_cosine_float() had a NEON kernel but no AVX one, so x86 builds
with SQLITE_VEC_ENABLE_AVX ran the scalar loop for cosine while L2 used
l2_sqr_float_avx(). cosine_float_avx() takes 16 floats per step into two
independent sets of dot, |a|^2 and |b|^2 accumulators, uses FMA when the
build has it (__FMA__) and mul+add otherwise, and finishes the tail of
fewer than 16 elements in scalar code. It is used for 16 or more
dimensions.

test_distance_cosine_float() now checks lengths around the 16-element
block (15 to 389, odd tails included) against a double-precision
reference.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant