-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Parquet statistics, dictionary and page-index filter fixes #23709
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
base: main
Are you sure you want to change the base?
Changes from all commits
50bb35a
80f09bc
a5133a2
117a2fc
1cc8c50
475d206
657b944
f5ef279
19d39fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,7 +16,9 @@ | |
|
|
||
| #include <cuda/stream> | ||
|
|
||
| #include <limits> | ||
| #include <memory> | ||
| #include <optional> | ||
| #include <span> | ||
| #include <utility> | ||
| #include <vector> | ||
|
|
@@ -53,6 +55,63 @@ enum class use_data_page_mask : bool { | |
| NO = false ///< Do not compute or use a data page mask | ||
| }; | ||
|
|
||
| /** | ||
| * @brief How closely a dictionary page byte range describes the page it points at | ||
| * | ||
| * An `upper_bound_if_present` range begins at the dictionary page if the column chunk has one, and | ||
| * ends no earlier than that page does. A writer is allowed to leave out where the page ends, and to | ||
| * say that a chunk is dictionary encoded when it holds no dictionary page at all, so a range of | ||
| * this kind is a bound on a page that may not be there. | ||
| */ | ||
| enum class dictionary_page_extent : bool { | ||
| exact, ///< The range is exactly the dictionary page | ||
| upper_bound_if_present ///< The range bounds a dictionary page that may not be there | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Byte range of a column chunk's dictionary page, and how closely it describes that page | ||
| * | ||
| * A caller is free to read less than an `upper_bound_if_present` range, which is how it caps what | ||
| * it spends looking for a page that may not be there. The reader still wants a span holding exactly | ||
| * one dictionary page, so a caller that reads such a range measures the page in it with | ||
| * `dictionary_page_length`, and passes an empty span for a chunk whose page is not there or does | ||
| * not fit in what was read. | ||
| */ | ||
| struct dictionary_page_range { | ||
| byte_range_info byte_range; ///< Byte range to read from the file | ||
| dictionary_page_extent extent; ///< How closely `byte_range` describes the dictionary page | ||
| }; | ||
|
|
||
| /** | ||
| * @brief Byte ranges to read for the specified dictionary page ranges | ||
| * | ||
| * No more than `max_upper_bound_size` bytes are read of a range that only bounds its dictionary | ||
| * page, which is how a caller caps what it spends looking for a page that may not be there. By | ||
| * default the whole of every range is read. What is read of such a range still has to be trimmed to | ||
| * the dictionary page before it is handed to the reader, see `dictionary_page_range`. | ||
| * | ||
| * @param dictionary_page_ranges Dictionary page ranges from `secondary_filters_byte_ranges` | ||
| * @param max_upper_bound_size Most bytes to read of a range that only bounds its dictionary page | ||
| * @return Byte ranges to read, one per input dictionary page range | ||
| */ | ||
| [[nodiscard]] std::vector<byte_range_info> dictionary_page_byte_ranges_to_read( | ||
| cudf::host_span<dictionary_page_range const> dictionary_page_ranges, | ||
| int64_t max_upper_bound_size = std::numeric_limits<int64_t>::max()); | ||
|
|
||
| /** | ||
| * @brief Length of the dictionary page at the front of the specified bytes, header included | ||
| * | ||
| * What was read of a range that only bounds its dictionary page begins at that page and runs past | ||
| * it. The page's own header says how long the page is, so this reads that header to find where the | ||
| * page ends, which is what turns such a range into the one page the reader takes. | ||
| * | ||
| * @param page_bytes Bytes read for a dictionary page range, from the start of the range | ||
| * @return Length of the dictionary page, or `std::nullopt` if these bytes do not begin with a whole | ||
| * dictionary page, which is the case for a column chunk that has none to prune with | ||
| */ | ||
| [[nodiscard]] std::optional<int64_t> dictionary_page_length( | ||
| cudf::host_span<uint8_t const> page_bytes); | ||
|
|
||
| /** | ||
| * @brief The experimental parquet reader class to optimally read parquet files subject to | ||
| * highly selective filters, called a Hybrid Scan operation | ||
|
|
@@ -148,18 +207,29 @@ enum class use_data_page_mask : bool { | |
| * current_row_group_indices = stats_filtered_row_group_indices; | ||
| * | ||
| * // Get byte ranges of bloom filters and dictionaries for the current row groups | ||
| * auto [bloom_filter_byte_ranges, dict_page_byte_ranges] = | ||
| * auto [bloom_filter_byte_ranges, dict_page_ranges] = | ||
| * reader->secondary_filters_byte_ranges(current_row_group_indices, options); | ||
| * | ||
| * // Optional: Prune row groups if we have valid dictionary pages | ||
| * auto dict_filtered_row_group_indices = std::vector<size_type>{}; | ||
| * | ||
| * if (dict_page_byte_ranges.size()) { | ||
| * if (dict_page_ranges.size()) { | ||
| * // Decide how much of each range to read. A range that only bounds its dictionary page can be | ||
| * // much larger than the page it bounds, so read no more of it than a dictionary page is worth. | ||
| * auto const dict_page_byte_ranges = | ||
| * dictionary_page_byte_ranges_to_read(dict_page_ranges, max_dict_page_size); | ||
| * | ||
| * // Fetch dictionary page byte ranges into device buffers and create spans | ||
| * auto [dict_page_buffers, dict_page_data, dict_page_tasks] = | ||
| * parquet::fetch_byte_ranges_to_device_async(datasource, dict_page_byte_ranges, stream, mr); | ||
| * dict_page_tasks.get(); | ||
| * | ||
| * // The spans above are what the reader takes as long as every range was exactly a page. What | ||
| * // was read of a range that only bounds its page runs past that page instead, and may hold no | ||
| * // page at all, so such a range has to be fetched into host memory, measured with | ||
| * // `dictionary_page_length`, and copied to the device cut down to its page. A column chunk | ||
| * // left with an empty span is not pruned with. | ||
| * | ||
| * // Prune row groups using dictionaries | ||
| * dict_filtered_row_group_indices = reader->filter_row_groups_with_dictionary_pages( | ||
| * dict_page_data, current_row_group_indices, options, stream); | ||
|
Comment on lines
+216
to
235
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Trim
📍 Affects 6 files
🤖 Prompt for AI Agents |
||
|
|
@@ -396,16 +466,20 @@ class hybrid_scan_reader { | |
| * | ||
| * @param row_group_indices Input row groups indices | ||
| * @param options Parquet reader options | ||
| * @return Pair of vectors of byte ranges of column chunk with bloom filters and dictionary | ||
| * pages subject to filter predicate | ||
| * @return Pair of a vector of byte ranges of column chunks with bloom filters and a vector of | ||
| * dictionary page ranges, subject to filter predicate | ||
| */ | ||
| [[nodiscard]] std::pair<std::vector<byte_range_info>, std::vector<byte_range_info>> | ||
| [[nodiscard]] std::pair<std::vector<byte_range_info>, std::vector<dictionary_page_range>> | ||
| secondary_filters_byte_ranges(std::span<size_type const> row_group_indices, | ||
| parquet_reader_options const& options) const; | ||
|
|
||
| /** | ||
| * @brief Filter the row groups using column chunk dictionary pages | ||
| * | ||
| * Each span must hold exactly one dictionary page, or nothing at all for a column chunk that has | ||
| * no dictionary page to prune with. See `dictionary_page_range` for trimming a range that only | ||
| * bounds its page. | ||
| * | ||
| * @param dictionary_page_data Device spans of dictionary page data of column chunks with an | ||
| * (in)equality predicate, in the same order as the byte ranges returned by | ||
| * `secondary_filters_byte_ranges` including empty spans against empty byte ranges | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -293,8 +293,14 @@ CUDF_KERNEL void query_dictionaries(cudf::device_span<T> decoded_data, | |
|
|
||
| // Evaluate the scalar against all cuco hash sets of this column | ||
| for (auto set_idx = group.thread_rank(); set_idx < total_row_groups; set_idx += group.size()) { | ||
| // If the set is empty (no dictionary page data), then skip the dictionary page filter | ||
| if (set_offsets[set_idx + 1] - set_offsets[set_idx] == 0) { | ||
| // Number of values in this hash set | ||
| auto const num_set_values = value_offsets[set_idx + 1] - value_offsets[set_idx]; | ||
|
|
||
| // Skip the dictionary page filter for a column chunk with no dictionary page. Emptiness must be | ||
| // read from the value count and not from the number of slots, because cuco rounds every | ||
| // capacity up to at least one bucket, so an empty dictionary still has slots. Its set was never | ||
| // built, so probing it would report the literal as absent and prune the row group. | ||
| if (num_set_values == 0) { | ||
| result[set_idx] = operators[scalar_idx] == ast::ast_operator::EQUAL; | ||
| continue; | ||
| } | ||
|
|
@@ -311,8 +317,6 @@ CUDF_KERNEL void query_dictionaries(cudf::device_span<T> decoded_data, | |
| storage_ref}; | ||
| auto set_find_ref = hash_set_ref.rebind_operators(cuco::contains); | ||
|
|
||
| // Number of values in this hash set | ||
| auto const num_set_values = value_offsets[set_idx + 1] - value_offsets[set_idx]; | ||
| // Literal value to find in this hash set | ||
| auto const literal_value = scalar.value<T>(); | ||
|
|
||
|
|
@@ -901,6 +905,8 @@ CUDF_KERNEL void __launch_bounds__(DECODE_BLOCK_SIZE) | |
| results[i][row_group_idx] = false; | ||
| } | ||
|
|
||
| group.sync(); | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing sync resulted in above initialization running after results[i] setting below. |
||
|
|
||
| // Decode values from the current dictionary page with the current thread block | ||
| for (auto value_idx = group.thread_rank(); value_idx < page.num_input_values; | ||
| value_idx += group.num_threads()) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Document the precondition exception.
dictionary_page_byte_ranges_to_readrejects a negativemax_upper_bound_size, but its Doxygen does not declare that failure mode. Add an@throwentry for the exception raised byCUDF_EXPECTS.As per coding guidelines, “Doxygen documentation required (
@brief,@param,@return,@throw,@tparam).”🤖 Prompt for AI Agents
Source: Coding guidelines