From 3c6038778778093e46b709c19f6be35dcd83e46e Mon Sep 17 00:00:00 2001 From: hyw Date: Sun, 6 Sep 2026 03:01:10 +0800 Subject: [PATCH 1/2] Add byte-oriented sizing and validation for Bloom filters --- include/cuco/bloom_filter.cuh | 52 ++++++++++++++ .../cuco/detail/bloom_filter/bloom_filter.inl | 57 ++++++++++++++++ tests/CMakeLists.txt | 1 + tests/bloom_filter/size_test.cu | 68 +++++++++++++++++++ 4 files changed, 178 insertions(+) create mode 100644 tests/bloom_filter/size_test.cu diff --git a/include/cuco/bloom_filter.cuh b/include/cuco/bloom_filter.cuh index 38f841db8..70069b524 100644 --- a/include/cuco/bloom_filter.cuh +++ b/include/cuco/bloom_filter.cuh @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,11 @@ namespace cuco { +/** + * @brief A strong type wrapper for specifying an exact Bloom filter size in bytes. + */ +CUCO_DEFINE_STRONG_TYPE(bloom_filter_size_bytes, std::size_t) + /** * @brief A GPU-accelerated Bloom filter. * @@ -116,6 +122,48 @@ class bloom_filter { Allocator const& alloc = {}, cuda::stream_ref stream = cuda::stream_ref{cudaStream_t{nullptr}}); + /** + * @brief Constructs a Bloom filter with exactly the requested storage size in bytes. + * + * @note Validates the size before allocating device storage. + * + * @throws cuco::logic_error If the size is zero, not a multiple of the block size, + * exceeds `max_size()`, or does not match a static extent + * + * @param size_bytes Exact storage size in bytes + * @param scope The scope in which operations will be performed + * @param policy Fingerprint generation policy + * @param alloc Allocator used for allocating device-accessible storage + * @param stream CUDA stream used to initialize the filter + */ + __host__ explicit bloom_filter(bloom_filter_size_bytes size_bytes, + cuda_thread_scope scope = {}, + Policy const& policy = {}, + Allocator const& alloc = {}, + cuda::stream_ref stream = cuda::stream_ref{cudaStream_t{nullptr}}); + + /** + * @brief Returns the largest supported block-aligned byte count not exceeding a budget. + * + * Rounds down to a multiple of `words_per_block * sizeof(word_type)` and caps the result + * at `max_size()`. This utility does not enforce a particular static extent. + * + * @throws cuco::logic_error If the budget cannot accommodate one filter block + * + * @param size_bytes Storage budget in bytes + * @return Positive, block-aligned storage size in bytes + */ + [[nodiscard]] __host__ static constexpr std::size_t aligned_size(std::size_t size_bytes); + + /** + * @brief Returns the maximum storage size in bytes supported by the policy and size type. + * + * @note This limit does not account for available device memory or a particular static extent. + * + * @return Maximum storage size in bytes + */ + [[nodiscard]] __host__ static constexpr std::size_t max_size() noexcept; + /** * @brief Erases all information from the filter. * @@ -444,6 +492,10 @@ class bloom_filter { [[nodiscard]] __host__ constexpr ref_type<> ref() const noexcept; private: + template + __host__ static constexpr extent_type make_block_extent(bloom_filter_size_bytes size_bytes, + cuco::extent); + allocator_type allocator_; ///< Allocator used to allocate device-accessible storage std::unique_ptr::filter_block_type, detail::custom_deleter> diff --git a/include/cuco/detail/bloom_filter/bloom_filter.inl b/include/cuco/detail/bloom_filter/bloom_filter.inl index 3b62d1041..cb50b96a0 100644 --- a/include/cuco/detail/bloom_filter/bloom_filter.inl +++ b/include/cuco/detail/bloom_filter/bloom_filter.inl @@ -5,16 +5,73 @@ #pragma once +#include #include #include #include +#include +#include #include #include namespace cuco { +template +__host__ bloom_filter::bloom_filter( + bloom_filter_size_bytes size_bytes, + cuda_thread_scope scope, + Policy const& policy, + Allocator const& alloc, + cuda::stream_ref stream) + : bloom_filter{make_block_extent(size_bytes, Extent{0}), scope, policy, alloc, stream} +{ +} + +template +[[nodiscard]] __host__ constexpr std::size_t +bloom_filter::max_size() noexcept +{ + constexpr auto block_bytes = words_per_block * sizeof(word_type); + constexpr auto max_blocks = cuda::std::min( + static_cast(Policy::max_filter_blocks), + cuda::std::min(static_cast(cuda::std::numeric_limits::max()), + cuda::std::numeric_limits::max() / block_bytes)); + return max_blocks * block_bytes; +} + +template +[[nodiscard]] __host__ constexpr std::size_t +bloom_filter::aligned_size(std::size_t size_bytes) +{ + constexpr auto block_bytes = words_per_block * sizeof(word_type); + CUCO_EXPECTS(size_bytes >= block_bytes, + "Storage size must accommodate at least one filter block"); + auto const capped_bytes = cuda::std::min(size_bytes, max_size()); + return capped_bytes - capped_bytes % block_bytes; +} + +template +template +__host__ constexpr typename bloom_filter::extent_type +bloom_filter::make_block_extent( + bloom_filter_size_bytes size_bytes, cuco::extent) +{ + static_assert(N == cuco::dynamic_extent || detail::is_static_extent_representable(), + "Static extent must be representable by its size type"); + constexpr auto block_bytes = words_per_block * sizeof(word_type); + CUCO_EXPECTS(size_bytes.value > 0, "Storage size must be positive"); + CUCO_EXPECTS(size_bytes.value % block_bytes == 0, + "Storage size must be a multiple of the filter block size"); + CUCO_EXPECTS(size_bytes.value <= max_size(), "Storage size exceeds the maximum filter size"); + auto const num_blocks = size_bytes.value / block_bytes; + auto const extent = extent_type{static_cast(num_blocks)}; + CUCO_EXPECTS(static_cast(static_cast(extent)) == num_blocks, + "Storage size must match the static block extent"); + return extent; +} + template __host__ bloom_filter::bloom_filter(Extent num_blocks, cuda_thread_scope, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ae98b1216..2e92f9eac 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -144,6 +144,7 @@ ConfigureTest(HYPERLOGLOG_TEST ################################################################################################### # - bloom_filter ---------------------------------------------------------------------------------- ConfigureTest(BLOOM_FILTER_TEST + bloom_filter/size_test.cu bloom_filter/unique_sequence_test.cu bloom_filter/variable_cg_test.cu bloom_filter/merge_intersect_test.cu diff --git a/tests/bloom_filter/size_test.cu b/tests/bloom_filter/size_test.cu new file mode 100644 index 000000000..b79c70c6e --- /dev/null +++ b/tests/bloom_filter/size_test.cu @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include + +#include +#include +#include + +TEMPLATE_TEST_CASE_SIG("bloom_filter byte sizing", + "[bloom_filter][sizing][host]", + (class Policy), + (cuco::bloom_filter_policy), + (cuco::bloom_filter_policy, 8, 8>)) +{ + using filter_type = + cuco::bloom_filter, cuda::thread_scope_device, Policy>; + constexpr auto block_bytes = Policy::words_per_block * sizeof(typename Policy::word_type); + constexpr auto max_bytes = Policy::max_filter_blocks * block_bytes; + + STATIC_REQUIRE(filter_type::max_size() == max_bytes); + STATIC_REQUIRE(filter_type::aligned_size(block_bytes) == block_bytes); + REQUIRE(filter_type::aligned_size(2 * block_bytes - 1) == block_bytes); + REQUIRE(filter_type::aligned_size(max_bytes) == max_bytes); + REQUIRE(filter_type::aligned_size(std::numeric_limits::max()) == max_bytes); + REQUIRE_THROWS_AS(filter_type::aligned_size(block_bytes - 1), cuco::logic_error); + + // These must throw before device allocation, including on hosts without a GPU. + for (auto bytes : {std::size_t{0}, block_bytes + 1, max_bytes + block_bytes}) { + REQUIRE_THROWS_AS(filter_type{cuco::bloom_filter_size_bytes{bytes}}, cuco::logic_error); + } +} + +TEST_CASE("bloom_filter byte sizing extent limits", "[bloom_filter][sizing][host]") +{ + using filter_type = cuco::bloom_filter>; + constexpr auto block_bytes = + filter_type::words_per_block * sizeof(typename filter_type::word_type); + constexpr auto max_bytes = std::numeric_limits::max() * block_bytes; + + STATIC_REQUIRE(filter_type::max_size() == max_bytes); + REQUIRE_THROWS_AS(filter_type{cuco::bloom_filter_size_bytes{max_bytes + block_bytes}}, + cuco::logic_error); + + using static_filter = cuco::bloom_filter>; + REQUIRE_THROWS_AS(static_filter{cuco::bloom_filter_size_bytes{block_bytes}}, cuco::logic_error); +} + +TEMPLATE_TEST_CASE_SIG("bloom_filter byte construction", + "[bloom_filter][sizing][gpu]", + (class Extent), + (cuco::extent), + (cuco::extent)) +{ + using filter_type = cuco::bloom_filter; + constexpr auto bytes = 2 * filter_type::words_per_block * sizeof(typename filter_type::word_type); + + auto by_bytes = filter_type{cuco::bloom_filter_size_bytes{bytes}}; + auto by_blocks = filter_type{Extent{2}}; + REQUIRE(static_cast(by_bytes.block_extent()) == 2); + REQUIRE(by_bytes.block_extent() == by_blocks.block_extent()); +} From c80884c25614d43320e5bd134daf1c95cede815f Mon Sep 17 00:00:00 2001 From: HuangYuwei Date: Fri, 11 Sep 2026 18:56:17 +0800 Subject: [PATCH 2/2] Adopt upper-bound semantics for Bloom filter byte sizing Treat bloom_filter_bytes as a storage budget, round down to whole blocks, and cap at max_size(). Restrict the overload to dynamic extents and fold coverage into the existing policy test. Signed-off-by: HuangYuwei --- include/cuco/bloom_filter.cuh | 39 ++++------- .../cuco/detail/bloom_filter/bloom_filter.inl | 48 ++++--------- tests/CMakeLists.txt | 1 - tests/bloom_filter/size_test.cu | 68 ------------------- tests/bloom_filter/unique_sequence_test.cu | 6 +- 5 files changed, 33 insertions(+), 129 deletions(-) delete mode 100644 tests/bloom_filter/size_test.cu diff --git a/include/cuco/bloom_filter.cuh b/include/cuco/bloom_filter.cuh index 70069b524..607462e3d 100644 --- a/include/cuco/bloom_filter.cuh +++ b/include/cuco/bloom_filter.cuh @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -26,9 +27,9 @@ namespace cuco { /** - * @brief A strong type wrapper for specifying an exact Bloom filter size in bytes. + * @brief A strong type wrapper for specifying a Bloom filter storage budget in bytes. */ -CUCO_DEFINE_STRONG_TYPE(bloom_filter_size_bytes, std::size_t) +CUCO_DEFINE_STRONG_TYPE(bloom_filter_bytes, std::size_t) /** * @brief A GPU-accelerated Bloom filter. @@ -123,38 +124,30 @@ class bloom_filter { cuda::stream_ref stream = cuda::stream_ref{cudaStream_t{nullptr}}); /** - * @brief Constructs a Bloom filter with exactly the requested storage size in bytes. + * @brief Constructs a Bloom filter within a storage budget in bytes. * - * @note Validates the size before allocating device storage. + * The allocated size is rounded down to a whole number of filter blocks and capped at + * `max_size()`. * - * @throws cuco::logic_error If the size is zero, not a multiple of the block size, - * exceeds `max_size()`, or does not match a static extent + * @note This overload requires a dynamic extent. * - * @param size_bytes Exact storage size in bytes + * @throws cuco::logic_error If the budget cannot accommodate one filter block + * + * @param size_bytes Storage budget in bytes * @param scope The scope in which operations will be performed * @param policy Fingerprint generation policy * @param alloc Allocator used for allocating device-accessible storage * @param stream CUDA stream used to initialize the filter */ - __host__ explicit bloom_filter(bloom_filter_size_bytes size_bytes, + template >>> + __host__ explicit bloom_filter(bloom_filter_bytes size_bytes, cuda_thread_scope scope = {}, Policy const& policy = {}, Allocator const& alloc = {}, cuda::stream_ref stream = cuda::stream_ref{cudaStream_t{nullptr}}); - /** - * @brief Returns the largest supported block-aligned byte count not exceeding a budget. - * - * Rounds down to a multiple of `words_per_block * sizeof(word_type)` and caps the result - * at `max_size()`. This utility does not enforce a particular static extent. - * - * @throws cuco::logic_error If the budget cannot accommodate one filter block - * - * @param size_bytes Storage budget in bytes - * @return Positive, block-aligned storage size in bytes - */ - [[nodiscard]] __host__ static constexpr std::size_t aligned_size(std::size_t size_bytes); - /** * @brief Returns the maximum storage size in bytes supported by the policy and size type. * @@ -492,10 +485,6 @@ class bloom_filter { [[nodiscard]] __host__ constexpr ref_type<> ref() const noexcept; private: - template - __host__ static constexpr extent_type make_block_extent(bloom_filter_size_bytes size_bytes, - cuco::extent); - allocator_type allocator_; ///< Allocator used to allocate device-accessible storage std::unique_ptr::filter_block_type, detail::custom_deleter> diff --git a/include/cuco/detail/bloom_filter/bloom_filter.inl b/include/cuco/detail/bloom_filter/bloom_filter.inl index cb50b96a0..2a6fa228a 100644 --- a/include/cuco/detail/bloom_filter/bloom_filter.inl +++ b/include/cuco/detail/bloom_filter/bloom_filter.inl @@ -19,13 +19,24 @@ namespace cuco { template +template __host__ bloom_filter::bloom_filter( - bloom_filter_size_bytes size_bytes, + bloom_filter_bytes size_bytes, cuda_thread_scope scope, Policy const& policy, Allocator const& alloc, cuda::stream_ref stream) - : bloom_filter{make_block_extent(size_bytes, Extent{0}), scope, policy, alloc, stream} + : bloom_filter{[size_bytes] { + constexpr auto block_bytes = sizeof(typename ref_type<>::filter_block_type); + CUCO_EXPECTS(size_bytes.value >= block_bytes, + "Storage size must accommodate at least one filter block"); + return extent_type{static_cast( + cuda::std::min(size_bytes.value, max_size()) / block_bytes)}; + }(), + scope, + policy, + alloc, + stream} { } @@ -33,7 +44,7 @@ template ::max_size() noexcept { - constexpr auto block_bytes = words_per_block * sizeof(word_type); + constexpr auto block_bytes = sizeof(typename ref_type<>::filter_block_type); constexpr auto max_blocks = cuda::std::min( static_cast(Policy::max_filter_blocks), cuda::std::min(static_cast(cuda::std::numeric_limits::max()), @@ -41,37 +52,6 @@ bloom_filter::max_size() noexcept return max_blocks * block_bytes; } -template -[[nodiscard]] __host__ constexpr std::size_t -bloom_filter::aligned_size(std::size_t size_bytes) -{ - constexpr auto block_bytes = words_per_block * sizeof(word_type); - CUCO_EXPECTS(size_bytes >= block_bytes, - "Storage size must accommodate at least one filter block"); - auto const capped_bytes = cuda::std::min(size_bytes, max_size()); - return capped_bytes - capped_bytes % block_bytes; -} - -template -template -__host__ constexpr typename bloom_filter::extent_type -bloom_filter::make_block_extent( - bloom_filter_size_bytes size_bytes, cuco::extent) -{ - static_assert(N == cuco::dynamic_extent || detail::is_static_extent_representable(), - "Static extent must be representable by its size type"); - constexpr auto block_bytes = words_per_block * sizeof(word_type); - CUCO_EXPECTS(size_bytes.value > 0, "Storage size must be positive"); - CUCO_EXPECTS(size_bytes.value % block_bytes == 0, - "Storage size must be a multiple of the filter block size"); - CUCO_EXPECTS(size_bytes.value <= max_size(), "Storage size exceeds the maximum filter size"); - auto const num_blocks = size_bytes.value / block_bytes; - auto const extent = extent_type{static_cast(num_blocks)}; - CUCO_EXPECTS(static_cast(static_cast(extent)) == num_blocks, - "Storage size must match the static block extent"); - return extent; -} - template __host__ bloom_filter::bloom_filter(Extent num_blocks, cuda_thread_scope, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2e92f9eac..ae98b1216 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -144,7 +144,6 @@ ConfigureTest(HYPERLOGLOG_TEST ################################################################################################### # - bloom_filter ---------------------------------------------------------------------------------- ConfigureTest(BLOOM_FILTER_TEST - bloom_filter/size_test.cu bloom_filter/unique_sequence_test.cu bloom_filter/variable_cg_test.cu bloom_filter/merge_intersect_test.cu diff --git a/tests/bloom_filter/size_test.cu b/tests/bloom_filter/size_test.cu deleted file mode 100644 index b79c70c6e..000000000 --- a/tests/bloom_filter/size_test.cu +++ /dev/null @@ -1,68 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include - -#include -#include - -#include -#include -#include - -TEMPLATE_TEST_CASE_SIG("bloom_filter byte sizing", - "[bloom_filter][sizing][host]", - (class Policy), - (cuco::bloom_filter_policy), - (cuco::bloom_filter_policy, 8, 8>)) -{ - using filter_type = - cuco::bloom_filter, cuda::thread_scope_device, Policy>; - constexpr auto block_bytes = Policy::words_per_block * sizeof(typename Policy::word_type); - constexpr auto max_bytes = Policy::max_filter_blocks * block_bytes; - - STATIC_REQUIRE(filter_type::max_size() == max_bytes); - STATIC_REQUIRE(filter_type::aligned_size(block_bytes) == block_bytes); - REQUIRE(filter_type::aligned_size(2 * block_bytes - 1) == block_bytes); - REQUIRE(filter_type::aligned_size(max_bytes) == max_bytes); - REQUIRE(filter_type::aligned_size(std::numeric_limits::max()) == max_bytes); - REQUIRE_THROWS_AS(filter_type::aligned_size(block_bytes - 1), cuco::logic_error); - - // These must throw before device allocation, including on hosts without a GPU. - for (auto bytes : {std::size_t{0}, block_bytes + 1, max_bytes + block_bytes}) { - REQUIRE_THROWS_AS(filter_type{cuco::bloom_filter_size_bytes{bytes}}, cuco::logic_error); - } -} - -TEST_CASE("bloom_filter byte sizing extent limits", "[bloom_filter][sizing][host]") -{ - using filter_type = cuco::bloom_filter>; - constexpr auto block_bytes = - filter_type::words_per_block * sizeof(typename filter_type::word_type); - constexpr auto max_bytes = std::numeric_limits::max() * block_bytes; - - STATIC_REQUIRE(filter_type::max_size() == max_bytes); - REQUIRE_THROWS_AS(filter_type{cuco::bloom_filter_size_bytes{max_bytes + block_bytes}}, - cuco::logic_error); - - using static_filter = cuco::bloom_filter>; - REQUIRE_THROWS_AS(static_filter{cuco::bloom_filter_size_bytes{block_bytes}}, cuco::logic_error); -} - -TEMPLATE_TEST_CASE_SIG("bloom_filter byte construction", - "[bloom_filter][sizing][gpu]", - (class Extent), - (cuco::extent), - (cuco::extent)) -{ - using filter_type = cuco::bloom_filter; - constexpr auto bytes = 2 * filter_type::words_per_block * sizeof(typename filter_type::word_type); - - auto by_bytes = filter_type{cuco::bloom_filter_size_bytes{bytes}}; - auto by_blocks = filter_type{Extent{2}}; - REQUIRE(static_cast(by_bytes.block_extent()) == 2); - REQUIRE(by_bytes.block_extent() == by_blocks.block_extent()); -} diff --git a/tests/bloom_filter/unique_sequence_test.cu b/tests/bloom_filter/unique_sequence_test.cu index 92b546f2f..44e702881 100644 --- a/tests/bloom_filter/unique_sequence_test.cu +++ b/tests/bloom_filter/unique_sequence_test.cu @@ -87,8 +87,12 @@ TEMPLATE_TEST_CASE_SIG( using filter_type = cuco::bloom_filter, cuda::thread_scope_device, Policy>; constexpr size_type num_keys{400}; + constexpr std::size_t num_blocks{1000}; + constexpr auto block_bytes = sizeof(typename filter_type::template ref_type<>::filter_block_type); - auto filter = filter_type{1000}; + STATIC_REQUIRE(filter_type::max_size() == Policy::max_filter_blocks * block_bytes); + auto filter = filter_type{cuco::bloom_filter_bytes{num_blocks * block_bytes + block_bytes - 1}}; + REQUIRE(static_cast(filter.block_extent()) == num_blocks); test_unique_sequence(filter, num_keys); }