Skip to content
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a012ff3
examples/arduino: fix the library build so models can actually run
psiddh Aug 1, 2026
f3ae272
examples/arduino: ship third-party license notices
psiddh Aug 1, 2026
70ef989
examples/arduino: package pte_to_header.py with the library
psiddh Aug 1, 2026
839ee4f
examples/arduino: register the dim_order ops the Cortex-M path emits
psiddh Aug 1, 2026
257a438
examples/arduino: name the library ExecuTorch, not ExecuTorchArduino
psiddh Aug 1, 2026
5ed1847
examples/arduino: point library.properties at the published repo
psiddh Aug 1, 2026
a4ea734
examples/arduino: make the generated library reproducible
psiddh Aug 1, 2026
62ce6a1
examples/arduino: check in the example models the build script converts
psiddh Aug 1, 2026
3b79d76
examples/arduino: require static link mode, and correct the size table
psiddh Aug 1, 2026
392407e
examples/arduino: do not claim hardware results that were not measured
psiddh Aug 1, 2026
4fc7424
examples/arduino: HelloExecuTorch verified on hardware; report real s…
psiddh Aug 1, 2026
8e21c0a
examples/arduino: stop pointing users at the Ethos-U header generator
psiddh Aug 2, 2026
6920b3d
examples/arduino: ship one platform backend, and let its logs escape
psiddh Aug 2, 2026
0a3a363
examples/arduino: ship the validated keyword spotting model
psiddh Aug 2, 2026
efad893
examples/arduino: re-export the keyword spotting model against main
psiddh Aug 3, 2026
3c919e7
examples/arduino: write the README for whoever inherits this
psiddh Aug 3, 2026
2d9ee33
examples/arduino: sort the pte_to_header import
psiddh Aug 3, 2026
f98f941
examples/arduino: fix review findings in the build script
psiddh Aug 3, 2026
7e6daac
Merge branch 'main' into arduino-library-fixes
psiddh Aug 3, 2026
60d40b4
examples/arduino: right-size the operator registry, and fix stale docs
psiddh Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ tokenizer.json
*.ptd
!test_bpe_tokenizer.bin
!test_tiktoken_tokenizer.model
# Arduino examples ship a model, so build_arduino_library.sh has something to
# turn into the model.h their sketches include. 1.1 KB each.
Comment thread
psiddh marked this conversation as resolved.
Outdated
!examples/arduino/examples/*/model.pte

# Editor temporaries
*.idea
Expand Down
File renamed without changes.
196 changes: 168 additions & 28 deletions examples/arduino/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
```

Expand All @@ -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
```

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 |
Comment thread
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.
Loading
Loading