Skip to content

Rework join filter pushdown rewrites to leave hints and apply adaptively - #23584

Open
wence- wants to merge 17 commits into
NVIDIA:mainfrom
wence-:wence/fea/polars-filter-hint
Open

Rework join filter pushdown rewrites to leave hints and apply adaptively#23584
wence- wants to merge 17 commits into
NVIDIA:mainfrom
wence-:wence/fea/polars-filter-hint

Conversation

@wence-

@wence- wence- commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Description

In #22996 and #22997 we added join filter pushdown optimisations for cudf-polars plans. These are represented as semi joins using the existing IR structure. As such, we are on the hook to execute them, even if that would not be beneficial for runtime execution. Examples are cases where the join that is being filtered will be performed via broadcast, or is already compatibly shuffled. In such cases carrying out the semi join merely adds extra work.

To fix this, introduce a special PushdownFilterHint node that is, optionally, applied at runtime. To decide whether, and how, to apply these hints we now sample join inputs and estimate cardinality in addition to size. We use the cardinality estimate to decide whether or not a bloom filter would be effective: calculating the estimated false positive rate.

During lowering, we build a join planning state that is updated at runtime with cardinality estimates and other relevant information such as partitioning. When we then come to execute a filter hint we can inspect this state and take an appropriate action.

Filter hints are classified into two types:

  1. Those that apply directly to a join input Join(Hint(target, domain), domain)
  2. Those that apply indirectly: Join1(Join2(Hint(target, j1_domain), j2_domain), j1_domain)

The former check at runtime whether or not the domain is already compatibly distributed (or will be broadcast) and then elide the hint. The latter cannot do so without inducing a cycle in the execution DAG, which is undesirable. We therefore always apply these "indirect" hints, but still only if the hint does not induce a shuffle. Better cost models for placement might elide some of these, or give us enough information to decide whether to reject them based on sampling.

Checklist

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

wence- added 12 commits August 7, 2026 12:16
The joins introduced by the filter pushdown optimisation pass on plans are
not required for correctness of the query. Instead they are only hints that
might be useful for providing a faster implementation.

Currently we cannot take advantage of this optional component of the
filter because there is no way to distinguish between a semi join that is
part of the user query, and one that the optimisation pass introduces.

To fix this, introduce a PushdownFilterHint node. Initially, this is
removed during lowering to actors.
@wence-
wence- requested a review from a team as a code owner August 7, 2026 16:11
@wence-
wence- requested a review from vyasr August 7, 2026 16:11
@github-actions github-actions Bot added Python Affects Python cuDF API. cudf-polars Issues specific to cudf-polars labels Aug 7, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 8db20e2f-4f3c-4a4e-830f-d9d4fc10c229

📥 Commits

Reviewing files that changed from the base of the PR and between 5065c2b and 09dfe8f.

📒 Files selected for processing (6)
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join_planning.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/utils.py
🚧 Files skipped from review as they are similar to previous changes (4)
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join.py

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Improved streaming join filter pushdown for dynamic joins.
    • Added automatic selection between Bloom filters, broadcast filtering, and skipping based on data size and memory limits.
    • Added configurable Bloom filter memory limits, defaulting to 32 MiB.
    • Enhanced explain plans and tracing with prefilter strategies, decisions, and row-count details.
    • Added support for standalone filter hints in streaming plans.
  • Bug Fixes

    • Improved handling of filtered joins, external domains, nullable keys, shared inputs, and unsupported join shapes.
    • Ensured buffered streaming data is cleaned up during completion, cancellation, and errors.

Walkthrough

This PR adds optional runtime filtering for streaming joins. It introduces filter-hint IR nodes, runtime sampling and method selection, Bloom and broadcast semi-join execution, configurable Bloom-filter limits, explain serialization, tracing, and expanded tests.

Changes

Join prefilter planning and execution

