Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,7 @@ add_library(
src/io/orc/writer_impl.cu
src/io/parquet/arrow_schema_writer.cpp
src/io/parquet/bloom_filter_reader.cu
src/io/parquet/column_path_helpers.cpp
src/io/parquet/compact_protocol_reader.cpp
src/io/parquet/compact_protocol_writer.cpp
src/io/parquet/decode_preprocess.cu
Expand Down
144 changes: 144 additions & 0 deletions cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,26 @@
#include <benchmarks/io/cuio_common.hpp>
#include <benchmarks/io/nvbench_helpers.hpp>

#include <cudf/ast/expressions.hpp>
#include <cudf/detail/utilities/integer_utils.hpp>
#include <cudf/io/datasource.hpp>
#include <cudf/io/parquet.hpp>
#include <cudf/io/parquet_metadata.hpp>
#include <cudf/scalar/scalar.hpp>
#include <cudf/utilities/default_stream.hpp>

#include <cuda/iterator>

#include <nvbench/nvbench.cuh>

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <limits>
#include <string>
#include <utility>
#include <vector>

// Common mixed dtypes used by all benchmarks in this file
auto const mixed_dtypes = get_type_or_group({static_cast<int32_t>(data_type::STRING),
static_cast<int32_t>(data_type::INTEGRAL),
Expand Down Expand Up @@ -74,6 +84,27 @@ auto write_file_data(cudf::size_type num_cols,
return source_sink;
}

// Combines `operands` into a balanced AST tree using `op`: pairing adjacent operands gives a tree
// of depth ceil(log2(n)) rather than the n-deep chain a left fold would produce.
[[nodiscard]] cudf::ast::expression const* reduce_balanced(
cudf::ast::tree& tree,
cudf::ast::ast_operator op,
std::vector<cudf::ast::expression const*> operands)
{
CUDF_EXPECTS(not operands.empty(), "Cannot reduce an empty set of operands");
while (operands.size() > 1) {
std::vector<cudf::ast::expression const*> next;
next.reserve((operands.size() + 1) / 2);
for (std::size_t i = 0; i + 1 < operands.size(); i += 2) {
next.push_back(&tree.push(cudf::ast::operation(op, *operands[i], *operands[i + 1])));
}
// Carry an odd trailing operand up to the next level unchanged.
if (operands.size() % 2 == 1) { next.push_back(operands.back()); }
operands = std::move(next);
}
return operands.front();
}

} // namespace

// Benchmark to measure parquet footer read time
Expand Down Expand Up @@ -207,6 +238,111 @@ void BM_parquet_column_selection(nvbench::state& state)
mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage");
}

// Benchmark Parquet filter column-name resolution during reader construction.
Comment thread
qbacpey marked this conversation as resolved.
void BM_parquet_filter_name_resolution(nvbench::state& state)
Comment thread
mhaseeb123 marked this conversation as resolved.
{
auto const num_cols = static_cast<cudf::size_type>(state.get_int64("num_cols"));
auto const case_sensitive = state.get_int64("case_sensitive") != 0;
auto const heavy_filter = state.get_int64("heavy_filter") != 0;
auto const source_type = retrieve_io_type_enum(state.get_string("io_type"));

cuio_source_sink_pair source_sink(source_type);

// Flat, single-row table of INT32 columns with deterministic names col0..col{n-1}. INT32 keeps
// the filter literal trivially type-correct; name-resolution cost is independent of dtype.
constexpr cudf::size_type num_rows = 1;
auto const tbl =
create_random_table(cycle_dtypes({cudf::type_id::INT32}, num_cols),
row_count{num_rows},
data_profile_builder().cardinality(0).avg_run_length(1).no_validity());
auto const view = tbl->view();

cudf::io::table_input_metadata input_meta(view);
std::vector<std::string> file_names(num_cols);
for (cudf::size_type i = 0; i < num_cols; i++) {
file_names[i] = "col" + std::to_string(i);
input_meta.column_metadata[i].set_name(file_names[i]);
}

cudf::io::parquet_writer_options write_opts =
cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view)
.metadata(std::move(input_meta))
.compression(cudf::io::compression_type::NONE);
cudf::io::write_parquet(write_opts);

// Query name: exact when case-sensitive, upper-cased when case-insensitive so the converter must
// normalize on lookup.
auto const to_query_case = [case_sensitive](std::string s) {
if (not case_sensitive) {
std::transform(
s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::toupper(c); });
}
return s;
};

