Skip to content

Prefetch byte ranges using kvikio - #23317

Open
Matt711 wants to merge 2 commits into
NVIDIA:mainfrom
Matt711:fea/polars/hybrid-scan-kvikio-pinned-2
Open

Prefetch byte ranges using kvikio#23317
Matt711 wants to merge 2 commits into
NVIDIA:mainfrom
Matt711:fea/polars/hybrid-scan-kvikio-pinned-2

Conversation

@Matt711

@Matt711 Matt711 commented Jul 17, 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 APIs to compute the exact byte ranges needed for the filter and payload columns separately. Once we have the byte ranges for a split, we prefetch the data using kvikio into pinned host memory and transfer to device once the producer is ready.

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. FusedScan fuses multiple smaller files into one (think logically one larger file) 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 so it's only supported for the streaming engine.

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 the DO NOT MERGE Hold off on merging; see PR for details label Jul 17, 2026
@copy-pr-bot

copy-pr-bot Bot commented Jul 17, 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. Python Affects Python cuDF API. CMake CMake build issue cudf-polars Issues specific to cudf-polars pylibcudf Issues specific to the pylibcudf package labels Jul 17, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jul 17, 2026
@Matt711 Matt711 changed the title EXPERIMENT 1: Prefetching parquet byte ranges into pinned host memory and use kvikio to fill those buffers Dispatch SplitScan to HybridScanReader and prefetch byte ranges using kvikio Jul 21, 2026
@Matt711 Matt711 added non-breaking Non-breaking change feature request New feature or request and removed DO NOT MERGE Hold off on merging; see PR for details labels Jul 21, 2026
@Matt711
Matt711 marked this pull request as ready for review July 22, 2026 21:46
@Matt711
Matt711 requested review from a team as code owners July 22, 2026 21:46
@Matt711

Matt711 commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

/ok to test 245827f

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

Perhaps I missed it, but for review purposes can you split the C++ hybrid scan changes, and the pylibcudf exposure, into separate PRs?

Comment on lines +35 to +59
class PinnedBuffer:
"""Pinned host buffer backed by a rapidsmpf PinnedMemoryResource pool."""

__slots__ = ("array", "mr", "nbytes", "ptr", "reservation", "stream")

def __init__(
self,
mr: PinnedMemoryResource,
nbytes: int,
stream: Stream,
reservation: MemoryReservation,
) -> None:
self.mr = mr
self.nbytes = nbytes
self.stream = stream
self.reservation = reservation
self.ptr = mr.allocate(nbytes, stream)
self.array = memoryview((ctypes.c_uint8 * nbytes).from_address(self.ptr))

def __del__(self) -> None: # noqa: D105
# Guard against partial init.
if hasattr(self, "reservation"):
self.reservation.clear()
if hasattr(self, "ptr"):
self.mr.deallocate(self.ptr, self.nbytes, self.stream)

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.

Can we use an existing vocab type. For example a rapidsmpf::Buffer should be fine? I think.

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.

I think so. We need two things first

  1. make_buffer exposed on BufferResource
  2. Implement __buffer__ to get a memoryview that we can hand to pread

Comment thread cpp/include/cudf/io/experimental/hybrid_scan.hpp Outdated
@Matt711

Matt711 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Perhaps I missed it, but for review purposes can you split the C++ hybrid scan changes, and the pylibcudf exposure, into separate PRs?

Yes, will open them shortly

@Matt711
Matt711 removed request for a team, PointKernel, mhaseeb123 and msarahan August 5, 2026 03:37
@Matt711 Matt711 removed libcudf Affects libcudf (C++/CUDA) code. CMake CMake build issue pylibcudf Issues specific to the pylibcudf package labels Aug 5, 2026
@Matt711
Matt711 requested a review from rjzamora August 5, 2026 17:29
rapids-bot Bot pushed a commit that referenced this pull request Aug 5, 2026
Adds support for byte slicing pylibcudf `gpumemoryview`s. Used in #23317 where we make a single device allocation for all byte ranges and then slice it into sub-views when passing them to hybrid scan APIs.

