Skip to content

Commit 63a1c00

Browse files
ThomasJannaudfacebook-github-bot
authored andcommitted
Preserve distinct mutation output names in graph signatures (#24105)
Summary: Problem `ExportGraphSignature.buffers_to_mutate` and `user_inputs_to_mutate` index writes by output node name. No-op folding and CSE can merge two writes into one node, so one of the mutation entries is lost from the signature. Solution Rebuild the signature from `output_specs` (`_get_updated_graph_signature` in `exir/_program_utils.py`). Copy a computed result only when two writes collide in the same mutation map, and keep positional user outputs and ordinary graph uses shared. Select ATen, edge or dim-order cloning from the program's verifier. Preserve value/debug metadata and FX provenance on the copies without inheriting allocation or custom-kernel placement annotations. Mutation output copies keep independent storage, as required by the existing output pointer contract. This is the ExecuTorch half of D125489522, split out so that ExecuTorch owners can review it on its own. The Helios half (removing the Helios-side duplicated-output splitter that this change makes unnecessary) stays in D125489522, stacked on top. fbcode and xplat copies are identical. {F2001970137} Differential Revision: D127072149
1 parent cce0234 commit 63a1c00

7 files changed

Lines changed: 619 additions & 16 deletions

File tree

‎exir/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ fbcode_target(_kind = runtime.python_library,
296296
],
297297
deps = [
298298
"//caffe2:torch",
299+
"//executorch/exir/dialects:lib",
299300
],
300301
)
301302

‎exir/_program_utils.py‎

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,105 @@
66

77
# pyre-unsafe
88

9+
from collections.abc import Callable
10+
911
import torch
12+
from executorch.exir.dialects._ops import ops as exir_ops
1013
from torch.export.exported_program import (
1114
ConstantArgument,
1215
ExportGraphSignature,
1316
InputSpec,
1417
OutputSpec,
1518
)
19+
from torch.export.graph_signature import OutputKind
20+
from torch.fx.traceback import NodeSource, NodeSourceAction
21+
22+
_MUTATION_OUTPUT_CLONE_OP = "_mutation_output_clone_op"
23+
24+
25+
def _get_mutation_output_clone_op(
26+
program: torch.export.ExportedProgram,
27+
clone_op: Callable[..., torch.Tensor] | None = None,
28+
) -> Callable[..., torch.Tensor] | None:
29+
if clone_op is not None:
30+
return clone_op
31+
if program.dialect == "ATEN":
32+
return torch.ops.aten.clone.default
33+
kind = program.graph_module.meta.get(_MUTATION_OUTPUT_CLONE_OP)
34+
if kind == "aten":
35+
return torch.ops.aten.clone.default
36+
if kind == "edge":
37+
return exir_ops.edge.aten.clone.default
38+
if kind == "edge_dim_order":
39+
return exir_ops.edge.dim_order_ops._clone_dim_order.default
40+
return None
41+
42+
43+
def _set_mutation_output_clone_op(
44+
gm: torch.fx.GraphModule, clone_op: Callable[..., torch.Tensor] | None
45+
) -> None:
46+
"""Record the clone dialect and layout as metadata preserved by deepcopy."""
47+
if clone_op is None:
48+
return
49+
if clone_op is torch.ops.aten.clone.default:
50+
kind = "aten"
51+
elif clone_op is exir_ops.edge.aten.clone.default:
52+
kind = "edge"
53+
elif clone_op is exir_ops.edge.dim_order_ops._clone_dim_order.default:
54+
kind = "edge_dim_order"
55+
else:
56+
raise ValueError(f"Unsupported mutation output clone operator: {clone_op}")
57+
gm.meta[_MUTATION_OUTPUT_CLONE_OP] = kind
58+
59+
60+
def _disambiguate_mutation_outputs(
61+
signature: ExportGraphSignature,
62+
gm: torch.fx.GraphModule,
63+
*,
64+
clone_op: Callable[..., torch.Tensor] | None,
65+
) -> bool:
66+
"""Copy colliding mutation outputs in-place, preserving positional user outputs."""
67+
output_node = gm.graph.output_node()
68+
output_args = output_node.args[0]
69+
assert isinstance(output_args, (tuple, list))
70+
assert len(output_args) == len(
71+
signature.output_specs
72+
), "Number of outputs changed after transformation"
73+
seen_mutations: set[tuple[OutputKind, torch.fx.Node]] = set()
74+
new_output_args = list(output_args)
75+
for i, (node, spec) in enumerate(zip(output_args, signature.output_specs)):
76+
if spec.kind not in (
77+
OutputKind.BUFFER_MUTATION,
78+
OutputKind.USER_INPUT_MUTATION,
79+
):
80+
continue
81+
assert isinstance(node, torch.fx.Node)
82+
mutation = (spec.kind, node)
83+
if mutation in seen_mutations:
84+
if clone_op is None:
85+
raise RuntimeError(
86+
"Mutation output repair requires an explicit clone operator "
87+
"for programs without an Edge compile configuration"
88+
)
89+
# Mutation dictionaries are keyed by node name, while user outputs
90+
# are positional and may share a node. Copy only colliding writes.
91+
with gm.graph.inserting_before(output_node):
92+
clone = gm.graph.call_function(clone_op, args=(node,))
93+
clone.meta = {
94+
key: node.meta[key]
95+
for key in ("val", "stack_trace", "nn_module_stack", "debug_handle")
96+
if key in node.meta
97+
}
98+
clone.meta["from_node"] = [
99+
NodeSource(node, "_disambiguate_mutation_outputs", NodeSourceAction.CREATE)
100+
]
101+
new_output_args[i] = clone
102+
seen_mutations.add(mutation)
103+
if new_output_args == list(output_args):
104+
return False
105+
output_node.args = (tuple(new_output_args),) + output_node.args[1:]
106+
gm.recompile()
107+
return True
16108

17109

18110
def _get_updated_range_constraints(gm, old_range_constraints=None):
@@ -55,9 +147,7 @@ def _get_updated_graph_signature(
55147
old_signature: ExportGraphSignature,
56148
new_gm: torch.fx.GraphModule,
57149
) -> ExportGraphSignature:
58-
"""
59-
Update the graph signature's user_input/user_outputs.
60-
"""
150+
"""Rebuild the positional signature without modifying the graph module."""
61151
new_input_specs = []
62152
i = 0
63153
for node in new_gm.graph.nodes:
@@ -87,6 +177,11 @@ def _get_updated_graph_signature(
87177
output_node = new_gm.graph.output_node()
88178
assert output_node.op == "output"
89179

180+
output_args = output_node.args[0]
181+
assert isinstance(output_args, (tuple, list))
182+
assert len(output_args) == len(
183+
old_signature.output_specs
184+
), "Number of outputs changed after transformation"
90185
new_output_specs = []
91186
for i, node in enumerate(output_node.args[0]):
92187
assert i < len(

‎exir/pass_manager.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
import torch.fx.passes.infra.pass_manager as fx
1616
import torch.utils._pytree as pytree
1717
from executorch.exir._program_utils import (
18+
_disambiguate_mutation_outputs,
19+
_get_mutation_output_clone_op,
1820
_get_updated_graph_signature,
1921
_get_updated_range_constraints,
22+
_set_mutation_output_clone_op,
2023
)
2124
from executorch.exir.error import ExportError, ExportErrorType
2225
from executorch.exir.pass_base import ExportedProgramPassBase, ExportedProgramPassResult
@@ -179,6 +182,8 @@ def __call__( # noqa: C901
179182
self,
180183
exported_program: ExportedProgram,
181184
override_verifiers: Optional[list[Type[Verifier]]] = None,
185+
*,
186+
mutation_output_clone_op: Callable[..., torch.Tensor] | None = None,
182187
) -> ExportedProgramPassResult:
183188
"""
184189
Runs passes on an ExportedProgram.
@@ -195,6 +200,9 @@ def __call__( # noqa: C901
195200
self.solve_constraints()
196201

197202
exported_program = copy.copy(exported_program)
203+
clone_op = _get_mutation_output_clone_op(
204+
exported_program, mutation_output_clone_op
205+
)
198206

199207
if override_verifiers:
200208
exported_program._verifiers = override_verifiers
@@ -222,9 +230,14 @@ def __call__( # noqa: C901
222230
# possible that the verifier will fail upon new ExportedProgram construction,
223231
# and we should only run verification after each pass if
224232
# run_checks_after_each_pass is True.
225-
new_graph_signature = _get_updated_graph_signature(
233+
_disambiguate_mutation_outputs(
226234
exported_program.graph_signature,
227235
res.graph_module,
236+
clone_op=clone_op,
237+
)
238+
_set_mutation_output_clone_op(res.graph_module, clone_op)
239+
new_graph_signature = _get_updated_graph_signature(
240+
exported_program.graph_signature, res.graph_module
228241
)
229242
# ExportedProgram.__init__ runs this, but this path mutates
230243
# the program in place and never reconstructs it. Duplicate

‎exir/program/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fbcode_target(_kind = runtime.python_library,
3535
"//executorch/exir/backend:partitioner",
3636
"//executorch/exir/backend/canonical_partitioners:canonical_partitioner_lib",
3737
"//executorch/exir/capture:config",
38+
"//executorch/exir/dialects:lib",
3839
"//executorch/exir/emit:emit",
3940
"//executorch/exir/emit:lib",
4041
"//executorch/exir/operator:util",

‎exir/program/_program.py‎

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import logging
1212
import os
1313
from collections import defaultdict
14-
from typing import Any, Dict, List, Optional, Sequence, Set, TextIO, Type, Union
14+
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, TextIO, Type, Union
1515

1616
import torch
1717
import torch._export
@@ -30,6 +30,7 @@
3030
from executorch.exir.backend.partitioner import Partitioner
3131
from executorch.exir.capture._config import EdgeCompileConfig, ExecutorchBackendConfig
3232
from executorch.exir.delegate import executorch_call_delegate, is_lowered_module
33+
from executorch.exir.dialects._ops import ops as exir_ops
3334
from executorch.exir.emit import emit_program, EmitterOutput
3435
from executorch.exir.emit._emitter import _DelegateDebugIdentifierMap
3536
from executorch.exir.error import ExportError
@@ -132,15 +133,19 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
132133

133134

134135
from executorch.exir._program_utils import ( # noqa: E402
136+
_disambiguate_mutation_outputs,
137+
_get_mutation_output_clone_op,
135138
_get_updated_graph_signature,
136139
_get_updated_range_constraints,
140+
_set_mutation_output_clone_op,
137141
)
138142

139143

140144
def _transform(
141145
self,
142146
*passes: PassType,
143147
override_verifiers: None | list[Type[Verifier]] = None,
148+
mutation_output_clone_op: Callable[..., torch.Tensor] | None = None,
144149
) -> "ExportedProgram":
145150
"""
146151
Transforms the program according to the provided passes.
@@ -150,6 +155,8 @@ def _transform(
150155
*passes: A sequence of passes to apply to the program
151156
override_verifiers: Optional list of verifier classes to use instead of the default verifiers.
152157
This is needed if the transforms yields illegal graph that the default verifier cannot handle.
158+
mutation_output_clone_op: Clone operator for the transformed graph's dialect and layout mode.
159+
Defaults to ATen cloning or the contract recorded during Edge conversion.
153160
154161
Returns:
155162
ExportedProgram: A new ExportedProgram with the transformations applied, or self if no changes were made
@@ -160,14 +167,19 @@ def _transform(
160167
), f"Expected all passes to be of PassType, not list or Verifier. Use override_verifiers kwarg instead. Got: {list(passes)}"
161168

162169
return _transform_with_pass_manager(
163-
self, ExportedProgramPassManager(list(passes)), override_verifiers
170+
self,
171+
ExportedProgramPassManager(list(passes)),
172+
override_verifiers,
173+
mutation_output_clone_op=mutation_output_clone_op,
164174
)
165175

166176

167177
def _transform_with_pass_manager(
168178
self: ExportedProgram,
169179
pass_manager: Union[ExportedProgramPassManager, GraphModulePassManager],
170180
override_verifiers: None | list[Type[Verifier]] = None,
181+
*,
182+
mutation_output_clone_op: Callable[..., torch.Tensor] | None = None,
171183
) -> "ExportedProgram":
172184
"""
173185
Transforms the program using the provided pass_manager.
@@ -184,7 +196,11 @@ def _transform_with_pass_manager(
184196
ExportedProgram: A new ExportedProgram with the transformations applied, or self if no changes were made
185197
"""
186198
if isinstance(pass_manager, ExportedProgramPassManager):
187-
res = pass_manager(self, override_verifiers)
199+
res = pass_manager(
200+
self,
201+
override_verifiers,
202+
mutation_output_clone_op=mutation_output_clone_op,
203+
)
188204
if not res.modified:
189205
return self
190206
return res.exported_program
@@ -193,20 +209,30 @@ def _transform_with_pass_manager(
193209
if not res.modified:
194210
return self
195211
return _update_exported_program_graph_module(
196-
self, res.graph_module, override_verifiers
212+
self,
213+
res.graph_module,
214+
override_verifiers,
215+
mutation_output_clone_op=mutation_output_clone_op,
197216
)
198217

199218

200219
def _update_exported_program_graph_module(
201220
exported_program: ExportedProgram,
202221
gm: torch.fx.GraphModule,
203222
override_verifiers: None | list[Type[Verifier]] = None,
223+
*,
224+
mutation_output_clone_op: Callable[..., torch.Tensor] | None = None,
204225
) -> "ExportedProgram":
226+
clone_op = _get_mutation_output_clone_op(exported_program, mutation_output_clone_op)
227+
_disambiguate_mutation_outputs(
228+
exported_program.graph_signature, gm, clone_op=clone_op
229+
)
205230
transformed_ep = ExportedProgram(
206231
root=gm,
207232
graph=gm.graph,
208233
graph_signature=_get_updated_graph_signature(
209-
exported_program.graph_signature, gm
234+
exported_program.graph_signature,
235+
gm,
210236
),
211237
state_dict=exported_program.state_dict,
212238
range_constraints=_get_updated_range_constraints(
@@ -219,6 +245,7 @@ def _update_exported_program_graph_module(
219245
)
220246
transformed_ep.graph_module.meta.update(exported_program.graph_module.meta)
221247
transformed_ep.graph_module.meta.update(gm.meta)
248+
_set_mutation_output_clone_op(transformed_ep.graph_module, clone_op)
222249
return transformed_ep
223250

224251

@@ -707,11 +734,21 @@ def _to_edge(ep, config: EdgeCompileConfig) -> "ExirExportedProgram":
707734
assert new_gm_res is not None
708735
new_gm = new_gm_res.graph_module
709736

737+
clone_op = (
738+
exir_ops.edge.aten.clone.default
739+
if config._skip_dim_order
740+
else exir_ops.edge.dim_order_ops._clone_dim_order.default
741+
)
742+
_disambiguate_mutation_outputs(
743+
new_ep.exported_program.graph_signature, new_gm, clone_op=clone_op
744+
)
745+
_set_mutation_output_clone_op(new_gm, clone_op)
710746
new_ep.exported_program = ExportedProgram(
711747
root=new_gm,
712748
graph=new_gm.graph,
713749
graph_signature=_get_updated_graph_signature(
714-
new_ep.exported_program.graph_signature, new_gm
750+
new_ep.exported_program.graph_signature,
751+
new_gm,
715752
),
716753
state_dict=new_ep.exported_program.state_dict,
717754
range_constraints=new_ep.exported_program.range_constraints,
@@ -848,10 +885,20 @@ def _generate_edge_program(
848885
assert gm_res is not None
849886
gm = gm_res.graph_module
850887

888+
clone_op = (
889+
exir_ops.edge.aten.clone.default
890+
if config._skip_dim_order
891+
else exir_ops.edge.dim_order_ops._clone_dim_order.default
892+
)
893+
_disambiguate_mutation_outputs(program.graph_signature, gm, clone_op=clone_op)
894+
_set_mutation_output_clone_op(gm, clone_op)
851895
edge_program = ExportedProgram(
852896
root=gm,
853897
graph=gm.graph,
854-
graph_signature=_get_updated_graph_signature(program.graph_signature, gm),
898+
graph_signature=_get_updated_graph_signature(
899+
program.graph_signature,
900+
gm,
901+
),
855902
state_dict=program.state_dict,
856903
range_constraints=program.range_constraints,
857904
module_call_graph=program.module_call_graph,
@@ -1612,6 +1659,11 @@ def transform(
16121659
"""
16131660

16141661
compile_config = compile_config or self.compile_config
1662+
clone_op = (
1663+
exir_ops.edge.aten.clone.default
1664+
if compile_config._skip_dim_order
1665+
else exir_ops.edge.dim_order_ops._clone_dim_order.default
1666+
)
16151667
new_programs: Dict[str, ExportedProgram] = {}
16161668

16171669
# Cast passes parameter upfront.
@@ -1640,11 +1692,17 @@ def transform(
16401692

16411693
# Depending on the passes parameter, call the corresponding transform function.
16421694
if passes_seq is not None:
1643-
new_programs[name] = _transform(program, *passes_seq)
1695+
new_programs[name] = _transform(
1696+
program, *passes_seq, mutation_output_clone_op=clone_op
1697+
)
16441698
elif passes_dict is not None:
1645-
new_programs[name] = _transform(program, *passes_dict[name])
1699+
new_programs[name] = _transform(
1700+
program, *passes_dict[name], mutation_output_clone_op=clone_op
1701+
)
16461702
elif pass_manager is not None:
1647-
new_programs[name] = _transform_with_pass_manager(program, pass_manager)
1703+
new_programs[name] = _transform_with_pass_manager(
1704+
program, pass_manager, mutation_output_clone_op=clone_op
1705+
)
16481706

16491707
# Verify the correctness of model graph after each transformation.
16501708
EXIREdgeDialectVerifier(edge_compile_config=compile_config)(

0 commit comments

Comments
 (0)