Run PyTorch models on Arduino using ExecuTorch.
PyTorch model ──► torch.export ──► .pte ──► model.h (C array)
│
Arduino sketch (.ino)
#include <ExecuTorch.h>
#include "model.h"
│
compile ──► upload ──► runs on the board
| Board | MCU | Status |
|---|---|---|
| Arduino UNO Q | STM32U585 (Cortex-M33) | Supported — CI-compiled and hardware-verified |
Nothing else yet, for reasons that differ per board family:
| Core | Boards | Why not |
|---|---|---|
mbed_* |
Nano 33 BLE, Portenta H7, Giga R1 | The core itself cannot compile in C++17 mode, which ExecuTorch requires. Mbed OS defines abs() as a macro and C++17 added std::chrono::abs; a two-line empty sketch fails with 115 errors under -std=gnu++17 |
renesas_uno, samd |
UNO R4, MKR family | 256 KB flash and 32 KB RAM. HelloExecuTorch overflows the UNO R4 by 33 KB, and while it fits an MKR Zero at 95%, AddModel — one operator — overflows by 35 KB |
renesas_portenta |
Portenta C33 | In progress. 2 MB flash and 512 KB RAM is ample, and its core already compiles C++17 |
avr |
UNO R3, Nano, Mega | Kilobytes of RAM against a runtime that needs hundreds |
The UNO Q's arduino:zephyr core uses a Zephyr SDK toolchain (GCC 12.2.0), where the rest
of Arduino's ARM cores bundle arm-none-eabi-gcc 7.2.1 from 2017. That older compiler is
not itself disqualifying — the incompatibilities it exposed in ExecuTorch were a handful of
specific constructs, not C++17 as a whole.
From the Arduino IDE: Tools → Manage Libraries…, search for ExecuTorch.
Or manually:
git clone https://github.com/meta-pytorch/executorch-arduino.git \
~/Arduino/libraries/ExecuTorchLibrary Manager pulls in Arduino_RouterBridge automatically. If you installed by hand,
install Arduino_RouterBridge from Library Manager too — a manual copy resolves no
dependencies, and the UNO Q core stops the build with an explicit #error without it.
Tools → Board → Arduino UNO Q, then Tools → Link mode → Static.
Without it, every example fails to build:
Sketch too big; text section exceeds available space in board
Compilation error: text section exceeds available space in board
The board defaults to Dynamic, and this is the first thing everyone hits. Two things make it easy to miss:
- The setting is per-sketch. Opening another example puts it back to Dynamic.
- Library examples live in a read-only folder, so the IDE may not remember the setting at all. If it keeps reverting, File → Save As into your own sketchbook first.
From the command line, put it in the FQBN:
arduino-cli compile --fqbn arduino:zephyr:unoq:link_mode=static examples/AddModelDynamic builds the sketch as a Zephyr loadable extension through a relocatable link
(-r). --gc-sections is passed either way, but it can only work in a final link, where
the linker has an entry point to trace reachability from; under -r nothing can be proven
unreachable, so every operator in the library is kept. A loadable extension is also loaded
into RAM, so the retained code costs RAM as well as flash. For AddModel on core 0.90.0
that is 507,876 bytes versus 787,508.
This is not something the library can fix — shipping only the operators actually registered (15 sources instead of 172) moves the Dynamic build by 852 bytes, and the rest is the runtime, FlatBuffers and CMSIS-NN.
#include <ExecuTorch.h>
#include "model.h" // generated from your .pte
using executorch::extension::BufferDataLoader;
using executorch::runtime::Program;
void setup() {
Serial.begin(115200);
executorch::runtime::runtime_init();
auto loader = BufferDataLoader(model_pte, sizeof(model_pte));
auto program = Program::load(&loader);
Serial.println(program.ok() ? "loaded" : "failed");
}Start from examples/HelloExecuTorch, then AddModel for a full inference pass, then
KeywordSpotting for a quantized DS-CNN using CMSIS-NN kernels.
Each one needs Link mode set to Static, as above — it does not carry over between sketches.
ExecuTorch's own diagnostics reach your sketch through a weak hook, so the library never
has to depend on Serial. Implement it or lose every runtime error message:
extern "C" void et_arduino_log(const char* msg) {
Serial.print("ET| ");
Serial.println(msg);
}python extras/tools/export_model.py --help # PyTorch model -> .pte
python extras/tools/pte_to_header.py --help # .pte -> model.hThe exporter and the runtime must come from the same ExecuTorch commit. A .pte built
against a different one loads, resolves every operator, and then fails inside
Method::execute with InvalidProgram (0x23) — nothing in that error says why. The
commit this library was generated from is in executorch_pin.txt.
Sync from ExecuTorch defaults the Arduino package version to the selected
checkout's version.txt. Selecting ExecuTorch v1.5.1, for example, generates
version=1.5.1. The existing 0.x releases keep their original versions and pins.
Maintainers can select a branch, tag or SHA; the default is main. The workflow
generates the library, checks the example models, and opens a PR. A development
checkout may already carry the next version, so use executorch_pin.txt and
extras/PROVENANCE.txt to identify the exact source commit and runtime version.
gh workflow run sync.yml --repo meta-pytorch/executorch-arduino \
-f executorch_ref=v1.5.1The optional library_version input selects an explicit Arduino package
version in X.Y.Z form while preserving the upstream version in provenance.
Maintainers decide how Arduino-only releases should be numbered. After CI and
board validation, confirm the package version is unused, merge, and tag
v<library_version> to publish.
This also supports older upstream tags whose generator still writes 0.x
metadata. It regenerates the library before applying the package version; do
not relabel the existing 0.2.0 sources as a newer runtime. See the upstream
release guide.
Everything under src/ and examples/ is build output from
examples/arduino/build_arduino_library.sh
in the ExecuTorch tree. It lives in a separate repository because the Arduino Library
Manager requires library.properties at the repository root.
Do not send patches against src/ — they will be overwritten by the next sync. Fix
things upstream in pytorch/executorch under examples/arduino/, then run the
Sync from ExecuTorch workflow here.
executorch_pin.txt— the ExecuTorch commit this tree was generated fromextras/PROVENANCE.txt— that commit, the CMSIS-NN revision, the operator set, and the kernel countextras/THIRD_PARTY_LICENSES/— licenses for the vendored CMSIS-NN, FlatBuffers, and flatcc sourcesextras/tools/— the generator and the model-conversion scripts
Measured on an Arduino UNO Q, board core 0.90.0, at
arduino:zephyr:unoq:link_mode=static, against 786,432 bytes of flash and 262,144 bytes
of RAM:
| Example | Flash | RAM | On hardware |
|---|---|---|---|
| HelloExecuTorch | 472,952 (60%) | 3,052 (1%) | Model loaded OK!, 1 method |
| AddModel | 507,876 (64%) | 12,268 (4%) | [1,2,3] + 1 = [2.00, 3.00, 4.00] |
| KeywordSpotting | 563,672 (71%) | 47,084 (17%) | detects yes, logit 8.95 |
Core 0.55.2 reported a 131,072-byte RAM ceiling and 0.90.0 reports 262,144, so figures from before that change are not comparable.
KeywordSpotting hands MemoryManager a 40 KB arena, which is bounded on both sides:
28 KB fails load_method with MemoryAllocationFailed (0x21), and enlarging it far enough
overruns the stack and heap Zephyr reserves before the sketch gets any — a build that can
still run and print the right answer, which is what makes it dangerous rather than safe.
Anything above 40 KB is untested on core 0.90.0.
Registering every portable operator instead of the default set costs about 1.58 MB of
text, which is roughly twice this board's flash. The operator set is chosen at generation
time; extras/PROVENANCE.txt records which one produced this build.
BSD-3-Clause, matching ExecuTorch. See LICENSE, and
extras/THIRD_PARTY_LICENSES/ for the vendored dependencies.