Skip to content

Reuse visited lists across batched Python queries - #676

Open
ShreyPandit wants to merge 1 commit into
nmslib:developfrom
ShreyPandit:perf-visited-list-leases
Open

Reuse visited lists across batched Python queries#676
ShreyPandit wants to merge 1 commit into
nmslib:developfrom
ShreyPandit:perf-visited-list-leases

Conversation

@ShreyPandit

Copy link
Copy Markdown

Summary

This PR reduces visited-list pool locking in Python HNSW batch queries by leasing one VisitedList per active
ParallelFor worker instead of acquiring and returning one for every query row.

For a batch with Q rows and W active workers, the number of mutex-protected pool operations falls from roughly
2Q to at most 2 min(Q, W). Each row still receives a fresh visited generation, and the HNSW traversal and
distance calculations are unchanged.

Implementation

The patch adds a non-copyable RAII VisitedListLease. Pool acquisition performs the first existing generation
reset, next() performs one reset before every subsequent row, and destruction returns the exact list to the
originating pool on both normal and exceptional exit.

The level-zero search implementation is factored so it can consume caller-owned visited scratch. The existing C++
searchKnnNoExceptions path retains its normal acquire/release behavior. Only Python Index.knn_query uses the new
entry point, with one lazily acquired lease per worker. Both normalized and non-normalized query paths use the same
lease protocol.

The patch does not change the graph, distance arithmetic, result ordering, index format, construction path,
BFIndex, or public Python signatures. The number of simultaneously live visited lists remains bounded by the
number of active workers.

Performance

The A/B benchmark used 30,000 deterministic standard-normal float32 base vectors, 2,000 held-out queries,
dimension 64, M=24, ef_construction=120, ef_search=48, k=10, and 16 query workers. For each of three build
seeds, baseline and candidate loaded the same baseline-produced serialized index. Each treatment ran in a fresh
process with two warmups and 31 retained full-batch timings.

Build seed Baseline QPS Patched QPS Gain Recall baseline/patched Output parity
7101 246,982 259,797 +5.19% 0.7785 / 0.7785 byte-identical
7102 247,714 259,439 +4.73% 0.7810 / 0.7810 byte-identical
7103 247,995 259,526 +4.65% 0.7810 / 0.7810 byte-identical

The median paired gain is +4.73%. The mean paired ratio is 1.0486x, with a two-sided 95% paired-t interval of
[1.0414x, 1.0558x] over the three build seeds. Peak RSS ratios were 0.9960, 0.9998, and 0.9966, so no memory
increase was observed.

A separate two-round matrix measured one- and two-row calls within 0.6% of baseline and found improvements from
eight rows upward in the tested configurations. The performance claim is limited to batched Python queries and the
tested environment; it is not a universal throughput claim.

Correctness and tests

  • Release C++ build and CTest: 16/16 passed.
  • Exception-disabled C++ build and CTest: 16/16 passed.
  • test_updates and test_updates update: passed in both builds.
  • Official Python unittest discovery: 17/17 passed, including the three new query-workspace cases.
  • Python BFIndex test: passed; all five Python examples passed.
  • AddressSanitizer full C++ suite: 16/16 passed with leak detection enabled.
  • ASan+UBSan: 15/16 passed; the remaining BruteforceSearch misaligned-load finding reproduces unchanged on the
    unmodified base. The new focused test passes under the combined sanitizers.

The added tests cover generation wrap, exception return, l2/ip/cosine, normalized queries, filters, deletions,
repeated use, concurrent batches, and the serial/threaded scheduling boundary.

Compatibility

The implementation is C++11-compatible and preserves the current exception-enabled and no-exception APIs. Index
serialization is unchanged. This PR targets develop at
df5814a61d6ac129fff6e5c27aa3ab99474aefac.

AI assistance

OpenAI Codex gpt-5.6-sol with xhigh reasoning produced and screened the initial candidate. OpenAI Codex gpt-5.6-sol with max reasoning was used to port, review, and validate the final change.

I reviewed and understand every submitted change.

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