Skip to content

Commit b1dd46a

Browse files
authored
reinplace_pass resyncs output_specs after in-place rewrites
Differential Revision: D111753298 Pull Request resolved: #20893
1 parent 28ebd34 commit b1dd46a

3 files changed

Lines changed: 41 additions & 41 deletions

File tree

backends/mlx/passes.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -99,49 +99,8 @@ def call(self, exported_program) -> ExportedProgramPassResult:
9999
}
100100
if ops_to_inplace:
101101
reinplace_pass(exported_program, ops_to_inplace=ops_to_inplace)
102-
self._resync_output_specs(exported_program)
103102
return ExportedProgramPassResult(exported_program, True)
104103

105-
@staticmethod
106-
def _resync_output_specs(exported_program) -> None:
107-
"""Re-sync graph-signature output names after reinplace.
108-
109-
``reinplace_pass`` rewrites an output-producing node (e.g. the final
110-
``exp`` -> ``exp_``) via ``replace_all_uses_with`` + erase, but does not
111-
update ``graph_signature.output_specs``. Output order is preserved, so we
112-
positionally re-sync each spec's argument name to the current output node
113-
arg; otherwise ``ExportedProgram.validate()`` (run by the pass manager)
114-
raises a SpecViolationError.
115-
116-
This positional pairing is only valid because reinplace does a 1:1
117-
``replace_all_uses_with`` + erase and never drops, adds, or reorders
118-
outputs. We assert ``len(output_specs) == len(out_args)`` so that a
119-
future change violating that invariant fails loudly here instead of
120-
silently mis-pairing names (``zip`` would otherwise truncate). Specs
121-
whose ``arg`` is not a named tensor (e.g. ``ConstantArgument``) carry no
122-
``name`` and are skipped by the ``getattr`` guard below.
123-
"""
124-
out_node = next(
125-
n for n in reversed(exported_program.graph.nodes) if n.op == "output"
126-
)
127-
out_args = out_node.args[0]
128-
if not isinstance(out_args, (tuple, list)):
129-
out_args = (out_args,)
130-
output_specs = exported_program.graph_signature.output_specs
131-
assert len(output_specs) == len(out_args), (
132-
"reinplace changed graph output count: "
133-
f"{len(output_specs)} output_specs vs {len(out_args)} output args. "
134-
"Positional output-spec re-sync assumes a 1:1, order-preserving "
135-
"rewrite."
136-
)
137-
for spec, arg in zip(output_specs, out_args):
138-
if (
139-
isinstance(arg, torch.fx.Node)
140-
and getattr(spec.arg, "name", None) is not None
141-
and spec.arg.name != arg.name
142-
):
143-
spec.arg.name = arg.name
144-
145104

146105
@dataclass
147106
class RMSNormMatch(PatternMatch):

exir/passes/reinplace.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,4 +507,26 @@ def reinplace_pass( # noqa: C901
507507
continue
508508

509509
seen_nodes.update(node.all_input_nodes)
510+
511+
# Reinplace rewrites may rename output nodes (e.g. aten_relu_default →
512+
# aten_relu__default) but replace_all_uses_with does not update the
513+
# graph signature. Fix output_specs to match the actual output node args.
514+
output_node = next((n for n in ep.graph.nodes if n.op == "output"), None)
515+
if output_node is not None:
516+
out_args = output_node.args[0]
517+
if not isinstance(out_args, (tuple, list)):
518+
out_args = (out_args,)
519+
output_specs = ep.graph_signature.output_specs
520+
assert len(output_specs) == len(out_args), (
521+
f"reinplace: output spec count changed: "
522+
f"{len(output_specs)} specs vs {len(out_args)} output args"
523+
)
524+
for spec, arg in zip(output_specs, out_args):
525+
if (
526+
isinstance(arg, torch.fx.Node)
527+
and getattr(spec.arg, "name", None) is not None
528+
and spec.arg.name != arg.name
529+
):
530+
spec.arg.name = arg.name
531+
510532
return ep

exir/tests/test_reinplace_pass.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,25 @@ def forward(
326326
# kernels in the other tests in this file.
327327
edge.to_executorch()
328328

329+
def test_output_specs_updated_after_reinplace(self) -> None:
330+
"""reinplace_pass must update graph_signature.output_specs to match
331+
the renamed output nodes (e.g. relu -> relu_)."""
332+
333+
class M(torch.nn.Module):
334+
def forward(self, x: torch.Tensor) -> torch.Tensor:
335+
return torch.relu(x + 1.0)
336+
337+
ep = export(M(), (torch.randn(4),), strict=True)
338+
edge_program = to_edge(ep).exported_program()
339+
340+
custom_set = {edge_ops.edge.aten.relu.default}
341+
ep = reinplace_pass(edge_program, ops_to_inplace=custom_set)
342+
343+
output_node = next(n for n in ep.graph.nodes if n.op == "output")
344+
actual_names = [arg.name for arg in output_node.args[0] if hasattr(arg, "name")]
345+
spec_names = [s.arg.name for s in ep.graph_signature.output_specs]
346+
self.assertEqual(actual_names, spec_names)
347+
329348
def test_broadcasting_self_not_reinplaced(self) -> None:
330349
"""An op whose mutated arg (self) broadcasts up to a larger output
331350
must NOT be reinplaced: the in-place form cannot grow self

0 commit comments

Comments
 (0)