Layer / File(s) Summary
Logical hint contracts and lowering
python/cudf_polars/cudf_polars/streaming/filter_hint.py, python/cudf_polars/cudf_polars/streaming/join.py, python/cudf_polars/cudf_polars/streaming/join_filter_pushdown.py, python/cudf_polars/cudf_polars/dsl/...
Join filter pushdown creates PushdownFilterHint nodes. Eligible dynamic joins lower these hints into JoinWithPrefilter nodes.
Runtime planning and prefilter execution
python/cudf_polars/cudf_polars/streaming/actor_graph/...
Runtime actors sample inputs, estimate cardinality and memory, select Bloom filtering, broadcast semi-joins, or skipping, and wire filtered channels into joins.
Standalone hint actor integration
python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py, python/cudf_polars/cudf_polars/streaming/actor_graph/core.py, python/cudf_polars/cudf_polars/streaming/actor_graph/collectives/...
Standalone hints gather runtime metadata, replay samples, execute selected filtering, and register their actor subnetworks.
Explain output, configuration, and validation
python/cudf_polars/cudf_polars/streaming/explain.py, python/cudf_polars/cudf_polars/utils/config.py, python/cudf_polars/tests/streaming/..., python/cudf_polars/tests/test_config.py
Explain output includes hint and prefilter properties. Bloom-filter sizing is configurable and validated. Tests cover lowering, runtime decisions, tracing, explain output, and configuration.

Estimated code review effort: 5 (Critical) | ~90 minutes

Possibly related PRs

  • rapidsai/cudf#22995: Earlier join-prefilter planning extended by this runtime implementation.
  • rapidsai/cudf#22996: Related streaming join-domain prefilter implementation replaced by hint-based lowering.
  • rapidsai/cudf#22997: Related join filter pushdown candidate selection and profitability logic.

Suggested labels: feature request

Suggested reviewers: vyasr, tomaugspurger, matt711

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Most changes support issue #23576, but the unrelated SPDX punctuation and ClassVar annotation changes are outside its stated scope. Remove or separately justify the SPDX header punctuation and unrelated ClassVar annotation changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 58.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the reworked join filter pushdown hints and adaptive runtime behavior.
Description check ✅ Passed The description directly explains the runtime hint design, adaptive planning, execution choices, and test coverage.
Linked Issues check ✅ Passed The changes implement issue #23576 by adding runtime hints, sampling, cardinality estimation, and adaptive prefilter decisions.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

Actionable comments posted: 3

🧹 Nitpick comments (9)
python/cudf_polars/tests/streaming/test_explain.py (1)

140-185: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the remaining prefilter serialization branches.

The test covers only a JoinInputDomain prefilter with placement == "join_input". Two new branches stay untested:

  • The ExternalDomain branch of _serialize_prefilter in python/cudf_polars/cudf_polars/streaming/explain.py (line 628), which emits {"type": "ExternalDomain"} with no side key.
  • The placement == "pushed_down" value rendered by the PushdownFilterHint representation and serializer.

A composite-candidate query, such as the one in test_composite_filter_pushdown_constrains_domain_first, produces both. Add a case that asserts those outputs.

As per coding guidelines: "Ensure test files provide comprehensive edge case coverage (empty, all-null, single-element, mixed types) and do not depend on external datasets".

🤖 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_explain.py` around lines 140 - 185,
Extend test_explain_pushdown_filter_hint_in_dynamic_physical_plan or add a
focused composite-candidate test using the pattern from
test_composite_filter_pushdown_constrains_domain_first. Assert the logical
PushdownFilterHint serialization uses placement "pushed_down", and the physical
prefilter serialization includes an ExternalDomain object with type
"ExternalDomain" and no side key, while retaining the existing JoinInputDomain
assertions.

Source: Coding guidelines

python/cudf_polars/cudf_polars/streaming/actor_graph/core.py (1)

182-187: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extend the explanatory comment to cover PushdownFilterHint.

The comment lists Union, Join, and Over but not PushdownFilterHint. Add the reason for the new type so the branch stays self-explanatory.

♻️ Proposed comment update
         elif isinstance(node, (Union, Join, Over, PushdownFilterHint)):
             # Union processes children sequentially; Join may broadcast one
             # side; Over buffers (or samples-then-replays) its input before
-            # producing output. In every case the input source needs
-            # unbounded fanout so other consumers don't block it.
+            # producing output; PushdownFilterHint consumes its domain in
+            # full before it emits filtered target chunks. In every case the
+            # input source needs unbounded fanout so other consumers don't
+            # block it.
             _mark_children_unbounded(node)
🤖 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/core.py` around lines
182 - 187, Extend the comment above the isinstance branch in the actor-graph
logic to explicitly explain why PushdownFilterHint also requires unbounded
fanout, while preserving the existing rationale for Union, Join, and Over.
python/cudf_polars/cudf_polars/streaming/filter_hint.py (1)

84-94: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the positional alignment contract for external domains.

The constructor validates only the count of external domains. Consumers such as make_join_planning_state in python/cudf_polars/cudf_polars/streaming/actor_graph/join_planning.py pair children[2:] with the external prefilters in iteration order. Record this order requirement in the class docstring so future callers keep the pairing correct.

