Repository navigation
fix: validate advanced encoder preconditions and bound HNSW prefetch look-ahead - #197
Merged
Merged
Conversation
code_factor_terms divides by dim - 1, so the advanced encoders returned a NaN error factor for dim == 1 (the stable API and the indexes already require more). rabitq_full_impl accepted a code element type too narrow for total_bits, truncating the sign bit for uint8_t at nine bits. SplitSingleQuery checked bit widths but not that padded_dim is a multiple of 32, although the AVX2 and AVX-512 transposition kernels load 32-byte blocks. All three now throw std::invalid_argument. The non-template helpers in utils/space.hpp gain internal linkage, as the implementation-linkage rule for headers reached from ISA-flagged sources requires.
…ol entries search_layer prefetched the first two link slots and the base-layer search the first four regardless of the link count, and the base-layer search prefetched level0_links(search_pool.next_id()) after every insert even when the pool had no next entry, in which case next_id() reads the spare slot that a full insert fills with the displaced entry, possibly carrying the checked bit. Those reads are stale or unwritten IDs, and the resulting pointer arithmetic leaves the allocation. Bound the look-ahead by the link count and guard the pool prefetch with has_next(). SIFT1M (1M x 128, HNSW M=16, ef_construction=200, 4 bits), Xeon Gold 6418H, GCC 14 -O3 portable build, same saved index for both builds: single-thread QPS medians 9568/6432/3910 before and 9487/6416/3898 after at ef 64/128/256 (within the run-to-run spread), 16-thread 84174/50656 before and 84168/50278 after at ef 128/256; recall@10 identical (0.9007/0.9179/0.9230). Construction with 32 threads: 18.5 s and 18.1 s before, 18.5 s and 18.2 s after.
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.
Third follow-up to the audit (#195, #196): the core precondition items and the HNSW prefetch address derivation. Two commits.
fix(core): validate advanced encoder preconditionscode_factor_termsdivides bydim - 1;quantize_full_single/quantize_split_singlewithdim == 1returned a NaN error factor. The stable v1 API and the indexes already require more; the advanced encoders now throwstd::invalid_argument.rabitq_full_impl<T, TP>accepted a code element type too narrow fortotal_bits:uint8_twith nine bits truncated the sign bit (static_cast<TP>(1 << 8)is 0). It now requirestotal_bitsin[1, 9]and<= numeric_limits<TP>::digits.SplitSingleQueryvalidated bit widths but notpadded_dim % 32 == 0; the AVX2/AVX-512 query transposition kernels load whole 32-byte blocks. Rejected now; the indexes always pass multiples of 32.utils/space.hppwere plaininlinealthough the header is reached from the ISA-flagged HNSW search sources; they arestatic inlinenow per the implementation-linkage rule.check-isa-symbols.shpasses at-O3and-O1.quantizer.md; three regression tests.fix(hnsw): derive prefetch addresses only from live link slots and pool entriessearch_layerprefetched link slots 0 and 1 andsearch_base_layer_directslots 1–4 regardless of the link count, and the base-layer search prefetchedlevel0_links(search_pool.next_id())after every insert even when the pool had no next entry — thennext_id()reads the spare slot a full insert fills with the displaced entry, possibly with the checked bit set, soid * record_byteslands outside the allocation. The prefetch itself cannot fault, but the index reads are stale or unwritten (uninitialized under parallel construction) and the pointer arithmetic is UB. The look-ahead is now bounded by the link count and the pool prefetch guarded byhas_next().Benchmark (prefetch is a hot-path change)
SIFT1M (1,000,000 × 128, 10,000 queries,
k=10), HNSWM=16,ef_construction=200, 4 bits, L2; Intel Xeon Gold 6418H (96 cores), GCC 14.3-O3portable build (native tuning OFF, runtime dispatch → AVX-512); one index built by the baseline binary and loaded by both; runs alternated base/new/base/new, 3 rounds each, pinned withtaskset. QPS medians (min–max):Construction (32 threads, alternated): baseline 18.5 s, 18.1 s; this PR 18.5 s, 18.2 s. All deltas are inside the run-to-run spread; results and recall are identical because the change only affects prefetch hints.
Verification
check-format.sh,git diff --check,mkdocs build --strict.ctest --no-tests=error440/440 (three new tests); Clang 23 ASan/UBSan (withfloat-cast-overflow,float-divide-by-zero): 440/440, no diagnostics.check-isa-symbols.shat-O3and-O1; installed-consumer build and tests against a fresh install prefix.api.cpp,ivf.cpp,qg.cppand thespace_*SIMD TUs.pytest tests/python837 passed, 2 skipped.Not verified: NEON (no hardware); MSVC.