Skip to content

Transcode parquet to cuDF dictionaries for flat STRING columns - #22532

Merged
rapids-bot[bot] merged 42 commits into
NVIDIA:mainfrom
y2kiran:ykiran-pq-decode
Aug 7, 2026
Merged

Transcode parquet to cuDF dictionaries for flat STRING columns#22532
rapids-bot[bot] merged 42 commits into
NVIDIA:mainfrom
y2kiran:ykiran-pq-decode

Conversation

@y2kiran

@y2kiran y2kiran commented May 15, 2026

Copy link
Copy Markdown
Contributor

Description

This PR adds a prototype implementation of transcoding dictionaries from parquet to cuDF directly when possible and specified for flat string type columns.

It also adds new tests to check the new transcode path, as well as a benchmark to evaluate performance.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

@y2kiran
y2kiran requested review from a team as code owners May 15, 2026 23:46
@y2kiran
y2kiran requested review from PointKernel and devavret May 15, 2026 23:46
@copy-pr-bot

copy-pr-bot Bot commented May 15, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added libcudf Affects libcudf (C++/CUDA) code. CMake CMake build issue labels May 15, 2026
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 890a9db to 08bd5cb Compare May 15, 2026 23:46
@coderabbitai

coderabbitai Bot commented May 15, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added output_dict_columns(true) to emit eligible dictionary-encoded Parquet flat string columns as DICTIONARY32 during reading (including empty and sliced inputs). List/nested dictionary-encoded strings remain list types.
    • Dictionary output is disabled when predicate filtering is used, and the fast path is bypassed when chunk/pass read limits are set.
  • Bug Fixes
    • Improved dictionary-index decoding for direct INT32 output, including safer null handling/zero-filling.
  • Tests
    • Added Parquet reader coverage for transcode, default behavior, nulls, empty, sliced, and list cases.
  • Benchmarks
    • Added a benchmark comparing standard decode, decode-then-encode, and direct DICTIONARY32 output.

Walkthrough

This PR adds optional Parquet reader support for returning fully dictionary-encoded flat string columns as DICTIONARY32, including public options, direct GPU index decoding, reader orchestration, fallback encoding, tests, benchmarks, and build wiring.

Changes

Parquet Dictionary Transcode Feature