🤖 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/filter_hint.py` around lines 84 -
94, Update the JoinWithPrefilter class docstring to document that external
domains in children[2:] must correspond positionally, in iteration order, to the
external prefilters. Keep the existing validation unchanged and explicitly
describe this ordering contract for callers such as make_join_planning_state.
python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py (1)

400-466: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Confirm the interaction between bloom_filter_max_size and target.total_size.

Line 436 compares bloom_bytes against min(bloom_filter_max_size, target.total_size). A small target therefore disables the Bloom filter even when the configured limit is large. The same applies to the exact path at line 446. The behavior looks intentional, because a filter should not cost more than the data it filters. Add a short comment that states this rule, so the two limits are not confused later.

🤖 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/prefilter.py` around
lines 400 - 466, The size checks in choose_prefilter_method should document that
a prefilter must not consume more memory than the target data it filters. Add a
concise comment near the bloom and exact size comparisons explaining that
min(bloom_filter_max_size or broadcast_limit, target.total_size) enforces both
the configured limit and the target-size cap.
python/cudf_polars/tests/streaming/test_tracing.py (1)

209-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the subprocess trace harness into a helper.

The three new tests repeat the same steps: copy the environment, set CUDF_POLARS_LOG_TRACES, run subprocess.check_output, and parse the PREFILTER_TRACE= line. Move these steps into one module-level helper that accepts the generated code and returns the decoded record. This removes about forty duplicated lines and keeps the parsing rule in one place.

🤖 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_tracing.py` around lines 209 - 222,
Extract the repeated subprocess trace setup and PREFILTER_TRACE parsing from the
tracing tests into a module-level helper that accepts the generated code and
returns the decoded JSON record. Update all three tests to call this helper,
preserving the copied environment, CUDF_POLARS_LOG_TRACES setting, subprocess
options, and single-trace-line parsing behavior.
python/cudf_polars/cudf_polars/streaming/join.py (1)

344-353: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Assign preserve_prefilters directly from the condition.

The if/else block only assigns True or False. A direct assignment is shorter and keeps the condition in one place.

♻️ Proposed simplification
-    if (
+    preserve_prefilters = (
         dynamic_planning
         and ir.options[0] != "Cross"
         and ir.options[5] == "none"
         and not has_non_pointwise_keys
         and any(is_direct_join_prefilter(child) for child in ir.children)
-    ):
-        preserve_prefilters = True
-    else:
-        preserve_prefilters = False
+    )
🤖 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/join.py` around lines 344 - 353, In
the join planning logic, replace the if/else assignment to preserve_prefilters
with a direct boolean assignment using the existing dynamic_planning, join-type,
options, has_non_pointwise_keys, and is_direct_join_prefilter conditions.
Preserve the condition and resulting True/False behavior unchanged.
python/cudf_polars/tests/test_config.py (1)

715-732: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add edge cases for bloom_filter_max_size.

The validation code rejects bool explicitly at line 433 of python/cudf_polars/cudf_polars/utils/config.py, and the documentation defines 0 as the disabling value. Neither case is covered here. Add one case that passes True and expects TypeError, and one case that passes 0 and expects success.

Based on path instructions: "Ensure test files provide comprehensive edge case coverage (empty, all-null, single-element, mixed types)".

🤖 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/test_config.py` around lines 715 - 732, Extend the
bloom_filter_max_size tests in ConfigOptions.from_polars_engine coverage with a
True input that expects TypeError and a 0 input that succeeds, preserving the
existing invalid-string and negative-value cases.

Source: Path instructions

python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py (1)

168-181: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Define a shared Protocol for add_prefilter.spec
add_prefilter reads only target_on, domain_on, and nulls_equal, which both Prefilter and PushdownFilterHint provide. Use a Protocol for these fields to make the structural contract explicit.

🤖 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/prefilter_actor.py`
around lines 168 - 181, Define a shared structural Protocol for the
add_prefilter spec argument containing target_on, domain_on, and nulls_equal,
and annotate add_prefilter to accept that Protocol. Ensure both Prefilter and
PushdownFilterHint satisfy the contract without duplicating or narrowing their
existing field definitions.
python/cudf_polars/cudf_polars/streaming/parallel.py (1)

18-18: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the unused filter_hint import. filter_hint.py registers no handlers, and join.py imports its classes before registering PushdownFilterHint.

