Skip to content

Add Int8Rescored: int8 graph walk with exact float re-ranking - #31

Merged
matengo merged 2 commits into
masterfrom
quantization-rescoring
Sep 16, 2026
Merged

matengo merged 2 commits into
masterfrom
quantization-rescoring

Conversation

@matengo

@matengo matengo commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

Adds VectorQuantization.Int8Rescored: the HNSW graph is built and walked on int8 codes exactly as in Int8, but the original floats are kept in the file and the efSearch candidates are re-ranked against them. Returned scores and top-k are the float ones; GetByGuid returns the original vector.

This is the "float refiner" the Cohere 1M benchmark write-up identified as the remaining lever, and the equivalent of zvec's --is-using-refiner.

Format

No version bump, no new QuantizationMode. A rescored file is an int8 file (QuantizationMode = 2) that additionally carries section 1 (Vectors) as an optional, non-Required section in slot 8, with HasOptionalSections set. Readers that predate this change open it as plain int8. QvecDatabase.Quantization reports Int8Rescored when the section is present; reopening with the wrong mode throws in both directions. Header validation checks the float section's shape when present. Float and plain int8 files are unchanged (existing tests pass untouched).

Implementation

  • _vectorSectionOffset split into _floatVectorSectionOffset / _codesSectionOffset; _rescore flag.
  • WriteVectorToDisk writes codes + params and, when rescored, the floats too — so AddEntry, AddEntries, UpdateVector and Vacuum all keep both sections in step.
  • ReadVectorInto returns the exact floats when available.
  • FinalScore / RescoreCandidates: exact float scoring after SearchLayerNearest / SearchLayerFiltered, and in every exhaustive path. No-ops for float and int8 files.
  • Benchmark: --quantization int8rescored.

Tests

RescoredQuantizationTests (16): layout, Open reports the mode, mismatch both ways, exact GetByGuid, scores equal float brute force (1e-4) in all three metrics, recall >= 97 % on tight cosine clusters where int8 alone measured 89.8 %, filtered search, Update/Delete/Vacuum, Grow, parallel AddEntries. Fast suite + Slow suite green, 0 warnings.

Measurements (Cohere 1M, 768-d cosine, k=100, M=15, 12 threads)

mode build file ef recall@100 QPS 12 thr QPS 1 thr
float 419 s 3,376 MiB 180 94.8 % 3,764 565
int8 181 s 1,194 MiB 180 93.2 % 6,841 839
int8 rescored 170 s 4,124 MiB 180 94.7 % 6,778 630
float 320 97.4 % 2,207 351
int8 320 95.3 % 4,714 502
int8 rescored 320 97.4 % 3,884 421

Float recall at int8 throughput, paid for in storage (~1.25x float). Honest caveat, in the docs: at efSearch == k re-ranking cannot add candidates, so the ef 100 row stays at int8 recall (89.2 %). Float/int8 indexes re-queried the same day were within 6 % of the table.

siftsmall: rescored 99.1 / 99.8 % recall@10 at ef 20 / 80 vs float 98.6 / 99.5 and int8 98.3 / 99.0.

Docs

docs/design-quantization-rescoring.md (new), docs/design-quantization-int8.md, docs/design-format-v4.md, benchmarks/README.md, README.md.

matengo and others added 2 commits September 16, 2026 21:08
An int8 file may now also carry the original floats as an optional
(non-Required) Vectors section in slot 8, flagged with HasOptionalSections.
QuantizationMode stays 2, so a reader that predates this change opens the
file as plain int8. The API reports VectorQuantization.Int8Rescored when
the section is present.

The graph is built and walked on the int8 codes exactly as before; the ef
candidates from the bottom layer are then re-ranked against the floats, so
returned scores and top-k are the float ones. Exhaustive paths (SearchSimple,
SearchSimpleParallel, the filtered fallback, Rerank) score on floats directly,
and GetByGuid returns the original vector. Grow, Update, Delete and Vacuum
keep both sections in step because every write goes through WriteVectorToDisk.

Benchmark: --quantization int8rescored.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Cohere 1M (12 threads, k=100, M=15): rescored reaches float recall@100 at
efSearch 180 and 320 (94.7 / 97.4 %) at 6,778 / 3,884 QPS, against float
3,764 / 2,207 and int8 6,841 / 4,714. Build 170 s vs 419 s float; file
4,124 MiB. No recall gain when efSearch == k, stated as such.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@matengo
matengo merged commit 6d1ea88 into master Sep 16, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant