docs: measured ARM numbers, and a correction

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) <[email protected]>
This commit is contained in:
osobh
2026-09-21 17:34:51 -07:00
co-authored by Claude Opus 5
parent 56a8c2f3d0
commit 114a2dfcba
4 changed files with 65 additions and 13 deletions
+31 -5
View File
@@ -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 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.) 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 #### On ARM (Raspberry Pi 5, Cortex-A76)
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 `dot_i8` has two aarch64 kernels: `SDOT` for CPUs with the ARMv8.2
kernel would remove that caveat. On an x86-64 deployment, turning it on is a dot-product extension (Cortex-A76 and later, Neoverse-N1, all Apple Silicon)
win on every axis measured. 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 A measurement trap worth recording: the synthetic `clustered` generator in the
`clawhdf5-ann` tests draws clusters far tighter than any real embedding, so `clawhdf5-ann` tests draws clusters far tighter than any real embedding, so
+25
View File
@@ -1,5 +1,30 @@
# Changelog # 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) ## v2.7.0 (2026-09-20)
### Upgrade Notes ### Upgrade Notes
+6 -4
View File
@@ -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 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 at 100K); because quantised distances are approximate and `ef` cannot
compensate, the query path then re-scores the candidate pool against the 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 exact embeddings, which holds recall at the f32 index's level. It is also
also 1.63x the QPS and 1.8x the build speed (`clawhdf5_accel::dot_i8`); it faster at equal recall: 1.63x the QPS on x86-64 (AVX2) and 1.18x on a
stays off by default only because that kernel is AVX2-only and aarch64 falls Raspberry Pi 5 (`clawhdf5_accel::dot_i8`, NEON `SDOT` via inline asm since
back to scalar. `hybrid_search` keeps one incremental BM25 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 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 activation boosts are persisted by the next checkpoint (or on drop), not per
query. Measure any search-path change with query. Measure any search-path change with
+3 -4
View File
@@ -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 (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 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 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 level. It is also **faster**: 1.63x the queries per second at equal recall on
the build speed at equal recall — because the int8 kernel is SIMD too. It x86-64 (AVX2) and 1.18x on a Raspberry Pi 5 (NEON `SDOT`), with index builds
stays off by default only because that kernel is AVX2-only and aarch64 falls 1.8x and 2.3x faster respectively. See `BENCHMARKS.md`, "Quantising the index copy".
back to a scalar loop. See `BENCHMARKS.md`, "Quantising the index copy".
| `parallel` | no | Rayon parallel search | | `parallel` | no | Rayon parallel search |
| `fast-math` | no | BLAS matrix-vector multiply | | `fast-math` | no | BLAS matrix-vector multiply |
| `accelerate` | no | Apple Accelerate / AMX (macOS) | | `accelerate` | no | Apple Accelerate / AMX (macOS) |