🤖 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/parallel.py` at line 18, Remove the
unused cudf_polars.streaming.filter_hint import from the module imports in
parallel.py; leave the existing join.py registration and class imports
unchanged.
🤖 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/prefilter_actor.py`:
- Around line 138-141: Update the domain validation in the prefilter actor
around names_to_indices so malformed optional filters do not abort query
execution: use an assert only if lowering guarantees key-only domains; otherwise
route non-key domains through the same replay/fallback behavior as the skip
branch instead of raising ValueError.

In `@python/cudf_polars/cudf_polars/streaming/explain.py`:
- Line 144: Update the docstrings for explain_query and
SerializablePlan.from_query (including serialize_query) to state that
non-physical output is the optimized pre-lowering logical plan, after
optimize_with_stats, and may contain PushdownFilterHint nodes; replace the
inaccurate “logical (pre-lowering)” wording while preserving the physical-plan
documentation.

In `@python/cudf_polars/tests/streaming/test_tracing.py`:
- Around line 356-377: Update
test_indirect_prefilter_trace_records_decision_and_effect to remove the
unreachable method == "skip" branch, since all parameterized cases select bloom
or broadcast_semi_join, and narrow the domain_rows annotation from int | None to
int to match the supplied values.

---

Nitpick comments:
In `@python/cudf_polars/cudf_polars/streaming/actor_graph/core.py`:
- Around line 182-187: Extend the comment above the isinstance branch in the
actor-graph logic to explicitly explain why PushdownFilterHint also requires
unbounded fanout, while preserving the existing rationale for Union, Join, and
Over.

In `@python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py`:
- Around line 168-181: Define a shared structural Protocol for the add_prefilter
spec argument containing target_on, domain_on, and nulls_equal, and annotate
add_prefilter to accept that Protocol. Ensure both Prefilter and
PushdownFilterHint satisfy the contract without duplicating or narrowing their
existing field definitions.

In `@python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py`:
- Around line 400-466: The size checks in choose_prefilter_method should
document that a prefilter must not consume more memory than the target data it
filters. Add a concise comment near the bloom and exact size comparisons
explaining that min(bloom_filter_max_size or broadcast_limit, target.total_size)
enforces both the configured limit and the target-size cap.

In `@python/cudf_polars/cudf_polars/streaming/filter_hint.py`:
- Around line 84-94: Update the JoinWithPrefilter class docstring to document
that external domains in children[2:] must correspond positionally, in iteration
order, to the external prefilters. Keep the existing validation unchanged and
explicitly describe this ordering contract for callers such as
make_join_planning_state.

In `@python/cudf_polars/cudf_polars/streaming/join.py`:
- Around line 344-353: In the join planning logic, replace the if/else
assignment to preserve_prefilters with a direct boolean assignment using the
existing dynamic_planning, join-type, options, has_non_pointwise_keys, and
is_direct_join_prefilter conditions. Preserve the condition and resulting
True/False behavior unchanged.

In `@python/cudf_polars/cudf_polars/streaming/parallel.py`:
- Line 18: Remove the unused cudf_polars.streaming.filter_hint import from the
module imports in parallel.py; leave the existing join.py registration and class
imports unchanged.

In `@python/cudf_polars/tests/streaming/test_explain.py`:
- Around line 140-185: Extend
test_explain_pushdown_filter_hint_in_dynamic_physical_plan or add a focused
composite-candidate test using the pattern from
test_composite_filter_pushdown_constrains_domain_first. Assert the logical
PushdownFilterHint serialization uses placement "pushed_down", and the physical
prefilter serialization includes an ExternalDomain object with type
"ExternalDomain" and no side key, while retaining the existing JoinInputDomain
assertions.

In `@python/cudf_polars/tests/streaming/test_tracing.py`:
- Around line 209-222: Extract the repeated subprocess trace setup and
PREFILTER_TRACE parsing from the tracing tests into a module-level helper that
accepts the generated code and returns the decoded JSON record. Update all three
tests to call this helper, preserving the copied environment,
CUDF_POLARS_LOG_TRACES setting, subprocess options, and single-trace-line
parsing behavior.

In `@python/cudf_polars/tests/test_config.py`:
- Around line 715-732: Extend the bloom_filter_max_size tests in
ConfigOptions.from_polars_engine coverage with a True input that expects
TypeError and a 0 input that succeeds, preserving the existing invalid-string
and negative-value cases.
🪄 Autofix

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: 5743318a-7d20-4592-ac8a-7495dbfa3acc

📥 Commits

