Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
a18c546
Add test for filtering row groups with Bloom filters on real data
qbacpey Jun 14, 2026
b609755
[WIP] Implement debug logging for Bloom filter data in parquet reader…
qbacpey Jun 16, 2026
a32a95f
Refactor hybrid scan bloom filter test to clarify A/B comparison with…
qbacpey Jun 16, 2026
03c1834
Enhance hybrid scan bloom filter test with parameterized fixtures
qbacpey Jun 16, 2026
df8fc33
Refactor bloom filter handling in hybrid scan to improve performance …
qbacpey Jun 24, 2026
be4fde6
Refactor hybrid scan implementation to streamline bloom filter handling
qbacpey Jun 26, 2026
48517bc
Update copyright notices and improve code clarity in hybrid scan files
qbacpey Jun 26, 2026
f59589e
Refactor bloom filter header processing in `fetch_bloom_filters_to_de…
qbacpey Jun 26, 2026
12d607b
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jun 26, 2026
518d278
Add new function to fetch bloom filters from multiple datasources
qbacpey Jun 26, 2026
d9f37e2
Refactor comments in parquet_io_utils to improve clarity
qbacpey Jun 26, 2026
c568dc2
Updated the stale hybrid scan docs.
qbacpey Jun 26, 2026
6d87652
Formatting
qbacpey Jun 26, 2026
9cf8b38
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jun 30, 2026
15ddaee
Refactor bloom filter fetching in hybrid scan to use aligned memory r…
qbacpey Jun 30, 2026
61dbf3b
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jun 30, 2026
50bbc17
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jul 1, 2026
4ee65aa
Refactor bloom filter handling in Parquet I/O
qbacpey Jul 1, 2026
cb9067f
Use speculative single-read for Parquet bloom filter fetch
qbacpey Jul 1, 2026
442cb67
Refine bloom filter fetch logic in Parquet I/O
qbacpey Jul 1, 2026
8981d57
Refactor bloom filter reading in Parquet I/O
qbacpey Jul 1, 2026
6e6443a
Refactor bloom filter fetching in Parquet I/O
qbacpey Jul 3, 2026
b3cd38b
Update task dispatch documentation in Parquet I/O
qbacpey Jul 3, 2026
3969eed
Enhance bloom filter reading logic in Parquet I/O
qbacpey Jul 6, 2026
3d438af
Refactor bloom filter speculative read size in Parquet I/O
qbacpey Jul 6, 2026
b82a758
Enhance bloom filter validation in Parquet I/O
qbacpey Jul 6, 2026
be4f00f
Enhance bloom filter reading and host data fetching in Parquet I/O
qbacpey Jul 7, 2026
e73fc76
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jul 7, 2026
177f978
Share host/device fetch helpers between byte-range and bloom paths
qbacpey Jul 8, 2026
588187d
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jul 13, 2026
cbb0da3
Merge remote-tracking branch 'upstream/main' into fix/bloom-header-hy…
qbacpey Jul 16, 2026
7606a90
Revert `fetch_byte_ranges_to_device_async_impl` while use mutex
qbacpey Jul 16, 2026
44a5743
Refactor `read_ranges_to_host` to return host buffers directly instea…
qbacpey Jul 16, 2026
b3741f9
Refactor `fetch_bloom_filters_to_device_impl` to improve clarity and …
qbacpey Jul 16, 2026
344a3b3
Enhance `read_bloom_filters` and `read_ranges_to_host` functions for …
qbacpey Jul 17, 2026
d36cddc
Refactor bloom filter handling in Parquet I/O. Updated function signa…
qbacpey Jul 17, 2026
d0af7d2
Optimize mutex commit
qbacpey Jul 17, 2026
59e7df3
Nested
qbacpey Jul 19, 2026
4f458b1
Refactor Parquet I/O functions to enhance clarity and efficiency. Upd…
qbacpey Jul 20, 2026
3851e9f
Refactor bloom filter data processing in `aggregate_reader_metadata::…
qbacpey Jul 20, 2026
1159a8b
Remove unnecessary alignment checks in `fetch_bloom_filters_to_device…
qbacpey Jul 20, 2026
dc50d9d
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 20, 2026
dfc2efe
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 20, 2026
37d10c2
Formatting
qbacpey Jul 20, 2026
c562afd
Merge branch 'main' into fix/bloom-header-hybrid
mhaseeb123 Jul 21, 2026
f6f8fb3
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 21, 2026
4de4bcf
Refactor fetch_bloom_filters_to_device_impl to streamline memory mana…
qbacpey Jul 21, 2026
083e63d
use `cuda::std::as_bytes(span)`
qbacpey Jul 21, 2026
66a58d3
Update bloom filter header parsing to use bytes per block for validation
qbacpey Jul 22, 2026
afb13f6
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 22, 2026
069e642
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 22, 2026
1872860
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 24, 2026
1145ad5
Enhance bloom filter logging and improve mutex scope in `fetch_bloom_…
qbacpey Jul 24, 2026
4cb0596
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 24, 2026
2e7b1a7
Merge branch 'main' into fix/bloom-header-hybrid
qbacpey Jul 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -12,8 +12,6 @@
#include <cudf/io/text/byte_range_info.hpp>
#include <cudf/utilities/memory_resource.hpp>

