Skip to content

Drop dynamic predicates from logical plan - #22820

Merged
rapids-bot[bot] merged 15 commits into
NVIDIA:mainfrom
Matt711:bug/polars/dyn-pred
Jun 15, 2026
Merged

Drop dynamic predicates from logical plan#22820
rapids-bot[bot] merged 15 commits into
NVIDIA:mainfrom
Matt711:bug/polars/dyn-pred

Conversation

@Matt711

@Matt711 Matt711 commented Jun 9, 2026

Copy link
Copy Markdown
Member

Description

Fixes performance regression in nightly benchmark (due to falling back to polars CPU). In polars 1.42, we should be able to properly handle dynamic predicates since they'll be available in the node visitor.

Also ensures that we fail if we fallback to polars CPU.

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 requested a review from a team as a code owner June 9, 2026 13:41
@Matt711 Matt711 added the bug Something isn't working label Jun 9, 2026
@Matt711
Matt711 requested a review from nirandaperera June 9, 2026 13:41
@Matt711 Matt711 added the non-breaking Non-breaking change label Jun 9, 2026
@github-actions github-actions Bot added Python Affects Python cuDF API. cudf-polars Issues specific to cudf-polars labels Jun 9, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Jun 9, 2026
@coderabbitai

coderabbitai Bot commented Jun 9, 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

Adds predicate translation helpers that remove Polars dynamic_pred hint nodes and return an optional predicate, wires this into Scan and Filter to omit collapsed predicates, standardizes raise_on_fail=True via setdefault() across GPU engine frontends, and adds a regression test for hint-dropping.

Changes

Dynamic Predicate Hint Translation

Layer / File(s) Summary
Dynamic predicate hint translation functions
python/cudf_polars/cudf_polars/dsl/translate.py
New _drop_dyn_pred_hints() recursively removes dynamic_pred nodes from expression trees and translate_predicate() returns an expr.NamedExpr or None when the predicate collapses.
Predicate translation wiring in Scan and Filter
python/cudf_polars/cudf_polars/dsl/translate.py
Scan and Filter translation paths now call translate_predicate() and omit the predicate/mask when it returns None after dropping dynamic-hint-only branches.
Regression test for dynamic predicate handling
python/cudf_polars/tests/test_filter.py
Adds test_filter_drops_dynamic_predicate_hint that verifies translation produces a Filter IR with no errors and a mask referencing only non-hint columns.

Engine Options Configuration

Layer / File(s) Summary
Consistent raise_on_fail defaults across execution frontends
python/cudf_polars/cudf_polars/streaming/benchmarks/utils.py
In-memory, SPMD, Ray, and Dask execution paths set engine_options.setdefault("raise_on_fail", True) before constructing Polars GPU engines.

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested reviewers:

  • mroeschke
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% 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
Title check ✅ Passed The title clearly and concisely summarizes the main change: dropping dynamic predicates from the logical plan to fix a performance regression.
Description check ✅ Passed The description is directly related to the changeset, explaining the motivation (performance regression in nightly benchmark), the solution (drop dynamic predicates), and additional improvements (ensuring failure on CPU fallback).
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Comment thread python/cudf_polars/tests/test_filter.py Outdated
)


def test_filter_drops_dynamic_predicate_hint():

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'm testing it this way mainly because I don't to deal with getting the optimizer to deterministically insert dynamic hints.

@Matt711

Matt711 commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

/merge

@Matt711

Matt711 commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

/merge

@Matt711

Matt711 commented Jun 11, 2026

Copy link
Copy Markdown
Member Author

Update: Tests are failing with 1.41, but I'm not sure which tests yet.

right = _drop_dyn_pred_hints(translator, node.right, schema)
if left is None:
return right
if right is None:

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.

TODO code coverage is missing here

@Matt711

Matt711 commented Jun 15, 2026

Copy link
Copy Markdown
Member Author

/merge

engine_options.setdefault("raise_on_fail", True)
engine = pl.GPUEngine(
executor="in-memory",
raise_on_fail=True,

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.

Why do you need this?

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.

Because currently our nightly benchmarks are not raising an error when we fallback to CPU. This was missed in all the refactoring we did to tests / benchmarks

Comment thread python/cudf_polars/tests/test_filter.py Outdated
expr.BinOp._MAPPING[node.op],
left,
right,
)

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 are these hints and what is going on here?

@Matt711 Matt711 Jun 15, 2026

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.

They are same dynamic predicate hints we discussed in pola-rs/polars#27616 (comment). I think your real question is why are they showing up in the inputs of binop expressions? I found an example:

In [2]: import polars as pl
   ...: 
   ...: ldf = pl.LazyFrame(
   ...:     {"a": [1, 2, 3, 4, 5], "b": [5, 4, 3, 2, 1], "c": [1, 1, 3, 3, 5]}
   ...: )
   ...: q = ldf.filter((pl.col("a") > 1) & (pl.col("c") == 3)).sort("b").head(3)
   ...: print(q.explain())
SORT BY [slice: (0, 3, dynamic_pred: 2a68ecf7-b479-48bd-9737-f7bebb1e5ecf)] [col("b")]
  FILTER [([([(col("a")) > (1)]) & ([(col("c")) == (3)])]) & (col("b").dynamic_predicate())]
  FROM
    DF ["a", "b", "c"]; PROJECT */3 COLUMNS

Polars will insert a dynamic predicate in the binary expression; essentially saying "you're doing a sort + slice and you're filtering by a > 1 and c == 3, you can also skip rows where b is above the top-3 once we (cudf-polars) figure what the threshold is"

@Matt711
Matt711 force-pushed the bug/polars/dyn-pred branch from 17427c6 to ff1e9b3 Compare June 15, 2026 17:34
@rapids-bot
rapids-bot Bot merged commit a165694 into NVIDIA:main Jun 15, 2026
136 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python Jun 15, 2026
rapids-bot Bot pushed a commit that referenced this pull request Jun 25, 2026
The original test I added in #22820 does not produce the dynamic predicate on the same side of the filter determinitically.
```python
import polars as pl

df = pl.LazyFrame({"a": [1, 2, 3, 4, 5], "b": [5, 4, 3, 2, 1], "c": [1, 1, 3, 3, 5]})
q = df.filter((pl.col("a") > 1) & (pl.col("c") == 3)).sort("b").head(3)
```
Inspect the logical plan on successive calls to `explain()`
```
SORT BY [slice: (0, 3, dynamic_pred: c6412d2b-fd62-4ecb-b5fc-c575f7fe4411)] [col("b")]
  FILTER [([(col("b").dynamic_predicate()) & ([(col("a")) > (1)])]) & ([(col("c")) == (3)])]
  FROM
    DF ["a", "b", "c"]; PROJECT */3 COLUMNS
```

```
SORT BY [slice: (0, 3, dynamic_pred: 347e337d-20db-4311-900f-53d387531102)] [col("b")]
  FILTER [([([(col("a")) > (1)]) & ([(col("c")) == (3)])]) & (col("b").dynamic_predicate())]
  FROM
    DF ["a", "b", "c"]; PROJECT */3 COLUMNS
```

So this PR skips code coverage to avoid this nondeterminism. We could write a hacky test to without sacrificing code coverage, but it's not worth it IMO.

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

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

URL: #22973
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working cudf-polars Issues specific to cudf-polars non-breaking Non-breaking change Python Affects Python cuDF API.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants