From fe75cb8779b91f08f2ed410c0ad133b328939775 Mon Sep 17 00:00:00 2001 From: Tianyu Liu Date: Tue, 14 Jan 2025 15:15:11 -0500 Subject: [PATCH 1/2] Update kvikio call due to upstream changes (#17733) KvikIO has just introduced slightly breaking code changes (https://github.com/rapidsai/kvikio/pull/581) where the utility function `getenv_or` is moved out of the `detail` namespace and becomes part of the public API. This PR adapts cuDF for this change. This PR has been tested locally using the abovementioned KvikIO branch. Authors: - Tianyu Liu (https://github.com/kingcrimsontianyu) - GALI PREM SAGAR (https://github.com/galipremsagar) Approvers: - Devavret Makkar (https://github.com/devavret) - Bradley Dice (https://github.com/bdice) - Muhammad Haseeb (https://github.com/mhaseeb123) URL: https://github.com/rapidsai/cudf/pull/17733 --- cpp/src/io/utilities/config_utils.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cpp/src/io/utilities/config_utils.cpp b/cpp/src/io/utilities/config_utils.cpp index cea0ebad8f5..726feca328b 100644 --- a/cpp/src/io/utilities/config_utils.cpp +++ b/cpp/src/io/utilities/config_utils.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021-2024, NVIDIA CORPORATION. + * Copyright (c) 2021-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,8 +56,7 @@ void set_up_kvikio() { static std::once_flag flag{}; std::call_once(flag, [] { - auto const compat_mode = - kvikio::detail::getenv_or("KVIKIO_COMPAT_MODE", kvikio::CompatMode::ON); + auto const compat_mode = kvikio::getenv_or("KVIKIO_COMPAT_MODE", kvikio::CompatMode::ON); kvikio::defaults::compat_mode_reset(compat_mode); auto const nthreads = getenv_or("KVIKIO_NTHREADS", 4u); From 41215e254c6eac9f94a12fbeeccfeceaa4526200 Mon Sep 17 00:00:00 2001 From: Muhammad Haseeb <14217455+mhaseeb123@users.noreply.github.com> Date: Tue, 14 Jan 2025 12:35:15 -0800 Subject: [PATCH 2/2] Support reading bloom filters from Parquet files and filter row groups using them (#17289) This PR adds support to read bloom filters from Parquet files and use them to filter row groups based on `col == literal` like predicate(s), if provided. Related to #17164 Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) Approvers: - Yunsong Wang (https://github.com/PointKernel) - Vukasin Milovanovic (https://github.com/vuule) - Karthikeyan (https://github.com/karthikeyann) - Bradley Dice (https://github.com/bdice) URL: https://github.com/rapidsai/cudf/pull/17289 --- cpp/CMakeLists.txt | 1 + cpp/src/io/parquet/bloom_filter_reader.cu | 683 ++++++++++++++++++ .../io/parquet/compact_protocol_reader.cpp | 35 +- .../io/parquet/compact_protocol_reader.hpp | 6 +- cpp/src/io/parquet/parquet.hpp | 52 +- cpp/src/io/parquet/predicate_pushdown.cpp | 136 ++-- cpp/src/io/parquet/reader_impl_helpers.cpp | 5 +- cpp/src/io/parquet/reader_impl_helpers.hpp | 90 ++- cpp/src/io/parquet/reader_impl_preprocess.cu | 5 +- cpp/tests/CMakeLists.txt | 5 +- cpp/tests/io/parquet_bloom_filter_test.cu | 90 +++ ...d_ndv_100_bf_fpp0.1_nostats.snappy.parquet | Bin 0 -> 41508 bytes ...ed_card_ndv_100_chunk_stats.snappy.parquet | Bin 0 -> 37036 bytes ...d_ndv_500_bf_fpp0.1_nostats.snappy.parquet | Bin 0 -> 77985 bytes ...ed_card_ndv_500_chunk_stats.snappy.parquet | Bin 0 -> 58150 bytes python/cudf/cudf/tests/test_parquet.py | 57 +- 16 files changed, 1098 insertions(+), 67 deletions(-) create mode 100644 cpp/src/io/parquet/bloom_filter_reader.cu create mode 100644 cpp/tests/io/parquet_bloom_filter_test.cu create mode 100644 python/cudf/cudf/tests/data/parquet/mixed_card_ndv_100_bf_fpp0.1_nostats.snappy.parquet create mode 100644 python/cudf/cudf/tests/data/parquet/mixed_card_ndv_100_chunk_stats.snappy.parquet create mode 100644 python/cudf/cudf/tests/data/parquet/mixed_card_ndv_500_bf_fpp0.1_nostats.snappy.parquet create mode 100644 python/cudf/cudf/tests/data/parquet/mixed_card_ndv_500_chunk_stats.snappy.parquet diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 4d83cbd907c..354560998c5 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -516,6 +516,7 @@ add_library( src/datetime/timezone.cpp src/io/orc/writer_impl.cu src/io/parquet/arrow_schema_writer.cpp + src/io/parquet/bloom_filter_reader.cu src/io/parquet/compact_protocol_reader.cpp src/io/parquet/compact_protocol_writer.cpp src/io/parquet/decode_preprocess.cu diff --git a/cpp/src/io/parquet/bloom_filter_reader.cu b/cpp/src/io/parquet/bloom_filter_reader.cu new file mode 100644 index 00000000000..8c404950efa --- /dev/null +++ b/cpp/src/io/parquet/bloom_filter_reader.cu @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "compact_protocol_reader.hpp" +#include "io/parquet/parquet.hpp" +#include "reader_impl_helpers.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +namespace cudf::io::parquet::detail { +namespace { + +/** + * @brief Converts bloom filter membership results (for each column chunk) to a device column. + * + */ +struct bloom_filter_caster { + cudf::device_span const> bloom_filter_spans; + host_span parquet_types; + size_t total_row_groups; + size_t num_equality_columns; + + enum class is_int96_timestamp : bool { YES, NO }; + + template + std::unique_ptr query_bloom_filter(cudf::size_type equality_col_idx, + cudf::data_type dtype, + ast::literal const* const literal, + rmm::cuda_stream_view stream) const + { + using key_type = T; + using policy_type = cuco::arrow_filter_policy; + using word_type = typename policy_type::word_type; + + // Boolean, List, Struct, Dictionary types are not supported + if constexpr (std::is_same_v or + (cudf::is_compound() and not std::is_same_v)) { + CUDF_FAIL("Bloom filters do not support boolean or compound types"); + } else { + // Check if the literal has the same type as the predicate column + CUDF_EXPECTS( + dtype == literal->get_data_type() and + cudf::have_same_types( + cudf::column_view{dtype, 0, {}, {}, 0, 0, {}}, + cudf::scalar_type_t(T{}, false, stream, cudf::get_current_device_resource_ref())), + "Mismatched predicate column and literal types"); + } + + // Filter properties + auto constexpr bytes_per_block = sizeof(word_type) * policy_type::words_per_block; + + rmm::device_buffer results{total_row_groups, stream, cudf::get_current_device_resource_ref()}; + cudf::device_span results_span{static_cast(results.data()), total_row_groups}; + + // Query literal in bloom filters from each column chunk (row group). + thrust::tabulate( + rmm::exec_policy_nosync(stream), + results_span.begin(), + results_span.end(), + [filter_span = bloom_filter_spans.data(), + d_scalar = literal->get_value(), + col_idx = equality_col_idx, + num_equality_columns = num_equality_columns] __device__(auto row_group_idx) { + // Filter bitset buffer index + auto const filter_idx = col_idx + (num_equality_columns * row_group_idx); + auto const filter_size = filter_span[filter_idx].size(); + + // If no bloom filter, then fill in `true` as membership cannot be determined + if (filter_size == 0) { return true; } + + // Number of filter blocks + auto const num_filter_blocks = filter_size / bytes_per_block; + + // Create a bloom filter view. + cuco::bloom_filter_ref, + cuco::thread_scope_thread, + policy_type> + filter{reinterpret_cast(filter_span[filter_idx].data()), + num_filter_blocks, + {}, // Thread scope as the same literal is being searched across different bitsets + // per thread + {}}; // Arrow policy with cudf::hashing::detail::XXHash_64 seeded with 0 for Arrow + // compatibility + + // If int96_timestamp type, convert literal to string_view and query bloom + // filter + if constexpr (cuda::std::is_same_v and + IS_INT96_TIMESTAMP == is_int96_timestamp::YES) { + auto const int128_key = static_cast<__int128_t>(d_scalar.value()); + cudf::string_view probe_key{reinterpret_cast(&int128_key), 12}; + return filter.contains(probe_key); + } else { + // Query the bloom filter and store results + return filter.contains(d_scalar.value()); + } + }); + + return std::make_unique(cudf::data_type{cudf::type_id::BOOL8}, + static_cast(total_row_groups), + std::move(results), + rmm::device_buffer{}, + 0); + } + + // Creates device columns from bloom filter membership + template + std::unique_ptr operator()(cudf::size_type equality_col_idx, + cudf::data_type dtype, + ast::literal* const literal, + rmm::cuda_stream_view stream) const + { + // For INT96 timestamps, use cudf::string_view type and set is_int96_timestamp to YES + if constexpr (cudf::is_timestamp()) { + if (parquet_types[equality_col_idx] == Type::INT96) { + // For INT96 timestamps, use cudf::string_view type and set is_int96_timestamp to YES + return query_bloom_filter( + equality_col_idx, dtype, literal, stream); + } + } + + // For all other cases + return query_bloom_filter(equality_col_idx, dtype, literal, stream); + } +}; + +/** + * @brief Collects lists of equality predicate literals in the AST expression, one list per input + * table column. This is used in row group filtering based on bloom filters. + */ +class equality_literals_collector : public ast::detail::expression_transformer { + public: + equality_literals_collector() = default; + + equality_literals_collector(ast::expression const& expr, cudf::size_type num_input_columns) + : _num_input_columns{num_input_columns} + { + _equality_literals.resize(_num_input_columns); + expr.accept(*this); + } + + /** + * @copydoc ast::detail::expression_transformer::visit(ast::literal const& ) + */ + std::reference_wrapper visit(ast::literal const& expr) override + { + return expr; + } + + /** + * @copydoc ast::detail::expression_transformer::visit(ast::column_reference const& ) + */ + std::reference_wrapper visit(ast::column_reference const& expr) override + { + CUDF_EXPECTS(expr.get_table_source() == ast::table_reference::LEFT, + "BloomfilterAST supports only left table"); + CUDF_EXPECTS(expr.get_column_index() < _num_input_columns, + "Column index cannot be more than number of columns in the table"); + return expr; + } + + /** + * @copydoc ast::detail::expression_transformer::visit(ast::column_name_reference const& ) + */ + std::reference_wrapper visit( + ast::column_name_reference const& expr) override + { + CUDF_FAIL("Column name reference is not supported in BloomfilterAST"); + } + + /** + * @copydoc ast::detail::expression_transformer::visit(ast::operation const& ) + */ + std::reference_wrapper visit(ast::operation const& expr) override + { + using cudf::ast::ast_operator; + auto const operands = expr.get_operands(); + auto const op = expr.get_operator(); + + if (auto* v = dynamic_cast(&operands[0].get())) { + // First operand should be column reference, second should be literal. + CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2, + "Only binary operations are supported on column reference"); + auto const literal_ptr = dynamic_cast(&operands[1].get()); + CUDF_EXPECTS(literal_ptr != nullptr, + "Second operand of binary operation with column reference must be a literal"); + v->accept(*this); + + // Push to the corresponding column's literals list iff equality predicate is seen + if (op == ast_operator::EQUAL) { + auto const col_idx = v->get_column_index(); + _equality_literals[col_idx].emplace_back(const_cast(literal_ptr)); + } + } else { + // Just visit the operands and ignore any output + std::ignore = visit_operands(operands); + } + + return expr; + } + + /** + * @brief Vectors of equality literals in the AST expression, one per input table column + * + * @return Vectors of equality literals, one per input table column + */ + [[nodiscard]] std::vector> get_equality_literals() && + { + return std::move(_equality_literals); + } + + private: + std::vector> _equality_literals; + + protected: + std::vector> visit_operands( + cudf::host_span const> operands) + { + std::vector> transformed_operands; + for (auto const& operand : operands) { + auto const new_operand = operand.get().accept(*this); + transformed_operands.push_back(new_operand); + } + return transformed_operands; + } + size_type _num_input_columns; +}; + +/** + * @brief Converts AST expression to bloom filter membership (BloomfilterAST) expression. + * This is used in row group filtering based on equality predicate. + */ +class bloom_filter_expression_converter : public equality_literals_collector { + public: + bloom_filter_expression_converter( + ast::expression const& expr, + size_type num_input_columns, + cudf::host_span const> equality_literals) + : _equality_literals{equality_literals} + { + // Set the num columns + _num_input_columns = num_input_columns; + + // Compute and store columns literals offsets + _col_literals_offsets.reserve(_num_input_columns + 1); + _col_literals_offsets.emplace_back(0); + + std::transform(equality_literals.begin(), + equality_literals.end(), + std::back_inserter(_col_literals_offsets), + [&](auto const& col_literal_map) { + return _col_literals_offsets.back() + + static_cast(col_literal_map.size()); + }); + + // Add this visitor + expr.accept(*this); + } + + /** + * @brief Delete equality literals getter as it's not needed in the derived class + */ + [[nodiscard]] std::vector> get_equality_literals() && = delete; + + // Bring all overloads of `visit` from equality_predicate_collector into scope + using equality_literals_collector::visit; + + /** + * @copydoc ast::detail::expression_transformer::visit(ast::operation const& ) + */ + std::reference_wrapper visit(ast::operation const& expr) override + { + using cudf::ast::ast_operator; + auto const operands = expr.get_operands(); + auto const op = expr.get_operator(); + + if (auto* v = dynamic_cast(&operands[0].get())) { + // First operand should be column reference, second should be literal. + CUDF_EXPECTS(cudf::ast::detail::ast_operator_arity(op) == 2, + "Only binary operations are supported on column reference"); + CUDF_EXPECTS(dynamic_cast(&operands[1].get()) != nullptr, + "Second operand of binary operation with column reference must be a literal"); + v->accept(*this); + + if (op == ast_operator::EQUAL) { + // Search the literal in this input column's equality literals list and add to the offset. + auto const col_idx = v->get_column_index(); + auto const& equality_literals = _equality_literals[col_idx]; + auto col_literal_offset = _col_literals_offsets[col_idx]; + auto const literal_iter = std::find(equality_literals.cbegin(), + equality_literals.cend(), + dynamic_cast(&operands[1].get())); + CUDF_EXPECTS(literal_iter != equality_literals.end(), "Could not find the literal ptr"); + col_literal_offset += std::distance(equality_literals.cbegin(), literal_iter); + + // Evaluate boolean is_true(value) expression as NOT(NOT(value)) + auto const& value = _bloom_filter_expr.push(ast::column_reference{col_literal_offset}); + _bloom_filter_expr.push(ast::operation{ + ast_operator::NOT, _bloom_filter_expr.push(ast::operation{ast_operator::NOT, value})}); + } + // For all other expressions, push an always true expression + else { + _bloom_filter_expr.push( + ast::operation{ast_operator::NOT, + _bloom_filter_expr.push(ast::operation{ast_operator::NOT, _always_true})}); + } + } else { + auto new_operands = visit_operands(operands); + if (cudf::ast::detail::ast_operator_arity(op) == 2) { + _bloom_filter_expr.push(ast::operation{op, new_operands.front(), new_operands.back()}); + } else if (cudf::ast::detail::ast_operator_arity(op) == 1) { + _bloom_filter_expr.push(ast::operation{op, new_operands.front()}); + } + } + return _bloom_filter_expr.back(); + } + + /** + * @brief Returns the AST to apply on bloom filter membership. + * + * @return AST operation expression + */ + [[nodiscard]] std::reference_wrapper get_bloom_filter_expr() const + { + return _bloom_filter_expr.back(); + } + + private: + std::vector _col_literals_offsets; + cudf::host_span const> _equality_literals; + ast::tree _bloom_filter_expr; + cudf::numeric_scalar _always_true_scalar{true}; + ast::literal const _always_true{_always_true_scalar}; +}; + +/** + * @brief Reads bloom filter data to device. + * + * @param sources Dataset sources + * @param num_chunks Number of total column chunks to read + * @param bloom_filter_data Device buffers to hold bloom filter bitsets for each chunk + * @param bloom_filter_offsets Bloom filter offsets for all chunks + * @param bloom_filter_sizes Bloom filter sizes for all chunks + * @param chunk_source_map Association between each column chunk and its source + * @param stream CUDA stream used for device memory operations and kernel launches + */ +void read_bloom_filter_data(host_span const> sources, + size_t num_chunks, + cudf::host_span bloom_filter_data, + cudf::host_span> bloom_filter_offsets, + cudf::host_span> bloom_filter_sizes, + std::vector const& chunk_source_map, + rmm::cuda_stream_view stream) +{ + // Read tasks for bloom filter data + std::vector> read_tasks; + + // Read bloom filters for all column chunks + std::for_each( + thrust::counting_iterator(0), + thrust::counting_iterator(num_chunks), + [&](auto const chunk) { + // If bloom filter offset absent, fill in an empty buffer and skip ahead + if (not bloom_filter_offsets[chunk].has_value()) { + bloom_filter_data[chunk] = {}; + return; + } + // Read bloom filter iff present + auto const bloom_filter_offset = bloom_filter_offsets[chunk].value(); + + // 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; + auto const initial_read_size = + static_cast(bloom_filter_sizes[chunk].value_or(bloom_filter_size_guess)); + + // 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); + + // Get the hardcoded words_per_block value from `cuco::arrow_filter_policy` using a temporary + // `std::byte` key type. + auto constexpr words_per_block = + cuco::arrow_filter_policy::words_per_block; + + // 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::Compression::UNCOMPRESSED and + header.algorithm.algorithm == BloomFilterAlgorithm::Algorithm::SPLIT_BLOCK and + header.hash.hash == BloomFilterHash::Hash::XXHASH; + + // Do not read if the bloom filter is invalid + if (not is_header_valid) { + 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(cp.bytecount()); + auto const bitset_size = static_cast(header.num_bytes); + + // Check if we already read in the filter bitset in the initial read. + if (initial_read_size >= bloom_filter_header_size + bitset_size) { + bloom_filter_data[chunk] = + rmm::device_buffer{buffer->data() + bloom_filter_header_size, bitset_size, stream}; + } + // Read the bitset from datasource. + else { + auto const bitset_offset = bloom_filter_offset + bloom_filter_header_size; + // Directly read to device if preferred + if (source->is_device_read_preferred(bitset_size)) { + bloom_filter_data[chunk] = rmm::device_buffer{bitset_size, stream}; + auto future_read_size = + source->device_read_async(bitset_offset, + bitset_size, + static_cast(bloom_filter_data[chunk].data()), + stream); + + read_tasks.emplace_back(std::move(future_read_size)); + } else { + buffer = source->host_read(bitset_offset, bitset_size); + bloom_filter_data[chunk] = rmm::device_buffer{buffer->data(), buffer->size(), stream}; + } + } + }); + + // Read task sync function + for (auto& task : read_tasks) { + task.wait(); + } +} + +} // namespace + +std::vector aggregate_reader_metadata::read_bloom_filters( + host_span const> sources, + host_span const> row_group_indices, + host_span column_schemas, + size_type total_row_groups, + rmm::cuda_stream_view stream) const +{ + // Descriptors for all the chunks that make up the selected columns + auto const num_input_columns = column_schemas.size(); + auto const num_chunks = total_row_groups * num_input_columns; + + // Association between each column chunk and its source + std::vector chunk_source_map(num_chunks); + + // Keep track of column chunk file offsets + std::vector> bloom_filter_offsets(num_chunks); + std::vector> bloom_filter_sizes(num_chunks); + + // Gather all bloom filter offsets and sizes. + size_type chunk_count = 0; + + // Flag to check if we have at least one valid bloom filter offset + auto have_bloom_filters = false; + + // For all data sources + std::for_each(thrust::counting_iterator(0), + thrust::counting_iterator(row_group_indices.size()), + [&](auto const src_index) { + // Get all row group indices in the data source + auto const& rg_indices = row_group_indices[src_index]; + // For all row groups + std::for_each(rg_indices.cbegin(), rg_indices.cend(), [&](auto const rg_index) { + // For all column chunks + std::for_each( + column_schemas.begin(), column_schemas.end(), [&](auto const schema_idx) { + auto& col_meta = get_column_metadata(rg_index, src_index, schema_idx); + + // Get bloom filter offsets and sizes + bloom_filter_offsets[chunk_count] = col_meta.bloom_filter_offset; + bloom_filter_sizes[chunk_count] = col_meta.bloom_filter_length; + + // Set `have_bloom_filters` if `bloom_filter_offset` is valid + if (col_meta.bloom_filter_offset.has_value()) { have_bloom_filters = true; } + + // Map each column chunk to its source index + chunk_source_map[chunk_count] = src_index; + chunk_count++; + }); + }); + }); + + // Exit early if we don't have any bloom filters + if (not have_bloom_filters) { return {}; } + + // Vector to hold bloom filter data + std::vector bloom_filter_data(num_chunks); + + // Read bloom filter data + read_bloom_filter_data(sources, + num_chunks, + bloom_filter_data, + bloom_filter_offsets, + bloom_filter_sizes, + chunk_source_map, + stream); + + // Return bloom filter data + return bloom_filter_data; +} + +std::vector aggregate_reader_metadata::get_parquet_types( + host_span const> row_group_indices, + host_span column_schemas) const +{ + std::vector parquet_types(column_schemas.size()); + // Find a source with at least one row group + auto const src_iter = std::find_if(row_group_indices.begin(), + row_group_indices.end(), + [](auto const& rg) { return rg.size() > 0; }); + CUDF_EXPECTS(src_iter != row_group_indices.end(), ""); + + // Source index + auto const src_index = std::distance(row_group_indices.begin(), src_iter); + std::transform(column_schemas.begin(), + column_schemas.end(), + parquet_types.begin(), + [&](auto const schema_idx) { + // Use the first row group in this source + auto constexpr row_group_index = 0; + return get_column_metadata(row_group_index, src_index, schema_idx).type; + }); + + return parquet_types; +} + +std::optional>> aggregate_reader_metadata::apply_bloom_filters( + host_span const> sources, + host_span const> input_row_group_indices, + host_span output_dtypes, + host_span output_column_schemas, + std::reference_wrapper filter, + rmm::cuda_stream_view stream) const +{ + // Number of input table columns + auto const num_input_columns = static_cast(output_dtypes.size()); + + // Total number of row groups after StatsAST filtration + auto const total_row_groups = std::accumulate( + input_row_group_indices.begin(), + input_row_group_indices.end(), + size_t{0}, + [](size_t sum, auto const& per_file_row_groups) { return sum + per_file_row_groups.size(); }); + + // Check if we have less than 2B total row groups. + CUDF_EXPECTS(total_row_groups <= std::numeric_limits::max(), + "Total number of row groups exceed the size_type's limit"); + + // Collect equality literals for each input table column + auto const equality_literals = + equality_literals_collector{filter.get(), num_input_columns}.get_equality_literals(); + + // Collect schema indices of columns with equality predicate(s) + std::vector equality_col_schemas; + thrust::copy_if(thrust::host, + output_column_schemas.begin(), + output_column_schemas.end(), + equality_literals.begin(), + std::back_inserter(equality_col_schemas), + [](auto& eq_literals) { return not eq_literals.empty(); }); + + // Return early if no column with equality predicate(s) + if (equality_col_schemas.empty()) { return std::nullopt; } + + // Read a vector of bloom filter bitset device buffers for all columns with equality + // predicate(s) across all row groups + auto bloom_filter_data = read_bloom_filters( + sources, input_row_group_indices, equality_col_schemas, total_row_groups, stream); + + // No bloom filter buffers, return the original row group indices + if (bloom_filter_data.empty()) { return std::nullopt; } + + // Get parquet types for the predicate columns + auto const parquet_types = get_parquet_types(input_row_group_indices, equality_col_schemas); + + // Create spans from bloom filter bitset buffers to use in cuco::bloom_filter_ref. + std::vector> h_bloom_filter_spans; + h_bloom_filter_spans.reserve(bloom_filter_data.size()); + std::transform(bloom_filter_data.begin(), + bloom_filter_data.end(), + std::back_inserter(h_bloom_filter_spans), + [&](auto& buffer) { + return cudf::device_span{ + static_cast(buffer.data()), buffer.size()}; + }); + + // Copy bloom filter bitset spans to device + auto const bloom_filter_spans = cudf::detail::make_device_uvector_async( + h_bloom_filter_spans, stream, cudf::get_current_device_resource_ref()); + + // Create a bloom filter query table caster + bloom_filter_caster const bloom_filter_col{ + bloom_filter_spans, parquet_types, total_row_groups, equality_col_schemas.size()}; + + // Converts bloom filter membership for equality predicate columns to a table + // containing a column for each `col[i] == literal` predicate to be evaluated. + // The table contains #sources * #column_chunks_per_src rows. + std::vector> bloom_filter_membership_columns; + size_t equality_col_idx = 0; + std::for_each( + thrust::counting_iterator(0), + thrust::counting_iterator(output_dtypes.size()), + [&](auto input_col_idx) { + auto const& dtype = output_dtypes[input_col_idx]; + + // Skip if no equality literals for this column + if (equality_literals[input_col_idx].empty()) { return; } + + // Skip if non-comparable (compound) type except string + if (cudf::is_compound(dtype) and dtype.id() != cudf::type_id::STRING) { return; } + + // Add a column for all literals associated with an equality column + for (auto const& literal : equality_literals[input_col_idx]) { + bloom_filter_membership_columns.emplace_back(cudf::type_dispatcher( + dtype, bloom_filter_col, equality_col_idx, dtype, literal, stream)); + } + equality_col_idx++; + }); + + // Create a table from columns + auto bloom_filter_membership_table = cudf::table(std::move(bloom_filter_membership_columns)); + + // Convert AST to BloomfilterAST expression with reference to bloom filter membership + // in above `bloom_filter_membership_table` + bloom_filter_expression_converter bloom_filter_expr{ + filter.get(), num_input_columns, {equality_literals}}; + + // Filter bloom filter membership table with the BloomfilterAST expression and collect + // filtered row group indices + return collect_filtered_row_group_indices(bloom_filter_membership_table, + bloom_filter_expr.get_bloom_filter_expr(), + input_row_group_indices, + stream); +} + +} // namespace cudf::io::parquet::detail diff --git a/cpp/src/io/parquet/compact_protocol_reader.cpp b/cpp/src/io/parquet/compact_protocol_reader.cpp index f1ecf66c29f..b8e72aaac88 100644 --- a/cpp/src/io/parquet/compact_protocol_reader.cpp +++ b/cpp/src/io/parquet/compact_protocol_reader.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024, NVIDIA CORPORATION. + * Copyright (c) 2018-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -658,6 +658,33 @@ void CompactProtocolReader::read(ColumnChunk* c) function_builder(this, op); } +void CompactProtocolReader::read(BloomFilterAlgorithm* alg) +{ + auto op = std::make_tuple(parquet_field_union_enumerator(1, alg->algorithm)); + function_builder(this, op); +} + +void CompactProtocolReader::read(BloomFilterHash* hash) +{ + auto op = std::make_tuple(parquet_field_union_enumerator(1, hash->hash)); + function_builder(this, op); +} + +void CompactProtocolReader::read(BloomFilterCompression* comp) +{ + auto op = std::make_tuple(parquet_field_union_enumerator(1, comp->compression)); + function_builder(this, op); +} + +void CompactProtocolReader::read(BloomFilterHeader* bf) +{ + auto op = std::make_tuple(parquet_field_int32(1, bf->num_bytes), + parquet_field_struct(2, bf->algorithm), + parquet_field_struct(3, bf->hash), + parquet_field_struct(4, bf->compression)); + function_builder(this, op); +} + void CompactProtocolReader::read(ColumnChunkMetaData* c) { using optional_size_statistics = @@ -665,7 +692,9 @@ void CompactProtocolReader::read(ColumnChunkMetaData* c) using optional_list_enc_stats = parquet_field_optional, parquet_field_struct_list>; - auto op = std::make_tuple(parquet_field_enum(1, c->type), + using optional_i64 = parquet_field_optional; + using optional_i32 = parquet_field_optional; + auto op = std::make_tuple(parquet_field_enum(1, c->type), parquet_field_enum_list(2, c->encodings), parquet_field_string_list(3, c->path_in_schema), parquet_field_enum(4, c->codec), @@ -677,6 +706,8 @@ void CompactProtocolReader::read(ColumnChunkMetaData* c) parquet_field_int64(11, c->dictionary_page_offset), parquet_field_struct(12, c->statistics), optional_list_enc_stats(13, c->encoding_stats), + optional_i64(14, c->bloom_filter_offset), + optional_i32(15, c->bloom_filter_length), optional_size_statistics(16, c->size_statistics)); function_builder(this, op); } diff --git a/cpp/src/io/parquet/compact_protocol_reader.hpp b/cpp/src/io/parquet/compact_protocol_reader.hpp index b87f2e9c692..360197b19ad 100644 --- a/cpp/src/io/parquet/compact_protocol_reader.hpp +++ b/cpp/src/io/parquet/compact_protocol_reader.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024, NVIDIA CORPORATION. + * Copyright (c) 2018-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -108,6 +108,10 @@ class CompactProtocolReader { void read(IntType* t); void read(RowGroup* r); void read(ColumnChunk* c); + void read(BloomFilterAlgorithm* bf); + void read(BloomFilterHash* bf); + void read(BloomFilterCompression* bf); + void read(BloomFilterHeader* bf); void read(ColumnChunkMetaData* c); void read(PageHeader* p); void read(DataPageHeader* d); diff --git a/cpp/src/io/parquet/parquet.hpp b/cpp/src/io/parquet/parquet.hpp index 2851ef67a65..dc0c4b1540e 100644 --- a/cpp/src/io/parquet/parquet.hpp +++ b/cpp/src/io/parquet/parquet.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2024, NVIDIA CORPORATION. + * Copyright (c) 2018-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -382,12 +382,62 @@ struct ColumnChunkMetaData { // Set of all encodings used for pages in this column chunk. This information can be used to // determine if all data pages are dictionary encoded for example. std::optional> encoding_stats; + // Byte offset from beginning of file to Bloom filter data. + std::optional bloom_filter_offset; + // Size of Bloom filter data including the serialized header, in bytes. Added in 2.10 so readers + // may not read this field from old files and it can be obtained after the BloomFilterHeader has + // been deserialized. Writers should write this field so readers can read the bloom filter in a + // single I/O. + std::optional bloom_filter_length; // Optional statistics to help estimate total memory when converted to in-memory representations. // The histograms contained in these statistics can also be useful in some cases for more // fine-grained nullability/list length filter pushdown. std::optional size_statistics; }; +/** + * @brief The algorithm used in bloom filter + */ +struct BloomFilterAlgorithm { + // Block-based Bloom filter. + enum class Algorithm { UNDEFINED, SPLIT_BLOCK }; + Algorithm algorithm{Algorithm::SPLIT_BLOCK}; +}; + +/** + * @brief The hash function used in Bloom filter + */ +struct BloomFilterHash { + // xxHash_64 + enum class Hash { UNDEFINED, XXHASH }; + Hash hash{Hash::XXHASH}; +}; + +/** + * @brief The compression used in the bloom filter + */ +struct BloomFilterCompression { + enum class Compression { UNDEFINED, UNCOMPRESSED }; + Compression compression{Compression::UNCOMPRESSED}; +}; + +/** + * @brief Bloom filter header struct + * + * The bloom filter data of a column chunk stores this header at the beginning + * following by the filter bitset. + */ +struct BloomFilterHeader { + // The size of bitset in bytes + int32_t num_bytes; + // The algorithm for setting bits + BloomFilterAlgorithm algorithm; + // The hash function used for bloom filter + BloomFilterHash hash; + // The compression used in the bloom filter + BloomFilterCompression compression; +}; + /** * @brief Thrift-derived struct describing a chunk of data for a particular * column diff --git a/cpp/src/io/parquet/predicate_pushdown.cpp b/cpp/src/io/parquet/predicate_pushdown.cpp index 9047ff9169b..0e307bac097 100644 --- a/cpp/src/io/parquet/predicate_pushdown.cpp +++ b/cpp/src/io/parquet/predicate_pushdown.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024, NVIDIA CORPORATION. + * Copyright (c) 2023-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,7 @@ #include #include +#include #include #include #include @@ -388,6 +389,7 @@ class stats_expression_converter : public ast::detail::expression_transformer { } // namespace std::optional>> aggregate_reader_metadata::filter_row_groups( + host_span const> sources, host_span const> row_group_indices, host_span output_dtypes, host_span output_column_schemas, @@ -396,7 +398,6 @@ std::optional>> aggregate_reader_metadata::fi { auto mr = cudf::get_current_device_resource_ref(); // Create row group indices. - std::vector> filtered_row_group_indices; std::vector> all_row_group_indices; host_span const> input_row_group_indices; if (row_group_indices.empty()) { @@ -412,18 +413,22 @@ std::optional>> aggregate_reader_metadata::fi } else { input_row_group_indices = row_group_indices; } - auto const total_row_groups = std::accumulate(input_row_group_indices.begin(), - input_row_group_indices.end(), - 0, - [](size_type sum, auto const& per_file_row_groups) { - return sum + per_file_row_groups.size(); - }); + auto const total_row_groups = std::accumulate( + input_row_group_indices.begin(), + input_row_group_indices.end(), + size_t{0}, + [](size_t sum, auto const& per_file_row_groups) { return sum + per_file_row_groups.size(); }); + + // Check if we have less than 2B total row groups. + CUDF_EXPECTS(total_row_groups <= std::numeric_limits::max(), + "Total number of row groups exceed the size_type's limit"); // Converts Column chunk statistics to a table // where min(col[i]) = columns[i*2], max(col[i])=columns[i*2+1] // For each column, it contains #sources * #column_chunks_per_src rows. std::vector> columns; - stats_caster const stats_col{total_row_groups, per_file_metadata, input_row_group_indices}; + stats_caster const stats_col{ + static_cast(total_row_groups), per_file_metadata, input_row_group_indices}; for (size_t col_idx = 0; col_idx < output_dtypes.size(); col_idx++) { auto const schema_idx = output_column_schemas[col_idx]; auto const& dtype = output_dtypes[col_idx]; @@ -452,44 +457,23 @@ std::optional>> aggregate_reader_metadata::fi CUDF_EXPECTS(predicate.type().id() == cudf::type_id::BOOL8, "Filter expression must return a boolean column"); - auto const host_bitmask = [&] { - auto const num_bitmasks = num_bitmask_words(predicate.size()); - if (predicate.nullable()) { - return cudf::detail::make_host_vector_sync( - device_span(predicate.null_mask(), num_bitmasks), stream); - } else { - auto bitmask = cudf::detail::make_host_vector(num_bitmasks, stream); - std::fill(bitmask.begin(), bitmask.end(), ~bitmask_type{0}); - return bitmask; - } - }(); + // Filter stats table with StatsAST expression and collect filtered row group indices + auto const filtered_row_group_indices = collect_filtered_row_group_indices( + stats_table, stats_expr.get_stats_expr(), input_row_group_indices, stream); - auto validity_it = cudf::detail::make_counting_transform_iterator( - 0, [bitmask = host_bitmask.data()](auto bit_index) { return bit_is_set(bitmask, bit_index); }); + // Span of row groups to apply bloom filtering on. + auto const bloom_filter_input_row_groups = + filtered_row_group_indices.has_value() + ? host_span const>(filtered_row_group_indices.value()) + : input_row_group_indices; - auto const is_row_group_required = cudf::detail::make_host_vector_sync( - device_span(predicate.data(), predicate.size()), stream); + // Apply bloom filtering on the bloom filter input row groups + auto const bloom_filtered_row_groups = apply_bloom_filters( + sources, bloom_filter_input_row_groups, output_dtypes, output_column_schemas, filter, stream); - // Return only filtered row groups based on predicate - // if all are required or all are nulls, return. - if (std::all_of(is_row_group_required.cbegin(), - is_row_group_required.cend(), - [](auto i) { return bool(i); }) or - predicate.null_count() == predicate.size()) { - return std::nullopt; - } - size_type is_required_idx = 0; - for (auto const& input_row_group_index : input_row_group_indices) { - std::vector filtered_row_groups; - for (auto const rg_idx : input_row_group_index) { - if ((!validity_it[is_required_idx]) || is_row_group_required[is_required_idx]) { - filtered_row_groups.push_back(rg_idx); - } - ++is_required_idx; - } - filtered_row_group_indices.push_back(std::move(filtered_row_groups)); - } - return {std::move(filtered_row_group_indices)}; + // Return bloom filtered row group indices iff collected + return bloom_filtered_row_groups.has_value() ? bloom_filtered_row_groups + : filtered_row_group_indices; } // convert column named expression to column index reference expression @@ -510,14 +494,14 @@ named_to_reference_converter::named_to_reference_converter( std::reference_wrapper named_to_reference_converter::visit( ast::literal const& expr) { - _stats_expr = std::reference_wrapper(expr); + _converted_expr = std::reference_wrapper(expr); return expr; } std::reference_wrapper named_to_reference_converter::visit( ast::column_reference const& expr) { - _stats_expr = std::reference_wrapper(expr); + _converted_expr = std::reference_wrapper(expr); return expr; } @@ -531,7 +515,7 @@ std::reference_wrapper named_to_reference_converter::visi } auto col_index = col_index_it->second; _col_ref.emplace_back(col_index); - _stats_expr = std::reference_wrapper(_col_ref.back()); + _converted_expr = std::reference_wrapper(_col_ref.back()); return std::reference_wrapper(_col_ref.back()); } @@ -546,7 +530,7 @@ std::reference_wrapper named_to_reference_converter::visi } else if (cudf::ast::detail::ast_operator_arity(op) == 1) { _operators.emplace_back(op, new_operands.front()); } - _stats_expr = std::reference_wrapper(_operators.back()); + _converted_expr = std::reference_wrapper(_operators.back()); return std::reference_wrapper(_operators.back()); } @@ -640,4 +624,60 @@ class names_from_expression : public ast::detail::expression_transformer { return names_from_expression(expr, skip_names).to_vector(); } +std::optional>> collect_filtered_row_group_indices( + cudf::table_view table, + std::reference_wrapper ast_expr, + host_span const> input_row_group_indices, + rmm::cuda_stream_view stream) +{ + // Filter the input table using AST expression + auto predicate_col = cudf::detail::compute_column( + table, ast_expr.get(), stream, cudf::get_current_device_resource_ref()); + auto predicate = predicate_col->view(); + CUDF_EXPECTS(predicate.type().id() == cudf::type_id::BOOL8, + "Filter expression must return a boolean column"); + + auto const host_bitmask = [&] { + auto const num_bitmasks = num_bitmask_words(predicate.size()); + if (predicate.nullable()) { + return cudf::detail::make_host_vector_sync( + device_span(predicate.null_mask(), num_bitmasks), stream); + } else { + auto bitmask = cudf::detail::make_host_vector(num_bitmasks, stream); + std::fill(bitmask.begin(), bitmask.end(), ~bitmask_type{0}); + return bitmask; + } + }(); + + auto validity_it = cudf::detail::make_counting_transform_iterator( + 0, [bitmask = host_bitmask.data()](auto bit_index) { return bit_is_set(bitmask, bit_index); }); + + // Return only filtered row groups based on predicate + auto const is_row_group_required = cudf::detail::make_host_vector_sync( + device_span(predicate.data(), predicate.size()), stream); + + // Return if all are required, or all are nulls. + if (predicate.null_count() == predicate.size() or std::all_of(is_row_group_required.cbegin(), + is_row_group_required.cend(), + [](auto i) { return bool(i); })) { + return std::nullopt; + } + + // Collect indices of the filtered row groups + size_type is_required_idx = 0; + std::vector> filtered_row_group_indices; + for (auto const& input_row_group_index : input_row_group_indices) { + std::vector filtered_row_groups; + for (auto const rg_idx : input_row_group_index) { + if ((!validity_it[is_required_idx]) || is_row_group_required[is_required_idx]) { + filtered_row_groups.push_back(rg_idx); + } + ++is_required_idx; + } + filtered_row_group_indices.push_back(std::move(filtered_row_groups)); + } + + return {filtered_row_group_indices}; +} + } // namespace cudf::io::parquet::detail diff --git a/cpp/src/io/parquet/reader_impl_helpers.cpp b/cpp/src/io/parquet/reader_impl_helpers.cpp index 0dd1aff41e9..25baa1e0ec8 100644 --- a/cpp/src/io/parquet/reader_impl_helpers.cpp +++ b/cpp/src/io/parquet/reader_impl_helpers.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1030,6 +1030,7 @@ std::vector aggregate_reader_metadata::get_pandas_index_names() con std::tuple, std::vector> aggregate_reader_metadata::select_row_groups( + host_span const> sources, host_span const> row_group_indices, int64_t skip_rows_opt, std::optional const& num_rows_opt, @@ -1042,7 +1043,7 @@ aggregate_reader_metadata::select_row_groups( // if filter is not empty, then gather row groups to read after predicate pushdown if (filter.has_value()) { filtered_row_group_indices = filter_row_groups( - row_group_indices, output_dtypes, output_column_schemas, filter.value(), stream); + sources, row_group_indices, output_dtypes, output_column_schemas, filter.value(), stream); if (filtered_row_group_indices.has_value()) { row_group_indices = host_span const>(filtered_row_group_indices.value()); diff --git a/cpp/src/io/parquet/reader_impl_helpers.hpp b/cpp/src/io/parquet/reader_impl_helpers.hpp index fd692c0cdd6..a28ce616e2c 100644 --- a/cpp/src/io/parquet/reader_impl_helpers.hpp +++ b/cpp/src/io/parquet/reader_impl_helpers.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -195,6 +195,38 @@ class aggregate_reader_metadata { */ void column_info_for_row_group(row_group_info& rg_info, size_type chunk_start_row) const; + /** + * @brief Reads bloom filter bitsets for the specified columns from the given lists of row + * groups. + * + * @param sources Dataset sources + * @param row_group_indices Lists of row groups to read bloom filters from, one per source + * @param[out] bloom_filter_data List of bloom filter data device buffers + * @param column_schemas Schema indices of columns whose bloom filters will be read + * @param stream CUDA stream used for device memory operations and kernel launches + * + * @return A flattened list of bloom filter bitset device buffers for each predicate column across + * row group + */ + [[nodiscard]] std::vector read_bloom_filters( + host_span const> sources, + host_span const> row_group_indices, + host_span column_schemas, + size_type num_row_groups, + rmm::cuda_stream_view stream) const; + + /** + * @brief Collects Parquet types for the columns with the specified schema indices + * + * @param row_group_indices Lists of row groups, once per source + * @param column_schemas Schema indices of columns whose types will be collected + * + * @return A list of parquet types for the columns matching the provided schema indices + */ + [[nodiscard]] std::vector get_parquet_types( + host_span const> row_group_indices, + host_span column_schemas) const; + public: aggregate_reader_metadata(host_span const> sources, bool use_arrow_schema, @@ -323,26 +355,49 @@ class aggregate_reader_metadata { /** * @brief Filters the row groups based on predicate filter * + * @param sources Lists of input datasources * @param row_group_indices Lists of row groups to read, one per source - * @param output_dtypes Datatypes of of output columns + * @param output_dtypes Datatypes of output columns * @param output_column_schemas schema indices of output columns * @param filter AST expression to filter row groups based on Column chunk statistics * @param stream CUDA stream used for device memory operations and kernel launches - * @return Filtered row group indices, if any is filtered. + * @return Filtered row group indices, if any is filtered */ [[nodiscard]] std::optional>> filter_row_groups( + host_span const> sources, host_span const> row_group_indices, host_span output_dtypes, host_span output_column_schemas, std::reference_wrapper filter, rmm::cuda_stream_view stream) const; + /** + * @brief Filters the row groups using bloom filters + * + * @param sources Dataset sources + * @param row_group_indices Lists of input row groups to read, one per source + * @param output_dtypes Datatypes of output columns + * @param output_column_schemas schema indices of output columns + * @param filter AST expression to filter row groups based on bloom filter membership + * @param stream CUDA stream used for device memory operations and kernel launches + * + * @return Filtered row group indices, if any is filtered + */ + [[nodiscard]] std::optional>> apply_bloom_filters( + host_span const> sources, + host_span const> input_row_group_indices, + host_span output_dtypes, + host_span output_column_schemas, + std::reference_wrapper filter, + rmm::cuda_stream_view stream) const; + /** * @brief Filters and reduces down to a selection of row groups * * The input `row_start` and `row_count` parameters will be recomputed and output as the valid * values based on the input row group list. * + * @param sources Lists of input datasources * @param row_group_indices Lists of row groups to read, one per source * @param row_start Starting row of the selection * @param row_count Total number of rows selected @@ -351,10 +406,11 @@ class aggregate_reader_metadata { * @param filter Optional AST expression to filter row groups based on Column chunk statistics * @param stream CUDA stream used for device memory operations and kernel launches * @return A tuple of corrected row_start, row_count, list of row group indexes and its - * starting row, and list of number of rows per source. + * starting row, and list of number of rows per source */ [[nodiscard]] std::tuple, std::vector> - select_row_groups(host_span const> row_group_indices, + select_row_groups(host_span const> sources, + host_span const> row_group_indices, int64_t row_start, std::optional const& row_count, host_span output_dtypes, @@ -413,14 +469,14 @@ class named_to_reference_converter : public ast::detail::expression_transformer std::reference_wrapper visit(ast::operation const& expr) override; /** - * @brief Returns the AST to apply on Column chunk statistics. + * @brief Returns the converted AST expression * * @return AST operation expression */ [[nodiscard]] std::optional> get_converted_expr() const { - return _stats_expr; + return _converted_expr; } private: @@ -428,7 +484,7 @@ class named_to_reference_converter : public ast::detail::expression_transformer cudf::host_span const> operands); std::unordered_map column_name_to_index; - std::optional> _stats_expr; + std::optional> _converted_expr; // Using std::list or std::deque to avoid reference invalidation std::list _col_ref; std::list _operators; @@ -445,4 +501,22 @@ class named_to_reference_converter : public ast::detail::expression_transformer std::optional> expr, std::vector const& skip_names); +/** + * @brief Filter table using the provided (StatsAST or BloomfilterAST) expression and + * collect filtered row group indices + * + * @param table Table of stats or bloom filter membership columns + * @param ast_expr StatsAST or BloomfilterAST expression to filter with + * @param input_row_group_indices Lists of input row groups to read, one per source + * @param stream CUDA stream used for device memory operations and kernel launches + * + * @return Collected filtered row group indices, one vector per source, if any. A std::nullopt if + * all row groups are required or if the computed predicate is all nulls + */ +[[nodiscard]] std::optional>> collect_filtered_row_group_indices( + cudf::table_view ast_table, + std::reference_wrapper ast_expr, + host_span const> input_row_group_indices, + rmm::cuda_stream_view stream); + } // namespace cudf::io::parquet::detail diff --git a/cpp/src/io/parquet/reader_impl_preprocess.cu b/cpp/src/io/parquet/reader_impl_preprocess.cu index 326232ced60..43666f9e42d 100644 --- a/cpp/src/io/parquet/reader_impl_preprocess.cu +++ b/cpp/src/io/parquet/reader_impl_preprocess.cu @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2024, NVIDIA CORPORATION. + * Copyright (c) 2022-2025, NVIDIA CORPORATION. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1286,7 +1286,8 @@ void reader::impl::preprocess_file(read_mode mode) _file_itm_data.global_num_rows, _file_itm_data.row_groups, _file_itm_data.num_rows_per_source) = - _metadata->select_row_groups(_options.row_group_indices, + _metadata->select_row_groups(_sources, + _options.row_group_indices, _options.skip_rows, _options.num_rows, output_dtypes, diff --git a/cpp/tests/CMakeLists.txt b/cpp/tests/CMakeLists.txt index 35877ac34b9..6a89b1e48d6 100644 --- a/cpp/tests/CMakeLists.txt +++ b/cpp/tests/CMakeLists.txt @@ -318,14 +318,15 @@ ConfigureTest( ) ConfigureTest( PARQUET_TEST - io/parquet_test.cpp + io/parquet_bloom_filter_test.cu io/parquet_chunked_reader_test.cu io/parquet_chunked_writer_test.cpp io/parquet_common.cpp io/parquet_misc_test.cpp io/parquet_reader_test.cpp - io/parquet_writer_test.cpp + io/parquet_test.cpp io/parquet_v2_test.cpp + io/parquet_writer_test.cpp GPUS 1 PERCENT 30 ) diff --git a/cpp/tests/io/parquet_bloom_filter_test.cu b/cpp/tests/io/parquet_bloom_filter_test.cu new file mode 100644 index 00000000000..d858f58fa56 --- /dev/null +++ b/cpp/tests/io/parquet_bloom_filter_test.cu @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025, NVIDIA CORPORATION. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +using StringType = cudf::string_view; + +class ParquetBloomFilterTest : public cudf::test::BaseFixture {}; + +TEST_F(ParquetBloomFilterTest, TestStrings) +{ + using key_type = StringType; + using policy_type = cuco::arrow_filter_policy; + using word_type = policy_type::word_type; + + std::size_t constexpr num_filter_blocks = 4; + auto stream = cudf::get_default_stream(); + + // strings keys to insert + auto keys = cudf::test::strings_column_wrapper( + {"seventh", "fifteenth", "second", "tenth", "fifth", "first", + "seventh", "tenth", "ninth", "ninth", "seventeenth", "eighteenth", + "thirteenth", "fifth", "fourth", "twelfth", "second", "second", + "fourth", "seventh", "seventh", "tenth", "thirteenth", "seventeenth", + "fifth", "seventeenth", "eighth", "fourth", "second", "eighteenth", + "fifteenth", "second", "seventeenth", "thirteenth", "eighteenth", "fifth", + "seventh", "tenth", "fourteenth", "first", "fifth", "fifth", + "tenth", "thirteenth", "fourteenth", "third", "third", "sixth", + "first", "third"}); + + auto d_keys = cudf::column_device_view::create(keys); + + // Spawn a bloom filter + cuco::bloom_filter, + cuda::thread_scope_device, + policy_type, + cudf::detail::cuco_allocator> + filter{num_filter_blocks, + cuco::thread_scope_device, + {{cudf::DEFAULT_HASH_SEED}}, + cudf::detail::cuco_allocator{rmm::mr::polymorphic_allocator{}, stream}, + stream}; + + // Add strings to the bloom filter + filter.add(d_keys->begin(), d_keys->end(), stream); + + // Number of words in the filter + cudf::size_type const num_words = filter.block_extent() * filter.words_per_block; + + // Filter bitset as a column + auto const bitset = cudf::column_view{ + cudf::data_type{cudf::type_id::UINT32}, num_words, filter.data(), nullptr, 0, 0, {}}; + + // Expected filter bitset words computed using Arrow's implementation here: + // https://godbolt.org/z/oKfqcPWbY + auto expected = cudf::test::fixed_width_column_wrapper( + {4194306U, 4194305U, 2359296U, 1073774592U, 524544U, 1024U, 268443648U, + 8519680U, 2147500040U, 8421380U, 269500416U, 4202624U, 8396802U, 100665344U, + 2147747840U, 5243136U, 131146U, 655364U, 285345792U, 134222340U, 545390596U, + 2281717768U, 51201U, 41943553U, 1619656708U, 67441680U, 8462730U, 361220U, + 2216738864U, 587333888U, 4219272U, 873463873U}); + + // Check the bitset for equality + CUDF_TEST_EXPECT_COLUMNS_EQUAL(bitset, expected); +} diff --git a/python/cudf/cudf/tests/data/parquet/mixed_card_ndv_100_bf_fpp0.1_nostats.snappy.parquet b/python/cudf/cudf/tests/data/parquet/mixed_card_ndv_100_bf_fpp0.1_nostats.snappy.parquet new file mode 100644 index 0000000000000000000000000000000000000000..4123545a6e030b7c0b1b7852571348ed53074f72 GIT binary patch literal 41508 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 literal 0 HcmV?d00001 diff --git a/python/cudf/cudf/tests/data/parquet/mixed_card_ndv_500_bf_fpp0.1_nostats.snappy.parquet b/python/cudf/cudf/tests/data/parquet/mixed_card_ndv_500_bf_fpp0.1_nostats.snappy.parquet new file mode 100644 index 0000000000000000000000000000000000000000..e898f1d7d1bf6976b661b87b7ea39b615f318102 GIT binary patch literal 77985 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 j#=DSFxFSOiDc@*yN_?Y0W{;;Ubpehgmi@ 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", np.uint32(0))]], 1000), + ( + [ + ("str", "!=", "FINDME"), + ("fixed_pt", "==", decimal.Decimal(float(500))), + ], + 0, + ), + ], +) +def test_parquet_bloom_filters( + datadir, stats_fname, bloom_filter_fname, predicate, expected_len +): + fname_stats = datadir / stats_fname + fname_bf = datadir / bloom_filter_fname + df_stats = cudf.read_parquet(fname_stats, filters=predicate).reset_index( + drop=True + ) + df_bf = cudf.read_parquet(fname_bf, filters=predicate).reset_index( + drop=True + ) + + # Check if tables equal + assert_eq( + df_stats, + df_bf, + ) + + # Check for table length + assert_eq( + len(df_stats), + expected_len, + )