-
Notifications
You must be signed in to change notification settings - Fork 223
CAGRA Bloom Filter #2236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CAGRA Bloom Filter #2236
Changes from all commits
21e5f20
16946a4
a3d7b96
80027b5
56d959a
93db300
4e6aa9c
cf40ea1
c575717
2bebdb9
4b33230
863e9fb
0569eb7
241ca5d
dfe8f9f
5106f94
d59ca43
d29ab50
22f1caf
39ade55
84042aa
9667682
fcb30d4
40c22ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| #pragma once | ||
|
|
||
| #include <cuvs/core/export.hpp> | ||
| #include <raft/core/device_mdarray.hpp> | ||
| #include <raft/core/resources.hpp> | ||
|
|
||
| #include <cstddef> | ||
| #include <cstdint> | ||
| #include <memory> | ||
|
|
||
| namespace CUVS_EXPORT cuvs { | ||
| namespace core { | ||
|
|
||
| /** | ||
| * @brief cuVS-owned Bloom filter wrapper with opaque implementation. | ||
| * | ||
| * This class intentionally hides cuCollections types from the cuVS public API. | ||
| * The wrapper supports the expected bulk host APIs used by ANN workflows. | ||
| */ | ||
| class CUVS_EXPORT bloom_filter { | ||
| private: | ||
| struct impl; | ||
|
|
||
| public: | ||
| using key_type = std::uint32_t; | ||
|
|
||
| /** | ||
| * @brief Construct a Bloom filter with user-facing quality knobs. | ||
| * | ||
| * @p dataset_rows is the number of rows in the indexed dataset. The filter uses it with | ||
| * @p filtering_rate to estimate the number of inserted valid ids and compute a target filter | ||
| * size that satisfies the requested false-positive rate. | ||
| * | ||
| * The primary tuning knobs are: | ||
| * - @p filtering_rate: expected fraction of dataset rows that will be inserted as valid ids. | ||
| * - @p target_false_positive_rate: desired Bloom filter false-positive probability. | ||
| * | ||
| * Sizing math used internally: | ||
| * - `expected_insertions = ceil(dataset_rows * filtering_rate)` | ||
| * - The default policy uses 256-bit blocks split into eight 32-bit words and sets one bit in each | ||
| * word per inserted key. | ||
| * - For each candidate block count, the expected false-positive rate accounts for the binomial | ||
| * distribution of inserted keys across blocks and the fixed eight-bit fingerprint. | ||
| * - The smallest block count whose expected false-positive rate meets | ||
| * @p target_false_positive_rate is selected. | ||
| * | ||
| * Practical knob behavior: | ||
| * - Lower @p target_false_positive_rate -> larger filter, fewer false positives, typically higher | ||
| * filtered-search recall. | ||
| * - Higher @p filtering_rate -> larger filter for the same target false-positive rate. | ||
| */ | ||
| bloom_filter(raft::resources const& res, | ||
| std::size_t dataset_rows, | ||
| float filtering_rate = 1.0f, | ||
| float target_false_positive_rate = 0.01f); | ||
| ~bloom_filter(); | ||
|
|
||
| bloom_filter(bloom_filter const&) = delete; | ||
| bloom_filter& operator=(bloom_filter const&) = delete; | ||
| bloom_filter(bloom_filter&&) noexcept; | ||
| bloom_filter& operator=(bloom_filter&&) noexcept; | ||
|
|
||
| void clear(raft::resources const& res); | ||
| void clear_async(raft::resources const& res); | ||
|
|
||
| void add(raft::resources const& res, raft::device_vector_view<const key_type, int64_t> keys); | ||
| void add_async(raft::resources const& res, | ||
| raft::device_vector_view<const key_type, int64_t> keys); | ||
|
|
||
| void contains(raft::resources const& res, | ||
| raft::device_vector_view<const key_type, int64_t> keys, | ||
| raft::device_vector_view<std::uint8_t, int64_t> output) const; | ||
| void contains_async(raft::resources const& res, | ||
| raft::device_vector_view<const key_type, int64_t> keys, | ||
| raft::device_vector_view<std::uint8_t, int64_t> output) const; | ||
|
|
||
| [[nodiscard]] std::size_t num_blocks() const noexcept; | ||
|
|
||
| /** | ||
| * @brief Return the estimated fraction of dataset rows rejected by this filter. | ||
| * | ||
| * The estimate is derived at construction from the configured valid-row fraction and the | ||
| * expected false-positive rate of the selected filter geometry. It performs no device work. | ||
| */ | ||
| [[nodiscard]] float estimate_filtering_rate() const noexcept; | ||
|
|
||
| private: | ||
| friend impl const& get_bloom_filter_impl(bloom_filter const& filter) noexcept; | ||
|
|
||
| std::unique_ptr<impl> impl_; | ||
| }; | ||
|
|
||
| } // namespace core | ||
| } // namespace CUVS_EXPORT cuvs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. | ||
| * SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
|
|
@@ -33,6 +33,9 @@ | |
| #endif | ||
|
|
||
| namespace CUVS_EXPORT cuvs { | ||
| namespace core { | ||
| class bloom_filter; | ||
| } | ||
| namespace neighbors { | ||
| /** | ||
| * @addtogroup cagra_cpp_index_params | ||
|
|
@@ -497,7 +500,7 @@ namespace filtering { | |
| * @{ | ||
| */ | ||
|
|
||
| enum class FilterType { None, Bitmap, Bitset, UDF }; | ||
| enum class FilterType : int { None = 0, Bitmap = 1, Bitset = 2, Bloom = 3, UDF = 100 }; | ||
|
|
||
| struct base_filter { | ||
| ~base_filter() = default; | ||
|
|
@@ -617,6 +620,34 @@ struct bitset_filter : public base_filter { | |
| void to_csr(raft::resources const& handle, csr_matrix_t& csr); | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Filter CAGRA candidates with a global @c cuvs::core::bloom_filter over the index. | ||
| * | ||
| * Build the filter once on the host with bulk @c add() over the allowed dataset row ids and pass | ||
| * the owning @c cuvs::core::bloom_filter to this wrapper. CAGRA internals build/cache the device | ||
| * payload, similar to @ref bitset_filter, and the linked JIT-LTO fragment probes the same filter | ||
| * for every query and candidate with probabilistic membership tests. | ||
| * | ||
| * Bloom filters have no false negatives: if a row was inserted, @c contains returns @c true. False | ||
| * positives are possible, so highly selective predicates may still need a bitset or UDF for exact | ||
| * filtering. | ||
| * | ||
| * This adapter is non-owning. The referenced @c cuvs::core::bloom_filter must outlive the adapter | ||
| * and any searches that use it, and must not be moved or mutated concurrently with a search. | ||
| */ | ||
| struct bloom_filter : public base_filter { | ||
| void* filter_data{nullptr}; | ||
|
|
||
| bloom_filter() = default; | ||
|
|
||
| explicit bloom_filter(const cuvs::core::bloom_filter& bloom_filter) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The referenced core filter must outlive the adapter and all searches, and must not be moved or mutated concurrently, right? The stored pointer otherwise becomes invalid or refers to a moved-from object without a diagnostic, so we should document this if I'm right.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I'll add the note but it is a problem with all our filters as we separate the implementation from the interface. |
||
| : filter_data(const_cast<cuvs::core::bloom_filter*>(&bloom_filter)) | ||
| { | ||
| } | ||
|
|
||
| FilterType get_filter_type() const override { return FilterType::Bloom; } | ||
| }; | ||
|
|
||
| /** | ||
| * @brief JIT-LTO user-defined filter predicate. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quick question, why assign the new value to udf instead of to bloom?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It follows the metric enum convention to separate pre-compiled types vs UDF unknown type as a sentinel value. New types can fill up between 3 and 100 going forward.