Skip to content

feat: add HNSW RaBitQ with rotated storage and staged search - #1814

Merged
sre-ci-robot merged 9 commits into
zilliztech:mainfrom
CLiqing:feat/hnsw-rabitq
Sep 17, 2026
Merged

sre-ci-robot merged 9 commits into
zilliztech:mainfrom
CLiqing:feat/hnsw-rabitq

Conversation

@CLiqing

@CLiqing CLiqing commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

issue: #1747

Summary

Add HNSW_RABITQ to Knowhere with L2, inner product and cosine support.

  • Build the graph using FP32 distances, then populate random-rotated Faiss RaBitQ storage using bounded encoding batches.
  • Support database precision rbq_bits=1..9 (default 1) and request-local rbq_bits_query=0..8 (default 4) for search, range search and iteration.
  • Use a one-bit estimate followed by multi-bit scoring during graph traversal, with metric-aware distance adapters and optimized query-estimate/full-distance SIMD kernels. Batch four independent qb=4 estimates with query-plane reuse and prefetch.
  • Preserve original input data for cosine norm correction and optional refinement; support existing refine codecs and refine_k.
  • Cover filtering/brute-force fallback, serialization, distance-by-ID and index registration. Keep the common HNSW input conversion unchanged; RaBitQ-specific bounded encoding lives in the bundled Faiss contrib utility.

Faiss relationship and scope

This selectively adapts the standard RaBitQ layout and staged-search approach from facebookresearch/faiss#5526, reference d8a85956060c7567ff40dc1eb0a77578533d8bad, to Knowhere's bundled Faiss. FP32 graph construction, rotation, cosine, filtering, refinement and request-local query parameters are Knowhere integration responsibilities.

New index/adaptor classes are under faiss/cppcontrib/knowhere. Existing core RaBitQ changes add a batched estimate interface and optimize SIMD kernels and unaligned-load safety.

Current boundaries

  • Immutable after initial build; mmap, multi-vector and embedding-list support are not advertised.
  • The parent Knowhere index must outlive its iterators, consistent with the current common iterator ownership limitation.
  • ARM64, early AMD and native Sapphire Rapids hardware execution remain CI/platform follow-ups; local dispatch overrides and SPR syntax checks do not replace hardware coverage.

@CLiqing

CLiqing commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

/kind feature

