Repository navigation
Use NEON on Linux arm64 - #21
Conversation
The Makefile enabled NEON only on macOS arm64, so a Linux arm64 build ran every distance's scalar loop. NEON is part of the arm64 baseline: GCC and Clang define __ARM_NEON for aarch64 with no -m flag, so the Linux build needs only the define, inside the guard that keeps host detection away from Android cross-compiles. OMIT_SIMD=1 still builds without it. The define follows the compiler's target ($(CC) -dumpmachine), not uname, as mceachen's photostructure/sqlite-vec@350c67a has it: 32-bit Raspberry Pi OS runs a 64-bit kernel, so uname says aarch64 while gcc targets 32-bit ARM, which lacks the AArch64-only intrinsics the NEON kernels use. Under an aarch64 kernel, make loadable CC=arm-linux-gnueabihf-gcc fails when uname decides (implicit declaration of vaddvq_f32, unknown type name float64x2_t) and builds a 32-bit ARM extension when the compiler does; for gcc and clang targeting aarch64 the build commands are the same either way. GCC rejected the NEON code: l1_int8_neon() passed the signed vectors vabdq_s8() returns to vpaddlq_u8(), and the unsigned sums to vaddq_s32(), conversions Clang makes implicitly and GCC refuses. They are now explicit reinterprets, as photostructure/sqlite-vec a5aec74 writes them, which changes no result: L1 distances between int8 vectors match the scalar loop's. Built with GCC 14 at -O3 on Linux arm64, an L2 KNN query on a 100,000-row float[768] table takes 32.6 ms instead of 77.1 ms for k = 10 and 39.7 ms instead of 84.3 ms for k = 100 (medians of 20 queries). L1 and Hamming distances are bit-identical to the scalar loops' on 2,000 random pairs at each of six lengths from 8 to 1,536. The float32 L1 kernel adds in another order than the scalar loop, though, both in double, so where the sum rounds the two differ in the last digits: on values spread from 1e-8 to 1e8, for 176 of 500 random pairs at 8 dimensions and 467 of 500 at 768. Float32 L2 distances differ in the last digits, since the NEON kernel splits the sum across 16 partial sums where the scalar loop keeps one, and are closer to the exact value: against a float64 reference the mean relative error at 768 dimensions is 3.0e-8 instead of 1.7e-7. So a KNN query can order rows with nearly equal distances differently, as the macOS arm64 build already does; 4 of the 2,000 ranks of the k = 100 queries swapped such rows. The int8 L2 kernel sums exactly and the scalar loop in float, exact only below 2^24, so the two differ only past that, from about 1,536 random dimensions. The Test workflow ran nothing on Linux arm64: build-linux-aarch64-extension cross-compiles for 32-bit ARM on an x86_64 runner, where the Makefile leaves NEON off, and runs no tests. test-linux-aarch64-extension runs the x86_64 job's build and tests on ubuntu-24.04-arm. test-loadable.py passes on the NEON and the scalar GCC build, and make test-unit, which compiles without the Makefile's CFLAGS, passes too.
4605626 to
396b74c
Compare
|
Thanks, this reproduces exactly as described: I could not push to your branch: your repository is a fork of asg017/sqlite-vec rather than of this one, so GitHub never grants the maintainer-edit permission here even with the box ticked (the push is refused with 403). So I merged the PR with one commit of mine on top, 7ad5801: the int8 L2 accumulator your description mentions is now an The test in One thing I noticed but did not change: |
The Makefile enables NEON only on macOS arm64, so a Linux arm64 build runs every distance's scalar loop. NEON is part of the arm64 baseline: GCC and Clang define
__ARM_NEONfor aarch64 with no-mflag, so the Linux build needs only the define, inside the guard that keeps host detection away from Android cross-compiles.OMIT_SIMD=1still builds without it.The define follows the compiler's target,
$(CC) -dumpmachine, notuname, as mceachen's photostructure/sqlite-vec@350c67a has it: 32-bit Raspberry Pi OS runs a 64-bit kernel, sounamesays aarch64 while gcc targets 32-bit ARM, which lacks the AArch64-only intrinsics the NEON kernels use. Under an aarch64 kernel,make loadable CC=arm-linux-gnueabihf-gccfails whenunamedecides (implicit declaration ofvaddvq_f32, unknown type namefloat64x2_t) and builds a 32-bit ARM extension when the compiler does; for gcc and clang targeting aarch64,make -n loadable static cliprints the same commands either way.GCC could not build the NEON code:
l1_int8_neon()passes the signed vectorsvabdq_s8()returns tovpaddlq_u8(), and the unsigned sums tovaddq_s32(), conversions Clang makes implicitly and GCC refuses, so a GCC build with-DSQLITE_VEC_ENABLE_NEONfails on main. They are now explicit reinterprets, as photostructure/sqlite-vec@a5aec74 writes them; L1 distances betweenint8vectors still match the scalar loop's.Built with GCC 14 at
-O3on Linux arm64 (medians of 20 queries on a 100,000-rowfloat[768]table with the default L2 metric):What changes in the results, on 2,000 random pairs at each of six lengths from 8 to 1,536:
float32L1 kernel adds in another order than the scalar loop, though, both in double, so where the sum rounds the two differ in the last digits: on values spread from 1e-8 to 1e8, for 176 of 500 random pairs at 8 dimensions and 467 of 500 at 768.float32L2 distances differ in the last digits, since the NEON kernel splits the sum across 16 partial sums where the scalar loop keeps one, and they are closer to the exact value: against a float64 reference, the mean relative error at 768 dimensions is 3.0e-8 instead of 1.7e-7. So a KNN query can order rows with nearly equal distances differently, as the macOS arm64 build already does; 4 of the 2,000 ranks of the k = 100 queries above swapped such rows.int8L2 distances: the kernel sums exactly and the scalar loop infloat, which is exact only below 2^24, so they differ only past that, from about 1,536 random dimensions. The kernel keeps that sum in 32 bits, whichvec_distance_l2()overflows past 33,026 elements at opposite extremes, as it already does on macOS arm64; photostructure/sqlite-vec@e395db1 keeps it in 64 bits. vec0 tables allow at most 8,192 dimensions, so KNN queries are not affected.The Test workflow ran nothing on Linux arm64:
build-linux-aarch64-extensioncross-compiles for 32-bit ARM on an x86_64 runner, where the Makefile leaves NEON off, and runs no tests.test-linux-aarch64-extensionruns the x86_64 job's build and tests onubuntu-24.04-arm, without the artifact upload, whose name the cross-compile job already uses, and without the Rust harness, whose build script compiles without SIMD. Locallymake test-loadable(143 passed, 4 skipped) passes on the NEON and the scalar GCC builds, andmake test-unit, which compiles without the Makefile'sCFLAGS, passes too. CLAUDE.md's SIMD line names Linux arm64.#20 ports the NEON cosine kernels. The two merge cleanly with each other and with #9 through #14, #17 through #19 and #22 through #24, and together they build with GCC and Clang without warnings and pass the suite. The same change is on photostructure/sqlite-vec's main, from photostructure#6 with mceachen's 350c67a on top, where the NEON code already compiled with GCC, and it is open on asg017/sqlite-vec as asg017#339, on top of asg017#322's fix for the same conversions.