Skip to content

Commit e158056

Browse files
authored
Add Hybrid scan APIs for single-step table materialization (#20906)
This PR adds new hybrid scan parquet reader APIs for single-step table materialization Authors: - Muhammad Haseeb (https://github.com/mhaseeb123) - https://github.com/apps/pre-commit-ci Approvers: - David Wendt (https://github.com/davidwendt) - Bradley Dice (https://github.com/bdice) - Nghia Truong (https://github.com/ttnghia) - Vukasin Milovanovic (https://github.com/vuule) URL: #20906
1 parent c1b43a6 commit e158056

7 files changed

Lines changed: 240 additions & 40 deletions

File tree

cpp/include/cudf/io/experimental/hybrid_scan.hpp

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -499,6 +499,31 @@ class hybrid_scan_reader {
499499
parquet_reader_options const& options,
500500
rmm::cuda_stream_view stream) const;
501501

502+
/**
503+
* @brief Get byte ranges of column chunks of all (or selected) columns
504+
*
505+
* @param row_group_indices Input row groups indices
506+
* @param options Parquet reader options
507+
* @return Vector of byte ranges to column chunks of all (or selected) columns
508+
*/
509+
[[nodiscard]] std::vector<byte_range_info> all_column_chunks_byte_ranges(
510+
cudf::host_span<size_type const> row_group_indices,
511+
parquet_reader_options const& options) const;
512+
513+
/**
514+
* @brief Materializes all (or selected) columns and returns the final output table
515+
*
516+
* @param row_group_indices Input row groups indices
517+
* @param column_chunk_buffers Device buffers containing column chunk data of all columns
518+
* @param options Parquet reader options
519+
* @param stream CUDA stream used for device memory operations and kernel launches
520+
* @return Table of all materialized columns and metadata
521+
*/
522+
[[nodiscard]] table_with_metadata materialize_all_columns(
523+
cudf::host_span<size_type const> row_group_indices,
524+
std::vector<rmm::device_buffer>&& column_chunk_buffers,
525+
parquet_reader_options const& options,
526+
rmm::cuda_stream_view stream) const;
502527
/**
503528
* @brief Setup chunking information for filter columns and preprocess the input data pages
504529
*

cpp/src/io/parquet/experimental/hybrid_scan.cpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -232,6 +232,33 @@ table_with_metadata hybrid_scan_reader::materialize_payload_columns(
232232
stream);
233233
}
234234

235+
std::vector<byte_range_info> hybrid_scan_reader::all_column_chunks_byte_ranges(
236+
cudf::host_span<size_type const> row_group_indices, parquet_reader_options const& options) const
237+
{
238+
CUDF_FUNC_RANGE();
239+
240+
auto const input_row_group_indices =
241+
std::vector<std::vector<size_type>>{{row_group_indices.begin(), row_group_indices.end()}};
242+
243+
return _impl->all_column_chunks_byte_ranges(input_row_group_indices, options).first;
244+
}
245+
246+
table_with_metadata hybrid_scan_reader::materialize_all_columns(
247+
cudf::host_span<size_type const> row_group_indices,
248+
std::vector<rmm::device_buffer>&& column_chunk_buffers,
249+
parquet_reader_options const& options,
250+
rmm::cuda_stream_view stream) const
251+
{
252+
CUDF_FUNC_RANGE();
253+
254+
// Temporary vector with row group indices from the first source
255+
auto const input_row_group_indices =
256+
std::vector<std::vector<size_type>>{{row_group_indices.begin(), row_group_indices.end()}};
257+
258+
return _impl->materialize_all_columns(
259+
input_row_group_indices, std::move(column_chunk_buffers), options, stream);
260+
}
261+
235262
void hybrid_scan_reader::setup_chunking_for_filter_columns(
236263
std::size_t chunk_read_limit,
237264
std::size_t pass_read_limit,

cpp/src/io/parquet/experimental/hybrid_scan_impl.cpp

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -105,9 +105,32 @@ void hybrid_scan_reader_impl::setup_page_index(
105105
void hybrid_scan_reader_impl::select_columns(read_columns_mode read_columns_mode,
106106
parquet_reader_options const& options)
107107
{
108-
// Select only columns required by the filter
109-
if (read_columns_mode == read_columns_mode::FILTER_COLUMNS) {
108+
if (read_columns_mode == read_columns_mode::ALL_COLUMNS) {
109+
if (_is_all_columns_selected) { return; }
110+
111+
// Select only columns required by the options and filter.
112+
// Using as is from:
113+
// https://github.com/rapidsai/cudf/blob/a8b25cd205dc5d04b9918dcb0b3abd6b8c4e4a74/cpp/src/io/parquet/reader_impl.cpp#L556-L569
114+
std::optional<std::vector<std::string>> filter_only_columns_names;
115+
if (options.get_filter().has_value() and options.get_columns().has_value()) {
116+
filter_only_columns_names = cudf::io::parquet::detail::get_column_names_in_expression(
117+
options.get_filter(), *(options.get_columns()));
118+
_num_filter_only_columns = filter_only_columns_names->size();
119+
}
120+
std::tie(_input_columns, _output_buffers, _output_column_schemas) =
121+
_metadata->select_columns(options.get_columns(),
122+
filter_only_columns_names,
123+
options.is_enabled_use_pandas_metadata(),
124+
_strings_to_categorical,
125+
options.is_enabled_ignore_missing_columns(),
126+
_options.timestamp_type.id());
127+
128+
_is_all_columns_selected = true;
129+
_is_filter_columns_selected = false;
130+
_is_payload_columns_selected = false;
131+
} else if (read_columns_mode == read_columns_mode::FILTER_COLUMNS) {
110132
if (_is_filter_columns_selected) { return; }
133+
111134
// list, struct, dictionary are not supported by AST filter yet.
112135
_filter_columns_names =
113136
names_from_expression(
@@ -124,6 +147,7 @@ void hybrid_scan_reader_impl::select_columns(read_columns_mode read_columns_mode
124147

125148
_is_filter_columns_selected = true;
126149
_is_payload_columns_selected = false;
150+
_is_all_columns_selected = false;
127151
} else {
128152
if (_is_payload_columns_selected) { return; }
129153

@@ -137,6 +161,7 @@ void hybrid_scan_reader_impl::select_columns(read_columns_mode read_columns_mode
137161

138162
_is_payload_columns_selected = true;
139163
_is_filter_columns_selected = false;
164+
_is_all_columns_selected = false;
140165
}
141166

142167
// Reset the rows processed so far
@@ -418,6 +443,17 @@ hybrid_scan_reader_impl::payload_column_chunks_byte_ranges(
418443
return get_input_column_chunk_byte_ranges(row_group_indices);
419444
}
420445

446+
std::pair<std::vector<byte_range_info>, std::vector<cudf::size_type>>
447+
hybrid_scan_reader_impl::all_column_chunks_byte_ranges(
448+
cudf::host_span<std::vector<size_type> const> row_group_indices,
449+
parquet_reader_options const& options)
450+
{
451+
CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
452+
453+
select_columns(read_columns_mode::ALL_COLUMNS, options);
454+
return get_input_column_chunk_byte_ranges(row_group_indices);
455+
}
456+
421457
table_with_metadata hybrid_scan_reader_impl::materialize_filter_columns(
422458
cudf::host_span<std::vector<size_type> const> row_group_indices,
423459
std::vector<rmm::device_buffer>&& column_chunk_buffers,
@@ -482,6 +518,29 @@ table_with_metadata hybrid_scan_reader_impl::materialize_payload_columns(
482518
return read_chunk_internal(read_mode::READ_ALL, read_columns_mode::PAYLOAD_COLUMNS, row_mask);
483519
}
484520

521+
table_with_metadata hybrid_scan_reader_impl::materialize_all_columns(
522+
cudf::host_span<std::vector<size_type> const> row_group_indices,
523+
std::vector<rmm::device_buffer>&& column_chunk_buffers,
524+
parquet_reader_options const& options,
525+
rmm::cuda_stream_view stream)
526+
{
527+
CUDF_EXPECTS(not row_group_indices.empty(), "Empty input row group indices encountered");
528+
529+
reset_internal_state();
530+
531+
initialize_options(row_group_indices, options, stream);
532+
533+
select_columns(read_columns_mode::ALL_COLUMNS, options);
534+
535+
// Convert the input expression (must be done after column selection)
536+
_expr_conv = build_converted_expression(options.get_filter());
537+
538+
prepare_data(read_mode::READ_ALL, row_group_indices, std::move(column_chunk_buffers), {});
539+
540+
// Use the main reader's function
541+
return reader_impl::read_chunk_internal(read_mode::READ_ALL);
542+
}
543+
485544
void hybrid_scan_reader_impl::setup_chunking_for_filter_columns(
486545
std::size_t chunk_read_limit,
487546
std::size_t pass_read_limit,
@@ -643,6 +702,8 @@ void hybrid_scan_reader_impl::initialize_options(
643702
named_to_reference_converter hybrid_scan_reader_impl::build_converted_expression(
644703
std::optional<std::reference_wrapper<const ast::expression>> filter)
645704
{
705+
if (not filter.has_value()) { return named_to_reference_converter(std::nullopt, {}, {}); }
706+
646707
table_metadata metadata;
647708
populate_metadata(metadata);
648709
auto expr_conv =
@@ -821,14 +882,16 @@ table_with_metadata hybrid_scan_reader_impl::finalize_output(
821882
// Create a table from the output columns.
822883
auto read_table = std::make_unique<table>(std::move(out_columns));
823884

885+
CUDF_EXPECTS(row_mask.is_empty() or row_mask.type().id() == type_id::BOOL8,
886+
"Input row mask must be empty or a boolean column");
887+
824888
// If the input row mask is empty, return the table as is.
825889
if (row_mask.is_empty()) { return {std::move(read_table), std::move(out_metadata)}; }
826890

827-
// If reading filter columns, compute the predicate, apply it to the table, and update the input
828-
// row mask to reflect the final surviving rows.
891+
// For filter columns, apply the filter expression and update the input row mask
829892
if constexpr (std::is_same_v<RowMaskView, cudf::mutable_column_view>) {
830893
CUDF_EXPECTS(read_columns_mode == read_columns_mode::FILTER_COLUMNS, "Invalid read mode");
831-
// Apply the row selection predicate on the read table to get the final row mask
894+
832895
auto final_row_mask =
833896
cudf::detail::compute_column(*read_table,
834897
_expr_conv.get_converted_expr().value().get(),
@@ -850,11 +913,9 @@ table_with_metadata hybrid_scan_reader_impl::finalize_output(
850913
// Return the final output table and metadata
851914
return {std::move(output_table), std::move(out_metadata)};
852915
}
853-
// Otherwise, simply apply the input row mask to the table.
916+
// For payload columns, simply apply the input row mask to the table.
854917
else {
855918
CUDF_EXPECTS(read_columns_mode == read_columns_mode::PAYLOAD_COLUMNS, "Invalid read mode");
856-
CUDF_EXPECTS(row_mask.type().id() == type_id::BOOL8,
857-
"Predicate filter should return a boolean");
858919

859920
auto const mask_offset = _rows_processed_so_far;
860921
_rows_processed_so_far += read_table->num_rows();

cpp/src/io/parquet/experimental/hybrid_scan_impl.hpp

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION.
2+
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

@@ -180,6 +180,27 @@ class hybrid_scan_reader_impl : public parquet::detail::reader_impl {
180180
parquet_reader_options const& options,
181181
rmm::cuda_stream_view stream);
182182

183+
/**
184+
* @brief Fetches byte ranges for all (or selected) column chunks
185+
*
186+
* @param row_group_indices Input row groups indices
187+
* @param options Parquet reader options
188+
* @return Pair of a vector of byte ranges to column chunks of all (or selected) columns and a
189+
* vector of their corresponding input source file indices
190+
*/
191+
[[nodiscard]] std::pair<std::vector<byte_range_info>, std::vector<cudf::size_type>>
192+
all_column_chunks_byte_ranges(cudf::host_span<std::vector<size_type> const> row_group_indices,
193+
parquet_reader_options const& options);
194+
195+
/**
196+
* @copydoc cudf::io::experimental::hybrid_scan::materialize_all_columns
197+
*/
198+
[[nodiscard]] table_with_metadata materialize_all_columns(
199+
cudf::host_span<std::vector<size_type> const> row_group_indices,
200+
std::vector<rmm::device_buffer>&& column_chunk_buffers,
201+
parquet_reader_options const& options,
202+
rmm::cuda_stream_view stream);
203+
183204
/**
184205
* @copydoc cudf::io::experimental::hybrid_scan::setup_chunking_for_filter_columns
185206
*/
@@ -242,9 +263,9 @@ class hybrid_scan_reader_impl : public parquet::detail::reader_impl {
242263

243264
private:
244265
/**
245-
* @brief The enum indicating whether we are reading the filter columns or the payload columns
266+
* @brief The enum indicating whether we are reading the filter, payload, or all columns
246267
*/
247-
enum class read_columns_mode { FILTER_COLUMNS, PAYLOAD_COLUMNS };
268+
enum class read_columns_mode { FILTER_COLUMNS, PAYLOAD_COLUMNS, ALL_COLUMNS };
248269

249270
/**
250271
* @brief Initialize the necessary options related internal variables for use later on
@@ -439,6 +460,7 @@ class hybrid_scan_reader_impl : public parquet::detail::reader_impl {
439460

440461
bool _is_filter_columns_selected{false};
441462
bool _is_payload_columns_selected{false};
463+
bool _is_all_columns_selected{false};
442464
};
443465

444466
} // namespace cudf::io::parquet::experimental::detail

cpp/tests/io/experimental/hybrid_scan_common.cpp

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
/*
32
* SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION.
43
* SPDX-License-Identifier: Apache-2.0
@@ -200,7 +199,7 @@ std::tuple<std::unique_ptr<cudf::table>,
200199
cudf::io::table_metadata,
201200
cudf::io::table_metadata,
202201
std::unique_ptr<cudf::column>>
203-
hybrid_scan(std::vector<char>& buffer,
202+
hybrid_scan(cudf::host_span<uint8_t const> file_buffer_span,
204203
cudf::ast::operation const& filter_expression,
205204
cudf::size_type num_filter_columns,
206205
std::optional<std::vector<std::string>> const& payload_column_names,
@@ -215,10 +214,6 @@ hybrid_scan(std::vector<char>& buffer,
215214
// Set payload column names if provided
216215
if (payload_column_names.has_value()) { options.set_columns(payload_column_names.value()); }
217216

218-
// Input file buffer span
219-
auto const file_buffer_span =
220-
cudf::host_span<uint8_t const>(reinterpret_cast<uint8_t const*>(buffer.data()), buffer.size());
221-
222217
auto [reader, filtered_row_group_indices, row_mask] =
223218
apply_parquet_filters(file_buffer_span, options, stream, mr);
224219

@@ -271,7 +266,7 @@ std::tuple<std::unique_ptr<cudf::table>,
271266
cudf::io::table_metadata,
272267
cudf::io::table_metadata,
273268
std::unique_ptr<cudf::column>>
274-
chunked_hybrid_scan(std::vector<char> const& buffer,
269+
chunked_hybrid_scan(cudf::host_span<uint8_t const> file_buffer_span,
275270
cudf::ast::operation const& filter_expression,
276271
cudf::size_type num_filter_columns,
277272
std::optional<std::vector<std::string>> const& payload_column_names,
@@ -286,10 +281,6 @@ chunked_hybrid_scan(std::vector<char> const& buffer,
286281
// Set payload column names if provided
287282
if (payload_column_names.has_value()) { options.set_columns(payload_column_names.value()); }
288283

289-
// Input file buffer span
290-
auto const file_buffer_span =
291-
cudf::host_span<uint8_t const>(reinterpret_cast<uint8_t const*>(buffer.data()), buffer.size());
292-
293284
auto [reader, filtered_row_group_indices, row_mask] =
294285
apply_parquet_filters(file_buffer_span, options, stream, mr);
295286

@@ -387,3 +378,34 @@ chunked_hybrid_scan(std::vector<char> const& buffer,
387378
std::move(payload_metadata),
388379
std::move(row_mask)};
389380
}
381+
382+
cudf::io::table_with_metadata hybrid_scan_single_step(
383+
cudf::host_span<uint8_t const> file_buffer_span,
384+
std::optional<cudf::ast::operation> filter_expression,
385+
std::optional<std::vector<std::string>> const& column_names,
386+
rmm::cuda_stream_view stream,
387+
rmm::device_async_resource_ref mr)
388+
{
389+
// Create reader options with empty source info
390+
cudf::io::parquet_reader_options options = cudf::io::parquet_reader_options::builder().build();
391+
392+
if (column_names.has_value()) { options.set_columns(column_names.value()); }
393+
if (filter_expression.has_value()) { options.set_filter(filter_expression.value()); }
394+
395+
auto [reader, filtered_row_group_indices, _ /*row_mask*/] =
396+
apply_parquet_filters(file_buffer_span, options, stream, mr);
397+
398+
auto current_row_group_indices = cudf::host_span<cudf::size_type>(filtered_row_group_indices);
399+
400+
// Get all column chunk byte ranges from the reader
401+
auto const all_column_chunk_byte_ranges =
402+
reader->all_column_chunks_byte_ranges(current_row_group_indices, options);
403+
404+
// Fetch column chunk device buffers from the input buffer
405+
auto all_column_chunk_buffers =
406+
fetch_byte_ranges(file_buffer_span, all_column_chunk_byte_ranges, stream, mr);
407+
408+
// Materialize the table with all columns
409+
return reader->materialize_all_columns(
410+
current_row_group_indices, std::move(all_column_chunk_buffers), options, stream);
411+
}

0 commit comments

Comments
 (0)