Skip to content

Commit ff2bf9c

Browse files
authored
Add RemoveBNTrackingMutationsPass (#19980)
Differential Revision: D107395650 Pull Request resolved: #19980
1 parent a79f3e4 commit ff2bf9c

3 files changed

Lines changed: 330 additions & 1 deletion

File tree

backends/cadence/aot/BUCK

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,22 @@ fbcode_target(_kind = python_unittest,
534534
],
535535
)
536536

537+
fbcode_target(_kind = python_unittest,
538+
name = "test_remove_bn_tracking_pass",
539+
srcs = [
540+
"tests/test_remove_bn_tracking_pass.py",
541+
],
542+
supports_static_listing = False,
543+
typing = True,
544+
deps = [
545+
"//caffe2:torch",
546+
"//executorch/backends/cadence/aot:remove_ops",
547+
"//executorch/backends/cadence/aot/quantizer:quantizer",
548+
"//executorch/exir:lib",
549+
"//pytorch/ao:torchao",
550+
],
551+
)
552+
537553
fbcode_target(_kind = python_unittest,
538554
name = "test_simplify_ops_passes",
539555
srcs = [

backends/cadence/aot/remove_ops.py

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,16 @@
3030
)
3131
from executorch.exir.dialects._ops import ops as exir_ops
3232
from executorch.exir.dialects.edge._ops import EdgeOpOverload, EdgeOpOverloadPacket
33-
from executorch.exir.pass_base import ExportPass, PassResult
33+
from executorch.exir.pass_base import (
34+
ExportedProgramPassBase,
35+
ExportedProgramPassResult,
36+
ExportPass,
37+
PassResult,
38+
)
3439
from executorch.exir.pass_manager import PassManager, PassType
3540
from executorch.exir.passes import dead_code_elimination_pass
41+
from torch.export import ExportedProgram
42+
from torch.export.graph_signature import InputKind, OutputKind
3643
from torch.fx.node import Node
3744
from torch.utils import _pytree as pytree
3845

@@ -869,6 +876,103 @@ class CommonRemovePasses:
869876
]
870877

871878

