Skip to content

Fix Parquet V2 inputs to decompression scratch queries - #24006

Open
a-hirota wants to merge 2 commits into
NVIDIA:mainfrom
a-hirota:fix/parquet-v2-decompression-scratch
Open

Fix Parquet V2 inputs to decompression scratch queries#24006
a-hirota wants to merge 2 commits into
NVIDIA:mainfrom
a-hirota:fix/parquet-v2-decompression-scratch

Conversation

@a-hirota

@a-hirota a-hirota commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Description

Chunked Parquet reads with a non-zero input pass limit can hang or encounter an illegal device access on Zstd DataPageV2 files. The extended decompression scratch-size query receives each whole page, including uncompressed repetition/definition levels and pages whose values are not compressed. The decompressor already handles these cases when preparing its actual input buffers.

Use a shared input-selection helper to skip uncompressed V2 pages, advance past level bytes, and exclude pages without a remaining payload. Apply that selection to both cumulative and total decompression-size estimates, so skipped pages cannot dilute the extended/legacy scratch adjustment ratio. Retain full page output sizes as conservative bounds, consistent with the decompressor's codec statistics; prefixes with no compressed pages require no scratch query.

Add 16 reader cases covering V1, V2 with default chunk-level compression, V2 with page-level compression, nullable/list columns, dictionary/plain encoding, mixed compressed/uncompressed pages, and empty pages within Zstd chunks. Each checks values and nulls with small and large pass limits through direct file input and pre-parsed footers. Two direct regression tests check input pointers/lengths and verify that inserting skipped pages cannot reduce the scratch estimate for the same compressed pages.

Keep the existing parquet_read_subrowgroup_chunks benchmark's 144 Snappy/V1 configurations at 512 MiB. Add a separate parquet_read_v2_scratch benchmark with only 12 Zstd/V2 configurations at a committed default of 8 MiB, covering integer/string/list inputs, page-level compression on/off, and pass limits of 0/500,000 bytes.

Closes #24002.

Validation

  • Built libcudf, PARQUET_TEST, HYBRID_SCAN_TEST, and PARQUET_READER_CHUNKS_NVBENCH from main at a8ad2045e17034209e29cec01a6b4a51d87764d7 (26.10), using a separate worktree/build and an isolated dependency environment on NVIDIA GB10 / Linux aarch64: CUDA 13.3, GCC 14.4, RMM/KvikIO 26.10 nightlies, nvCOMP 5.3.0.16.
  • PARQUET_TEST: 566 passed, 1 skipped, 4 disabled. HYBRID_SCAN_TEST: all 97 passed.
  • CUDA 13.3 compute-sanitizer --tool memcheck: all 18 new cases passed, with 0 errors.
  • Negative controls: restoring whole-page input spans makes DecompressionInputs fail on V2 offsets and skipped pages. Restoring unfiltered page-size accounting makes SkippedPagesDoNotReduceScratchEstimate fail: its first compressed-page estimate falls from 3,279 to 1,588 bytes when skipped pages are inserted. These tests detect the defects without relying on nvCOMP hanging on invalid input. Restored the fix and rebuilt before running the full suites and sanitizer above.
  • A local input auditor observed 656 valid Zstd frame prefixes in scratch-query inputs across the 18 cases.
  • Re-ran the issue's PyArrow 21.0.0 / seed=2 / 200,000-row input: all values and nulls matched in 12 runs covering V1/V2, direct/pre-parsed-footer readers, and pass limits of 0, 1,000,000, and 1,024,000,000 bytes; no scratch-query fallback warnings.
  • Benchmark: all 12 new configurations completed with 20 samples each using their committed axis defaults, including the 8 MiB input size. Benchmark registration confirms 144 configurations for the existing sub-row-group benchmark and 12 for the new focused benchmark.
  • Local C++ pre-commit checks passed. The existing Theseus 26.08 code and development environment were not modified.

