2424from executorch .backends .transforms .fuse_identical_input_transforms_pass import (
2525 FuseIdenticalInputTransformsPass ,
2626)
27+ from executorch .backends .transforms .permute_view_meta import refresh_permute_view_meta
2728from executorch .exir import ExportedProgram
2829from executorch .exir .dialects ._ops import ops as exir_ops
2930from executorch .exir .pass_base import ExportPass , PassResult
31+ from torch ._subclasses .fake_tensor import FakeTensor , FakeTensorMode
32+ from torch .fx .node import map_arg
3033
3134
3235@dataclass (frozen = True )
@@ -113,22 +116,10 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
113116 graph_module = self ._retrace (graph_module )
114117
115118 while True :
116- iteration_modified = False
117- # A rewrite invalidates metadata along its path and downstream.
118- # Defer that region while still batching independent branches.
119- stale_nodes : set [torch .fx .Node ] = set ()
120- for node in list (graph_module .graph .nodes ):
121- if node in stale_nodes :
122- continue
123- if node .target in self ._targets :
124- if len (node .users ) == 0 :
125- continue
126- if self ._propagate (node , stale_nodes ):
127- iteration_modified = True
119+ iteration_modified = self ._propagate_iteration (graph_module )
128120
129121 if iteration_modified :
130122 modified = True
131- graph_module = self ._retrace (graph_module )
132123 continue
133124
134125 result = self .fuse_horizontal (graph_module )
@@ -147,36 +138,69 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult:
147138
148139 return PassResult (graph_module , modified )
149140
150- @staticmethod
151- def _mark_downstream_nodes_stale (
152- node : torch .fx .Node , stale_nodes : set [torch .fx .Node ]
153- ) -> None :
154- pending = [node ]
155- while pending :
156- current = pending .pop ()
157- if current in stale_nodes :
141+ def _propagate_iteration (
142+ self ,
143+ graph_module : torch .fx .GraphModule ,
144+ ) -> bool :
145+ modified = False
146+ for node in self ._propagation_order (graph_module ):
147+ if node .graph is None :
158148 continue
159- stale_nodes .add (current )
160- pending .extend (current .users )
149+ if node .target not in self ._targets or len (node .users ) == 0 :
150+ continue
151+ modified |= self ._propagate (node )
152+ return modified
161153
162- def _mark_stale_region (
163- self ,
164- nodes : Iterable [torch .fx .Node ],
165- stale_nodes : set [torch .fx .Node ],
166- ) -> None :
167- for node in nodes :
168- self ._mark_downstream_nodes_stale (node , stale_nodes )
154+ def _propagation_order (
155+ self , graph_module : torch .fx .GraphModule
156+ ) -> Iterable [torch .fx .Node ]:
157+ return list (graph_module .graph .nodes )
169158
170159 def _retrace (self , graph_module : torch .fx .GraphModule ) -> torch .fx .GraphModule :
171160 graph_module .graph .eliminate_dead_code ()
172161 graph_module .graph .lint ()
173162 return super ().call (graph_module ).graph_module
174163
164+ def _refresh_node_meta (self , node : torch .fx .Node ) -> None :
165+ if not node .all_input_nodes :
166+ return
167+ if node .target in {self ._VIEW_TARGET , self ._PERMUTE_TARGET }:
168+ # Derive copy-op shapes from the input so existing SymInts retain
169+ # their identity instead of being recreated by fake execution.
170+ refresh_permute_view_meta (node )
171+ return
172+
173+ args = map_arg (node .args , lambda input_node : input_node .meta ["val" ])
174+ kwargs = map_arg (node .kwargs , lambda input_node : input_node .meta ["val" ])
175+ fake_mode = next (
176+ (
177+ value .fake_mode
178+ for input_node in node .all_input_nodes
179+ if isinstance ((value := input_node .meta .get ("val" )), FakeTensor )
180+ ),
181+ None ,
182+ )
183+ if fake_mode is None :
184+ fake_mode = FakeTensorMode (allow_non_fake_inputs = True )
185+ args = torch .utils ._pytree .tree_map_only (
186+ torch .Tensor , fake_mode .from_tensor , args
187+ )
188+ kwargs = torch .utils ._pytree .tree_map_only (
189+ torch .Tensor , fake_mode .from_tensor , kwargs
190+ )
191+ with fake_mode :
192+ node .meta ["val" ] = node .target (* args , ** kwargs ) # type: ignore[operator]
193+
194+ @abstractmethod
195+ def _refresh_propagation_meta (
196+ self , moving_node : torch .fx .Node , propagation_path : Sequence [torch .fx .Node ]
197+ ) -> None :
198+ pass
199+
175200 def _validated_next_nodes (
176201 self ,
177202 node : torch .fx .Node ,
178203 frontier : torch .fx .Node ,
179- stale_nodes : set [torch .fx .Node ],
180204 ) -> list [torch .fx .Node ] | None :
181205 next_nodes = list (self ._get_next_nodes (frontier ))
182206 if not next_nodes :
@@ -186,9 +210,6 @@ def _validated_next_nodes(
186210 ), f"{ self .__class__ .__name__ } reached an endpoint node which is not a placeholder or output: { frontier } "
187211 return None
188212
189- if any (next_node in stale_nodes for next_node in next_nodes ):
190- return None
191-
192213 if not self ._can_cross_next_nodes (frontier , next_nodes ):
193214 return None
194215
@@ -217,17 +238,15 @@ def _advance_through_next_node(
217238 next_node .args = swapped_args [1 ]
218239 return True
219240
220- def _propagate (self , node : torch .fx .Node , stale_nodes : set [torch .fx .Node ]) -> bool :
221- """Propagate one node without consulting metadata invalidated this
222- scan.
223- """
241+ def _propagate (self , node : torch .fx .Node ) -> bool :
242+ """Propagate one node and refresh metadata changed by the rewrite."""
224243
225244 frontier = node
226245 previous_frontier = None
227246 propagation_path = [node ]
228247 moved = False
229248 while True :
230- next_nodes = self ._validated_next_nodes (node , frontier , stale_nodes )
249+ next_nodes = self ._validated_next_nodes (node , frontier )
231250 if next_nodes is None :
232251 break
233252
@@ -236,7 +255,7 @@ def _propagate(self, node: torch.fx.Node, stale_nodes: set[torch.fx.Node]) -> bo
236255 node , frontier , previous_frontier , next_nodes
237256 ):
238257 break
239- self ._mark_stale_region (( * propagation_path , * next_nodes ), stale_nodes )
258+ self ._refresh_propagation_meta ( node , propagation_path )
240259 return True
241260
242261 next_node = next_nodes [0 ]
@@ -251,7 +270,7 @@ def _propagate(self, node: torch.fx.Node, stale_nodes: set[torch.fx.Node]) -> bo
251270 # Perform the swap directly in this case and return.
252271 # Otherwise break and move the node before the concat
253272 if self ._maybe_split_upwards_cat_fanout (node , next_node ):
254- self ._mark_stale_region (( * propagation_path , next_node ), stale_nodes )
273+ self ._refresh_propagation_meta ( node , propagation_path )
255274 return True
256275
257276 # Unhandled case, stop propagation
@@ -262,7 +281,7 @@ def _propagate(self, node: torch.fx.Node, stale_nodes: set[torch.fx.Node]) -> bo
262281
263282 assert previous_frontier is not None
264283 self ._move_node (node , frontier , previous_frontier )
265- self ._mark_stale_region ( propagation_path , stale_nodes )
284+ self ._refresh_propagation_meta ( node , propagation_path )
266285 return True
267286
268287 def duplicate_user_fusion_exclusions (self ) -> frozenset :
@@ -513,6 +532,15 @@ def _shape_seen_by_next_node(self, moving_node: torch.fx.Node) -> Any:
513532 val = moving_node .meta .get ("val" )
514533 return getattr (val , "shape" , None )
515534
535+ def _refresh_propagation_meta (self , moving_node , propagation_path ) -> None :
536+ # After moving up, dependencies run from the transform through the
537+ # crossed nodes in reverse propagation order.
538+ if moving_node .graph is not None :
539+ self ._refresh_node_meta (moving_node )
540+ for node in reversed (propagation_path [1 :]):
541+ if node .graph is not None :
542+ self ._refresh_node_meta (node )
543+
516544 def fuse_horizontal (self , graph_module ):
517545 modified = False
518546 result = FuseDuplicateUsersPass (self .duplicate_user_fusion_exclusions ()).call (
@@ -815,6 +843,20 @@ def _shape_seen_by_next_node(self, moving_node: torch.fx.Node) -> Any:
815843 val = cast (torch .fx .Node , moving_node .args [0 ]).meta .get ("val" )
816844 return getattr (val , "shape" , None )
817845
846+ def _propagation_order (
847+ self , graph_module : torch .fx .GraphModule
848+ ) -> Iterable [torch .fx .Node ]:
849+ return reversed (list (graph_module .graph .nodes ))
850+
851+ def _refresh_propagation_meta (self , moving_node , propagation_path ) -> None :
852+ # After moving down, dependencies run through the crossed nodes in
853+ # propagation order and end at the transform.
854+ for node in propagation_path [1 :]:
855+ if node .graph is not None :
856+ self ._refresh_node_meta (node )
857+ if moving_node .graph is not None :
858+ self ._refresh_node_meta (moving_node )
859+
818860 def fuse_horizontal (self , graph_module ):
819861 modified = False
820862 result = FuseIdenticalInputTransformsPass (
0 commit comments