879+
class RemoveBNTrackingMutationsPass(ExportedProgramPassBase):
880+
"""Remove num_batches_tracked buffer mutations from an ExportedProgram.
881+
882+
run_decompositions() re-introduces num_batches_tracked mutable buffer
883+
outputs even when batch_norm uses training=False. These mutations are
884+
dead (the counter is never read in eval mode) but inflate the PTE.
885+
886+
Removes both the mutation outputs AND the dead input placeholders,
887+
along with their corresponding graph signature entries and state dict
888+
tensors.
889+
"""
890+
891+
def call(self, exported_program: ExportedProgram) -> ExportedProgramPassResult:
892+
ep = exported_program
893+
nbt_fqns = {
894+
fqn
895+
for fqn in ep.graph_signature.buffers_to_mutate.values()
896+
if "num_batches_tracked" in fqn
897+
}
898+
if not nbt_fqns:
899+
return ExportedProgramPassResult(ep, False)
900+
901+
nbt_output_names = {
902+
name
903+
for name, fqn in ep.graph_signature.buffers_to_mutate.items()
904+
if fqn in nbt_fqns
905+
}
906+
# buffers_to_mutate / inputs_to_buffers are keyed by the FX node name
907+
# (arg.name), which can differ from node.target when export
908+
# uniquifies or sanitizes placeholder names. Match on node.name.
909+
nbt_input_names = {
910+
name
911+
for name, fqn in ep.graph_signature.inputs_to_buffers.items()
912+
if fqn in nbt_fqns
913+
}
914+
915+
gm = ep.graph_module
916+
917+
# Remove mutation outputs
918+
output_node = gm.graph.output_node()
919+
output_args = list(output_node.args[0])
920+
for idx in sorted(
921+
(
922+
i
923+
for i, n in enumerate(output_args)
924+
if isinstance(n, torch.fx.Node) and n.name in nbt_output_names
925+
),
926+
reverse=True,
927+
):
928+
output_args.pop(idx)
929+
output_node.args = (tuple(output_args),)
930+
931+
gm.graph.eliminate_dead_code()
932+
933+
removed_nbt_fqns: Set[str] = set()
934+
935+
# Remove dead input placeholders
936+
for node in list(gm.graph.nodes):
937+
if (
938+
node.op == "placeholder"
939+
and node.name in nbt_input_names
940+
and len(node.users) == 0
941+
):
942+
removed_nbt_fqns.add(ep.graph_signature.inputs_to_buffers[node.name])
943+
gm.graph.erase_node(node)
944+
945+
gm.recompile()
946+
947+
# Update output specs
948+
ep.graph_signature.output_specs = [
949+
s
950+
for s in ep.graph_signature.output_specs
951+
if not (
952+
s.kind == OutputKind.BUFFER_MUTATION
953+
and s.target is not None
954+
and s.target in nbt_fqns
955+
)
956+
]
957+
958+
ep.graph_signature.input_specs = [
959+
s
960+
for s in ep.graph_signature.input_specs
961+
if not (
962+
s.kind == InputKind.BUFFER
963+
and s.target is not None
964+
and s.target in removed_nbt_fqns
965+
)
966+
]
967+
968+
# Remove state for buffers whose placeholders were removed.
969+
for fqn in removed_nbt_fqns:
970+
ep.state_dict.pop(fqn, None)
971+
ep.constants.pop(fqn, None)
972+
973+
return ExportedProgramPassResult(ep, True)
974+
975+
872976
class CadenceRemoveNops:
873977
passes: List[Type[ExportPass]] = CommonRemovePasses.passes + [
874978
SimplifySliceOpPass,
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
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+
# pyre-strict
8+
9+
import unittest
10+
11+
import torch
12+
import torch.nn as nn
13+
import torchao
14+
from executorch.backends.cadence.aot.remove_ops import RemoveBNTrackingMutationsPass
15+
from executorch.exir import to_edge
16+
from torch.export.graph_signature import InputKind, OutputKind
17+
from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e
18+
19+
20+
class SimpleBNModel(nn.Module):
21+
def __init__(self) -> None:
22+
super().__init__()
23+
self.conv = nn.Conv1d(3, 8, kernel_size=3, padding=1)
24+
self.bn = nn.BatchNorm1d(8)
25+
26+
def forward(self, x: torch.Tensor) -> torch.Tensor:
27+
return torch.relu(self.bn(self.conv(x)))
28+
29+
30+
class MultiBNModel(nn.Module):
31+
def __init__(self) -> None:
32+
super().__init__()
33+
self.conv1 = nn.Conv1d(3, 8, kernel_size=3, padding=1)
34+
self.bn1 = nn.BatchNorm1d(8)
35+
self.conv2 = nn.Conv1d(8, 16, kernel_size=3, padding=1)
36+
self.bn2 = nn.BatchNorm1d(16)
37+
self.fc = nn.Linear(16, 4)
38+
39+
def forward(self, x: torch.Tensor) -> torch.Tensor:
40+
x = torch.relu(self.bn1(self.conv1(x)))
41+
x = torch.relu(self.bn2(self.conv2(x)))
42+
x = x.mean(dim=-1)
43+
return self.fc(x)
44+
45+
46+
class ReadBNTrackingModel(nn.Module):
47+
def __init__(self) -> None:
48+
super().__init__()
49+
self.bn = nn.BatchNorm1d(3)
50+
51+
def forward(self, x: torch.Tensor) -> torch.Tensor:
52+
num_batches_tracked = self.bn.num_batches_tracked
53+
assert num_batches_tracked is not None
54+
return self.bn(x) + num_batches_tracked.to(dtype=x.dtype)
55+
56+
57+
def _qat_export_to_edge(
58+
model: nn.Module,
59+
example_input: tuple[torch.Tensor, ...],
60+
) -> torch.export.ExportedProgram:
61+
"""Simulate the QAT export path that produces BN tracking mutations.
62+
63+
QAT models are traced in training mode (model.train()), then converted
64+
back to eval via move_exported_model_to_eval(). run_decompositions()
65+
in to_edge() then re-introduces num_batches_tracked mutations.
66+
"""
67+
from executorch.backends.cadence.aot.quantizer.quantizer import (
68+
CadenceFusedConvReluQuantizer,
69+
)
70+
71+
model.train()
72+
captured = torch.export.export(model, example_input, strict=False).module()
73+
prepared = prepare_pt2e(captured, CadenceFusedConvReluQuantizer(is_qat=True))
74+
75+
for _ in range(3):
76+
prepared(*example_input)
77+
78+
torchao.quantization.pt2e.move_exported_model_to_eval(prepared)
79+
converted = convert_pt2e(prepared)
80+
81+
exported = torch.export.export(converted, example_input)
82+
edge = to_edge(exported)
83+
return edge.exported_program()
84+
85+
86+
class RemoveBNTrackingMutationsTest(unittest.TestCase):
87+
def _get_nbt_mutations(self, ep: torch.export.ExportedProgram) -> dict[str, str]:
88+
return {
89+
k: v
90+
for k, v in ep.graph_signature.buffers_to_mutate.items()
91+
if "num_batches_tracked" in v
92+
}
93+
94+
def _get_nbt_placeholders(self, ep: torch.export.ExportedProgram) -> list[str]:
95+
placeholders: list[str] = []
96+
for n in ep.graph_module.graph.nodes:
97+
if (
98+
n.op == "placeholder"
99+
and isinstance(n.target, str)
100+
and "num_batches_tracked" in n.target
101+
):
102+
placeholders.append(n.target)
103+
return placeholders
104+
105+
def _get_nbt_input_specs(self, ep: torch.export.ExportedProgram) -> list[str]:
106+
input_specs: list[str] = []
107+
for s in ep.graph_signature.input_specs:
108+
if (
109+
s.kind == InputKind.BUFFER
110+
and s.target is not None
111+
and "num_batches_tracked" in s.target
112+
):
113+
input_specs.append(s.target)
114+
return input_specs
115+
116+
def _run_remove_pass_on_qat_model(
117+
self,
118+
model: nn.Module,
119+
example_input: tuple[torch.Tensor, ...],
120+
) -> torch.export.ExportedProgram:
121+
edge_ep = _qat_export_to_edge(model, example_input)
122+
nbt = self._get_nbt_mutations(edge_ep)
123+
self.assertGreater(
124+
len(nbt), 0, "expected pre-pass num_batches_tracked mutations"
125+
)
126+
127+
result = RemoveBNTrackingMutationsPass()(edge_ep)
128+
self.assertTrue(result.modified)
129+
return result.exported_program
130+
131+
def test_single_bn_no_tracking_mutations(self) -> None:
132+
model = SimpleBNModel()
133+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
134+
nbt = self._get_nbt_mutations(edge_ep)
135+
self.assertEqual(len(nbt), 0, f"num_batches_tracked mutations present: {nbt}")
136+
137+
def test_multi_bn_no_tracking_mutations(self) -> None:
138+
model = MultiBNModel()
139+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
140+
nbt = self._get_nbt_mutations(edge_ep)
141+
self.assertEqual(len(nbt), 0, f"num_batches_tracked mutations present: {nbt}")
142+
143+
def test_no_nbt_output_specs(self) -> None:
144+
model = MultiBNModel()
145+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
146+
nbt_specs = [
147+
s
148+
for s in edge_ep.graph_signature.output_specs
149+
if s.kind == OutputKind.BUFFER_MUTATION
150+
and s.target is not None
151+
and "num_batches_tracked" in s.target
152+
]
153+
self.assertEqual(
154+
len(nbt_specs), 0, f"num_batches_tracked output specs present: {nbt_specs}"
155+
)
156+
157+
def test_no_nbt_input_placeholders(self) -> None:
158+
"""All num_batches_tracked input placeholders should be removed."""
159+
model = MultiBNModel()
160+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
161+
nbt_placeholders = self._get_nbt_placeholders(edge_ep)
162+
self.assertEqual(
163+
len(nbt_placeholders),
164+
0,
165+
f"num_batches_tracked placeholders still present: {nbt_placeholders}",
166+
)
167+
168+
def test_no_nbt_input_specs(self) -> None:
169+
"""No input_specs for num_batches_tracked buffers should remain."""
170+
model = MultiBNModel()
171+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
172+
nbt_input_specs = self._get_nbt_input_specs(edge_ep)
173+
self.assertEqual(
174+
len(nbt_input_specs),
175+
0,
176+
f"num_batches_tracked input specs still present: {nbt_input_specs}",
177+
)
178+
179+
def test_live_nbt_input_spec_preserved(self) -> None:
180+
model = ReadBNTrackingModel()
181+
edge_ep = self._run_remove_pass_on_qat_model(model, (torch.randn(1, 3, 32),))
182+
183+
nbt_placeholders = self._get_nbt_placeholders(edge_ep)
184+
nbt_input_specs = self._get_nbt_input_specs(edge_ep)
185+
self.assertGreater(
186+
len(nbt_placeholders),
187+
0,
188+
"expected live num_batches_tracked placeholder to remain",
189+
)
190+
self.assertEqual(len(nbt_placeholders), len(nbt_input_specs))
191+
192+
def test_no_bn_model_unaffected(self) -> None:
193+
class NoBNModel(nn.Module):
194+
def __init__(self) -> None:
195+
super().__init__()
196+
self.linear = nn.Linear(8, 4)
197+
198+
def forward(self, x: torch.Tensor) -> torch.Tensor:
199+
return self.linear(x)
200+
201+
model = NoBNModel()
202+
model.eval()
203+
ep = torch.export.export(model, (torch.randn(1, 8),))
204+
edge_ep = to_edge(ep).exported_program()
205+
result = RemoveBNTrackingMutationsPass()(edge_ep)
206+
self.assertFalse(result.modified)
207+
self.assertEqual(
208+
len(result.exported_program.graph_signature.buffers_to_mutate), 0
209+
)

0 commit comments

Comments
 (0)