Skip to content

Improve parquet footer reading performance by speculatively reading footer bytes - #22782

Merged
rapids-bot[bot] merged 25 commits into
NVIDIA:mainfrom
TomAugspurger:tom/libcudf-speculative-footer-read
Jun 15, 2026
Merged

Improve parquet footer reading performance by speculatively reading footer bytes#22782
rapids-bot[bot] merged 25 commits into
NVIDIA:mainfrom
TomAugspurger:tom/libcudf-speculative-footer-read

Conversation

@TomAugspurger

@TomAugspurger TomAugspurger commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Description

This PR aims to improve the performance of reading parquet footers (and all functions calling it, including read_parquet) on high-latency storage systems like S3. On these storage systems, each read call translates to an HTTP range request, which takes 10s - 100s of ms to complete. On main, we make two read requests in order to only read exactly the bytes necessary

  1. The last 8 bytes of the file, which incudes the size of the file's footer
  2. The n bytes required to read the actual footer

On high-latency storage systems, it's faster to make just one read (HTTP request), even if that read happens to be larger than is strictly necessary. It's (much) faster to read, say 64 KB in a single HTTP request than it is to read 8 bytes + (say) 32 KB in two HTTP requests.


This PR updates all the sites that eventually read the footer, including read_parquet, read_parquet_metadata, etc. to accept a metadata_size_hint. If it's not provided, we fall back to a default controlled in our configuration system. The default is 64KB (matching arrow C++ and polars, smaller than DuckDB and datafusion).

Speculative reading can be disabled at call sites by setting metadata_size_hint=0, or globally by setting LIBCUDF_PARQUET_METADATA_SIZE_HINT=0. Either will restore the old behavior of reading exactly 8 bytes for the suffix, and then exactly the footer size.

Python bindings in pylibcudf are added too.

Closes #22668

#22782 (comment) has some performance analysis when reading from S3.

Checklist

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

@copy-pr-bot

copy-pr-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@github-actions github-actions Bot added libcudf Affects libcudf (C++/CUDA) code. Python Affects Python cuDF API. pylibcudf Issues specific to the pylibcudf package labels Jun 4, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jun 4, 2026
@TomAugspurger TomAugspurger added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Jun 5, 2026
Comment thread cpp/include/cudf/io/config_utils.hpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

/ok to test f6e7eca

@TomAugspurger
TomAugspurger marked this pull request as ready for review June 5, 2026 15:45
@TomAugspurger
TomAugspurger requested review from a team as code owners June 5, 2026 15:45
@coderabbitai

coderabbitai Bot commented Jun 5, 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

Walkthrough

This PR implements speculative Parquet footer reading by introducing a configurable metadata_size_hint() that defaults to 64 KiB and can be overridden via the LIBCUDF_PARQUET_METADATA_SIZE_HINT environment variable. The footer fetch logic is refactored to perform a single speculative tail read, validate footer magic bytes and length within that buffer, then either extract the complete footer or stitch together prefix and suffix bytes as needed.

Changes

Parquet metadata size hint and speculative footer reading

Layer / File(s) Summary
Configuration accessor contract and implementation
cpp/include/cudf/io/config_utils.hpp, cpp/src/io/utilities/config_utils.cpp
metadata_size_hint() is declared in the cudf::io::parquet_reader namespace with [[nodiscard]] and implemented to read LIBCUDF_PARQUET_METADATA_SIZE_HINT with a 64 KiB default, caching the result in a function-local static.
Header includes and dependencies
cpp/include/cudf/io/detail/parquet.hpp, cpp/src/io/parquet/reader_impl_helpers.hpp, cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
<cstddef>, <optional>, and standard library algorithm/utility headers are added to support std::size_t, optional usage, and new implementation requirements. config_utils.hpp is included in parquet_io_utils.cpp.
Speculative footer read implementation
cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
fetch_footers_to_host_impl is refactored to compute metadata_size_hint() once and perform a speculative host read of the tail from each datasource. Footer magic bytes and length are validated from the speculative buffer; the footer is extracted directly if fully contained, or the missing prefix is read and stitched together with the suffix. The wrapper fetch_footer_to_host delegates to the multi-source implementation via a datasource reference array.
Error path validation tests
cpp/tests/io/parquet_reader_test.cpp
Mock cudf::io::datasource implementations track and truncate reads. Helpers generate minimal in-memory Parquet buffers and verify thrown cudf::logic_error substrings. The MetadataFooterErrorMessages test validates exact error messages for corrupted headers, footers, shortened reads, and length fields using the new infrastructure.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes


