Skip to content

Fetch parquet metadata in rapidsmpf streaming network - #23145

Open
TomAugspurger wants to merge 23 commits into
rapidsai:mainfrom
TomAugspurger:tom/cudf-polars-prefetch-metadata-actors-2
Open

Fetch parquet metadata in rapidsmpf streaming network#23145
TomAugspurger wants to merge 23 commits into
rapidsai:mainfrom
TomAugspurger:tom/cudf-polars-prefetch-metadata-actors-2

Conversation

@TomAugspurger

@TomAugspurger TomAugspurger commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Description

This moves the parquet metadata prefetching that (optionally) happens between IR lowering and streaming network execution into the streaming network. This ensures that any given Scan node can begin executing as soon as its metadata is ready, rather than all the metadata in a query.

At a high level, this PR adds new Actors to the streaming network that are responsible solely for fetching parquet metadata for some paths. We make exactly one new Prefetching actor per Scan actor. The prefetching and

The actors send that metadata to the Actors running the Scan nodes.

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 Jul 7, 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 Python Affects Python cuDF API. cudf-polars Issues specific to cudf-polars labels Jul 7, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jul 7, 2026
@TomAugspurger TomAugspurger added improvement Improvement / enhancement to an existing function non-breaking Non-breaking change labels Jul 7, 2026
Comment thread python/cudf_polars/cudf_polars/dsl/utils/io.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/core.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
@TomAugspurger
TomAugspurger force-pushed the tom/cudf-polars-prefetch-metadata-actors-2 branch from a4215ea to f89d255 Compare July 8, 2026 15:43
This moves the parquet metadata prefetching that (optionally) happens between IR
lowering and streaming network execution into the streaming network. This
ensures that any given Scan node can begin executing as soon as *its* metadata is
ready, rather than *all* the metadata in a query.

At a high level, this PR adds new Actors to the streaming network that are
responsible solely for fetching parquet metadata for some paths. The actors send
that metadata to the Actors running the Scan nodes.

One wrinkle is that we want to preserve the "no duplicate reads of parquet
metadata" feature of cudf-polars' prefetching. If two Scan nodes refer to the
same paths, we want a single actor that reads the metadata and sends it to both
actors doing the Scans. PDS-H Query 7 exhibits this behavior.
@TomAugspurger
TomAugspurger force-pushed the tom/cudf-polars-prefetch-metadata-actors-2 branch from 73f00db to 98b2ed5 Compare July 8, 2026 18:28
@TomAugspurger
TomAugspurger marked this pull request as ready for review July 8, 2026 18:29
@TomAugspurger
TomAugspurger requested a review from a team as a code owner July 8, 2026 18:29
@TomAugspurger
TomAugspurger requested review from Matt711 and rjzamora July 8, 2026 18:29
@coderabbitai

coderabbitai Bot commented Jul 8, 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 moves parquet footer metadata prefetching into streaming scan actor nodes, threads prefetched metadata through scan execution, and updates tests for shared-path, disjoint-path, and handler validation cases.

Changes

Parquet metadata prefetch relocation to streaming actor graph

Layer / File(s) Summary
Path-based prefetch helper and IR cleanup
python/cudf_polars/cudf_polars/dsl/utils/io.py, python/cudf_polars/cudf_polars/engine/core.py
Adds path-ordered parquet metadata prefetch helpers and removes the IR-level cached-metadata attachment branch.
Scan metadata interfaces
python/cudf_polars/cudf_polars/dsl/ir.py, python/cudf_polars/cudf_polars/streaming/io.py
Adds scan methods for prefetched metadata and updates the streaming scan argument shape used during evaluation.
Metadata payload, prefetch actor, and scan wiring
python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Adds metadata payload types, the parquet metadata prefetch actor, sequential receive helpers, scan-node metadata consumption, and IR traversal for metadata-scanned nodes.
Actor graph metadata channels
python/cudf_polars/cudf_polars/streaming/actor_graph/core.py, python/cudf_polars/cudf_polars/streaming/actor_graph/dispatch.py
Adds metadata scan state, per-scan channels, metadata prefetch node creation, and actor-graph state typing for metadata-aware generation.
Streaming metadata tests
python/cudf_polars/tests/streaming/test_scan.py
Adds parquet metadata prefetch integration tests, scan metadata-planning tests, handler failure tests, and method-level coverage for prefetched metadata support.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related issues

