Repository navigation
Conversation
🔗 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. ✅ No FailuresAs of commit 8ba8560 with merge base 703a411 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
@pssrawat has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126708891. |
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
ed2f8b5 to
1dda032
Compare
1dda032 to
9de9200
Compare
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:
`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
9de9200 to
8ba8560
Compare
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
| tester.run_method_and_compare_outputs(inputs=inputs, qtol=1) | ||
|
|
||
|
|
||
| def test_explicit_layout_accepts_conv_fed_by_view_of_lut_activation(): |
There was a problem hiding this comment.
Please extend coverage to (1, 8, 16) input through tanh → Conv1d(8,8,1) → view(1,4,32) → Conv1d(4,4,1).
This still fails validation: canonicalization merges the annotated reshape into a synthetic view without qparams, and the helper falls back to its stale float32 fake tensor. Removing only validation produces two int8 convolutions and matching outputs.
Assert that the normal pipeline lowers both convolutions and matches the quantized reference. Pair that with a negative test for the explicit-float32 full_like case above, so validation both accepts valid chains and rejects float inputs to int8 kernels.
| not isinstance(input_value, torch.Tensor) | ||
| or input_value.dtype != torch.int8 | ||
| ): | ||
| input_dtype = ( |
There was a problem hiding this comment.
Please separate early layout-eligibility checks from dtype validation, and validate actual kernel inputs after AtenToCortexMPass has retraced the graph.
Conv1d(full_like(x, 0.5, dtype=torch.float32)) now lowers and serializes with a float32 tensor feeding quantized_conv2d_nhwc. Its folded qparams never change the producer’s explicit dtype.
The final check needs to inspect activation inputs to int8-only Cortex-M kernels. Checking the convolution’s output or its substitution registration misses this case because its output really does become int8. Moving this validator unchanged also won’t suffice: the channels_last.* targets disappear during lowering.
Keep this graph rejected unless the required quantization conversion is preserved.
Summary:
ValidateCortexMExplicitLayoutPassrequires every NHWC layout copy to movean int8 tensor, and reads
node.meta["val"].dtypeto decide. Inexplicit_layout_pass_listthat validator runs beforeAtenToCortexMPass,so at validation time only the activations that
ActivationFusionPasshasalready 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 becomescortex_m::softmax) are stillaten.*with an fp32 fake tensor, eventhough
FoldAndAnnotateQParamsPasshas 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))raisesout of
_CortexMLoweringPass.LutActivationThenConv1dintest_explicit_layout_pipeline.pyis that graph.Judge the condition the kernels will actually see.
produces_int8acceptsan int8 fake tensor as before, and otherwise reads the folded
output_qparams[0], which is in place well before this point. Graphs thatwere 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_int8also asks whether a rewrite is coming: eitherAtenToCortexMPasshas a substitution registered for the target, orQuantizedClampActivationPasshas already put the node on int8 terms. Itlives in
aten_to_cortex_m_pass.py, which owns the substitution registryand is therefore the module that can answer the question.
QuantizedClampActivationPassis the pass that makes the second armnecessary, 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 -> Conv1dleftaten.reluclamping at 0 instead of thezero point, erasing everything below it, where the relu should have been a
no-op;
tanh -> clamp(-0.5, 0.5) -> Conv2dkept fp32 limits and an fp32value on the tensor
cortex_m::quantized_conv2d_nhwcreads as int8. Bothfail an output comparison against the quantized eager module, so this was
wrong numerics, not a wrong annotation. It now asks
produces_int8, sevenpasses ahead of the validator, and records the clamps it quantized.
That record is a node predicate in
cortex_m/utils.py, alongsideskips_quantized_max_pool2d, which uses the samemeta["custom"]convention. A clamp never becomes a
cortex_m::kernel -- it stays on theportable 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_qparamsmoves tocortex_m/utils.pyas part of this. It was privateto
aten_to_cortex_m_pass; utils is already where this backend keeps itsnode 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