// Always-true filter. `heavy_filter` controls how many columns it references:
// light: `col0 >= MIN` (converter resolves 1 name reference)
// heavy: `(col0 >= MIN) OR (col1 >= MIN) OR ...` (one reference per column)
// The heavy OR tree is built balanced (depth ~log2(num_cols)) to keep visitor recursion shallow.
cudf::numeric_scalar<int32_t> filter_literal_value{std::numeric_limits<int32_t>::min()};
cudf::ast::tree expr;
auto const& lit_expr = expr.push(cudf::ast::literal(filter_literal_value));
auto const make_predicate = [&](cudf::size_type col) -> cudf::ast::expression const& {
auto const& col_ref =
expr.push(cudf::ast::column_name_reference(to_query_case(file_names[col])));
return expr.push(
cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, lit_expr));
};

auto const num_predicates = heavy_filter ? num_cols : cudf::size_type{1};
std::vector<cudf::ast::expression const*> predicates;
predicates.reserve(num_predicates);
for (cudf::size_type col = 0; col < num_predicates; col++) {
predicates.push_back(&make_predicate(col));
}
// Reduce the per-column predicates into a single balanced OR tree so the AST depth stays
// logarithmic in the column count (see reduce_balanced).
auto const& filter_expr =
*reduce_balanced(expr, cudf::ast::ast_operator::LOGICAL_OR, std::move(predicates));

auto constexpr chunk_read_limit = 0;
auto constexpr pass_read_limit = 0;

// No column projection is requested, so the reader reads all columns; this isolates filter name
// resolution (named_to_reference_converter) from select_columns name scanning.
auto read_opts = cudf::io::parquet_reader_options::builder(source_sink.make_source_info())
.use_arrow_schema(false)
.build();
read_opts.enable_case_sensitive_names(case_sensitive);
read_opts.set_filter(filter_expr);

state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value()));
auto const mem_stats_logger = cudf::memory_stats_logger();
state.exec(
nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) {
auto const source_info = source_sink.make_source_info();
drop_page_cache_if_enabled(source_info.filepaths());
auto sources = cudf::io::make_datasources(source_info);
auto metadatas = cudf::io::read_parquet_footers(sources);

// Reader construction resolves all referenced filter column names
// (named_to_reference_converter) and runs column selection. Construction throws if a
// referenced name is missing, so successful construction is the validation; has_next() is
// intentionally not called so per-sample timing is not perturbed by row-group filter
// evaluation over the wide predicate.
timer.start();
[[maybe_unused]] auto const reader = cudf::io::chunked_parquet_reader(
chunk_read_limit, pass_read_limit, std::move(sources), std::move(metadatas), read_opts);
timer.stop();
});
Comment thread
PointKernel marked this conversation as resolved.

auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value");
state.add_element_count(static_cast<double>(num_cols) / time, "cols_per_sec");
// Should be 0, but adding for completeness
state.add_buffer_size(
mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage");
}

NVBENCH_BENCH(BM_parquet_read_footer)
.set_name("parquet_read_footer")
.set_min_samples(4)
Expand All @@ -228,3 +364,11 @@ NVBENCH_BENCH(BM_parquet_column_selection)
.set_min_samples(4)
.add_string_axis("io_type", {"FILEPATH"})
.add_int64_axis("num_cols", {64, 512, 2048});

NVBENCH_BENCH(BM_parquet_filter_name_resolution)
.set_name("parquet_filter_name_resolution")
.set_min_samples(4)
.add_string_axis("io_type", {"FILEPATH"})
.add_int64_axis("num_cols", {64, 128, 256, 512, 1024, 1536, 2048, 4096})
.add_int64_axis("case_sensitive", {1, 0})
.add_int64_axis("heavy_filter", {0, 1});
54 changes: 54 additions & 0 deletions cpp/src/io/parquet/column_path_helpers.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#include "column_path_helpers.hpp"

#include <algorithm>
#include <cctype>
#include <cstddef>
#include <functional>
#include <string>
#include <string_view>

