Repository navigation
Commit 671e11b
authored
Support equi joins that Polars plans with a fused predicate (NVIDIA#24858)
### What Polars gives us
Polars plans an equi join with an extra, non-equality condition as a single join with a "fused predicate". For example `SQL ... WHERE a.key = b.key AND x <> y`, `join_where` with an equality and an inequality, and correlated `EXISTS` / `NOT EXISTS` subqueries such as the ones in TPC-H queries 7, 17, 19, 20 and 21. With Polars 2.0 the visitor did not expose that condition, so cudf-polars fell back to the CPU. Polars 2.1 reports the join as `("Inner", "fused_predicate")`, with the equality keys in `left_on`/`right_on` and the extra condition in `node.predicate`.
```python
import polars as pl
orders = pl.LazyFrame({"key": [1, 2, 3], "supp_nation": ["FRANCE", "GERMANY", "FRANCE"]})
customers = pl.LazyFrame({"key": [1, 2, 3], "cust_nation": ["GERMANY", "FRANCE", "FRANCE"]})
q = pl.SQLContext(orders=orders, customers=customers).execute(
"SELECT * FROM orders, customers WHERE orders.key = customers.key AND supp_nation <> cust_nation"
)
q.collect(engine=pl.GPUEngine(raise_on_fail=True)) # before: NotImplementedError, now: runs on the GPU
```
### The obvious approach does not scale
The first version of this PR translated the join as an equi join followed by a filter. That is simple, but the join writes out every pair of rows that match on the keys before the filter sees any of them. If 30,000 rows on each side share one key, that is 900 million pairs, even when the predicate rejects all of them.
```python
n = 30_000
left = pl.LazyFrame({"k": [1] * n, "a": range(n)})
right = pl.LazyFrame({"k": [1] * n, "b": range(n)})
q = pl.SQLContext(left=left, right=right).execute(
"SELECT * FROM left, right WHERE left.k = right.k AND left.a + right.b < 0"
)
q.collect(engine=pl.GPUEngine(raise_on_fail=True))
# join then filter: out of memory (a 7.2 GB allocation) on a 32 GB GPU
# this PR: 0 rows
```
### What this PR does
libcudf already has a mixed join. It builds a hash table on the keys, finds the rows with the same key, and tests the extra condition on the spot. It only writes out the pairs that pass, so memory scales with the answer, not with the number of key matches. `Join` now takes an optional predicate and evaluates it with `plc.join.mixed_inner_join`.
The catch is that the condition must be written in libcudf's AST. The AST is strict: no implicit casts, both operands of an operation must have the same type, decimal128 only supports comparisons, and there are no string functions. Polars conditions are arbitrary expressions, such as `price > 1.2 * avg_price` (TPC-DS query 6) or `lower(a) <> lower(b)`.
So the condition is split. Any part that uses only one input is computed once per row on that input, and added as an extra column to the table the AST looks at. The condition is rewritten so the AST only compares columns of the two inputs. That is 60,000 evaluations in the example above, not 900 million, and it should also be cheaper than evaluating the same expression for every pair (I have not benchmarked that). Conditions that cannot be split this way fall back to the CPU: a non-pointwise expression on one input, or operands that still differ in type.
## Changes
- `dsl/ir.py`: `Join` takes an optional predicate (inner joins only) and calls `plc.join.mixed_inner_join` when there is one. Gather maps, row ordering and the streaming paths are unchanged.
- `dsl/to_ast.py`: `split_join_predicate` does the split described above.
- `dsl/translate.py`: `_translate_fused_predicate` builds that `Join`. Other join kinds fall back to the CPU (I only saw a predicate fused into an inner join, including when the original query used an outer join and Polars rewrote it).
- `streaming/join.py`, `streaming/filter_hint.py`, `streaming/actor_graph/join.py`: pass the predicate along when a join is rebuilt. Prefilters are not used for a join with a predicate.
- `FUSED_PREDICATE_XFAIL` in `tests/test_join.py` is now conditional on Polars 2.0, where the visitor still hides the predicate (uses `POLARS_VERSION_LT_21` from NVIDIA#24825).
- Tests, all skipped on Polars 2.0:
- `tests/test_join_fused_predicate.py`: the SQL query above, an inner join followed by a filter (`nulls_equal` both ways, with NULL keys that pass the predicate so the two settings differ), `join_where`, suffixed columns, correlated `EXISTS` and `NOT EXISTS`, an empty result, a decimal compared with a computed float (the TPC-DS query 6 shape), expressions of each input, a repeated expression, a string function, mismatched integer widths, a check that no `Filter` is left in the plan, and the 30,000 row case above.
- `tests/dsl/test_join_predicate.py`: unit tests for the split and for the cases that fall back.
## Notes for reviewers
- This is partly a workaround for limits of libcudf's AST. The hoisting of one-sided work is worth doing anyway, but doing it for type and string reasons is not ideal. From reading the libcudf source (not tested), it has AST casts to int64, uint64 and float64 and the float64 cast accepts decimal sources, but pylibcudf does not expose them and they do not cover narrower types, strings or decimal128 arithmetic. Exposing them, and better type promotion in the AST, would be worth a follow-up. It would shrink the split but not replace it. `ConditionalJoin` already works around the same limits by pre-casting decimal and float columns.
- `Join` gains a positional `predicate` argument, so every `Join(...)` call site changed.
- The 30,000 row test is skipped on spmd because it takes 36 s with many small partitions.
## Verification
| Check | Result |
| --- | --- |
| `tests/test_join_fused_predicate.py` on Polars 2.1.0, in-memory, dask and spmd | 59 passed, 2 skipped |
| `tests/test_join_fused_predicate.py`, `tests/dsl/test_join_predicate.py`, `tests/test_join.py` | 633 passed, 173 skipped, 5 xfailed |
| New tests against the join-then-filter version | `test_predicate_is_evaluated_inside_the_join` and the 30,000 row test fail (out of memory, 7.2 GB) |
| 30,000 rows per side, one shared key, predicate rejects all (V100, 32 GB) | before: out of memory. now: 0 rows on the GPU |
| `test_join.py`, `test_join_fused_predicate.py`, `test_sql.py`, `tests/dsl`, `tests/streaming` on this branch and on the join-then-filter version | the same 318 failures in both, with identical test ids. They are environment failures on my host (TPC-DS and TPC-H data, hive scans, metadata, a libcudf JIT cache directory). 2,683 passed here, 2,616 before |
| TPC-DS query 6 (it broke with a first version of the predicate join, fixed by the split) | passes |
| Coverage of the added lines | all covered except `JoinWithPrefilter.predicate = None`, which only runs with dynamic planning (rapidsmpf) |
| ruff and mypy | `check-style` passed in CI after the mypy fix; later commits are small refactors, CI re-running |
| Polars 2.0, upstream Polars tests, TPC-H 7, 17, 19, 20, 21 and 22, dynamic planning, speed against join then filter | not run after this change |
Authors:
- Matthew Murray (https://github.com/Matt711)
Approvers:
- Tom Augspurger (https://github.com/TomAugspurger)
- Matthew Roeschke (https://github.com/mroeschke)
URL: NVIDIA#248581 parent 427634a commit 671e11b
12 files changed
Lines changed: 638 additions & 13 deletions
File tree
- python/cudf_polars
- cudf_polars
- dsl
- streaming
- actor_graph
- tests
- dsl
- streaming
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
80 | 81 | | |
81 | 82 | | |
82 | 83 | | |
| 84 | + | |
83 | 85 | | |
84 | 86 | | |
85 | 87 | | |
| |||
2979 | 2981 | | |
2980 | 2982 | | |
2981 | 2983 | | |
| 2984 | + | |
| 2985 | + | |
| 2986 | + | |
| 2987 | + | |
| 2988 | + | |
| 2989 | + | |
| 2990 | + | |
| 2991 | + | |
| 2992 | + | |
| 2993 | + | |
| 2994 | + | |
| 2995 | + | |
| 2996 | + | |
| 2997 | + | |
| 2998 | + | |
| 2999 | + | |
| 3000 | + | |
| 3001 | + | |
| 3002 | + | |
| 3003 | + | |
| 3004 | + | |
| 3005 | + | |
| 3006 | + | |
| 3007 | + | |
| 3008 | + | |
| 3009 | + | |
| 3010 | + | |
| 3011 | + | |
| 3012 | + | |
| 3013 | + | |
| 3014 | + | |
2982 | 3015 | | |
2983 | 3016 | | |
2984 | 3017 | | |
| |||
3127 | 3160 | | |
3128 | 3161 | | |
3129 | 3162 | | |
3130 | | - | |
| 3163 | + | |
3131 | 3164 | | |
3132 | 3165 | | |
3133 | 3166 | | |
3134 | 3167 | | |
3135 | 3168 | | |
| 3169 | + | |
3136 | 3170 | | |
3137 | | - | |
| 3171 | + | |
3138 | 3172 | | |
3139 | 3173 | | |
3140 | 3174 | | |
3141 | 3175 | | |
| 3176 | + | |
| 3177 | + | |
| 3178 | + | |
| 3179 | + | |
| 3180 | + | |
3142 | 3181 | | |
3143 | 3182 | | |
3144 | 3183 | | |
| |||
3176 | 3215 | | |
3177 | 3216 | | |
3178 | 3217 | | |
| 3218 | + | |
3179 | 3219 | | |
3180 | 3220 | | |
3181 | 3221 | | |
| |||
3184 | 3224 | | |
3185 | 3225 | | |
3186 | 3226 | | |
3187 | | - | |
| 3227 | + | |
| 3228 | + | |
| 3229 | + | |
| 3230 | + | |
| 3231 | + | |
| 3232 | + | |
| 3233 | + | |
| 3234 | + | |
| 3235 | + | |
| 3236 | + | |
| 3237 | + | |
| 3238 | + | |
| 3239 | + | |
| 3240 | + | |
| 3241 | + | |
| 3242 | + | |
| 3243 | + | |
| 3244 | + | |
3188 | 3245 | | |
3189 | 3246 | | |
3190 | 3247 | | |
| |||
3370 | 3427 | | |
3371 | 3428 | | |
3372 | 3429 | | |
| 3430 | + | |
| 3431 | + | |
| 3432 | + | |
| 3433 | + | |
| 3434 | + | |
| 3435 | + | |
3373 | 3436 | | |
3374 | 3437 | | |
3375 | 3438 | | |
| |||
3442 | 3505 | | |
3443 | 3506 | | |
3444 | 3507 | | |
| 3508 | + | |
| 3509 | + | |
| 3510 | + | |
| 3511 | + | |
| 3512 | + | |
| 3513 | + | |
| 3514 | + | |
| 3515 | + | |
| 3516 | + | |
| 3517 | + | |
| 3518 | + | |
| 3519 | + | |
| 3520 | + | |
3445 | 3521 | | |
3446 | 3522 | | |
3447 | 3523 | | |
| |||
3464 | 3540 | | |
3465 | 3541 | | |
3466 | 3542 | | |
3467 | | - | |
3468 | | - | |
3469 | | - | |
| 3543 | + | |
| 3544 | + | |
| 3545 | + | |
| 3546 | + | |
| 3547 | + | |
| 3548 | + | |
| 3549 | + | |
| 3550 | + | |
| 3551 | + | |
| 3552 | + | |
| 3553 | + | |
| 3554 | + | |
| 3555 | + | |
| 3556 | + | |
3470 | 3557 | | |
3471 | 3558 | | |
3472 | 3559 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
| 19 | + | |
| 20 | + | |
19 | 21 | | |
20 | 22 | | |
21 | 23 | | |
22 | | - | |
| 24 | + | |
23 | 25 | | |
24 | 26 | | |
25 | 27 | | |
| |||
112 | 114 | | |
113 | 115 | | |
114 | 116 | | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
115 | 140 | | |
116 | 141 | | |
117 | 142 | | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
118 | 146 | | |
119 | 147 | | |
120 | 148 | | |
| |||
441 | 469 | | |
442 | 470 | | |
443 | 471 | | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
| 539 | + | |
| 540 | + | |
| 541 | + | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
| 569 | + | |
| 570 | + | |
| 571 | + | |
| 572 | + | |
| 573 | + | |
| 574 | + | |
| 575 | + | |
| 576 | + | |
| 577 | + | |
0 commit comments