Skip to content

Commit 33f68d1

Browse files
authored
Cortex-M: annotate and preserve in-place activations (#21819)
### Summary A model that runs its activation in place emits aten.silu_ rather than aten.silu in the pre-dispatch graph. This is not a corner case: Ultralytics gives every Conv block the same class-level nn.SiLU instance, which initialize_weights() then flips to inplace=True, so one attribute decides the whole network. ACTIVATION_OP_PATTERNS listed only the functional overloads, so the quantizer never annotated those nodes; FoldAndAnnotateQParamsPass then declined to fold them, since it folds only into nodes carrying ArmAnnotationInfo, and each activation was left as an fp32 island between two quantized convolutions. On yolo11n that is 76 of them, visible only as a count of unannotated nodes in the quantizer report. The other pattern dicts in this file already enumerate the in-place variants of relu, hardtanh, clamp and hardsigmoid, so this follows that convention rather than adding a normalization pass. aten.gelu_ is left out because no idiomatic model reaches it: there is no Tensor.gelu_, and neither nn.GELU nor F.gelu takes an inplace argument. Annotating alone is not enough on the compiler path users actually run. aot_arm_compiler.py preserved hardsigmoid and hardswish through to_edge but not silu, so silu decomposed into sigmoid * mul, the mul carried qparams on only one input, and AtenToCortexMPass raised a KeyError. That made an annotated conv+SiLU model fail to compile where it had previously produced a working fp32 island. Adding silu to that preserve list, and to the same list in the overview doc, makes it lower to quantized_conv2d + quantized_activation instead. The test harness had silu preserved already, which is why no existing test saw this. That list is Cortex-M specific, and the Arm backend maps aten.silu to a TOSA TABLE op, so preserving it does not disturb the Ethos-U path. ### Test plan pytest backends/cortex_m/test/ops/test_activation_quant.py -- 48 cases, dialect and Corstone-300, green; 24 dialect cases also green on cortex-m0plus and cortex-m7. Full backends/cortex_m/test dialect run: 407 passed. Removing the three dict entries fails the three new in-place cases; removing the preserve entry reproduces the compile failure above on conv + nn.SiLU(inplace=True). Authored with assistance from Claude Code. cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell
1 parent 5c3cb65 commit 33f68d1

4 files changed

Lines changed: 95 additions & 0 deletions

File tree

backends/arm/scripts/aot_arm_compiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,7 @@ def _to_channels_last(x):
973973
torch.ops.aten.hardsigmoid_.default,
974974
torch.ops.aten.hardswish.default,
975975
torch.ops.aten.hardswish_.default,
976+
torch.ops.aten.silu.default,
976977
],
977978
_check_ir_validity=False,
978979
),

backends/cortex_m/quantizer/quantizer_support.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,11 @@
125125

126126
ACTIVATION_OP_PATTERNS = {
127127
(torch.ops.aten.sigmoid.default,): CortexMActivationCheck,
128+
(torch.ops.aten.sigmoid_.default,): CortexMActivationCheck,
128129
(torch.ops.aten.tanh.default,): CortexMActivationCheck,
130+
(torch.ops.aten.tanh_.default,): CortexMActivationCheck,
129131
(torch.ops.aten.silu.default,): CortexMActivationCheck,
132+
(torch.ops.aten.silu_.default,): CortexMActivationCheck,
130133
(torch.ops.aten.gelu.default,): CortexMActivationCheck,
131134
}
132135

backends/cortex_m/test/ops/test_activation_quant.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,19 @@ def forward(self, x):
3939
return torch.sigmoid(x)
4040

4141