Possibly related PRs

  • rapidsai/cudf#22679: Both PRs refactor Parquet footer fetching in parquet_io_utils.cpp, with this PR using speculative tail reads and metadata_size_hint while the retrieved PR skips the initial header-magic validation.
  • rapidsai/cudf#22613: Both PRs modify multi-datasource Parquet footer-fetching in fetch_footers_to_host_impl, with this PR changing the read strategy to speculative tail reads while the retrieved PR introduces the multi-source fetch utilities.

Suggested labels

cuIO, 4 - Needs Review


Suggested reviewers

  • bdice
  • qbacpey
  • davidwendt
  • PointKernel
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.89% 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 and specifically describes the main change: improving parquet footer reading performance through speculative reading, which is the core objective of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the performance problem, solution approach, configuration mechanism, and performance analysis.
Linked Issues check ✅ Passed The PR implements the requested feature from #22668: speculative footer reading with configurable metadata_size_hint (default 64KB), ability to disable per-call or globally, and consolidates reads to improve performance on high-latency storage.
Out of Scope Changes check ✅ Passed All changes are in-scope: config_utils for the metadata_size_hint function/environment variable, parquet_io_utils for speculative reading logic, header includes, and comprehensive test coverage for error handling.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@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

🧹 Nitpick comments (1)
cpp/tests/io/parquet_reader_test.cpp (1)

2972-3004: ⚡ Quick win

HIGH: Add explicit metadata_size_hint == 0 boundary case in call-count tests

Line 2998 and Line 3071 currently validate only full_hint and minimal_hint=8. Please also assert hint=0 behavior so the “disable speculative read” contract is regression-tested in both metadata and footer paths.

Suggested test addition
 TEST_F(ParquetMetadataReaderTest, MetadataSizeHintReadCallCount)
 {
@@
   EXPECT_EQ(tracking_source.reads()[0].second, minimal_hint);
+
+  tracking_source.reset();
+  constexpr size_t zero_hint = 0;
+  auto const metadata_zero_hint = cudf::io::read_parquet_metadata(source, zero_hint);
+  EXPECT_EQ(metadata_zero_hint.num_rows(), num_rows);
+  ASSERT_EQ(tracking_source.reads().size(), 2);
+  EXPECT_EQ(tracking_source.reads()[0].first, tracking_source.size() - 8);
+  EXPECT_EQ(tracking_source.reads()[0].second, 8);
 }
@@
 TEST_F(ParquetMetadataReaderTest, FooterSizeHintReadCallCount)
 {
@@
   EXPECT_EQ(tracking_source.reads()[0].second, minimal_hint);
+
+  tracking_source.reset();
+  constexpr size_t zero_hint = 0;
+  auto const zero_hint_footers = cudf::io::read_parquet_footers(sources, zero_hint);
+  ASSERT_EQ(zero_hint_footers.size(), 1);
+  EXPECT_EQ(zero_hint_footers[0].num_rows, num_rows);
+  ASSERT_EQ(tracking_source.reads().size(), 2);
+  EXPECT_EQ(tracking_source.reads()[0].first, tracking_source.size() - 8);
+  EXPECT_EQ(tracking_source.reads()[0].second, 8);
 }

As per coding guidelines: “Test functions must cover edge cases: empty input, null values, sliced columns, boundary sizes, multi-block sizes”.

Also applies to: 3045-3078

