Repository navigation
Cortex-M: fix quantized lowering for decomposed attention - #22796
amacharla15 wants to merge 5 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22796
Note: Links to docs will display an error until the docs builds have been completed. ❗ 1 Active SEVsThere are 1 currently active SEVs. If your PR is affected, please view them below: ❌ 14 Awaiting Approval, 1 Unclassified FailureAs of commit c09864c with merge base 9918b00 ( AWAITING APPROVAL - The following workflows need approval before CI can run:
UNCLASSIFIED FAILURE - DrCI could not classify the following job because the workflow did not run on the merge base. The failure may be pre-existing on trunk or introduced by this PR:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
Hi @amacharla15! Thank you for your pull request and welcome to our community. Action RequiredIn order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you. ProcessIn order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA. Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks! |
|
|
|
@pytorchbot label "release notes: none" |
|
Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks! |
|
The following ciflow label(s) have been added but CI has not been triggered yet because the workflows are awaiting approval:
Once a maintainer approves the workflows (scroll to the bottom of the PR page), the corresponding CI jobs will be triggered automatically. Please ping one of the reviewers if you do not have access to approve and run workflows. |
|
Thanks for triggering the Cortex-M CI. The first CI run exposed a few regressions from the initial implementation that were broader than the intended fix:
I narrowed both parts in the latest commit:
I also narrowed the Transformer regression so it checks the relevant decomposed attention lowering without requiring unrelated LayerNorm MULs to lower. After these changes, locally:
I updated the PR description to reflect the narrowed implementation and scope. While the new CI run is waiting/running, I am also checking serialization/FVP loading of the lowered Transformer as an additional validation step. |
|
Additional runtime validation on the latest commit ( I serialized the decomposed Transformer and tested it on the Cortex-M55 Corstone-300 FVP. The stock Cortex-M runner initially could not load the full program because its selective operator list omits five portable operators present in the decomposed Transformer: The narrowed For local validation only, I rebuilt the runner with those five existing portable kernels enabled. No PR source files were changed, and the working tree remained clean. With that runner, the full path passed:
So the end-to-end FVP validation passes with the required portable operators registered. The CI checks for |
|
I also ran a few targeted ablations on the latest implementation:
So the two materialization passes are independently needed at different stages, and the retracing fix is required in both passes. These were local-only checks; the working tree was restored afterward. |
There was a problem hiding this comment.
🟡 Changes recommended
Critical issues remain for explicit-layout integration and dynamic-shape scalar broadcasting, with additional constant-materialization and test-coverage concerns.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Fixes Cortex-M lowering for explicitly decomposed, quantized Transformer attention.
Changes:
- Adds Cortex-M-aware quantized retracing.
- Materializes scalar broadcasts and quantized MUL constants.
- Adds Transformer lowering regression coverage.
File summaries
| File | Reviewed changes |
|---|---|
backends/cortex_m/test/models/test_transformer.py |
Adds decomposed Transformer regression coverage. |
backends/cortex_m/passes/replace_scalar_with_tensor_pass.py |
Uses Cortex-M-aware scalar replacement. |
backends/cortex_m/passes/replace_quant_nodes_pass.py |
Uses Cortex-M-aware retracing. |
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py |
Materializes quantized MUL constants. |
backends/cortex_m/passes/materialize_mul_broadcast_pass.py |
Materializes scalar MUL broadcasts. |
backends/cortex_m/passes/cortex_m_quantized_pass.py |
Preserves quantized linear output dtypes. |
backends/cortex_m/passes/cortex_m_pass_manager.py |
Integrates the lowering passes. |
Review details
Suppressed comments (2)
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:41
- This predicate succeeds when any user path reaches a quantized MUL, but
ComputeConstantOpsAOTPass.compute_node_aotthen replaces all users of theaten.fullnode with one quantized placeholder. If a shared full constant also feeds an unrelated float operation, that consumer receives the int8 buffer and its dtype/value changes. Clone/materialize only the eligible MUL use, or require all users to have compatible quantization.
if user.target == exir_ops.edge.aten.mul.Tensor:
return bool(user.meta.get("input_qparams")) and bool(
user.meta.get("output_qparams")
)
backends/cortex_m/test/models/test_transformer.py:97
- This only proves that some
mullowered tocortex_m.quantized_mul; it does not prove that the decomposed attention scaling multiply was the one lowered. The Transformer/decomposition graph contains multiple MULs, andcheck()only checks for presence, so the test can pass while the problematicaten.mul.Tensorremains. Please add a targeted assertion for the scale node (or a smaller graph where that MUL is unambiguous), while keeping the intentional allowance for unrelated Transformer MULs.
"executorch_exir_dialects_edge__ops_cortex_m_quantized_mul_default",
- Files reviewed: 7/7 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved pipeline coverage, packaging, and quantized lowering issues block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (5)
backends/cortex_m/passes/cortex_m_pass_manager.py:63
- These new passes are wired only into
legacy_pass_list. Theexplicit_layout_pass_liststill uses the generic scalar-replacement pass and omitsMaterializeQuantizedMulConstantsPass, souse_explicit_layout=Truedoes not get the post-qparam constant materialization or dtype-preserving retrace needed by the same decomposed attention graph. Add the equivalent passes to that list or explicitly constrain this fix to legacy lowering.
MaterializeQuantizedMulConstantsPass,
CortexMReplaceScalarWithTensorArgPass,
backends/cortex_m/passes/cortex_m_quantized_pass.py:79
- This base pass retraces the entire graph, but the fallback here only preserves quantized
linear. A quantizedaten.bmmalso has requantized output qparams afterFoldAndAnnotateQParamsPass; falling through to the generic fakeaten.bmmrecreates its accumulator dtype, so the scalar-replacement retrace can feed later attention ops with metadata that no longer matches the Cortex-M BMM output. Include BMM in this no-fake-kernel path (the existing Arm pass already treats it this way).
return super().call_operator(op, args, kwargs, meta)
backends/cortex_m/passes/materialize_mul_broadcast_pass.py:30
numel() == 1is not enough to identify a scalar-generated constant: a model can also contain an ordinaryaten.full((1,), ...)tensor used by a MUL. This pass will expand that unrelated constant to the full output shape before quantization, potentially creating large persistent buffers and changing the fallback behavior the PR says it is preserving. Mark full nodes created by scalar replacement and require that marker here.
return get_first_fake_tensor(node).numel() == 1
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:58
- This path explicitly accepts
full -> clone -> mul, but the AOT replacement only turns thefullinto a precomputed buffer; it never changes the clone's shape. When the full is still the rank-1 scalar created by scalar replacement, the clone remains rank-1 and the later Cortex-Mquantized_mulstill receives an unsupported broadcast. Make the shape-materialization pass follow the same passthrough chain (or reject it here) so every accepted path has matching MUL input shapes.
if not self._feeds_single_quantized_mul(node):
return False
return super().compute_node_aot(node)
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:45
- This predicate accepts a partially annotated MUL because it only checks that
input_qparamsis non-empty, but_get_mul_replacementlater unconditionally readsinput_qparams[0]and[1]. If only one operand is quantized, this pass can materialize the constant and leave a graph that fails during lowering; require both operand entries (and make the positive unit test model both).
return bool(user.meta.get("input_qparams")) and bool(
user.meta.get("output_qparams")
)
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Quantized BMM handling and constant-size restrictions need correction, and pass-level behavior remains insufficiently tested.
Review details
Suppressed comments (3)
backends/cortex_m/passes/cortex_m_quantized_pass.py:70
- This override only avoids fake-kernel execution for
aten.linear, but both new subclasses retrace the entire folded graph and still send quantizedaten.bmmnodes through the genericExportPass.call_operator. The existingArmPassexplicitly handles quantized BMM because the ATen fake kernel cannot propagate int8 inputs (backends/arm/_passes/arm_pass.py:89-98,backends/cortex_m/passes/matmul_to_bmm_pass.py:82-84), so the same int32/error path can remain after the linear fix. Preserve that no-fake-kernel BMM handling as well.
def call_operator(self, op, args, kwargs, meta):
if (
op == exir_ops.edge.aten.linear.default
and isinstance(meta, NodeMetadata)
and self._is_quantized_meta(meta)
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:57
- This pass is documented as handling scalar
aten.fullconstants, but this predicate never checks the tensor size. Any quantizedaten.fullof arbitrary shape with a single passthrough path into a quantized MUL will therefore be converted into a persistent buffer byComputeConstantOpsAOTPass, which can unnecessarily inflate the Cortex-M PTE for large masks or lookup tensors. Restrict this pass to one-element constants (or make the intended size policy explicit).
output_qparams = node.meta.get("output_qparams", {})
if set(output_qparams) != {0}:
return False
if not self._feeds_single_quantized_mul(node):
return False
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:56
- The new tests cover only
_feeds_single_quantized_mul; they never runcompute_node_aot()/call()on an exported graph. The behavior introduced here—quantizing and replacing an eligibleaten.fullwith a buffer while rejecting unrelated constants—therefore remains unverified, and these predicate tests would pass if the actual pass were a no-op. Add a pass-level graph test that inspects the transformed graph and state dict.
def compute_node_aot(self, node: Node) -> bool:
output_qparams = node.meta.get("output_qparams", {})
if set(output_qparams) != {0}:
return False
if not self._feeds_single_quantized_mul(node):
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Two unresolved comments request end-to-end pass-level coverage for quantized constant materialization.
Review details
Suppressed comments (2)
backends/cortex_m/passes/materialize_quantized_mul_constants_pass.py:56
- Please add a pass-level test for this new
compute_node_aotpath. The added unit tests only exercise_feeds_single_quantized_mul, so a regression that stops replacing theaten.fullwith a quantized persistent constant (or mishandles its qparams) would still pass; the transformer test validates lowering and numerics but not this materialization. Assert the pass result and the resulting graph/buffer state after invoking the pass.
def compute_node_aot(self, node: Node) -> bool:
output_qparams = node.meta.get("output_qparams", {})
if set(output_qparams) != {0}:
return False
if not self._feeds_single_quantized_mul(node):
backends/cortex_m/test/test_materialize_quantized_mul_constants.py:36
- This only tests the private path predicate; it never runs
MaterializeQuantizedMulConstantsPass.call()orcompute_node_aot(), so regressions in the actualaten.fullreplacement/quantization would still pass. Add an end-to-end pass test that verifies the full node is replaced with the expected quantized constant while preserving the clone path.
assert MaterializeQuantizedMulConstantsPass._feeds_single_quantized_mul(full)
- Files reviewed: 9/9 changed files
- Comments generated: 0 new
- Review effort level: Lite
|
@zingo the workflows on |
AdrianLundell
left a comment
There was a problem hiding this comment.
Hello @amacharla15 and welcome with your first contribution to Executorch!
There are a number of things going on in this patch, as a general tip I strongly recommend splitting it up into several PR:s with one pass each for example, with briefer commit messages. This will significantly increase the chance of getting the changes merged since it is much faster to review and an issue in one pass does not block all other passes for example.
In regards to the patch:
Linear Q/DQ change: I happen to know that there will be a prototype for changing how the folded q/dq parameters are handled in the graph coming up just this week, so it makes sense to wait for that before merging a new solution, bad timing unfortunately.
Matrialize mul change: Introducing full shape constants for a single scalar mul is not so efficient, would prefer to see this by modifying the qparams, or a new accelerated kernel if there are cases where that does not work.
Matrialize constant change: Likely not need with other mul approach.
Let me know if you have any questions! Best regards,
Adrian
|
Thanks @AdrianLundell ; Here is my observation and work: I split the linear and scalar-MUL issues apart and followed up on your suggestion to handle the scalar scale through qparams rather than materializing a full-shape constant. While investigating that path, I found the existing draft PR #21027, which removes a The SDPA decomposition splits the attention scale across Q and K, with each branch using
The other 4 are correctly rejected by the fold, so those remaining MULs originate from the qparam choices made earlier during quantization rather than from the folding logic itself. I then prototyped a small Cortex-M quantizer stage that derives the scaled activation qparams from the input:
for the positive scalar-scaled BMM-input pattern emitted by the existing SDPA decomposition. One detail from the real graph: the scaled value reaches BMM through With that change, all 6/6 attention-scale branches in the Transformer reproducer satisfy the identity relation needed by the later fold. I also tested the actual decomposed SDPA path with head dimensions 16 and 64, including the non-exact I have kept the linear int8/int32 metadata issue separate and stopped pursuing the Cortex-M-specific retracing workaround there, since you mentioned the upcoming folded-Q/DQ prototype. Does this derived-qparam approach look like the direction you had in mind for the scalar-MUL issue? If so, I can clean it up and send it as a separate focused PR. |
|
Sounds like good progress, I like this direction so definitely worth opening a PR! As a general heads up I prefer to keep the quantizer simple and predictable, do any decomps needed to get correct qparams in transform_for_annotation, and then do fusings in the cortex-m lowering pass pipeline, if this is possible rather than the derived-qparam approach. Having a clear split of responsibilities across the different stages of the lowering makes it easier to understand. |
|
Thanks @AdrianLundell , I implemented the It moves the split default-SDPA score scale after the score BMM, keeps I’ve kept the linear folded-Q/DQ issue separate as discussed. |
Summary
Fix Cortex-M lowering for explicitly decomposed Transformer attention so that quantized
bmm, softmax, and scalar-multiply operations can lower to Cortex-M operators.This does not add general Transformer support or automatically decompose Transformer operators. The regression test explicitly applies the existing SDPA and LayerNorm decomposition passes before PT2E quantization.
Problem
After decomposing Transformer attention before quantization,
_softmaxandbmmcould be quantized, but the attention scalingmuldid not lower correctly.There were three related issues in the Cortex-M lowering pipeline:
Scalar multiplication is represented as an
aten.fulltensor after scalar-to-tensor replacement. For attention scaling, this rank-1 scalar tensor needs to be materialized at the MUL output shape before quantization so that Cortex-M quantized MUL can be selected.After
FoldAndAnnotateQParamsPass, output requantization information is stored in node metadata while the graph still containsaten.linear. Generic ATen fake/meta retracing therefore reconstructs the linear output asint32, even though the Cortex-M quantized linear output is requantized toint8. This later causes anint8/int32mismatch when retracing BMM.Quantized scalar constants needed by MUL must be materialized after qparams are attached, without globally folding unrelated constant operations.
Changes
aten.linearnodes use the dtype described by their output qparams.aten.fulloperands when their shape must match a MUL output.aten.fullconstants that eventually feed a quantized MUL.torch.nn.Transformerwith explicit SDPA and LayerNorm decomposition.The regression verifies that decomposed attention lowers to:
cortex_m.quantized_batch_matmulcortex_m.softmaxcortex_m.quantized_muland that decomposed attention no longer retains the corresponding ATen BMM or softmax operations.
Validation
Local validation on the updated implementation:
qtol=2.9 passed, 4 xfailed.240 passed, 1 skipped, 24 xfailed.lintrunner -apasses, including mypy.Cortex-M55 FVP validation
As additional validation, I serialized the decomposed Transformer and tested it end-to-end on the Cortex-M55 Corstone-300 FVP.
The stock Cortex-M runner initially could not load the full decomposed Transformer because its selective operator list does not include several portable operators present in the resulting program:
aten::permute_copy.outaten::squeeze_copy.dims_outaten::expand_copy.outaten::mean.outaten::var.correction_outI also checked the remaining
expand_copyoperations explicitly. The decomposed Edge graph contains 12aten.expand_copynodes before the Cortex-M backend passes run, and the lowered graph contains the same 12 afterward. The narrowedMaterializeMulBroadcastPassin this PR therefore does not introduce additionalexpand_copyoperations; these shape operations are already present before Cortex-M lowering.For local runtime validation only, I rebuilt the Cortex-M runner with those five existing portable kernels enabled. No PR source files were changed, and the working tree remained clean.
With that runner, the full path completed successfully:
qtol=2The end-to-end FVP run passed.
Scope / limitations
This PR intentionally does not:
The changes are limited to making the existing explicit decomposition workflow lower the targeted quantized attention operations correctly while preserving existing behavior for unrelated Cortex-M graphs.
use_explicit_layout=Truepipeline is not covered by this fix. I reproduced the same foldedaten.linearint8/int32 retracing failure there. Mirroring the legacy-path fixes gets past the initial failure, but the same issue then appears while retracing throughReplaceSqueezeAndUnsqueezeWithViewPassandCortexMCanonicalizeViewCopyPermutePass. Supporting decomposed Transformer attention in explicit-layout mode therefore requires a broader Cortex-M-aware retracing change.Additional validation after review fixes
torchvision/torchaudiodependencies are not installed locally:466 passed, 5 skipped, 36 xfailed.qtol=2.qtol=2.lintrunner -apasses, including MYPY.Addresses #21943
cc @digantdesai @freddan80 @per @zingo @oscarandersson8218 @mansnils @Sebastian-Larsson @robell @rascani