@@ -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