Checklist

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

@a-hirota
a-hirota requested a review from a team as a code owner September 6, 2026 10:52
@copy-pr-bot

copy-pr-bot Bot commented Sep 6, 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 the libcudf Affects libcudf (C++/CUDA) code. label Sep 6, 2026
@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 91481361-311a-458d-a36b-cfd8ea105f4b

📥 Commits

Reviewing files that changed from the base of the PR and between 6195c43 and 2fe0ccb.

📒 Files selected for processing (4)
  • cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp
  • cpp/src/io/parquet/reader_impl_chunking_utils.cu
  • cpp/src/io/parquet/reader_impl_chunking_utils.cuh
  • cpp/tests/io/parquet_chunked_reader_test.cu

Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Improved chunked Parquet reading for V2 pages, including nullable and repeated values.
    • Corrected handling of uncompressed, empty, skipped, and compressed page data.
    • Improved reader initialization when checking for additional data or using pre-parsed metadata.
  • Tests

    • Added regression coverage across varied Parquet page configurations and compression states.
  • Benchmarks

    • Expanded Parquet read benchmarks to cover V2 headers, page compression, compression codecs, and selected data types.

Walkthrough

Changes

Parquet chunked-read handling

Layer / File(s) Summary
V2 decompression input selection
cpp/src/io/parquet/reader_impl_chunking_utils.cu, cpp/src/io/parquet/reader_impl_chunking_utils.cuh
Scratch sizing now uses the same compressed value spans as decompression. It skips V2 level bytes, uncompressed pages, and empty payloads.
Chunked-reader regression coverage
cpp/tests/io/parquet_chunked_reader_test.cu
Tests cover decompression input selection, scratch sizing, V2 page variants, skipped pages, metadata validation, and chunked-read equivalence.
Compression and V2 benchmark matrix
cpp/benchmarks/io/parquet/parquet_reader_chunks.cpp
The benchmark uses explicit compression and page-level settings and adds ZSTD V2 scratch cases.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: ⚪ Minimal · up to 2fe0c

Chunked Parquet reads now size Zstd decompression scratch space from actual compressed value payloads, avoiding V2 level bytes and skipped pages. The changed behavior is covered across relevant page and reader configurations, with no current merge-blocking risk identified.

Suggested reviewers: vyasr

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #24002 by selecting only valid compressed payloads, skipping V2 level bytes and uncompressed pages, preserving conservative scratch estimates, and adding coverage for chunked…
Out of Scope Changes check ✅ Passed The tests, benchmark updates, and exported helper changes support the linked issue and stated PR objectives. No unrelated code changes are evident.
Title check ✅ Passed The title clearly summarizes the main change: correcting Parquet V2 input handling for decompression scratch-size queries.
Description check ✅ Passed The description directly explains the Parquet V2 scratch-size fix, regression tests, benchmarks, validation results, and linked issue.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 3 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

/**
* @brief Returns the compressed values passed to the decompressor, excluding V2 level bytes.
*/
struct get_decompression_input {

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.

The definition of "what the decompressor actually consumes" now lives in three places: codec_stats::add_pages (reader_impl_chunking_utils.cu:121-146), set_parameters (reader_impl_chunking_utils.cu:529-575), and this new functor. Divergence between the first two and the scratch query is precisely the bug being fixed here, so leaving three copies invites a repeat.

Declaring the functor CUDF_HOST_DEVICE and calling it from both host-side loops would collapse the duplication and make the invariant enforced by construction rather than by comment.

{
return {parquet_compression_support(chunks[p.chunk_idx].codec).first,
auto const codec = parquet_compression_support(chunks[p.chunk_idx].codec).first;
if (get_decompression_input{}(p).empty()) { return {codec, 0, 0, 0}; }

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.

With get_decomp_info now excluding empty-payload V2 pages, the estimate can sit slightly below what decompression actually requests: codec_stats::add_pages still folds those pages' uncompressed_page_size into codec.total_decomp_size and max_decompressed_size, and both are passed straight to cudf::io::detail::decompress at reader_impl_chunking_utils.cu:618-619.

The gap is small (such pages carry only level bytes) and the previous code overestimated, so this is not a blocker, but it is the same class of divergence the PR sets out to remove and is worth either closing or calling out in the comment.

? get_decompression_input{}(page)
: device_span<uint8_t const>{};
});
// Copy only non-null spans

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.

The copy_if predicate just below still keys off span.data() != nullptr. Now that empty spans are produced deliberately and carry meaning, not span.empty() expresses the intent directly and is robust if get_decompression_input ever returns a non-null zero-length span.

check_chunked_read(filepath, expected, use_metadata);
}