LOG_KNOWHERE_ERROR_ << "Can not add data to an empty index.";
return Status::empty_index;
}
if (tmp_index_rabitq.size() != indexes.size() || tmp_index_rabitq.empty() || tmp_index_rabitq[0] == 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.

AddInternal fills the HNSW graph at line 3272 and the RaBitQ storage at lines 3282/3285, but only relinquishes tmp_index_rabitq at lines 3325/3335 after the storage splice succeeds. A throw in between — for example std::bad_alloc in the full graph copy-assignment at lines 3310-3311 — is caught at line 3337 and returns faiss_inner_error with tmp_index_rabitq[0] still non-null, so the guard at line 3258 that enforces "HNSW_RABITQ is immutable after its initial Add" admits a retried Add() and appends the same rows a second time to both the graph and the RaBitQ storage. Both end up at 2N, so check_storage_compatibility (thirdparty/faiss/faiss/cppcontrib/knowhere/IndexHNSWRaBitQ.cpp:119-122) and validate_norms at serialize time (impl/index_write.cpp:791) both pass and the duplicated index is persisted.

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 in 5088e6e. Add now moves the pending graph/refiner and RaBitQ storage into local owners before either is mutated, and publishes the index only on success. Any error discards both halves; another Add is rejected until Train/Build is repeated. Local fault injection after graph add, partial encoding and final storage validation passed for L2/IP/COSINE with and without FP32 refinement. The old revision reproduced 4101 -> 8202 rows after retry, including successful serialization. A default unit test also covers one-shot Add and rebuilding without duplicate rows.

Comment thread tests/ut/test_hnsw_rabitq_acceptance.cc Outdated
REQUIRE(index.Build(base, cfg) == knowhere::Status::success);
for (int qb : {0, 4, 8}) {
cfg["rbq_bits_query"] = qb;
faiss::SIMDConfig::set_level(faiss::SIMDLevel::NONE);

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.

Line 369 calls faiss::SIMDConfig::set_level(faiss::SIMDLevel::NONE) before the is_simd_level_available guard on line 376, so the guard does not protect it. On RISC-V, cmake/libs/libfaiss.cmake:576 compiles faiss with COMPILE_SIMD_RISCV_RVV and without FAISS_ENABLE_DD, and thirdparty/faiss/faiss/utils/simd_levels.cpp:282 then assigns supported_simd_levels = (1 << RISCV_RVV) with no NONE bit, so set_level throws SIMDConfig::set_level: level NONE is not available and the whole test case fails. src/simd/hook.cc:551-555 and tests/ut/test_knowhere_init.cc:94 already guard this exact call for this exact reason.

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 in 5088e6e. The reference level now checks whether NONE is available and otherwise uses the current supported level. The additional graph test includes the current level and only available overrides; single-level builds explicitly report that cross-level comparison is unavailable. Local x86 tests passed; RISC-V hardware execution has not been performed locally.

// Finite-ef filtered graph search is approximate. Record its
// recall instead of inventing a universal minimum for this
// random graph fixture. BF routing above must be exact.
std::cout << "RBQ_FILTER_RECALL metric=" << metric << " bits=" << bits << " qb=" << qb

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.

hits (lines 900-912) and full_hits (lines 950-952) are computed and then only std::cout-printed at lines 922 and 953. The suite's single accuracy assertion, REQUIRE(delta < 0.03f) at line 339, lives in the "[.hnsw_rabitq_realdata]" test case at line 277, which Catch2 hides by default and which also requires KNOWHERE_RBQ_ACCEPTANCE_DATA (line 278); Makefile:134 and .github/workflows/ut.yaml:54 run the binary with no tag spec. A regression that keeps distances correct but degrades the candidate set — an over-conservative should_refine window, a mis-sized threshold heap, a worse traversal order — passes CI and only changes printed numbers. Every other HNSW variant asserts REQUIRE(recall >= expected_recall) at tests/ut/test_faiss_hnsw.cc:300, and INDEX_HNSW_RABITQ appears nowhere in that file.

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 in 5088e6e. Added a default, self-contained graph-search test with exact FP32 ground truth and explicit recall assertions for L2/IP/COSINE, database bits 1/4/8/9, query bits 0/4/8, and unfiltered/25%-filtered queries. It checks the production routing predicates to ensure these requests do not fall back to brute force. The existing hidden real-data diagnostic is no longer the only quality check.

for (const auto* metric : {"L2", "IP", "COSINE"})
for (int bits : {1, 4, 8, 9}) {
knowhere::Json cfg = {{"dim", 65}, {"metric_type", metric}, {"M", 16}, {"efConstruction", 100}, {"ef", 128},
{"k", 128}, {"rbq_bits", bits}};

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.

The SIMD cross-level test builds 128 rows (line 356) and searches with k = 128 (line 361). WhetherPerformBruteForceSearch returns true when k >= ntotal * 0.5 (src/index/hnsw/impl/IndexConditionalWrapper.cc:45, threshold at IndexConditionalWrapper.h:81), so both searches at lines 370 and 384 run IndexBruteForceWrapper and the SIMD-agreement check never reaches IndexHNSWRaBitQWrapper::search_query, the staged threshold heap, or bitwise_q4_batch_4. The same threshold applies to ef for range search (IndexConditionalWrapper.cc:78): ef = n = 128 at lines 987-988 here and ef = 128 over 256 rows at tests/ut/test_hnsw_rabitq.cc:478 both force brute force, so every HNSW_RABITQ range-search test is exhaustive and the exact set equality at line 1023 proves nothing about the graph path.

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 in 5088e6e. Kept and renamed the existing exhaustive cases as brute-force coverage. Added N=1024, k=10, ef=128 tests that verify both production BF routing predicates are false, including with filtering, and exercise graph search across available SIMD levels. Graph RangeSearch now has independent boundary, distance and recall assertions against full-code references rather than requiring exhaustive result-set equality.

@alexanderguzhva

Copy link
Copy Markdown
Collaborator

@CLiqing please rebase on top of master

Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Move RaBitQ dispatch and staged evaluation out of generic HNSW code. Reuse the refinement predicate and count full refinements in search statistics.

Reject non-RaBitQ payloads before replacing a live index, retain filtered iterator bridges, and add targeted and SQ/PQ regression coverage. Remove unrelated serialization formatting changes.

Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Remove the separate native RaBitQ traversal. Keep search state, queues and stopping rules in the existing Knowhere searcher, with RaBitQ-specific staged distance evaluation and qb4 SIMD/prefetch batching. Preserve candidate-order refinement and request-local query parameters.

Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
@CLiqing

CLiqing commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main at 3e15e83 and addressed the four review comments in 5088e6e. Local Release build and 31 targeted/regression test cases passed. New CI runs have started; DCO passed. Keeping this PR in Draft.


// RaBitQ codes and query bit planes are byte-aligned, including their tails.
// memcpy preserves the unaligned load contract without pointer-alignment UB.
inline uint64_t load_u64_unaligned(const uint8_t* ptr) {

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 is correct, but modern compilers can handle this situation. So, please get rid of this.

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.

Removed the helper in 7ac8e88. The call sites now use local fixed-size memcpy loads, preserving byte-aligned access without adding an abstraction. Release regression and targeted ASan/UBSan runs both passed (32 test cases each).

@alexanderguzhva

Copy link
Copy Markdown
Collaborator

@CLiqing please similarly add a PR into the faiss baseline repo for the faiss baseline files, so that PR would be adopted into the baseline.

Signed-off-by: ChenLiqing <23721160+CLiqing@users.noreply.github.com>
@CLiqing

CLiqing commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator Author

Opened facebookresearch/faiss#5647 for the reusable RaBitQ core extensions, based on current Faiss main. It includes the batch-four estimate API/kernels, 9-bit SIMD scoring, associated dispatch/load changes, and standalone tests. Knowhere-specific integration remains here. The load helper review fix is also pushed in 7ac8e88; keeping this PR in Draft.

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

@foxspy foxspy left a comment

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.

/lgtm

@sre-ci-robot

Copy link
Copy Markdown
Collaborator

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: CLiqing, foxspy

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

@CLiqing
CLiqing marked this pull request as ready for review September 17, 2026 09:48
@mergify mergify Bot added ci-passed and removed ci-passed labels Sep 17, 2026
@sre-ci-robot
sre-ci-robot merged commit 2af2a86 into zilliztech:main Sep 17, 2026
13 checks passed
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.

5 participants