Skip to content

Commit bdb489e

Browse files
authored
Reland unused exported constant cleanup after Cortex-M lowering
### Summary Cortex-M layout conversion leaves original tensor buffers in the exported program. Reland pytorch#22862 to lift new constants and remove unused placeholders and storage after lowering. This restores the exact eight-file patch from the original landing, applied cleanly to current main, including preservation of the original program, shared state, and custom pass-list behavior. The original landing was reverted because a downstream custom adapter invoked ExportedProgram-aware passes on a GraphModule. The adapter migration is tracked in [D123765406](https://www.internalfb.com/diff/D123765406) and uses the native CortexMPassManager(...).passes API. Keep this reland in draft until that prerequisite lands. Authored with AI assistance from OpenAI Codex. ### Test plan 35 Python tests passed: ```bash pytest -q backends/transforms/test/test_remove_unused_constants_pass.py \ backends/cortex_m/test/test_pass_manager.py \ backends/cortex_m/test/test_explicit_layout_pipeline.py \ -k 'not test_implementation_explicit_layout_fuses_same_padding and not test_implementation_transpose_conv2d_strided_pointwise' ``` This covers constant pruning, original-program validity, shared state, mutation and gradient preservation, both layouts, native and legacy export entry points, PTE serialization, and custom pass-list ordering. Six FVP implementation cases were deselected. Lintrunner passed on all eight changed files. The reviewed downstream recipe passed twelve compatibility cases with this cleanup. Four additional ModAI integration tests passed, covering ten primary exports on the internal baseline where the cleanup remains reverted. Differential Revision: D124196546 Pull Request resolved: pytorch#23599
1 parent c4e9346 commit bdb489e

8 files changed

Lines changed: 426 additions & 13 deletions

File tree

‎backends/cortex_m/passes/BUCK‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ fbcode_target(_kind = runtime.python_library,
5858
"//executorch/backends/transforms:convert_conv1d_to_conv2d_pass",
5959
"//executorch/backends/transforms:remove_getitem_op",
6060
"//executorch/backends/transforms:remove_permutes_around_elementwise_ops",
61+
"//executorch/backends/transforms:remove_unused_constants_pass",
6162
"//executorch/backends/transforms:replace_scalar_with_tensor",
6263
"//executorch/backends/transforms:replace_ops_with_channels_last_variants",
6364
"//executorch/backends/transforms:replace_squeeze_unsqueeze_with_view",

‎backends/cortex_m/passes/cortex_m_pass_manager.py‎

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# LICENSE file in the root directory of this source tree.
55

66

7+
import copy
78
import inspect
89
from typing import Any, Optional, Type
910

@@ -20,6 +21,9 @@
2021
from executorch.backends.transforms.remove_permutes_around_elementwise_ops import (
2122
RemovePermutesAroundElementwiseOps,
2223
)
24+
from executorch.backends.transforms.remove_unused_constants_pass import (
25+
RemoveUnusedConstantsPass,
26+
)
2327
from executorch.backends.transforms.replace_scalar_with_tensor import (
2428
ReplaceScalarWithTensorArgPass,
2529
)
@@ -34,6 +38,7 @@
3438
from executorch.exir.pass_manager import ExportedProgramPassManager, PassType
3539
from executorch.exir.program._program import _transform, lift_constant_tensor_pass
3640
from torch.export import ExportedProgram
41+
from torch.fx import GraphModule
3742

3843
from .activation_fusion_pass import ActivationFusionPass
3944
from .aten_to_cortex_m_pass import AtenToCortexMPass
@@ -52,7 +57,29 @@
5257
from .quantized_clamp_activation_pass import QuantizedClampActivationPass
5358
from .replace_quant_nodes_pass import ReplaceQuantNodesPass
5459

55-
PassClass = Type[ExportPass]
60+
PassClass = Type[ExportPass | ExportedProgramPassBase]
61+
62+
63+
class LiftConstantTensorsPass(ExportedProgramPassBase):
64+
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
65+
# The pass manager shallow-copies programs; lifting mutates shared structures.
66+
graph = copy.deepcopy(exported_program.graph)
67+
for original, cloned in zip(exported_program.graph.nodes, graph.nodes):
68+
cloned.name = original.name
69+
graph_module = GraphModule(exported_program.graph_module, graph)
70+
graph_module.meta = exported_program.graph_module.meta.copy()
71+
exported_program._graph_module = graph_module
72+
exported_program._graph_signature = copy.deepcopy(
73+
exported_program.graph_signature
74+
)
75+
exported_program._state_dict = exported_program.state_dict.copy()
76+
77+
buffer_count = len(exported_program.graph_signature.buffers)
78+
exported_program = lift_constant_tensor_pass(exported_program)
79+
return ExportedProgramPassResult(
80+
exported_program,
81+
len(exported_program.graph_signature.buffers) != buffer_count,
82+
)
5683

5784

5885
class _CortexMLoweringPass(ExportedProgramPassBase):
@@ -77,10 +104,6 @@ def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
77104
modified |= transformed is not exported_program
78105
exported_program = transformed
79106

80-
# Passes can introduce tensor attributes that must become program inputs.
81-
buffer_count = len(exported_program.graph_signature.buffers)
82-
exported_program = lift_constant_tensor_pass(exported_program)
83-
modified |= len(exported_program.graph_signature.buffers) != buffer_count
84107
return ExportedProgramPassResult(exported_program, modified)
85108

86109

@@ -97,6 +120,8 @@ class CortexMPassManager(ExportedProgramPassManager):
97120
AtenToCortexMPass,
98121
FuseConvPaddingPass,
99122
InitializeScratchBuffersPass,
123+
LiftConstantTensorsPass,
124+
RemoveUnusedConstantsPass,
100125
]
101126

