Skip to content

feat: add high-performance HNSW RaBitQ index - #1798

Open
CLiqing wants to merge 1 commit into
zilliztech:mainfrom
CLiqing:codex/pr-hnsw-rabitq
Open

feat: add high-performance HNSW RaBitQ index#1798
CLiqing wants to merge 1 commit into
zilliztech:mainfrom
CLiqing:codex/pr-hnsw-rabitq

Conversation

@CLiqing

@CLiqing CLiqing commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

issue: #1747

Summary

  • introduce HNSW_RABITQ with 1-8 bit dense database codes and FP32 queries (qb=0), supporting L2, inner product, and cosine similarity
  • build the HNSW graph with exact FP32 distances, then attach RandomRotationMatrix -> IndexRaBitQ storage for search
  • implement cosine semantics without modifying the input dataset: retain per-vector inverse norms and apply query/database norm correction during distance computation
  • add optimized scalar, AVX2, AVX-512, and Sapphire Rapids single/batch-4 multi-bit distance kernels

Compatibility and scope

  • HNSW_RABITQ V1 is immutable after its initial build and supports L2/IP/COSINE
  • database codes use the bundled-Faiss dense RaBitQ layout; query quantization is intentionally not exposed in V1, and nonzero rbq_bits_query is rejected explicitly
  • the HNSW wrapper exposes RBQ1-RBQ8; the bundled generic IndexRaBitQ retains its existing 1-9 bit range
  • cosine coverage includes search, range search, iterator, distance-by-ID, FP32 refinement, and serialization/deserialization; Cohere and OpenAI checks agree with pre-normalized IP within 0.0004 recall
  • this implementation is not index-format or runtime-behavior compatible with Faiss #5526: it uses an exact FP32 graph followed by random rotation and full multi-bit L2/IP scoring, whereas #5526 builds directly with RaBitQ storage and uses staged L2 search
  • the two implementations can coexist in source: the Knowhere-specific type is under faiss::cppcontrib::knowhere and uses IHRK/IHRC, while IHNr remains reserved for #5526; shared bundled-Faiss quantizer/SIMD changes will still require normal reconciliation during future Faiss updates

@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: CLiqing

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@mergify

mergify Bot commented Aug 28, 2026

Copy link
Copy Markdown

@CLiqing 🔍 Important: PR Classification Needed!

For efficient project management and a seamless review process, it's essential to classify your PR correctly. Here's how:

  1. If you're fixing a bug, label it as kind/bug.
  2. For small tweaks (less than 20 lines without altering any functionality), please use kind/improvement.
  3. Significant changes that don't modify existing functionalities should be tagged as kind/enhancement.
  4. Adjusting APIs or changing functionality? Go with kind/feature.

For any PR outside the kind/improvement category, ensure you link to the associated issue using the format: “issue: #”.

Thanks for your efforts and contribution to the community!.

Comment thread thirdparty/faiss/tests/test_rabitq_simd.cpp
Comment thread thirdparty/faiss/benchs/bench_rabitq_simd.cpp
Comment thread src/index/hnsw/impl/IndexBruteForceWrapper.cc Outdated
Comment thread src/index/hnsw/impl/IndexBruteForceWrapper.cc Outdated
Comment thread thirdparty/faiss/faiss/impl/index_read.cpp Outdated
Comment thread thirdparty/faiss/faiss/impl/RaBitQuantizer.cpp
Comment thread thirdparty/faiss/faiss/utils/simd_impl/rabitq_avx2.cpp
Comment thread thirdparty/faiss/faiss/utils/simd_impl/rabitq_avx512.cpp
Comment thread thirdparty/faiss/faiss/CMakeLists.txt
@alexanderguzhva

Copy link
Copy Markdown
Collaborator

/hold

@alexanderguzhva

Copy link
Copy Markdown
Collaborator

@CLiqing just in case, is this PR compatible with facebookresearch/faiss#5526 ?

