Skip to content

Commit 0c6ecea

Browse files
authored
Fold non-matching end permutes instead of discarding the region (pytorch#22293)
Differential Revision: D117939968 Pull Request resolved: pytorch#22293
1 parent 2cd8f26 commit 0c6ecea

3 files changed

Lines changed: 251 additions & 74 deletions

File tree

‎backends/cadence/aot/tests/test_remove_ops_passes.py‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,11 +477,21 @@ def test_keep_permutes_around_elemwise_ops_add(self) -> None:
477477
)
478478
builder.output([permute])
479479
original = builder.get_graph_module()
480+
gm_before = copy.deepcopy(original)
480481
p = RemovePermutesAroundElementwiseOps()
481482
graph_after_passes = cast(PassResult, p(original)).graph_module
482-
# Ensure no permutes were removed, since the dimensions don't fit the expected pattern
483+
# The end permute is not the inverse of the start one, so it cannot be
484+
# dropped. It absorbs the start permute instead, leaving one behind.
483485
self.assertEqual(
484-
count_node(graph_after_passes, exir_ops.edge.aten.permute_copy.default), 2
486+
count_node(graph_after_passes, exir_ops.edge.aten.permute_copy.default), 1
487+
)
488+
489+
sample_inputs = [torch.randn(1, 8, 4, 4, dtype=torch.float32)]
490+
validate(
491+
gm_before,
492+
graph_after_passes,
493+
sample_inputs,
494+
"RemovePermutesAroundElementwiseOps",
485495
)
486496

487497
def test_remove_permutes_around_elemwise_ops_add_mean(self) -> None:

‎backends/transforms/remove_permutes_around_elementwise_ops.py‎

