Cortex-M: annotate and preserve in-place activations - #21819
Conversation
### 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). backends/cortex_m/test/models/test_yolo11.py exercises the motivating model, but it importorskips ultralytics, which is not installed in the cortex-m CI image, so it does not run in CI. Authored with assistance from Claude Code.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21819
Note: Links to docs will display an error until the docs builds have been completed. ❌ 2 New Failures, 1 Unrelated Failure, 3 Unclassified FailuresAs of commit 55aab80 with merge base a56af1c ( NEW FAILURES - The following jobs have failed:
UNCLASSIFIED FAILURES - DrCI could not classify the following jobs because the workflow did not run on the merge base. The failures may be pre-existing on trunk or introduced by this PR:
BROKEN TRUNK - The following job failed but were present on the merge base:👉 Rebase onto the `viable/strict` branch to avoid these failures
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
Erik-Lundell
left a comment
There was a problem hiding this comment.
LGTM. One question
| torch.ops.aten.hardsigmoid_.default, | ||
| torch.ops.aten.hardswish.default, | ||
| torch.ops.aten.hardswish_.default, | ||
| torch.ops.aten.silu.default, |
There was a problem hiding this comment.
Is there a longer term solution for this - how does the end user know that silu should be supplied here to get correct handling of silu?
| torch.ops.aten.hardsigmoid_.default, | ||
| torch.ops.aten.hardswish.default, | ||
| torch.ops.aten.hardswish_.default, | ||
| torch.ops.aten.silu.default, |
There was a problem hiding this comment.
Ok, I guess this answers my question about how to know. But still doesn't seem super scalable?
There was a problem hiding this comment.
Agreed. I forgot to tag you on #21826. It was actually worse than just these 2 places.
pytorch#21819 landed the same silu preserve_ops entry this branch was adding, in the two copies it touched: the AOT compiler and the docs recipe. Both conflicts resolve to the shared factory, whose op set is byte-identical to the one pytorch#21819 settled on, so the merge keeps their fix and removes the remaining copies of it. The docs snippet also loses the _core_aten_ops_exception_list that main still carries, which is the deliberate change in the previous commit: it feeds a verifier that _check_ir_validity=False disables. Still outstanding after the merge, and the reason this branch is worth landing on top of pytorch#21819: the cortex-m skill doc and the two examples remain copies, and the skill doc passes no preserve_ops at all.
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