From 114a2dfcbae304f18129a8e493c0992f38b9a42a Mon Sep 17 00:00:00 2001 From: osobh Date: Mon, 21 Sep 2026 17:34:51 -0700 Subject: [PATCH] docs: measured ARM numbers, and a correction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a Raspberry Pi 5 at N = 100 000 and equal recall (0.9940 vs 0.9945), medians of three runs: f32 33 413 ms build 6 164 QPS int8 scalar 18 950 ms ~6 190 QPS (what v2.7.0 shipped) int8 NEON ~17 000 ms 6 640 QPS int8 SDOT 14 464 ms 7 267 QPS 1.18x f32, 2.3x build The docs said quantised search stayed off by default because aarch64 "falls back to the scalar loop, where the original trade still applies" — that it was ~13% slower than f32 there, as on x86. That was extrapolated rather than measured, and it was wrong: x86's portable baseline is SSE2 against hand-written AVX2 f32 kernels, but on aarch64 NEON is the baseline and the scalar loop vectorises well, so it already matched f32. Corrected in BENCHMARKS.md, README.md and CLAUDE.md; the released v2.7.0 changelog entry is left as it was and the correction is recorded in a new one. Labelled as Pi 5 figures throughout — a Pi's memory bandwidth and cache are far below an M-series or flagship phone, so the ratios will move. Co-Authored-By: Claude Opus 5 (1M context) --- BENCHMARKS.md | 36 +++++++++++++++++++++++++++++++----- CHANGELOG.md | 25 +++++++++++++++++++++++++ CLAUDE.md | 10 ++++++---- README.md | 7 +++---- 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/BENCHMARKS.md b/BENCHMARKS.md index edcbdcb..1d782e4 100644 --- a/BENCHMARKS.md +++ b/BENCHMARKS.md @@ -108,11 +108,37 @@ second**, builds **1.8x faster**, and holds a quarter of the vectors. (Compare only at equal `ef`: with re-scoring the harness raises `ef` to at least the candidate pool, so the `ef = 16` and `ef = 32` rows are not like-for-like.) -It is still **off by default**, for portability rather than performance: the -int8 kernel is AVX2-only, and on aarch64 — including `clawhdf5-android` — it -falls back to the scalar loop, where the original trade still applies. A NEON -kernel would remove that caveat. On an x86-64 deployment, turning it on is a -win on every axis measured. +#### On ARM (Raspberry Pi 5, Cortex-A76) + +`dot_i8` has two aarch64 kernels: `SDOT` for CPUs with the ARMv8.2 +dot-product extension (Cortex-A76 and later, Neoverse-N1, all Apple Silicon) +and plain NEON (`vmull_s8` + `vpadalq_s16`) otherwise. Medians of three runs +at N = 100 000, ef = 64, recall@10 0.9940 in every int8 row against f32's +0.9945: + +| int8 kernel | build | QPS | vs f32 | +|---|---:|---:|---:| +| *(f32 baseline)* | 33 413 ms | 6 164 | 1.00x | +| scalar (what v2.7.0 shipped) | 18 950 ms | ~6 190 | 1.00x | +| plain NEON | ~17 000 ms | 6 640 | 1.08x | +| **SDOT** | **14 464 ms** | **7 267** | **1.18x** | + +These are Pi 5 numbers, not "ARM" numbers: a Pi has far less memory bandwidth +and cache than an Apple M-series or a flagship phone, so the ratios will move +on other hardware. The plain-NEON row is that code on an A76 with `SDOT` +disabled, not a measurement of a pre-A76 core. + +**A correction.** Until this was measured, this section said aarch64 "falls +back to the scalar loop, where the original trade still applies" — that is, +that quantised search was ~13% slower than f32 on ARM. That was extrapolated +from x86 and it was wrong. On x86-64 the portable baseline is SSE2 while the +f32 kernels are hand-written AVX2, so scalar int8 lost; on aarch64 NEON *is* +the baseline, the compiler vectorises the scalar loop well, and scalar int8 +already matched f32 for search while building 1.76x faster. + +So on every configuration measured — x86-64 AVX2, and Pi 5 with each of the +three int8 kernels — the quantised index is at least as fast as f32 at equal +recall, builds faster, and holds a quarter of the vectors. A measurement trap worth recording: the synthetic `clustered` generator in the `clawhdf5-ann` tests draws clusters far tighter than any real embedding, so diff --git a/CHANGELOG.md b/CHANGELOG.md index 73a987a..c2a8e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # Changelog +## Unreleased + +### Performance +- `clawhdf5-accel`: **`dot_i8` has aarch64 kernels** — `SDOT` for CPUs with + the ARMv8.2 dot-product extension (Cortex-A76 and later, Neoverse-N1, every + Apple Silicon generation) and plain NEON (`vmull_s8` + `vpadalq_s16`) for + the rest, selected at runtime. `SDOT` is issued through inline assembly, + because the `vdotq_s32` intrinsic is still behind the unstable + `stdarch_neon_dotprod` feature. On a Raspberry Pi 5 at N = 100 000 and + equal recall, the quantised index answers **1.18x the queries per second** + of f32 (7 267 vs 6 164) and builds **2.3x faster** (14 464 vs 33 413 ms). + Both kernels are tested bit-for-bit against scalar on real hardware, each + explicitly — dispatch only ever takes one path on a given CPU, so testing + through it alone would have left the plain-NEON fallback unexercised on any + machine with `SDOT`. + +### Corrections +- The v2.7.0 entry for `dot_i8` said `quantized_index` stayed off by default + because "aarch64 falls back to the scalar loop", implying the ~13% search + penalty measured on x86 applied on ARM too. It did not. That figure came + from scalar int8 against hand-written AVX2 f32 kernels on x86, whose + portable baseline is SSE2; on aarch64 NEON is the baseline, and measured on + a Pi 5 the scalar int8 loop already matched f32 for search while building + 1.76x faster. The claim was extrapolated rather than measured. + ## v2.7.0 (2026-09-20) ### Upgrade Notes diff --git a/CLAUDE.md b/CLAUDE.md index b295dd3..021d3a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,10 +44,12 @@ Cargo workspace with 16 crates under `crates/` (plus `libaec-sys`, an internal F which roughly halves a loaded store's memory (2.72x -> 1.74x the raw vectors at 100K); because quantised distances are approximate and `ef` cannot compensate, the query path then re-scores the candidate pool against the - exact embeddings, which holds recall at the f32 index's level. On AVX2 it is - also 1.63x the QPS and 1.8x the build speed (`clawhdf5_accel::dot_i8`); it - stays off by default only because that kernel is AVX2-only and aarch64 falls - back to scalar. `hybrid_search` keeps one incremental BM25 + exact embeddings, which holds recall at the f32 index's level. It is also + faster at equal recall: 1.63x the QPS on x86-64 (AVX2) and 1.18x on a + Raspberry Pi 5 (`clawhdf5_accel::dot_i8`, NEON `SDOT` via inline asm since + the intrinsic is unstable; plain NEON on pre-dotprod cores). The aarch64 + code is `cfg`'d out on x86, so x86 CI never compiles or lints it — test it + on real ARM (`rpivision02`, 10.0.2.3, is a Pi 5). `hybrid_search` keeps one incremental BM25 index for the life of the store and never writes the store: Hebbian activation boosts are persisted by the next checkpoint (or on drop), not per query. Measure any search-path change with diff --git a/README.md b/README.md index 2a611a9..838dd67 100644 --- a/README.md +++ b/README.md @@ -442,10 +442,9 @@ copy of the embeddings as `i8`, roughly halving a loaded store's memory (2.72x -> 1.74x the raw vectors at 100k x 384). Quantised distances are approximate, so the query path re-scores the candidate pool against the exact embeddings the store already holds, which keeps recall at the `f32` index's -level. On AVX2 it is also **faster** — 1.63x the queries per second and 1.8x -the build speed at equal recall — because the int8 kernel is SIMD too. It -stays off by default only because that kernel is AVX2-only and aarch64 falls -back to a scalar loop. See `BENCHMARKS.md`, "Quantising the index copy". +level. It is also **faster**: 1.63x the queries per second at equal recall on +x86-64 (AVX2) and 1.18x on a Raspberry Pi 5 (NEON `SDOT`), with index builds +1.8x and 2.3x faster respectively. See `BENCHMARKS.md`, "Quantising the index copy". | `parallel` | no | Rayon parallel search | | `fast-math` | no | BLAS matrix-vector multiply | | `accelerate` | no | Apple Accelerate / AMX (macOS) |