perf(vector): reuse prepared f32 PQ8 codebooks during partition search - #9230
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-pq-prepared-codebooks
branch
from
September 15, 2026 05:56
f0d0307 to
21591b7
Compare
xiaguanglei
force-pushed
the
index/perf-pq-prepared-codebooks
branch
from
September 15, 2026 06:14
21591b7 to
2b96fbe
Compare
xiaguanglei
force-pushed
the
index/perf-pq-prepared-codebooks
branch
from
September 16, 2026 02:03
2b96fbe to
a8ba950
Compare
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.
Problem
An IVF_PQ query builds one query-to-codeword distance table for every probed
partition. The query residual changes per partition, but the PQ codebook does
not.
For an f32 PQ8 index, the existing table builders pass the original AoS
codebook to the generic batch distance API. That API prepares/transposes the
same 256 centroids again for every sub-vector and every probed partition. With
nprobes=32, a DIM=128 query therefore repeatedly prepares the same 128 KiBcodebook layout 32 times before bulk PQ-code scoring can begin. Once the Lance
index cache is warm, this repeated CPU setup is a large part of end-to-end
latency.
The cost grows with the number of probes and IVF partitions. This explains the
measured shape of the baseline: one-probe queries have no reusable work, while
high-probe searches spend a substantial fraction of their time preparing an
unchanged codebook.
Dot-product PQ had the same horizontal-reduction shape but no reusable prepared
target implementation. The current-format V3 partition loader also rebuilt
partition storage from metadata alone, so an index-level prepared L2 codebook
could not reach the partition search path.
Solution
DotPreparedSoA target layout matching the existingL2Prepareddesign; LLVM can vectorize across PQ centroids on AArch64 andx86_64
ProductQuantizerand share its preparedtargets with every loaded PQ partition through
ArcOnceLock, sopartitions share one allocation and concurrent first use is safe
only one partition
and end-to-end testing did not show enough benefit to justify changing its
setup path
The per-partition distance table is still computed from that partition's query
residual. The optimization removes only redundant codebook preparation; it
does not reuse a distance table across different residuals or change distance
semantics.
Scope
The fast path covers current-format f32 PQ8 storage:
PQ4, f16, f64, legacy partition loading, serialized metadata, and public APIs
remain on their existing paths.
End-to-end benchmark
Method
Measured on SIFT-1M, the industry-standard ANN benchmark
(corpus-texmex): 1M x 128-d f32 base, the
official 10k-query set, and the official exact top-100 ground truth. The exact
input files:
sift_base.fvecs21f66e2975057b5728ba56de1c825bac4f4d89d596609ae985741c6242631816sift_query.fvecsf7fc9be140accdfd64116c2fa2365ecdb69b8f084970c6b0532db5ff79ac8fdcsift_groundtruth.ivecs2b71de0a8d5a83e6a84eec3e23fb8b611d8801dd9b3a6cd62f070ab65ea65f4fEnvironment and controls:
release-with-debugprofile
opened read-only by both saved binaries
BCCBBCCBA/B protocol, warm cache,concurrency=1,median-of-medians aggregation
this implementation PR
IVF_PQ (SIFT-1M, nlist=256, PQ8, L2)
Index
sift1m-ivf256-pq16-l2.lance(IVF_PQ, 8-bit PQ), interleavedBCCBBCCBA/B,
repetitions=6, warm cache,concurrency=1:nprobesResult hashes and Recall@10 matched in every cell. The win peaks at
nprobes=32(-16.4%) and falls off at higher probe counts where partition I/Obecomes the larger share of the query — the shape the setup-cost model predicts:
one-probe queries have no reusable work (-2.3%, within noise), and each
additional probe amortizes the once-only codebook preparation.
IVF_HNSW_PQ (SIFT-1M, nlist=256, PQ8, L2)
Secondary check on
sift1m-ivf256-hnswpq-l2.lance(IVF_HNSW_PQ, same PQ8 fastpath):
nprobesHNSW_PQ shows a smaller, positive win because HNSW graph traversal adds fixed
work outside PQ table construction.
Prewarm path (
prewarm_indexAPI)The query warm-up path above exercises
load_partitionduring queryexecution. A separate API,
prewarm_index, eagerly materializes partitionstorage into the index cache. Before this PR, that path called
try_from_batch_with_remapper, which rebuilt every prewarmed partition frommetadata alone, so each partition owned an empty prepared-target cache and
re-transposed the codebook on first use — i.e. the prewarm path silently
missed the prepared-codebook optimization entirely.
This PR fixes
materialize_partition_for_prewarmto pass the retainedindex-level quantizer through (
try_from_batch_with_quantizer), so prewarmedpartitions share the same lazily initialized prepared targets as query-loaded
partitions. A regression test
(
test_prewarm_materialization_shares_prepared_targets) locks this in.IVF_PQprewarm path, SIFT-1M, nlist=256, PQ8, L2 —--cache prewarm(partitions loaded via
prewarm_index), interleavedBCCBBCCB,repetitions=6,warmup_rounds=1:nprobesResult hashes and Recall@10 matched in every cell. The prewarm-path win
(peaking ~-20%) is larger than the query-path win (-16.4%) because the
un-fixed prewarm path was fully on the legacy rebuild path, whereas the
query path was already partially optimized.
Memory and compatibility
Prepared targets are lazy and only the active metric can allocate one set. A
DIM=128/PQ8 index adds one shared 128 KiB f32 codebook layout after the first
multi-probe use, regardless of how many partitions are loaded.
DeepSizeOfuses
Arc-aware accounting so shared targets are not charged once perpartition.
There are no file-format, protobuf, serialized metadata, dependency, or public
API changes. Legacy readers and writers are untouched.
Correctness coverage
DotPreparedwith the generic Dot implementation across empty, odd,short, PQ-like, and large shapes
L2 and Dot
storage; Flat, SQ, and RQ keep their existing ownership and memory behavior
initializing the lazy targets
Validation
Earlier full-suite validation of the same PQ8 implementation also passed
cargo test -p lance-index,cargo test -p lance,cargo clippy --all --tests --benches -- -D warnings, andcargo check --workspace --tests --benches.