@CLiqing
CLiqing force-pushed the codex/pr-hnsw-rabitq branch 2 times, most recently from abfc167 to a9c7389 Compare September 1, 2026 03:32
@CLiqing

CLiqing commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

@alexanderguzhva It is not index-format or runtime-behavior compatible with #5526 because the construction and search contracts differ.

This PR builds the graph with exact FP32 distances and then attaches RandomRotationMatrix -> IndexRaBitQ storage with full multi-bit L2/IP scoring; #5526 builds directly with RaBitQ storage and uses staged L2 search.

The implementations can coexist in source: the Knowhere type is under faiss::cppcontrib::knowhere and uses IHRK, while IHNr remains reserved for #5526. Shared RaBitQuantizer/SIMD changes would still need normal reconciliation during a future bundled-Faiss update.

Comment thread src/index/hnsw/faiss_hnsw.cc
Comment thread thirdparty/faiss/faiss/IndexRaBitQ.cpp
Comment thread src/index/hnsw/faiss_hnsw_config.h
Comment thread thirdparty/faiss/faiss/IndexRaBitQ.h
Comment thread src/index/hnsw/faiss_hnsw.cc Outdated
Comment thread thirdparty/faiss/perf_tests/CMakeLists.txt Outdated
Comment thread src/index/hnsw/faiss_hnsw_config.h Outdated
@CLiqing
CLiqing force-pushed the codex/pr-hnsw-rabitq branch from a9c7389 to 89f7b5d Compare September 1, 2026 14:20
@mergify mergify Bot removed the ci-passed label Sep 1, 2026
// Store one complete scalar code per dimension in a dense n-bit stream.
// This combines the sign bit and extra bits at build time while retaining
// the exact nbits/dimension budget. For nbits=8 this is one byte/dimension.
bool dense_layout = false;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RaBitQuantizer::dense_layout (RaBitQuantizer.h:53) changes the on-disk code layout but is not part of the standard wire format: index_write.cpp:1027 tags every multi-bit IndexRaBitQ as Ixrr and index_read.cpp:2856 default-constructs the index, so a faiss::write_index/read_index round-trip restores dense_layout=false and reinterprets dense codes with packed offsets. read_RaBitQuantizer does not recompute code_size, and for any d % 8 == 0 (64, 128, 768, 1536) the dense and packed sizes are byte-identical, so nothing rejects the file — every distance is silently wrong. IndexRaBitQFastScan::IndexRaBitQFastScan(const IndexRaBitQ&, int) (IndexRaBitQFastScan.cpp:106) copies the quantizer and then reads sign bits at orig_code[j/8] >> (j%8), which is garbage for dense codes, also without a guard.

@CLiqing CLiqing Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Dense multi-bit IndexRaBitQ now uses the distinct Ixrd tag; the standard reader restores dense_layout, recomputes and validates code_size, and validates the code buffer length. Converting a dense index to IndexRaBitQFastScan now throws explicitly. Cross-reader round-trip tests cover the standard and cppcontrib readers.

REQUIRE(index.Build(train, json) == knowhere::Status::success);
REQUIRE(index.Count() == kNb);

const auto before = index.Search(query, json, nullptr);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every assertion in test_hnsw_rabitq.cc is self-referential: CheckValidKnnResult (:65) only checks id range and isfinite, CheckKnnOrder (:78) only checks monotonicity, the serialization round-trip (:274) compares the index against itself, CalcDistByIDs (:253) compares two paths that resolve to the same distance computer, and the dense-vs-packed case (:282) compares two layouts of the same algorithm. If the query were rotated with a different matrix than the base vectors, or the shared RaBitQ distance formula had a sign error, all five TEST_CASEs still pass and the index ships returning wrong neighbors. HNSW_RABITQ appears in no other test file, so this suite is the only coverage.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The test now computes an independent FP32 brute-force oracle and checks recall for L2/IP, RBQ1/RBQ4/RBQ8, and aligned/unaligned dimensions. It also verifies COSINE storage/refine distances against original vectors, so rotation, sign, or metric mistakes are no longer self-validating.

}
}

