Skip to content

Fix silent row drops in multi-GPU joins with computed key expressions - #22318

Merged
rapids-bot[bot] merged 10 commits into
NVIDIA:release/26.06from
Matt711:bug/pdsds/validate-q8
May 21, 2026
Merged

Fix silent row drops in multi-GPU joins with computed key expressions#22318
rapids-bot[bot] merged 10 commits into
NVIDIA:release/26.06from
Matt711:bug/pdsds/validate-q8

Conversation

@Matt711

@Matt711 Matt711 commented Apr 28, 2026

Copy link
Copy Markdown
Member

Description

When a join key is a computed expression (e.g. pl.col("ca_zip").str.slice(0, 2)), the join actor incorrectly inferred that the side using that key was already partitioned on it. The join actor derived the column index from the expression's output name, then checked the upstream partitioning metadata against that index. But the data was partitioned on the raw column value, not the derived one. When both sides happened to have the same shuffle modulus from prior joins, the planner chose a chunkwise join and skipped re-shuffling, so matching rows on different ranks were never paired and the result was silently missing rows.

The fix passes None as the partitioning metadata for any side whose join key is a computed expression, so the chunkwise path is never taken for those joins.

For context: I found this while running TPC-DS Q8 (multi-GPU). It was failing validation ~2-4 out of 15 runs producing wrong row counts.

Closes #22317
Closes #22105
Contributes to #21813

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 April 28, 2026 16:28
@Matt711
Matt711 requested a review from mroeschke April 28, 2026 16:28
@Matt711 Matt711 added bug Something isn't working non-breaking Non-breaking change labels Apr 28, 2026
@copy-pr-bot

copy-pr-bot Bot commented Apr 28, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

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 Apr 28, 2026
@GPUtester GPUtester moved this to In Progress in cuDF Python Apr 28, 2026
Comment thread python/cudf_polars/cudf_polars/experimental/rapidsmpf/join.py Outdated
Comment thread python/cudf_polars/cudf_polars/experimental/rapidsmpf/join.py Outdated
Comment thread python/cudf_polars/cudf_polars/experimental/rapidsmpf/utils.py Outdated
@Matt711 Matt711 mentioned this pull request May 12, 2026
3 tasks
rapids-bot Bot pushed a commit that referenced this pull request May 13, 2026
Workaround for #22317