Lines changed: 47 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ class Subgraph:
4141
edges_in: set[tuple[torch.fx.Node, torch.fx.Node]] = field(default_factory=set)
4242
# Outgoing edges of the subgraph to permute nodes.
4343
edges_out: set[tuple[torch.fx.Node, torch.fx.Node]] = field(default_factory=set)
44+
# Outgoing edges to permutes that do not match end_permute. Those are
45+
# kept and their permutation rewritten to absorb the removed start
46+
# permute, as (producer, permute node, new permutation).
47+
edges_out_to_update: set[
48+
tuple[torch.fx.Node, torch.fx.Node, tuple[int, ...]]
49+
] = field(default_factory=set)
4450
# Incoming edges from constant nodes that need a compensating permute.
4551
constant_edges_in: set[tuple[torch.fx.Node, torch.fx.Node]] = field(
4652
default_factory=set
@@ -372,7 +378,6 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
372378
# Expected end permutation for the subgraph.
373379
end_permute = [start_permute.index(i) for i in range(len(start_permute))]
374380

375-
# Try direct users first (same-rank matching)
376381
for user in node.users:
377382
if (
378383
not self.is_node_permutable(user)
@@ -385,50 +390,6 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: # noqa: C901
385390
for n in subgraph.nodes:
386391
processed_nodes.add(n)
387392

388-
# Also try: permute → view(squeeze/unsqueeze) → chain → ...
389-
# If the permute's sole user is a squeeze/unsqueeze view,
390-
# adapt the permutation across the view and search for a
391-
# matching end permute at the new rank.
392-
users = list(node.users.keys())
393-
if (
394-
len(users) == 1
395-
and self._is_squeeze_unsqueeze_view(users[0])
396-
and node not in processed_nodes
397-
):
398-
view_node = users[0]
399-
adapted_start = self._adapt_permute_across_view(
400-
start_permute, view_node
401-
)
402-
if adapted_start is not None:
403-
adapted_end = [
404-
adapted_start.index(i) for i in range(len(adapted_start))
405-
]
406-
for view_user in view_node.users:
407-
if (
408-
not self.is_node_permutable(view_user)
409-
and self._interleave_triple(view_user) is None
410-
):
411-
continue
412-
subgraph = self.Subgraph(adapted_start, adapted_end)
413-
# Include the view in the subgraph
414-
subgraph.nodes.add(view_node)
415-
subgraph.node_end_permute[view_node] = adapted_end
416-
# Use the ORIGINAL start_permute for the view node
417-
# so update_view_copy can remap its shape correctly
418-
subgraph.node_start_permute[view_node] = start_permute
419-
# The start permute feeds into the view
420-
subgraph.edges_in.add((node, view_node))
421-
if self.visit(
422-
view_user,
423-
subgraph,
424-
processed_nodes,
425-
adapted_end,
426-
adapted_start,
427-
):
428-
subgraphs_found.append(subgraph)
429-
for n in subgraph.nodes:
430-
processed_nodes.add(n)
431-
432393
modified = False
433394
for subgraph in subgraphs_found:
434395
if self.permute_subgraph(subgraph):
@@ -526,29 +487,17 @@ def visit( # noqa: C901
526487
if user_perm == downstream_end:
527488
subgraph.edges_out.add((users_source, user))
528489
else:
529-
# Check if permute → view(squeeze/unsqueeze) forms an
530-
# end boundary at a different rank.
531-
user_users = list(user.users.keys())
532-
if len(user_users) == 1 and self._is_squeeze_unsqueeze_view(
533-
user_users[0]
534-
):
535-
view_after: torch.fx.Node = user_users[0]
536-
# Adapt the start permute across the view and derive
537-
# the expected end permute as its inverse.
538-
adapted_start_after = self._adapt_permute_across_view(
539-
downstream_start, view_after
490+
# Non-matching permute: keep it and fold the start permute into it
491+
# rather than discarding the region.
492+
if user_perm is None or len(user_perm) != len(downstream_start):
493+
return False
494+
subgraph.edges_out_to_update.add(
495+
(
496+
users_source,
497+
user,
498+
tuple(downstream_start[d] for d in user_perm),
540499
)
541-
if adapted_start_after is not None:
542-
adapted = [
543-
adapted_start_after.index(i)
544-
for i in range(len(adapted_start_after))
545-
]
546-
if user_perm == adapted:
547-
# Include both the permute and the view as end edges
548-
subgraph.edges_out.add((users_source, user))
549-
# Mark the view for inclusion so it gets preserved
550-
continue
551-
return False
500+
)
552501
elif user.op == "output":
553502
return False
554503
elif self._is_permutation_sink_view(user):
@@ -682,11 +631,27 @@ def _is_constant_pad(self, node: torch.fx.Node) -> bool:
682631

683632
return True
684633

634+
def _removes_a_permute(self, subgraph: Subgraph) -> bool:
635+
"""Whether rewriting this region reduces the number of permutes."""
636+
if subgraph.edges_out:
637+
return True
638+
rewired = set(subgraph.edges_in)
639+
for permute, _ in subgraph.edges_in:
640+
if all((permute, user) in rewired for user in permute.users):
641+
return True
642+
return False
643+
685644
def permute_subgraph(self, subgraph: Subgraph) -> bool: # noqa: C901
686645
# Ensure that the subgraph's edges have not been modified by an earlier rewrite before applying changes.
687646
if not self._subgraph_edges_are_current(subgraph):
688647
return False
689648

649+
# Folding an end permute only pays for itself if some permute goes away.
650+
# Otherwise the region is rewritten for nothing, and the composed
651+
# permutation is a worse fusion candidate for the passes downstream.
652+
if subgraph.edges_out_to_update and not self._removes_a_permute(subgraph):
653+
return False
654+
690655
# Nodes belonging to a repeat_interleave triple are rewritten as a unit
691656
# below, so they must skip the per-node dim handling and the view rank
692657
# check (the triple's interior ranks intentionally differ from the
@@ -794,6 +759,11 @@ def permute_subgraph(self, subgraph: Subgraph) -> bool: # noqa: C901
794759
assert out.target in PERMUTE_COPY_TARGETS
795760
out.replace_all_uses_with(inp)
796761

762+
# Update outgoing permutes that can't be eliminated.
763+
for _, out, new_permutation in subgraph.edges_out_to_update:
764+
assert out.target in PERMUTE_COPY_TARGETS
765+
set_arg(out, "dims", list(new_permutation))
766+
797767
return True
798768

799769
def _subgraph_edges_are_current(self, subgraph: Subgraph) -> bool:
@@ -802,10 +772,20 @@ def _subgraph_edges_are_current(self, subgraph: Subgraph) -> bool:
802772
if inp.target not in PERMUTE_COPY_TARGETS or inp not in out.all_input_nodes:
803773
return False
804774

775+
# edges_out_to_update can rewrite a permute in place, leaving it wired.
776+
if self.get_permutation(inp) != subgraph.node_start_permute.get(
777+
out, subgraph.start_permute
778+
):
779+
return False
780+
805781
for inp, out in subgraph.edges_out:
806782
if out.target not in PERMUTE_COPY_TARGETS or out not in inp.users:
807783
return False
808784

785+
for inp, out, _ in subgraph.edges_out_to_update:
786+
if out.target not in PERMUTE_COPY_TARGETS or out not in inp.users:
787+
return False
788+
809789
for const_node, user_node in subgraph.constant_edges_in:
810790
if const_node not in user_node.all_input_nodes:
811791
return False

0 commit comments

Comments
 (0)