You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
examples/arduino: Make the generated Arduino library actually run models (#21546)
## Summary
The Arduino library this directory generates could not compile, and had
it compiled it could not have run a model.
Kernels never reached the operator registry — ExecuTorch registers them
through codegen and the build script never ran it, so every
`Method::load` would have failed with `OperatorMissing`. CMSIS-NN was
never vendored, so the Cortex-M ops shipped without the library they
call. `schema/*.cpp` was never copied, the `*_aten.cpp` exclusion also
deleted the portable-mode `tensor_parser_exec_aten.cpp`, and `__errno`
was missing because the Zephyr core mixes picolibc with newlib's
`libm_nano`.
Both `minimal.cpp` and `zephyr.cpp` were vendored, so which `et_pal_*`
backend you got depended on link order, and every `ET_LOG` was discarded
either way. One backend now ships and its logs route to a hook the
examples implement against `Serial`.
The examples ship their models — previously none did, so every sketch
`#error`ed when opened from the IDE menu. The README pointed at the
Ethos-U `pte_to_header.py`, whose `network_model_sec` section no Arduino
core defines, so following the docs produced a model that fails
`Program::load`. Static link mode is mandatory and undocumented; the
Dynamic default yields a silently dead board. Renamed to `ExecuTorch`
because `arduino-lint` rejects "Arduino" in an Arduino library's name.
Registering every portable kernel costs 1.58 MB against 786 KB of flash,
so the op set is a curated default overridable via `ROOT_OPS`/`ALL_OPS`.
Models and libraries must come from the same ExecuTorch commit —
Cortex-M schemas change (`scratch` in #19636, #19825), and a mismatch
loads fine, resolves every operator, then fails at `Method::execute`.
The library now records and pins that commit. CI to enforce it follows
separately.
## Test plan
`arduino:zephyr:unoq`, board core 0.55.2, `link_mode=static`, flashed on
hardware:
```
HelloExecuTorch Model loaded OK!, 1 method 60% flash, 20% RAM
AddModel [1,2,3] + 1 = [2.00, 3.00, 4.00] 64% flash, 26% RAM
KeywordSpotting 10/10 keywords correct 70% flash, 53% RAM
```
All ten MFCC inputs in one sketch, exercising the CMSIS-NN conv /
depthwise / avgpool / linear kernels:
```
yes 8.95 no 4.78 up 4.63 down 9.72 left 7.87
right 7.41 on 12.03 off 8.02 stop 8.64 go 9.57
```
`arduino-lint --library-manager submit`: no errors, no warnings, under
both `specification` and `strict`. Clean regeneration is byte-identical
across all 626 generated files.
Only the Uno Q was tested; the other three boards remain marked Planned.
Authored with Claude Code (Opus 5).
The library is a *generated artifact*. Everything under the generated
216
+
`src/` is copied out of this repository, and the example models are
217
+
exported by this repository's Python. That gives one failure mode, and it
218
+
has cost multiple days:
219
+
220
+
**The model and the library must come from the same ExecuTorch commit.**
221
+
222
+
Cortex-M operator schemas change. `scratch` was added to the conv operators
223
+
on 2026-06-09 and to `avg_pool2d` later still. A `.pte` exported before a
224
+
schema change passes `Program::load`, resolves every operator, and then
225
+
fails inside `Method::execute` with `InvalidProgram (0x23)`, because the
226
+
generated kernel wrapper expects one more argument than the model supplies.
227
+
Nothing about that error names the real cause.
228
+
229
+
This bites hardest when the Python package and the C++ sources come from
230
+
different places. `pip install executorch` gives a release wheel that can be
231
+
months behind this checkout; the library you build here is current. Check
232
+
which one you are exporting with:
233
+
234
+
```bash
235
+
python -c "import executorch.backends.cortex_m.ops.operators as o; print(o.__file__)"
236
+
```
237
+
238
+
If that prints a `site-packages` path rather than your checkout, run
239
+
`./install_executorch.sh` first. Note that ExecuTorch refuses to build from a
240
+
directory not named exactly `executorch` (pytorch/executorch#6475), which is
241
+
a common reason people end up on a stale wheel without realising.
242
+
243
+
To check a model against a library without a board, decode the `.pte` and
244
+
compare each `KernelCall`'s argument count against the `stack.size() == N`
245
+
in the generated `src/executorch/codegen/RegisterCodegenUnboxedKernels*.cpp`.
246
+
A mismatch there is the bug, found in seconds instead of hours.
247
+
248
+
### Things that are not obvious
249
+
250
+
-**`link_mode=static` is mandatory.** The Uno Q defaults to Dynamic, which
251
+
builds the sketch as a Zephyr loadable extension. A library this size never
252
+
starts that way: no serial output at all, so the board looks dead and offers
253
+
nothing to diagnose. Dynamic also reports only the extension's size, roughly
254
+
half the real figure.
255
+
-**`ET_LOG` has to be routed somewhere.**`zephyr.cpp` logs through `fprintf`,
256
+
and `platform_stubs.c` stubs `fprintf` out. The build script rewrites the
257
+
logger to call a weak `et_arduino_log` hook, which the examples implement
258
+
against `Serial`. Without it every runtime failure is a bare hex code.
259
+
-**Only one platform backend may ship.**`minimal.cpp` and `zephyr.cpp` both
260
+
define `et_pal_*`; shipping both leaves the choice to link order, and
261
+
`minimal`'s logger is empty and its allocator returns `nullptr`.
262
+
-**Compiling proves very little.** Every failure worth finding here compiled
263
+
cleanly first. Flash a board.
264
+
265
+
### Error codes seen in practice
266
+
267
+
| Symptom | Cause |
268
+
|---|---|
269
+
| No serial output at all | Built in Dynamic link mode, or `Arduino_RouterBridge` missing |
270
+
|`Program::load` -> `0x23`| Model header put the array in a section the linker discards; use `pte_to_header.py` from this directory, not the Ethos-U one |
271
+
|`load_method` -> `0x14`| Operator not in the registered set; regenerate with `ROOT_OPS=`|
272
+
|`load_method` -> `0x21`|`method_pool` too small; the log line gives the exact shortfall |
273
+
|`execute` -> `0x23`| Model and library built from different ExecuTorch commits |
274
+
275
+
204
276
## What is inside the library
205
277
206
278
The `build_arduino_library.sh` script assembles these components from
@@ -229,31 +301,66 @@ Arduino's build system:
229
301
230
302
2.**`cmake_macros.h` stub** — c10/torch headers expect a cmake-generated
231
303
file. The build script generates a stub; `C10_USING_CUSTOM_GENERATED_MACROS`
232
-
is defined in `ExecuTorchArduino.h` to skip the include.
304
+
is defined in `ExecuTorch.h` to skip the include.
233
305
234
306
3.**`platform_stubs.c`** — provides weak stubs for `_Exit()`, `fprintf()`,
235
307
and `__aeabi_f2lz` for the LLEXT environment on boards that lack them.
0 commit comments