perf(index): cut fixed and per-partition overhead of IVF search - #9190
Open
xiaguanglei wants to merge 1 commit into
Open
xiaguanglei wants to merge 1 commit into
xiaguanglei wants to merge 1 commit into
Conversation
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 14, 2026 07:11
ed799ad to
26e6491
Compare
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 14, 2026 07:30
26e6491 to
d9ef2c5
Compare
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 14, 2026 08:08
d9ef2c5 to
46d8405
Compare
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 23, 2026 03:39
46d8405 to
7016446
Compare
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 23, 2026 05:08
7016446 to
a0976c1
Compare
Reuse per-query RaBitQ FastScan state and memoize partition-cache work so the fixed cost of an IVF query drops by ~15-23% (x86 5M synth, recall-neutral, result hashes unchanged): - Reuse the raw-query context across probed partitions: memoize the FastScan LUTs (normal_lut / accurate_lut) on RabitRawQueryContext, since the raw-query dist_table is centroid-independent and was being re-quantized per partition. - Precompute namespace-tagged BLAKE3 digests for partition cache keys, skipping a digest computation per probe; resolve() rejects cross-namespace digests. - Memoize IVF partition reconstruction in its own capacity-accounted cache entry (CachedReconstructedIndex), keyed by object store + fragment reuse index identity, so identical reopens reuse the built index without growing the already-inserted IvfIndexState past the cache's eviction weight. - Share Query via Arc across probed partitions (PreparedPartitionSearch.query) and add a parallel per-partition dist_q_c field, stopping a full Query clone per partition. IvfSubIndex::query_params keeps a default impl (no forced API break); FlatIndex / HNSW override it to avoid the clone. - Harden partition_cache_key with a debug_assert! bounds guard, and close the internal RabitRawQueryContext type with #[doc(hidden)] #[non_exhaustive] + new() (beta-period internal-plumbing closure, not a public API change). No on-disk format change; partition_cache_keys is runtime-only and excluded from the CacheCodec wire format. The reconstructed index is a separate in-memory cache entry (no codec), so it is never serialized.
xiaguanglei
force-pushed
the
index/perf-index-ivf-search-overhead
branch
from
September 23, 2026 10:10
a0976c1 to
e5ce309
Compare
Contributor
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The rebased IVF patch preserves the previously reviewed cache accounting and query behavior. Focused RaBitQ, reconstructed-index cache, and KNN ordering regressions pass on the new base.
Please mark this PR with the breaking-change label.
This branch has not been deployed
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.
perf(index): cut fixed and per-partition overhead of IVF search
Summary
IVF queries pay fixed per-query and per-partition overhead — re-quantizing the
RaBitQ FastScan LUT, hashing partition cache keys, and rebuilding the partition
reconstruction — on every probe. This PR reuses per-query RaBitQ state and
memoizes partition-cache work, cutting query latency by up to −18% on SIFT-1M
with no recall change and no on-disk format change.
Why this helps
A probe-heavy IVF query touches
nprobespartitions, so any per-partition orper-query fixed cost is paid
nprobestimes while contributing nothing to theactual scan. This PR removes three such costs by doing the work once and
reusing it:
re-quantized once per partition. Memoizing it per query turns
nprobesredundant quantizations into one.
namespace-tagged digests once at open removes the hash from the per-probe
path.
Query(including itskeyarray) was cloned per partition.Sharing it via
Arc— with only the scalardist_q_cdiffering perpartition — removes
nprobesarray clones.Each reuse removes an
O(1)cost multiplied bynprobes, which accumulatesinto the measured up-to-−18% latency reduction on SIFT-1M.
Changes
dist_tableis centroid-independent, so its FastScan LUT was beingre-quantized once per partition. The LUTs (
normal_lut/accurate_lut) arenow memoized on
RabitRawQueryContextand built lazily on first use.rust/lance-index/src/vector/storage.rs:314-384skipping a digest computation per probe.
resolve()rejects digests from aforeign namespace and re-hashes, so cross-namespace reuse stays correct.
rust/lance-core/src/cache/key.rs:136-165·rust/lance/src/index/vector/ivf/v2.rs:959-1055entry, keyed by object store + fragment reuse index identity, so identical
reopens reuse the built index instead of rebuilding it. The live index
(readers, object store, remapper, RaBitQ scratch pools) is inserted as a
dedicated
CachedReconstructedIndexentry whose weight is computed at inserttime — rather than being mutated into the already-inserted
IvfIndexState,which would silently grow past the index cache's memory limit without
increasing its eviction weight.
rust/lance/src/index/vector/ivf/v2.rs:109-150,2813-2901QueryviaArcacross probed partitions and add a parallelper-partition
dist_q_cfield onPreparedPartitionSearch, stopping a fullQueryclone per partition.IvfSubIndex::query_paramskeeps its defaultimpl (no forced API break);
FlatIndex/HNSWoverride it to avoid theclone.
rust/lance/src/index/vector/ivf/v2.rs:468-479·rust/lance-index/src/vector/v3/subindex.rs:51·rust/lance-index/src/vector/flat/index.rs:82-103·rust/lance-index/src/vector/hnsw/builder.rs:1423-1446partition_cache_keywith adebug_assert!bounds guard for theprecomputed-key array.
rust/lance/src/index/vector/ivf/v2.rs:1054-1060API note
RabitRawQueryContextis apubtype (inpub mod vector::storage, returnedby
RabitQuantizationStorage::prepare_raw_query_context) that shipped inv12.0.0-beta.18. This PR adds privatenormal_lut/accurate_lutmemofields and marks it
#[doc(hidden)] #[non_exhaustive]with construction goingthrough
RabitRawQueryContext::new.This is an internal-plumbing-type closure, not a public API change: it is a
12.0.0-betaline (no stable API contract), the sixpubfields are all RaBitQinternals no downstream crate can meaningfully construct, and closing the type
now is cheaper than after a stable release.
Performance
IVF_RQ on SIFT-1M (x86 Skylake, nlist=256, RQ5, warm, AVX-512, median-of-medians):
Result hashes are bit-identical and recall is unchanged across the matrix. The
win concentrates at low nprobes and decays as the partition scan begins to
dominate, consistent with removing fixed per-query/per-partition costs.
The shared IVF paths (coarse select + reconstruct/cache) carry the win beyond
RQ (SIFT-1M, nlist=256, same protocol — re-tested on the same dataset):
The gain holds both cold and warm, and index build is unaffected (the
wall-clock
preparedelta sits within the measurement floor).Methodology
All end-to-end measurements are interleaved A/B on the same host, driven by
identical dataset/query/ground-truth paths, so the two binaries experience the
same conditions within each cell. Everything below is reproducible from public
data and a small in-tree-toolchain harness.
ANN benchmark (
sift.tar.gz, 168 MB). 1M × 128-df32base(
sift_base.fvecs), the official 10k-query set (sift_query.fvecs), and theofficial exact top-100 ground truth (
sift_groundtruth.ivecs) — no syntheticdata, no self-computed ground truth.
nlist=256, RaBitQrq-bits=5, fast rotation,metric=l2.release-with-debug.BCCBBCCBinterleaving (4 baseline + 4 candidate runs pernprobes); each run = 6 repetitions × 200 queries after 3 warm-up rounds,
concurrency=1. Reported latency is the median of the four run p50s pervariant. Recall and result hashes are checked for identity against baseline on
every cell (recall-neutral, bit-identical results).
577091e53vsed799ad7a(branchindex/perf-index-ivf-search-overhead).The harness converts the fvecs/ivecs to Lance, builds the index, and runs the
search under the above protocol.
Behavior
partition_cache_keysis runtime-only and excludedfrom the
CacheCodecwire format; the reconstructed index is a separatein-memory cache entry (no codec), so it is never serialized.
sort_to_indicespath — no behavioral change.
Test plan
cargo fmt --allcargo clippy --all --tests --benches -- -D warningscargo test -p lance-core -p lance-index -p lance(0 failed)test_reconstructed_index_cache_key_distinguishes_inputs(reconstructed-index cache-key reuse rules)
test_vector_cache_uses_current_object_storeasserts the reconstructedindex is held as a capacity-accounted entry (
cache weight >= its deep size)