Skip to content

Substitute on qparams, not the input fake tensor (#24121) - #24121

Open
pssrawat wants to merge 2 commits into
pytorch:mainfrom
pssrawat:export-D127046193
Open

pssrawat wants to merge 2 commits into
pytorch:mainfrom
pssrawat:export-D127046193

Conversation

@pssrawat

@pssrawat pssrawat commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary:

_get_permute_replacement, _get_minimum_replacement and _get_maximum_replacement choose their kernel from the input fake tensor. That tensor is stale inside AtenToCortexMPass: AtenToDialectPass gives each new cortex_m node the replaced node's meta dict, so an input already rewritten from aten.tanh still reads fp32 until the retrace at the end of the pass.

Read lowered_output_dtype instead, as the layout validator and clamp pass now do. Rewritten nodes keep their folded output_qparams, so it reports int8 for them; a cortex_m dequantize has none and still reports float.

The cost was coverage, not numerics. Conv1d(tanh(x)) lowered its input-side layout move to a portable aten::permute_copy and its output-side move to cortex_m::transpose, and minimum(tanh(x), y) / maximum(tanh(x), y) stayed on portable kernels. That breaks the explicit-layout guarantee that every layout move is a CMSIS transpose, and a selective build that registers only the expected kernels fails to load.

Differential Revision: D127046193

@pssrawat
pssrawat requested a review from rascani as a code owner October 23, 2026 16:48
@pytorch-bot

pytorch-bot Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❌ 1 New Failure

As of commit eb39e44 with merge base 703a411 (image):

NEW FAILURE - The following job has failed:

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 23, 2026
@meta-codesync

meta-codesync Bot commented Oct 23, 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 D127046193.

@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.

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 reads the folded
`output_qparams[0]`, which is in place well before this point. 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.

Folded qparams on their own are the wrong bar, though, and this is what the
first version of the diff got wrong. They say what dtype a node was
annotated for, not that anything acts on the annotation, and a node nothing
rewrites keeps an fp32 kernel in the middle of an int8 chain. So
`produces_int8` also asks whether a rewrite is coming: either
`AtenToCortexMPass` has a substitution registered for the target, or
`QuantizedClampActivationPass` has already put the node on int8 terms. It
lives in `aten_to_cortex_m_pass.py`, which owns the substitution registry
and is therefore the module that can answer the question.

`QuantizedClampActivationPass` is the pass that makes the second arm
necessary, and it had the same premature read. It quantizes the bounds of a
clamp that will see int8, and decided that from the producer's fake tensor,
so a clamp behind a LUT activation was skipped -- harmless while the
validator rejected those graphs outright, silently wrong once it stopped.
`sigmoid -> relu -> Conv1d` left `aten.relu` clamping at 0 instead of the
zero point, erasing everything below it, where the relu should have been a
no-op; `tanh -> clamp(-0.5, 0.5) -> Conv2d` kept fp32 limits and an fp32
value on the tensor `cortex_m::quantized_conv2d_nhwc` reads as int8. Both
fail an output comparison against the quantized eager module, so this was
wrong numerics, not a wrong annotation. It now asks `produces_int8`, seven
passes ahead of the validator, and records the clamps it quantized.

That record is a node predicate in `cortex_m/utils.py`, alongside
`skips_quantized_max_pool2d`, which uses the same `meta["custom"]`
convention. A clamp never becomes a `cortex_m::` kernel -- it stays on the
portable one and reads int8 only because its bounds were rewritten into
codes -- so no other pass can re-derive the decision, and a clamp the pass
declined (bounds that are not compile-time scalars, output qparams that are
not per-tensor) is indistinguishable from one it never saw. With the
decision recorded, a declined clamp is rejected by the validator instead of
being lowered.

`has_qparams` moves to `cortex_m/utils.py` as part of this. It was private
to `aten_to_cortex_m_pass`; 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
Summary:

`_get_permute_replacement`, `_get_minimum_replacement` and `_get_maximum_replacement` choose their kernel from the input fake tensor. That tensor is stale inside `AtenToCortexMPass`: `AtenToDialectPass` gives each new `cortex_m` node the replaced node's meta dict, so an input already rewritten from `aten.tanh` still reads fp32 until the retrace at the end of the pass.

Read `lowered_output_dtype` instead, as the layout validator and clamp pass now do. Rewritten nodes keep their folded `output_qparams`, so it reports int8 for them; a `cortex_m` dequantize has none and still reports float.

The cost was coverage, not numerics. `Conv1d(tanh(x))` lowered its input-side layout move to a portable `aten::permute_copy` and its output-side move to `cortex_m::transpose`, and `minimum(tanh(x), y)` / `maximum(tanh(x), y)` stayed on portable kernels. That breaks the explicit-layout guarantee that every layout move is a CMSIS transpose, and a selective build that registers only the expected kernels fails to load.

Differential Revision: D127046193
@meta-codesync meta-codesync Bot changed the title Substitute on qparams, not the input fake tensor Substitute on qparams, not the input fake tensor (#24121) Oct 23, 2026

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant