Skip to content

Commit 3ed70e2

Browse files
committed
up
1 parent 0b55801 commit 3ed70e2

8 files changed

Lines changed: 78 additions & 78 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):

backends/native/fat_pte.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ def pack_fat_blob(specializations: List[Tuple[str, bytes]]) -> bytes:
3939
entries = []
4040
offset = 0
4141
for backend_id, payload in specializations:
42-
bid = backend_id.encode("utf-8")[:32].ljust(32, b"\x00")
42+
encoded = backend_id.encode("ascii")
43+
if len(encoded) > 32:
44+
raise ValueError(
45+
f"Backend ID '{backend_id}' is {len(encoded)} bytes; max is 32"
46+
)
47+
bid = encoded.ljust(32, b"\x00")
4348
entries.append(struct.pack("<" + _ENTRY_FMT, bid, offset, len(payload)))
4449
offset += len(payload)
4550

backends/native/partitioner.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@
1010
Claims core ATen ops (torch.Tag.core) plus an explicit opt-in set.
1111
"""
1212

13+
import json
14+
1315
from typing import Callable, final, List, Mapping, Optional, Tuple
1416

1517
import executorch.backends.native.specializations # noqa: F401 — register recipes
1618

1719
import torch
1820

21+
from executorch.exir.backend.backend_details import CompileSpec
1922
from executorch.exir.backend.partitioner import (
2023
DelegationSpec,
2124
Partitioner,
@@ -64,21 +67,33 @@ def is_node_supported(
6467

6568
@final
6669
class NativePartitioner(Partitioner):
70+
_SPECIALIZATIONS_KEY = "native_specializations"
71+
6772
def __init__(
6873
self,
6974
specializations: Optional[List[str]] = None,
7075
) -> None:
7176
self.specializations = specializations
7277
from executorch.backends.native.preprocess import NativeBackend
7378

74-
self.delegation_spec = DelegationSpec(NativeBackend.__name__, [])
79+
compile_specs = []
80+
if specializations:
81+
compile_specs.append(
82+
CompileSpec(
83+
self._SPECIALIZATIONS_KEY,
84+
json.dumps(specializations).encode("utf-8"),
85+
)
86+
)
87+
self.delegation_spec = DelegationSpec(NativeBackend.__name__, compile_specs)
7588

7689
def ops_to_not_decompose(
7790
self, ep: ExportedProgram
7891
) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[Node], bool]]]:
7992
# Already-partitioned graph -> nothing to preserve.
93+
from executorch.exir.lowered_backend_module import executorch_call_delegate
94+
8095
for node in ep.graph.nodes:
81-
if node.op == "get_attr" and "lowered_module" in node.name:
96+
if node.op == "call_function" and node.target is executorch_call_delegate:
8297
return ([], None)
8398

8499
present: List[torch._ops.OpOverload] = []
@@ -95,10 +110,6 @@ def ops_to_not_decompose(
95110
return (present, None)
96111

97112
def partition(self, exported_program: ExportedProgram) -> PartitionResult:
98-
from executorch.backends.native.preprocess import NativeBackend
99-
100-
NativeBackend._specialization_names = self.specializations
101-
102113
partition_tags = {}
103114

104115
capability_partitioner = CapabilityBasedPartitioner(

backends/native/preprocess.py

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
memory planning → emit → serialize via _program_to_flatbuffer.
1212
"""
1313

14+
import json
1415
from functools import partial
15-
from typing import final, List, Optional, Sequence, Tuple
16+
from typing import final, List, Tuple
1617

1718
import torch
1819

@@ -65,6 +66,8 @@ def _build_backend_inplace_ops() -> frozenset:
6566

6667

6768
class _AnyOp(Verifier):
69+
# "TRAINING" is the only dialect that allows non-functional (mutating) ops.
70+
# After reinplace converts e.g. relu → relu_, the verifier must accept them.
6871
dialect = "TRAINING"
6972

7073
def allowed_op_types(self):
@@ -93,16 +96,17 @@ def _apply_passes(program: ExportedProgram, passes) -> ExportedProgram:
9396

9497
@final
9598
class NativeBackend(BackendDetails):
96-
# Set by NativePartitioner before preprocess is called.
97-
_specialization_names: Optional[Sequence[str]] = None
98-
9999
@classmethod
100100
def preprocess(
101101
cls,
102102
program: ExportedProgram,
103103
module_compile_spec: List[CompileSpec],
104104
) -> PreprocessResult:
105-
names = cls._specialization_names
105+
names = None
106+
for spec in module_compile_spec:
107+
if spec.key == "native_specializations":
108+
names = json.loads(spec.value.decode("utf-8"))
109+
106110
if not names:
107111
return cls._preprocess_native(program)
108112

@@ -140,22 +144,6 @@ def _preprocess_native(
140144

141145
program = _et_reinplace_pass(program, ops_to_inplace=BACKEND_INPLACE_OPS)
142146

143-
# reinplace may rename output nodes; fix the graph signature to match.
144-
output_node = next(
145-
n for n in program.graph_module.graph.nodes if n.op == "output"
146-
)
147-
actual_names = [arg.name for arg in output_node.args[0] if hasattr(arg, "name")]
148-
new_output_specs = []
149-
for spec, name in zip(program.graph_signature.output_specs, actual_names):
150-
new_output_specs.append(
151-
type(spec)(
152-
kind=spec.kind,
153-
arg=type(spec.arg)(name=name),
154-
target=spec.target,
155-
)
156-
)
157-
program._graph_signature.output_specs = new_output_specs
158-
159147
# Re-run SpecPropPass: reinplace copies meta["val"] but not meta["spec"].
160148
program = _apply_passes(program, [SpecPropPass()])
161149

@@ -212,6 +200,7 @@ def _preprocess_native(
212200
algo_list=[greedy_memory_planning]
213201
)
214202

203+
# Tells the emitter to use out-variant kernels for ops that support them.
215204
program.graph_module.encounter_to_out_var_failure = True
216205

217206
program = _apply_passes(

backends/native/test/test_fat_pte.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,13 +65,10 @@ def test_empty_specializations(self):
6565
self.assertEqual(count, 0)
6666
self.assertEqual(len(blob), 12)
6767

68-
def test_backend_id_truncated_to_32_bytes(self):
69-
long_name = "A" * 100
70-
blob = pack_fat_blob([(long_name, b"x")])
71-
header_size = 12
72-
bid, _, _ = struct.unpack_from("<" + _ENTRY_FMT, blob, header_size)
73-
self.assertEqual(len(bid), 32)
74-
self.assertEqual(bid, b"A" * 32)
68+
def test_backend_id_over_32_bytes_raises(self):
69+
long_name = "A" * 33
70+
with self.assertRaises(ValueError):
71+
pack_fat_blob([(long_name, b"x")])
7572

7673

7774
class TestBuildFatResult(unittest.TestCase):

backends/native/test/test_preprocess.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
from executorch.backends.native.fat_pte import FAT_MAGIC
1414
from executorch.backends.native.partitioner import NativePartitioner
15-
from executorch.backends.native.preprocess import NativeBackend
1615
from executorch.backends.native.specializations import (
1716
_SPECIALIZATION_REGISTRY,
1817
register_specialization,
@@ -45,7 +44,6 @@ def setUp(self):
4544
def tearDown(self):
4645
_SPECIALIZATION_REGISTRY.clear()
4746
_SPECIALIZATION_REGISTRY.update(self._saved)
48-
NativeBackend._specialization_names = None
4947

5048
def test_register_and_lookup(self):
5149
register_specialization("TestBackend", lambda ep: b"test")

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)