Reviewing files that changed from the base of the PR and between bbeea4b and 30984cd.

📒 Files selected for processing (20)
  • python/cudf_polars/cudf_polars/dsl/ir.py
  • python/cudf_polars/cudf_polars/dsl/utils/column_domain.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/__init__.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/collectives/common.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/core.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join_planning.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter_actor.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/utils.py
  • python/cudf_polars/cudf_polars/streaming/explain.py
  • python/cudf_polars/cudf_polars/streaming/filter_hint.py
  • python/cudf_polars/cudf_polars/streaming/join.py
  • python/cudf_polars/cudf_polars/streaming/join_filter_pushdown.py
  • python/cudf_polars/cudf_polars/streaming/parallel.py
  • python/cudf_polars/cudf_polars/utils/config.py
  • python/cudf_polars/tests/streaming/test_explain.py
  • python/cudf_polars/tests/streaming/test_join_filter_pushdown.py
  • python/cudf_polars/tests/streaming/test_tracing.py
  • python/cudf_polars/tests/test_config.py

# Include row-count statistics for the logical plan
with cm:
stats = collect_statistics(ir, config, executor)
ir = optimize_with_stats(ir, config, stats)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the docstrings for the new logical-plan semantics.

explain_query and SerializablePlan.from_query now apply optimize_with_stats before rendering the non-physical plan. The physical parameter is still documented as "If False, show the logical (pre-lowering) plan." in explain_query (line 108) and in serialize_query. The output is now the optimized pre-lowering plan, which contains PushdownFilterHint nodes that the translator never produces. Update both docstrings so callers know the rewrite is applied.

Also applies to: 888-893

🤖 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/explain.py` at line 144, Update the
docstrings for explain_query and SerializablePlan.from_query (including
serialize_query) to state that non-physical output is the optimized pre-lowering
logical plan, after optimize_with_stats, and may contain PushdownFilterHint
nodes; replace the inaccurate “logical (pre-lowering)” wording while preserving
the physical-plan documentation.

Comment on lines +356 to +377
@pytest.mark.parametrize(
"broadcast_limit,bloom_filter_max_size,method,reason,domain_rows",
[
(1, 32 * 1024 * 1024, "bloom", "bloom_fits", 15),
(512, 0, "broadcast_semi_join", "exact_domain_fits", 15),
(
1_000_000,
32 * 1024 * 1024,
"bloom",
"bloom_fits",
15,
),
],
ids=["bloom", "exact", "bloom_despite_intervening_broadcast"],
)
def test_indirect_prefilter_trace_records_decision_and_effect(
timeout_seconds: int,
broadcast_limit: int,
bloom_filter_max_size: int,
method: str,
reason: str,
domain_rows: int | None,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the unreachable skip branch.

No parameter set of test_indirect_prefilter_trace_records_decision_and_effect produces method == "skip". The branch at lines 484-486 never runs. The annotation domain_rows: int | None is also wider than the supplied values, which are all 15. Either add a skip case or delete the branch and narrow the annotation to int.

Also applies to: 484-486

🤖 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_tracing.py` around lines 356 - 377,
Update test_indirect_prefilter_trace_records_decision_and_effect to remove the
unreachable method == "skip" branch, since all parameterized cases select bloom
or broadcast_semi_join, and narrow the domain_rows annotation from int | None to
int to match the supplied values.

@wence- wence- added the improvement Improvement / enhancement to an existing function label Aug 7, 2026

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

Partial review, but overall the strategy and implementation make sense so far.

if node in unbounded:
_mark_children_unbounded(node)
elif isinstance(node, (Union, Join, Over)):
elif isinstance(node, (Union, Join, Over, PushdownFilterHint)):

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.

Nitpick: the comment below describes why Union, Join, and Over need unbounded fanout.

Maybe it's just a person preference thing, but I'd prefer an IR.needs_unbounaded_fanout property / class variable, and then we can override that for specific IR nodes, and document the reasoning where we set the property.

But this is fine too.

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.

Whether one needs unbounded fanout is a property of the implementation choice, not the IR node per-se. So I prefer it here.

Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/join_planning.py Outdated
Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py Outdated
chunks.clear()


async def count_rows_passthrough(

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 could live in actor_graph/tracing.py or actor_graph/utils.py. Only the join prefiltering is using it today, but I could see that changing in the near future.

Comment thread python/cudf_polars/cudf_polars/streaming/actor_graph/prefilter.py Outdated
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 Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEA] Use optional runtime filtering optimizations in cudf-polars

3 participants