Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1615e0d
introduce ParquetScanTask
rjzamora Sep 2, 2026
d3699ef
variation 2
rjzamora Sep 2, 2026
e3c2d08
cleanup
rjzamora Sep 2, 2026
0ebcc5c
minor cleanup
rjzamora Sep 2, 2026
b16adaf
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 2, 2026
18eb8af
update docstring
rjzamora Sep 2, 2026
ebd78c9
Merge branch 'main' into parquet-scan-task
rjzamora Sep 2, 2026
4ce5e77
address comment
rjzamora Sep 2, 2026
78a213f
Merge branch 'parquet-scan-task' of github.com:rjzamora/cudf into par…
rjzamora Sep 2, 2026
84b312b
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 2, 2026
ae2c728
rename class
rjzamora Sep 3, 2026
92dfbb0
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 3, 2026
dcd5a08
experiment with new design
rjzamora Sep 3, 2026
57e2d42
avoid historical constraint
rjzamora Sep 3, 2026
780d8f5
simplify again
rjzamora Sep 3, 2026
b327ec7
minor cleanup
rjzamora Sep 3, 2026
6426ead
more cleanup
rjzamora Sep 3, 2026
c380db9
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 3, 2026
018aa0b
addresss CI
rjzamora Sep 3, 2026
b3c11e2
remove silly fix
rjzamora Sep 3, 2026
b716987
roll back
rjzamora Sep 3, 2026
6e1d5fc
try again
rjzamora Sep 3, 2026
5e1278b
more scan -> task cleanup
rjzamora Sep 3, 2026
5358c6f
revise SplitScanBounds convention a bit
rjzamora Sep 3, 2026
30eda0d
heavier cleanup
rjzamora Sep 3, 2026
e165d5e
make _split_scan_bounds a method
rjzamora Sep 3, 2026
7f6d5e3
docstring cleanup
rjzamora Sep 3, 2026
00cc0b9
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 3, 2026
518ed51
reuse the metadata we prefatch at do_evaluate time
rjzamora Sep 3, 2026
6e1d01c
push staged revisions - dropping nesting
rjzamora Sep 8, 2026
3aec20e
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 8, 2026
95d27b3
require split_index/num_splits in all cases to be consistent
rjzamora Sep 8, 2026
9cf1ea1
move to simple ParquetScanTask(ScanTask)
rjzamora Sep 8, 2026
c7161b8
fix CI
rjzamora Sep 8, 2026
89d302b
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 8, 2026
5c4c971
Merge branch 'main' into parquet-scan-task
rjzamora Sep 8, 2026
20f28a1
fix code-cov
rjzamora Sep 8, 2026
76b652b
Merge remote-tracking branch 'upstream/main' into parquet-scan-task
rjzamora Sep 8, 2026
848337f
address comments
rjzamora Sep 8, 2026
867e0ad
Merge remote-tracking branch 'upstream/release/26.10' into parquet-sc…
rjzamora Sep 8, 2026
aa6fdbe
small revision
rjzamora Sep 8, 2026
d6d264a
address more comments
rjzamora Sep 8, 2026
f139694
remove parquet_options from ScanTask
rjzamora Sep 8, 2026
f09f6ee
Merge remote-tracking branch 'upstream/release/26.10' into parquet-sc…
rjzamora Sep 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 20 additions & 13 deletions python/cudf_polars/cudf_polars/dsl/utils/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@

from cudf_polars.dsl.tracing import nvtx_annotate_cudf_polars
from cudf_polars.dsl.traversal import traversal
from cudf_polars.streaming.io import Scan, StreamingScan
from cudf_polars.streaming.io import (
ParquetSourceInfo,
Scan,
StreamingScan,
)

if TYPE_CHECKING:
from cudf_polars.dsl.ir import IR
Expand Down Expand Up @@ -181,14 +185,12 @@ def prefetch_parquet_file_metadata_for_ir(
-------
A dictionary mapping each individual path to its cached parquet metadata.
"""
from cudf_polars.streaming.io import ParquetSourceInfo, StreamingScan

all_paths: set[str] = set()

for node in traversal([root]):
if isinstance(node, StreamingScan) and node.base_scan.typ == "parquet":
for scan in node.scans:
for path in scan.paths:
for task in node.tasks:
for path in task.paths:
Comment on lines -190 to +193

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.

lots of this diff is just renaming "scan(s)" to "task(s)" to clearly distinguish between the full-table Scan node and the individual Scan "tasks" used to generate each chunk.

all_paths.add(path)
elif isinstance(node, Scan) and node.typ == "parquet": # pragma: no cover
raise RuntimeError("Unexpected parquet 'Scan' node in lowered IR graph.")
Expand Down Expand Up @@ -241,7 +243,7 @@ def attach_cached_parquet_metadata(
cached_parquet_info_map: dict[str, CachedParquetInfo],
) -> None:
"""
Attach prefetched metadata to scan nodes.
Attach prefetched metadata to parquet scan tasks.

This is an optimization only and does not affect IR identity.

Expand All @@ -254,10 +256,15 @@ def attach_cached_parquet_metadata(
"""
for node in traversal([root]):
if isinstance(node, StreamingScan) and node.base_scan.typ == "parquet":
for scan in node.scans:
if not all(path in cached_parquet_info_map for path in scan.paths):
continue
cached = [cached_parquet_info_map[path] for path in scan.paths]
Scan._validate_cached_parquet_info(scan.paths, cached)
scan.cached_parquet_info = cached
scan._non_child_args = (*scan._non_child_args[:-1], cached)
base_scan = node.base_scan
task_paths = {path for task in node.tasks for path in task.paths}
cached_paths = [
path
for path in base_scan.paths
if path in task_paths and path in cached_parquet_info_map
]
cached = [cached_parquet_info_map[path] for path in cached_paths]
if not cached:
continue
Scan._validate_cached_parquet_info(cached_paths, cached)
base_scan.cached_parquet_info = cached

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.

We keep the cache on the base_scan instead of creating a separate cache on each task. In the future, this may also allow us to avoid re-reading the same footer metadata on the same rank when it isn't already cached. However, I didn't add that optimization yet, because we would probably need some kind of locking mechanism on the cache (because we have concurrent producers on the rank).

48 changes: 25 additions & 23 deletions python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
send_metadata,
)
from cudf_polars.streaming.io import (
ScanTask,
StreamingScan,
StreamingSink,
_prepare_sink_directory,
Expand All @@ -60,7 +61,6 @@
IOPartitionPlan,
PartitionInfo,
)
from cudf_polars.streaming.io import FusedScan, SplitScan
from cudf_polars.utils.config import MaxConcurrentIOTasks


Expand Down Expand Up @@ -516,7 +516,7 @@ def _(

async def read_chunk(
context: Context,
scan: IR,
task: IR,
seq_num: int,
ch_out: Channel[TableChunk],
ir_context: IRExecutionContext,
Expand All @@ -530,8 +530,8 @@ async def read_chunk(
----------
context
The rapidsmpf context.
scan
The Scan or DataFrameScan node.
task
The scan task to evaluate.
seq_num
The sequence number.
ch_out
Expand All @@ -546,7 +546,7 @@ async def read_chunk(
"""
reservation_bytes = (
estimated_chunk_bytes
if isinstance(scan, DataFrameScan)
if isinstance(task, DataFrameScan)
else 2 * estimated_chunk_bytes
)
start = time.monotonic_ns()
Expand All @@ -558,8 +558,8 @@ async def read_chunk(
admitted = time.monotonic_ns()
with opaque_memory_usage(reservation):
df = await ir_context.to_thread(
scan.do_evaluate,
*scan._non_child_args,
task.do_evaluate,
*task._non_child_args,
context=ir_context,
)
chunk = TableChunk.from_pylibcudf_table(
Expand All @@ -569,14 +569,17 @@ async def read_chunk(
br=context.br(),
)
stop = time.monotonic_ns()
ir_type = (
task.trace_ir_type() if isinstance(task, ScanTask) else type(task).__name__
)
log(
"IO Task",
scope=Scope.IO_TASK.value,
start=start,
admitted=admitted,
stop=stop,
ir_id=scan.get_stable_id(),
ir_type=type(scan).__name__,
ir_id=task.get_stable_id(),
ir_type=ir_type,
sequence_number=seq_num,
estimated_output_bytes=estimated_chunk_bytes,
reservation_bytes=reservation_bytes,
Expand Down Expand Up @@ -613,7 +616,7 @@ async def scan_node(
Estimated retained output size of each chunk in bytes. Used to estimate
peak memory for admission before launching each read.
"""
scans: Sequence[SplitScan] | Sequence[FusedScan] = ir.scans
tasks: Sequence[ScanTask] = ir.tasks

async with shutdown_on_error(
context, ch_out, trace_ir=ir, ir_context=ir_context
Expand All @@ -622,21 +625,21 @@ async def scan_node(
await send_metadata(
ch_out,
context,
ChannelMetadata(local_count=len(scans)),
ChannelMetadata(local_count=len(tasks)),
)

# If there is nothing to scan, drain the channel and return
if len(scans) == 0:
if len(tasks) == 0:
await ch_out.drain(context)
return

# If there is only one scan or one producer, we can
# If there is only one task or one producer, we can
# skip the lineariser and read the chunks directly
if len(scans) == 1 or num_producers == 1:
for seq_num, scan in enumerate(scans):
if len(tasks) == 1 or num_producers == 1:
for seq_num, task in enumerate(tasks):
await read_chunk(
context,
scan,
task,
seq_num,
ch_out,
ir_context,
Expand All @@ -647,23 +650,22 @@ async def scan_node(
return

# Use Lineariser to ensure ordered delivery
num_producers = min(num_producers, len(scans))
num_producers = min(num_producers, len(tasks))
lineariser = Lineariser(context, ch_out, num_producers)

# Assign tasks to producers using round-robin
producer_tasks: list[list[tuple[int, SplitScan | FusedScan]]] = [
producer_tasks: list[list[tuple[int, ScanTask]]] = [
[] for _ in range(num_producers)
]
for task_idx, scan in enumerate(scans):
for task_idx, task in enumerate(tasks):
producer_id = task_idx % num_producers
# mypy resolves __iter__ on union-of-sequences to the common base (IR)
producer_tasks[producer_id].append((task_idx, scan)) # type: ignore[arg-type]
producer_tasks[producer_id].append((task_idx, task))

async def _producer(producer_id: int, ch_out: Channel) -> None:
for task_idx, scan in producer_tasks[producer_id]:
for task_idx, task in producer_tasks[producer_id]:
await read_chunk(
context,
scan,
task,
task_idx,
ch_out,
ir_context,
Expand Down
2 changes: 1 addition & 1 deletion python/cudf_polars/cudf_polars/streaming/explain.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ def _(ir: Scan) -> dict[str, Serializable]:
def _(ir: StreamingScan) -> dict[str, Serializable]:
return {
"typ": ir.base_scan.typ,
"scan_count": len(ir.scans),
"task_count": len(ir.tasks),
"prefix": os.path.commonprefix(ir.base_scan.paths),
"predicate": (
_serialize_expr(ir.base_scan.predicate) if ir.base_scan.predicate else None
Expand Down
Loading
Loading