102127
explicit_layout_pass_list: list[PassClass] = [
@@ -117,11 +142,13 @@ class CortexMPassManager(ExportedProgramPassManager):
117142
AtenToCortexMPass,
118143
FuseConvPaddingPass,
119144
InitializeScratchBuffersPass,
145+
LiftConstantTensorsPass,
146+
RemoveUnusedConstantsPass,
120147
]
121148

122149
pass_list = legacy_pass_list
123150

124-
pass_list_transform_for_annotation: list[PassClass] = [
151+
pass_list_transform_for_annotation: list[Type[ExportPass]] = [
125152
DecomposeSDPAPass,
126153
ScalarsToAttributePass,
127154
ReplaceScalarWithTensorArgPass,

‎backends/cortex_m/test/targets.bzl‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ def define_common_targets(is_fbcode = False):
8282
"//executorch/backends/cortex_m/passes:cortex_passes",
8383
"//executorch/backends/cortex_m/quantizer:quantizer",
8484
"//executorch/backends/test/harness:tester",
85+
"//executorch/backends/transforms:remove_unused_constants_pass",
86+
"//executorch/exir:lib",
8587
"//executorch/exir/dialects:lib",
8688
":tester",
8789
"fbsource//third-party/pypi/pytest:pytest",

‎backends/cortex_m/test/test_explicit_layout_pipeline.py‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@
55
# This source code is licensed under the BSD-style license found in the
66
# LICENSE file in the root directory of this source tree.
77

8+
import copy
9+
810
import pytest
911
import torch
12+
from executorch.backends.cortex_m.passes.cortex_m_pass_manager import (
13+
CortexMPassManager,
14+
LiftConstantTensorsPass,
15+
)
1016
from executorch.backends.cortex_m.quantizer.quantizer import CortexMQuantizer
1117
from executorch.backends.cortex_m.target_config import CortexM, CortexMTargetConfig
1218
from executorch.backends.cortex_m.test.tester import (
@@ -15,6 +21,10 @@
1521
CortexMTester,
1622
)
1723
from executorch.backends.test.harness.stages import Quantize, StageType
24+
from executorch.backends.transforms.remove_unused_constants_pass import (
25+
RemoveUnusedConstantsPass,
26+
)
27+
from executorch.exir import to_edge
1828
from executorch.exir.dialects._ops import ops as exir_ops
1929
from torch.fx import Node
2030

@@ -156,6 +166,11 @@ def test_layout_pipelines_select_distinct_spatial_operators():
156166
)
157167
explicit_program = explicit.get_artifact(StageType.RUN_PASSES).exported_program()
158168

169+
for program in (legacy_program, explicit_program):
170+
assert all(
171+
node.users for node in program.graph.nodes if node.op == "placeholder"
172+
)
173+
159174
assert _count(legacy_program, exir_ops.edge.cortex_m.quantized_conv2d.default) == 1
160175
assert (
161176
_count(
@@ -177,6 +192,104 @@ def test_layout_pipelines_select_distinct_spatial_operators():
177192
assert _count(explicit_program, exir_ops.edge.cortex_m.transpose.default) == 2
178193

179194

195+
@pytest.mark.parametrize("entry_point", ["legacy", "edge_passes", "edge_manager"])
196+
@pytest.mark.parametrize(
197+
"passes,lifted,pruned",
198+
[
199+
pytest.param([], False, False, id="empty"),
200+
pytest.param([LiftConstantTensorsPass], True, False, id="lift"),
201+
pytest.param(
202+
[LiftConstantTensorsPass, RemoveUnusedConstantsPass],
203+
True,
204+
True,
205+
id="lift_and_prune",
206+
),
207+
],
208+
)
209+
def test_constant_cleanup_respects_pass_list_and_lift_order(
210+
passes, lifted, pruned, entry_point
211+
):
212+
class Model(torch.nn.Module):
213+
def __init__(self):
214+
super().__init__()
215+
self.register_buffer("_lifted_tensor_constant0", torch.tensor([1.0]))
216+
self.register_buffer("_lifted_tensor_constant1", torch.tensor([2.0]))
217+
218+
def forward(self, input):
219+
return input + self._lifted_tensor_constant1
220+
221+
class OtherEntryPoint(torch.nn.Module):
222+
def __init__(self):
223+
super().__init__()
224+
self.register_buffer("_lifted_tensor_constant2", torch.tensor([7.0]))
225+
226+
def forward(self, x):
227+
return x + self._lifted_tensor_constant2
228+
229+
inputs = (torch.zeros(1),)
230+
edge = to_edge(torch.export.export(Model(), inputs))
231+
program = edge.exported_program()
232+
assert "_lifted_tensor_constant0" in program.graph_signature.buffers
233+
program.graph_module.register_buffer("new_tensor", torch.tensor([3.0]))
234+
output = next(node for node in program.graph.nodes if node.op == "output")
235+
original = output.args[0][0]
236+
with program.graph.inserting_before(original):
237+
constant = program.graph.get_attr("new_tensor")
238+
constant.meta = original.meta.copy()
239+
result = program.graph.call_function(
240+
exir_ops.edge.aten.add.Tensor, (original.args[1], constant)
241+
)
242+
result.meta = original.meta.copy()
243+
original.replace_input_with(original.args[1], result)
244+
program.graph_module.recompile()
245+
program.validate()
246+
247+
other = to_edge(torch.export.export(OtherEntryPoint(), inputs)).exported_program()
248+
program.state_dict.update(other.state_dict)
249+
other._state_dict = program.state_dict
250+
other.validate()
251+
original_code = program.graph_module.code
252+
original_signature = copy.deepcopy(program.graph_signature)
253+
original_state = program.state_dict.copy()
254+
program.graph_module.meta["constant_lifting_test"] = "preserved"
255+
256+
if entry_point == "edge_passes":
257+
transformed = edge.transform(
258+
[pass_cls() for pass_cls in passes]
259+
).exported_program()
260+
elif entry_point == "edge_manager":
261+
transformed = edge.transform(
262+
CortexMPassManager(passes=passes)
263+
).exported_program()
264+
else:
265+
transformed = CortexMPassManager(program, passes=passes).transform()
266+
267+
program.validate()
268+
other.validate()
269+
torch.testing.assert_close(program.module()(*inputs), torch.tensor([5.0]))
270+
torch.testing.assert_close(other.module()(*inputs), torch.tensor([7.0]))
271+
assert program.graph_module.code == original_code
272+
assert program.graph_signature == original_signature
273+
assert program.state_dict.keys() == original_state.keys()
274+
for name, tensor in original_state.items():
275+
assert program.state_dict[name] is tensor
276+
assert other.state_dict[name] is tensor
277+
transformed.validate()
278+
assert transformed.graph_signature.user_inputs == original_signature.user_inputs
279+
assert transformed.graph_module.meta["constant_lifting_test"] == "preserved"
280+
assert ("_lifted_tensor_constant0" in transformed.graph_signature.buffers) == (
281+
not pruned
282+
)
283+
assert sum(node.op == "get_attr" for node in transformed.graph.nodes) == (
284+
not lifted
285+
)
286+
assert (
287+
transformed.state_dict["_lifted_tensor_constant1"]
288+
is original_state["_lifted_tensor_constant1"]
289+
)
290+
torch.testing.assert_close(transformed.module()(*inputs), torch.tensor([5.0]))
291+
292+
180293
def test_conv1d_is_quantized_before_layout_conversion():
181294
tester = CortexMTester(Conv1d().eval(), (torch.randn(1, 2, 8),))
182295
tester.quantize(Quantize(CortexMQuantizer(use_explicit_layout=True)))

‎backends/cortex_m/test/test_pass_manager.py‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def test_empty_pass_list_reports_no_change():
142142
manager.transform()
143143

144144

145-
def test_empty_pass_list_still_lifts_constants():
145+
def test_empty_pass_list_preserves_unlifted_constants():
146146
class AddConstant(ExportPass):
147147
def call_operator(self, op, args, kwargs, meta):
148148
result = super().call_operator(op, args, kwargs, meta)
@@ -159,9 +159,10 @@ def call_operator(self, op, args, kwargs, meta):
159159
assert any(node.op == "get_attr" for node in program.graph.nodes)
160160

161161
result = CortexMPassManager(passes=[])(program)
162-
assert result.modified
163-
lowered = result.exported_program
164-
lowered.validate()
165-
assert all(node.op != "get_attr" for node in lowered.graph.nodes)
166-
assert len(lowered.graph_signature.buffers) == 1
167-
torch.testing.assert_close(lowered.module()(*inputs), torch.tensor([2.0, 4.0]))
162+
assert not result.modified
163+
assert result.exported_program.graph is program.graph
164+
assert result.exported_program.graph_signature is program.graph_signature
165+
program.validate()
166+
assert any(node.op == "get_attr" for node in program.graph.nodes)
167+
assert not program.graph_signature.buffers
168+
torch.testing.assert_close(program.module()(*inputs), torch.tensor([2.0, 4.0]))
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import copy
8+
9+
from executorch.exir.pass_base import ExportedProgramPassBase, ExportedProgramPassResult
10+
from torch.export import ExportedProgram
11+
from torch.export.graph_signature import ExportGraphSignature, InputKind, TensorArgument
12+
from torch.fx import GraphModule
13+
14+
15+
class RemoveUnusedConstantsPass(ExportedProgramPassBase):
16+
"""Retire unused tensor constants after backend graph rewrites."""
17+
18+
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
19+
signature = exported_program.graph_signature
20+
placeholders = {
21+
node.name: node
22+
for node in exported_program.graph.nodes
23+
if node.op == "placeholder"
24+
}
25+
protected_targets = {
26+
spec.target for spec in signature.output_specs if spec.target is not None
27+
}
28+
preserved_names = {
29+
argument.name
30+
for entry in exported_program.module_call_graph
31+
if entry.signature is not None
32+
for argument in (*entry.signature.inputs, *entry.signature.outputs)
33+
if isinstance(argument, TensorArgument)
34+
}
35+
unused = [
36+
spec
37+
for spec in signature.input_specs
38+
if spec.kind
39+
in (InputKind.PARAMETER, InputKind.BUFFER, InputKind.CONSTANT_TENSOR)
40+
and spec.target not in protected_targets
41+
and spec.arg.name not in preserved_names
42+
and not placeholders[spec.arg.name].users
43+
]
44+
if not unused:
45+
return ExportedProgramPassResult(exported_program, False)
46+
47+
unused_names = {spec.arg.name for spec in unused}
48+
signature = ExportGraphSignature(
49+
input_specs=[
50+
spec
51+
for spec in signature.input_specs
52+
if spec.arg.name not in unused_names
53+
],
54+
output_specs=list(signature.output_specs),
55+
)
56+
# The pass manager shallow-copies programs, so their graph is still shared.
57+
graph = copy.deepcopy(exported_program.graph)
58+
for original_node, node in zip(exported_program.graph.nodes, list(graph.nodes)):
59+
# FX copying can rename built-ins such as "input", used by the signature.
60+
node.name = original_node.name
61+
if node.op == "placeholder" and node.name in unused_names:
62+
graph.erase_node(node)
63+
graph_module = GraphModule(exported_program.graph_module, graph)
64+
graph_module.meta = exported_program.graph_module.meta.copy()
65+
66+
remaining_targets = {spec.target for spec in signature.input_specs}
67+
# Entry points can share state dictionaries; retain their tensor identities.
68+
state_dict = exported_program.state_dict.copy()
69+
constants = exported_program.constants.copy()
70+
for spec in unused:
71+
if spec.target not in remaining_targets:
72+
state_dict.pop(spec.target, None)
73+
constants.pop(spec.target, None)
74+
75+
exported_program._state_dict = state_dict
76+
exported_program._constants = constants
77+
exported_program._graph_signature = signature
78+
exported_program._graph_module = graph_module
79+
return ExportedProgramPassResult(exported_program, True)

‎backends/transforms/targets.bzl‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,16 @@ def define_common_targets():
214214
],
215215
)
216216

217+
runtime.python_library(
218+
name = "remove_unused_constants_pass",
219+
srcs = ["remove_unused_constants_pass.py"],
220+
visibility = ["PUBLIC"],
221+
deps = [
222+
"//caffe2:torch",
223+
"//executorch/exir:pass_base",
224+
],
225+
)
226+
217227
runtime.python_library(
218228
name = "mean_to_sum_div",
219229
srcs = ["mean_to_sum_div.py"],

0 commit comments

Comments
 (0)