Repository navigation
Neighbour spacing, hybrid and adaptive quantization, routing, and zero-copy scans - #2
Merged
Merged
Conversation
Three CSS-keyframe SVGs that GitHub plays inside <img>: two-stage re-scoring (schematic), adaptive re-scoring depth by a per-query margin (schematic), and adaptive bit allocation at 48 bytes on SciFact's real PCA spectrum (uniform binary 0.611, mixed48-pca 0.694, mixed48-pca-w 0.703 recall@10). Light and dark steps follow prefers-color-scheme; under prefers-reduced-motion they rest on the final labelled frame. make_animations.py regenerates all three from the stored allocation.
Records the neighbour-spacing and two-stage re-scoring results, the margin policy, the mixed<B> codec, the query-preparation hook and the README animations as done; the hybrid x adaptive matrix as running; and the follow-ups: automatic re-score depth at ingest, per-vector adaptive bits, per-cluster PCA, and the spacing caveats. Phase 7 notes that its query hook now exists. Next drops the finished re-scoring item and adds graded relevance for NFCorpus.
bench rescore --trace <file> writes one JSON line per scan codec with, for every query, the scan's rank of each exact top-k neighbour, the exact band and the band read from the scan's own scores, and the margin policy's depth on a 0.25-sigma grid. With an exact second stage that is enough to replay any depth policy offline; a test checks the replay against the computed fixed and margin cells. evaluate now returns an Evaluation struct carrying the traces.
Replays fixed depth, the margin policy, a ratio router (depth from noise over the scan's own band) and a hindsight oracle from per-query traces, compared at equal mean depth: headroom, share captured, worst-decile recall. Also tests per-collection routing, predicting each collection's cheapest scan codec and depth for a recall target with the curve fitted on the other corpora. Results follow when the run completes.
Codec::decode, score and score_prepared take &[u8] instead of &Encoded, so a codec can score codes in place in a contiguous buffer or a database page. Encoded stays as encode's owned output and derefs to [u8], so existing callers compile unchanged. F32Codec::score reads the stored floats in place in one fused pass, with no Vec<f32> per vector; distance::cosine is one pass instead of three. Both are bit-identical to the previous results (tested). binary-hamming stores the same sign bits as binary but binarises the query once in prepare_query and scores by XOR and popcount over 64-bit words, estimating cosine as cos(pi * hamming / d). It is the symmetric form a scan-speed claim can be made for; its error model is documented as the one codec whose score is not the metric on its decoded vector.
CodeBuffer holds a collection's codes back to back in one allocation, with ids and sources as side arrays, and scans them in place: the query is prepared once, each code is a borrowed slice, a k-sized heap keeps the best hits, and the scan can split over scoped threads with identical results. It can be written to a flat code file and read back with one read. A memory-mapped file is left out: it needs unsafe, which the workspace forbids. Store::search now scans a cached buffer per collection and reads source, ordinal and text for the top k only. The cache is dropped on this handle's writes and when another connection commits (PRAGMA data_version). load_codes builds a buffer from borrowed blobs (one copy in all); search_borrowed scans SQLite's rows in place; set_mmap_size turns on SQLite's own memory map; search_by_rows keeps the original path for measurement. Tests: buffered search equals the row scan, writes and other connections' commits are seen.
hvec bench scan times one f32 collection three ways: the storage paths (SQLite rows copied, SQLite rows borrowed with and without SQLite's memory map, the cached buffer) and the cost of building the buffer from SQLite or a flat file; every codec scanned in place at each thread count (ms/query, GB/s, recall@k against exact f32, self excluded); and a sequential-read memory-bandwidth ceiling at the same thread counts. experiments/2026-11-zero-copy-scan has run.sh and summarise.py; results follow from a quiet machine.
Ten collections, eleven scan codecs, none/fixed/margin policies, int8 and f32 second stages. Adaptive bits: mixed48-pca-w keeps 0.768 of the exact top 10 with no re-scoring against 0.747 for pq48 and 0.669 for binary, and leads at 24 and 96 bytes too; the gain is all on MiniLM, a tie on bge-small; without the PCA rotation it is no better than binary-centred. Adaptive depth beats fixed depth at equal mean depth in 522 of 526 cells, +1.5 points of recall and +3.7 of worst-decile recall, at a p90 depth 1.3-2.1x the mean. Includes a benefits-and-drawbacks table for each combination.
38,124 query traces over six scan codecs and ten collections. A hindsight oracle beats fixed re-score depth by 5.5-7 points of recall@10 at equal mean depth, but the band visible in the scan's own scores tracks the true band only at Spearman 0.52-0.70: the margin policy captures 0-6% of the headroom and a ratio router does worse than fixed depth up to 50 candidates. Per-collection routing picks the right scan size in 10/10 collections, but its predicted depth misses a 95% target by up to 9 points, so depth should be measured, not predicted. Traces are gzipped; analyse.py reads either form.
On an otherwise idle M4 Pro: the cached contiguous buffer searches 5.1x faster than the original row scan on FiQA (14.7 vs 74.7 ms) and SQLite's memory map alone gives 2.7x. Every asymmetric codec costs 200-490 ns per vector against 207 for f32, under 2% of memory bandwidth, so fewer bytes do not mean a faster scan. binary-hamming costs 10 ns per vector; with an f32 re-score of 369 candidates it reaches 95% recall@10 on FiQA about 19x faster than exact f32 on one thread. mixed48-pca-w is the slowest scan, 2.2x f32 per vector.
The matrix write-up reported +1.5 points of recall for adaptive depth at equal mean depth; that came from interpolating between fixed depths of only 20, 50 and 100, which runs below the true curve. Scored exactly from the routing traces the gain is 0.3-0.6 points (ahead in 156 of 180), with up to 4 points for the worst decile. The write-up now says so and why. README: the Faster search row makes its first claim (binary-hamming plus an f32 re-score), binary-hamming is listed, the stale note about f32 decoding is replaced, and the adaptive-depth section states the measured effect. Roadmap: phase 8 items done, and new follow-ups (codec choice at ingest, layered codes, a faster mixed scan, end-to-end hybrid timing from disk, SIMD kernels, the unsafe decision for memory-mapped files).
…riments Four experiment pages, each with its hypothesis, setup, result table and caveats, linking to the full write-up, and a post that ties them together: hybrid retrieval gets the neighbours back, adaptive bits beat PQ at equal bytes on average, adaptive depth helps only a little (with the 1.5-point figure corrected), and only symmetric popcount scoring is faster. The README animations are embedded from docs/assets.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five experiments and the tooling behind them. Every result has a write-up under
experiments/, and all but the first (published earlier) get a site page here.Experiments
experiments/2026-11-neighbour-spacing): five BEIR-format corpora (SciFact, NFCorpus, ArguAna, FiQA, CoSQA code) × bge-small and MiniLM × nine codecs. Codec noise ÷ neighbour band ranks recall@10 loss with Spearman 0.98 over 90 points (noise alone: 0.64). Content type matters only through spacing.2026-11-two-stage-rescoring): the same ratio predicts the re-score depth (ρ ≈ 0.93). At 48 bytes, 50 f32 candidates recover 0.95–0.99 of the top 10 and nearly all of the nDCG loss.2026-11-hybrid-adaptive-matrix):mixed48-pca-w(PCA + bits allocated by variance × query energy) beatspq48at equal bytes on average (0.768 vs 0.747), all of it on MiniLM. Adaptive re-score depth gains 0.3–0.6 points at equal work, up to 4 for the worst decile. Includes a benefits-and-drawbacks table for each combination.2026-11-routing-oracle): a hindsight oracle has 5.5–7 points of headroom over fixed depth, but the scan's own scores reveal almost none of it. Per collection, the noise-to-spacing curve picks the right codec size in 10/10 collections, but its predicted depth misses a 95% target by up to 9 points.2026-11-zero-copy-scan): the cached in-place buffer searches 5.1× faster than the original row scan. Every asymmetric codec costs about what f32 costs per vector;binary-hammingplus an f32 re-score reaches 95% recall@10 on FiQA about 19× faster than exact f32 on one thread.Code
Codec:prepare_query/score_prepared;decodeandscoreborrow&[u8](Encodedderefs to[u8], so existing callers are unchanged).mixed<B>[-pca][-w](adaptive bits) andbinary-hamming(symmetric popcount).hvec-store:CodeBuffer, a contiguous per-collection buffer scanned in place, optionally over threads.Store::searchuses a cached buffer and reads text for the hits only. The cache is invalidated on writes and on other connections' commits.bench recallreportsneighbour_band,boundary_gapandrank_noise.bench rescore(fixed and margin policies, worst-decile recall, depth for target recall,--traceper-query output) andbench scan(storage paths, codecs, threads, bandwidth ceiling).Docs
main.Corrections made along the way
For review
unsafe_codeis forbidden workspace-wide. Reading a flat file is already fast (110 MB in 9 ms). It's listed on the roadmap as a decision.Testing
cargo test --workspacepasses, andcargo clippy --all-targetsandcargo fmt --checkare clean.bench scanend to end.