🤖 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/tests/io/parquet_reader_test.cpp` around lines 2972 - 3004, Add a third
subcase calling cudf::io::read_parquet_metadata(source, 0) inside
ParquetMetadataReaderTest::MetadataSizeHintReadCallCount and the other similar
test (around lines 3045-3078) to explicitly exercise the metadata_size_hint == 0
boundary; verify the TrackingFooterDatasource::reads() shows that speculative
suffix reads are disabled (i.e., the implementation issues a single precise
footer read rather than a prior minimal speculative read) by asserting the reads
count and that the first read offset/length correspond to the exact footer fetch
rather than a minimal suffix read. Ensure you reference the test name
MetadataSizeHintReadCallCount and the TrackingFooterDatasource instance
(tracking_source) when adding the assertions.
🤖 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 `@python/pylibcudf/tests/io/test_parquet.py`:
- Around line 295-353: Add edge-case tests for metadata_size_hint: extend the
existing tests (e.g., test_read_parquet_metadata_with_size_hint,
test_read_parquet_footers_with_size_hint, or add new functions) to call
plc.io.parquet_metadata.read_parquet_metadata and
plc.io.parquet_metadata.read_parquet_footers with metadata_size_hint=0 and
assert correct behavior (should succeed and produce same metadata as default),
and add a test that passes a negative metadata_size_hint (e.g., -1) to those
same functions and asserts they raise ValueError; reference SourceInfo,
plc.io.parquet_metadata.read_parquet_metadata,
plc.io.parquet_metadata.read_parquet_footers, and plc.io.parquet.read_parquet
(if using pre-materialized footers) when adding these assertions.

---

Nitpick comments:
In `@cpp/tests/io/parquet_reader_test.cpp`:
- Around line 2972-3004: Add a third subcase calling
cudf::io::read_parquet_metadata(source, 0) inside
ParquetMetadataReaderTest::MetadataSizeHintReadCallCount and the other similar
test (around lines 3045-3078) to explicitly exercise the metadata_size_hint == 0
boundary; verify the TrackingFooterDatasource::reads() shows that speculative
suffix reads are disabled (i.e., the implementation issues a single precise
footer read rather than a prior minimal speculative read) by asserting the reads
count and that the first read offset/length correspond to the exact footer fetch
rather than a minimal suffix read. Ensure you reference the test name
MetadataSizeHintReadCallCount and the TrackingFooterDatasource instance
(tracking_source) when adding the assertions.
🪄 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: 9c836288-7803-42d7-87b3-8050db6734d2

📥 Commits

Reviewing files that changed from the base of the PR and between b00d56a and f6e7eca.

📒 Files selected for processing (16)
  • cpp/include/cudf/io/config_utils.hpp
  • cpp/include/cudf/io/detail/parquet.hpp
  • cpp/include/cudf/io/parquet_io_utils.hpp
  • cpp/include/cudf/io/parquet_metadata.hpp
  • cpp/src/io/functions.cpp
  • cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
  • cpp/src/io/parquet/reader_impl.cpp
  • cpp/src/io/parquet/reader_impl_helpers.cpp
  • cpp/src/io/parquet/reader_impl_helpers.hpp
  • cpp/src/io/utilities/config_utils.cpp
  • cpp/tests/io/parquet_reader_test.cpp
  • python/pylibcudf/pylibcudf/io/parquet_metadata.pxd
  • python/pylibcudf/pylibcudf/io/parquet_metadata.pyi
  • python/pylibcudf/pylibcudf/io/parquet_metadata.pyx
  • python/pylibcudf/pylibcudf/libcudf/io/parquet_metadata.pxd
  • python/pylibcudf/tests/io/test_parquet.py

Comment thread python/pylibcudf/tests/io/test_parquet.py Outdated
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

Some performance analysis:

Details
import boto3
import nvtx
import pylibcudf as plc

bucket = "rapids-tpch"
key = "scale-10/nation/part.0.parquet"
url = f"s3://{bucket}/{key}"

sources = plc.io.types.SourceInfo([url])

with nvtx.annotate("warmup"):
    plc.io.parquet_metadata.read_parquet_metadata(sources)


with nvtx.annotate("read_parquet_metadata"):
    with nvtx.annotate("speculative"):
        _ = plc.io.parquet_metadata.read_parquet_metadata(sources)

    with nvtx.annotate("non-speculative"):
        _ = plc.io.parquet_metadata.read_parquet_metadata(sources, metadata_size_hint=0)

with nvtx.annotate("read_parquet_footers"):
    with nvtx.annotate("speculative"):
        footers = plc.io.parquet_metadata.read_parquet_footers(sources)

    with nvtx.annotate("non-speculative"):
        footers_with_size = plc.io.parquet_metadata.read_parquet_footers(sources, metadata_size_hint=0)

And here's a screnshot of the nsys profile captured with nsys profile --python-backtrace=cuda --python-sampling=true -o speculative -f true --trace=cuda,osrt,nvtx python speculative.py

image

The most interesting bit is the fetch_footer_to_host ranges. That takes about 75-80ms with our speculative footer read, and 150-170ms without.

It's not the easiest to follow since it's spread across threads, but this comes down to making one read (HTTP request) rather than two.

Speculative read:

image

Non-speculative read:

image

@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/tests/io/parquet_reader_test.cpp`:
- Around line 3254-3260: The test uses a fixed hint 64 which can be larger than
the actual footer start and make the speculative-read check flaky; instead
compute the hint from the actual footer length so the speculative read does NOT
include the full footer bytes (e.g., set hint = footer_len +
sizeof(file_ender_s) - 1) and pass that hint to read_parquet_metadata(source,
hint). Update the TailShortReadDatasource/parquet_bytes-based test setup to
derive footer_len from the generated parquet_bytes and use that derived hint in
the read_parquet_metadata call so the "Speculative metadata read did not include
full footer bytes" branch is reliably exercised.
🪄 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: 27f85695-3cdd-4740-8a5a-fbb90cbb475f

📥 Commits

Reviewing files that changed from the base of the PR and between f6e7eca and 79ba978.

📒 Files selected for processing (3)
  • cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
  • cpp/tests/io/parquet_reader_test.cpp
  • python/pylibcudf/tests/io/test_parquet.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • python/pylibcudf/tests/io/test_parquet.py
  • cpp/src/io/parquet/io_utils/parquet_io_utils.cpp

Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
TomAugspurger and others added 3 commits June 8, 2026 13:42
- revert unnecessary change to cudf::host_span construction
- use std::format for error messages
@TomAugspurger
TomAugspurger removed the request for review from a team June 9, 2026 16:35
Comment thread cpp/include/cudf/io/config_utils.hpp Outdated
Comment thread cpp/src/io/utilities/config_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
Comment thread cpp/tests/io/parquet_reader_test.cpp Outdated
@vuule

vuule commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

I would heavily cut down the added tests (see comments above).
I think instead we need a few tests that count the read number based on the hint env var. Env var can be varied through a fixture (see tmp_env_var and DecompressionTest).
Overall I would like this PR to be smaller/simpler than it as ATM. @mhaseeb123 let me know if you disagree

Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp Outdated
Comment thread cpp/src/io/parquet/io_utils/parquet_io_utils.cpp
Comment thread cpp/src/io/utilities/config_utils.cpp Outdated
Comment thread cpp/src/io/parquet/reader_impl_helpers.hpp Outdated
Comment thread cpp/include/cudf/io/detail/parquet.hpp Outdated

@vuule vuule 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.

Looks good, assuming memmove comments are resolved.
Thank you for iterating on this @TomAugspurger !

Comment on lines +138 to +143
auto const missing_prefix_size = speculative_read_offset - footer_offset;
auto missing_prefix = datasource.host_read(footer_offset, missing_prefix_size);
CUDF_EXPECTS(missing_prefix->size() == missing_prefix_size,
"Failed to read the missing footer prefix bytes");
std::vector<uint8_t> footer_bytes(ender->footer_len);
std::memcpy(footer_bytes.data(), missing_prefix->data(), missing_prefix_size);

@mhaseeb123 mhaseeb123 Jun 10, 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.

Saves us one memcpy if we allocate footer_bytes before hand and directly host_read into it.

Suggested change
auto const missing_prefix_size = speculative_read_offset - footer_offset;
auto missing_prefix = datasource.host_read(footer_offset, missing_prefix_size);
CUDF_EXPECTS(missing_prefix->size() == missing_prefix_size,
"Failed to read the missing footer prefix bytes");
std::vector<uint8_t> footer_bytes(ender->footer_len);
std::memcpy(footer_bytes.data(), missing_prefix->data(), missing_prefix_size);
std::vector<uint8_t> footer_bytes(ender->footer_len);
auto const missing_prefix_size = speculative_read_offset - footer_offset;
datasource.host_read(footer_offset, missing_prefix_size, footer_bytes.data());

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.

Thanks @mhaseeb123. With that change, one of the (new) tests fails:

$ ./cpp/build/conda/cuda-12.9/release/gtests/PARQUET_TEST  --gtest_filter=ParquetMetadataSizeHintTest.ReadParquet
Note: Google Test filter = ParquetMetadataSizeHintTest.ReadParquet
[==========] Running 1 test from 1 test suite.
[----------] Global test environment set-up.
[----------] 1 test from ParquetMetadataSizeHintTest
[ RUN      ] ParquetMetadataSizeHintTest.ReadParquet
unknown file: Failure
C++ exception with description "CUDF failure at: /home/coder/cudf/cpp/src/io/parquet/reader_impl_helpers.cpp:324: Cannot initialize schema" thrown in the test body.

