-
Notifications
You must be signed in to change notification settings - Fork 1.1k
examples/arduino: Make the generated Arduino library actually run models #21546
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
a012ff3
f3ae272
70ef989
839ee4f
257a438
5ed1847
a4ea734
62ce6a1
3b79d76
392407e
4fc7424
8e21c0a
6920b3d
0a3a363
efad893
3c919e7
2d9ee33
f98f941
7e6daac
60d40b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,21 +15,30 @@ Arduino library. A build script vendors the runtime sources from this | |
| repository into a self-contained library that Arduino users install | ||
| through the Library Manager or by copying into their libraries folder. | ||
|
|
||
| > **Who this is for.** This README is for maintainers of the packaging. | ||
| > `build_arduino_library.sh` is a release tool, not something an Arduino | ||
| > developer ever runs. Users install a prebuilt library from the Library | ||
| > Manager and never see this directory; their documentation lives in | ||
| > [meta-pytorch/executorch-arduino](https://github.com/meta-pytorch/executorch-arduino). | ||
| > | ||
| > If you are here to change ExecuTorch and want to know whether you broke | ||
| > the Arduino library, read [Keeping this working](#keeping-this-working). | ||
|
|
||
| ## How It Works | ||
|
|
||
| ``` | ||
| PyTorch Model ──► torch.export ──► .pte file ──► model.h (C array) | ||
| │ | ||
| Arduino Sketch (.ino) | ||
| #include <ExecuTorchArduino.h> | ||
| #include <ExecuTorch.h> | ||
| #include "model.h" | ||
| │ | ||
| arduino-cli compile ──► Upload ──► Runs on board | ||
| ``` | ||
|
|
||
| ### The three pieces | ||
|
|
||
| 1. **The library** (`arduino_lib/ExecuTorchArduino/`) — the ExecuTorch | ||
| 1. **The library** (`arduino_lib/ExecuTorch/`) — the ExecuTorch | ||
| runtime, CMSIS-NN kernels, and portable ops packaged for the Arduino | ||
| build system. Generated by `build_arduino_library.sh`; not checked in. | ||
|
|
||
|
|
@@ -65,34 +74,34 @@ cd examples/arduino | |
| ``` | ||
|
|
||
| This copies the required ExecuTorch sources from the repository into | ||
| `arduino_lib/ExecuTorchArduino/`, ready for Arduino. | ||
| `arduino_lib/ExecuTorch/`, ready for Arduino. | ||
|
|
||
| ### 2. Install the library | ||
|
|
||
| Copy the generated library into your Arduino libraries folder: | ||
|
|
||
| ```bash | ||
| # macOS: | ||
| cp -r arduino_lib/ExecuTorchArduino ~/Documents/Arduino/libraries/ | ||
| cp -r arduino_lib/ExecuTorch ~/Documents/Arduino/libraries/ | ||
| # Linux: | ||
| cp -r arduino_lib/ExecuTorchArduino ~/Arduino/libraries/ | ||
| cp -r arduino_lib/ExecuTorch ~/Arduino/libraries/ | ||
| ``` | ||
|
|
||
| Or with `arduino-cli`: | ||
|
|
||
| ```bash | ||
| cd arduino_lib && zip -r ExecuTorchArduino.zip ExecuTorchArduino && cd .. | ||
| arduino-cli lib install --zip-path arduino_lib/ExecuTorchArduino.zip | ||
| cd arduino_lib && zip -r ExecuTorch.zip ExecuTorch && cd .. | ||
| arduino-cli lib install --zip-path arduino_lib/ExecuTorch.zip | ||
| ``` | ||
|
|
||
| ### 3. Export a model | ||
|
|
||
| Each sketch needs a `model.h` file — a `.pte` model converted to a C | ||
| byte array. Use `pte_to_header.py` from the Arm examples to convert | ||
| byte array. Use `pte_to_header.py` to convert | ||
| any `.pte` file: | ||
|
|
||
| ```bash | ||
| python examples/arm/executor_runner/pte_to_header.py \ | ||
| python examples/arduino/pte_to_header.py \ | ||
| -p model.pte -d examples/arduino/examples/AddModel -o model.h | ||
| ``` | ||
|
|
||
|
|
@@ -108,7 +117,7 @@ class Add(torch.nn.Module): | |
| et = to_edge(export(Add().eval(), (torch.tensor([1.,2.,3.]),))).to_executorch() | ||
| with open('add.pte','wb') as f: f.write(bytes(et.buffer))" | ||
|
|
||
| python examples/arm/executor_runner/pte_to_header.py \ | ||
| python examples/arduino/pte_to_header.py \ | ||
| -p add.pte -d examples/arduino/examples/AddModel -o model.h | ||
| ``` | ||
|
|
||
|
|
@@ -150,7 +159,7 @@ cmake build. If you haven't built ExecuTorch yet, run | |
| ### 4. Write a sketch | ||
|
|
||
| ```cpp | ||
| #include <ExecuTorchArduino.h> | ||
| #include <ExecuTorch.h> | ||
| #include "model.h" | ||
|
|
||
| using executorch::extension::BufferDataLoader; | ||
|
|
@@ -196,11 +205,74 @@ Arduino-specific abstractions. | |
| ### 5. Compile and upload | ||
|
|
||
| ```bash | ||
| arduino-cli compile --fqbn arduino:zephyr:unoq MySketch | ||
| arduino-cli upload --fqbn arduino:zephyr:unoq -p /dev/cu.usbmodem* MySketch | ||
| arduino-cli compile --fqbn arduino:zephyr:unoq:link_mode=static MySketch | ||
| arduino-cli upload --fqbn arduino:zephyr:unoq:link_mode=static -p /dev/cu.usbmodem* MySketch | ||
| arduino-cli monitor -p /dev/cu.usbmodem* --config baudrate=115200 | ||
| ``` | ||
|
|
||
| ## Keeping this working | ||
|
|
||
| The library is a *generated artifact*. Everything under the generated | ||
| `src/` is copied out of this repository, and the example models are | ||
| exported by this repository's Python. That gives one failure mode, and it | ||
| has cost multiple days: | ||
|
|
||
| **The model and the library must come from the same ExecuTorch commit.** | ||
|
|
||
| Cortex-M operator schemas change. `scratch` was added to the conv operators | ||
| on 2026-06-09 and to `avg_pool2d` later still. A `.pte` exported before a | ||
| schema change passes `Program::load`, resolves every operator, and then | ||
| fails inside `Method::execute` with `InvalidProgram (0x23)`, because the | ||
| generated kernel wrapper expects one more argument than the model supplies. | ||
| Nothing about that error names the real cause. | ||
|
|
||
| This bites hardest when the Python package and the C++ sources come from | ||
| different places. `pip install executorch` gives a release wheel that can be | ||
| months behind this checkout; the library you build here is current. Check | ||
| which one you are exporting with: | ||
|
|
||
| ```bash | ||
| python -c "import executorch.backends.cortex_m.ops.operators as o; print(o.__file__)" | ||
| ``` | ||
|
|
||
| If that prints a `site-packages` path rather than your checkout, run | ||
| `./install_executorch.sh` first. Note that ExecuTorch refuses to build from a | ||
| directory not named exactly `executorch` (pytorch/executorch#6475), which is | ||
| a common reason people end up on a stale wheel without realising. | ||
|
|
||
| To check a model against a library without a board, decode the `.pte` and | ||
| compare each `KernelCall`'s argument count against the `stack.size() == N` | ||
| in the generated `src/executorch/codegen/RegisterCodegenUnboxedKernels*.cpp`. | ||
| A mismatch there is the bug, found in seconds instead of hours. | ||
|
|
||
| ### Things that are not obvious | ||
|
|
||
| - **`link_mode=static` is mandatory.** The Uno Q defaults to Dynamic, which | ||
| builds the sketch as a Zephyr loadable extension. A library this size never | ||
| starts that way: no serial output at all, so the board looks dead and offers | ||
| nothing to diagnose. Dynamic also reports only the extension's size, roughly | ||
| half the real figure. | ||
| - **`ET_LOG` has to be routed somewhere.** `zephyr.cpp` logs through `fprintf`, | ||
| and `platform_stubs.c` stubs `fprintf` out. The build script rewrites the | ||
| logger to call a weak `et_arduino_log` hook, which the examples implement | ||
| against `Serial`. Without it every runtime failure is a bare hex code. | ||
| - **Only one platform backend may ship.** `minimal.cpp` and `zephyr.cpp` both | ||
| define `et_pal_*`; shipping both leaves the choice to link order, and | ||
| `minimal`'s logger is empty and its allocator returns `nullptr`. | ||
| - **Compiling proves very little.** Every failure worth finding here compiled | ||
| cleanly first. Flash a board. | ||
|
|
||
| ### Error codes seen in practice | ||
|
|
||
| | Symptom | Cause | | ||
| |---|---| | ||
| | No serial output at all | Built in Dynamic link mode, or `Arduino_RouterBridge` missing | | ||
| | `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 | | ||
| | `load_method` -> `0x14` | Operator not in the registered set; regenerate with `ROOT_OPS=` | | ||
| | `load_method` -> `0x21` | `method_pool` too small; the log line gives the exact shortfall | | ||
| | `execute` -> `0x23` | Model and library built from different ExecuTorch commits | | ||
|
|
||
|
|
||
| ## What is inside the library | ||
|
|
||
| The `build_arduino_library.sh` script assembles these components from | ||
|
|
@@ -229,31 +301,66 @@ Arduino's build system: | |
|
|
||
| 2. **`cmake_macros.h` stub** — c10/torch headers expect a cmake-generated | ||
| file. The build script generates a stub; `C10_USING_CUSTOM_GENERATED_MACROS` | ||
| is defined in `ExecuTorchArduino.h` to skip the include. | ||
| is defined in `ExecuTorch.h` to skip the include. | ||
|
|
||
| 3. **`platform_stubs.c`** — provides weak stubs for `_Exit()`, `fprintf()`, | ||
| and `__aeabi_f2lz` for the LLEXT environment on boards that lack them. | ||
|
|
||
| 4. **Compile-time defines** — `ExecuTorchArduino.h` sets | ||
| 4. **Compile-time defines** — `ExecuTorch.h` sets | ||
| `ET_ENABLE_DEPRECATED_CONSTANT_BUFFER=0` (requires models exported with | ||
| current ExecuTorch) and `FLATBUFFERS_MAX_ALIGNMENT=1024`. | ||
|
|
||
| ## Development | ||
|
|
||
| ### Updating the library | ||
|
|
||
| After modifying ExecuTorch sources, regenerate the library: | ||
|
|
||
| ```bash | ||
| ./build_arduino_library.sh # rebuild | ||
| ./build_arduino_library.sh # rebuild | ||
| ./build_arduino_library.sh --clean # remove generated output | ||
| ROOT_OPS="aten::add.out,..." ./build_arduino_library.sh # pick the op set | ||
| ALL_OPS=1 ./build_arduino_library.sh # every portable op | ||
| ``` | ||
|
|
||
| The op set is a size decision. Registering every portable kernel costs about | ||
| 1.6 MB of text, twice the Uno Q's flash, because portable kernels are | ||
| dtype-templated. The default registers the Cortex-M operators plus a small | ||
| portable set, which lands around a quarter of flash. | ||
|
|
||
| ### Re-exporting the example models | ||
|
|
||
| Each example ships a `model.pte` that the build script converts to the | ||
| `model.h` its sketch includes. Regenerate them whenever an operator schema | ||
| changes, or the models will fail at `execute` against the new runtime: | ||
|
|
||
| ```bash | ||
| # keyword spotting, from the checked-in checkpoint (no retraining) | ||
| python export_model.py --checkpoint examples/KeywordSpotting/model.pth \ | ||
| --output /tmp/kws.h | ||
| ``` | ||
|
|
||
| ### Bumping the pin in executorch-arduino | ||
|
|
||
| The published library records the commit it was generated from in | ||
| `extras/PROVENANCE.txt`, and pins that commit in `executorch_pin.txt` | ||
| alongside it — the same one-SHA-per-file convention ExecuTorch uses in | ||
| `.ci/docker/ci_commit_pins/`. To move it forward: | ||
|
|
||
| 1. Update `executorch_pin.txt` to the new ExecuTorch commit | ||
| 2. Regenerate the library from a checkout at that commit | ||
| 3. Re-export the example models from the same checkout | ||
| 4. Confirm each model's `KernelCall` argument counts match the regenerated | ||
| `RegisterCodegenUnboxedKernels*.cpp` | ||
| 5. Compile every example at `link_mode=static`, and flash at least one | ||
|
|
||
| Steps 2 and 3 have to happen together. Bumping the library without | ||
| re-exporting the models is the mismatch described in | ||
| [Keeping this working](#keeping-this-working). | ||
|
|
||
| ### Testing | ||
|
|
||
| ```bash | ||
| arduino-cli compile --fqbn arduino:zephyr:unoq examples/HelloExecuTorch | ||
| arduino-cli upload --fqbn arduino:zephyr:unoq -p /dev/cu.usbmodem* examples/HelloExecuTorch | ||
| arduino-cli compile --fqbn arduino:zephyr:unoq:link_mode=static examples/HelloExecuTorch | ||
| arduino-cli upload --fqbn arduino:zephyr:unoq:link_mode=static -p /dev/cu.usbmodem* examples/HelloExecuTorch | ||
| arduino-cli monitor -p /dev/cu.usbmodem* --config baudrate=115200 | ||
| ``` | ||
|
|
||
|
|
@@ -325,20 +432,53 @@ Training and test audio from [Google Speech Commands v2](https://arxiv.org/abs/1 | |
| people. Standard dataset used by the MLPerf Tiny benchmark. Download | ||
| via `torchaudio.datasets.SPEECHCOMMANDS` (2.3 GB). | ||
|
|
||
| The dataset is © Google, released under | ||
| [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which asks for | ||
| attribution. The keyword spotting weights checked in here | ||
| (`examples/KeywordSpotting/model.pth` and the `.pte` generated from it) are | ||
| trained on it and carry the same attribution. | ||
|
|
||
| Only the ten keyword classes are needed, so the full archive never has to | ||
| land on disk: | ||
|
|
||
| ```bash | ||
| mkdir -p outputs/speech_commands/SpeechCommands/speech_commands_v0.02 | ||
| cd outputs/speech_commands/SpeechCommands/speech_commands_v0.02 | ||
| curl -sL http://download.tensorflow.org/data/speech_commands_v0.02.tar.gz \ | ||
| | tar xz ./yes ./no ./up ./down ./left ./right ./on ./off ./stop ./go | ||
| ``` | ||
|
|
||
| That is 1.2 GB extracted instead of 2.3 GB downloaded plus 2.4 GB unpacked. | ||
|
|
||
| The DS-CNN KWS benchmark uses 12 output classes (silence, unknown, plus | ||
| 10 keywords). The Arduino export script trains the 10 keyword classes: | ||
| yes, no, up, down, left, right, on, off, stop, go. | ||
|
|
||
| ## LLEXT Memory Budget | ||
| ## Link Mode and Memory Budget | ||
|
|
||
| The Uno Q defaults to Dynamic link mode, which builds the sketch as a Zephyr | ||
| loadable extension. Sketches this size never start that way: no serial output | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will this still be able to work without Zephyr? I don't believe all Arduino projects run Zephyr.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it won't. architectures=zephyr is correct: the only Zephyr-dependent file is runtime/platform/default/zephyr.cpp. It uses 3 zephyr specific functions , but everything else in the library is platform-neutral. Uno Q is the only Arduino board on a Zephyr core (afaik) . Added para making that explicit rather than implied.. So after this initial change lands, I am planning to put up a follow up PR to make use of generic arduino.cpp, PAL would work on every core and then it becomes architectures=* , where it becomes generic |
||
| at all, so the board looks dead and offers nothing to diagnose. Build with | ||
| `link_mode=static`. A 2 KB sketch runs fine under Dynamic, so the ceiling sits | ||
| somewhere between that and these builds; it has not been pinned down. | ||
|
|
||
| The Arduino Uno Q loads sketches as LLEXT (Loadable Extensions). | ||
| Sizes reported by `arduino-cli compile` (Zephyr board core 0.55.2): | ||
| Dynamic also reports only the extension's own size, which reads far lower than | ||
| what the board actually holds. Measured on an Arduino Uno Q, board core 0.55.2, | ||
| against 786,432 bytes of flash and 131,072 bytes of RAM: | ||
|
|
||
| | Build | Code | Data | Total | Status | | ||
| |-------|------|------|-------|--------| | ||
| | HelloExecuTorch (portable ops) | 62 KB | 27 KB | 89 KB | ✅ | | ||
| | Add model (portable ops) | 88 KB | 35 KB | 123 KB | ✅ | | ||
| | DS-CNN (selective CMSIS-NN) | 87 KB | 57 KB | 144 KB | ✅ | | ||
| | Build | Flash (static) | RAM | Dynamic reported | On hardware | | ||
| |-------|---------------|-----|------------------|-------------| | ||
| | HelloExecuTorch | 472,492 (60%) | 26,612 (20%) | 27% | `Model loaded OK!`, 1 method | | ||
| | AddModel | 507,628 (64%) | 34,804 (26%) | 30% | `[1,2,3] + 1 = [2.00, 3.00, 4.00]` | | ||
| | KeywordSpotting (CMSIS-NN) | 559,620 (71%) | 57,332 (43%) | 30% | fails, see below | | ||
|
psiddh marked this conversation as resolved.
Outdated
|
||
|
|
||
| All CMSIS-NN sources are compiled, but the linker's | ||
| `--gc-sections` discards unused functions from the final binary. | ||
|
|
||
| RAM is the binding constraint, not flash. Zephyr reserves 32 KB of main stack | ||
| and a 32 KB heap out of 128 KB before the sketch gets any, and the arena the | ||
| sketch hands to `MemoryManager` comes out of what remains. KeywordSpotting's | ||
| DS-CNN plans 16 KB of buffers but needs considerably more for the method's own | ||
| structures: a 28 KB arena fails `load_method` with `MemoryAllocationFailed` | ||
| (0x21), and a 64 KB one loads but then fails `execute` with `InvalidProgram` | ||
| (0x23), which is memory being overrun rather than a malformed program. | ||
Uh oh!
There was an error while loading. Please reload this page.