Skip to content

Dispatch SplitScan to HybridScanReader in the streaming engine - #22857

Closed
Matt711 wants to merge 30 commits into
NVIDIA:mainfrom
Matt711:fea/polars/hybrid-scan
Closed

Dispatch SplitScan to HybridScanReader in the streaming engine#22857
Matt711 wants to merge 30 commits into
NVIDIA:mainfrom
Matt711:fea/polars/hybrid-scan

Conversation

@Matt711

@Matt711 Matt711 commented Jun 11, 2026

Copy link
Copy Markdown
Member

Description

Hybrid scan lets us have more control over what happens when executing a Scan. By default, the streaming engine uses cudf::io::read_parquet to compute each Scan. Using cudf::io::parquet::experimental::HybridScanReader in cudf_polars essentially lets us break up the I/O and compute that happens during a call to cudf::io::read_parquet. In hybrid-scan speak, we split the read into two passes: first we read only the filter columns and compute a row mask, then we read only the payload columns for rows that survive the filter and combine. The typical benefit is for a selective filter. We only transfer the payload columns for rows that pass, rather than reading everything upfront and discarding filtered rows afterward.

For remote IO, HybridScanReader exposes the exact byte ranges needed for the filter columns and payload columns separately. We can prefetch the filter column byte ranges asynchronously into pinned host memory and transfer to device memory. For row groups that don't survive the filter, the payload byte ranges are never fetched at all. In this way, we aren't bottlenecked by transferring payload data that will only be discarded. This will be done in a folow-up PR.

We only dispatch to HybridScanReader for SplitScans in the streaming engine. SplitScan is used by the streaming engine to parallelize reads of a single large file. This is good for the single-file HybridScanReader. FusedScan fuses multiple smaller files into one (logically) and AFAICT would be a good candidate for the multi-file hybrid scan reader (see #22583). Finally for the in-memory engine, there isn't a good justification for using hybrid scan (prefer streaming over in-memory always).

Dispatch is also conditional on having a filter predicate (we'll fallback to default libcudf parquet reader otherwise).

So for this first PR: Enabling CUDF_POLARS__PARQUET_OPTIONS__USE_HYBRID_SCAN=1 means dispatching SplitScans to HybridScanReader whenever there's a filter predicate (that we support translating to libcudf).

Checklist

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

@Matt711 Matt711 added feature request New feature or request non-breaking Non-breaking change labels Jun 11, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jun 11, 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. CMake CMake build issue cudf-polars Issues specific to cudf-polars pylibcudf Issues specific to the pylibcudf package labels Jun 11, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jun 11, 2026
@rjzamora

Copy link
Copy Markdown
Contributor

Thanks @Matt711 ! Just a quick note that most real-world S3 datasets that I see have files on the smaller side. Therefore, it makes perfect sense to start with SplitScan as long as we have plans to support FusedScan in the future.

The SplitScan-only support also makes me wonder if we should treat single-file reads as a special case of SplitScan rather than a special case of FusedScan?

return scans


def _read_with_hybrid_scan(

@Matt711 Matt711 Jun 11, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Benchmarks: TPC-H Q6, SF1k, one iteration

On main: 2 sec

Image

This PR: 10 sec

Image

I'm seeing large gaps between SplitScan tasks with hybrid scan (54ms -> 249ms ~4x longer). I'm not sure if this is due to python overhead in the two-pass approach or something else. Note I dont see how this could be due to the synchronous plc.io.parquet_io_utils.fetch_byte_ranges_to_device since this is inter-scan not intra-scan. I'm noting here because that might be a thought you had since plc.io.parquet_io_utils.fetch_byte_ranges_to_device is synchronous. Thatisn't to say we arnt paying for that synchroneity, but 5x slower is do to the inter-scan cost is my current hypothesis.

@Matt711

Matt711 commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Thanks @Matt711 ! Just a quick note that most real-world S3 datasets that I see have files on the smaller side. Therefore, it makes perfect sense to start with SplitScan as long as we have plans to support FusedScan in the future.

Thanks, yup FusedScan support with hybrid scan is TODO.

The SplitScan-only support also makes me wonder if we should treat single-file reads as a special case of SplitScan rather than a special case of FusedScan?

Oh nice, I think we should do this

@Matt711

Matt711 commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

/ok to test 78b3ac9

@copy-pr-bot

copy-pr-bot Bot commented Jun 12, 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.

@NVIDIA NVIDIA deleted a comment from copy-pr-bot Bot Jun 15, 2026
@Matt711

Matt711 commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

/ok to test 7f2cba1

Comment thread python/cudf_polars/cudf_polars/utils/config.py
Comment thread python/pylibcudf/pylibcudf/io/parquet_io_utils.pyx Outdated
"""
with opaque_memory_usage(
await reserve_memory(
with nvtx_annotate_cudf_polars(message="reserve_memory"):

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.

Just a note that the annotations produced nvtx.annotate (which this nvtx_annotate_cudf_polars uses) spanning an await can be confusing.

The await will suspend execution here until the future resolves. The thread executing this coroutine might move onto another read_chunk task, and push another "reserve_memory" nvtx range onto the stack, making it look like a child of the first nvtx.annotate.

The begin_range() / end_range() APIs might be less confusing. #22718 ran into similar issues.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Ah good call out, thanks

@Matt711

Matt711 commented Jul 8, 2026

Copy link
Copy Markdown
Member Author

/ok to test a027ad9

@Matt711

Matt711 commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

/ok to test 258c008

@Matt711

Matt711 commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Superceded by #23317

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

Labels

CMake CMake build issue cudf-polars Issues specific to cudf-polars feature request New feature or request libcudf Affects libcudf (C++/CUDA) code. non-breaking Non-breaking change pylibcudf Issues specific to the pylibcudf package Python Affects Python cuDF API.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

5 participants