Skip to content

Validate layout copies on qparams, not fake-tensor dtype (#24071) - #24071

Open
pssrawat wants to merge 1 commit into
pytorch:mainfrom
pssrawat:export-D126708891
Open

pssrawat wants to merge 1 commit into
pytorch:mainfrom
pssrawat:export-D126708891

Conversation

@pssrawat

@pssrawat pssrawat commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

ValidateCortexMExplicitLayoutPass requires every NHWC layout copy to move
an int8 tensor, and reads node.meta["val"].dtype to decide. In
explicit_layout_pass_list that validator runs before AtenToCortexMPass,
so at validation time only the activations that ActivationFusionPass has
already folded into the convolution clamp -- relu, clamp, hardtanh -- carry
an int8 fake tensor. The LUT activations (tanh, sigmoid, which become
cortex_m::quantized_activation) and softmax (which becomes
cortex_m::softmax) are still aten.* with an fp32 fake tensor, even
though FoldAndAnnotateQParamsPass has already attached their int8 qparams.
That fp32 propagates into any convolution fed by one, and the layout copy on
its output trips the assertion.

The explicit-layout path validates rather than falling back, so this is a
hard abort, not a coverage loss -- and explicit layout is the only mode that
supports Conv1d. A two-node graph reproduces it: Conv1d(relu(x)) lowers,
Conv1d(tanh(x)) raises

RuntimeError: Cortex-M layout copy channels_last_permute_copy_default
must move an int8 tensor.

out of _CortexMLoweringPass. LutActivationThenConv1d in
test_explicit_layout_pipeline.py is that graph.

Judge the condition the kernels will actually see. _produces_int8 accepts
an int8 fake tensor as before, and otherwise asks has_qparams, which is
what AtenToCortexMPass reads before it will rewrite a node at all, plus an
int8 output_qparams[0]. Both annotations are in place by the time this
pass runs. Anything looser accepts a node the dialect goes on to decline,
which really does leave a portable fp32 kernel feeding an int8 transpose.
Graphs that were rejected lower unchanged: on a 31-convolution ECAPA-TDNN
speaker model, 0 of 31 convolutions reached a CMSIS-NN kernel before and 31
of 31 do after, with 0.000e+00 max absolute difference against the
quantized eager module.

has_qparams moves to cortex_m/utils.py to make that sharing possible.
It was private to aten_to_cortex_m_pass, and a pass that has to predict a
later one should not be reaching into it for the rule; utils is already
where this backend keeps its node predicates. Its ten existing call sites
change in name only.

The error message now also names the producing node. The layout copy is
synthesized by this pass and appears in no graph the caller holds, so its
own name gives nothing to go on.

Differential Revision: D126708891

@pssrawat
pssrawat requested a review from rascani as a code owner October 21, 2026 20:35
@pytorch-bot

pytorch-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/24071

Note: Links to docs will display an error until the docs builds have been completed.

❌ 1 New Failure, 2 Unrelated Failures

As of commit 1dda032 with merge base 9bf80eb (image):

NEW FAILURE - The following job has failed:

FLAKY - The following job failed but was likely due to flakiness present on trunk:

BROKEN TRUNK - The following job failed but was 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 Oct 21, 2026
@meta-codesync

meta-codesync Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@pssrawat has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126708891.

@pssrawat pssrawat added the release notes: none Do not include this in the release notes label Oct 21, 2026
Summary:

`ValidateCortexMExplicitLayoutPass` requires every NHWC layout copy to move
an int8 tensor, and reads `node.meta["val"].dtype` to decide. In
`explicit_layout_pass_list` that validator runs before `AtenToCortexMPass`,
so at validation time only the activations that `ActivationFusionPass` has
already folded into the convolution clamp -- relu, clamp, hardtanh -- carry
an int8 fake tensor. The LUT activations (tanh, sigmoid, which become
`cortex_m::quantized_activation`) and softmax (which becomes
`cortex_m::softmax`) are still `aten.*` with an fp32 fake tensor, even
though `FoldAndAnnotateQParamsPass` has already attached their int8 qparams.
That fp32 propagates into any convolution fed by one, and the layout copy on
its output trips the assertion.

The explicit-layout path validates rather than falling back, so this is a
hard abort, not a coverage loss -- and explicit layout is the only mode that
supports Conv1d. A two-node graph reproduces it: `Conv1d(relu(x))` lowers,
`Conv1d(tanh(x))` raises

    RuntimeError: Cortex-M layout copy channels_last_permute_copy_default
    must move an int8 tensor.

out of `_CortexMLoweringPass`. `LutActivationThenConv1d` in
`test_explicit_layout_pipeline.py` is that graph.

Judge the condition the kernels will actually see. `_produces_int8` accepts
an int8 fake tensor as before, and otherwise asks `has_qparams`, which is
what `AtenToCortexMPass` reads before it will rewrite a node at all, plus an
int8 `output_qparams[0]`. Both annotations are in place by the time this
pass runs. Anything looser accepts a node the dialect goes on to decline,
which really does leave a portable fp32 kernel feeding an int8 transpose.
Graphs that were rejected lower unchanged: on a 31-convolution ECAPA-TDNN
speaker model, 0 of 31 convolutions reached a CMSIS-NN kernel before and 31
of 31 do after, with 0.000e+00 max absolute difference against the
quantized eager module.

`has_qparams` moves to `cortex_m/utils.py` to make that sharing possible.
It was private to `aten_to_cortex_m_pass`, and a pass that has to predict a
later one should not be reaching into it for the rule; utils is already
where this backend keeps its node predicates. Its ten existing call sites
change in name only.

The error message now also names the producing node. The layout copy is
synthesized by this pass and appears in no graph the caller holds, so its
own name gives nothing to go on.

Differential Revision: D126708891
@meta-codesync meta-codesync Bot changed the title Validate layout copies on qparams, not fake-tensor dtype Validate layout copies on qparams, not fake-tensor dtype (#24071) Oct 21, 2026

@rascani rascani left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@claude review this PR

if not has_qparams(input_node):
return False
folded = input_node.meta["output_qparams"].get(0)
return folded is not None and folded.dtype == torch.int8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

QuantizedClampActivationPass also checks fake-tensor dtype and skips rewriting clamps after LUT activations. The new validator then accepts those graphs:

  • sigmoid → ReLU → Conv1d clamps at integer 0 instead of zero-point −128.
  • tanh → clamp(-0.5, 0.5) → Conv2d serializes float inputs feeding int8-only Cortex-M kernels.

Update the clamp rewrite to handle folded qparams before admitting these graphs, and add activation-chain regression tests.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. meta-exported release notes: none Do not include this in the release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants