1111
1212import torch
1313from executorch .exir .dialects ._ops import ops as exir_ops
14+ from executorch .exir .dialects .edge ._ops import EdgeOpOverload
1415from executorch .exir .pass_base import ExportPass , PassResult
1516
1617
1718class 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