perf(ann): unit-vector dot product and a reusable visited set

- Cosine distance was 1 - dot/(|a||b|), re-deriving both norms on every
  evaluation in the innermost loop of build and search. The index now stores
  unit vectors (prepared at build, insert, graph load and HDF5 load; the query
  once per search) and uses 1 - dot. Zero vectors stay zero, giving distance 1
  as before. Returned distances are unchanged.
- search_layer allocated a HashSet of visited nodes per call. It is now an
  epoch-stamped u32 array in thread-local scratch, reused across calls, so
  search(&self) stays shareable between threads.
- clawhdf5-accel caches the detected SIMD backend in a OnceLock.

Recall is identical. Build 2.75 -> 1.89 s (10K), ~38 -> 21 s (100K); QPS at
ef=64 22.7K -> 39K (10K), 10.4K -> 14K (100K).

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
osobh
2026-09-19 13:21:40 -07:00
co-authored by Claude Fable 5.1
parent 09480747aa
commit f15bf2eb22
4 changed files with 150 additions and 10 deletions
+45
View File
@@ -176,6 +176,51 @@ index incrementally.
| 10000 | 120 | 2916 | 33.1 | 14.0 | 15.4 | 2.15 | 3.30 | 421.2 |
| 100000 | 1591 | 40515 | 747.3 | 324.7 | 158.9 | 23.07 | 30.42 | 41.3 |
### After: unit-vector dot product, reusable visited set
Cosine distance recomputed both vector norms on every evaluation; the index now
stores unit vectors and uses a plain dot product. The per-call `HashSet` of
visited nodes became a reusable epoch-stamped array. Recall is unchanged.
Build: **2.75 -> 1.89 s** (10K), **~38 -> 21 s** (100K). QPS at `ef = 64`:
**22.7K -> 39K** (10K), **10.4K -> 14K** (100K).
### HNSW, N = 1000, dim = 384, M = 16, ef_construction = 64
build: 113.4 ms (8821 vectors/s) · exact scan: 4375 QPS, p50 225 µs
| ef | recall@10 | QPS | p50 µs | p99 µs |
|---:|---:|---:|---:|---:|
| 16 | 0.9990 | 144379 | 7 | 15 |
| 32 | 1.0000 | 110654 | 9 | 17 |
| 64 | 1.0000 | 80446 | 12 | 25 |
| 128 | 1.0000 | 38220 | 26 | 36 |
| 256 | 1.0000 | 20041 | 50 | 62 |
### HNSW, N = 10000, dim = 384, M = 16, ef_construction = 64
build: 1519.4 ms (6581 vectors/s) · exact scan: 422 QPS, p50 2368 µs
| ef | recall@10 | QPS | p50 µs | p99 µs |
|---:|---:|---:|---:|---:|
| 16 | 0.9975 | 54608 | 15 | 45 |
| 32 | 1.0000 | 66009 | 14 | 24 |
| 64 | 1.0000 | 49854 | 19 | 31 |
| 128 | 1.0000 | 22403 | 45 | 57 |
| 256 | 1.0000 | 10096 | 100 | 120 |
### HNSW, N = 100000, dim = 384, M = 16, ef_construction = 64
build: 21084.6 ms (4743 vectors/s) · exact scan: 39 QPS, p50 24739 µs
| ef | recall@10 | QPS | p50 µs | p99 µs |
|---:|---:|---:|---:|---:|
| 16 | 0.9235 | 15139 | 61 | 154 |
| 32 | 0.9675 | 18181 | 53 | 121 |
| 64 | 0.9840 | 13980 | 70 | 139 |
| 128 | 0.9990 | 10959 | 86 | 174 |
| 256 | 0.9990 | 3731 | 254 | 697 |
## Vector Search Latency
Brute-force cosine similarity over 384-dimensional embeddings (OpenAI text-embedding-3-small size).