Authors:
  - Matthew Murray (https://github.com/Matt711)

Approvers:
  - Tom Augspurger (https://github.com/TomAugspurger)

URL: #23541


def prefetch_scan_byte_ranges(
scan: SplitScan,

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 see a few places spread across this PR where the details of using HybridScan and prefetching data is very specific to SplitScan. Although this makes sense for prototyping, I have a strong feeling that we should start with infrastructure that is agnostic as possible to SplitScan-vs-FusedScan. You will obviously need the SplitScan variety to actually enable hybrid-scan (for now), but I'd consider the FusedScan variety a P0 follow-up to this work.

Comment on lines +631 to +639
use_prefetch = (
first is not None
and ir.scan_type == "split"
and first.parquet_options.use_hybrid_scan
and first.parquet_options.prefetch_file_metadata
and first.cached_parquet_info is not None
and first.base_scan.predicate is not None
and context.br().pinned_mr is not None
)

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.

Maybe this check should be a dedicated helper/utility for now? I think you may do similar validation in other places, so it would possibly used there as well.

Estimated size of each chunk in bytes. Used for memory reservation
with block spilling to avoid thrashing.
"""
scans: Sequence[SplitScan] | Sequence[FusedScan] = ir.scans

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.

In the case that we are using hybrid scan (or maybe even whenever we have prefetched metadata), I think we should map our list of SplitScans or FusedScans onto a list of task objects that are row-group aligned and more friendly to hybrid scan. For example:

class AlignedParquetScan(IR):
    base_scan: Scan
    cached_parquet_info: list[CachedParquetInfo]
    row_groups_per_source: list[list[int]]
    parquet_options: ParquetOptions

Why?

  • When hybrid scan is enabled and footer metadata has been prefetched, we can assign explicit row-group indices to each read task instead of relying on a uniform split_index / total_splits for every file. Hence “Aligned”: each task reads whole parquet row groups.
  • This gives us a hybrid-scan-friendly execution node that is agnostic to how the original streaming task was planned (SplitScan vs FusedScan). That should make the follow-up multi-file path much easier: the first version may have exactly one cached_parquet_info item per task, but the same AlignedParquetScan design can later represent multi-source reads by filling cached_parquet_info and row_groups_per_source with one entry per source.
  • This also centralizes hybrid-scan eligibility and fallback logic, instead of spreading SplitScan-specific checks across planning, prefetch, and evaluation.

s, stream_pool.get_stream(), pinned_mr, context, loop
)

futures = [executor.submit(_task, scan) for scan in scans]

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 you pointed this out to me offline. If I understand correctly, we are submitting prefetching tasks for all our data up-front, and we are relying on pinned-host memory reservations for back-pressure?

stream=stream,
)

if prefetched is not None and prefetched.payload_host is not None:

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.

What happens when the projected columns are all filter columns?

For example, scan_parquet(...).select("x").filter(pl.col("x") < 10) seems like it could produce an empty payload-column set after materialize_filter_columns. Does this logic handle that case cleanly?

@Matt711
Matt711 force-pushed the fea/polars/hybrid-scan-kvikio-pinned-2 branch from 94a8da6 to 0b7b5d9 Compare August 17, 2026 14:56
rapids-bot Bot pushed a commit to rapidsai/rapidsmpf that referenced this pull request Aug 17, 2026
Adds Python bindings for `rapidsmpf::Buffer` and exposes `BufferResource::make_buffer`.

Needed by NVIDIA/cudf#23317

Authors:
  - Matthew Murray (https://github.com/Matt711)

Approvers:
  - Mads R. B. Kristensen (https://github.com/madsbk)

URL: #1149
@Matt711 Matt711 changed the title Dispatch SplitScan to HybridScanReader and prefetch byte ranges using kvikio Prefetch byte ranges using kvikio Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cudf-polars Issues specific to cudf-polars feature request New feature or request non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

6 participants