-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Add IR-fusion optimization pass to streaming cudf-polars executor #18797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
53 commits
Select commit
Hold shift + click to select a range
247eaaf
initial pass at IR fusion
rjzamora 2c882d7
add general task-fusion support
rjzamora 7c986bc
Merge remote-tracking branch 'upstream/branch-25.06' into ir-fusion
rjzamora 65bff68
experimental tests passing
rjzamora ecf0db5
deterministic partitioning
rjzamora 21ec311
Merge branch 'branch-25.06' into ir-fusion
rjzamora 3da460c
fix shuffle bug
rjzamora 6c57bfa
Merge branch 'ir-fusion' of github.com:rjzamora/cudf into ir-fusion
rjzamora 8695863
Merge remote-tracking branch 'upstream/branch-25.06' into ir-fusion
rjzamora 08e8205
roll back breaking changes
rjzamora ddab700
fix spilling
rjzamora 502ac9f
Merge branch 'branch-25.06' into ir-fusion
rjzamora ccb909f
avoid random in metadata sampling
rjzamora fddf093
avoid random in metadata sampling
rjzamora 24e4ce5
Merge branch 'ir-fusion' of github.com:rjzamora/cudf into ir-fusion
rjzamora 6db02ec
add test coverage for explain
rjzamora 768e56c
Merge branch 'branch-25.06' into ir-fusion
rjzamora a857850
refactor
rjzamora 981a25b
Merge branch 'ir-fusion' of github.com:rjzamora/cudf into ir-fusion
rjzamora c129bc3
avoid calling fuse_ir_graph unless task_fusion is True
rjzamora f9056ed
bump coverage
rjzamora 0e9bd96
Merge branch 'branch-25.06' into ir-fusion
rjzamora d03fc03
drop unnecessary assertion
rjzamora 5e014db
fix strict=False added by pre-commit
rjzamora c4751d2
address review
rjzamora deb549b
improve comments
rjzamora 558aad8
Merge branch 'branch-25.06' into ir-fusion
rjzamora 7cf61ef
Merge branch 'branch-25.06' into ir-fusion
rjzamora d07c350
avoid fusing Sort nodes for now
rjzamora 00e8098
pre-commit
rjzamora e34530b
Merge branch 'branch-25.06' into ir-fusion
rjzamora e6636b3
Merge remote-tracking branch 'upstream/branch-25.06' into ir-fusion
rjzamora 8c6efcf
cleanup
rjzamora 9daf47b
Merge remote-tracking branch 'upstream/branch-25.06' into ir-fusion
rjzamora 4fa356c
fix explain
rjzamora b5384f7
fix explain test
rjzamora 15f2332
fix typing import
rjzamora 772f584
add flag to pdsh.py
rjzamora 7e26603
Merge remote-tracking branch 'upstream/branch-25.06' into ir-fusion
rjzamora b68a241
fix obvious mistake
rjzamora d21da69
fix misunderstanding
rjzamora c5807e7
Merge branch 'branch-25.06' into ir-fusion
rjzamora 924b4e5
Merge remote-tracking branch 'upstream/branch-25.08' into ir-fusion
rjzamora 517c081
Merge remote-tracking branch 'upstream/branch-25.08' into ir-fusion
rjzamora 4da62d4
fix pdsh.py
rjzamora 28a6f8b
Merge remote-tracking branch 'upstream/branch-25.08' into ir-fusion
rjzamora 1c6b3a3
Merge branch 'branch-25.08' into ir-fusion
rjzamora 2ae7e08
Merge branch 'ir-fusion' of github.com:rjzamora/cudf into ir-fusion
rjzamora 5ad4b1a
Merge remote-tracking branch 'upstream/branch-25.08' into ir-fusion
rjzamora f60282f
address minor code-review comments
rjzamora 19cbf46
Merge remote-tracking branch 'upstream/branch-25.10' into ir-fusion
rjzamora 523f750
Merge branch 'branch-25.10' into ir-fusion
rjzamora 519ba25
Merge branch 'branch-25.10' into ir-fusion
rjzamora File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,270 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION & AFFILIATES. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| """IR node fusion.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import operator | ||
| from collections import defaultdict | ||
| from functools import reduce | ||
| from typing import TYPE_CHECKING, Any, TypeAlias, TypedDict | ||
|
|
||
| from cudf_polars.dsl.ir import ( | ||
| IR, | ||
| Cache, | ||
| Filter, | ||
| GroupBy, | ||
| HStack, | ||
| MapFunction, | ||
| Projection, | ||
| Select, | ||
| Union, | ||
| ) | ||
| from cudf_polars.dsl.traversal import CachingVisitor, traversal | ||
| from cudf_polars.experimental.dispatch import generate_ir_tasks | ||
| from cudf_polars.experimental.io import Scan, SplitScan | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Callable, MutableMapping, Sequence | ||
|
|
||
| from typing_extensions import Self | ||
|
|
||
| from cudf_polars.containers import DataFrame | ||
| from cudf_polars.experimental.base import PartitionInfo | ||
| from cudf_polars.typing import GenericTransformer, Schema | ||
|
|
||
|
|
||
| class State(TypedDict): | ||
| """ | ||
| State for fusing IR sub-graphs. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| unique_subplans | ||
| Set of unique sub-plans in the IR graph. | ||
| partition_info | ||
| Partition info of the IR graph. | ||
| """ | ||
|
|
||
| unique_subplans: set[IR] | ||
| partition_info: MutableMapping[IR, PartitionInfo] | ||
|
|
||
|
|
||
| IRFusor: TypeAlias = ( | ||
| "GenericTransformer[IR, tuple[IR, MutableMapping[IR, PartitionInfo]], State]" | ||
| ) | ||
| """Protocol for fusing IR nodes.""" | ||
|
|
||
|
|
||
| class Fused(IR): | ||
| """Fused IR node.""" | ||
|
|
||
| __slots__ = ("fusible", "subnodes") | ||
| _non_child = ("schema", "subnodes", "fusible") | ||
| subnodes: tuple[IR, ...] | ||
| """Fused sub-nodes.""" | ||
| fusible: bool | ||
| """Whether a parent may fuse to this node.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| schema: Schema, | ||
| subnodes: tuple[IR, ...], | ||
| fusible: bool, # noqa: FBT001 | ||
| *children: IR, | ||
| ): | ||
| self.schema = schema | ||
| self.subnodes = subnodes | ||
| self.fusible = fusible | ||
| self._non_child_args = ( | ||
| [node.do_evaluate for node in subnodes], | ||
| [tuple(node._non_child_args) for node in self.subnodes], | ||
| ) | ||
| self.children = children | ||
|
|
||
| def fuse_parent(self, parent: IR, *, fusible: bool = True) -> Self: | ||
| """Return a new IR node with a fused parent.""" | ||
| assert self.fusible, f"Fused node {self} is not fusible!" | ||
| return type(self)( | ||
| parent.schema, (*self.subnodes, parent), fusible, *self.children | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_ir(cls, ir: IR, *, fusible: bool = True) -> Self: | ||
| "Construct from a single IR node." | ||
| return cls(ir.schema, (ir,), fusible, *ir.children) | ||
|
|
||
| @classmethod | ||
| def do_evaluate( | ||
| cls, | ||
| funcs: Sequence[Callable], | ||
| subargs: Sequence[Sequence[Any]], | ||
| *children: DataFrame, | ||
| ) -> DataFrame: | ||
| """Evaluate and return a dataframe.""" | ||
| for func, args in zip(funcs, subargs, strict=True): | ||
| children = (func(*args, *children),) | ||
| return children[0] | ||
|
|
||
|
|
||
| class FusedIO(IR): | ||
| """Fused-IO IR node.""" | ||
|
|
||
| __slots__ = ("fused_io", "fusible", "subnodes") | ||
| _non_child = ("schema", "subnodes", "fused_io", "fusible") | ||
| subnodes: tuple[IR, ...] | ||
| """Fused sub-nodes.""" | ||
| fused_io: Union | ||
| """Fused IO node.""" | ||
| fusible: bool | ||
| """Whether a parent may fuse to this node.""" | ||
|
|
||
| def __init__( | ||
| self, | ||
| schema: Schema, | ||
| subnodes: tuple[IR, ...], | ||
| fused_io: Union, | ||
| fusible: bool, # noqa: FBT001 | ||
| ): | ||
| self.schema = schema | ||
| self.subnodes = subnodes | ||
| self.fused_io = fused_io | ||
| self.fusible = fusible | ||
| self._non_child_args = ( | ||
| [node.do_evaluate for node in subnodes], | ||
| [tuple(node._non_child_args) for node in self.subnodes], | ||
| ) | ||
| self.children = () | ||
|
|
||
| def fuse_parent(self, parent: IR, *, fusible: bool = True) -> Self: | ||
| """Return a new IR node with a fused parent.""" | ||
| assert self.fusible, f"FusedIO node {self} is not fusible!" | ||
| return type(self)( | ||
| parent.schema, | ||
| (*self.subnodes, parent), | ||
| self.fused_io, | ||
| fusible, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def from_ir(cls, ir: Union, *, fusible: bool = True) -> Self: | ||
| "Construct from a single IR node." | ||
| return cls(ir.schema, (), ir, fusible) | ||
|
|
||
| @classmethod | ||
| def do_evaluate( | ||
| cls, | ||
| funcs: Sequence[Callable], | ||
| subargs: Sequence[Sequence[Any]], | ||
| io_cls: type[Scan | SplitScan], | ||
| io_args: Sequence[Any], | ||
| ) -> DataFrame: | ||
| """Evaluate and return a dataframe.""" | ||
| return Fused.do_evaluate(funcs, subargs, io_cls.do_evaluate(*io_args)) | ||
|
|
||
|
|
||
| def _maybe_make_fused( | ||
| ir: IR, | ||
| children: Sequence[IR], | ||
| partition_info: MutableMapping[IR, PartitionInfo], | ||
| *, | ||
| fusible: bool = True, | ||
| ) -> IR: | ||
| """Return a Fused node if fusion is allowed.""" | ||
| new_node: IR | ||
| if ( | ||
| isinstance(ir, Union) | ||
| and all(isinstance(n, (Scan, SplitScan)) for n in ir.children) | ||
| and partition_info[ir].count == len(ir.children) | ||
| ): | ||
| # IO Fusion. | ||
| # Multi-partition IO is always implemented as a Union | ||
| # over Scan/SplitScan nodes. The number of children | ||
| # must correspond to the number of partitions. | ||
| new_node = FusedIO.from_ir(ir, fusible=fusible) | ||
| elif isinstance( | ||
| ir, | ||
| (Cache, Filter, GroupBy, HStack, MapFunction, Projection, Select), | ||
| ): | ||
| # Basic fusion. | ||
| # These are partition-wise operations if they exist | ||
| # in the graph after lowering (the data-movement | ||
| # has been encoded with Shuffle/Reduction/etc). | ||
| if isinstance(children[0], (Fused, FusedIO)) and children[0].fusible: | ||
| new_node = children[0].fuse_parent(ir, fusible=fusible) | ||
| else: | ||
| new_node = Fused.from_ir(ir, fusible=fusible).reconstruct(children) | ||
| else: | ||
| new_node = ir.reconstruct(children) | ||
|
|
||
| return new_node | ||
|
|
||
|
|
||
| def _fuse_ir_node(ir: IR, rec: IRFusor) -> tuple[IR, MutableMapping[IR, PartitionInfo]]: | ||
| partition_info: MutableMapping[IR, PartitionInfo] = {} | ||
| children = () | ||
| if ir.children: | ||
| children, _partition_info = zip(*(rec(c) for c in ir.children), strict=True) | ||
| partition_info = reduce(operator.or_, _partition_info) | ||
|
|
||
| new_node = _maybe_make_fused( | ||
| ir, | ||
| children, | ||
| rec.state["partition_info"], | ||
| # The new Fused node may only be fused by its | ||
| # parent if it corresponds to a unique sub-plan. | ||
| fusible=ir in rec.state["unique_subplans"], | ||
| ) | ||
| partition_info[new_node] = rec.state["partition_info"][ir] | ||
| return new_node, partition_info | ||
|
|
||
|
|
||
| @generate_ir_tasks.register(FusedIO) | ||
| def _( | ||
| ir: FusedIO, partition_info: MutableMapping[IR, PartitionInfo] | ||
| ) -> MutableMapping[Any, Any]: | ||
| graph: MutableMapping[Any, Any] = {} | ||
| for i, key in enumerate(partition_info[ir].keys(ir)): | ||
| io = ir.fused_io.children[i] | ||
|
TomAugspurger marked this conversation as resolved.
|
||
| assert isinstance(io, (Scan, SplitScan)) | ||
| graph[key] = ( | ||
| ir.do_evaluate, | ||
| *ir._non_child_args, | ||
| type(io), | ||
| list(io._non_child_args), | ||
| ) | ||
| return graph | ||
|
|
||
|
|
||
| def fuse_ir_graph( | ||
| ir: IR, | ||
| partition_info: MutableMapping[IR, PartitionInfo], | ||
| ) -> tuple[IR, MutableMapping[IR, PartitionInfo]]: | ||
| """ | ||
| Rewrite an IR graph with fused nodes. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| ir | ||
| Root of the lowered-IR graph to rewrite. | ||
| partition_info | ||
| Initial partitioning information. | ||
|
|
||
| Returns | ||
| ------- | ||
| new_ir, partition_info | ||
| The rewritten graph, and a mapping from unique nodes | ||
| in the new graph to associated partitioning information. | ||
| """ | ||
| parents: defaultdict[IR, int] = defaultdict(int) | ||
| parents[ir] = 1 | ||
| for node in traversal([ir]): | ||
| for child in node.children: | ||
| parents[child] += 1 | ||
| unique_subplans = {node for node, count in parents.items() if count == 1} | ||
| state: State = { | ||
| "unique_subplans": unique_subplans, | ||
| "partition_info": partition_info, | ||
| } | ||
| mapper: IRFusor = CachingVisitor(_fuse_ir_node, state=state) | ||
| return mapper(ir) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.