template <SIMDLevel SL>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tests/faiss/CMakeLists.txt builds faiss_tests from a hardcoded FAISS_TEST_SRCS list that does not contain test_rabitq_simd.cpp, and this PR does not add it, so the 174 lines of new AVX2/AVX-512 comparison tests never run in knowhere CI. The only remaining coverage for the ~1400 new kernel lines is the dense-vs-packed case in test_hnsw_rabitq.cc:282, which exercises exactly one runtime-detected SIMD level: on an AVX-512 CI host the AVX2 dense kernels are never executed, yet they are the ones that run on AVX2-only production hosts, where a kernel bug produces wrong distances undetected.

@CLiqing CLiqing Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. test_rabitq_simd.cpp is now listed in tests/faiss/CMakeLists.txt. I built faiss_tests locally and ran 10 RaBitQ tests, including explicit AVX2/AVX-512 single and batch-4 equivalence and the non-BMI2 fallback.


IndexHNSWRaBitQ::IndexHNSWRaBitQ() = default;

IndexHNSWRaBitQ::IndexHNSWRaBitQ(faiss::IndexPreTransform* storage_in, int M)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IndexHNSWRaBitQ(IndexPreTransform*, int) delegates to IndexHNSW(storage, M) (IndexHNSW.cpp:241), which only builds an empty hnsw(M); entry_point stays -1 so HNSW.cpp:1006 returns immediately and every search yields no results, while the constructor still sets ntotal from the storage and presents the index as populated — and add() is overridden to throw, so the graph can never be filled in. It also sets own_fields = true at line 19 before calling validate_storage() at line 22: if validation throws, ~IndexHNSW runs and deletes the storage the caller still owns, giving a double free. Neither this constructor nor the non-const pretransform_index()/rabitq_index() overloads have any caller.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The unused pointer constructor and the unused non-const accessors were removed, eliminating both the empty-graph behavior and the exception-path ownership hazard.

@mergify mergify Bot added the ci-passed label Sep 1, 2026

// Private Knowhere serialization tag. Upstream Faiss reserves "IHNr" for
// its incompatible direct-build/staged-search IndexHNSWRaBitQ format.
inline constexpr char kHnswRaBitQFourcc[] = "IHRK";

@alexanderguzhva alexanderguzhva Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should not be here. It needs to be in index_read.cpp and index_write.cpp. Yes, let it be duplicate. Also, please make sure that our four-CC codes do not intersect with Faiss ones. So, a given four-CC should not be used by Faiss.

@CLiqing CLiqing Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Knowhere HNSW RaBitQ serialization is now implemented in the standard Faiss index_read.cpp/index_write.cpp, while the cppcontrib implementation remains duplicated as requested. The tags are IHRK, IHRC, and IRKC; I checked them against the bundled/upstream Faiss tag set. Cross-reader tests cover standard Faiss and cppcontrib IO.

@alexanderguzhva

Copy link
Copy Markdown
Collaborator

@CLiqing Please create a corresponding PR in the baseline faiss code and we'll ask Faiss guys to take a look. Thanks.

@CLiqing
CLiqing force-pushed the codex/pr-hnsw-rabitq branch from 89f7b5d to f78e4ac Compare September 2, 2026 09:24
@mergify mergify Bot removed the ci-passed label Sep 2, 2026
@CLiqing

CLiqing commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator Author

Created the requested baseline Faiss PR: facebookresearch/faiss#5552

It contains the reusable dense multi-bit RaBitQ layout, serialization, SIMD single/batch-4 scorers, BMI2 runtime fallback, and focused tests. The Knowhere-specific HNSW integration remains in this PR.

Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
@CLiqing
CLiqing force-pushed the codex/pr-hnsw-rabitq branch from f78e4ac to c74c993 Compare September 2, 2026 13:09
@mergify mergify Bot added the ci-passed label Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants