-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Write variable bit-width keys for Parquet dictionary encoded pages #22279
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rapids-bot
merged 45 commits into
NVIDIA:main
from
mhaseeb123:fea/pq-dict-encode-optimize
May 15, 2026
Merged
Changes from all commits
Commits
Show all changes
45 commits
Select commit
Hold shift + click to select a range
786f1e0
Phase 0
mhaseeb123 08f02ae
OPT: assign parquet dict_ids in first-appearance order (#13995)
mhaseeb123 5cc906e
OPT: per-page variable bit-width RLE for parquet dict indices (#13995)
mhaseeb123 5b07584
Update benchmark
mhaseeb123 1f2c46c
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 7dd5460
Improve tests
mhaseeb123 4600d21
Minor
mhaseeb123 14469b9
Minor
mhaseeb123 fda8a8a
Humanize phase 1
mhaseeb123 816902b
Clean up
mhaseeb123 6686d19
humanize gtest
mhaseeb123 47f7ad3
Cleanup
mhaseeb123 47012e2
Cleanup
mhaseeb123 cdc379e
Cleanup
mhaseeb123 589f9d0
Humanize phase 2
mhaseeb123 6982ab4
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 01c41f6
Style fix
mhaseeb123 46761ec
Humanize phase 3
mhaseeb123 3ffbf46
Add todo
mhaseeb123 818d9a0
Minor
mhaseeb123 e84dfc6
Style again
mhaseeb123 2954870
Copilot's comments
mhaseeb123 db808a7
Minor
mhaseeb123 b9209e9
Minor
mhaseeb123 ccc7bf9
Apply suggestion from @mhaseeb123
mhaseeb123 9a755c1
Use freq instead of hot to match with rare
mhaseeb123 6da2c43
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 f3002f8
Minor
mhaseeb123 6d6717b
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 c192583
Merge branch 'fea/pq-dict-encode-optimize' of https://github.com/mhas…
mhaseeb123 ccf8b25
Modify benchmark to use string type
mhaseeb123 a3bb04e
Widen PageFragment::num_rows field to int32
mhaseeb123 7a5ab88
Add comment
mhaseeb123 1598e5a
style
mhaseeb123 7cc001c
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 2262d52
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 758b9dd
Apply suggestion
mhaseeb123 ac9f3d3
Disable some checks
mhaseeb123 b2a416c
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 84a9ada
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 93a0b69
Minor
mhaseeb123 1ceaaef
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 91a266f
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 d2b1028
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 e84a14f
Merge branch 'main' into fea/pq-dict-encode-optimize
mhaseeb123 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| #include "io/parquet/compact_protocol_reader.hpp" | ||
|
|
||
| #include <benchmarks/common/memory_stats.hpp> | ||
| #include <benchmarks/io/cuio_common.hpp> | ||
|
|
||
| #include <cudf_test/column_wrapper.hpp> | ||
|
|
||
| #include <cudf/column/column.hpp> | ||
| #include <cudf/io/datasource.hpp> | ||
| #include <cudf/io/experimental/hybrid_scan.hpp> | ||
| #include <cudf/io/parquet.hpp> | ||
| #include <cudf/io/parquet_io_utils.hpp> | ||
| #include <cudf/io/parquet_schema.hpp> | ||
| #include <cudf/table/table.hpp> | ||
| #include <cudf/types.hpp> | ||
| #include <cudf/utilities/default_stream.hpp> | ||
| #include <cudf/utilities/error.hpp> | ||
|
|
||
| #include <nvbench/nvbench.cuh> | ||
|
|
||
| #include <algorithm> | ||
| #include <cmath> | ||
| #include <numeric> | ||
| #include <random> | ||
| #include <string> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
||
| namespace { | ||
|
|
||
| constexpr auto frequent_pages_ratio = | ||
| 0.8; ///< 80% of the pages will only contain elements from the frequent set | ||
|
|
||
| /** | ||
| * @brief Build a string column such that certain pages only contain elements from the frequent set | ||
| and others only contain elements from the rare set | ||
| * | ||
| * Each generated value is of the form `"k_<v>"`, where `v` is a `cudf::size_type` index in `[0, | ||
| cardinality)`, so cardinality maps 1:1 to distinct strings. | ||
| * | ||
| * @param num_rows Total number of rows | ||
| * @param page_size_rows Number of rows per page | ||
| * @param cardinality Total number of distinct values | ||
| * @param frequent_set_ratio Fraction of `cardinality` assigned to the frequent set | ||
| * @return Constructed column values | ||
| */ | ||
| std::vector<std::string> build_string_column(cudf::size_type num_rows, | ||
| cudf::size_type page_size_rows, | ||
| cudf::size_type cardinality, | ||
| double frequent_set_ratio) | ||
| { | ||
| static constexpr auto dict_rng_seed = 0xC0DEFACE; | ||
|
|
||
| CUDF_EXPECTS(frequent_set_ratio > 0.0 and frequent_set_ratio < 1.0, | ||
| "frequent_set_ratio must be between 0.0 and 1.0"); | ||
| CUDF_EXPECTS(num_rows % page_size_rows == 0, "num_rows must be a multiple of page_size_rows"); | ||
| static_assert(frequent_pages_ratio > 0.0 and frequent_pages_ratio < 1.0, | ||
| "frequent_pages_ratio must be between 0.0 and 1.0"); | ||
|
|
||
| auto const total_pages = num_rows / page_size_rows; | ||
| auto const frequent_set_threshold = | ||
| static_cast<cudf::size_type>(total_pages * frequent_pages_ratio) * page_size_rows; | ||
|
|
||
| auto const frequent_set_size = | ||
| static_cast<cudf::size_type>(static_cast<double>(cardinality) * frequent_set_ratio); | ||
|
|
||
| std::mt19937 rng{dict_rng_seed}; | ||
| std::uniform_int_distribution<cudf::size_type> freq_dist(0, frequent_set_size - 1); | ||
| std::uniform_int_distribution<cudf::size_type> rare_dist(frequent_set_size, cardinality - 1); | ||
|
|
||
| cudf::size_type row_idx = 0; | ||
| std::vector<std::string> values(num_rows); | ||
| std::generate_n(values.begin(), num_rows, [&]() { | ||
| auto const v = row_idx++ < frequent_set_threshold ? freq_dist(rng) : rare_dist(rng); | ||
| return "k_" + std::to_string(v); | ||
| }); | ||
|
|
||
| return values; | ||
| } | ||
|
|
||
| /** | ||
| * @brief Build a table with a single STRING column | ||
| * | ||
| * @tparam reverse_order Whether to reverse the order of the values | ||
| * @param num_rows Number of rows | ||
| * @param page_size_rows Number of rows per page | ||
| * @param cardinality Total number of distinct values | ||
| * @param frequent_set_ratio Fraction of `cardinality` assigned to the frequent set | ||
| * @return std::unique_ptr<cudf::table> | ||
| */ | ||
| template <bool reverse_order = false> | ||
| [[nodiscard]] std::unique_ptr<cudf::table> build_table(cudf::size_type num_rows, | ||
| cudf::size_type page_size_rows, | ||
| cudf::size_type cardinality, | ||
| double frequent_set_ratio) | ||
| { | ||
| constexpr cudf::size_type num_cols = 1; | ||
|
|
||
| auto values = build_string_column(num_rows, page_size_rows, cardinality, frequent_set_ratio); | ||
| if constexpr (reverse_order) { std::reverse(values.begin(), values.end()); } | ||
| std::vector<std::unique_ptr<cudf::column>> cols; | ||
| cols.reserve(num_cols); | ||
| cols.emplace_back(cudf::test::strings_column_wrapper(values.begin(), values.end()).release()); | ||
| return std::make_unique<cudf::table>(std::move(cols)); | ||
| } | ||
|
|
||
| /** | ||
| * @brief Compute per-page RLE bit widths for dictionary-encoded pages from the parquet page index | ||
| * | ||
| * Assumption: All parquet pages are dictionary-encoded, no nulls, no rep/def levels | ||
| * | ||
| * @param source Datasource | ||
| * @param footer File metadata | ||
| * @return Vector of number of bits per page for dictionary-encoded pages | ||
| */ | ||
| [[nodiscard]] std::vector<int> compute_page_dict_bits(cudf::io::datasource& source, | ||
| cudf::io::parquet::FileMetaData const& footer) | ||
| { | ||
| using namespace cudf::io::parquet; | ||
|
|
||
| std::vector<int> bits; | ||
|
|
||
| for (auto const& rg : footer.row_groups) { | ||
| for (auto const& chunk : rg.columns) { | ||
| if (not chunk.offset_index.has_value()) { continue; } | ||
| for (auto const& page_loc : chunk.offset_index->page_locations) { | ||
| if (page_loc.offset <= 0 or page_loc.compressed_page_size <= 0) { continue; } | ||
| auto const buffer = source.host_read(page_loc.offset, page_loc.compressed_page_size); | ||
| detail::CompactProtocolReader cp(buffer->data(), buffer->size()); | ||
| PageHeader page_header; | ||
| cp.read(&page_header); | ||
| // Check if the page is dictionary-encoded. | ||
| auto const is_dict_encoded = | ||
| (page_header.type == PageType::DATA_PAGE and | ||
| (page_header.data_page_header.encoding == Encoding::PLAIN_DICTIONARY or | ||
| page_header.data_page_header.encoding == Encoding::RLE_DICTIONARY)) or | ||
| (page_header.type == PageType::DATA_PAGE_V2 and | ||
| (page_header.data_page_header_v2.encoding == Encoding::PLAIN_DICTIONARY or | ||
| page_header.data_page_header_v2.encoding == Encoding::RLE_DICTIONARY)); | ||
| if (not is_dict_encoded) { continue; } | ||
| // `cp` is positioned at the first byte of the page payload after the | ||
| // header thrift; that byte is the RLE bit width for dict-indexed | ||
| // pages (valid only with no rep/def levels). | ||
| bits.push_back(cp.getb()); | ||
| } | ||
| } | ||
| } | ||
| return bits; | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| void BM_parq_write_dict_encoding(nvbench::state& state) | ||
| { | ||
| auto const num_rows = static_cast<cudf::size_type>(state.get_int64("num_rows")); | ||
| auto const reverse_order = static_cast<bool>(state.get_int64("reverse_order")); | ||
| auto const cardinality = static_cast<cudf::size_type>(state.get_int64("cardinality")); | ||
| auto const frequent_set_ratio = static_cast<double>(state.get_float64("freq_set_ratio")); | ||
| auto const page_size_rows = static_cast<cudf::size_type>(state.get_int64("page_size_rows")); | ||
|
|
||
| CUDF_EXPECTS(page_size_rows <= num_rows and num_rows % page_size_rows == 0, | ||
| "num_rows must be a multiple of page_size_rows"); | ||
|
|
||
| cuio_source_sink_pair source_sink(io_type::FILEPATH); | ||
|
|
||
| auto const table = [&]() { | ||
| if (reverse_order) { | ||
| return build_table<true>(num_rows, page_size_rows, cardinality, frequent_set_ratio); | ||
| } else { | ||
| return build_table<false>(num_rows, page_size_rows, cardinality, frequent_set_ratio); | ||
| } | ||
| }(); | ||
|
|
||
| auto const mem_stats_logger = cudf::memory_stats_logger(); | ||
| state.set_cuda_stream(nvbench::make_cuda_stream_view(cudf::get_default_stream().value())); | ||
| state.exec( | ||
| nvbench::exec_tag::timer | nvbench::exec_tag::sync, [&](nvbench::launch&, auto& timer) { | ||
| timer.start(); | ||
| auto const write_opts = | ||
| cudf::io::parquet_writer_options::builder(source_sink.make_sink_info(), table->view()) | ||
| .compression(cudf::io::compression_type::NONE) | ||
| .dictionary_policy(cudf::io::dictionary_policy::ALWAYS) | ||
| .stats_level(cudf::io::statistics_freq::STATISTICS_COLUMN) | ||
| .row_group_size_rows(num_rows) | ||
| .max_page_size_rows(page_size_rows) | ||
| .max_page_size_bytes(std::size_t{64} << 20) | ||
| .build(); | ||
| cudf::io::write_parquet(write_opts); | ||
| timer.stop(); | ||
| }); | ||
|
|
||
| state.add_element_count(static_cast<double>(table->num_rows()), "rows"); | ||
| state.add_buffer_size( | ||
| 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"); | ||
|
|
||
| // Use hybrid scan reader to get footer with page index. | ||
| { | ||
| auto const datasource = | ||
| std::move(cudf::io::make_datasources(source_sink.make_source_info()).front()); | ||
| auto const datasource_ref = std::ref(*datasource); | ||
| auto const footer_buf = cudf::io::parquet::fetch_footer_to_host(datasource_ref); | ||
| cudf::io::parquet::experimental::hybrid_scan_reader reader(*footer_buf, | ||
| cudf::io::parquet_reader_options{}); | ||
| auto const page_index_bytes = reader.page_index_byte_range(); | ||
| CUDF_EXPECTS(not page_index_bytes.is_empty(), "Page index is required"); | ||
| auto const page_index_buffer = | ||
| cudf::io::parquet::fetch_page_index_to_host(datasource_ref, page_index_bytes); | ||
| reader.setup_page_index(*page_index_buffer); | ||
|
|
||
| auto const metadata = reader.parquet_metadata(); | ||
| auto const page_dict_bits = compute_page_dict_bits(datasource_ref, metadata); | ||
|
|
||
| CUDF_EXPECTS(not page_dict_bits.empty(), "No dictionary-encoded pages found"); | ||
|
|
||
| auto const [min_it, max_it] = std::minmax_element(page_dict_bits.begin(), page_dict_bits.end()); | ||
| auto const sum = | ||
| std::accumulate(page_dict_bits.begin(), page_dict_bits.end(), std::uint64_t{0}); | ||
| auto const mean = | ||
| std::round(static_cast<double>(sum) / static_cast<double>(page_dict_bits.size())); | ||
| state.add_element_count(static_cast<double>(*min_it), "dict_rle_bits_min"); | ||
| state.add_element_count(static_cast<double>(*max_it), "dict_rle_bits_max"); | ||
| state.add_element_count(mean, "dict_rle_bits_mean"); | ||
| } | ||
| } | ||
|
|
||
| NVBENCH_BENCH(BM_parq_write_dict_encoding) | ||
| .set_name("parquet_write_dict_encoding") | ||
| .set_min_samples(4) | ||
| .add_int64_axis("reverse_order", {false, true}) | ||
| .add_int64_axis("num_rows", {1'000'000}) | ||
| .add_int64_axis("page_size_rows", {10'000, 100'000}) | ||
| .add_int64_axis("cardinality", {64'000, 100'000}) | ||
| .add_float64_axis("freq_set_ratio", {0.001, 0.01}); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.