Skip to content

Commit 857039d

Browse files
authored
Optimize FuseViewCopyTransform pass (#20591)
This is an optimizing and refactoring patch targeting the FuseViewCopyTransform pass. Prior to this patch, the pass worked in two full iterations and recompilations of the graph: The first iteration updated view nodes, recompiled the graph and the second iteration removed any redundant view nodes. Optimize this pass to instead do the same work in just one graph iteration and compilation. The pass now works by finding fusible chains, removing redundant view nodes, and form updated node edges within a single iteration. The table below shows the improvement when lowering models on a computer using the TOSA FP profile. | Model | Before | After | Fuse delta | Total delta | |--------------|-------:|------:|----------------:|------------:| | mobilenet_v2 | 3.21s | 1.61s | -1.61s / -50.0% | -2.7% | | deit_tiny | 10.51s | 5.27s | -5.24s / -49.9% | -1.6% | | swin2sr | 2.55s | 1.26s | -1.29s / -50.7% | -1.2% | cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani Signed-off-by: Martin Lindström <Martin.Lindstroem@arm.com> Authored-by: Martin Lindström <Martin.Lindstroem@arm.com>
1 parent a406b49 commit 857039d

1 file changed

Lines changed: 76 additions & 55 deletions

File tree

backends/transforms/fuse_view_copy.py

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,35 @@
1111

1212
import torch
1313
from executorch.exir.dialects._ops import ops as exir_ops
14+
from executorch.exir.dialects.edge._ops import EdgeOpOverload
1415
from executorch.exir.pass_base import ExportPass, PassResult
1516

1617

1718
class FuseViewCopyTransform(ExportPass):
19+
"""Fuse redundant ``view_copy`` nodes through supported unary elementwise
20+
chains.
21+
22+
Example:
23+
let view(sN) be a view node with output shape sN
24+
25+
Input:
26+
view(s0) -> view(s1) -> abs -> view(s2) -> sqrt -> view(s3) -> user0
27+
`-----> user1
28+
Output:
29+
view(s3) -> abs -> sqrt -> user0
30+
`---> user1
31+
32+
Note that all view nodes except the first one have been elimininated.
33+
That first node now outputs the final view shape s3 instead. user0 and
34+
user1 have been reconnected to the last retained node sqrt because the
35+
former connection has been broken with the removal of the view nodes.
36+
"""
37+
1838
_passes_required_after: Set[Type[ExportPass]] = set()
1939

20-
VIEW_OP = exir_ops.edge.aten.view_copy.default
40+
VIEW_OP: EdgeOpOverload = exir_ops.edge.aten.view_copy.default
2141

22-
UNARY_ELEMENTWISE_OPS = [
42+
UNARY_ELEMENTWISE_OPS: list[EdgeOpOverload] = [
2343
exir_ops.edge.aten.alias_copy.default,
2444
exir_ops.edge.aten.clone.default,
2545
exir_ops.edge.dim_order_ops._clone_dim_order.default,
@@ -46,71 +66,72 @@ class FuseViewCopyTransform(ExportPass):
4666
exir_ops.edge.aten.log.default,
4767
]
4868

49-
def merge_view_copy_chains(
50-
self, graph: torch.fx.Graph
51-
) -> tuple[torch.fx.Graph, bool]:
52-
"""
53-
Find chains of view_copy nodes and unary elementwise ops and set all
54-
view_copy nodes to have the final shape. The views will then be removed
55-
by the remove_noop_view_copy call.
69+
def _find_view_copy_chain(
70+
self, start_node: torch.fx.Node, ops: list[EdgeOpOverload]
71+
) -> tuple[torch.fx.Node, list[torch.fx.Node], list[torch.fx.Node]]:
72+
"""Collect a fusible chain starting at ``start_node``.
5673
57-
Only merges view_copy nodes that are not used by any other nodes.
74+
Returns:
75+
list[torch.fx.Node]: View nodes following ``node`` that can be removed.
5876
"""
59-
view_op = self.VIEW_OP
60-
modified = False
61-
ops = self.UNARY_ELEMENTWISE_OPS + [view_op]
62-
for node in graph.nodes:
63-
if node.op == "call_function" and node.target == view_op:
64-
# Find a chain of unary elementwise ops and save all view_copy nodes
65-
end_node = node
66-
view_ops = [node]
67-
while (
68-
end_node.op == "call_function"
69-
and end_node.target in ops
70-
and len(end_node.users) == 1
71-
and list(end_node.users)[0].target in ops
72-
):
73-
end_node = list(end_node.users)[0]
74-
if end_node.target == view_op:
75-
view_ops.append(end_node)
76-
77-
# Set all view_copy nodes to have the final shape
78-
if len(view_ops) > 1:
79-
final_shape = view_ops[-1].args[1]
80-
for node in view_ops:
81-
new_args = (node.args[0], final_shape)
82-
node.args = new_args
83-
modified = True
84-
if modified:
85-
graph.eliminate_dead_code()
86-
return graph, modified
8777

88-
def remove_noop_view_copy(
78+
end_node = start_node
79+
view_nodes: list[torch.fx.Node] = []
80+
while (
81+
end_node.op == "call_function"
82+
and end_node.target in ops
83+
and len(end_node.users) == 1
84+
and (end_node_user := next(iter(end_node.users))).target in ops
85+
):
86+
if end_node_user.target == self.VIEW_OP:
87+
view_nodes.append(end_node_user)
88+
89+
end_node = end_node_user
90+
91+
return view_nodes
92+
93+
def _merge_view_copy_chains(
8994
self, graph: torch.fx.Graph
9095
) -> tuple[torch.fx.Graph, bool]:
96+
"""Merge redundant view nodes in linear chains.
97+
98+
For each view node, search forward through a single-user chain of view
99+
and supported unary elementwise ops. If the chain contains later view
100+
nodes, update the first view to the final view shape and bypass each
101+
later view by replacing its uses with its input. The view nodes are
102+
then eliminated.
103+
104+
Args:
105+
graph (torch.fx.Graph): Graph to rewrite.
106+
107+
Returns:
108+
tuple[torch.fx.Graph, bool]: The rewritten graph and whether it was
109+
modified.
91110
"""
92-
Remove view_copy nodes that are no-ops.
93-
"""
94-
view_op = self.VIEW_OP
95111
modified = False
96-
for node in graph.nodes:
97-
if node.op == "call_function" and node.target == view_op:
98-
input_shape = list(node.args[0].meta["val"].shape)
99-
target_shape = list(node.meta["val"].shape)
100-
if input_shape == target_shape:
101-
node.replace_all_uses_with(node.args[0])
102-
modified = True
112+
ops: list[EdgeOpOverload] = self.UNARY_ELEMENTWISE_OPS + [self.VIEW_OP]
113+
for node in graph.find_nodes(op="call_function", target=self.VIEW_OP):
114+
view_nodes_to_remove = self._find_view_copy_chain(node, ops)
115+
116+
if len(view_nodes_to_remove) > 0:
117+
modified = True
118+
119+
# Set the first view node in the chain to have the final shape
120+
final_shape = view_nodes_to_remove[-1].args[1]
121+
new_args = (node.args[0], final_shape)
122+
node.args = new_args
123+
124+
# Redirect output edges from removed view nodes to bypass them
125+
for view_node in view_nodes_to_remove:
126+
view_node.replace_all_uses_with(view_node.args[0])
127+
103128
if modified:
104129
graph.eliminate_dead_code()
130+
105131
return graph, modified
106132

107133
def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
108-
graph_module.graph, modified = self.merge_view_copy_chains(graph_module.graph)
109-
if modified:
110-
graph_module.recompile()
111-
graph_module = super().call(graph_module).graph_module
112-
113-
graph_module.graph, modified = self.remove_noop_view_copy(graph_module.graph)
134+
graph_module.graph, modified = self._merge_view_copy_chains(graph_module.graph)
114135
if modified:
115136
graph_module.recompile()
116137
graph_module = super().call(graph_module).graph_module

0 commit comments

Comments
 (0)