[  FAILED  ] ParquetMetadataSizeHintTest.ReadParquet (114 ms)
[----------] 1 test from ParquetMetadataSizeHintTest (114 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test suite ran. (114 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] ParquetMetadataSizeHintTest.ReadParquet

And when running all the tests with ./cpp/build/conda/cuda-12.9/release/gtests/PARQUET_TEST there are a few more, seemingly the ones that have large footers:

[----------] Global test environment tear-down
[==========] 455 tests from 129 test suites ran. (117895 ms total)
[  PASSED  ] 451 tests.
[  FAILED  ] 4 tests, listed below:
[  FAILED  ] ParquetReaderTest.TableTooLargeOverflows
[  FAILED  ] ParquetChunkedWriterTest.LargeTables
[  FAILED  ] ParquetChunkedWriterTest.ManyTables
[  FAILED  ] ParquetMetadataSizeHintTest.ReadParquet

It seems to me like

datasource.host_read(footer_offset, missing_prefix_size, footer_bytes.data());

doesn't get us the full footer. IIUC, footer_bytes is a newly allocated vec with the size of the full footer. After this host_read, the only initialized bytes will be [footer_start, missing_prefix), which will be a subset of the footer; just the bytes we failed to read in the speculative read. For example, if we have

  1. Footer size: 6405 Bytes
  2. Speculative read: 6400 Bytes

Then we'd have missing_prefix = 5 and footer_bytes would contain just the 5 bytes at the start of the footer plus uninitialized memory.

But I could easily be misreading things!

@TomAugspurger

TomAugspurger commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

Here are some numbers from the benchmark ./cpp/build/conda/cuda-12.9/latest/benchmarks/PARQUET_READER_METADATA_NVBENCH -b parquet_read_footer --devices 0 --rmm_mode cuda.

Recall the tradeoff here: we're potentially reading too many bytes, in the hope that we capture the full footer. The possible outcomes are

  1. The speculative read contains the full footer: We've read too many bytes, but we've done one read instead of two. Whether this is beneficial depends on the latency / throughput of the storage system, network, memory, etc.
  2. The speculative read contains a partial footer. We still need two reads to read the footer. We also need a memcpy to stitch together the speculatively read bytes with the partial footer, and the rump of the footer missed by the speculative read.

https://gist.github.com/TomAugspurger/069abda3bbfe7728b910187a09ff7e5f is a little script to the output of this branch with speculative reading on and disabled (full results below). There are two cases where the time with speculative reading on is outside the noise band of the run with it off. The num_cols=64, num_row_groups=10 is faster. num_cols=512, num_row_groups=50 is slower.

2 row(s) outside the noise band:

 page_index  num_cols  num_row_groups  cpu_time_us_off  cpu_noise_off  noise_lower  noise_upper  cpu_time_us_on  delta_pct
          1        64              10          1451.00           0.06      1370.90      1531.10         1361.00      -6.20
          0       512              50         27911.00           0.08     25764.64     30057.36        33918.00      21.52

First, with the new default to speculatively read 64 KB:

parquet_read_footer

[0] Tesla V100-SXM2-32GB

io_type page_index num_cols num_row_groups Samples CPU Time Noise GPU Time Noise colchunks_per_sec peak_memory_usage
FILEPATH 1 64 10 1472x 1.361 ms 7.87% 1.350 ms 7.83% 473925 0.000 B
FILEPATH 0 64 10 880x 725.928 us 11.96% 715.158 us 11.94% 894907 0.000 B
FILEPATH 1 256 10 1072x 4.271 ms 6.39% 4.259 ms 6.33% 601108 0.000 B
FILEPATH 0 256 10 784x 2.347 ms 7.99% 2.335 ms 7.94% 1096422 0.000 B
FILEPATH 1 512 10 1392x 7.820 ms 7.47% 7.807 ms 7.47% 655819 0.000 B
FILEPATH 0 512 10 1056x 5.140 ms 6.05% 5.130 ms 6.04% 998038 0.000 B
FILEPATH 1 64 50 832x 6.217 ms 6.62% 6.203 ms 6.60% 515874 0.000 B
FILEPATH 0 64 50 1040x 3.155 ms 9.92% 3.140 ms 9.97% 1019063 0.000 B
FILEPATH 1 256 50 464x 20.304 ms 7.08% 20.292 ms 7.07% 630792 0.000 B
FILEPATH 0 256 50 768x 10.706 ms 11.63% 10.692 ms 11.63% 1197110 0.000 B
FILEPATH 1 512 50 235x 51.295 ms 13.83% 51.282 ms 13.84% 499199 0.000 B
FILEPATH 0 512 50 383x 33.918 ms 11.28% 33.907 ms 11.28% 755015 0.000 B

Second, this branch with speculative reading disabled through the env var: LIBCUDF_PARQUET_METADATA_SIZE_HINT=0 ./cpp/build/conda/cuda-12.9/latest/benchmarks/PARQUET_READER_METADATA_NVBENCH -b parquet_read_footer --devices 0 --rmm_mode cuda

parquet_read_footer

[0] Tesla V100-SXM2-32GB

io_type page_index num_cols num_row_groups Samples CPU Time Noise GPU Time Noise colchunks_per_sec peak_memory_usage
FILEPATH 1 64 10 352x 1.451 ms 5.52% 1.438 ms 5.47% 444964 0.000 B
FILEPATH 0 64 10 1200x 767.236 us 8.49% 750.831 us 8.50% 852388 0.000 B
FILEPATH 1 256 10 1344x 4.394 ms 7.45% 4.381 ms 7.42% 584341 0.000 B
FILEPATH 0 256 10 1136x 2.343 ms 8.21% 2.330 ms 8.17% 1098601 0.000 B
FILEPATH 1 512 10 1296x 7.807 ms 6.65% 7.793 ms 6.64% 657006 0.000 B
FILEPATH 0 512 10 1408x 5.129 ms 7.15% 5.117 ms 7.15% 1000500 0.000 B
FILEPATH 1 64 50 1296x 6.010 ms 6.39% 5.997 ms 6.35% 533618 0.000 B
FILEPATH 0 64 50 640x 3.207 ms 14.10% 3.192 ms 14.14% 1002656 0.000 B
FILEPATH 1 256 50 435x 21.641 ms 8.83% 21.626 ms 8.83% 591869 0.000 B
FILEPATH 0 256 50 656x 10.545 ms 7.30% 10.531 ms 7.29% 1215515 0.000 B
FILEPATH 1 512 50 241x 49.266 ms 11.98% 49.252 ms 11.98% 519772 0.000 B
FILEPATH 0 512 50 450x 27.911 ms 7.69% 27.900 ms 7.68% 917557 0.000 B

Also, if you're curious, only the first two cases (num_cols=64, num_row_groups=10) actually fit within the speculative read size.

@TomAugspurger

TomAugspurger commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

And comparing to main:

 page_index  num_cols  num_row_groups  cpu_time_us_off  cpu_noise_off  noise_lower  noise_upper  cpu_time_us_on  delta_pct
          1        64              10          1475.00           0.07      1370.13      1579.87         1361.00      -7.73
          0       512              50         28056.00           0.06     26296.89     29815.11        33918.00      20.89

So that one benchmark where we do the one-shot footer read is faster. The benchmark with the largest footer does appear to be slower: we fail to read the footer in a single read, and pay the cost of the memcpys.

@vuule

vuule commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

So that one benchmark where we do the one-shot footer read is faster. The benchmark with the largest footer does appear to be slower: we fail to read the footer in a single read, and pay the cost of the memcpys.

I feel like this is a good result. The only slower case has quite a few columns and row groups. In reality, such file would be huge and the few milliseconds lost reading the footer would not affect the total time significantly.

@TomAugspurger

Copy link
Copy Markdown
Contributor Author

The only slower case has quite a few columns and row groups. In reality, such file would be huge and the few milliseconds lost reading the footer would not affect the total time significantly.

Yep. As another point of reference, all of the footers are less that 64 KB for the the tpc-h files generated by tpch-rs at scale-1000. The largest is ~24 KB.

@vuule
vuule requested a review from mhaseeb123 June 12, 2026 21:41
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

/merge

@rapids-bot
rapids-bot Bot merged commit cd0b1b9 into NVIDIA:main Jun 15, 2026
257 of 259 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jun 15, 2026
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews!

@TomAugspurger
TomAugspurger deleted the tom/libcudf-speculative-footer-read branch June 15, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvement / enhancement to an existing function libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change pylibcudf Issues specific to the pylibcudf package Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA]: Speculatively read parquet footer in read_parquet

4 participants