Skip to content

Commit e9eb01a

Browse files
authored
Arm backend: Fix fuse_identical_input_transforms for user inputs (pytorch#20885)
Sink identical view_copy and permute_copy inputs through supported elementwise and concat ops. This removes redundant transform nodes and normalizes lifted transform input placeholders so graph signatures stay consistent. Signed-off-by: Adrian Lundell <adrian.lundell@arm.com> Co-authored-by: Saoirse Stewart <saoirse.stewart@arm.com>
1 parent 710edda commit e9eb01a

4 files changed

Lines changed: 536 additions & 4 deletions

File tree

backends/arm/_passes/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@
122122
from .fuse_equal_placeholders_pass import FuseEqualPlaceholdersPass # noqa
123123
from .fuse_identical_input_transforms_pass import ( # noqa
124124
FuseIdenticalInputTransformsPass,
125+
NormalizeTransformInputPlaceholdersPass,
125126
)
126127
from .fuse_quantized_activation_pass import FuseQuantizedActivationPass # noqa
127128
from .fuse_view_copy_transform_pass import FuseViewCopyTransformPass # noqa

backends/arm/_passes/arm_pass_manager.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
NormalizeDelegateIOLayoutPass,
130130
NormalizeIndexPutBoolIndexTensorPass,
131131
NormalizeIndexPutNoneIndicesPass,
132+
NormalizeTransformInputPlaceholdersPass,
132133
NormalizeWhileInitialArgsPass,
133134
PromoteBoolOperandsPass,
134135
QuantizeClampArgumentsPass,
@@ -502,7 +503,7 @@ def _tosa_pipeline(
502503
# TODO: DecomposeLinearPass should run after InsertRescaleInt32Pass or
503504
# before FoldAndAnnotateQParamsPass but is unable to at the moment.
504505
# Ticket: MLETORCH-1539
505-
FuseIdenticalInputTransformsPass(),
506+
FuseIdenticalInputTransformsPass(exported_program),
506507
DecomposeLinearPass(),
507508
InsertRescaleInt32Pass(),
508509
FuseConsecutiveRescalesPass(),
@@ -638,6 +639,7 @@ def _tosa_pipeline(
638639
[
639640
CastInt64BuffersToInt32Pass(exported_program),
640641
FuseEqualPlaceholdersPass(exported_program),
642+
NormalizeTransformInputPlaceholdersPass(exported_program),
641643
ExirToTosaPass(exported_program),
642644
SymbolicToTosaShapesPass(),
643645
InsertDynamicPaddingPass(),

backends/arm/_passes/fuse_identical_input_transforms_pass.py

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,94 @@
1414
from executorch.backends.arm._passes.dim_maps import PermuteMap, same_numel, ViewMap
1515
from executorch.exir.dialects._ops import ops as exir_ops
1616
from executorch.exir.pass_base import ExportPass, PassResult
17+
from torch.export.exported_program import ExportedProgram
18+
from torch.export.graph_signature import InputSpec, TensorArgument
1719
from torch.fx import GraphModule, Node
1820

1921

22+
class NormalizeTransformInputPlaceholdersPass(ExportPass):
23+
"""Normalize placeholder names for lifted transform inputs."""
24+
25+
def __init__(self, exported_program: ExportedProgram | None = None) -> None:
26+
super().__init__()
27+
self.exported_program = exported_program
28+
29+
def call(self, graph_module: GraphModule) -> PassResult:
30+
return PassResult(
31+
graph_module,
32+
self._normalize_transform_user_input_placeholders(graph_module),
33+
)
34+
35+
def _normalize_transform_user_input_placeholders(
36+
self, graph_module: GraphModule
37+
) -> bool:
38+
if self.exported_program is None:
39+
return False
40+
41+
signature = self.exported_program.graph_signature
42+
placeholder_names = {
43+
node.name for node in graph_module.graph.nodes if node.op == "placeholder"
44+
}
45+
name_updates: dict[str, str] = {}
46+
modified = False
47+
for node in graph_module.graph.nodes:
48+
if node.op != "placeholder":
49+
continue
50+
if not self._placeholder_represents_transform(node):
51+
continue
52+
old_target = str(node.target)
53+
54+
if node.target != node.name:
55+
node.target = node.name
56+
modified = True
57+
if old_target not in placeholder_names:
58+
name_updates[old_target] = node.name
59+
60+
if not modified:
61+
return False
62+
63+
if name_updates:
64+
signature.input_specs = [
65+
self._renamed_input_spec(spec, name_updates)
66+
for spec in signature.input_specs
67+
]
68+
graph_module.graph.lint()
69+
graph_module.recompile()
70+
return True
71+
72+
def _placeholder_represents_transform(self, node: Node) -> bool:
73+
return any(
74+
self._matches_transform_placeholder_name(node.name, target)
75+
or self._matches_transform_placeholder_name(str(node.target), target)
76+
for target in FuseIdenticalInputTransformsPass._TARGETS
77+
)
78+
79+
def _matches_transform_placeholder_name(
80+
self, name: str, target: torch.fx.node.Target
81+
) -> bool:
82+
base_name = self._target_placeholder_name(target)
83+
if name == base_name:
84+
return True
85+
86+
suffix = name.removeprefix(f"{base_name}_")
87+
return suffix != name and suffix.isdecimal()
88+
89+
def _target_placeholder_name(self, target: torch.fx.node.Target) -> str:
90+
return target.__name__.replace(".", "_") # type: ignore[union-attr]
91+
92+
def _renamed_input_spec(
93+
self, spec: InputSpec, name_updates: dict[str, str]
94+
) -> InputSpec:
95+
if isinstance(spec.arg, TensorArgument) and spec.arg.name in name_updates:
96+
return InputSpec(
97+
kind=spec.kind,
98+
arg=TensorArgument(name=name_updates[spec.arg.name]),
99+
target=spec.target,
100+
persistent=spec.persistent,
101+
)
102+
return spec
103+
104+
20105
class FuseIdenticalInputTransformsPass(ArmOpTargetedPass):
21106
"""Sink identical input transforms through pointwise ops with multiple
22107
inputs. Note that this is only valid for data movement transforms; most
@@ -60,8 +145,16 @@ class FuseIdenticalInputTransformsPass(ArmOpTargetedPass):
60145

61146
target_ops = _BINARY_ELEMENTWISE_OPS | _CONCAT_OPS
62147

148+
def __init__(self, exported_program: ExportedProgram | None = None) -> None:
149+
super().__init__()
150+
self.exported_program = exported_program
151+
63152
def call(self, graph_module: GraphModule) -> PassResult:
64-
modified = False
153+
modified = (
154+
NormalizeTransformInputPlaceholdersPass(self.exported_program)
155+
.call(graph_module)
156+
.modified
157+
)
65158

66159
while True:
67160
iteration_modified = False
@@ -124,7 +217,7 @@ def _sink_identical_input_transforms(self, node: Node) -> bool:
124217
args=transform_args,
125218
kwargs=dict(transform.kwargs),
126219
)
127-
new_node.meta = copy.copy(transform.meta)
220+
new_node.meta = self._new_transform_meta(node, transform)
128221
refresh_permute_view_meta(new_node)
129222

130223
for user in list(node.users):
@@ -133,6 +226,14 @@ def _sink_identical_input_transforms(self, node: Node) -> bool:
133226

134227
return True
135228

229+
def _new_transform_meta(self, node: Node, transform: Node) -> dict[str, Any]:
230+
meta = copy.copy(transform.meta)
231+
if "delegation_tag" in node.meta:
232+
meta["delegation_tag"] = node.meta["delegation_tag"]
233+
else:
234+
meta.pop("delegation_tag", None)
235+
return meta
236+
136237
def _updated_node_args(
137238
self, node: Node, transform: Node, node_val: Any, input_transforms: list[Node]
138239
) -> (

0 commit comments

Comments
 (0)