From 33b1c2a0b11eea6b170ee4660297c0d59f51c7f6 Mon Sep 17 00:00:00 2001 From: Aaron Paterson Date: Thu, 22 Oct 2026 18:42:37 -0500 Subject: [PATCH] perf(Makefile): use NEON on Linux arm64 The Makefile enabled NEON only on Darwin arm64, so the linux-arm64 and linux-arm64-musl release binaries, which release.yaml builds with make loadable on arm64 runners, 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, and unlike AVX2 on x86_64 there is no Linux arm64 CPU it would exclude. OMIT_SIMD=1 still builds without it. On Linux arm64 (GCC 14, -O3), an L2 KNN query on a 100,000-row float[768] table takes 19.3 ms instead of 57.4 ms for k = 10 and 25.2 ms instead of 65.1 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. 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 darwin-arm64 binary already does; 4 of the 2,000 ranks of the k = 100 queries swapped such rows. The int8 L2 kernel sums exactly in 64-bit lanes 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 had no Linux arm64 job, so the GCC build of the NEON code went untested before a release. build-linux-aarch64-extension runs build-linux-x86_64-extension's steps on ubuntu-24.04-arm, the runner release.yaml builds linux-arm64 on. The full suite and make test-unit pass on Linux arm64, and the source compiles against musl with -Wall -Wextra and no warnings. --- .github/workflows/test.yaml | 22 ++++++++++++++++++++++ CHANGELOG.md | 1 + CLAUDE.md | 2 +- Makefile | 6 +++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e4208de2..7af139a3 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -45,6 +45,28 @@ jobs: path: dist/* # after the upload, so dist/test-unit stays out of the artifact - run: make test-unit + build-linux-aarch64-extension: + needs: memory-gate # Require memory tests to pass + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@681c641aba71e4a1c380be3ab5e12ad51f415867 # v7.1.6 + with: + enable-cache: true + - run: ./scripts/vendor.sh + - run: make loadable static + # Ubuntu's system Python links the system SQLite (3.45.1 on 24.04); uv's + # managed Python bundles a newer one, as on the macOS runners + - run: uv sync --locked --managed-python --directory tests + - run: make test-loadable python=./tests/.venv/bin/python + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: sqlite-vec-linux-aarch64-extension + path: dist/* + # after the upload, so dist/test-unit stays out of the artifact + - run: make test-unit build-macos-x86_64-extension: runs-on: macos-15-intel steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index da5f90d1..cd754144 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Changed - Reading a vector column without a KNN query, as in `select embedding from v`, reads each chunk's vectors through one blob handle instead of opening the chunk's blob again for every row: selecting every vector of a 10,000-row `float[768]` table took 25 ms instead of 2,080 ms, and `select vec_distance_cosine(embedding, ?) from v` 41 ms instead of 2,093 ms. +- The linux-arm64 and linux-arm64-musl binaries use NEON, as the darwin-arm64 binary does: `make` enables it on Linux arm64, and `OMIT_SIMD=1` builds without it. An L2 KNN query on a 100,000-row `float[768]` table took 19 ms instead of 57 ms there. L1 and Hamming distances are unchanged, but `float32` L2 distances can differ from 2.1.1's in the last digits, closer to the exact value (at 768 dimensions, a mean relative error of 3.0e-8 instead of 1.7e-7), so a KNN query can order rows with nearly equal distances differently; `int8` L2 distances whose squared differences sum past 2^24 are computed from the exact sum. ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 8f379587..6de03e76 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,7 @@ Run `./scripts/vendor.sh` first to download vendored dependencies (sqlite3.c, sh - Set `AR=` to use a different archiver (default: ar) **SIMD control:** -- SIMD is auto-enabled on Darwin x86_64 (AVX) and Darwin arm64 (NEON) +- SIMD is auto-enabled on Darwin x86_64 (AVX), Darwin arm64 (NEON), and Linux arm64 (NEON) - Set `OMIT_SIMD=1` to disable SIMD optimizations ### Testing diff --git a/Makefile b/Makefile index a701a2f4..9050cc7b 100644 --- a/Makefile +++ b/Makefile @@ -29,12 +29,16 @@ else # Windows: -ldl not needed (Win32 API), math is linked by default endif -# ── SIMD (auto-enabled on Apple Silicon and x86_64 Mac) ────────────────────────── +# ── SIMD (auto-enabled on Apple Silicon, arm64 Linux and x86_64 Mac) ───────────── +# NEON is in the arm64 baseline (GCC and Clang define __ARM_NEON for aarch64 +# with no -m flag), so the Linux build needs only the define. ifndef OMIT_SIMD ifeq ($(shell uname -sm),Darwin x86_64) CFLAGS += -mavx -DSQLITE_VEC_ENABLE_AVX else ifeq ($(shell uname -sm),Darwin arm64) CFLAGS += -mcpu=apple-m1 -DSQLITE_VEC_ENABLE_NEON + else ifeq ($(shell uname -sm),Linux aarch64) + CFLAGS += -DSQLITE_VEC_ENABLE_NEON endif endif