#include <rmm/mr/aligned_resource_adaptor.hpp>

#include <unordered_set>
#include <vector>

Expand Down Expand Up @@ -104,12 +102,10 @@ std::vector<cudf::size_type> apply_row_group_filters(

if (filters.contains(hybrid_scan_filter_type::ROW_GROUPS_WITH_BLOOM_FILTERS) and
bloom_filter_byte_ranges.size()) {
// Fetch 32-byte aligned bloom filter data buffers from the input file buffer
auto constexpr bloom_filter_alignment = rmm::CUDA_ALLOCATION_ALIGNMENT;
auto aligned_mr = rmm::mr::aligned_resource_adaptor(mr, bloom_filter_alignment);
// Fetch the header-stripped, 32-byte-aligned bloom filter bitsets from the input file
auto [bloom_filter_buffers, bloom_filter_data, bloom_read_tasks] =
cudf::io::parquet::fetch_byte_ranges_to_device_async(
datasource, bloom_filter_byte_ranges, stream, aligned_mr);
cudf::io::parquet::fetch_bloom_filters_to_device_async(
datasource, bloom_filter_byte_ranges, stream, mr);
bloom_read_tasks.get();

bloom_filtered_row_groups = reader.filter_row_groups_with_bloom_filters(
Expand Down
11 changes: 4 additions & 7 deletions cpp/examples/hybrid_scan_io/hybrid_scan_composer.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand All @@ -16,8 +16,6 @@
#include <cudf/io/types.hpp>
#include <cudf/utilities/memory_resource.hpp>

#include <rmm/mr/aligned_resource_adaptor.hpp>

#include <nvtx3/nvtx3.hpp>

#include <unordered_set>
Expand Down Expand Up @@ -175,14 +173,13 @@ std::vector<cudf::size_type> apply_row_group_filters(
bloom_filtered_row_group_indices.reserve(current_row_group_indices.size());
if (filters.contains(hybrid_scan_filter_type::ROW_GROUPS_WITH_BLOOM_FILTERS) and
bloom_filter_byte_ranges.size()) {
// Fetch 32-byte aligned bloom filter data buffers from the input file buffer
auto constexpr bloom_filter_alignment = rmm::CUDA_ALLOCATION_ALIGNMENT;
auto aligned_mr = rmm::mr::aligned_resource_adaptor(temp_mr, bloom_filter_alignment);
if (verbose) { std::cout << "READER: Filter row groups with bloom filters...\n"; }
timer.reset();
nvtxRangePush("fetch_bloom_filter_byte_ranges");
// Fetch the header-stripped, 32-byte-aligned bloom filter bitsets from the input file
auto [bloom_filter_buffers, bloom_filter_data, bloom_read_tasks] =
fetch_byte_ranges_async(datasource, bloom_filter_byte_ranges, stream, aligned_mr);
cudf::io::parquet::fetch_bloom_filters_to_device_async(
datasource, bloom_filter_byte_ranges, stream, temp_mr);
bloom_read_tasks.get();
nvtxRangePop();

Expand Down
22 changes: 22 additions & 0 deletions cpp/include/cudf/io/parquet_io_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,28 @@ fetch_byte_ranges_to_device_async(
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr);

/**
* @brief Fetches Parquet bloom filter bitsets from a datasource into device buffers
*
* @ingroup io_utils
*
* @param datasource Input datasource
* @param bloom_filter_byte_ranges Byte ranges of complete bloom filters to fetch, must span a
* complete bloom filter
* @param stream CUDA stream
* @param mr Device memory resource
*
* @return A tuple containing the device buffers, the device spans of the bitset data (one per input
* range; empty for ranges without a bloom filter), and a future to wait on the read tasks
*/
std::tuple<std::vector<rmm::device_buffer>,
std::vector<cudf::device_span<uint8_t const>>,
std::future<void>>
fetch_bloom_filters_to_device_async(cudf::io::datasource& datasource,
cudf::host_span<byte_range_info const> bloom_filter_byte_ranges,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr);
Comment thread
qbacpey marked this conversation as resolved.
Resolved

/** @} */ // end of group
} // namespace io::parquet
} // namespace CUDF_EXPORT cudf
56 changes: 34 additions & 22 deletions cpp/src/io/parquet/bloom_filter_reader.cu
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -32,6 +32,7 @@
#include <future>
#include <numeric>
#include <optional>
#include <utility>

namespace cudf::io::parquet::detail {
namespace {
Expand Down Expand Up @@ -304,7 +305,6 @@ void read_bloom_filter_data(host_span<std::unique_ptr<datasource> const> sources
cuco::extent<std::size_t>,
cuco::thread_scope_thread,
policy_type>::filter_block_type);
auto constexpr words_per_block = policy_type::words_per_block;

// Read tasks for bloom filter data
std::vector<std::future<std::size_t>> read_tasks;
Expand All @@ -322,39 +322,28 @@ void read_bloom_filter_data(host_span<std::unique_ptr<datasource> const> sources
// Read bloom filter iff present
auto const bloom_filter_offset = bloom_filter_offsets[chunk].value();

Comment thread
qbacpey marked this conversation as resolved.
Resolved
// If Bloom filter size (header + bitset) is available, just read the entire thing.
// Else just read 256 bytes which will contain the entire header and may contain the
// entire bitset as well.
auto constexpr bloom_filter_size_guess = 256;
// If the bloom filter size (header + bitset) is available, read the entire thing. Else read
// the max header size, which contains the entire header and may contain the entire bitset.
auto const initial_read_size =
static_cast<std::size_t>(bloom_filter_sizes[chunk].value_or(bloom_filter_size_guess));
static_cast<std::size_t>(bloom_filter_sizes[chunk].value_or(bloom_filter_header_max_size));

// Read an initial buffer from source
auto& source = sources[chunk_source_map[chunk]];
auto buffer = source->host_read(bloom_filter_offset, initial_read_size);

// Deserialize the Bloom filter header from the buffer.
BloomFilterHeader header;
CompactProtocolReader cp{buffer->data(), buffer->size()};
cp.read(&header);

// Check if the bloom filter header is valid.
auto const is_header_valid =
(header.num_bytes % words_per_block) == 0 and
header.compression.compression == BloomFilterCompression::UNCOMPRESSED and
header.algorithm.algorithm == BloomFilterAlgorithm::SPLIT_BLOCK and
header.hash.hash == BloomFilterHash::XXHASH;
// Deserialize and validate the bloom filter header from the buffer.
auto const header_info = parse_bloom_filter_header({buffer->data(), buffer->size()});

// Do not read if the bloom filter is invalid
if (not is_header_valid) {
if (not header_info.has_value()) {
bloom_filter_data[chunk] = {};
CUDF_LOG_WARN("Encountered an invalid bloom filter header. Skipping");
return;
}

// Bloom filter header size
auto const bloom_filter_header_size = static_cast<int64_t>(cp.bytecount());
auto const bitset_size = static_cast<std::size_t>(header.num_bytes);
// Bloom filter header and bitset sizes
auto const bloom_filter_header_size = header_info->first;
auto const bitset_size = header_info->second;

// Check if we already read in the filter bitset in the initial read.
if (initial_read_size >= bloom_filter_header_size + bitset_size) {
Expand Down Expand Up @@ -405,6 +394,29 @@ void read_bloom_filter_data(host_span<std::unique_ptr<datasource> const> sources

} // namespace

std::optional<std::pair<int64_t, std::size_t>> parse_bloom_filter_header(
Comment thread
qbacpey marked this conversation as resolved.
Resolved
host_span<uint8_t const> bytes)
{
using policy_type = arrow_filter_policy<cuda::std::byte>;
auto constexpr words_per_block = policy_type::words_per_block;

// Deserialize the bloom filter header from the front of the buffer
BloomFilterHeader header;
CompactProtocolReader cp{bytes.data(), bytes.size()};
cp.read(&header);

// Check if the bloom filter header is valid
auto const is_header_valid =
(header.num_bytes % words_per_block) == 0 and
Comment thread
qbacpey marked this conversation as resolved.
Resolved
header.compression.compression == BloomFilterCompression::UNCOMPRESSED and
header.algorithm.algorithm == BloomFilterAlgorithm::SPLIT_BLOCK and
header.hash.hash == BloomFilterHash::XXHASH;
if (not is_header_valid) { return std::nullopt; }

return std::pair{static_cast<int64_t>(cp.bytecount()),
static_cast<std::size_t>(header.num_bytes)};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

std::size_t aggregate_reader_metadata::get_bloom_filter_alignment() const
{
// Required alignment:
Expand Down
93 changes: 93 additions & 0 deletions cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "io/comp/common.hpp"
#include "io/parquet/parquet_common.hpp"
#include "io/parquet/reader_impl_helpers.hpp"

#include <cudf/detail/nvtx/ranges.hpp>
#include <cudf/detail/utilities/cuda_memcpy.hpp>
Expand All @@ -25,15 +26,19 @@

#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <format>
#include <functional>
#include <iterator>
#include <mutex>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>
#include <vector>

/**
Expand Down Expand Up @@ -375,6 +380,69 @@ fetch_byte_ranges_to_device_async_impl(
std::async(std::launch::deferred, sync_function, std::move(device_read_tasks))};
}

std::tuple<std::vector<rmm::device_buffer>,
std::vector<device_spans_per_source_type>,
std::future<void>>
fetch_bloom_filters_to_device_async_impl(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Mostly same as the read_bloom_filter_data, but refactor with parallel speculative reading.

This is the bench result with a wide BI fact table (10M rows, 82 row groups, 11 cols, bloom on 4 dims):

Image
query meaning after_stats / after_bloom rows upstream/main current branch speedup
needle (==501, odd/absent) bloom prunes all -> no decode (bloom-fetch bound) 82 / 0 0 3.23 ms 1.92 ms 1.68x
lookup (==250, present) bloom prunes none -> decode all (decode bound) 82 / 82 20,000 48.90 ms 48.31 ms 1.01x (equal)

Takeaway: the branch wins when the read is bloom-fetch bound (selective / needle queries) and is a
no-op when the read decodes real data (the bloom fetch is ~1 ms of a ~49 ms decode). It never loses.

cudf::host_span<std::reference_wrapper<cudf::io::datasource> const> datasources,
cudf::host_span<cudf::host_span<cudf::io::text::byte_range_info const> const>
bloom_filter_byte_ranges_per_source,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
rmm::device_async_resource_ref mr)
rmm::device_async_resource_ref aligned_mr)

@qbacpey qbacpey Jul 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

We now use rmm::device_buffer(..., 32, stream, mr) alignment ctor directly, so a regular MR is sufficient

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

not addressed in the current version (missing commits?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

{
auto const num_sources = datasources.size();
CUDF_EXPECTS(num_sources == bloom_filter_byte_ranges_per_source.size(),
"Encountered mismatch in number of datasources and bloom filter byte range spans");

// Read + parse a single bloom filter header to host and return its bitset-only byte range
Comment thread
qbacpey marked this conversation as resolved.
Resolved
auto const fetch_bitset_range =
[](cudf::io::datasource& datasource,
cudf::io::text::byte_range_info const& bloom_range) -> cudf::io::text::byte_range_info {
// placeholder for a chunk whose column has no bloom filter written
if (bloom_range.is_empty()) { return {0, 0}; }
auto const header_read_size =
std::min<int64_t>(bloom_range.size(), detail::bloom_filter_header_max_size);
auto const header = datasource.host_read(static_cast<std::size_t>(bloom_range.offset()),
static_cast<std::size_t>(header_read_size));
auto const header_info = detail::parse_bloom_filter_header({header->data(), header->size()});
CUDF_EXPECTS(header_info.has_value(), "Encountered an invalid bloom filter header");
auto const [header_size, bitset_size] = header_info.value();
return {bloom_range.offset() + header_size, static_cast<int64_t>(bitset_size)};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

HIGH: Bound-check the parsed bitset against the supplied bloom range.

Issue: Line 410 trusts bitset_size from the header without verifying that header_size + bitset_size <= bloom_range.size().
Why: A truncated/corrupt bloom range can fetch unrelated bytes as the bitset, producing incorrect pruning decisions.

🐛 Proposed fix
     auto const header_info = detail::parse_bloom_filter_header({header->data(), header->size()});
     CUDF_EXPECTS(header_info.has_value(), "Encountered an invalid bloom filter header");
     auto const [header_size, bitset_size] = header_info.value();
+    CUDF_EXPECTS(header_size >= 0 and
+                   header_size <= bloom_range.size() and
+                   bitset_size <= static_cast<std::size_t>(bloom_range.size() - header_size),
+                 "Bloom filter byte range does not contain the complete bitset");
     return {bloom_range.offset() + header_size, static_cast<int64_t>(bitset_size)};

As per coding guidelines, logic errors producing wrong row-group pruning are high-impact correctness issues.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto const header_info = detail::parse_bloom_filter_header({header->data(), header->size()});
CUDF_EXPECTS(header_info.has_value(), "Encountered an invalid bloom filter header");
auto const [header_size, bitset_size] = header_info.value();
return {bloom_range.offset() + header_size, static_cast<int64_t>(bitset_size)};
auto const header_info = detail::parse_bloom_filter_header({header->data(), header->size()});
CUDF_EXPECTS(header_info.has_value(), "Encountered an invalid bloom filter header");
auto const [header_size, bitset_size] = header_info.value();
CUDF_EXPECTS(header_size >= 0 and
header_size <= bloom_range.size() and
bitset_size <= static_cast<std::size_t>(bloom_range.size() - header_size),
"Bloom filter byte range does not contain the complete bitset");
return {bloom_range.offset() + header_size, static_cast<int64_t>(bitset_size)};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/parquet/io_utils/parquet_io_utils.cpp` around lines 407 - 410, The
bloom filter parsing in detail::parse_bloom_filter_header usage must validate
that the parsed bitset fits inside the supplied bloom_range before returning
offsets. In parquet_io_utils.cpp, update the logic around
header_info/header_size/bitset_size so it checks header_size + bitset_size <=
bloom_range.size() and rejects truncated or corrupt ranges via CUDF_EXPECTS
before constructing the returned range. Keep the fix localized to the bloom
filter range computation so callers of the bloom filter parsing path cannot read
past the provided range.

Source: Coding guidelines

};

// Parse each source's bloom filter headers with one task per source
auto const bitset_byte_ranges_per_source =
dispatch_fetch_tasks(num_sources, [&](std::size_t source_idx) {
auto const& bloom_ranges = bloom_filter_byte_ranges_per_source[source_idx];
auto& datasource = datasources[source_idx].get();
std::vector<cudf::io::text::byte_range_info> bitset_ranges;
bitset_ranges.reserve(bloom_ranges.size());
std::transform(
bloom_ranges.begin(),
bloom_ranges.end(),
std::back_inserter(bitset_ranges),
[&](auto const& bloom_range) { return fetch_bitset_range(datasource, bloom_range); });
return bitset_ranges;
});
Comment thread
qbacpey marked this conversation as resolved.
Resolved

// Fetch only the header-free bitsets to device
auto result =
fetch_byte_ranges_to_device_async(datasources, bitset_byte_ranges_per_source, stream, mr);

auto const& bitset_spans_per_source = std::get<1>(result);
CUDF_EXPECTS(std::all_of(bitset_spans_per_source.begin(),
bitset_spans_per_source.end(),
[](auto const& spans) {
return std::all_of(spans.begin(), spans.end(), [](auto const& span) {
return span.empty() or
(reinterpret_cast<std::uintptr_t>(span.data()) % 32) == 0;
});
}),
"Bloom filter bitset is not 32-byte aligned");
Comment thread
qbacpey marked this conversation as resolved.
Resolved

return result;
}

} // namespace

[[nodiscard]] std::size_t metadata_size_hint()
Expand Down Expand Up @@ -471,4 +539,29 @@ fetch_byte_ranges_to_device_async(
mr);
}

std::tuple<std::vector<rmm::device_buffer>,
std::vector<cudf::device_span<uint8_t const>>,
std::future<void>>
fetch_bloom_filters_to_device_async(
cudf::io::datasource& datasource,
cudf::host_span<cudf::io::text::byte_range_info const> bloom_filter_byte_ranges,
rmm::cuda_stream_view stream,
rmm::device_async_resource_ref mr)
{
CUDF_FUNC_RANGE();

// Wrap the inputs into arrays and delegate to the multi-source implementation
std::array<std::reference_wrapper<cudf::io::datasource>, 1> datasources{std::ref(datasource)};
std::array<cudf::host_span<cudf::io::text::byte_range_info const>, 1>
bloom_filter_byte_ranges_per_source{bloom_filter_byte_ranges};

auto [buffers, fetched_byte_ranges, fut] = fetch_bloom_filters_to_device_async_impl(
{datasources.data(), datasources.size()},
{bloom_filter_byte_ranges_per_source.data(), bloom_filter_byte_ranges_per_source.size()},
stream,
mr);

return {std::move(buffers), std::move(fetched_byte_ranges.front()), std::move(fut)};
}

} // namespace cudf::io::parquet
18 changes: 18 additions & 0 deletions cpp/src/io/parquet/reader_impl_helpers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

#include <cstddef>
#include <functional>
#include <optional>
#include <string>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>

namespace cudf::io::parquet::detail {
Expand Down Expand Up @@ -142,6 +144,22 @@ struct surviving_row_group_metrics {
std::optional<size_type> after_bloom_filter; // number of surviving row groups after bloom filter
};

/**
* @brief Upper bound on the size in bytes of a Parquet `BloomFilterHeader`
*/
inline constexpr int64_t bloom_filter_header_max_size = 256;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If the bloom_filter_length-absent case is common, we might want to use a large speculative read value, since a bloom filter would easily exceed 256 bytes. InfluxData reports real-world overhead of 2 KB – 8 KB per column per row group. By raising the speculative read size to cover that range, we could save 1 RTT, which would matter a lot for remote-IO scenarios.


/**
* @brief Parses and validates a Parquet `BloomFilterHeader` from the front of `bytes`
*
* @param bytes Host bytes starting at the beginning of a bloom filter (header followed by bitset)
*
* @return A pair of the bloom filter header size and the bitset size in bytes, or `std::nullopt`
* if the header is missing or unsupported
*/
[[nodiscard]] std::optional<std::pair<int64_t, std::size_t>> parse_bloom_filter_header(
Comment thread
qbacpey marked this conversation as resolved.
Resolved
host_span<uint8_t const> bytes);

class aggregate_reader_metadata {
protected:
std::vector<metadata> per_file_metadata;
Expand Down
11 changes: 4 additions & 7 deletions cpp/tests/io/experimental/hybrid_scan_composer.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -104,13 +104,10 @@ auto apply_hybrid_scan_filters(cudf::io::datasource& datasource,
std::vector<cudf::size_type> bloom_filtered_row_group_indices;
bloom_filtered_row_group_indices.reserve(current_row_group_indices.size());
if (bloom_filter_byte_ranges.size()) {
// Fetch 32 byte aligned bloom filter data buffers from the input file buffer
auto aligned_mr = rmm::mr::aligned_resource_adaptor(cudf::get_current_device_resource_ref(),
bloom_filter_alignment);

// Fetch the header-stripped, 32-byte-aligned bloom filter bitsets from the input file
auto [bloom_filter_buffers, bloom_filter_data, bloom_read_tasks] =
cudf::io::parquet::fetch_byte_ranges_to_device_async(
datasource, bloom_filter_byte_ranges, stream, aligned_mr);
cudf::io::parquet::fetch_bloom_filters_to_device_async(
datasource, bloom_filter_byte_ranges, stream, mr);
bloom_read_tasks.get();

// Filter row groups with bloom filters
Expand Down
Loading