42+
# nn.Sigmoid and nn.Tanh take no `inplace` argument, so the tensor method is
43+
# the only way to reach aten.sigmoid_ / aten.tanh_ from Python.
44+
class _SigmoidInplace(torch.nn.Module):
45+
ops_before_transforms = {
46+
**_OPS_BEFORE,
47+
"executorch_exir_dialects_edge__ops_aten_sigmoid_default": 1,
48+
}
49+
ops_after_transforms = _OPS_AFTER
50+
51+
def forward(self, x):
52+
return x.sigmoid_()
53+
54+
4255
class _Tanh(torch.nn.Module):
4356
ops_before_transforms = {
4457
**_OPS_BEFORE,
@@ -50,6 +63,17 @@ def forward(self, x):
5063
return torch.tanh(x)
5164

5265

66+
class _TanhInplace(torch.nn.Module):
67+
ops_before_transforms = {
68+
**_OPS_BEFORE,
69+
"executorch_exir_dialects_edge__ops_aten_tanh_default": 1,
70+
}
71+
ops_after_transforms = _OPS_AFTER
72+
73+
def forward(self, x):
74+
return x.tanh_()
75+
76+
5377
class _SiLU(torch.nn.Module):
5478
ops_before_transforms = {
5579
**_OPS_BEFORE,
@@ -61,6 +85,48 @@ def forward(self, x):
6185
return torch.nn.functional.silu(x)
6286

6387

88+
class _SiLUInplace(torch.nn.Module):
89+
ops_before_transforms = {
90+
**_OPS_BEFORE,
91+
"executorch_exir_dialects_edge__ops_aten_silu_default": 1,
92+
}
93+
ops_after_transforms = _OPS_AFTER
94+
95+
def __init__(self):
96+
super().__init__()
97+
self.silu = torch.nn.SiLU(inplace=True)
98+
99+
def forward(self, x):
100+
return self.silu(x)
101+
102+
103+
class _ConvSiLUInplace(torch.nn.Module):
104+
"""The shape a real model has: the activation consumes a convolution
105+
output, so the conv is matched first by the per-channel quantizer and the
106+
activation only afterwards.
107+
"""
108+
109+
# No _OPS_BEFORE here: the convolution brings its own weight quant/dequant,
110+
# so the boundary counts the other cases share do not apply.
111+
ops_before_transforms = {
112+
"executorch_exir_dialects_edge__ops_aten_silu_default": 1,
113+
"executorch_exir_dialects_edge__ops_aten_convolution_default": 1,
114+
}
115+
ops_after_transforms = {
116+
"executorch_exir_dialects_edge__ops_cortex_m_quantized_activation_default": 1,
117+
"executorch_exir_dialects_edge__ops_cortex_m_quantized_conv2d_default": 1,
118+
"executorch_exir_dialects_edge__ops_aten_silu_default": 0,
119+
}
120+
121+
def __init__(self):
122+
super().__init__()
123+
self.conv = torch.nn.Conv2d(4, 8, 3, padding=1)
124+
self.silu = torch.nn.SiLU(inplace=True)
125+
126+
def forward(self, x):
127+
return self.silu(self.conv(x))
128+
129+
64130
class _GELU(torch.nn.Module):
65131
ops_before_transforms = {
66132
**_OPS_BEFORE,
@@ -111,6 +177,16 @@ def _zero_input(shape):
111177
model=_Sigmoid(),
112178
example_inputs=(_zero_input((16,)),),
113179
),
180+
# These three activate the placeholder itself, so calibration rewrites the
181+
# input tensor. Building it per call keeps one case from feeding the next;
182+
# within a case both sides still see the rewritten tensor, which narrows the
183+
# compared range. That is fine here -- they exist to prove the in-place
184+
# spelling gets annotated, and the functional siblings above already cover
185+
# the LUT over its full range -- but do not read them as range coverage.
186+
"sigmoid_inplace": McuTestCase(
187+
model=_SigmoidInplace(),
188+
example_inputs=lambda: (ramp_tensor(-4, 4, (1, 8, 4, 4)),),
189+
),
114190
"tanh_rank1": McuTestCase(
115191
model=_Tanh(),
116192
example_inputs=(ramp_tensor(-3, 3, (16,)),),
@@ -131,6 +207,10 @@ def _zero_input(shape):
131207
model=_Tanh(),
132208
example_inputs=(_zero_input((16,)),),
133209
),
210+
"tanh_inplace": McuTestCase(
211+
model=_TanhInplace(),
212+
example_inputs=lambda: (ramp_tensor(-2, 2, (1, 8, 4, 4)),),
213+
),
134214
"silu_rank1": McuTestCase(
135215
model=_SiLU(),
136216
example_inputs=(ramp_tensor(-6, 6, (16,)),),
@@ -151,6 +231,16 @@ def _zero_input(shape):
151231
model=_SiLU(),
152232
example_inputs=(_zero_input((16,)),),
153233
),
234+
"silu_inplace": McuTestCase(
235+
model=_SiLUInplace(),
236+
example_inputs=lambda: (ramp_tensor(-4, 4, (1, 8, 4, 4)),),
237+
),
238+
"conv_silu_inplace": McuTestCase(
239+
model=_ConvSiLUInplace(),
240+
example_inputs=lambda: (
241+
ramp_tensor(-4, 4, (1, 4, 8, 8)).to(memory_format=torch.channels_last),
242+
),
243+
),
154244
"gelu_rank1": McuTestCase(
155245
model=_GELU(),
156246
example_inputs=(ramp_tensor(-6, 6, (16,)),),

docs/source/backends/arm-cortex-m/arm-cortex-m-overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ config = EdgeCompileConfig(
113113
torch.ops.aten.hardsigmoid_.default,
114114
torch.ops.aten.hardswish.default,
115115
torch.ops.aten.hardswish_.default,
116+
torch.ops.aten.silu.default,
116117
],
117118
_check_ir_validity=False,
118119
_core_aten_ops_exception_list=[torch.ops.aten.max_pool2d.default],

0 commit comments

Comments
 (0)