feat: add HNSW RaBitQ with rotated storage and staged search - #1814
Conversation
|
/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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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}}; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
|
@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>
821a243 to
5088e6e
Compare
|
|
||
| // 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) { |
There was a problem hiding this comment.
this is correct, but modern compilers can handle this situation. So, please get rid of this.
There was a problem hiding this comment.
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).
|
@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>
|
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. |
|
[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 DetailsNeeds approval from an approver in each of these files:
Approvers can indicate their approval by writing |
issue: #1747
Summary
Add
HNSW_RABITQto Knowhere with L2, inner product and cosine support.rbq_bits=1..9(default 1) and request-localrbq_bits_query=0..8(default 4) for search, range search and iteration.qb=4estimates with query-plane reuse and prefetch.refine_k.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