feat(agent): optional int8 vector index, re-scored against exact embeddings
`MemoryConfig::quantized_index` stores the HNSW index's own copy of the embeddings as i8 rather than f32. At 100k x 384 that takes the index from 266 to 123 MiB and the whole reopened store from 399 to 256 MiB — 2.72x to 1.74x the raw vectors, the largest remaining item in the footprint. Quantised distances are approximate and `ef` cannot compensate, because the loss is in the distances rather than in the graph: recall@10 tops out at 0.967 against f32's 0.9995 and does not move between ef=128 and ef=256. The store already holds the exact embeddings, though, so when the index is quantised the query path re-scores the candidate pool against them before fusion. That restores recall (0.9940 vs 0.9945 at ef=64) and costs about 13% of QPS. Off by default: it trades query speed for memory and which side is worth more depends on the deployment. The flag is persisted in `/meta`, so a reopened store does not silently revert to four times the index memory, and the sidecar graph is rehydrated into the configured storage. Also on the CLI as `create --quantized-index`. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -57,6 +57,52 @@ What remains at 2.43x: the flat vectors (1.0x), the HNSW index's own copy of
|
||||
them (1.0x), and text, ids and graph (~0.4x). The index copy is the next
|
||||
target — it is what a quantised or borrowed representation would address.
|
||||
|
||||
### Quantising the index copy (`quantized_index`)
|
||||
|
||||
`MemoryConfig::quantized_index` stores the index's copy as `i8` instead of
|
||||
`f32`. Same harness, same binary, `--footprint --full` with and without
|
||||
`--int8`:
|
||||
|
||||
| N | vectors (raw) | indexes, f32 | indexes, int8 | reopened, f32 | reopened, int8 |
|
||||
|---:|---:|---:|---:|---:|---:|
|
||||
| 1 000 | 1 MiB | 2 MiB | 1 MiB | 4 MiB (2.40x) | 2 MiB (1.64x) |
|
||||
| 10 000 | 15 MiB | 32 MiB | 14 MiB | 44 MiB (3.03x) | 27 MiB (1.81x) |
|
||||
| 100 000 | 146 MiB | 266 MiB | **123 MiB** | 399 MiB (2.72x) | **256 MiB (1.74x)** |
|
||||
|
||||
The scale is **per row**, not global. A unit-length row in `d` dimensions has
|
||||
components around `1/sqrt(d)`, so a fixed `[-1, 1]` scale spends fewer than 12
|
||||
of the 255 levels on a 128-dimensional vector: measured against an exact
|
||||
ranking that gives 0.35 top-10 overlap — unusable. Scaling each row by its own
|
||||
largest component brings the same measurement to 0.99.
|
||||
|
||||
Quantised distances still cost recall on their own, and **`ef` does not buy it
|
||||
back**, because the loss is in the distances rather than in the graph
|
||||
(`--ann-only --full`, N = 100 000):
|
||||
|
||||
| ef | recall@10, f32 | recall@10, int8 | recall@10, int8 + re-score |
|
||||
|---:|---:|---:|---:|
|
||||
| 32 | 0.9775 | 0.9415 | 0.9785 |
|
||||
| 64 | 0.9945 | 0.9625 | 0.9940 |
|
||||
| 128 | 0.9995 | 0.9670 | 0.9990 |
|
||||
| 256 | 0.9995 | 0.9670 (ceiling) | 0.9990 |
|
||||
|
||||
Re-scoring closes the gap: the store already holds the exact embeddings, so
|
||||
the query path re-scores the candidate pool against them before fusion. That
|
||||
is done automatically whenever the index is quantised. What it costs is
|
||||
throughput — about 13% of QPS and 16% of build time at 100 000 x 384. So the
|
||||
setting trades ~13% of query speed for ~36% of the process's memory at equal
|
||||
recall. It is **off by default**: the right side of that trade depends on
|
||||
whether the deployment is short of memory or short of CPU.
|
||||
|
||||
A measurement trap worth recording: the synthetic `clustered` generator in the
|
||||
`clawhdf5-ann` tests draws clusters far tighter than any real embedding, so
|
||||
neighbours there sit closer together than the quantisation error and top-10
|
||||
*identity* is noise. Scored on that fixture int8 looks catastrophic (0.57
|
||||
overlap) — a fact about the fixture, not the storage. The tests use random
|
||||
vectors, and recall is measured against brute-force ground truth rather than
|
||||
against the f32 index, whose own approximation errors a re-scored search is
|
||||
entitled to get right.
|
||||
|
||||
## Read harness
|
||||
|
||||
Produced by `cargo run --release -p clawhdf5-bench --bin read_harness`: a 4096 x
|
||||
|
||||
Reference in New Issue
Block a user