Skip to content

Commit 82c3bc0

Browse files
authored
Arm backend: Avoid repeated retracing in view-permute propagation (pytorch#23055)
Refresh fake-tensor metadata only for nodes crossed by each view or permute rewrite. This allows dependent rewrites to continue in the same scan instead of retracing the full graph after every rewrite wave. Visit nodes in reverse graph order during downward propagation so later transforms move first instead of blocking earlier transforms. Keep one final full retrace to validate and reconstruct the graph. On the SmolLM2 VGF export, avoiding repeated retracing reduced AOT compilation from 2h08m to 37m23s in the original measurement. Add coverage for metadata refresh, retrace scheduling, and reverse propagation order. AI-Assisted-by: Codex Signed-off-by: Xingguo Li <xingguo.li@arm.com>
1 parent fb67e17 commit 82c3bc0

2 files changed

Lines changed: 215 additions & 42 deletions

File tree

‎backends/arm/test/passes/test_propagate_permutes_views_pass.py‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Tuple
88

99
import pytest
10+
import sympy # type: ignore[import-untyped]
1011
import torch
1112
from executorch.backends.arm._passes import (
1213
MoveDataMovementOpsToSmallerDtypePass,
@@ -23,6 +24,8 @@
2324
)
2425
from executorch.exir import ExportedProgram
2526
from executorch.exir.dialects._ops import ops as exir_ops
27+
from torch._subclasses.fake_tensor import FakeTensorMode
28+
from torch.fx.experimental.symbolic_shapes import ShapeEnv
2629

2730
input_t = Tuple[torch.Tensor]
2831

@@ -334,6 +337,134 @@ def _run_pass_on_graph_module(
334337
return result.graph_module
335338

336339

340+
class _CountingDownPass(PropagateViewCopyPermuteDownPass):
341+
def __init__(self) -> None:
342+
super().__init__()
343+
self.retrace_count = 0
344+
self.local_refresh_count = 0
345+
346+
def _refresh_propagation_meta(self, moving_node, propagation_path):
347+
self.local_refresh_count += 1
348+
return super()._refresh_propagation_meta(moving_node, propagation_path)
349+
350+
def _retrace(self, graph_module):
351+
self.retrace_count += 1
352+
return super()._retrace(graph_module)
353+
354+
355+
def test_propagation_retraces_once_after_reaching_fixed_point() -> None:
356+
graph = torch.fx.Graph()
357+
x = graph.placeholder("x")
358+
x.meta["val"] = torch.empty((1, 2, 3, 4))
359+
permute = graph.call_function(PERMUTE, args=(x, [0, 2, 3, 1]))
360+
permute.meta["val"] = torch.empty((1, 3, 4, 2))
361+
relu = graph.call_function(RELU, args=(permute,))
362+
relu.meta["val"] = torch.empty((1, 3, 4, 2))
363+
neg = graph.call_function(NEG, args=(relu,))
364+
neg.meta["val"] = torch.empty((1, 3, 4, 2))
365+
graph.output(neg)
366+
graph_module = torch.fx.GraphModule(torch.nn.Module(), graph)
367+
propagation_pass = _CountingDownPass()
368+
369+
result = propagation_pass.call(graph_module)
370+
371+
assert result.modified
372+
assert propagation_pass.retrace_count == 1
373+
assert propagation_pass.local_refresh_count == 1
374+
375+
376+
@pytest.mark.parametrize("target", [VIEW, PERMUTE], ids=["view", "permute"])
377+
def test_local_refresh_preserves_symbolic_view_permute_meta(target) -> None:
378+
shape_env = ShapeEnv()
379+
batch = shape_env.create_symintnode(sympy.Symbol("batch"), hint=2)
380+
assert isinstance(batch, torch.SymInt)
381+
shape_env.constrain_symbol_range(batch.node.expr, compiler_min=1, compiler_max=8)
382+
383+
with FakeTensorMode(shape_env=shape_env, allow_non_fake_inputs=True):
384+
graph = torch.fx.Graph()
385+
x = graph.placeholder("x")
386+
x.meta["val"] = torch.empty((batch, 2, 3))
387+
if target == VIEW:
388+
transform = graph.call_function(target, args=(x, [batch, 6]))
389+
symbolic_axis = 0
390+
else:
391+
transform = graph.call_function(target, args=(x, [1, 0, 2]))
392+
symbolic_axis = 1
393+
transform.meta["val"] = torch.empty((2, 6))
394+
graph.output(transform)
395+
396+
PropagateViewCopyPermuteDownPass()._refresh_node_meta(transform)
397+
398+
output_dim = transform.meta["val"].shape[symbolic_axis]
399+
assert isinstance(output_dim, torch.SymInt)
400+
assert output_dim.node.expr == batch.node.expr
401+
402+
403+
def test_down_propagation_visits_nodes_in_reverse_order() -> None:
404+
graph = torch.fx.Graph()
405+
x = graph.placeholder("x")
406+
x.meta["val"] = torch.empty((1, 2, 3, 4))
407+
view = graph.call_function(VIEW, args=(x, [1, 2, 3, 4]))
408+
view.meta["val"] = torch.empty((1, 2, 3, 4))
409+
relu = graph.call_function(RELU, args=(view,))
410+
relu.meta["val"] = torch.empty((1, 2, 3, 4))
411+
permute = graph.call_function(PERMUTE, args=(relu, [0, 2, 3, 1]))
412+
permute.meta["val"] = torch.empty((1, 3, 4, 2))
413+
neg = graph.call_function(NEG, args=(permute,))
414+
neg.meta["val"] = torch.empty((1, 3, 4, 2))
415+
graph.output(neg)
416+
graph_module = torch.fx.GraphModule(torch.nn.Module(), graph)
417+
propagation_pass = PropagateViewCopyPermuteDownPass()
418+
419+
assert propagation_pass._propagate_iteration(graph_module)
420+
421+
targets = [
422+
node.target for node in graph_module.graph.nodes if node.op == "call_function"
423+
]
424+
assert targets == [RELU, NEG, VIEW, PERMUTE]
425+
assert not propagation_pass._propagate_iteration(graph_module)
426+
427+
428+
def test_down_propagation_refreshes_metadata_in_dependency_order() -> None:
429+
graph = torch.fx.Graph()
430+
x = graph.placeholder("x")
431+
x.meta["val"] = torch.empty((1, 2, 3, 4))
432+
permute = graph.call_function(PERMUTE, args=(x, [0, 2, 3, 1]))
433+
permute.meta["val"] = torch.empty((1, 3, 4, 2))
434+
relu = graph.call_function(RELU, args=(permute,))
435+
relu.meta["val"] = torch.empty((1, 3, 4, 2))
436+
neg = graph.call_function(NEG, args=(relu,))
437+
neg.meta["val"] = torch.empty((1, 3, 4, 2))
438+
graph.output(neg)
439+
torch.fx.GraphModule(torch.nn.Module(), graph)
440+
441+
assert PropagateViewCopyPermuteDownPass()._propagate(permute)
442+
443+
assert relu.meta["val"].shape == torch.Size((1, 2, 3, 4))
444+
assert neg.meta["val"].shape == torch.Size((1, 2, 3, 4))
445+
assert permute.meta["val"].shape == torch.Size((1, 3, 4, 2))
446+
447+
448+
def test_up_propagation_refreshes_metadata_in_dependency_order() -> None:
449+
graph = torch.fx.Graph()
450+
x = graph.placeholder("x")
451+
x.meta["val"] = torch.empty((1, 2, 3, 4))
452+
relu = graph.call_function(RELU, args=(x,))
453+
relu.meta["val"] = torch.empty((1, 2, 3, 4))
454+
neg = graph.call_function(NEG, args=(relu,))
455+
neg.meta["val"] = torch.empty((1, 2, 3, 4))
456+
permute = graph.call_function(PERMUTE, args=(neg, [0, 2, 3, 1]))
457+
permute.meta["val"] = torch.empty((1, 3, 4, 2))
458+
graph.output(permute)
459+
torch.fx.GraphModule(torch.nn.Module(), graph)
460+
461+
assert PropagateViewCopyPermuteUpPass()._propagate(permute)
462+
463+
assert permute.meta["val"].shape == torch.Size((1, 3, 4, 2))
464+
assert relu.meta["val"].shape == torch.Size((1, 3, 4, 2))
465+
assert neg.meta["val"].shape == torch.Size((1, 3, 4, 2))
466+
467+
337468
def _run_pass_on_graph(
338469
graph: torch.fx.Graph,
339470
pass_cls: type[ArmPass] = PropagateViewCopyPermuteUpPass,

‎backends/transforms/propagate_view_copy_permute_pass.py‎

Lines changed: 84 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@
2424
from 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
2728
from executorch.exir import ExportedProgram
2829
from executorch.exir.dialects._ops import ops as exir_ops
2930
from 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

Comments
 (0)