namespace cudf::io::parquet::detail {

std::string normalize_column_path(std::string_view col_path, bool case_sensitive_names)
{
if (case_sensitive_names) { return std::string{col_path}; }
auto normalized_path = std::string(col_path.size(), '\0');
std::transform(col_path.begin(), col_path.end(), normalized_path.begin(), [](unsigned char c) {
return std::tolower(c);
});
return normalized_path;
}

bool are_column_paths_equal(std::string_view lhs, std::string_view rhs, bool case_sensitive)
{
if (lhs.size() != rhs.size()) { return false; }
if (case_sensitive) { return lhs == rhs; }
// Optimize by normalizing and comparing char-by-char instead of whole strings
return std::equal(
lhs.begin(), lhs.end(), rhs.begin(), [](unsigned char lhs_char, unsigned char rhs_char) {
return std::equal_to<>{}(std::tolower(lhs_char), std::tolower(rhs_char));
});
}

std::size_t column_path_hash::operator()(std::string_view path) const
{
return std::hash<std::string>{}(normalize_column_path(path, case_sensitive_names));
}

bool column_path_equal::operator()(std::string_view lhs, std::string_view rhs) const
{
return are_column_paths_equal(lhs, rhs, case_sensitive_names);
}

column_path_set make_column_path_set(bool case_sensitive_names, std::size_t bucket_hint)
{
return column_path_set(
bucket_hint, column_path_hash{case_sensitive_names}, column_path_equal{case_sensitive_names});
}

} // namespace cudf::io::parquet::detail
103 changes: 103 additions & 0 deletions cpp/src/io/parquet/column_path_helpers.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION.
* SPDX-License-Identifier: Apache-2.0
*/

#pragma once

#include <cstddef>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>

namespace cudf::io::parquet::detail {

/**
* @brief Returns a normalized (lowercased) column name or path when case-insensitive matching is
* enabled
*
* @param col_path The column name or path to normalize
* @param case_sensitive_names Whether to normalize the column path case-insensitively
*
* @return The normalized column path
*/
[[nodiscard]] std::string normalize_column_path(std::string_view col_path,
bool case_sensitive_names);

/**
* @brief Compares two column paths with specified case sensitivity
*
* @param lhs The left-hand side column path
* @param rhs The right-hand side column path
* @param case_sensitive Whether to compare the column paths case-sensitively
*
* @return Boolean indicating if the column paths are equal
*/
[[nodiscard]] bool are_column_paths_equal(std::string_view lhs,
std::string_view rhs,
bool case_sensitive);

/**
* @brief Transparent hash for column paths that honors a case-sensitivity policy.
*
* Hashes the column path according to `case_sensitive_names`, so that two paths that compare equal
* under `are_column_paths_equal` also hash equally.
*/
struct column_path_hash {
using is_transparent = void;
bool case_sensitive_names{true};

std::size_t operator()(std::string_view path) const;
};

/**
* @brief Transparent equality for column paths that honors a case-sensitivity policy.
*
* Delegates to `are_column_paths_equal`, keeping the comparison consistent with `column_path_hash`.
*/
struct column_path_equal {
using is_transparent = void;
bool case_sensitive_names{true};
Comment thread
qbacpey marked this conversation as resolved.

bool operator()(std::string_view lhs, std::string_view rhs) const;
};

/**
* @brief A set of column paths matched with a configurable case-sensitivity policy.
*/
using column_path_set = std::unordered_set<std::string, column_path_hash, column_path_equal>;

/**
* @brief Constructs an empty `column_path_set` whose hash/equality use the given policy.
*
* @param case_sensitive_names Whether column-path matching is case-sensitive
* @param bucket_hint Optional initial bucket count
* @return An empty `column_path_set` using the requested policy
*/
[[nodiscard]] column_path_set make_column_path_set(bool case_sensitive_names,
std::size_t bucket_hint = 0);

/**
* @brief A map keyed by column path matched with a configurable case-sensitivity policy.
*/
template <typename Value>
using column_path_map = std::unordered_map<std::string, Value, column_path_hash, column_path_equal>;

/**
* @brief Constructs an empty `column_path_map` whose hash/equality use the given policy.
*
* @tparam Value Mapped value type
* @param case_sensitive_names Whether column-path matching is case-sensitive
* @param bucket_hint Optional initial bucket count
* @return An empty `column_path_map` using the requested policy
*/
template <typename Value>
[[nodiscard]] column_path_map<Value> make_column_path_map(bool case_sensitive_names,
std::size_t bucket_hint = 0)
{
return column_path_map<Value>(
bucket_hint, column_path_hash{case_sensitive_names}, column_path_equal{case_sensitive_names});
}

} // namespace cudf::io::parquet::detail
Loading
Loading