Skip to content

Cortex-M: annotate and preserve in-place activations - #21819

Merged
rascani merged 1 commit into
pytorch:mainfrom
rascani:cortex-m-inplace-activations
Aug 14, 2026
Merged

Cortex-M: annotate and preserve in-place activations#21819
rascani merged 1 commit into
pytorch:mainfrom
rascani:cortex-m-inplace-activations

Conversation

@rascani

@rascani rascani commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

### 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.
@pytorch-bot

pytorch-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🔗 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 Failures

As of commit 55aab80 with merge base a56af1c (image):

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.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 13, 2026
@github-actions github-actions Bot added ciflow/trunk module: arm Issues related to arm backend labels Aug 13, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

@Erik-Lundell Erik-Lundell left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. One question

torch.ops.aten.hardsigmoid_.default,
torch.ops.aten.hardswish.default,
torch.ops.aten.hardswish_.default,
torch.ops.aten.silu.default,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I guess this answers my question about how to know. But still doesn't seem super scalable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I forgot to tag you on #21826. It was actually worse than just these 2 places.

@rascani
rascani merged commit 33f68d1 into pytorch:main Aug 14, 2026
497 of 506 checks passed
@rascani
rascani deleted the cortex-m-inplace-activations branch August 14, 2026 17:05
rascani added a commit to rascani/executorch that referenced this pull request Aug 14, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ciflow/trunk CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: arm Issues related to arm backend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants