examples/arduino: Make the generated Arduino library actually run models - #21546
examples/arduino: Make the generated Arduino library actually run models#21546psiddh wants to merge 18 commits into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21546
Note: Links to docs will display an error until the docs builds have been completed. ❌ 7 New FailuresAs of commit f98f941 with merge base d46e620 ( NEW FAILURES - The following jobs have failed:
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
There was a problem hiding this comment.
Pull request overview
This PR fixes the examples/arduino/ packaging pipeline so the generated Arduino library is self-contained and can actually load and execute ExecuTorch .pte models on supported boards (notably Zephyr-based Arduino cores), including proper kernel registration, CMSIS-NN vendoring, and usable runtime logging.
Changes:
- Renames the library to
ExecuTorch, updates headers/docs, and adds an Arduino-specificpte_to_header.pyto generate compatiblemodel.h. - Updates the Arduino library generator to run ExecuTorch codegen for operator registration, vendor CMSIS-NN, and route ET logs through a weak
et_arduino_loghook used by the example sketches. - Improves maintainer documentation around commit/schema pinning and static link mode requirements; ensures example models are shipped (via
.ptesources).
Reviewed changes
Copilot reviewed 9 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/arduino/README.md | Maintainer-focused documentation, updated library name/usage, and guidance on schema/commit pinning and static link mode. |
| examples/arduino/pte_to_header.py | New Arduino-compatible .pte → C header converter (plain rodata array). |
| examples/arduino/platform_stubs.c | Adds weak __errno() stub for Zephyr/newlib libm_nano interoperability. |
| examples/arduino/library.properties | Renames the library, updates URL/includes, and adds dependency metadata. |
| examples/arduino/export_model.py | Reuses to_header() to generate model headers consistently. |
| examples/arduino/ExecuTorch.h | New public Arduino-facing wrapper header with required defines/includes. |
| examples/arduino/examples/KeywordSpotting/KeywordSpotting.ino | Switches to ExecuTorch.h and adds et_arduino_log hook implementation. |
| examples/arduino/examples/HelloExecuTorch/HelloExecuTorch.ino | Switches header, adds et_arduino_log, and improves load-status reporting. |
| examples/arduino/examples/AddModel/AddModel.ino | Switches header and adds et_arduino_log hook. |
| examples/arduino/build_arduino_library.sh | Major generator updates: codegen oplist/registration, CMSIS-NN vendoring, licensing/provenance output, and platform backend/log routing patches. |
| .gitignore | Ensures example model.pte files are not ignored so the generator can build model headers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (2)
examples/arduino/build_arduino_library.sh:458
- The provenance/pin section has raw text starting at line 457 that is not commented and not redirected to a file. In a shell script this will be executed as commands (e.g.,
Generated), causingbuild_arduino_library.shto fail.
echo "$ET_SHA" > "$OUT_DIR/executorch_pin.txt"
Generated by examples/arduino/build_arduino_library.sh
examples/arduino/examples/AddModel/AddModel.ino:27
- The header-generation instructions still point to
examples/arm/executor_runner/pte_to_header.py, but this PR introducesexamples/arduino/pte_to_header.pyand updates the README accordingly. This comment will send users to a script that produces an incompatible section attribute for Arduino cores.
// 2. Convert to header: python examples/arm/executor_runner/pte_to_header.py \
// -p add.pte -o model.h
#include <ExecuTorch.h>
The generated library compiled but could not execute a model, and the artifact published to meta-pytorch/executorch-arduino could not compile at all. Six separate defects: Kernels never reached the operator registry. ExecuTorch registers kernels through codegen, and the script never ran it, so Method::load failed with OperatorMissing for every op. Step 3b now runs gen_oplist and codegen.gen, covering both the portable ops and the Cortex-M operators.yaml. Registering every portable op costs 1.58 MB of text against the Uno Q's 786 KB of flash, so the op set is now a curated default overridable via ROOT_OPS or ALL_OPS. The *_aten.cpp exclusion also matched tensor_parser_exec_aten.cpp, which is portable-mode and required; Program::load survived without it but Method::load did not link. schema/*.cpp was never copied, leaving ExtendedHeader::Parse undefined. CMSIS-NN vendoring pulled in Source/Bindings, which is pybind11 host code, and the f16/f32 sources that ARM_NN_ENABLE_F32/F16 gate off by default and which need CMSIS-DSP types the backend never uses. Its LICENSE now ships. The Zephyr core builds against picolibc but takes math from newlib's libm_nano, which calls __errno(); platform_stubs.c now provides it weakly. Verified with arduino-cli 1.5.1 against arduino:zephyr:unoq 0.55.2: HelloExecuTorch 24% flash, AddModel 27% (real exported add.pte, executes aten::add.out), KeywordSpotting 28% with the CMSIS-NN Cortex-M ops linked. Authored with Claude Code (Opus 5).
CMSIS-NN, FlatBuffers and flatcc are all Apache-2.0 and are redistributed in source form, so section 4 requires their licenses to travel with the copies. Only flatcc's portable subdirectory carried one. c10 and torch are exact copies of PyTorch core headers and were unattributed. They go under extras/, which the Arduino library spec reserves for content the build ignores, rather than an unexpected folder at the library root. Authored with Claude Code (Opus 5).
The README tells users to convert their .pte with it, but it lives in examples/arm/executor_runner and an installed Arduino library has no path back to the ExecuTorch tree. Authored with Claude Code (Opus 5).
A quantized DS-CNN lowered for Cortex-M asks for dim_order_ops::_clone_dim_order, not aten::clone, because the model is channels-last. Without it the op set looks complete and Method::load still fails. Verified against a real quantized DS-CNN exported through CortexMQuantizer and CortexMPassManager: all eight operators it references now resolve, and the sketch links. Authored with Claude Code (Opus 5).
arduino-lint rule LP014 rejects "Arduino" inside the name of an Arduino library as superfluous, and it is an error under --compliance strict. Renaming the umbrella header alongside it clears LS008 too, so the library now lints clean in every mode. Every TensorFlow Lite library actually carried in the Library Manager index does the same thing - ArduTFLite, MicroTFLite, and notably TensorFlowLite_ESP32, whose repository is named Arduino_TensorFlowLite_ESP32 while its library name drops the prefix. Google's own Arduino_TensorFlowLite is absent from the index entirely, so it is no precedent here. The repository keeps its name. This is the library.properties name field, the umbrella header, and the includes that follow from it. Authored with Claude Code (Opus 5).
url pointed at pytorch/executorch, which is the runtime, not the thing a user installs. Arduino shows this as the library's home page. depends=Arduino_RouterBridge is what the Uno Q core needs for Serial; without it Library Manager users get a #error out of the core's stubs on first compile. Authored with Claude Code (Opus 5).
The published artifact needed six files the script did not produce - model.h for two examples, a provenance record, and three of the four tools under extras/. They had to be re-added by hand after every regeneration, which is how a resync silently drops the models and leaves the examples #erroring on open. Examples now carry a model.pte that the script converts to model.h and then removes, so a header can never drift from the .pte it came from. extras gets all four tools rather than just pte_to_header.py, and a PROVENANCE.txt recording the executorch commit, the CMSIS-NN revision, the op set and the kernel count - none of which is recoverable from the artifact otherwise. pte_to_header.py is new here rather than reused from examples/arm/executor_runner, which places the array in a network_model_sec section for the Ethos-U linker script. No Arduino core defines that section. Authored with Claude Code (Opus 5).
*.pte is ignored repo-wide to keep exported models out of git. These two are 1.1 KB fixtures that build_arduino_library.sh turns into the model.h the sketches include, so they need an explicit exception rather than living outside version control - otherwise the shipped examples cannot be rebuilt. Authored with Claude Code (Opus 5).
The board defaults to Dynamic, which builds the sketch as a Zephyr loadable extension. Validated on an Arduino Uno Q: AddModel built statically prints [1,2,3] + 1 = [2.00, 3.00, 4.00], and the same sketch under Dynamic produces no serial output whatsoever - it never reaches its first println, so the board reads as dead with nothing to diagnose. The old table reported Dynamic sizes, which cover only the extension and run roughly half of what the board actually holds. Replaced with measured static figures, and with the RAM arithmetic that actually binds: Zephyr takes 32 KB of stack and a 32 KB heap out of 128 KB before the sketch sees any. Authored with Claude Code (Opus 5).
HelloExecuTorch was never flashed; the table reported it as loading on the strength of a successful compile. KeywordSpotting fails on the board today. Authored with Claude Code (Opus 5).
…tate in loop Confirmed on an Arduino Uno Q built with link_mode=static: prints "Model loaded OK!", 1156 bytes, 1 method. loop() printed a fixed "ExecuTorch ready" whether or not the model had loaded, and setup() has already scrolled past by the time a serial monitor attaches. That reads as a pass when nothing works - it very nearly fooled us here. It now reflects the load result, and the failure branch prints the error code instead of a bare message. Authored with Claude Code (Opus 5).
The README told users to convert their .pte with examples/arm/executor_runner/pte_to_header.py, which emits the array into a network_model_sec section for the Ethos-U linker script. No Arduino core defines that section, so the model lands in an orphan section that is present in the .elf and absent from the .bin. The sketch then reads whatever happens to be at that address and Program::load returns InvalidProgram (0x23). Reproduced on an Arduino Uno Q. export_model.py never used that script - it emitted a plain rodata array itself, which is why the keyword spotting demo worked when it was written and why following the README does not reproduce it. That inline converter is now shared with pte_to_header.py rather than duplicated, and pte_to_header.py takes the same -p/-d/-o arguments the README already documents. Authored with Claude Code (Opus 5).
minimal.cpp and zephyr.cpp both define the et_pal_* backend and both were being vendored, so which one a sketch got depended on link order. minimal's et_pal_emit_log_message is an empty body and its et_pal_allocate returns nullptr. zephyr.cpp logs through fprintf, which platform_stubs.c stubs out to nothing. Either way every ET_LOG was discarded. That is worth more than it sounds. Debugging a keyword spotting failure on an Uno Q meant reading bare hex codes for hours; with logging restored the runtime immediately said "Memory allocation failed: 24B requested, 16B available" and then named the operator and arg count it disagreed about. zephyr.cpp now routes through a weak et_arduino_log hook, which the examples implement against Serial. Authored with Claude Code (Opus 5).
Checks in the .pte behind the DS-CNN demo, verified on an Arduino Uno Q against all ten keywords in one run: 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. Ten out of ten, every one by a clear margin, through CMSIS-NN on the Cortex-M33. Until now the model existed only as an untracked file in one directory on one machine, which is why the example could not be reproduced by anyone else and why a stale exporter went unnoticed for so long. 53 KB of .pte, 40 KB across all three fixtures once compressed. model.pth rides along so the network can be re-exported without retraining on Speech Commands, but it stays out of the generated library - a training checkpoint is not something the runtime needs. The weights derive from Google Speech Commands v2, which is CC BY 4.0 and wants attribution; the README does not carry that yet. Authored with Claude Code (Opus 5).
The checked-in model came from a June export and carried avg_pool2d with one argument fewer than main's schema, so it failed at execute against a library built from main. Re-exported from the same checkpoint against current main: depthwise 16, conv2d 15, avg_pool2d 11, all matching. Verified on an Arduino Uno Q across all ten keywords in a single run - yes, no, up, down, left, right, on, off, stop, go - 10/10 correct. Authored with Claude Code (Opus 5).
Splits the audience. build_arduino_library.sh is a release tool for whoever maintains the packaging; an Arduino developer installs a prebuilt library and never sees this directory. The README said none of that. Adds a "Keeping this working" section for the failure that has now cost several days twice over: a model exported against one ExecuTorch commit and a library built from another. Cortex-M operator schemas change, and the mismatch surfaces as InvalidProgram at execute, long after load succeeds and every operator resolves. It records how to tell which ExecuTorch you are exporting with, why a pip wheel is so often the wrong one, and how to compare a model's argument counts against a library without a board. Also documents what took longest to learn the hard way: static link mode is mandatory and its absence looks like a dead board, ET_LOG has to be routed somewhere or every failure is a bare hex code, and compiling proves very little. There is a table mapping the error codes to their causes. extras/PROVENANCE.txt now explains itself rather than listing fields, and the generated library carries executorch_pin.txt - one SHA in one file, matching the convention in .ci/docker/ci_commit_pins/. The pin is the input a maintainer edits; PROVENANCE records what was actually used. Speech Commands is CC BY 4.0 and the attribution was missing. The dataset section now carries it, along with a streaming download that keeps 1.2 GB of the ten classes needed instead of unpacking 4.7 GB. Authored with Claude Code (Opus 5).
lintrunner UFMT. Authored with Claude Code (Opus 5).
An earlier edit left the previous heredoc body loose in the script, so everything after the provenance write was parsed as commands and set -e aborted the run. The generated tree still looked right because the abort happened after the artifacts were written, and I had been checking those rather than the exit status. The flatbuffers and flatcc license copies now say which submodule is missing instead of failing with a bare cp error, AddModel.ino no longer points at the Ethos-U header converter, and the codegen no longer prints the whole op list mid-build. Authored with Claude Code (Opus 5).
f98df43 to
f98f941
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (1)
examples/arduino/build_arduino_library.sh:441
- This provenance line uses
$(grep -c ...)underset -e. If the generated file format changes and the pattern no longer matches,grep -cexits with status 1 (even though it prints0), which aborts the script while writingPROVENANCE.txt.
Make the count robust by tolerating a non-match and defaulting to 0.
kernels: $(grep -c 'Kernel(' "$CODEGEN_OUT/RegisterCodegenUnboxedKernelsEverything.cpp")
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (1)
examples/arduino/README.md:473
- The README’s memory budget table marks
KeywordSpotting (CMSIS-NN)as “fails, see below”, but the PR description/test plan states KeywordSpotting runs successfully on hardware (10/10 keywords correct). Please reconcile the README with the current tested behavior (either update the table row to reflect success and current RAM/flash numbers, or adjust the PR description/test plan if it still fails).
| 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 |
There was a problem hiding this comment.
Do you want to check models in here or generate them as part of the build_arduino_library.sh?
|
|
||
| # gen writes the same content to both names; keeping both is a duplicate-symbol error. | ||
| rm -f "$CODEGEN_OUT/RegisterCodegenUnboxedKernels_0.cpp" | ||
| rm -f "$CODEGEN_OUT/selected_operators.yaml" |
There was a problem hiding this comment.
If you keep the selected_operators.yaml and add:
"$PYTHON" -m codegen.tools.gen_max_kernel_num \
--oplist-yaml="$CODEGEN_OUT/selected_operators.yaml" \
--prim-ops-source="$ET_ROOT/kernels/prim_ops/register_prim_ops.cpp" \
--output-path="$ET_SRC/runtime/kernel/selected_max_kernel_num.h"
You can cutdown the RAM cost by nearly 24 KiB.
| !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. |
There was a problem hiding this comment.
The 1.1 KB comment seems wrong.
| ## 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 |
There was a problem hiding this comment.
Will this still be able to work without Zephyr? I don't believe all Arduino projects run Zephyr.
| |-------|---------------|-----|------------------|-------------| | ||
| | 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 | |
There was a problem hiding this comment.
This doesn't line up with the test plan (claims KWS is working). Is this just stale?
rascani
left a comment
There was a problem hiding this comment.
Approving to ensure unblocked.
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::loadwould have failed withOperatorMissing. CMSIS-NN was never vendored, so the Cortex-M ops shipped without the library they call.schema/*.cppwas never copied, the*_aten.cppexclusion also deleted the portable-modetensor_parser_exec_aten.cpp, and__errnowas missing because the Zephyr core mixes picolibc with newlib'slibm_nano.Both
minimal.cppandzephyr.cppwere vendored, so whichet_pal_*backend you got depended on link order, and everyET_LOGwas discarded either way. One backend now ships and its logs route to a hook the examples implement againstSerial.The examples ship their models — previously none did, so every sketch
#errored when opened from the IDE menu. The README pointed at the Ethos-Upte_to_header.py, whosenetwork_model_secsection no Arduino core defines, so following the docs produced a model that failsProgram::load. Static link mode is mandatory and undocumented; the Dynamic default yields a silently dead board. Renamed toExecuTorchbecausearduino-lintrejects "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 (
scratchin #19636, #19825), and a mismatch loads fine, resolves every operator, then fails atMethod::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:All ten MFCC inputs in one sketch, exercising the CMSIS-NN conv / depthwise / avgpool / linear kernels:
arduino-lint --library-manager submit: no errors, no warnings, under bothspecificationandstrict. 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).