INSTANTIATE_TEST_SUITE_P(PageVersionAndReader,

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.

The 16 reader cases are more than this bug needs, and two of the parameterization axes cannot interact with the code being fixed.

use_metadata doubles every case to compare the two chunked_parquet_reader constructors, but both converge on the same chunking and scratch-estimation code once the footer is parsed. dictionary doubles it again, yet dictionary pages never carry PAGEINFO_FLAGS_V2, so is_compressed is always true and the level offset always zero for them, yielding no variation in the three conditions the fix turns on.

Dropping use_metadata and dictionary leaves V2 with chunk-level compression, V2 with page-level compression, and a V1 regression guard, which covers the defect at about a third of the runtime.

tmp_env_var const nvcomp{nvcomp_policy_env_var, "ALWAYS"};
tmp_env_var const host_decomp{host_decomp_env_var, "OFF"};
auto const stream = cudf::get_default_stream();
// A Zstd frame containing 32,768 repetitions of '*'.

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.

The hand-written 19-byte Zstd frame is opaque and pins the test to a specific frame encoding. Producing it with cudf::io::detail::compress(compression_type::ZSTD, ...) at test setup would be self-describing and would keep the test valid if the accepted frame layout ever shifts.

NVBENCH_BENCH_TYPES(BM_parquet_read_subrowgroup_chunks, NVBENCH_TYPE_AXES(d_type_list))
.set_name("parquet_read_subrowgroup_chunks")
.add_string_axis("io_type", {"DEVICE_BUFFER"})
.add_string_axis("compression_type", {"SNAPPY"})

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.

Adding compression_type, write_v2_headers, and page_level_compression as single-value axes is forced by sharing one callable across two registrations, but it changes every existing parquet_read_subrowgroup_chunks state string and so breaks continuity with previously recorded results. A thin wrapper function that hardcodes the Snappy/V1 values for the original benchmark would keep the old axis set intact.

@mhaseeb123 mhaseeb123 left a comment

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.

Same comments as @vuule plus the following

}
};

TEST_F(ParquetScratchTest, DecompressionInputs)

@mhaseeb123 mhaseeb123 Sep 8, 2026

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.

I think this and SkippedPagesDoNotReduceScratchEstimate tests should be removed and the fix should be covered via Parquet write/read. This also removes the internal-header include and the CUDF_EXPORT on compute_decompression_scratch_sizes util.

Note that pytests in pylibcudf or cudf-python can also be added if libcudf alone can't exercise the bug path.

Please move any generic decompression tests to the respective suite in tests/io/comp.

.add_int64_axis("row_group_size_bytes", {0})
.add_int64_axis("row_group_size_rows", {0});

// Keep the existing 512 MiB matrix unchanged; cover V2 scratch queries with 12 small cases.

@mhaseeb123 mhaseeb123 Sep 8, 2026

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.

I think these extra benchmarks should be removed as we don't really need a benchmark for a bug fix (unless the fix should affect the performance). We should cover V2 scratch queries in tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

libcudf Affects libcudf (C++/CUDA) code.

Projects

None yet

3 participants