cudf-polars-cu12 25.2.2__py3-none-any.whl → 25.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cudf_polars/VERSION +1 -1
- cudf_polars/callback.py +82 -65
- cudf_polars/containers/column.py +138 -7
- cudf_polars/containers/dataframe.py +26 -39
- cudf_polars/dsl/expr.py +3 -1
- cudf_polars/dsl/expressions/aggregation.py +27 -63
- cudf_polars/dsl/expressions/base.py +40 -72
- cudf_polars/dsl/expressions/binaryop.py +5 -41
- cudf_polars/dsl/expressions/boolean.py +25 -53
- cudf_polars/dsl/expressions/datetime.py +97 -17
- cudf_polars/dsl/expressions/literal.py +27 -33
- cudf_polars/dsl/expressions/rolling.py +110 -9
- cudf_polars/dsl/expressions/selection.py +8 -26
- cudf_polars/dsl/expressions/slicing.py +47 -0
- cudf_polars/dsl/expressions/sorting.py +5 -18
- cudf_polars/dsl/expressions/string.py +33 -36
- cudf_polars/dsl/expressions/ternary.py +3 -10
- cudf_polars/dsl/expressions/unary.py +35 -75
- cudf_polars/dsl/ir.py +749 -212
- cudf_polars/dsl/nodebase.py +8 -1
- cudf_polars/dsl/to_ast.py +5 -3
- cudf_polars/dsl/translate.py +319 -171
- cudf_polars/dsl/utils/__init__.py +8 -0
- cudf_polars/dsl/utils/aggregations.py +292 -0
- cudf_polars/dsl/utils/groupby.py +97 -0
- cudf_polars/dsl/utils/naming.py +34 -0
- cudf_polars/dsl/utils/replace.py +46 -0
- cudf_polars/dsl/utils/rolling.py +113 -0
- cudf_polars/dsl/utils/windows.py +186 -0
- cudf_polars/experimental/base.py +17 -19
- cudf_polars/experimental/benchmarks/__init__.py +4 -0
- cudf_polars/experimental/benchmarks/pdsh.py +1279 -0
- cudf_polars/experimental/dask_registers.py +196 -0
- cudf_polars/experimental/distinct.py +174 -0
- cudf_polars/experimental/explain.py +127 -0
- cudf_polars/experimental/expressions.py +521 -0
- cudf_polars/experimental/groupby.py +288 -0
- cudf_polars/experimental/io.py +58 -29
- cudf_polars/experimental/join.py +353 -0
- cudf_polars/experimental/parallel.py +166 -93
- cudf_polars/experimental/repartition.py +69 -0
- cudf_polars/experimental/scheduler.py +155 -0
- cudf_polars/experimental/select.py +92 -7
- cudf_polars/experimental/shuffle.py +294 -0
- cudf_polars/experimental/sort.py +45 -0
- cudf_polars/experimental/spilling.py +151 -0
- cudf_polars/experimental/utils.py +100 -0
- cudf_polars/testing/asserts.py +146 -6
- cudf_polars/testing/io.py +72 -0
- cudf_polars/testing/plugin.py +78 -76
- cudf_polars/typing/__init__.py +59 -6
- cudf_polars/utils/config.py +353 -0
- cudf_polars/utils/conversion.py +40 -0
- cudf_polars/utils/dtypes.py +22 -5
- cudf_polars/utils/timer.py +39 -0
- cudf_polars/utils/versions.py +5 -4
- {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info}/METADATA +10 -7
- cudf_polars_cu12-25.6.0.dist-info/RECORD +73 -0
- {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info}/WHEEL +1 -1
- cudf_polars/experimental/dask_serialize.py +0 -59
- cudf_polars_cu12-25.2.2.dist-info/RECORD +0 -48
- {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info/licenses}/LICENSE +0 -0
- {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Parallel Join Logic."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import operator
|
|
8
|
+
from functools import reduce
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from cudf_polars.dsl.ir import ConditionalJoin, Join
|
|
12
|
+
from cudf_polars.experimental.base import PartitionInfo, get_key_name
|
|
13
|
+
from cudf_polars.experimental.dispatch import generate_ir_tasks, lower_ir_node
|
|
14
|
+
from cudf_polars.experimental.repartition import Repartition
|
|
15
|
+
from cudf_polars.experimental.shuffle import Shuffle, _partition_dataframe
|
|
16
|
+
from cudf_polars.experimental.utils import _concat, _fallback_inform, _lower_ir_fallback
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from collections.abc import MutableMapping
|
|
20
|
+
|
|
21
|
+
from cudf_polars.dsl.expr import NamedExpr
|
|
22
|
+
from cudf_polars.dsl.ir import IR
|
|
23
|
+
from cudf_polars.experimental.parallel import LowerIRTransformer
|
|
24
|
+
from cudf_polars.utils.config import ConfigOptions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _maybe_shuffle_frame(
|
|
28
|
+
frame: IR,
|
|
29
|
+
on: tuple[NamedExpr, ...],
|
|
30
|
+
partition_info: MutableMapping[IR, PartitionInfo],
|
|
31
|
+
config_options: ConfigOptions,
|
|
32
|
+
output_count: int,
|
|
33
|
+
) -> IR:
|
|
34
|
+
# Shuffle `frame` if it isn't already shuffled.
|
|
35
|
+
if (
|
|
36
|
+
partition_info[frame].partitioned_on == on
|
|
37
|
+
and partition_info[frame].count == output_count
|
|
38
|
+
):
|
|
39
|
+
# Already shuffled
|
|
40
|
+
return frame
|
|
41
|
+
else:
|
|
42
|
+
# Insert new Shuffle node
|
|
43
|
+
frame = Shuffle(
|
|
44
|
+
frame.schema,
|
|
45
|
+
on,
|
|
46
|
+
config_options,
|
|
47
|
+
frame,
|
|
48
|
+
)
|
|
49
|
+
partition_info[frame] = PartitionInfo(
|
|
50
|
+
count=output_count,
|
|
51
|
+
partitioned_on=on,
|
|
52
|
+
)
|
|
53
|
+
return frame
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _make_hash_join(
|
|
57
|
+
ir: Join,
|
|
58
|
+
output_count: int,
|
|
59
|
+
partition_info: MutableMapping[IR, PartitionInfo],
|
|
60
|
+
left: IR,
|
|
61
|
+
right: IR,
|
|
62
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
63
|
+
# Shuffle left and right dataframes (if necessary)
|
|
64
|
+
new_left = _maybe_shuffle_frame(
|
|
65
|
+
left,
|
|
66
|
+
ir.left_on,
|
|
67
|
+
partition_info,
|
|
68
|
+
ir.config_options,
|
|
69
|
+
output_count,
|
|
70
|
+
)
|
|
71
|
+
new_right = _maybe_shuffle_frame(
|
|
72
|
+
right,
|
|
73
|
+
ir.right_on,
|
|
74
|
+
partition_info,
|
|
75
|
+
ir.config_options,
|
|
76
|
+
output_count,
|
|
77
|
+
)
|
|
78
|
+
if left != new_left or right != new_right:
|
|
79
|
+
ir = ir.reconstruct([new_left, new_right])
|
|
80
|
+
left = new_left
|
|
81
|
+
right = new_right
|
|
82
|
+
|
|
83
|
+
# Record new partitioning info
|
|
84
|
+
partitioned_on: tuple[NamedExpr, ...] = ()
|
|
85
|
+
if ir.left_on == ir.right_on or (ir.options[0] in ("Left", "Semi", "Anti")):
|
|
86
|
+
partitioned_on = ir.left_on
|
|
87
|
+
elif ir.options[0] == "Right":
|
|
88
|
+
partitioned_on = ir.right_on
|
|
89
|
+
partition_info[ir] = PartitionInfo(
|
|
90
|
+
count=output_count,
|
|
91
|
+
partitioned_on=partitioned_on,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return ir, partition_info
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _should_bcast_join(
|
|
98
|
+
ir: Join,
|
|
99
|
+
left: IR,
|
|
100
|
+
right: IR,
|
|
101
|
+
partition_info: MutableMapping[IR, PartitionInfo],
|
|
102
|
+
output_count: int,
|
|
103
|
+
) -> bool:
|
|
104
|
+
# Decide if a broadcast join is appropriate.
|
|
105
|
+
if partition_info[left].count >= partition_info[right].count:
|
|
106
|
+
small_count = partition_info[right].count
|
|
107
|
+
large = left
|
|
108
|
+
large_on = ir.left_on
|
|
109
|
+
else:
|
|
110
|
+
small_count = partition_info[left].count
|
|
111
|
+
large = right
|
|
112
|
+
large_on = ir.right_on
|
|
113
|
+
|
|
114
|
+
# Avoid the broadcast if the "large" table is already shuffled
|
|
115
|
+
large_shuffled = (
|
|
116
|
+
partition_info[large].partitioned_on == large_on
|
|
117
|
+
and partition_info[large].count == output_count
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Broadcast-Join Criteria:
|
|
121
|
+
# 1. Large dataframe isn't already shuffled
|
|
122
|
+
# 2. Small dataframe has 8 partitions (or fewer).
|
|
123
|
+
# TODO: Make this value/heuristic configurable).
|
|
124
|
+
# We may want to account for the number of workers.
|
|
125
|
+
# 3. The "kind" of join is compatible with a broadcast join
|
|
126
|
+
assert ir.config_options.executor.name == "streaming", (
|
|
127
|
+
"'in-memory' executor not supported in 'generate_ir_tasks'"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
not large_shuffled
|
|
132
|
+
and small_count <= ir.config_options.executor.broadcast_join_limit
|
|
133
|
+
and (
|
|
134
|
+
ir.options[0] == "Inner"
|
|
135
|
+
or (ir.options[0] in ("Left", "Semi", "Anti") and large == left)
|
|
136
|
+
or (ir.options[0] == "Right" and large == right)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _make_bcast_join(
|
|
142
|
+
ir: Join,
|
|
143
|
+
output_count: int,
|
|
144
|
+
partition_info: MutableMapping[IR, PartitionInfo],
|
|
145
|
+
left: IR,
|
|
146
|
+
right: IR,
|
|
147
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
148
|
+
if ir.options[0] != "Inner":
|
|
149
|
+
left_count = partition_info[left].count
|
|
150
|
+
right_count = partition_info[right].count
|
|
151
|
+
|
|
152
|
+
# Shuffle the smaller table (if necessary) - Notes:
|
|
153
|
+
# - We need to shuffle the smaller table if
|
|
154
|
+
# (1) we are not doing an "inner" join,
|
|
155
|
+
# and (2) the small table contains multiple
|
|
156
|
+
# partitions.
|
|
157
|
+
# - We cannot simply join a large-table partition
|
|
158
|
+
# to each small-table partition, and then
|
|
159
|
+
# concatenate the partial-join results, because
|
|
160
|
+
# a non-"inner" join does NOT commute with
|
|
161
|
+
# concatenation.
|
|
162
|
+
# - In some cases, we can perform the partial joins
|
|
163
|
+
# sequentially. However, we are starting with a
|
|
164
|
+
# catch-all algorithm that works for all cases.
|
|
165
|
+
if left_count >= right_count:
|
|
166
|
+
right = _maybe_shuffle_frame(
|
|
167
|
+
right,
|
|
168
|
+
ir.right_on,
|
|
169
|
+
partition_info,
|
|
170
|
+
ir.config_options,
|
|
171
|
+
right_count,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
left = _maybe_shuffle_frame(
|
|
175
|
+
left,
|
|
176
|
+
ir.left_on,
|
|
177
|
+
partition_info,
|
|
178
|
+
ir.config_options,
|
|
179
|
+
left_count,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
new_node = ir.reconstruct([left, right])
|
|
183
|
+
partition_info[new_node] = PartitionInfo(count=output_count)
|
|
184
|
+
return new_node, partition_info
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@lower_ir_node.register(ConditionalJoin)
|
|
188
|
+
def _(
|
|
189
|
+
ir: ConditionalJoin, rec: LowerIRTransformer
|
|
190
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
191
|
+
if ir.options[2]: # pragma: no cover
|
|
192
|
+
return _lower_ir_fallback(
|
|
193
|
+
ir,
|
|
194
|
+
rec,
|
|
195
|
+
msg="Slice not supported in ConditionalJoin for multiple partitions.",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Lower children
|
|
199
|
+
left, right = ir.children
|
|
200
|
+
left, pi_left = rec(left)
|
|
201
|
+
right, pi_right = rec(right)
|
|
202
|
+
|
|
203
|
+
# Fallback to single partition on the smaller table
|
|
204
|
+
left_count = pi_left[left].count
|
|
205
|
+
right_count = pi_right[right].count
|
|
206
|
+
output_count = max(left_count, right_count)
|
|
207
|
+
fallback_msg = "ConditionalJoin not supported for multiple partitions."
|
|
208
|
+
if left_count < right_count:
|
|
209
|
+
if left_count > 1:
|
|
210
|
+
left = Repartition(left.schema, left)
|
|
211
|
+
pi_left[left] = PartitionInfo(count=1)
|
|
212
|
+
_fallback_inform(fallback_msg, rec.state["config_options"])
|
|
213
|
+
elif right_count > 1:
|
|
214
|
+
right = Repartition(left.schema, right)
|
|
215
|
+
pi_right[right] = PartitionInfo(count=1)
|
|
216
|
+
_fallback_inform(fallback_msg, rec.state["config_options"])
|
|
217
|
+
|
|
218
|
+
# Reconstruct and return
|
|
219
|
+
new_node = ir.reconstruct([left, right])
|
|
220
|
+
partition_info = reduce(operator.or_, (pi_left, pi_right))
|
|
221
|
+
partition_info[new_node] = PartitionInfo(count=output_count)
|
|
222
|
+
return new_node, partition_info
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@lower_ir_node.register(Join)
|
|
226
|
+
def _(
|
|
227
|
+
ir: Join, rec: LowerIRTransformer
|
|
228
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
229
|
+
# Lower children
|
|
230
|
+
children, _partition_info = zip(*(rec(c) for c in ir.children), strict=True)
|
|
231
|
+
partition_info = reduce(operator.or_, _partition_info)
|
|
232
|
+
|
|
233
|
+
left, right = children
|
|
234
|
+
output_count = max(partition_info[left].count, partition_info[right].count)
|
|
235
|
+
if output_count == 1:
|
|
236
|
+
new_node = ir.reconstruct(children)
|
|
237
|
+
partition_info[new_node] = PartitionInfo(count=1)
|
|
238
|
+
return new_node, partition_info
|
|
239
|
+
elif ir.options[0] == "Cross": # pragma: no cover
|
|
240
|
+
return _lower_ir_fallback(
|
|
241
|
+
ir, rec, msg="Cross join not support for multiple partitions."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if _should_bcast_join(ir, left, right, partition_info, output_count):
|
|
245
|
+
# Create a broadcast join
|
|
246
|
+
return _make_bcast_join(
|
|
247
|
+
ir,
|
|
248
|
+
output_count,
|
|
249
|
+
partition_info,
|
|
250
|
+
left,
|
|
251
|
+
right,
|
|
252
|
+
)
|
|
253
|
+
else:
|
|
254
|
+
# Create a hash join
|
|
255
|
+
return _make_hash_join(
|
|
256
|
+
ir,
|
|
257
|
+
output_count,
|
|
258
|
+
partition_info,
|
|
259
|
+
left,
|
|
260
|
+
right,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@generate_ir_tasks.register(Join)
|
|
265
|
+
def _(
|
|
266
|
+
ir: Join, partition_info: MutableMapping[IR, PartitionInfo]
|
|
267
|
+
) -> MutableMapping[Any, Any]:
|
|
268
|
+
left, right = ir.children
|
|
269
|
+
output_count = partition_info[ir].count
|
|
270
|
+
|
|
271
|
+
left_partitioned = (
|
|
272
|
+
partition_info[left].partitioned_on == ir.left_on
|
|
273
|
+
and partition_info[left].count == output_count
|
|
274
|
+
)
|
|
275
|
+
right_partitioned = (
|
|
276
|
+
partition_info[right].partitioned_on == ir.right_on
|
|
277
|
+
and partition_info[right].count == output_count
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if output_count == 1 or (left_partitioned and right_partitioned):
|
|
281
|
+
# Partition-wise join
|
|
282
|
+
left_name = get_key_name(left)
|
|
283
|
+
right_name = get_key_name(right)
|
|
284
|
+
return {
|
|
285
|
+
key: (
|
|
286
|
+
ir.do_evaluate,
|
|
287
|
+
*ir._non_child_args,
|
|
288
|
+
(left_name, i),
|
|
289
|
+
(right_name, i),
|
|
290
|
+
)
|
|
291
|
+
for i, key in enumerate(partition_info[ir].keys(ir))
|
|
292
|
+
}
|
|
293
|
+
else:
|
|
294
|
+
# Broadcast join
|
|
295
|
+
left_parts = partition_info[left]
|
|
296
|
+
right_parts = partition_info[right]
|
|
297
|
+
if left_parts.count >= right_parts.count:
|
|
298
|
+
small_side = "Right"
|
|
299
|
+
small_name = get_key_name(right)
|
|
300
|
+
small_size = partition_info[right].count
|
|
301
|
+
large_name = get_key_name(left)
|
|
302
|
+
large_on = ir.left_on
|
|
303
|
+
else:
|
|
304
|
+
small_side = "Left"
|
|
305
|
+
small_name = get_key_name(left)
|
|
306
|
+
small_size = partition_info[left].count
|
|
307
|
+
large_name = get_key_name(right)
|
|
308
|
+
large_on = ir.right_on
|
|
309
|
+
|
|
310
|
+
graph: MutableMapping[Any, Any] = {}
|
|
311
|
+
|
|
312
|
+
out_name = get_key_name(ir)
|
|
313
|
+
out_size = partition_info[ir].count
|
|
314
|
+
split_name = f"split-{out_name}"
|
|
315
|
+
getit_name = f"getit-{out_name}"
|
|
316
|
+
inter_name = f"inter-{out_name}"
|
|
317
|
+
|
|
318
|
+
for part_out in range(out_size):
|
|
319
|
+
if ir.options[0] != "Inner":
|
|
320
|
+
graph[(split_name, part_out)] = (
|
|
321
|
+
_partition_dataframe,
|
|
322
|
+
(large_name, part_out),
|
|
323
|
+
large_on,
|
|
324
|
+
small_size,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
_concat_list = []
|
|
328
|
+
for j in range(small_size):
|
|
329
|
+
left_key: tuple[str, int] | tuple[str, int, int]
|
|
330
|
+
if ir.options[0] != "Inner":
|
|
331
|
+
left_key = (getit_name, part_out, j)
|
|
332
|
+
graph[left_key] = (operator.getitem, (split_name, part_out), j)
|
|
333
|
+
else:
|
|
334
|
+
left_key = (large_name, part_out)
|
|
335
|
+
join_children = [left_key, (small_name, j)]
|
|
336
|
+
if small_side == "Left":
|
|
337
|
+
join_children.reverse()
|
|
338
|
+
|
|
339
|
+
inter_key = (inter_name, part_out, j)
|
|
340
|
+
graph[(inter_name, part_out, j)] = (
|
|
341
|
+
ir.do_evaluate,
|
|
342
|
+
ir.left_on,
|
|
343
|
+
ir.right_on,
|
|
344
|
+
ir.options,
|
|
345
|
+
*join_children,
|
|
346
|
+
)
|
|
347
|
+
_concat_list.append(inter_key)
|
|
348
|
+
if len(_concat_list) == 1:
|
|
349
|
+
graph[(out_name, part_out)] = graph.pop(_concat_list[0])
|
|
350
|
+
else:
|
|
351
|
+
graph[(out_name, part_out)] = (_concat, *_concat_list)
|
|
352
|
+
|
|
353
|
+
return graph
|
|
@@ -1,59 +1,61 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES.
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES.
|
|
2
2
|
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
"""Multi-partition
|
|
3
|
+
"""Multi-partition evaluation."""
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import itertools
|
|
8
8
|
import operator
|
|
9
|
-
from functools import reduce
|
|
9
|
+
from functools import partial, reduce
|
|
10
10
|
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
|
+
import cudf_polars.experimental.distinct
|
|
13
|
+
import cudf_polars.experimental.groupby
|
|
12
14
|
import cudf_polars.experimental.io
|
|
13
|
-
import cudf_polars.experimental.
|
|
14
|
-
|
|
15
|
+
import cudf_polars.experimental.join
|
|
16
|
+
import cudf_polars.experimental.select
|
|
17
|
+
import cudf_polars.experimental.shuffle
|
|
18
|
+
import cudf_polars.experimental.sort # noqa: F401
|
|
19
|
+
from cudf_polars.dsl.ir import (
|
|
20
|
+
IR,
|
|
21
|
+
Cache,
|
|
22
|
+
Filter,
|
|
23
|
+
HConcat,
|
|
24
|
+
HStack,
|
|
25
|
+
MapFunction,
|
|
26
|
+
Projection,
|
|
27
|
+
Union,
|
|
28
|
+
)
|
|
15
29
|
from cudf_polars.dsl.traversal import CachingVisitor, traversal
|
|
16
|
-
from cudf_polars.experimental.base import PartitionInfo,
|
|
30
|
+
from cudf_polars.experimental.base import PartitionInfo, get_key_name
|
|
17
31
|
from cudf_polars.experimental.dispatch import (
|
|
18
32
|
generate_ir_tasks,
|
|
19
33
|
lower_ir_node,
|
|
20
34
|
)
|
|
35
|
+
from cudf_polars.experimental.utils import _concat, _lower_ir_fallback
|
|
21
36
|
|
|
22
37
|
if TYPE_CHECKING:
|
|
23
38
|
from collections.abc import MutableMapping
|
|
39
|
+
from typing import Any
|
|
24
40
|
|
|
25
41
|
from cudf_polars.containers import DataFrame
|
|
26
42
|
from cudf_polars.experimental.dispatch import LowerIRTransformer
|
|
43
|
+
from cudf_polars.utils.config import ConfigOptions
|
|
27
44
|
|
|
28
45
|
|
|
29
46
|
@lower_ir_node.register(IR)
|
|
30
|
-
def _(
|
|
47
|
+
def _(
|
|
48
|
+
ir: IR, rec: LowerIRTransformer
|
|
49
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]: # pragma: no cover
|
|
31
50
|
# Default logic - Requires single partition
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return ir, {
|
|
36
|
-
ir: PartitionInfo(count=1)
|
|
37
|
-
} # pragma: no cover; Missed by pylibcudf executor
|
|
38
|
-
|
|
39
|
-
# Lower children
|
|
40
|
-
children, _partition_info = zip(*(rec(c) for c in ir.children), strict=True)
|
|
41
|
-
partition_info = reduce(operator.or_, _partition_info)
|
|
42
|
-
|
|
43
|
-
# Check that child partitioning is supported
|
|
44
|
-
if any(partition_info[c].count > 1 for c in children):
|
|
45
|
-
raise NotImplementedError(
|
|
46
|
-
f"Class {type(ir)} does not support multiple partitions."
|
|
47
|
-
) # pragma: no cover
|
|
48
|
-
|
|
49
|
-
# Return reconstructed node and partition-info dict
|
|
50
|
-
partition = PartitionInfo(count=1)
|
|
51
|
-
new_node = ir.reconstruct(children)
|
|
52
|
-
partition_info[new_node] = partition
|
|
53
|
-
return new_node, partition_info
|
|
51
|
+
return _lower_ir_fallback(
|
|
52
|
+
ir, rec, msg=f"Class {type(ir)} does not support multiple partitions."
|
|
53
|
+
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
-
def lower_ir_graph(
|
|
56
|
+
def lower_ir_graph(
|
|
57
|
+
ir: IR, config_options: ConfigOptions
|
|
58
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
57
59
|
"""
|
|
58
60
|
Rewrite an IR graph and extract partitioning information.
|
|
59
61
|
|
|
@@ -61,6 +63,8 @@ def lower_ir_graph(ir: IR) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
|
61
63
|
----------
|
|
62
64
|
ir
|
|
63
65
|
Root of the graph to rewrite.
|
|
66
|
+
config_options
|
|
67
|
+
GPUEngine configuration options.
|
|
64
68
|
|
|
65
69
|
Returns
|
|
66
70
|
-------
|
|
@@ -77,7 +81,7 @@ def lower_ir_graph(ir: IR) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
|
77
81
|
--------
|
|
78
82
|
lower_ir_node
|
|
79
83
|
"""
|
|
80
|
-
mapper = CachingVisitor(lower_ir_node)
|
|
84
|
+
mapper = CachingVisitor(lower_ir_node, state={"config_options": config_options})
|
|
81
85
|
return mapper(ir)
|
|
82
86
|
|
|
83
87
|
|
|
@@ -119,48 +123,118 @@ def task_graph(
|
|
|
119
123
|
key_name = get_key_name(ir)
|
|
120
124
|
partition_count = partition_info[ir].count
|
|
121
125
|
if partition_count > 1:
|
|
122
|
-
graph[key_name] = (_concat,
|
|
126
|
+
graph[key_name] = (_concat, *partition_info[ir].keys(ir))
|
|
123
127
|
return graph, key_name
|
|
124
128
|
else:
|
|
125
129
|
return graph, (key_name, 0)
|
|
126
130
|
|
|
127
131
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
# The true type signature for get_scheduler() needs an overload. Not worth it.
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_scheduler(config_options: ConfigOptions) -> Any:
|
|
136
|
+
"""Get appropriate task scheduler."""
|
|
137
|
+
assert config_options.executor.name == "streaming", (
|
|
138
|
+
"'in-memory' executor not supported in 'generate_ir_tasks'"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
scheduler = config_options.executor.scheduler
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
scheduler == "distributed"
|
|
145
|
+
): # pragma: no cover; block depends on executor type and Distributed cluster
|
|
146
|
+
from distributed import get_client
|
|
147
|
+
|
|
148
|
+
from cudf_polars.experimental.dask_registers import DaskRegisterManager
|
|
149
|
+
|
|
150
|
+
client = get_client()
|
|
151
|
+
DaskRegisterManager.register_once()
|
|
152
|
+
DaskRegisterManager.run_on_cluster(client)
|
|
153
|
+
return client.get
|
|
154
|
+
elif scheduler == "synchronous":
|
|
155
|
+
from cudf_polars.experimental.scheduler import synchronous_scheduler
|
|
156
|
+
|
|
157
|
+
return synchronous_scheduler
|
|
158
|
+
else: # pragma: no cover
|
|
159
|
+
raise ValueError(f"{scheduler} not a supported scheduler option.")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def post_process_task_graph(
|
|
163
|
+
graph: MutableMapping[Any, Any],
|
|
164
|
+
key: str | tuple[str, int],
|
|
165
|
+
config_options: ConfigOptions,
|
|
166
|
+
) -> MutableMapping[Any, Any]:
|
|
167
|
+
"""
|
|
168
|
+
Post-process the task graph.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
graph
|
|
173
|
+
Task graph to post-process.
|
|
174
|
+
key
|
|
175
|
+
Output key for the graph.
|
|
176
|
+
config_options
|
|
177
|
+
GPUEngine configuration options.
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
graph
|
|
182
|
+
A Dask-compatible task graph.
|
|
183
|
+
"""
|
|
184
|
+
assert config_options.executor.name == "streaming", (
|
|
185
|
+
"'in-memory' executor not supported in 'post_process_task_graph'"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
if config_options.executor.rapidsmpf_spill: # pragma: no cover
|
|
189
|
+
from cudf_polars.experimental.spilling import wrap_dataframe_in_spillable
|
|
190
|
+
|
|
191
|
+
return wrap_dataframe_in_spillable(
|
|
192
|
+
graph, ignore_key=key, config_options=config_options
|
|
193
|
+
)
|
|
194
|
+
return graph
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def evaluate_streaming(ir: IR, config_options: ConfigOptions) -> DataFrame:
|
|
198
|
+
"""
|
|
199
|
+
Evaluate an IR graph with partitioning.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
ir
|
|
204
|
+
Logical plan to evaluate.
|
|
205
|
+
config_options
|
|
206
|
+
GPUEngine configuration options.
|
|
131
207
|
|
|
132
|
-
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
A cudf-polars DataFrame object.
|
|
211
|
+
"""
|
|
212
|
+
ir, partition_info = lower_ir_graph(ir, config_options)
|
|
133
213
|
|
|
134
214
|
graph, key = task_graph(ir, partition_info)
|
|
135
|
-
|
|
215
|
+
|
|
216
|
+
graph = post_process_task_graph(graph, key, config_options)
|
|
217
|
+
|
|
218
|
+
return get_scheduler(config_options)(graph, key)
|
|
136
219
|
|
|
137
220
|
|
|
138
221
|
@generate_ir_tasks.register(IR)
|
|
139
222
|
def _(
|
|
140
223
|
ir: IR, partition_info: MutableMapping[IR, PartitionInfo]
|
|
141
224
|
) -> MutableMapping[Any, Any]:
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
raise NotImplementedError(
|
|
146
|
-
f"Failed to generate multiple output tasks for {ir}."
|
|
147
|
-
) # pragma: no cover
|
|
148
|
-
|
|
149
|
-
child_names = []
|
|
150
|
-
for child in ir.children:
|
|
151
|
-
child_names.append(get_key_name(child))
|
|
152
|
-
if partition_info[child].count > 1:
|
|
153
|
-
raise NotImplementedError(
|
|
154
|
-
f"Failed to generate tasks for {ir} with child {child}."
|
|
155
|
-
) # pragma: no cover
|
|
156
|
-
|
|
157
|
-
key_name = get_key_name(ir)
|
|
225
|
+
# Generate pointwise (embarrassingly-parallel) tasks by default
|
|
226
|
+
child_names = [get_key_name(c) for c in ir.children]
|
|
227
|
+
bcast_child = [partition_info[c].count == 1 for c in ir.children]
|
|
158
228
|
return {
|
|
159
|
-
|
|
229
|
+
key: (
|
|
160
230
|
ir.do_evaluate,
|
|
161
231
|
*ir._non_child_args,
|
|
162
|
-
*
|
|
232
|
+
*[
|
|
233
|
+
(child_name, 0 if bcast_child[j] else i)
|
|
234
|
+
for j, child_name in enumerate(child_names)
|
|
235
|
+
],
|
|
163
236
|
)
|
|
237
|
+
for i, key in enumerate(partition_info[ir].keys(ir))
|
|
164
238
|
}
|
|
165
239
|
|
|
166
240
|
|
|
@@ -168,18 +242,16 @@ def _(
|
|
|
168
242
|
def _(
|
|
169
243
|
ir: Union, rec: LowerIRTransformer
|
|
170
244
|
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
245
|
+
# Check zlice
|
|
246
|
+
if ir.zlice is not None: # pragma: no cover
|
|
247
|
+
return _lower_ir_fallback(
|
|
248
|
+
ir, rec, msg="zlice is not supported for multiple partitions."
|
|
249
|
+
)
|
|
250
|
+
|
|
171
251
|
# Lower children
|
|
172
252
|
children, _partition_info = zip(*(rec(c) for c in ir.children), strict=True)
|
|
173
253
|
partition_info = reduce(operator.or_, _partition_info)
|
|
174
254
|
|
|
175
|
-
# Check zlice
|
|
176
|
-
if ir.zlice is not None: # pragma: no cover
|
|
177
|
-
if any(p[c].count > 1 for p, c in zip(children, _partition_info, strict=False)):
|
|
178
|
-
raise NotImplementedError("zlice is not supported for multiple partitions.")
|
|
179
|
-
new_node = ir.reconstruct(children)
|
|
180
|
-
partition_info[new_node] = PartitionInfo(count=1)
|
|
181
|
-
return new_node, partition_info
|
|
182
|
-
|
|
183
255
|
# Partition count is the sum of all child partitions
|
|
184
256
|
count = sum(partition_info[c].count for c in children)
|
|
185
257
|
|
|
@@ -202,8 +274,22 @@ def _(
|
|
|
202
274
|
}
|
|
203
275
|
|
|
204
276
|
|
|
277
|
+
@lower_ir_node.register(MapFunction)
|
|
278
|
+
def _(
|
|
279
|
+
ir: MapFunction, rec: LowerIRTransformer
|
|
280
|
+
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
281
|
+
# Allow pointwise operations
|
|
282
|
+
if ir.name in ("rename", "explode"):
|
|
283
|
+
return _lower_ir_pwise(ir, rec)
|
|
284
|
+
|
|
285
|
+
# Fallback for everything else
|
|
286
|
+
return _lower_ir_fallback(
|
|
287
|
+
ir, rec, msg=f"{ir.name} is not supported for multiple partitions."
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
205
291
|
def _lower_ir_pwise(
|
|
206
|
-
ir: IR, rec: LowerIRTransformer
|
|
292
|
+
ir: IR, rec: LowerIRTransformer, *, preserve_partitioning: bool = False
|
|
207
293
|
) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
|
|
208
294
|
# Lower a partition-wise (i.e. embarrassingly-parallel) IR node
|
|
209
295
|
|
|
@@ -213,41 +299,28 @@ def _lower_ir_pwise(
|
|
|
213
299
|
counts = {partition_info[c].count for c in children}
|
|
214
300
|
|
|
215
301
|
# Check that child partitioning is supported
|
|
216
|
-
if len(counts) > 1:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
302
|
+
if len(counts) > 1: # pragma: no cover
|
|
303
|
+
return _lower_ir_fallback(
|
|
304
|
+
ir,
|
|
305
|
+
rec,
|
|
306
|
+
msg=f"Class {type(ir)} does not support children with mismatched partition counts.",
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Preserve child partition_info if possible
|
|
310
|
+
if preserve_partitioning and len(children) == 1:
|
|
311
|
+
partition = partition_info[children[0]]
|
|
312
|
+
else:
|
|
313
|
+
partition = PartitionInfo(count=max(counts))
|
|
220
314
|
|
|
221
315
|
# Return reconstructed node and partition-info dict
|
|
222
|
-
partition = PartitionInfo(count=max(counts))
|
|
223
316
|
new_node = ir.reconstruct(children)
|
|
224
317
|
partition_info[new_node] = partition
|
|
225
318
|
return new_node, partition_info
|
|
226
319
|
|
|
227
320
|
|
|
228
|
-
|
|
321
|
+
_lower_ir_pwise_preserve = partial(_lower_ir_pwise, preserve_partitioning=True)
|
|
322
|
+
lower_ir_node.register(Projection, _lower_ir_pwise_preserve)
|
|
323
|
+
lower_ir_node.register(Filter, _lower_ir_pwise_preserve)
|
|
229
324
|
lower_ir_node.register(Cache, _lower_ir_pwise)
|
|
230
|
-
lower_ir_node.register(Filter, _lower_ir_pwise)
|
|
231
325
|
lower_ir_node.register(HStack, _lower_ir_pwise)
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
def _generate_ir_tasks_pwise(
|
|
235
|
-
ir: IR, partition_info: MutableMapping[IR, PartitionInfo]
|
|
236
|
-
) -> MutableMapping[Any, Any]:
|
|
237
|
-
# Generate partition-wise (i.e. embarrassingly-parallel) tasks
|
|
238
|
-
child_names = [get_key_name(c) for c in ir.children]
|
|
239
|
-
return {
|
|
240
|
-
key: (
|
|
241
|
-
ir.do_evaluate,
|
|
242
|
-
*ir._non_child_args,
|
|
243
|
-
*[(child_name, i) for child_name in child_names],
|
|
244
|
-
)
|
|
245
|
-
for i, key in enumerate(partition_info[ir].keys(ir))
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
generate_ir_tasks.register(Projection, _generate_ir_tasks_pwise)
|
|
250
|
-
generate_ir_tasks.register(Cache, _generate_ir_tasks_pwise)
|
|
251
|
-
generate_ir_tasks.register(Filter, _generate_ir_tasks_pwise)
|
|
252
|
-
generate_ir_tasks.register(HStack, _generate_ir_tasks_pwise)
|
|
253
|
-
generate_ir_tasks.register(Select, _generate_ir_tasks_pwise)
|
|
326
|
+
lower_ir_node.register(HConcat, _lower_ir_pwise)
|