ExecuTorch NS Tier-1 Kernels#
ExecuTorch PTEs exported for Cortex-M can be lowered against two CMSIS-NN
providers. The arm provider uses upstream arm-cmsis-nn and leaves ops
outside the stock cortex_m:: set as portable ATen fallbacks — float
kernels wrapped in a dequantize/requantize island. The ns provider adds
Ambiq's out-of-tree cortex_m_ns:: Tier-1 operators (sub, hardswish, mean,
relu/relu6/hardtanh/clamp, leaky_relu) backed by ns-cmsis-nn, so those ops
stay int8 end to end.
This page is the measured evidence for that difference. See Inference Engines for how to turn the NS kernels on.
Measurement provenance
Captured 2026-08-19 on an Apollo510 EVB (Cortex-M55, 96 MHz LP clock,
RTT transport), 25 iterations / 5 warmup, median aggregation, CPU + MVE +
memory PMU presets. Every run reported validity: valid.
Runtime: nsx-executorch main @ 4a257def (the PR #2 merge adding the
out-of-tree cortex_m_ns:: operators), ExecuTorch submodule 3a97429
(v1.3.0), arm-cmsis-nn 6d21a6f, ns-cmsis-nn v7.29.2 6317264. Numbers
are from one specific bench and one runtime pin — treat the ratios as
the transferable result, not the absolute cycle counts.
Models#
Each pair is two deterministic random-weight int8 exports of the same
PyTorch module, produced by
tools/export/executorch_tier1/export_tier1.py through
nsx_cortex_m.export(kernel_provider=...):
tier1 — channels_last conv trunk (16×32×32 activations):
conv → hardswish → conv → leaky_relu → sub → relu.
- arm PTE:
aten::leaky_relu.out,aten::sub.out, andaten::clamp.out(the relu) run as float portable kernels inside a dq…q island; hardswish uses the stockcortex_m::minimum+cortex_m::quantized_muldecomposition. - ns PTE:
cortex_m_ns::quantized_hardswish/leaky_relu/sub/relu, zero portable ops.
tier1mean — contiguous-layout micro model: mean(dim=(2,3), keepdim).
- arm PTE: float portable
aten::mean.out; ns PTE:cortex_m_ns::quantized_mean. - mean is kept out of the conv trunk deliberately — see Caveats.
Configs live in configs/executorch/tier1_{arm,ns}.yaml and
configs/executorch/tier1mean_{arm,ns}.yaml; the ns runs set
engine.config.ns_ops: true. The tier1_{arm,ns}_atfe.yaml variants build
the same PTEs with ATfE (requires ATFE_ROOT), so a gcc-vs-ATfE toolchain
comparison is one pair of runs plus
hpx compare results/tier1_arm results/tier1_arm_atfe.
Verifying the right kernels ran#
Three independent checks, because "the build succeeded" does not prove which kernels linked:
- Link-time symbols —
tools/verify_executorch_kernels.pyrunsarm-none-eabi-nmover the built firmware.armbuilds show the portable registration table and namedtorch::executor::native::kernels, and noarm_convolve_weight_sumorcortex_m_nssymbols.nsbuilds show the ns-cmsis-nn v7.29.2 weight-sum ABI (arm_convolve_weight_sum,arm_convolve_s8_get_weights_sum_size) plus thecortex_m_ns::native::quantized_*_outkernels. - Load-time fail-fast — a PTE containing
cortex_m_ns::ops failsMethod::load()with "operator missing" on any build withoutNSX_EXECUTORCH_ENABLE_NS_OPS=ON. Everynsrun that loads at all is itself proof of registration. The same holds for the selective portable list: thearmruns only load becauseNSX_EXECUTORCH_PORTABLE_SELECT_OPS_LISTregistered exactly the PTE's ATen fallbacks. - Per-instruction PMU signature — the captured instruction streams match
the operator counts recorded in
examples/models/tier1/tier1_manifest.jsonexactly (12 arm / 8 ns instructions fortier1; 5 / 3 fortier1mean).
Results — tier1 (conv trunk)#
| # | arm op | arm cycles | ns op | ns cycles |
|---|---|---|---|---|
| 0 | cortex_m::quantize |
92,689 | cortex_m::quantize |
92,831 |
| 1 | cortex_m::quantized_conv2d |
1,005,563 | cortex_m::quantized_conv2d |
1,001,585 |
| 2 | cortex_m::minimum (hardswish) |
8,443 | cortex_m_ns::quantized_hardswish |
153,135 |
| 3 | cortex_m::quantized_mul (hardswish) |
137,958 | cortex_m::quantized_conv2d |
1,000,661 |
| 4 | cortex_m::dequantize |
51,605 | cortex_m_ns::quantized_leaky_relu |
144,761 |
| 5 | cortex_m::quantized_conv2d |
1,004,539 | cortex_m_ns::quantized_sub |
222,067 |
| 6 | cortex_m::dequantize |
52,057 | cortex_m_ns::quantized_relu |
95,050 |
| 7 | aten::leaky_relu (portable, float) |
43,743 | cortex_m::dequantize |
52,226 |
| 8 | aten::sub (portable, float) |
560,030 | ||
| 9 | cortex_m::quantize |
86,982 | ||
| 10 | aten::clamp = relu (portable) |
413,279 | ||
| 11 | cortex_m::dequantize |
52,519 |
| Metric | arm | ns | Delta |
|---|---|---|---|
| Clean E2E cycles | 3,507,826 | 2,761,860 | −21.3% |
| Tier-1 op region1 | 1,156,091 | 461,878 | −60.0% (2.5×) |
| Planned arena | 196,608 B | 98,304 B | −50% (no float intermediates) |
Convolutions are effectively identical across providers (<0.5%), which confirms the delta comes from the Tier-1 ops rather than the conv path.
Results — tier1mean (mean micro model)#
| # | arm op | arm cycles | ns op | ns cycles |
|---|---|---|---|---|
| 0 | cortex_m::quantize |
87,874 | cortex_m::quantize |
92,963 |
| 1 | cortex_m::dequantize |
53,439 | cortex_m_ns::quantized_mean |
4,234 |
| 2 | aten::mean (portable, float) |
741,211 | cortex_m::dequantize |
740 |
| 3 | cortex_m::quantize |
866 | ||
| 4 | cortex_m::dequantize |
776 |
| Metric | arm | ns | Delta |
|---|---|---|---|
| Clean E2E cycles | 883,076 | 97,848 | −88.9% (9.0×) |
mean op alone |
741,211 | 4,234 | −99.4% (175×) |
The ns kernel is MVE-vectorized (arm_mean_reduce_spatial_mve_s8) and works
on int8 in place. The portable path dequantizes 16K values to float, reduces
scalar-by-scalar, and requantizes.
Reproducing#
$NSX_EXECUTORCH is the qualified nsx-executorch checkout.
# 1. Export both PTEs (deterministic, seed 20260819)
PYTHONPATH="$NSX_EXECUTORCH/external/executorch/src:$NSX_EXECUTORCH/aot" \
python tools/export/executorch_tier1/export_tier1.py \
--output-dir examples/models/tier1
# 2. Profile each side
hpx profile --config configs/executorch/tier1_arm.yaml
hpx profile --config configs/executorch/tier1_ns.yaml
# 3. Verify the linked kernels match the declared provider
python tools/verify_executorch_kernels.py --provider arm --expect-portable <arm ELF>
python tools/verify_executorch_kernels.py --provider ns --ns-ops <ns ELF>
# 4. Per-instruction cross-PTE comparison
python tools/export/executorch_tier1/compare_tier1.py \
--manifest examples/models/tier1/tier1_manifest.json \
--model tier1 --baseline results/tier1_arm --candidate results/tier1_ns
Add --jlink-serial <serial> to step 2 if more than one probe is connected.
Caveats#
meanand channels_last. The ns qualifier rejects channels_last input formean, and the portableaten::mean.outkernel failsMethod::execute()withInvalidArgument(error 18) on channels_last data. A channels_last graph that keepsmeanportable is therefore unrunnable on both providers. Keepmeanon contiguous tensors, or lower it tocortex_m_ns::quantized_mean.- Cross-PTE comparison.
hpx comparerequires the same model SHA, so it will not compare an arm run against an ns run. Usetools/export/executorch_tier1/compare_tier1.py, which matches operators through the export manifest instead. - Portable-op reporting.
nsx-executorch'sExportResult.portable_fallback_opsunder-reports — it misses theaten::relu.out/aten::leaky_relu.outedge spellings. Derive the portable op list from the serialized PTE operator list instead.
-
arm region = dq + leaky_relu + sub + q + clamp — the float island the portable fallbacks force. ns region = leaky_relu + sub + relu. ↩