The issue seems to be that we silently drop rows in multi-rank joins (described more in #22318 (comment)). This is why the sum changes non-deterministically from run-to-run. But the changes in that PR don't seem to be sufficient to validate Q8. Therefore, the change in this PR is a workaround until we figure where else we could be dropping rows.

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

Approvers:
  - Tom Augspurger (https://github.com/TomAugspurger)

URL: #22473
@Matt711
Matt711 marked this pull request as draft May 18, 2026 16:43
@Matt711
Matt711 changed the base branch from main to release/26.06 May 18, 2026 16:44
@Matt711
Matt711 force-pushed the bug/pdsds/validate-q8 branch from c9b22a0 to e6a6d89 Compare May 18, 2026 16:50
@Matt711
Matt711 force-pushed the bug/pdsds/validate-q8 branch from e6a6d89 to 7a43ab9 Compare May 18, 2026 16:58
@copy-pr-bot

copy-pr-bot Bot commented May 18, 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.

@Matt711
Matt711 marked this pull request as ready for review May 18, 2026 16:58
@coderabbitai

coderabbitai Bot commented May 18, 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 a concrete_prefix filter to the naming utility to drop trailing computed expressions when mapping names to schema indices; join and groupby planners use this filter to derive partitioning keys. Unit and integration tests verify the new mapping and multi-rank join behavior.

Changes

Computed Join Key Handling

Layer / File(s) Summary
Concrete prefix filtering in naming utility
python/cudf_polars/cudf_polars/dsl/utils/naming.py, python/cudf_polars/tests/dsl/test_naming.py
Introduce _concrete_prefix, import Col and Sequence, add keyword-only concrete_prefix to names_to_indices, and add a unit test verifying concrete-prefix behavior.
Join strategy selection with concrete key filtering
python/cudf_polars/cudf_polars/streaming/actor_graph/join.py, python/cudf_polars/tests/streaming/test_join.py
Pass concrete_prefix=True when deriving left and right join key indices in _choose_strategy; add a multi-rank join test that joins on a computed right key.
GroupBy partitioning key indices with concrete prefix
python/cudf_polars/cudf_polars/streaming/actor_graph/groupby.py
Import names_to_indices, extend _key_indices to accept concrete_prefix and use it for GroupBy key derivation; apply in groupby_actor dynamic partitioning and remove an obsolete comment.

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested Reviewers

  • wence-
  • vyasr
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.16% 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 specifically describes the main fix: preventing silent row drops in multi-GPU joins when join keys are computed expressions.
Linked Issues check ✅ Passed All code changes directly address the objectives: _concrete_prefix helper identifies actual data partitioning columns, names_to_indices with concrete_prefix=True returns only concrete column indices, join/groupby actors pass None for computed key metadata to disable chunkwise joins [#22317].
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the computed expression join bug: naming utility updates, join/groupby actor updates, and targeted test coverage for the fix.
Description check ✅ Passed The PR description clearly explains the bug (join keys as computed expressions incorrectly inferred to be already partitioned), the root cause, the fix, and provides context with specific issue references and real-world test case details.

✏️ 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.

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

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

393-447: ⚡ Quick win

Cover the symmetric computed-key path (left_on expression) in this test.

Line 393 currently validates only computed right_on. Since the planner change applies to both sides, parameterizing this test to also exercise computed left_on would better guard against one-sided regressions.

Suggested test update
+@pytest.mark.parametrize("computed_side", ["left", "right"])
-def test_join_computed_expr_right_key(streaming_engine_factory) -> None:
-    """Join on a computed key expression."""
+def test_join_computed_expr_key(streaming_engine_factory, computed_side) -> None:
+    """Join on a computed key expression from either side."""
@@
-    # Now join on a computed key expression.
-    # This should not silently drop rows across ranks
-    q = left.join(
-        right,
-        left_on="zip_prefix",
-        right_on=pl.col("full_zip").str.slice(0, 2),
-    )
+    # Now join on a computed key expression.
+    # This should not silently drop rows across ranks.
+    if computed_side == "right":
+        q = left.join(
+            right,
+            left_on="zip_prefix",
+            right_on=pl.col("full_zip").str.slice(0, 2),
+        )
+    else:
+        q = right.join(
+            left,
+            left_on=pl.col("full_zip").str.slice(0, 2),
+            right_on="zip_prefix",
+        )
     assert_gpu_result_equal(q, engine=engine, check_row_order=False)

As per coding guidelines: python/cudf_polars/**/test_*.py: verify tests compare GPU results against Polars CPU results and cover all supported expression 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/streaming/test_join.py` around lines 393 - 447, The
test test_join_computed_expr_right_key only exercises a computed expression on
right_on; update it to also exercise the symmetric case where the computed
expression is on left_on (e.g., call left.join(right,
left_on=pl.col("zip_prefix").str.slice(0, 2), right_on="full_zip") in addition
to the existing join), and ensure both queries (the original q and the new
symmetric one) are passed to assert_gpu_result_equal with the same engine and
check_row_order=False so the GPU/CPU results for both computed-key paths are
compared.
🤖 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.

Nitpick comments:
In `@python/cudf_polars/tests/streaming/test_join.py`:
- Around line 393-447: The test test_join_computed_expr_right_key only exercises
a computed expression on right_on; update it to also exercise the symmetric case
where the computed expression is on left_on (e.g., call left.join(right,
left_on=pl.col("zip_prefix").str.slice(0, 2), right_on="full_zip") in addition
to the existing join), and ensure both queries (the original q and the new
symmetric one) are passed to assert_gpu_result_equal with the same engine and
check_row_order=False so the GPU/CPU results for both computed-key paths are
compared.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 610e31f4-4ad3-433d-a6ff-46b178ccbdc8

📥 Commits

Reviewing files that changed from the base of the PR and between 7a43ab9 and 7533aa2.

📒 Files selected for processing (4)
  • python/cudf_polars/cudf_polars/dsl/utils/naming.py
  • python/cudf_polars/cudf_polars/streaming/actor_graph/join.py
  • python/cudf_polars/tests/dsl/test_naming.py
  • python/cudf_polars/tests/streaming/test_join.py
✅ Files skipped from review due to trivial changes (1)
  • python/cudf_polars/tests/dsl/test_naming.py

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

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

235-253: ⚡ Quick win

Add one end-to-end streaming regression case for mixed concrete/computed GroupBy keys.

This validates _key_indices directly, but it doesn’t verify distributed runtime correctness. Please add an assert_gpu_result_equal(...) case with mixed concrete/computed keys under streaming settings that exercise partition-planning decisions.

As per coding guidelines "In cudf_polars: verify tests compare GPU results against Polars CPU results and cover all supported expression 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/streaming/test_groupby.py` around lines 235 - 253,
The test only checks _key_indices statically; add an end-to-end streaming
regression that runs the GroupBy with mixed concrete/computed keys through the
GPU runtime and compares to Polars CPU using assert_gpu_result_equal under
streaming settings: construct a small input DataFrame matching the GroupBy
schema used in test_groupby_key_indices_concrete_prefix (use GroupBy with
NamedExpr("a", Col), NamedExpr("b", Literal), NamedExpr("c", Col)), enable
streaming/partitioning flags used elsewhere in tests, execute the GPU pipeline
and call assert_gpu_result_equal(...) against the equivalent Polars expression
to validate partition-planning/runtime correctness while still keeping the
existing static _key_indices assertions. Ensure the test references the same
symbols (GroupBy, _key_indices, expr.NamedExpr, ErrorNode,
assert_gpu_result_equal) so it exercises mixed concrete/computed keys
end-to-end.
🤖 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.

Nitpick comments:
In `@python/cudf_polars/tests/streaming/test_groupby.py`:
- Around line 235-253: The test only checks _key_indices statically; add an
end-to-end streaming regression that runs the GroupBy with mixed
concrete/computed keys through the GPU runtime and compares to Polars CPU using
assert_gpu_result_equal under streaming settings: construct a small input
DataFrame matching the GroupBy schema used in
test_groupby_key_indices_concrete_prefix (use GroupBy with NamedExpr("a", Col),
NamedExpr("b", Literal), NamedExpr("c", Col)), enable streaming/partitioning
flags used elsewhere in tests, execute the GPU pipeline and call
assert_gpu_result_equal(...) against the equivalent Polars expression to
validate partition-planning/runtime correctness while still keeping the existing
static _key_indices assertions. Ensure the test references the same symbols
(GroupBy, _key_indices, expr.NamedExpr, ErrorNode, assert_gpu_result_equal) so
it exercises mixed concrete/computed keys end-to-end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 59c4f45a-d0a9-457b-a77b-b8a7a2cc312b

📥 Commits

Reviewing files that changed from the base of the PR and between 7533aa2 and 9f73486.

📒 Files selected for processing (2)
  • python/cudf_polars/cudf_polars/streaming/actor_graph/groupby.py
  • python/cudf_polars/tests/streaming/test_groupby.py

Comment thread python/cudf_polars/cudf_polars/dsl/utils/naming.py
Comment on lines +116 to +118

Uses ``hash_partition(input, key_table, ...)`` to support
non-``Col`` (e.g. expression-derived) shuffle keys.

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.

Suggested change
Uses ``hash_partition(input, key_table, ...)`` to support
non-``Col`` (e.g. expression-derived) shuffle keys.

But please describe the parameters.

Comment on lines +45 to +53
concrete: list[str] = []
for n in names:
if isinstance(n, str):
concrete.append(n)
elif isinstance(n.value, Col):
concrete.append(n.value.name)
else:
break
return tuple(concrete)

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.

return tuple(
    n if isinstance(n, str) else n.value.name
    for n in itertools.takewhile(
        lambda n: isinstance(n, str) or isinstance(n.value, Col),
        names
   )
)

If we fancy being fancy

Comment on lines +74 to +77
concrete_prefix
If True, use only the prefix of names corresponding
to concrete column references. If False (default),
use all names.

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.

Under what circumstances to we not want this?

names_to_indices is used to convert name references into a column indices of a table. So by definition, I think, it can't be used to if the namedexpr isn't referring to a column?

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.

It depends on whether the schema we are indexing on corresponds the input or the output of the expressions in names.

If the schema references the output DataFrame, then it's fine for the expressions to be non-concrete - The output of the expressions are concrete columns. If the schema references the input DataFrame, then the expression must be concrete.

When we check if the input DataFrame is already partitioned correctly, we must pass in this concrete_prefix=True option.

assert not _use_pwise_join(executor, partition_info, join_ir)


def test_join_computed_expr_right_key(streaming_engine_factory) -> None:

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.

I can't remember exactly how we run these tests multi-rank. But we should ensure this test is run multi-rank.

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.

Yes, the Ray variation will run two ranks on the same visible device.

Comment thread python/cudf_polars/tests/streaming/test_groupby.py Outdated

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

Approving assuming the detail test is removed.

@rjzamora

Copy link
Copy Markdown
Contributor

/merge

@rapids-bot
rapids-bot Bot merged commit abd5619 into NVIDIA:release/26.06 May 21, 2026
93 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in cuDF Python May 21, 2026
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.

[BUG] Validate TPC-DS Q8

6 participants