Cortex-M backend: dispatch quantized_linear AOT layout on target ISA#19676
Cortex-M backend: dispatch quantized_linear AOT layout on target ISA#19676rascani wants to merge 1 commit into
Conversation
CMSIS-NN's `arm_fully_connected_s8` has three runtime paths, gated by compile-time `ARM_MATH_MVEI` / `ARM_MATH_DSP`. They split the bias and input_offset×sum(weight) offset term between two inputs, in incompatible conventions: * MVE (Helium): reads `ctx.buf` as a precomputed kernel_sum that must already include `input_offset × sum(weight)` and the bias contribution. The `bias` argument is `(void)bias;` — ignored. * DSP / scalar (Armv7E-M, Armv7-M, Armv6-M, Armv8-M Baseline): read the `bias` argument directly and fold the input_offset contribution at runtime. `ctx.buf` (kernel_sum) is `(void)kernel_sum;` — ignored. `ConvertToCortexMPass._get_linear_replacement` previously emitted only the MVE shape (kernel_sum populated, bias=None). On any non-MVE build the DSP/scalar path started the int32 accumulator at 0 instead of at `bias + input_offset × sum(weight)`, dropping both the bias and the offset contribution. The accumulator wound up much smaller than intended, requantization collapsed it to the output zero point, and every classifier with a deep, narrow tail produced essentially uniform near-zero outputs on Cortex-M0/0+/M3/M4/M7/M23/M33 builds — exactly where `CortexMTargetConfig.backend != cmsis_nn.Backend.MVE`. Use the target-ISA plumbing added by the CortexMTargetConfig PR (pytorch#19470) to dispatch the right input shape at AOT time: on MVE targets emit kernel_sum with bias folded in (bias=None); on DSP and scalar targets emit the raw int32 bias directly (kernel_sum=None). The CMSIS-NN runtime then matches exactly what it expects — no redundant copy of the bias in the .pte, no silent miscompute on target mismatch (the runtime errors loudly if ctx.buf is None on an MVE build, instead of producing garbage). Update `quantized_linear_impl` in `operators.py` to mirror the same contract: dispatch off whichever of kernel_sum / bias is non-None. Threading happens automatically via `CortexMPassManager`'s signature injection of `target_config` into the pass's `__init__`. Add `backends/cortex_m/test/misc/test_quantized_linear_small_magnitude.py` as a regression. A tiny `nn.Linear(512, 10)` on uniform[0, 0.002] input is the minimal reproducer for the small-magnitude regime where the missing offset terms dominate. The dialect test parametrizes over MVE/DSP/scalar target configs; the implementation test runs against whatever path the runner build matches. Identified by bisecting STResNet Pico's int8-output collapse on Corstone-300. The same collapse explains the historical MV2 / MV3 "deep classifier PTQ flakiness" xfails — both classifiers have small-magnitude inputs to their final quantized_linear and likely hit the same bug on any non-MVE deployment target. Authored with Claude.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/19676
Note: Links to docs will display an error until the docs builds have been completed. ❗ 2 Active SEVsThere are 2 currently active SEVs. If your PR is affected, please view them below: ❌ 8 New FailuresAs of commit 776a3a1 with merge base 1c9c115 ( NEW FAILURES - The following jobs have failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
Summary
CMSIS-NN's
arm_fully_connected_s8has three runtime paths, gated by compile-timeARM_MATH_MVEI/ARM_MATH_DSP. They split the bias and input_offset×sum(weight) offset term between two inputs, in incompatible conventions:ctx.bufas a precomputed kernel_sum that must already includeinput_offset × sum(weight)and the bias contribution. Thebiasargument is(void)bias;— ignored.biasargument directly and fold the input_offset contribution at runtime.ctx.buf(kernel_sum) is(void)kernel_sum;— ignored.ConvertToCortexMPass._get_linear_replacementpreviously emitted only the MVE shape (kernel_sum populated, bias=None). On any non-MVE build the DSP/scalar path started the int32 accumulator at 0 instead of atbias + input_offset × sum(weight), dropping both the bias and the offset contribution. The accumulator wound up much smaller than intended, requantization collapsed it to the output zero point, and every classifier with a deep, narrow tail produced essentially uniform near-zero outputs on non-MVE Cortex-M builds.Use the target-ISA plumbing added by the CortexMTargetConfig PR (#19470) to dispatch the right input shape at AOT time: on MVE targets emit kernel_sum with bias folded in (bias=None); on DSP and scalar targets emit the raw int32 bias directly (kernel_sum=None). The CMSIS-NN runtime then matches exactly what it expects.
Update
quantized_linear_implinoperators.pyto mirror the same contract: dispatch off whichever of kernel_sum / bias is non-None. Threading happens automatically viaCortexMPassManager's signature injection oftarget_configinto the pass's__init__.Test Plan
Add
backends/cortex_m/test/misc/test_quantized_linear_small_magnitude.pyas a regression. A tinynn.Linear(512, 10)on uniform[0, 0.002] input is the minimal reproducer for the small-magnitude regime where the missing offset terms dominate. The dialect test parametrizes over MVE/DSP/scalar target configs; the implementation test runs against whatever path the runner build matches.The DSP & Scalar tests will need #19520 for CI testing.
Authored with Claude.