Possibly related PRs

  • rapidsai/cudf#22700: Introduced the IR-level parquet metadata prefetch/attach flow that this PR removes and relocates into the streaming actor graph.
  • rapidsai/cudf#22758: Adds the streaming StreamingScan-based lowering path that this PR extends with metadata prefetch wiring.
  • rapidsai/cudf#22889: Touches scan_node and related streaming scan plumbing that this PR further extends with metadata-channel handling.

Suggested reviewers: Matt711, rjzamora, mroeschke

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.22% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main change: moving parquet metadata fetching into the streaming network.
Description check ✅ Passed The description matches the PR’s goal of moving parquet metadata prefetching into the streaming network for per-scan execution.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

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

♻️ Duplicate comments (1)
python/cudf_polars/cudf_polars/streaming/actor_graph/io.py (1)

704-706: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

HIGH: Fail closed when prefetched metadata is missing.

Issue: these .get(...) lookups silently pass None if a prefetch-enabled scan is missing its metadata key. Why: that masks actor-graph wiring bugs and reintroduces duplicate footer reads instead of preserving the no-duplicate-reads contract.

This is the same failure mode as the prior .get metadata-channel review.

Proposed fix
-                    prefetched_parquet_metadata.get(
-                        tuple(cast("FusedScan | SplitScan", scan).paths)
-                    ),
+                    (
+                        prefetched_parquet_metadata[
+                            tuple(cast("FusedScan | SplitScan", scan).paths)
+                        ]
+                        if metadata_channels_by_key
+                        else None
+                    ),
@@
-                    prefetched_parquet_metadata.get(tuple(scan.paths)),
+                    (
+                        prefetched_parquet_metadata[tuple(scan.paths)]
+                        if metadata_channels_by_key
+                        else None
+                    ),

Also applies to: 734-734

🤖 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 `@python/cudf_polars/cudf_polars/streaming/actor_graph/io.py` around lines 704
- 706, The prefetched metadata lookup in the actor-graph I/O path is failing
open by using a .get() that can return None for prefetch-enabled scans, which
can hide wiring bugs and trigger duplicate footer reads. Update the logic in the
relevant scan-handling code in io.py (around the prefetched_parquet_metadata
access, including the other matching .get site) to require the metadata entry to
exist and raise immediately if it is missing, so the no-duplicate-reads contract
is enforced.
🧹 Nitpick comments (2)
python/cudf_polars/tests/streaming/test_scan.py (2)

134-146: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Test doesn't verify the actual dedup/single-actor behavior.

This test only checks result correctness via assert_gpu_result_equal, but the PR's stated goal is that a single metadata-fetching actor serves both Scan nodes sharing the same paths. As written, the test would pass even if metadata were fetched twice per path (i.e., no dedup at all), since correctness doesn't depend on that.

Consider asserting something more direct about the actor graph (e.g., number of parquet_metadata_prefetch_node actors created equals 1 for the shared-path scenario), if such introspection is feasible.

🤖 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 `@python/cudf_polars/tests/streaming/test_scan.py` around lines 134 - 146, The
current test in test_scan_parquet_prefetch_metadata_shared_scan_paths only
validates result equality and does not prove shared metadata prefetching. Update
it to assert the dedup/single-actor behavior directly by inspecting the
streaming engine or actor graph after building the query, and verify that only
one parquet_metadata_prefetch_node is created for the shared-path Scan nodes.
Use the existing test_scan_parquet_prefetch_metadata_shared_scan_paths and
assert_gpu_result_equal setup as the entry point, but add a structural assertion
instead of relying only on output correctness.

720-742: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing success-path coverage for recv_prefetched_parquet_metadata_handler.

Only the two error branches are tested. Since recv_prefetched_parquet_metadata_handler in io.py marks both raise branches with # pragma: no cover; unreachable, but this test now explicitly exercises them, those pragma comments are stale and should be removed in io.py to keep coverage reporting accurate.

Additionally, consider adding a case where group_key matches, asserting the handler returns payload.cached_parquet_info unmodified.

🤖 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 `@python/cudf_polars/tests/streaming/test_scan.py` around lines 720 - 742, The
`recv_prefetched_parquet_metadata_handler` coverage is incomplete because
`test_recv_prefetched_parquet_metadata_handler_errors` only exercises the error
branches. Update the existing test in `test_scan.py` to add a success-path case
where the `Message.group_key` matches the scan input and assert the handler
returns `payload.cached_parquet_info` unchanged. Then remove the now-stale `#
pragma: no cover; unreachable` comments from the corresponding raise branches in
`recv_prefetched_parquet_metadata_handler` in `io.py` so coverage reporting
reflects the exercised code.
🤖 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.

Duplicate comments:
In `@python/cudf_polars/cudf_polars/streaming/actor_graph/io.py`:
- Around line 704-706: The prefetched metadata lookup in the actor-graph I/O
path is failing open by using a .get() that can return None for prefetch-enabled
scans, which can hide wiring bugs and trigger duplicate footer reads. Update the
logic in the relevant scan-handling code in io.py (around the
prefetched_parquet_metadata access, including the other matching .get site) to
require the metadata entry to exist and raise immediately if it is missing, so
the no-duplicate-reads contract is enforced.

---

Nitpick comments:
In `@python/cudf_polars/tests/streaming/test_scan.py`:
- Around line 134-146: The current test in
test_scan_parquet_prefetch_metadata_shared_scan_paths only validates result
equality and does not prove shared metadata prefetching. Update it to assert the
dedup/single-actor behavior directly by inspecting the streaming engine or actor
graph after building the query, and verify that only one
parquet_metadata_prefetch_node is created for the shared-path Scan nodes. Use
the existing test_scan_parquet_prefetch_metadata_shared_scan_paths and
assert_gpu_result_equal setup as the entry point, but add a structural assertion
instead of relying only on output correctness.
- Around line 720-742: The `recv_prefetched_parquet_metadata_handler` coverage
is incomplete because `test_recv_prefetched_parquet_metadata_handler_errors`
only exercises the error branches. Update the existing test in `test_scan.py` to
add a success-path case where the `Message.group_key` matches the scan input and
assert the handler returns `payload.cached_parquet_info` unchanged. Then remove
the now-stale `# pragma: no cover; unreachable` comments from the corresponding
raise branches in `recv_prefetched_parquet_metadata_handler` in `io.py` so
coverage reporting reflects the exercised code.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 185220c1-75b6-4a04-9e96-8857f651158a

📥 Commits

Reviewing files that changed from the base of the PR and between e642b7d and 98b2ed5.

📒 Files selected for processing (6)
  • python/cudf_polars/cudf_polars/dsl/utils/io.py
  • python/cudf_polars/cudf_polars/engine/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/dispatch.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
  • python/cudf_polars/tests/streaming/test_scan.py
💤 Files with no reviewable changes (1)
  • python/cudf_polars/cudf_polars/engine/core.py

ir_context=ir_context,
) as tracer:
for key, ch_metadata in metadata_channels_by_key.items():
msg = await ch_metadata.recv(context)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we wait for all metadata to arrive for the chunks being processed by this actor before we start reading 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.

I believe that's correct.

As you're implying, we can probably structure this so that each chunk's read can begin as soon as its metadata is ready. We just need to ensure that the ordering is consistent. I'll look into that now.

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.

890094a has that.

One subtle thing to call out: the prefetching channels are keyed by scan.paths: we want to read the parquet metadata just once per set of files. But for SplitScan nodes, we might have many individual scans reusing the same parquet file metadata, and these might be running concurrently. So we have a little helper cached_parquet_info_for_scan for receiving the prefetched metadata that handles

  1. doing the actual recv and parsing a Message into a list[CachedParquetInfo]
  2. caching, for reuse by other scan nodes sharing the same path. This cache needs to work for both cases where the lookups from the cache are sequential (easy) or concurrent (which is handled by making an asyncio.Task that wraps the awaitable receiving the parquet metadata, so that multiple producers can await the same ultimate data.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think I understand why we need to have so many distinct actors and channels. I would think that we only need a single parquet_metadata_actor for each scan_actor. The parquet_metadata_actor would construct an ordered list of unique list[tuple[str, ...]] groups needed by the down-stream scan_actor, and initiate the metadata reads for all these groups at once. As these metadata groups become available, we just push them into a channel that is connected to the scan_node as metadata (so we have an unbound queue of metadata messages that the scan_node can pull from).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I suppose we can pass the metadata as "data", but I don't think we need the single-item-capacity back pressure.

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.

f8001e8 has another pass at this. This implements the "1 Prefetch actor per Scan actor" thing we talked about earlier.

I have, at least temporarily, given up on the requirement that we not reread metadata when two nodes request the same metadata. I think it was creating too many complexities downstream. I'll see whether than can be recovered, but it might not be worth the hassle.

Relax the requirement to read exactly once, simplify Prefetch->Scan
actor relationship (1:1).

@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: 2

🤖 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/cudf_polars/cudf_polars/streaming/actor_graph/io.py`:
- Around line 686-703: The metadata receiver task started by
_start_metadata_receiver in scan_node is never cleaned up, so wrap the
shutdown_on_error block in a try/finally and explicitly cancel and await
_metadata_receiver_task on exit. Use the existing _metadata_receiver_task symbol
alongside shutdown_channels and cached_parquet_info_for_task so the task is
always finalized even on early return or failure.
- Around line 616-623: The _receive helper in the metadata prefetch flow should
not leave pending entries in metadata_futures unresolved if it exits early;
update the logic in _receive and the create_task path so any exception or
premature stop is propagated to all unfinished futures. Use the existing symbols
recv_prefetched_parquet_metadata, recv_prefetched_parquet_metadata_handler, and
futures to locate the fix, and ensure remaining futures are completed with the
same failure so downstream awaiters fail fast instead of hanging.
🪄 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: b70920bf-2a71-4034-94ab-653419f5c5d7

📥 Commits

Reviewing files that changed from the base of the PR and between a3a39ee and 4f9bdba.

📒 Files selected for processing (4)
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/dispatch.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
  • python/cudf_polars/tests/streaming/test_scan.py

Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
return tuple(metadata_scans)


def _build_metadata_channels_by_scan(

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.

This can probably be inlined now.

await ch_out.drain(context)


def _start_metadata_receiver(

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.

This is kind of annoying / complicated, but I think we need some mechanism to ensure that the right parquet metadata gets to the right SplitScan | FusedScan task when we have multiple producers running concurrently.

I think we do know for sure that metadata prefetcher Actor iterates over .scans and inserts a Message per scan in the original order from the StreamingScan. But I think when we're running multiple scan reader tasks concurrently, they might get to the await reading a message with parquet metadata out of order.

Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
@TomAugspurger TomAugspurger changed the title [WIP]: Fetch parquet metadata in rapidsmpf streaming network Fetch parquet metadata in rapidsmpf streaming network Jul 9, 2026

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

♻️ Duplicate comments (2)
python/cudf_polars/cudf_polars/streaming/actor_graph/io.py (2)

708-740: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Verify _metadata_receiver_task lifecycle cleanup.

A prior review pass on this same feature flagged that the receiver task started here was never cancelled/awaited on early exit or failure of scan_node, causing a leaked task whose exception is never retrieved. The AI summary confirms scan_node now "includes the metadata channel in shutdown handling," but it's unclear whether the task itself (as opposed to just the channel) is cancelled in a finally block. As per coding guidelines, "ensure asyncio.Tasks are explicitly canceled in a finally block upon failure."

#!/bin/bash
sed -n '690,800p' python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
rg -n '_metadata_receiver_task|cancel\(\)' python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
🤖 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 `@python/cudf_polars/cudf_polars/streaming/actor_graph/io.py` around lines 708
- 740, The metadata receiver task started by _start_metadata_receiver in
scan_node is still not explicitly cleaned up if scan_node exits early or fails.
Add a finally block around the shutdown_on_error scope that cancels
_metadata_receiver_task when it exists and awaits it to completion, handling
cancellation cleanly so no exception is left unretrieved. Keep the fix local to
the scan_node path that sets up metadata_futures and _metadata_receiver_task.

Source: Path instructions


647-651: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Receiver failures leave downstream futures unresolved forever.

If recv_prefetched_parquet_metadata or recv_prefetched_parquet_metadata_handler raises partway through the loop, futures[task_idx] for all remaining, not-yet-processed indices are never resolved. Any concurrent producer awaiting one of those futures (via cached_parquet_info_for_task) will hang indefinitely instead of failing fast — this is the same failure mode flagged in a prior review pass on this exact helper (_receive).

Wrap the loop and propagate the exception to all unfinished futures on failure.

🩹 Suggested fix
     async def _receive() -> None:
-        for task_idx, scan in enumerate(scans):
-            msg = await recv_prefetched_parquet_metadata(context, ch_metadata)
-            cached = recv_prefetched_parquet_metadata_handler(msg, tuple(scan.paths))
-            futures[task_idx].set_result(cached)
+        try:
+            for task_idx, scan in enumerate(scans):
+                msg = await recv_prefetched_parquet_metadata(context, ch_metadata)
+                cached = recv_prefetched_parquet_metadata_handler(
+                    msg, tuple(scan.paths)
+                )
+                futures[task_idx].set_result(cached)
+        except Exception as exc:
+            for fut in futures:
+                if not fut.done():
+                    fut.set_exception(exc)
+            raise

Please confirm whether this has already been addressed in the surrounding (unshown) code between lines 652-724, since the visible _receive body doesn't include any exception handling.

#!/bin/bash
ast-grep outline python/cudf_polars/cudf_polars/streaming/actor_graph/io.py --items all --match '_receive|_metadata_receiver_task|scan_node' --view expanded
sed -n '600,800p' python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
🤖 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 `@python/cudf_polars/cudf_polars/streaming/actor_graph/io.py` around lines 647
- 651, The _receive helper in io.py can leave unresolved futures if
recv_prefetched_parquet_metadata or recv_prefetched_parquet_metadata_handler
throws before the loop finishes. Update _receive to catch exceptions, then fail
every unfinished entry in futures (including the current and remaining task_idx
values) so cached_parquet_info_for_task does not hang; use the existing
_receive, futures, and recv_prefetched_parquet_metadata /
recv_prefetched_parquet_metadata_handler symbols to place the fix in the correct
async receiver path.
🤖 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.

Duplicate comments:
In `@python/cudf_polars/cudf_polars/streaming/actor_graph/io.py`:
- Around line 708-740: The metadata receiver task started by
_start_metadata_receiver in scan_node is still not explicitly cleaned up if
scan_node exits early or fails. Add a finally block around the shutdown_on_error
scope that cancels _metadata_receiver_task when it exists and awaits it to
completion, handling cancellation cleanly so no exception is left unretrieved.
Keep the fix local to the scan_node path that sets up metadata_futures and
_metadata_receiver_task.
- Around line 647-651: The _receive helper in io.py can leave unresolved futures
if recv_prefetched_parquet_metadata or recv_prefetched_parquet_metadata_handler
throws before the loop finishes. Update _receive to catch exceptions, then fail
every unfinished entry in futures (including the current and remaining task_idx
values) so cached_parquet_info_for_task does not hang; use the existing
_receive, futures, and recv_prefetched_parquet_metadata /
recv_prefetched_parquet_metadata_handler symbols to place the fix in the correct
async receiver path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 65bfb0e5-e4f1-46da-8117-0ef84ba953a0

📥 Commits

Reviewing files that changed from the base of the PR and between 4f9bdba and 25916ea.

📒 Files selected for processing (7)
  • python/cudf_polars/cudf_polars/dsl/ir.py
  • python/cudf_polars/cudf_polars/dsl/utils/io.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/dispatch.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
  • python/cudf_polars/cudf_polars/streaming/io.py
  • python/cudf_polars/tests/streaming/test_scan.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • python/cudf_polars/cudf_polars/streaming/actor_graph/dispatch.py
  • python/cudf_polars/tests/streaming/test_scan.py
  • python/cudf_polars/cudf_polars/dsl/utils/io.py

@TomAugspurger

Copy link
Copy Markdown
Contributor Author

CI hit a timeout in test_scan_parquet_prefetch_metadata_shared_scan_paths[spmd-small]. I want to understand the performance here a bit more.

This lets the sender get arbitrarily far ahead of the receiever, which
we want for parquet metadata footers (probably).
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

There were two things:

  1. We were using Channel.send / Channel.recv to send metadata between the prefetching actor and the scan actor; this hit some internal limit in Channel, so that we weren't prefetching as far ahead as I expected. I switched to using send_metadata / recv_metadata.
  2. (more importantly for the slow test): we just create a ton of partitions and so a ton of messages. Similar tests slice the dataframe to df = df.head(40) for the spmd-small case. I've added that.

@rjzamora

Copy link
Copy Markdown
Member

Sorry for pushing you in circles a bit here. I think I’m finally able to describe my main concern more concretely, but feel free to push back if you just want to get something in asap and move on to data pre-fetching...

This is looking better, but I’m still worried that the metadata protocol is aligned to each SplitScan / FusedScan task.

A group_key is the exact tuple(scan.paths) needed by one SplitScan / FusedScan task, but parquet footer metadata is naturally keyed by the individual paths in tuple(base_scan.paths). It feels like the metadata actor should be able to provide per-file metadata at whatever batching granularity it wants for optimal performance.

Would this be relatively clean if the scan actor owned a small MetadataProvider?

For example:

  • the metadata actor emits arbitrary batches of path -> CachedParquetInfo
  • one receiver task in the scan actor consumes those batches and stores metadata by path
  • each producer awaits provider.get(scan.paths) before calling read_chunk
  • provider.get(scan.paths) returns a list[CachedParquetInfo] ordered to match the current task’s scan.paths

Then read_chunk does not need to care how the metadata was originally batched upstream.



class ParquetMetadataCache:
"""

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.

Each actor gets a reference to this object.

The .get method is where all the complexity is, but the goal is to ensure that things are only prefetched once. The first time we see .paths, we

  1. Create a Future whose result will be the list[CachedParquetInfo] for .paths. Its tracked in the local cache.
  2. Create and await a coroutine (offloaded to a worker thread) that fetches the metadata for .paths.
  3. Trampoline the result of that coro to the Future for .paths

If a second actor comes along and asks for .paths while the coroutine is still fetching the metadata, it'll be pointed to the same Future. Upon future.set_result() that coro will wake up and be given the same parquet metadata.

If the result is already done, it's just handed the cached parquet metadata.

Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/io.py Outdated
@TomAugspurger

Copy link
Copy Markdown
Contributor Author

A group_key is the exact tuple(scan.paths) needed by one SplitScan / FusedScan task, but parquet footer metadata is naturally keyed by the individual paths in tuple(base_scan.paths). It feels like the metadata actor should be able to provide per-file metadata at whatever batching granularity it wants for optimal performance.

I'm a bit concerned about the overhead of doing anything at the individual path level; I'd rather give libcudf a list[str] and let it sort out how to do things quickly. But, as you say, we can do things at any batching granularity we want. And IIUC, the batching of the metadata actor needn't match the batching of the Scan actor. We'd just need to slice/concat the list[CachedParquetInfo], which is easy.

That said, I'm not sure why we'd want any batching other than what's on the (SplitScan|FusedScan).paths. IIUC, that's what we ultimately pass to plc.io.parquet.read_parquet, so aligning the metadata reads with that feels the most natural to me.

I do want to get some additional metrics/traces for when these Actors complete there work (and maybe even log things like individual messages, depending on the config). But my guess now is that the metadata actor gets quite far ahead of the Scan actor.

@rjzamora

Copy link
Copy Markdown
Member

I'm a bit concerned about the overhead of doing anything at the individual path level; I'd rather give libcudf a list[str] and let it sort out how to do things quickly. But, as you say, we can do things at any batching granularity we want. And IIUC, the batching of the metadata actor needn't match the batching of the Scan actor. We'd just need to slice/concat the list[CachedParquetInfo], which is easy.

Yes, this aligns with my thinking as well.

That said, I'm not sure why we'd want any batching other than what's on the (SplitScan|FusedScan).paths. IIUC, that's what we ultimately pass to plc.io.parquet.read_parquet, so aligning the metadata reads with that feels the most natural to me.

If we are reading 10 large files, we will have >10 SplitScan tasks, but exactly 10 unique group_keys. We probably want libcudf to simply read all of these footers in one go, even though group_key aligned tasks would result in 10 separate reads.

Even if each group_key happens to contain an optimal number of paths, the decision to align metadata pre-fetching with group_key makes it tricky to use the same pre-fetched metadata for two distinct scan_nodes that are reading from the same table. If the scan_actor is agnostic to the size of each metadata batch, the same metadata actor can more-easily feed metadata to multiple scan_actors.

@TomAugspurger

Copy link
Copy Markdown
Contributor Author

This is currently slower than our metadata prefetching on main. Looking at the nsys profiles, I see patterns of Scan nodes and prefetching node pairs taking approximately the same amount of time.

image

There's no reason for that read_parquet_footers to take 3 seconds, which makes me think there's a problem with this lock mechanism. I'll do some more investigation.

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 improvement Improvement / enhancement to an existing function non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants