Repository navigation
refactor: share OpenMP, core, binding and HNSW construction helpers - #187
Merged
Merged
Conversation
Add omp_for_dynamic for the dynamically scheduled OpenMP loops in IVF, the QG builder and PiPNN, which each repeated the ParallelException capture-and-rethrow scaffolding. Use ParallelException directly in the clustering assigners and the OpenMP branch of parallel_for instead of per-worker exception vectors; after the first failure the remaining iterations are skipped, matching the std::thread branch. Share the clamped OpenMP team size used by compute_centroid, exact_nn and QuantizedGraph batch search.
The one-bit encoder and xy code_factors computed the direct-code rescale with the same expression, and the v1 API and HNSW reconstruction each unpacked the full-64 / final-32 sign-bit layout by hand. Keep each in one helper in rabitq_impl.hpp. Results are bitwise identical.
Derive IvfIndex and SymqgIndex build state from the native index, remove the unused rotator_from_string, share NumPy rank conversion and the prefilled search outputs, drop C++ default arguments that duplicated the py::arg defaults, and let pybind11 copy k-means results. Python signatures, defaults, docstrings and error messages are unchanged.
Raw construction and quantized add repeated the upper-layer greedy descent and the ef_construction layer search, differing only in the distance function and a construction-data prefetch. Implement both once as private member templates. Single-threaded builds produce byte-identical index files, and the search kernels are unchanged.
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.
Summary
Remove duplicated logic in four areas. All changes are internal: no public C++ API, Python API, saved-index format or numerical result changes. One commit per area:
index_detail::omp_for_dynamic. It replaces 8 copies of the dynamically scheduledParallelExceptioncapture/rethrow loop: 5 in the QG builder, 2 in IVF, 1 in PiPNN.ParallelExceptionin the 4 clustering parallel regions and in the OpenMP branch ofparallel_for, instead of per-workerexception_ptrvectors.std::threadbranch.detail::openmp_team_size, shared bycompute_centroid,exact_nnandQuantizedGraphbatch search.code_factors.IvfIndex/SymqgIndexbuild state from the native index; this also removes a try/catch that only resynchronized a flag.rotator_from_string.py::argdefaults; only thepy::argdefaults ever took effect.memcpy.addshare one greedy upper-layer descent and oneef_constructionlayer search. They are private member templates over the distance function and the construction-data prefetch.Left out on purpose:
core/estimator.hppand the query corrections incore/query.hpp. Both are inlined into the HNSW search kernels and changed their code generation. Pinned measurements could not rule out a 1–4% search QPS loss.get_bin_est_direct/get_full_est_directhelpers, for the same reason.With these left out, the HNSW search kernels compile to identical machine code (generic, AVX2, AVX-512 core and AVX-512 VPOPCNTDQ).
Testing
Linux x86-64 host: Xeon Gold 6418H, 2 sockets; GCC 14.3;
RABITQ_ENABLE_NATIVE_OPTIMIZATION=OFF.--no-tests=error):RABITQ_USE_OPENMP=OFF: 167 passed.OMP_PROC_BIND=true, theThreadCounttests (11), including the boundparallel_forpath, pass in both builds.check-format.sh, fullcheck-tidy.sh build-tidy(clang-tidy 23, including bindings) andgit diff --checkpass.pytest tests/python: 794 passed, 2 skipped (wheel-only tests).encode,convert_codesandcompute;constructplus 5kadd) write byte-identical index files for L2/IP with 4 bits and L2 with 1 and 9 bits.Benchmarks
The process was pinned to one idle core and its NUMA node (
numactl --physcpubind=20 --membind=0), with baseline and candidate runs alternated. Shared host: load average 3–7 during the runs; CPU frequency was not locked.HNSW, synthetic data. 50k×128 vectors in 64 Gaussian clusters, M=16, efConstruction=200, single thread. Search uses 1000 queries, k=10, with recall measured against exact top-10.
The search code is byte-identical, so its spread (individual pair ratios between 0.95 and 1.13) shows the host noise. Construction and add stay within it. No speedup is claimed.
Stable core benchmark (
rabitq_core_benchmark 128 {4,9} 4096 64 7, 6 alternating runs): the median per-run ratio is between 0.944 and 1.038 forencode_packed,prepare_query,compute_packedandconvert_to_unpacked. Checksums are identical.Not run locally: NEON, Windows and macOS builds, clang parity for this exact combination (the core changes were checked with clang separately), and real-dataset benchmarks.