Layer / File(s) Summary
Public API and reader options
cpp/include/cudf/io/parquet.hpp
Adds the output_dict_columns option, accessors, and builder support, while disabling dictionary output when predicate filtering is enabled.
Reader state and transcode interfaces
cpp/src/io/parquet/reader_impl.hpp
Declares transcode helpers and tracks option and per-column eligibility state.
Dictionary index decode infrastructure
cpp/src/io/parquet/parquet_gpu.hpp, cpp/src/io/parquet/decode_fixed.cu
Adds the DICT_INT32 mask and direct dictionary-index decoding with null handling.
Dictionary transcode implementation
cpp/src/io/parquet/reader_impl_dict_transcode.cu
Detects eligible flat columns, rewrites page decoding, initializes index buffers, and assembles DICTIONARY32 outputs.
Reader integration and fallback
cpp/src/io/parquet/reader_impl.cpp
Wires transcode processing into chunk reads and re-encodes remaining string columns when requested.
Validation, benchmarks, and build wiring
cpp/tests/io/parquet_reader_dict_test.cpp, cpp/benchmarks/io/parquet/parquet_reader_dict.cpp, cpp/*/CMakeLists.txt, cpp/src/dictionary/detail/concatenate.cu
Adds flat, nested, empty, and sliced-string coverage, an NVBench comparison, build entries, and a concatenate TODO.

Estimated code review effort: 4 (Complex) | ~60 minutes

Suggested labels: libcudf, improvement, non-breaking, cuIO, Velox

Suggested reviewers: pointkernel, mhaseeb123, vuule, devavret, igorpeshansky

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: Parquet to cuDF dictionary transcode for flat STRING columns.
Description check ✅ Passed The description matches the changeset by describing the new transcode path, tests, and benchmark.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
cpp/src/io/parquet/reader_impl_dict_transcode.cu (1)

175-185: ⚡ Quick win

Drop the blocking stream sync here.

Line 185 forces a full _stream.synchronize() even though the rewritten subpass.pages.host_to_device_async(_stream) is already ordered before later decode work. That adds a host-side barrier to the transcode path without obvious correctness benefit.

As per coding guidelines, "Avoid unnecessary host-device synchronization that blocks the GPU pipeline."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cpp/src/io/parquet/reader_impl_dict_transcode.cu` around lines 175 - 185,
Remove the blocking host-device sync: delete the call to _stream.synchronize()
after subpass.pages.host_to_device_async(_stream) and after updating
subpass.kernel_mask via std::transform_reduce; rely on the existing
host_to_device_async ordering for correctness and add a short comment next to
subpass.pages.host_to_device_async(_stream) explaining that no explicit
host-side synchronization is required (keeps GPU pipeline unblocked). Ensure
references: subpass.pages.host_to_device_async, subpass.kernel_mask,
std::transform_reduce, and _stream.synchronize() are the locations you change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/io/parquet/reader_impl_dict_transcode.cu`:
- Around line 148-173: The code flips output buffer types via
_output_buffers[_input_columns[i].nesting[0]].type and sets
_dict_transcode_eligible before verifying any_rewritten; move the entire
std::for_each that changes out_buf.type (and any mutation of
_dict_transcode_eligible if present) to occur only after the host-page rewrite
succeeds (i.e., after the any_rewritten check and early return), so that you
only retag buffers to INT32 when pages were actually updated from
decode_kernel_mask::STRING_DICT to decode_kernel_mask::DICT_INT32;
alternatively, if you prefer the current order, roll back the type and
eligibility mutations before returning when any_rewritten is false.

In `@cpp/src/io/parquet/reader_impl.cpp`:
- Around line 923-937: The current logic eagerly calls
dictionary::detail::encode on any remaining top-level STRING columns when
_options.try_output_dict_columns is true (iterating over out_columns), which can
convert columns to DICTIONARY32 before predicate evaluation and break AST
filters that don't support dictionary columns; fix by deferring this fallback
encode until after the filter/predicate branch completes (i.e., move the loop
that encodes STRING columns to DICTIONARY32 to run after the predicate
evaluation/filter logic that lives around the predicate handling block), or
alternatively explicitly reject try_output_dict_columns when
options.get_filter() is set by adding a validation check on options.get_filter()
that returns an error if try_output_dict_columns is requested with a filter
present; update call sites involving _options.try_output_dict_columns,
out_columns, dictionary::detail::encode, and options.get_filter() accordingly.

In `@cpp/tests/io/parquet_reader_dict_test.cpp`:
- Around line 8-11: Add the test framework header include
<cudf_test/cudf_gtest.hpp> to the top of the test file (alongside the existing
includes) so the file uses the repository test convention instead of including
gtest directly; update the include list near the existing includes
(cudf_test/base_fixture.hpp, cudf_test/column_utilities.hpp,
cudf_test/column_wrapper.hpp) to add cudf_test/cudf_gtest.hpp.
- Around line 35-76: The tests only generate ASCII strings; update the
generators make_low_cardinality_strings() and
make_low_cardinality_lists_of_strings() to include non-ASCII UTF-8 samples
(e.g., accented characters, CJK, emoji) by building a small pool of values that
mixes "str_<n>" with UTF-8 literals and picking from that pool via value_dist
(use the existing seed and list_strings_seed for determinism); ensure both the
top-level strings vector and the child_strings used for lists sometimes select
those UTF-8 entries so transcoding/fallback paths are exercised while preserving
null logic and list offsets.
- Around line 112-177: Add two edge-case tests for the DICTIONARY32 transcode
path: (1) an empty-input test modeled on
ParquetReaderDictTest::FlatStringDictTranscode that constructs an empty column
(zero rows) from the same low-cardinality string generator, writes/reads it via
write_parquet and read_parquet_as_dict, and asserts read_table->num_rows()==0,
read column type is DICTIONARY32, and CUDF_TEST_EXPECT_COLUMNS_EQUAL compares
the decoded empty input and decoded read column; (2) a sliced-column test that
creates a sliced view of make_low_cardinality_strings (use column_view slicing
or cudf::slice), writes that sliced input, reads with read_parquet_as_dict, and
assert num_rows/num_columns, read column type==cudf::type_id::DICTIONARY32 and
use cudf::dictionary::encode/decode + CUDF_TEST_EXPECT_COLUMNS_EQUAL to verify
equality of the sliced original and decoded read column; locate code around
ParquetReaderDictTest::FlatStringDictTranscode, read_parquet_as_dict,
make_low_cardinality_strings, and CUDF_TEST_EXPECT_COLUMNS_EQUAL to implement
these tests.
- Around line 20-23: Add the missing header for std::min by including
<algorithm> in the top include block; locate the include list (where <memory>,
<random>, <string>, <vector> are included) in parquet_reader_dict_test.cpp and
add the <algorithm> include so uses of std::min (used around the code
referencing std::min at line ~89) are directly supported rather than relying on
transitive includes.

---

Nitpick comments:
In `@cpp/src/io/parquet/reader_impl_dict_transcode.cu`:
- Around line 175-185: Remove the blocking host-device sync: delete the call to
_stream.synchronize() after subpass.pages.host_to_device_async(_stream) and
after updating subpass.kernel_mask via std::transform_reduce; rely on the
existing host_to_device_async ordering for correctness and add a short comment
next to subpass.pages.host_to_device_async(_stream) explaining that no explicit
host-side synchronization is required (keeps GPU pipeline unblocked). Ensure
references: subpass.pages.host_to_device_async, subpass.kernel_mask,
std::transform_reduce, and _stream.synchronize() are the locations you change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 691612f6-fd44-4f0c-b37c-a4df33fc8bbb

📥 Commits

Reviewing files that changed from the base of the PR and between 2c52ba1 and 08bd5cb.

📒 Files selected for processing (9)
  • cpp/CMakeLists.txt
  • cpp/include/cudf/io/parquet.hpp
  • cpp/src/io/parquet/decode_fixed.cu
  • cpp/src/io/parquet/parquet_gpu.hpp
  • cpp/src/io/parquet/reader_impl.cpp
  • cpp/src/io/parquet/reader_impl.hpp
  • cpp/src/io/parquet/reader_impl_dict_transcode.cu
  • cpp/tests/CMakeLists.txt
  • cpp/tests/io/parquet_reader_dict_test.cpp

Comment thread cpp/src/io/parquet/reader_impl_dict_transcode.cu Outdated
Comment thread cpp/src/io/parquet/reader_impl.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_dict_test.cpp
Comment thread cpp/tests/io/parquet_reader_dict_test.cpp
Comment thread cpp/tests/io/parquet_reader_dict_test.cpp
Comment thread cpp/tests/io/parquet_reader_dict_test.cpp
@mhaseeb123
mhaseeb123 self-requested a review May 15, 2026 23:54
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 08bd5cb to fcb2a12 Compare May 16, 2026 00:27
@y2kiran
y2kiran marked this pull request as draft May 16, 2026 01:03
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from fcb2a12 to 1b99b9b Compare May 16, 2026 01:06
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 1b99b9b to 88a021a Compare May 27, 2026 17:20
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 88a021a to 8c8d35e Compare June 15, 2026 20:31
@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 8c8d35e to a561787 Compare June 24, 2026 16:49
@PointKernel PointKernel added feature request New feature or request non-breaking Non-breaking change labels Jun 24, 2026
@PointKernel

Copy link
Copy Markdown
Member

@y2kiran, could you please address all CodeRabbit review comments and mark the PR as ready for final review once everything is resolved? That will help us kick off the CI process at the right time. Thanks!

@y2kiran
y2kiran force-pushed the ykiran-pq-decode branch from 4030a68 to 7b6833f Compare June 29, 2026 20:30
@y2kiran
y2kiran marked this pull request as ready for review June 29, 2026 20:49
@y2kiran y2kiran changed the title [DRAFT] Transcode parquet to cuDF dictionaries for flat STRING columns Transcode parquet to cuDF dictionaries for flat STRING columns Jun 29, 2026
@y2kiran

y2kiran commented Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

@PointKernel Addressed all coderabbit feedback.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/benchmarks/io/parquet/parquet_reader_dict.cpp`:
- Around line 76-84: The sanity check in parquet_reader_dict.cpp only inspects
probe.tbl->view().column(0), so it can miss cases where other output columns
fall back to STRING and the benchmark measures a mixed path. Update the
validation in the try_dict block after cudf::io::read_parquet(read_opts) to
verify every column in probe.tbl matches the expected DICTIONARY32 type, not
just the first one. Use the existing num_cols check and the probe.tbl/view()
access pattern to iterate through all output columns and fail fast if any column
is not DICTIONARY32.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 22be6276-fd6d-4391-90b7-81612485179e

📥 Commits

Reviewing files that changed from the base of the PR and between fcb2a12 and bde4dc8.

📒 Files selected for processing (3)
  • cpp/CMakeLists.txt
  • cpp/benchmarks/CMakeLists.txt
  • cpp/benchmarks/io/parquet/parquet_reader_dict.cpp
✅ Files skipped from review due to trivial changes (1)
  • cpp/CMakeLists.txt

Comment thread cpp/benchmarks/io/parquet/parquet_reader_dict.cpp Outdated
@PointKernel

Copy link
Copy Markdown
Member

/ok to test bde4dc8

@mhaseeb123

Copy link
Copy Markdown
Contributor

Hi @y2kiran, looks like we have failing cudf-cpp-tests related to this PR

Comment thread cpp/include/cudf/io/parquet.hpp Outdated
Comment thread cpp/include/cudf/io/parquet.hpp Outdated
Comment thread cpp/src/dictionary/detail/concatenate.cu Outdated
Comment thread cpp/src/io/parquet/reader_impl.cpp Outdated
Comment thread cpp/src/io/parquet/reader_impl.cpp Outdated
Comment thread cpp/src/io/parquet/reader_impl_dict_transcode.cu
Comment thread cpp/src/io/parquet/reader_impl_dict_transcode.cu Outdated
Comment on lines +86 to +99
/**
* @brief Fold a single chunk's properties into its column's eligibility state.
*
* @param e The per-column eligibility state to update in place
* @param chunk The column chunk descriptor to classify
*/
void update_from_chunk(column_eligibility& e, ColumnChunkDesc const& chunk)
{
e.has_any_chunk = true;
if (chunk.max_nesting_depth != 1 or chunk.max_level[level_type::REPETITION] != 0 or
not is_host_byte_array_string_chunk(chunk) or chunk.num_dict_pages < 1) {
e.all_chunks_string = false;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confused here, why are we relying on information from one column chunks instead of checking all?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also please note that in Parquet V2, it is possible for some pages in a column chunk to be dict encoded some not (multiple encodings per chunk) is allowed and totally legal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm not mistaken, we are still processing every chunk here. And update all_chunks_string.

Comment thread cpp/src/io/parquet/reader_impl_dict_transcode.cu Outdated
// pre-shift indices into a global keyspace, because `cudf::dictionary::detail::concatenate`
// already re-maps the indices using `compute_children_offsets_fn`. Pre-shifting would cause
// double-offsetting and out-of-bounds reads in the `dispatch_compute_indices` kernel.
std::for_each(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since @davidwendt is working on #22839, we could just adjust the key indices and leave everything any cross chunk duplicates as is (no need to concatenate anything)

@mhaseeb123

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@PointKernel

Copy link
Copy Markdown
Member

/merge

@rapids-bot
rapids-bot Bot merged commit 8a6f847 into NVIDIA:main Aug 7, 2026
140 checks passed
@josephine-wolf-oberholtzer josephine-wolf-oberholtzer moved this to Burndown in libcudf Aug 12, 2026
@GregoryKimball GregoryKimball moved this from Burndown to Slip in libcudf Aug 20, 2026
rapids-bot Bot pushed a commit that referenced this pull request Aug 21, 2026
This PR adds benchmarks to test the newly added `output_dict_columns` options for the Parquet reader, which was introduced in this [PR](#22532)

Authors:
  - https://github.com/y2kiran

Approvers:
  - Vukasin Milovanovic (https://github.com/vuule)
  - Muhammad Haseeb (https://github.com/mhaseeb123)
  - Lawrence Mitchell (https://github.com/wence-)

URL: #23596
@GregoryKimball GregoryKimball moved this from Slip to Landed in libcudf Aug 21, 2026
@GregoryKimball GregoryKimball removed this from libcudf Aug 21, 2026
@GregoryKimball GregoryKimball moved this from Burndown to Landed in libcudf Aug 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CMake CMake build issue feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change

Projects

Status: Landed

Development

Successfully merging this pull request may close these issues.

6 participants