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.
Files changed (63) hide show
  1. cudf_polars/VERSION +1 -1
  2. cudf_polars/callback.py +82 -65
  3. cudf_polars/containers/column.py +138 -7
  4. cudf_polars/containers/dataframe.py +26 -39
  5. cudf_polars/dsl/expr.py +3 -1
  6. cudf_polars/dsl/expressions/aggregation.py +27 -63
  7. cudf_polars/dsl/expressions/base.py +40 -72
  8. cudf_polars/dsl/expressions/binaryop.py +5 -41
  9. cudf_polars/dsl/expressions/boolean.py +25 -53
  10. cudf_polars/dsl/expressions/datetime.py +97 -17
  11. cudf_polars/dsl/expressions/literal.py +27 -33
  12. cudf_polars/dsl/expressions/rolling.py +110 -9
  13. cudf_polars/dsl/expressions/selection.py +8 -26
  14. cudf_polars/dsl/expressions/slicing.py +47 -0
  15. cudf_polars/dsl/expressions/sorting.py +5 -18
  16. cudf_polars/dsl/expressions/string.py +33 -36
  17. cudf_polars/dsl/expressions/ternary.py +3 -10
  18. cudf_polars/dsl/expressions/unary.py +35 -75
  19. cudf_polars/dsl/ir.py +749 -212
  20. cudf_polars/dsl/nodebase.py +8 -1
  21. cudf_polars/dsl/to_ast.py +5 -3
  22. cudf_polars/dsl/translate.py +319 -171
  23. cudf_polars/dsl/utils/__init__.py +8 -0
  24. cudf_polars/dsl/utils/aggregations.py +292 -0
  25. cudf_polars/dsl/utils/groupby.py +97 -0
  26. cudf_polars/dsl/utils/naming.py +34 -0
  27. cudf_polars/dsl/utils/replace.py +46 -0
  28. cudf_polars/dsl/utils/rolling.py +113 -0
  29. cudf_polars/dsl/utils/windows.py +186 -0
  30. cudf_polars/experimental/base.py +17 -19
  31. cudf_polars/experimental/benchmarks/__init__.py +4 -0
  32. cudf_polars/experimental/benchmarks/pdsh.py +1279 -0
  33. cudf_polars/experimental/dask_registers.py +196 -0
  34. cudf_polars/experimental/distinct.py +174 -0
  35. cudf_polars/experimental/explain.py +127 -0
  36. cudf_polars/experimental/expressions.py +521 -0
  37. cudf_polars/experimental/groupby.py +288 -0
  38. cudf_polars/experimental/io.py +58 -29
  39. cudf_polars/experimental/join.py +353 -0
  40. cudf_polars/experimental/parallel.py +166 -93
  41. cudf_polars/experimental/repartition.py +69 -0
  42. cudf_polars/experimental/scheduler.py +155 -0
  43. cudf_polars/experimental/select.py +92 -7
  44. cudf_polars/experimental/shuffle.py +294 -0
  45. cudf_polars/experimental/sort.py +45 -0
  46. cudf_polars/experimental/spilling.py +151 -0
  47. cudf_polars/experimental/utils.py +100 -0
  48. cudf_polars/testing/asserts.py +146 -6
  49. cudf_polars/testing/io.py +72 -0
  50. cudf_polars/testing/plugin.py +78 -76
  51. cudf_polars/typing/__init__.py +59 -6
  52. cudf_polars/utils/config.py +353 -0
  53. cudf_polars/utils/conversion.py +40 -0
  54. cudf_polars/utils/dtypes.py +22 -5
  55. cudf_polars/utils/timer.py +39 -0
  56. cudf_polars/utils/versions.py +5 -4
  57. {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info}/METADATA +10 -7
  58. cudf_polars_cu12-25.6.0.dist-info/RECORD +73 -0
  59. {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info}/WHEEL +1 -1
  60. cudf_polars/experimental/dask_serialize.py +0 -59
  61. cudf_polars_cu12-25.2.2.dist-info/RECORD +0 -48
  62. {cudf_polars_cu12-25.2.2.dist-info → cudf_polars_cu12-25.6.0.dist-info/licenses}/LICENSE +0 -0
  63. {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 Dask execution."""
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.select # noqa: F401
14
- from cudf_polars.dsl.ir import IR, Cache, Filter, HStack, Projection, Select, Union
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, _concat, get_key_name
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 _(ir: IR, rec: LowerIRTransformer) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
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
- if len(ir.children) == 0:
34
- # Default leaf node has single partition
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(ir: IR) -> tuple[IR, MutableMapping[IR, PartitionInfo]]:
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, list(partition_info[ir].keys(ir)))
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
- def evaluate_dask(ir: IR) -> DataFrame:
129
- """Evaluate an IR graph with Dask."""
130
- from dask import get
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
- ir, partition_info = lower_ir_graph(ir)
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
- return get(graph, key)
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
- # Single-partition default behavior.
143
- # This is used by `generate_ir_tasks` for all unregistered IR sub-types.
144
- if partition_info[ir].count > 1:
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
- (key_name, 0): (
229
+ key: (
160
230
  ir.do_evaluate,
161
231
  *ir._non_child_args,
162
- *((child_name, 0) for child_name in child_names),
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
- raise NotImplementedError(
218
- f"Class {type(ir)} does not support unbalanced partitions."
219
- ) # pragma: no cover
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
- lower_ir_node.register(Projection, _lower_ir_pwise)
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)