Prefetch byte ranges using kvikio - #23317
Conversation
SplitScan to HybridScanReader and prefetch byte ranges using kvikio
|
/ok to test 245827f |
wence-
left a comment
There was a problem hiding this comment.
Perhaps I missed it, but for review purposes can you split the C++ hybrid scan changes, and the pylibcudf exposure, into separate PRs?
| 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) |
There was a problem hiding this comment.
Can we use an existing vocab type. For example a rapidsmpf::Buffer should be fine? I think.
There was a problem hiding this comment.
I think so. We need two things first
- make_buffer exposed on BufferResource
- Implement
__buffer__to get a memoryview that we can hand to pread
Yes, will open them shortly |
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, |
There was a problem hiding this comment.
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.
| 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 | ||
| ) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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: ParquetOptionsWhy?
- 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_splitsfor 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 (
SplitScanvsFusedScan). That should make the follow-up multi-file path much easier: the first version may have exactly onecached_parquet_infoitem per task, but the sameAlignedParquetScandesign can later represent multi-source reads by fillingcached_parquet_infoandrow_groups_per_sourcewith 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] |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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?
94a8da6 to
0b7b5d9
Compare
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
SplitScan to HybridScanReader and prefetch byte ranges using kvikio
Description
Hybrid scan lets us have more control over what happens when executing a
Scan. By default, the streaming engine usescudf::io::read_parquetto compute eachScan. Usingcudf::io::parquet::experimental::HybridScanReaderin cudf_polars essentially lets us break up the I/O and compute that happens during a call tocudf::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,
HybridScanReaderexposes 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
HybridScanReaderforSplitScans in the streaming engine.SplitScanis used by the streaming engine to parallelize reads of a single large file.FusedScanfuses 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=1means dispatchingSplitScans toHybridScanReaderwhenever there's a filter predicate (that we support translating to libcudf).SplitScantasks.FusedScantasks are left for a follow-up.EASY_THREADPOOLbackend to drive prefetching. We're leaving investigation into kvikio'sMULTI_POLLbackend to a follow-up.Checklist