From fe0fc7b93761af470d87da282816d39274161853 Mon Sep 17 00:00:00 2001 From: Qi Chen Date: Sat, 12 Sep 2026 23:12:53 +0200 Subject: [PATCH 1/4] Enhance benchmarks for hybrid scan and parquet reader - Added new benchmarks for hybrid scan column resolution, full read, and metadata. - Updated existing parquet reader metadata benchmark to include reader_common.cpp. - Introduced utility functions for writing mixed dtype parquet files and controlling file shape. - Improved organization and clarity in reader_common.hpp and reader_common.cpp. These changes aim to improve the benchmarking capabilities for hybrid scan operations and parquet file handling. --- cpp/benchmarks/CMakeLists.txt | 8 + .../hybrid_scan/hybrid_scan_metadata.cpp | 188 ++++++++++++++++++ .../io/parquet/parquet_reader_metadata.cpp | 75 ++----- cpp/benchmarks/io/parquet/reader_common.cpp | 112 +++++++++++ cpp/benchmarks/io/parquet/reader_common.hpp | 57 +++++- 5 files changed, 376 insertions(+), 64 deletions(-) create mode 100644 cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index ea8480a9fe2b..d212c5a6b792 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -270,6 +270,14 @@ ConfigureNVBench( io/parquet/experimental/hybrid_scan/hybrid_scan_composer.cpp ) +# ################################################################################################## +# * hybrid scan metadata benchmark +# ---------------------------------------------------------------------- +ConfigureNVBench( + HYBRID_SCAN_METADATA_NVBENCH io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp + io/parquet/reader_common.cpp +) + # ################################################################################################## # * interop benchmark ------------------------------------------------------------------------------ ConfigureNVBench(INTEROP_NVBENCH interop/interop.cpp interop/interop_stringview.cpp) diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp new file mode 100644 index 000000000000..9f4e08f8014f --- /dev/null +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp @@ -0,0 +1,188 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +/** + * Benchmark for hybrid scan's caller-visible setup path. + * + * Every cell runs the whole setup path and brackets the timer around one step. + */ +enum class setup_phase : int32_t { + FOOTER_FETCH, // copy footer bytes to host + READER_CTOR, // parse footer, construct reader + PAGE_INDEX_FETCH, // copy page-index bytes to host + PAGE_INDEX_SETUP, // parse page index into the reader + ALL // the whole path, for pairing against the naive reader +}; + +// NVBENCH_DECLARE_ENUM_TYPE_STRINGS macro must be used from global namespace scope +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + setup_phase, + [](setup_phase value) { + switch (value) { + case setup_phase::FOOTER_FETCH: return "FOOTER_FETCH"; + case setup_phase::READER_CTOR: return "READER_CTOR"; + case setup_phase::PAGE_INDEX_FETCH: return "PAGE_INDEX_FETCH"; + case setup_phase::PAGE_INDEX_SETUP: return "PAGE_INDEX_SETUP"; + case setup_phase::ALL: return "ALL"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +namespace { + +// Runs the whole setup path, timing only the step named by `Phase`, or all of it for ALL. +template +[[nodiscard]] auto run_setup_path(Timer& timer, + cudf::io::datasource& datasource, + cudf::io::parquet_reader_options const& options) +{ + constexpr auto all = Phase == setup_phase::ALL; + + if constexpr (Phase == setup_phase::FOOTER_FETCH or all) { timer.start(); } + auto const footer = cudf::io::parquet::fetch_footer_to_host(datasource); + if constexpr (Phase == setup_phase::FOOTER_FETCH) { timer.stop(); } + + if constexpr (Phase == setup_phase::READER_CTOR) { timer.start(); } + auto reader = + std::make_unique(*footer, options); + if constexpr (Phase == setup_phase::READER_CTOR) { timer.stop(); } + + if constexpr (Phase == setup_phase::PAGE_INDEX_FETCH) { timer.start(); } + auto const page_index = + cudf::io::parquet::fetch_page_index_to_host(datasource, reader->page_index_byte_range()); + if constexpr (Phase == setup_phase::PAGE_INDEX_FETCH) { timer.stop(); } + + if constexpr (Phase == setup_phase::PAGE_INDEX_SETUP) { timer.start(); } + reader->setup_page_index(*page_index); + if constexpr (Phase == setup_phase::PAGE_INDEX_SETUP or all) { timer.stop(); } + + return reader; +} + +} // namespace + +// Every cell runs the whole setup path and times the window named by the `phase` axis. +template +void BM_hybrid_scan_setup_phase(nvbench::state& state, + nvbench::type_list>) +{ + auto const num_cols = static_cast(state.get_int64("num_cols")); + auto const num_row_groups = static_cast(state.get_int64("num_row_groups")); + auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); + auto const write_page_index = state.get_int64("page_index") != 0; + + if constexpr (Phase == setup_phase::PAGE_INDEX_FETCH or Phase == setup_phase::PAGE_INDEX_SETUP) { + if (not write_page_index) { + state.skip("Page-index phases require page_index=true"); + return; + } + } + + auto source_sink = + write_mixed_dtype_parquet_file(num_cols, num_row_groups, source_type, write_page_index); + 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 datasource = std::move(cudf::io::make_datasources(source_info).front()); + auto const read_opts = cudf::io::parquet_reader_options::builder(source_info).build(); + + auto const reader = run_setup_path(timer, *datasource, read_opts); + CUDF_EXPECTS(std::cmp_equal(reader->all_row_groups(read_opts).size(), num_row_groups), + "Unexpected row group count"); + }); + + auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(num_cols * num_row_groups) / time, + "colchunks_per_sec"); + state.add_buffer_size( + mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); +} + +// Isolated page-index fetch vs parse as page count grows. +template +void BM_hybrid_scan_file_shape(nvbench::state& state, nvbench::type_list>) +{ + // Matches parquet_read_file_shape, which needs a string column for the naive reader to read the + // page index at all + auto constexpr d_type = cudf::type_id::STRING; + // Both phases measured here parse the page index, so it is always written + auto constexpr write_page_index = true; + + auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); + auto const num_rows = static_cast(state.get_int64("num_rows")); + auto const num_row_groups = static_cast(state.get_int64("num_row_groups")); + auto const num_pages_per_row_group = + static_cast(state.get_int64("pages_per_row_group")); + + auto source_sink = write_file_shape_parquet_file( + d_type, num_rows, num_row_groups, num_pages_per_row_group, source_type, write_page_index); + + auto const read_opts = + cudf::io::parquet_reader_options::builder(source_sink.make_source_info()).build(); + + state.exec( + nvbench::exec_tag::sync | nvbench::exec_tag::timer, [&](nvbench::launch& launch, auto& timer) { + drop_page_cache_if_enabled(read_opts.get_source().filepaths()); + auto datasource = std::move(cudf::io::make_datasources(read_opts.get_source()).front()); + + auto const reader = run_setup_path(timer, *datasource, read_opts); + CUDF_EXPECTS(not reader->all_row_groups(read_opts).empty(), + "Expected at least one row group"); + }); + + auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(num_row_groups * num_pages_per_row_group) / time, + "pages_per_sec"); + state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); +} + +using setup_phases = nvbench::enum_type_list; + +using page_index_phases = + nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(BM_hybrid_scan_setup_phase, NVBENCH_TYPE_AXES(setup_phases)) + .set_name("hybrid_scan_setup_phase") + .set_type_axes_names({"phase"}) + .set_min_samples(4) + .add_string_axis("io_type", {"FILEPATH"}) + .add_int64_axis("page_index", {true, false}) + .add_int64_axis("num_cols", {64, 256, 512}) + .add_int64_axis("num_row_groups", {10, 50}); + +NVBENCH_BENCH_TYPES(BM_hybrid_scan_file_shape, NVBENCH_TYPE_AXES(page_index_phases)) + .set_name("hybrid_scan_file_shape") + .set_type_axes_names({"phase"}) + .set_min_samples(4) + // Axes match parquet_read_file_shape so the naive and hybrid scan paths can be compared + .add_string_axis("io_type", {"DEVICE_BUFFER"}) + .add_int64_axis("num_rows", {10'000'000, 100'000'000}) + .add_int64_axis("num_row_groups", {1, 10}) + .add_int64_axis("pages_per_row_group", {1'000, 10'000}); diff --git a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp index 6172666b6d53..402cbb675d26 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp @@ -3,21 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ +#include "reader_common.hpp" + #include #include #include -#include #include -#include #include #include #include #include #include -#include - #include #include @@ -28,62 +26,8 @@ #include #include -// Common mixed dtypes used by all benchmarks in this file -auto const mixed_dtypes = get_type_or_group({static_cast(data_type::STRING), - static_cast(data_type::INTEGRAL), - static_cast(data_type::FLOAT), - static_cast(data_type::DECIMAL), - static_cast(data_type::LIST)}); - namespace { -// Helper to generate and write parquet data using chunked writer -auto write_file_data(cudf::size_type num_cols, - cudf::size_type num_row_groups, - io_type source_type, - bool write_page_index) -{ - cuio_source_sink_pair source_sink(source_type); - - // Minimum row group size that cudf will almost always follow - constexpr auto rows_per_row_group = 5000; - constexpr auto min_row_groups = 10; - constexpr auto table_rows = min_row_groups * rows_per_row_group; - - CUDF_EXPECTS(num_row_groups > 0 and num_row_groups % min_row_groups == 0, - "Number of requested row groups must be non-zero and a multiple of " + - std::to_string(min_row_groups)); - - // Create a table with the enough rows to cover min_row_groups - auto const tbl = - create_random_table(cycle_dtypes(mixed_dtypes, num_cols), - row_count{table_rows}, - data_profile_builder().cardinality(0).avg_run_length(1).distribution( - cudf::type_id::LIST, distribution_id::GEOMETRIC, 0, 4)); - auto const view = tbl->view(); - - auto const stats_level = write_page_index ? cudf::io::statistics_freq::STATISTICS_COLUMN - : cudf::io::statistics_freq::STATISTICS_ROWGROUP; - auto const options = - cudf::io::chunked_parquet_writer_options::builder(source_sink.make_sink_info()) - .row_group_size_rows(rows_per_row_group) - .compression(cudf::io::compression_type::NONE) - .stats_level(stats_level) - .build(); - auto writer = cudf::io::chunked_parquet_writer(options, cudf::get_default_stream()); - - // Compute the number of times the table needs to be written to cover the requested number of row - // groups - auto num_writes = cudf::util::div_rounding_up_unsafe(num_row_groups, min_row_groups); - std::for_each(cuda::counting_iterator{0}, - cuda::counting_iterator{num_writes}, - [&](cudf::size_type) { writer.write(view); }); - - std::ignore = writer.close(); - - 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( @@ -107,7 +51,9 @@ auto write_file_data(cudf::size_type num_cols, } // namespace -// Benchmark to measure parquet footer read time +// Benchmark to measure parquet footer read time. `read_parquet_footers` also loads page indexes +// when present. Pair the result with `hybrid_scan_reader_construction` (full hybrid setup), not +// with an isolated `hybrid_scan_footer` phase. void BM_parquet_read_footer(nvbench::state& state) { auto const num_cols = static_cast(state.get_int64("num_cols")); @@ -115,7 +61,8 @@ void BM_parquet_read_footer(nvbench::state& state) auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); auto const write_page_index = state.get_int64("page_index") != 0; - auto source_sink = write_file_data(num_cols, num_row_groups, source_type, write_page_index); + auto source_sink = + write_mixed_dtype_parquet_file(num_cols, num_row_groups, source_type, write_page_index); auto const mem_stats_logger = cudf::memory_stats_logger(); state.exec( @@ -145,7 +92,8 @@ void BM_parquet_read_footer(nvbench::state& state) mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); } -// Benchmark to measure chunked parquet reader construction time +// Benchmark to measure chunked parquet reader construction time. Pair with +// `hybrid_scan_reader_construction` on the same axes. void BM_parquet_reader_construction(nvbench::state& state) { auto const num_cols = static_cast(state.get_int64("num_cols")); @@ -153,7 +101,8 @@ void BM_parquet_reader_construction(nvbench::state& state) auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); auto const write_page_index = state.get_int64("page_index") != 0; - auto source_sink = write_file_data(num_cols, num_row_groups, source_type, write_page_index); + auto source_sink = + write_mixed_dtype_parquet_file(num_cols, num_row_groups, source_type, write_page_index); auto constexpr chunk_read_limit = 0; auto constexpr pass_read_limit = 0; @@ -195,7 +144,7 @@ void BM_parquet_column_selection(nvbench::state& state) // Create a table with minimal rows (1 row is enough to create valid parquet) constexpr cudf::size_type num_rows = 1; - auto const tbl = create_random_table(cycle_dtypes(mixed_dtypes, num_cols), + auto const tbl = create_random_table(cycle_dtypes(mixed_dtypes(), num_cols), row_count{num_rows}, data_profile_builder().cardinality(0).avg_run_length(1)); auto const view = tbl->view(); diff --git a/cpp/benchmarks/io/parquet/reader_common.cpp b/cpp/benchmarks/io/parquet/reader_common.cpp index 69a6ff929909..8e2df0e32fe2 100644 --- a/cpp/benchmarks/io/parquet/reader_common.cpp +++ b/cpp/benchmarks/io/parquet/reader_common.cpp @@ -8,12 +8,20 @@ #include #include #include +#include +#include #include #include +#include + #include +#include +#include // TEMPORARY, NOT FOR MERGE +#include + void parquet_read_common(cudf::size_type num_rows_to_read, cudf::size_type num_cols_to_read, cuio_source_sink_pair& source_sink, @@ -43,3 +51,107 @@ void parquet_read_common(cudf::size_type num_rows_to_read, mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); state.add_buffer_size(source_sink.size(), "encoded_file_size", "encoded_file_size"); } + +std::vector const& mixed_dtypes() +{ + static std::vector const dtypes = + get_type_or_group({static_cast(data_type::STRING), + static_cast(data_type::INTEGRAL), + static_cast(data_type::FLOAT), + static_cast(data_type::DECIMAL), + static_cast(data_type::LIST)}); + return dtypes; +} + +cuio_source_sink_pair write_mixed_dtype_parquet_file(cudf::size_type num_cols, + cudf::size_type num_row_groups, + io_type source_type, + bool write_page_index) +{ + cuio_source_sink_pair source_sink(source_type); + + // Minimum row group size that cudf will almost always follow + constexpr auto rows_per_row_group = 5000; + constexpr auto min_row_groups = 10; + constexpr auto table_rows = min_row_groups * rows_per_row_group; + + CUDF_EXPECTS(num_row_groups > 0 and num_row_groups % min_row_groups == 0, + "Number of requested row groups must be non-zero and a multiple of " + + std::to_string(min_row_groups)); + + // Create a table with the enough rows to cover min_row_groups + auto const tbl = + create_random_table(cycle_dtypes(mixed_dtypes(), num_cols), + row_count{table_rows}, + data_profile_builder().cardinality(0).avg_run_length(1).distribution( + cudf::type_id::LIST, distribution_id::GEOMETRIC, 0, 4)); + auto const view = tbl->view(); + + auto const stats_level = write_page_index ? cudf::io::statistics_freq::STATISTICS_COLUMN + : cudf::io::statistics_freq::STATISTICS_ROWGROUP; + auto const options = + cudf::io::chunked_parquet_writer_options::builder(source_sink.make_sink_info()) + .row_group_size_rows(rows_per_row_group) + .compression(cudf::io::compression_type::NONE) + .stats_level(stats_level) + .build(); + auto writer = cudf::io::chunked_parquet_writer(options, cudf::get_default_stream()); + + // Compute the number of times the table needs to be written to cover the requested number of row + // groups + auto num_writes = cudf::util::div_rounding_up_unsafe(num_row_groups, min_row_groups); + std::for_each(cuda::counting_iterator{0}, + cuda::counting_iterator{num_writes}, + [&](cudf::size_type) { writer.write(view); }); + + std::ignore = writer.close(); + + return source_sink; +} + +cuio_source_sink_pair write_file_shape_parquet_file(cudf::type_id dtype, + cudf::size_type num_rows, + cudf::size_type num_row_groups, + cudf::size_type pages_per_row_group, + io_type source_type, + bool write_page_index) +{ + cuio_source_sink_pair source_sink(source_type); + + auto const tbl = + create_random_table({dtype}, + row_count{num_rows}, + data_profile_builder().cardinality(num_rows / 10).avg_run_length(4)); + auto const view = tbl->view(); + + auto const rows_per_page = num_rows / (num_row_groups * pages_per_row_group); + + cudf::io::parquet_writer_options write_opts = + cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view) + .compression(cudf::io::compression_type::NONE) + .row_group_size_rows(num_rows / num_row_groups) + .max_page_size_rows(rows_per_page) + // Pages are assembled out of whole fragments, so without this the default 5000-row + // fragment is a floor on page size and fewer rows per page cannot be honored + .max_page_fragment_size(rows_per_page) + // Lift the default 512KB page limit so that it does not close pages before + // `max_page_size_rows` does + .max_page_size_bytes(1ul << 30) + // Write page index by setting stats_level to STATISTICS_COLUMN + .stats_level(write_page_index ? cudf::io::statistics_freq::STATISTICS_COLUMN + : cudf::io::statistics_freq::STATISTICS_ROWGROUP); + cudf::io::write_parquet(write_opts); + + // TEMPORARY, NOT FOR MERGE: copy the file out of the sink so that its row group and page layout + // can be inspected with the parquet_inspect example. Requires `-a io_type=FILEPATH`. + if (source_type == io_type::FILEPATH) { + std::filesystem::create_directories("/tmp/file_shape"); + std::filesystem::copy_file(source_sink.make_source_info().filepaths().front(), + "/tmp/file_shape/AFTER_r" + std::to_string(num_rows) + "_rg" + + std::to_string(num_row_groups) + "_pg" + + std::to_string(pages_per_row_group) + ".parquet", + std::filesystem::copy_options::overwrite_existing); + } + + return source_sink; +} diff --git a/cpp/benchmarks/io/parquet/reader_common.hpp b/cpp/benchmarks/io/parquet/reader_common.hpp index 90f581ac5234..33664f14dd01 100644 --- a/cpp/benchmarks/io/parquet/reader_common.hpp +++ b/cpp/benchmarks/io/parquet/reader_common.hpp @@ -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 */ @@ -7,11 +7,66 @@ #include +#include + #include +#include + constexpr cudf::size_type num_cols = 64; void parquet_read_common(cudf::size_type num_rows_to_read, cudf::size_type num_cols_to_read, cuio_source_sink_pair& source_sink, nvbench::state& state); + +/** + * @brief Mixed dtypes (STRING, INTEGRAL, FLOAT, DECIMAL, LIST) cycled to fill a wide schema. + * + * @return The dtype list + */ +[[nodiscard]] std::vector const& mixed_dtypes(); + +/** + * @brief Writes a parquet file of `mixed_dtypes()` columns with a fixed number of rows per row + * group, using the chunked writer to reach the requested row group count. + * + * Shared by the metadata and setup-cost benchmarks so that the naive and hybrid scan variants + * measure the same file layout. + * + * @param num_cols Number of columns in the schema + * @param num_row_groups Number of row groups to write; must be a non-zero multiple of 10 + * @param source_type IO type of the sink to write to + * @param write_page_index Whether to write the page index (STATISTICS_COLUMN) + * + * @return The source/sink pair holding the written file + */ +[[nodiscard]] cuio_source_sink_pair write_mixed_dtype_parquet_file(cudf::size_type num_cols, + cudf::size_type num_row_groups, + io_type source_type, + bool write_page_index); + +/** + * @brief Writes a single-column parquet file with an explicitly controlled row group and page + * layout. + * + * Shared by `parquet_read_file_shape` and `hybrid_scan_file_shape` so the naive and hybrid scan + * variants see the same file layout on the same axes. The page byte limit and fragment size are + * pinned so that the requested row group and page counts are what actually land in the file. + * + * @param dtype Type of the single column + * @param num_rows Number of rows in the table + * @param num_row_groups Number of row groups to request + * @param pages_per_row_group Number of pages to request per row group + * @param source_type IO type of the sink to write to + * @param write_page_index Whether to write the page index (STATISTICS_COLUMN) + * + * @return The source/sink pair holding the written file + */ +[[nodiscard]] cuio_source_sink_pair write_file_shape_parquet_file( + cudf::type_id dtype, + cudf::size_type num_rows, + cudf::size_type num_row_groups, + cudf::size_type pages_per_row_group, + io_type source_type, + bool write_page_index); From 7bcc6b15825ed8c02d2bf94b2456fab70145c303 Mon Sep 17 00:00:00 2001 From: Qi Chen Date: Mon, 14 Sep 2026 17:19:34 +0200 Subject: [PATCH 2/4] Refactor parquet benchmarks and remove reader_common - Updated CMakeLists.txt to reorganize hybrid scan benchmarks, renaming and restructuring for clarity. - Replaced instances of `reader_common.hpp` with `parquet_common.hpp` in various parquet reader files to streamline dependencies. - Removed the obsolete `reader_common.cpp` and `reader_common.hpp` files, consolidating functionality into `parquet_common.hpp`. - Enhanced benchmarks for hybrid scan operations and parquet file handling, improving overall organization and clarity. --- cpp/benchmarks/CMakeLists.txt | 36 ++++-- .../hybrid_scan/dict_page_filter.cpp | 107 ++++++++++----- .../hybrid_scan/hybrid_scan_metadata.cpp | 14 +- .../hybrid_scan/hybrid_scan_projection.cpp | 122 ++++++++++++++++++ .../{reader_common.cpp => parquet_common.cpp} | 37 ++++-- cpp/benchmarks/io/parquet/parquet_common.hpp | 44 +++++++ .../io/parquet/parquet_reader_chunks.cpp | 2 +- .../io/parquet/parquet_reader_compressed.cpp | 2 +- .../io/parquet/parquet_reader_encoding.cpp | 2 +- .../io/parquet/parquet_reader_input.cpp | 2 +- .../io/parquet/parquet_reader_metadata.cpp | 68 +++++++--- .../io/parquet/parquet_reader_strings.cpp | 21 +-- .../io/parquet/parquet_reader_wide.cpp | 2 +- cpp/benchmarks/io/parquet/reader_common.hpp | 72 ----------- 14 files changed, 354 insertions(+), 177 deletions(-) create mode 100644 cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_projection.cpp rename cpp/benchmarks/io/parquet/{reader_common.cpp => parquet_common.cpp} (84%) create mode 100644 cpp/benchmarks/io/parquet/parquet_common.hpp delete mode 100644 cpp/benchmarks/io/parquet/reader_common.hpp diff --git a/cpp/benchmarks/CMakeLists.txt b/cpp/benchmarks/CMakeLists.txt index d212c5a6b792..f231931be6ad 100644 --- a/cpp/benchmarks/CMakeLists.txt +++ b/cpp/benchmarks/CMakeLists.txt @@ -256,18 +256,18 @@ ConfigureNVBench( ConfigureNVBench(HASHING_NVBENCH hashing/hash.cpp hashing/partition.cpp) # ################################################################################################## -# * hybrid scan filters benchmark +# * hybrid scan column resolution benchmark # ---------------------------------------------------------------------- ConfigureNVBench( - HYBRID_SCAN_FILTERS_NVBENCH io/parquet/experimental/hybrid_scan/dict_page_filter.cpp + HYBRID_SCAN_PROJECTION_NVBENCH io/parquet/experimental/hybrid_scan/hybrid_scan_projection.cpp + io/parquet/parquet_common.cpp ) # ################################################################################################## -# * hybrid scan multithread benchmark +# * hybrid scan filters benchmark # ---------------------------------------------------------------------- ConfigureNVBench( - HYBRID_SCAN_MULTITHREAD_NVBENCH io/parquet/experimental/hybrid_scan/hybrid_scan_multithread.cpp - io/parquet/experimental/hybrid_scan/hybrid_scan_composer.cpp + HYBRID_SCAN_FILTERS_NVBENCH io/parquet/experimental/hybrid_scan/dict_page_filter.cpp ) # ################################################################################################## @@ -275,7 +275,15 @@ ConfigureNVBench( # ---------------------------------------------------------------------- ConfigureNVBench( HYBRID_SCAN_METADATA_NVBENCH io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp - io/parquet/reader_common.cpp + io/parquet/parquet_common.cpp +) + +# ################################################################################################## +# * hybrid scan multithread benchmark +# ---------------------------------------------------------------------- +ConfigureNVBench( + HYBRID_SCAN_MULTITHREAD_NVBENCH io/parquet/experimental/hybrid_scan/hybrid_scan_multithread.cpp + io/parquet/experimental/hybrid_scan/hybrid_scan_composer.cpp ) # ################################################################################################## @@ -305,34 +313,35 @@ ConfigureNVBench( # * parquet reader benchmark ---------------------------------------------------------------------- ConfigureNVBench( PARQUET_READER_NVBENCH io/parquet/parquet_reader_input.cpp io/parquet/parquet_reader_encoding.cpp - io/parquet/parquet_reader_options.cpp io/parquet/reader_common.cpp + io/parquet/parquet_reader_options.cpp io/parquet/parquet_common.cpp ) # ################################################################################################## # * parquet reader chunks benchmark # ------------------------------------------------------------------ ConfigureNVBench( - PARQUET_READER_CHUNKS_NVBENCH io/parquet/parquet_reader_chunks.cpp io/parquet/reader_common.cpp + PARQUET_READER_CHUNKS_NVBENCH io/parquet/parquet_reader_chunks.cpp io/parquet/parquet_common.cpp ) # ################################################################################################## # * parquet reader compressed benchmark ------------------------------------------------------------ ConfigureNVBench( PARQUET_READER_COMPRESSED_NVBENCH io/parquet/parquet_reader_compressed.cpp - io/parquet/reader_common.cpp + io/parquet/parquet_common.cpp ) # ################################################################################################## # * parquet reader strings benchmark # ------------------------------------------------------------------ ConfigureNVBench( - PARQUET_READER_STRINGS_NVBENCH io/parquet/parquet_reader_strings.cpp io/parquet/reader_common.cpp + PARQUET_READER_STRINGS_NVBENCH io/parquet/parquet_reader_strings.cpp + io/parquet/parquet_common.cpp ) # ################################################################################################## # * parquet reader wide benchmark ------------------------------------------------------------------ ConfigureNVBench( - PARQUET_READER_WIDE_NVBENCH io/parquet/parquet_reader_wide.cpp io/parquet/reader_common.cpp + PARQUET_READER_WIDE_NVBENCH io/parquet/parquet_reader_wide.cpp io/parquet/parquet_common.cpp ) # ################################################################################################## @@ -343,7 +352,10 @@ ConfigureNVBench(PARQUET_READER_FILTER_NVBENCH io/parquet/parquet_reader_filter. # ################################################################################################## # * parquet reader metadata benchmark # ---------------------------------------------------------------------- -ConfigureNVBench(PARQUET_READER_METADATA_NVBENCH io/parquet/parquet_reader_metadata.cpp) +ConfigureNVBench( + PARQUET_READER_METADATA_NVBENCH io/parquet/parquet_reader_metadata.cpp + io/parquet/parquet_common.cpp +) # ################################################################################################## # * parquet deletion vector benchmark diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp index d38bc7e60d40..0d366e7c6012 100644 --- a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp @@ -13,7 +13,10 @@ #include #include #include +#include #include +#include +#include #include @@ -21,11 +24,12 @@ constexpr cudf::size_type num_cols = 8; -void BM_filter_string_row_groups_with_dicts_common(nvbench::state& state, - data_profile const& table_profile, - cudf::ast::operation const& filter_expr, - double average_str_length, - cudf::size_type cardinality) +void BM_filter_row_groups_with_dicts_common(nvbench::state& state, + cudf::type_id dtype, + data_profile const& table_profile, + cudf::ast::operation const& filter_expr, + double average_value_width, + cudf::size_type cardinality) { auto const num_row_groups = static_cast(state.get_int64("num_row_groups")); auto constexpr rows_per_row_group = 5'000; //< Chosen such that it is not ignored by the writer @@ -35,8 +39,8 @@ void BM_filter_string_row_groups_with_dicts_common(nvbench::state& state, // Write table to parquet { - auto const table = create_random_table( - cycle_dtypes({cudf::type_id::STRING}, num_cols), row_count{num_rows}, table_profile); + auto const table = + create_random_table(cycle_dtypes({dtype}, num_cols), row_count{num_rows}, table_profile); cudf::io::parquet_writer_options write_opts = cudf::io::parquet_writer_options::builder(cudf::io::sink_info(&parquet_buffer), table->view()) @@ -110,8 +114,8 @@ void BM_filter_string_row_groups_with_dicts_common(nvbench::state& state, state.add_buffer_size( mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); state.add_element_count( - static_cast(cardinality * num_row_groups * average_str_length) / time, - "strings_per_second"); + static_cast(cardinality * num_row_groups * average_value_width) / time, + "values_per_second"); auto const total_dict_data_size = std::accumulate(dict_page_byte_ranges.begin(), dict_page_byte_ranges.end(), @@ -120,44 +124,89 @@ void BM_filter_string_row_groups_with_dicts_common(nvbench::state& state, state.add_buffer_size(total_dict_data_size, "total_dict_data_size", "total_dict_data_size"); } -void BM_filter_string_rowgroups_with_dicts(nvbench::state& state) +template +void run_dict_page_pruning(nvbench::state& state, + cudf::type_id dtype, + data_profile const& table_profile, + ScalarType& filter_value, + double average_value_width, + cudf::size_type cardinality) { - auto const min_length = static_cast(state.get_int64("min_length")); - auto const max_length = static_cast(state.get_int64("max_length")); - auto const cardinality = static_cast(state.get_int64("cardinality")); auto const is_inline_eval = static_cast(state.get_int64("is_inline")); - auto table_profile = - data_profile_builder() - .distribution(cudf::type_id::STRING, distribution_id::NORMAL, min_length, max_length) - .cardinality(cardinality); - auto col_ref = cudf::ast::column_name_reference("_col0"); - auto scalar = cudf::string_scalar("000010000"); - auto literal = cudf::ast::literal(scalar); + auto literal = cudf::ast::literal(filter_value); auto expr1 = cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref, literal); auto expr2 = cudf::ast::operation(cudf::ast::ast_operator::NOT_EQUAL, col_ref, literal); auto expr3 = cudf::ast::operation(cudf::ast::ast_operator::EQUAL, col_ref, literal); auto filter_expr_few_literals = cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_AND, expr1, expr2); - auto filter_expr_many_literals = cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_OR, filter_expr_few_literals, expr3); - return BM_filter_string_row_groups_with_dicts_common( - state, - table_profile, - is_inline_eval ? filter_expr_few_literals : filter_expr_many_literals, - (static_cast(min_length) + static_cast(max_length)) / 2, - cardinality); + BM_filter_row_groups_with_dicts_common(state, + dtype, + table_profile, + is_inline_eval ? filter_expr_few_literals + : filter_expr_many_literals, + average_value_width, + cardinality); +} + +void BM_hybrid_scan_dict_page_pruning_string(nvbench::state& state) +{ + auto const min_length = static_cast(state.get_int64("min_length")); + auto const max_length = static_cast(state.get_int64("max_length")); + auto const cardinality = static_cast(state.get_int64("cardinality")); + + auto table_profile = data_profile_builder().cardinality(cardinality); + table_profile.distribution( + cudf::type_id::STRING, distribution_id::NORMAL, min_length, max_length); + + auto filter_value = cudf::string_scalar("000010000"); + run_dict_page_pruning(state, + cudf::type_id::STRING, + table_profile, + filter_value, + (static_cast(min_length) + static_cast(max_length)) / 2.0, + cardinality); +} + +template +void BM_hybrid_scan_dict_page_pruning_fixed_width(nvbench::state& state, + nvbench::type_list>) +{ + auto const cardinality = static_cast(state.get_int64("cardinality")); + + using T = cudf::id_to_type; + auto filter_value = cudf::numeric_scalar(static_cast(0)); + + // The dictionary entry width of a fixed-width column is the type width + run_dict_page_pruning(state, + DType, + data_profile_builder().cardinality(cardinality), + filter_value, + static_cast(cudf::size_of(cudf::data_type{DType})), + cardinality); } -NVBENCH_BENCH(BM_filter_string_rowgroups_with_dicts) - .set_name("hybrid_scan_filter_string_rowgroups_with_dicts") +using dict_fixed_width_dtypes = nvbench::enum_type_list; + +NVBENCH_BENCH(BM_hybrid_scan_dict_page_pruning_string) + .set_name("hybrid_scan_dict_page_pruning_string") .set_min_samples(4) .add_int64_axis("num_row_groups", {32, 64, 128}) .add_int64_axis("min_length", {4}) .add_int64_axis("max_length", {64, 128}) .add_int64_axis("cardinality", {1'000, 10'000}) .add_int64_axis("is_inline", {true, false}); + +NVBENCH_BENCH_TYPES(BM_hybrid_scan_dict_page_pruning_fixed_width, + NVBENCH_TYPE_AXES(dict_fixed_width_dtypes)) + .set_name("hybrid_scan_dict_page_pruning_fixed_width") + .set_type_axes_names({"dtype"}) + .set_min_samples(4) + .add_int64_axis("num_row_groups", {32, 64, 128}) + .add_int64_axis("cardinality", {1'000, 10'000}) + .add_int64_axis("is_inline", {true, false}); diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp index 9f4e08f8014f..839e7aed21db 100644 --- a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include @@ -20,11 +20,6 @@ #include #include -/** - * Benchmark for hybrid scan's caller-visible setup path. - * - * Every cell runs the whole setup path and brackets the timer around one step. - */ enum class setup_phase : int32_t { FOOTER_FETCH, // copy footer bytes to host READER_CTOR, // parse footer, construct reader @@ -121,14 +116,11 @@ void BM_hybrid_scan_setup_phase(nvbench::state& state, mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); } -// Isolated page-index fetch vs parse as page count grows. +// Measure how page-index fetch and setup cost scales with file shape. template void BM_hybrid_scan_file_shape(nvbench::state& state, nvbench::type_list>) { - // Matches parquet_read_file_shape, which needs a string column for the naive reader to read the - // page index at all - auto constexpr d_type = cudf::type_id::STRING; - // Both phases measured here parse the page index, so it is always written + auto constexpr d_type = cudf::type_id::STRING; auto constexpr write_page_index = true; auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_projection.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_projection.cpp new file mode 100644 index 000000000000..03a9737525a3 --- /dev/null +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_projection.cpp @@ -0,0 +1,122 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +enum class projection_side : int32_t { + FILTER = 0, // one or two filter column names + PAYLOAD = 1, // payload set derived from the schema, minus filter columns + PAYLOAD_EXPLICIT = 2 // payload set supplied explicitly, minus filter columns +}; + +// NVBENCH_DECLARE_ENUM_TYPE_STRINGS macro must be used from global namespace scope +NVBENCH_DECLARE_ENUM_TYPE_STRINGS( + projection_side, + [](projection_side value) { + switch (value) { + case projection_side::FILTER: return "FILTER"; + case projection_side::PAYLOAD: return "PAYLOAD"; + case projection_side::PAYLOAD_EXPLICIT: return "PAYLOAD_EXPLICIT"; + default: return "Unknown"; + } + }, + [](auto) { return std::string{}; }) + +template +void BM_hybrid_scan_projection(nvbench::state& state, nvbench::type_list>) +{ + auto const num_cols = static_cast(state.get_int64("num_cols")); + + auto source_sink = write_named_resolution_parquet_file(num_cols, io_type::FILEPATH); + + // The deterministic column names the fixture wrote, regenerated here for the PAYLOAD_EXPLICIT + // cell's projection. + std::vector column_names(num_cols); + for (cudf::size_type i = 0; i < num_cols; ++i) { + column_names[i] = "col" + std::to_string(i); + } + + cudf::numeric_scalar filter_literal{std::numeric_limits::min()}; + cudf::ast::tree filter_tree; + auto const& col_ref = filter_tree.push(cudf::ast::column_name_reference("col0")); + auto const& lit = filter_tree.push(cudf::ast::literal(filter_literal)); + auto const& filter_expr = + filter_tree.push(cudf::ast::operation(cudf::ast::ast_operator::GREATER_EQUAL, col_ref, lit)); + + auto read_opts_builder = + cudf::io::parquet_reader_options::builder(source_sink.make_source_info()).filter(filter_expr); + + // Caller-supplied payload list + if constexpr (Side == projection_side::PAYLOAD_EXPLICIT) { + read_opts_builder.column_names(column_names); + } + auto const read_opts = read_opts_builder.build(); + + auto const source_info = source_sink.make_source_info(); + auto datasource = std::move(cudf::io::make_datasources(source_info).front()); + auto const footer = cudf::io::parquet::fetch_footer_to_host(*datasource); + auto const file_metadata = + cudf::io::parquet::experimental::hybrid_scan_metadata{*footer, read_opts}; + auto reader = + std::make_unique(file_metadata); + auto const row_groups = reader->all_row_groups(read_opts); + + if constexpr (Side == projection_side::PAYLOAD or Side == projection_side::PAYLOAD_EXPLICIT) { + // Prime the filter-side column selection so payload measures the expensive branch. + // Without this the payload cell resolves through the cheap branch. + std::ignore = reader->filter_column_chunks_byte_ranges(row_groups, read_opts); + } + + auto const mem_stats_logger = cudf::memory_stats_logger(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().get())); + + state.exec(nvbench::exec_tag::sync | nvbench::exec_tag::timer, + [&](nvbench::launch& launch, auto& timer) { + drop_page_cache_if_enabled(source_info.filepaths()); + reader->reset_column_selection(); + + timer.start(); + if constexpr (Side == projection_side::FILTER) { + std::ignore = reader->filter_column_chunks_byte_ranges(row_groups, read_opts); + } else { + // PAYLOAD and PAYLOAD_EXPLICIT differ only in whether the payload list is + // caller-supplied or derived from the schema + std::ignore = reader->payload_column_chunks_byte_ranges(row_groups, read_opts); + } + timer.stop(); + }); + + auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(num_cols) / time, "cols_per_sec"); + state.add_buffer_size( + mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); +} + +using projection_sides = nvbench::enum_type_list; + +NVBENCH_BENCH_TYPES(BM_hybrid_scan_projection, NVBENCH_TYPE_AXES(projection_sides)) + .set_name("hybrid_scan_projection") + .set_type_axes_names({"side"}) + .set_min_samples(4) + .add_int64_axis("num_cols", {64, 512, 2048, 4096}); diff --git a/cpp/benchmarks/io/parquet/reader_common.cpp b/cpp/benchmarks/io/parquet/parquet_common.cpp similarity index 84% rename from cpp/benchmarks/io/parquet/reader_common.cpp rename to cpp/benchmarks/io/parquet/parquet_common.cpp index 8e2df0e32fe2..e60133deccc8 100644 --- a/cpp/benchmarks/io/parquet/reader_common.cpp +++ b/cpp/benchmarks/io/parquet/parquet_common.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include @@ -19,7 +19,6 @@ #include #include -#include // TEMPORARY, NOT FOR MERGE #include void parquet_read_common(cudf::size_type num_rows_to_read, @@ -142,16 +141,32 @@ cuio_source_sink_pair write_file_shape_parquet_file(cudf::type_id dtype, : cudf::io::statistics_freq::STATISTICS_ROWGROUP); cudf::io::write_parquet(write_opts); - // TEMPORARY, NOT FOR MERGE: copy the file out of the sink so that its row group and page layout - // can be inspected with the parquet_inspect example. Requires `-a io_type=FILEPATH`. - if (source_type == io_type::FILEPATH) { - std::filesystem::create_directories("/tmp/file_shape"); - std::filesystem::copy_file(source_sink.make_source_info().filepaths().front(), - "/tmp/file_shape/AFTER_r" + std::to_string(num_rows) + "_rg" + - std::to_string(num_row_groups) + "_pg" + - std::to_string(pages_per_row_group) + ".parquet", - std::filesystem::copy_options::overwrite_existing); + return source_sink; +} + +cuio_source_sink_pair write_named_resolution_parquet_file(cudf::size_type num_cols, + io_type source_type) +{ + cuio_source_sink_pair source_sink(source_type); + + // A single INT32 row keeps the file negligible so name resolution dominates the read; resolution + // cost is independent of dtype, so the cheapest type is used. + 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()); + + cudf::io::table_input_metadata input_meta(tbl->view()); + for (cudf::size_type i = 0; i < num_cols; ++i) { + input_meta.column_metadata[i].set_name("col" + std::to_string(i)); } + cudf::io::parquet_writer_options write_opts = + cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), tbl->view()) + .metadata(std::move(input_meta)) + .compression(cudf::io::compression_type::NONE); + cudf::io::write_parquet(write_opts); + return source_sink; } diff --git a/cpp/benchmarks/io/parquet/parquet_common.hpp b/cpp/benchmarks/io/parquet/parquet_common.hpp new file mode 100644 index 000000000000..da772b317452 --- /dev/null +++ b/cpp/benchmarks/io/parquet/parquet_common.hpp @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include + +#include + +#include + +constexpr cudf::size_type num_cols = 64; + +void parquet_read_common(cudf::size_type num_rows_to_read, + cudf::size_type num_cols_to_read, + cuio_source_sink_pair& source_sink, + nvbench::state& state); + +// A mix of string, integral, float, decimal and list types used to fill a schema of the +// requested width +[[nodiscard]] std::vector const& mixed_dtypes(); + +// Writes a mixed-type file with a fixed number of rows per row group +[[nodiscard]] cuio_source_sink_pair write_mixed_dtype_parquet_file(cudf::size_type num_cols, + cudf::size_type num_row_groups, + io_type source_type, + bool write_page_index); + +// Writes a single-column file with an explicitly controlled row group and page layout +[[nodiscard]] cuio_source_sink_pair write_file_shape_parquet_file( + cudf::type_id dtype, + cudf::size_type num_rows, + cudf::size_type num_row_groups, + cudf::size_type pages_per_row_group, + io_type source_type, + bool write_page_index); + +// Writes a negligible single-row file with deterministically named columns +[[nodiscard]] cuio_source_sink_pair write_named_resolution_parquet_file(cudf::size_type num_cols, + io_type source_type); diff --git a/cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp b/cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp index 9d58cce16b70..01dc78b81fd8 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include diff --git a/cpp/benchmarks/io/parquet/parquet_reader_compressed.cpp b/cpp/benchmarks/io/parquet/parquet_reader_compressed.cpp index 1f66b6bd6079..93b59b52b859 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_compressed.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_compressed.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include diff --git a/cpp/benchmarks/io/parquet/parquet_reader_encoding.cpp b/cpp/benchmarks/io/parquet/parquet_reader_encoding.cpp index dcf8d9a0e130..38f72c0cefd7 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_encoding.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_encoding.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include diff --git a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp index 748ce682c77d..1ec72af694e8 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_input.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_input.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include diff --git a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp index 402cbb675d26..df4fb5fd0f0b 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include @@ -195,30 +195,14 @@ void BM_parquet_filter_name_resolution(nvbench::state& state) 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(); + auto source_sink = write_named_resolution_parquet_file(num_cols, source_type); - cudf::io::table_input_metadata input_meta(view); + // The deterministic column names the fixture wrote, regenerated here to build the filter tree. std::vector 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) { @@ -292,6 +276,46 @@ void BM_parquet_filter_name_resolution(nvbench::state& state) mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); } +// Benchmark full-projection column-name resolution during naive reader construction. +void BM_parquet_read_column_projection(nvbench::state& state) +{ + auto const num_cols = static_cast(state.get_int64("num_cols")); + auto const source_type = retrieve_io_type_enum(state.get_string("io_type")); + + auto source_sink = write_named_resolution_parquet_file(num_cols, source_type); + + // The deterministic column names the fixture wrote, requested here as a full projection. + std::vector column_names(num_cols); + for (cudf::size_type i = 0; i < num_cols; i++) { + column_names[i] = "col" + std::to_string(i); + } + + auto read_opts = cudf::io::parquet_reader_options::builder(source_sink.make_source_info()) + .column_names(column_names) + .build(); + + auto const mem_stats_logger = cudf::memory_stats_logger(); + state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().get())); + 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()); + // Measures column selection alone + auto sources = cudf::io::make_datasources(source_info); + auto metadatas = cudf::io::read_parquet_footers(sources); + + timer.start(); + [[maybe_unused]] auto const reader = + cudf::io::chunked_parquet_reader(0, 0, std::move(sources), std::move(metadatas), read_opts); + timer.stop(); + }); + + auto const time = state.get_summary("nv/cold/time/gpu/mean").get_float64("value"); + state.add_element_count(static_cast(num_cols) / time, "cols_per_sec"); + 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) @@ -321,3 +345,9 @@ NVBENCH_BENCH(BM_parquet_filter_name_resolution) .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}); + +NVBENCH_BENCH(BM_parquet_read_column_projection) + .set_name("parquet_read_column_projection") + .set_min_samples(4) + .add_string_axis("io_type", {"FILEPATH"}) + .add_int64_axis("num_cols", {64, 512, 2048, 4096}); diff --git a/cpp/benchmarks/io/parquet/parquet_reader_strings.cpp b/cpp/benchmarks/io/parquet/parquet_reader_strings.cpp index a89b4548cc33..26517a088c65 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_strings.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_strings.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include @@ -75,23 +75,8 @@ void BM_parquet_read_file_shape(nvbench::state& state) static_cast(state.get_int64("pages_per_row_group")); auto const has_page_idx = static_cast(state.get_int64("has_page_idx")); - cuio_source_sink_pair source_sink(source_type); - - auto const tbl = - create_random_table({d_type}, - row_count{num_rows}, - data_profile_builder().cardinality(num_rows / 10).avg_run_length(4)); - auto const view = tbl->view(); - - cudf::io::parquet_writer_options write_opts = - cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), view) - .compression(cudf::io::compression_type::NONE) - .row_group_size_rows(num_rows / num_row_groups) - .max_page_size_rows(num_rows / (num_row_groups * num_pages_per_row_group)) - // Write page index by setting stats_level to STATISTICS_COLUMN - .stats_level(has_page_idx ? cudf::io::statistics_freq::STATISTICS_COLUMN - : cudf::io::statistics_freq::STATISTICS_ROWGROUP); - cudf::io::write_parquet(write_opts); + auto source_sink = write_file_shape_parquet_file( + d_type, num_rows, num_row_groups, num_pages_per_row_group, source_type, has_page_idx); cudf::io::parquet_reader_options read_opts = cudf::io::parquet_reader_options::builder(source_sink.make_source_info()); diff --git a/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp b/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp index a4149715ab3d..a810e7b8bf0c 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -#include "reader_common.hpp" +#include "parquet_common.hpp" #include #include diff --git a/cpp/benchmarks/io/parquet/reader_common.hpp b/cpp/benchmarks/io/parquet/reader_common.hpp deleted file mode 100644 index 33664f14dd01..000000000000 --- a/cpp/benchmarks/io/parquet/reader_common.hpp +++ /dev/null @@ -1,72 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include - -#include - -#include - -#include - -constexpr cudf::size_type num_cols = 64; - -void parquet_read_common(cudf::size_type num_rows_to_read, - cudf::size_type num_cols_to_read, - cuio_source_sink_pair& source_sink, - nvbench::state& state); - -/** - * @brief Mixed dtypes (STRING, INTEGRAL, FLOAT, DECIMAL, LIST) cycled to fill a wide schema. - * - * @return The dtype list - */ -[[nodiscard]] std::vector const& mixed_dtypes(); - -/** - * @brief Writes a parquet file of `mixed_dtypes()` columns with a fixed number of rows per row - * group, using the chunked writer to reach the requested row group count. - * - * Shared by the metadata and setup-cost benchmarks so that the naive and hybrid scan variants - * measure the same file layout. - * - * @param num_cols Number of columns in the schema - * @param num_row_groups Number of row groups to write; must be a non-zero multiple of 10 - * @param source_type IO type of the sink to write to - * @param write_page_index Whether to write the page index (STATISTICS_COLUMN) - * - * @return The source/sink pair holding the written file - */ -[[nodiscard]] cuio_source_sink_pair write_mixed_dtype_parquet_file(cudf::size_type num_cols, - cudf::size_type num_row_groups, - io_type source_type, - bool write_page_index); - -/** - * @brief Writes a single-column parquet file with an explicitly controlled row group and page - * layout. - * - * Shared by `parquet_read_file_shape` and `hybrid_scan_file_shape` so the naive and hybrid scan - * variants see the same file layout on the same axes. The page byte limit and fragment size are - * pinned so that the requested row group and page counts are what actually land in the file. - * - * @param dtype Type of the single column - * @param num_rows Number of rows in the table - * @param num_row_groups Number of row groups to request - * @param pages_per_row_group Number of pages to request per row group - * @param source_type IO type of the sink to write to - * @param write_page_index Whether to write the page index (STATISTICS_COLUMN) - * - * @return The source/sink pair holding the written file - */ -[[nodiscard]] cuio_source_sink_pair write_file_shape_parquet_file( - cudf::type_id dtype, - cudf::size_type num_rows, - cudf::size_type num_row_groups, - cudf::size_type pages_per_row_group, - io_type source_type, - bool write_page_index); From 9a2fd60df4b722ac9e52a85b13cddd2ecef15695 Mon Sep 17 00:00:00 2001 From: Qi Chen Date: Tue, 15 Sep 2026 09:11:02 +0200 Subject: [PATCH 3/4] pre-commit --- .../hybrid_scan/dict_page_filter.cpp | 16 ++++++++-------- .../io/parquet/parquet_reader_wide.cpp | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp index 0d366e7c6012..f4a341451dd6 100644 --- a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/dict_page_filter.cpp @@ -145,13 +145,13 @@ void run_dict_page_pruning(nvbench::state& state, auto filter_expr_many_literals = cudf::ast::operation(cudf::ast::ast_operator::LOGICAL_OR, filter_expr_few_literals, expr3); - BM_filter_row_groups_with_dicts_common(state, - dtype, - table_profile, - is_inline_eval ? filter_expr_few_literals - : filter_expr_many_literals, - average_value_width, - cardinality); + BM_filter_row_groups_with_dicts_common( + state, + dtype, + table_profile, + is_inline_eval ? filter_expr_few_literals : filter_expr_many_literals, + average_value_width, + cardinality); } void BM_hybrid_scan_dict_page_pruning_string(nvbench::state& state) @@ -179,7 +179,7 @@ void BM_hybrid_scan_dict_page_pruning_fixed_width(nvbench::state& state, { auto const cardinality = static_cast(state.get_int64("cardinality")); - using T = cudf::id_to_type; + using T = cudf::id_to_type; auto filter_value = cudf::numeric_scalar(static_cast(0)); // The dictionary entry width of a fixed-width column is the type width diff --git a/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp b/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp index a810e7b8bf0c..dbd8edb80ebf 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_wide.cpp @@ -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 */ From a90d85bd0c173126956aa0bb9923c4a9295afc19 Mon Sep 17 00:00:00 2001 From: Qi Chen Date: Tue, 15 Sep 2026 09:41:10 +0200 Subject: [PATCH 4/4] Update comments --- .../experimental/hybrid_scan/hybrid_scan_metadata.cpp | 1 - cpp/benchmarks/io/parquet/parquet_common.cpp | 5 ++--- cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp | 9 ++------- 3 files changed, 4 insertions(+), 11 deletions(-) diff --git a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp index 839e7aed21db..e851920fd914 100644 --- a/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp +++ b/cpp/benchmarks/io/parquet/experimental/hybrid_scan/hybrid_scan_metadata.cpp @@ -173,7 +173,6 @@ NVBENCH_BENCH_TYPES(BM_hybrid_scan_file_shape, NVBENCH_TYPE_AXES(page_index_phas .set_name("hybrid_scan_file_shape") .set_type_axes_names({"phase"}) .set_min_samples(4) - // Axes match parquet_read_file_shape so the naive and hybrid scan paths can be compared .add_string_axis("io_type", {"DEVICE_BUFFER"}) .add_int64_axis("num_rows", {10'000'000, 100'000'000}) .add_int64_axis("num_row_groups", {1, 10}) diff --git a/cpp/benchmarks/io/parquet/parquet_common.cpp b/cpp/benchmarks/io/parquet/parquet_common.cpp index e60133deccc8..96ac7c03673d 100644 --- a/cpp/benchmarks/io/parquet/parquet_common.cpp +++ b/cpp/benchmarks/io/parquet/parquet_common.cpp @@ -136,7 +136,6 @@ cuio_source_sink_pair write_file_shape_parquet_file(cudf::type_id dtype, // Lift the default 512KB page limit so that it does not close pages before // `max_page_size_rows` does .max_page_size_bytes(1ul << 30) - // Write page index by setting stats_level to STATISTICS_COLUMN .stats_level(write_page_index ? cudf::io::statistics_freq::STATISTICS_COLUMN : cudf::io::statistics_freq::STATISTICS_ROWGROUP); cudf::io::write_parquet(write_opts); @@ -149,8 +148,8 @@ cuio_source_sink_pair write_named_resolution_parquet_file(cudf::size_type num_co { cuio_source_sink_pair source_sink(source_type); - // A single INT32 row keeps the file negligible so name resolution dominates the read; resolution - // cost is independent of dtype, so the cheapest type is used. + // 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), diff --git a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp index df4fb5fd0f0b..29ec84e4f0b5 100644 --- a/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp +++ b/cpp/benchmarks/io/parquet/parquet_reader_metadata.cpp @@ -51,9 +51,7 @@ namespace { } // namespace -// Benchmark to measure parquet footer read time. `read_parquet_footers` also loads page indexes -// when present. Pair the result with `hybrid_scan_reader_construction` (full hybrid setup), not -// with an isolated `hybrid_scan_footer` phase. +// Benchmark to measure parquet footer read time void BM_parquet_read_footer(nvbench::state& state) { auto const num_cols = static_cast(state.get_int64("num_cols")); @@ -92,8 +90,7 @@ void BM_parquet_read_footer(nvbench::state& state) mem_stats_logger.peak_memory_usage(), "peak_memory_usage", "peak_memory_usage"); } -// Benchmark to measure chunked parquet reader construction time. Pair with -// `hybrid_scan_reader_construction` on the same axes. +// Benchmark to measure chunked parquet reader construction time void BM_parquet_reader_construction(nvbench::state& state) { auto const num_cols = static_cast(state.get_int64("num_cols")); @@ -197,7 +194,6 @@ void BM_parquet_filter_name_resolution(nvbench::state& state) auto source_sink = write_named_resolution_parquet_file(num_cols, source_type); - // The deterministic column names the fixture wrote, regenerated here to build the filter tree. std::vector file_names(num_cols); for (cudf::size_type i = 0; i < num_cols; i++) { file_names[i] = "col" + std::to_string(i); @@ -284,7 +280,6 @@ void BM_parquet_read_column_projection(nvbench::state& state) auto source_sink = write_named_resolution_parquet_file(num_cols, source_type); - // The deterministic column names the fixture wrote, requested here as a full projection. std::vector column_names(num_cols); for (cudf::size_type i = 0; i < num_cols; i++) { column_names[i] = "col" + std::to_string(i);