Skip to content

Commit 415d3ea

Browse files
committed
Provide the ESP-NN delegate and esp_nn:: op library in the ESP-IDF component
The component now vendors the ESP-NN delegate runtime (backends/espressif/espnn_runtime) and the esp_nn:: op library (backends/espressif/ops), depends on espressif/esp-nn (or a project's own components/esp-nn), and carries the delegate's Kconfig options in its menu. The op library is a fourth kernel library for gen_kernels.py, after portable, quantized and Cadence generic. The delegate is linked only when the kernel bundle records a model that uses EspNNBackend, or when the project calls executorch_add_backend(EspNNBackend): a small source in the delegate's library defines an anchor that references register_espnn_backend, so -u on that anchor pulls in espnn_backend.cpp and its static registration from a library that is otherwise never linked. Only espnn_backend.h is public, and it now says when the generated component links it. The kernel library, too, is linked only when the bundle has kernel libraries or prim ops, so a delegate-only bundle never builds it, and a project that declares only the backend also defines the symbol et_model needs to link. The smoke consumer now embeds any number of models, each with the test case that split_bundled_program.py extracts from its bundled program, and compares every output with the reference on the device. CI runs four images on the emulator. An ESP32-S3 image runs the smoke model on portable kernels, a quantized mul on esp_nn::quantized_mul_int8 and the conv-act-pool-linear ESP-NN delegate program together. An ESP32-C6 image runs the first two and must not contain EspNNBackend. Two more ESP32-S3 images run the delegate alone, one from a bundle with no kernel libraries, which must not build the kernel library, and one from executorch_add_backend with no bundle, whose registry must hold no kernels. A project with its own components/esp-nn must build that one and not the managed copy. The empty-app size check still passes with esp-nn compiled in. export_smoke_model.py loses --pte-output, which nothing uses now. Authored with Claude Code.
1 parent 4c21d62 commit 415d3ea

16 files changed

Lines changed: 576 additions & 161 deletions

File tree

‎.ci/scripts/test_espressif_idf_component.sh‎

Lines changed: 158 additions & 48 deletions
Large diffs are not rendered by default.

‎backends/espressif/espnn_runtime/include/espnn/espnn_backend.h‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,13 @@ struct DelegateStats {
3838
/**
3939
* Registers EspNNBackend with the ExecuTorch runtime.
4040
*
41-
* Registration also happens from a static initializer when the component is
42-
* linked whole-archive; calling this is only needed when the linker could
43-
* drop that initializer. Safe to call more than once.
41+
* Registration also happens from a static initializer once the delegate is
42+
* linked: when the espnn_runtime component is linked whole-archive or, in the
43+
* generated ExecuTorch ESP-IDF component, when the kernel bundle records
44+
* EspNNBackend or the project calls executorch_add_backend(EspNNBackend).
45+
* Without either, that component does not link the delegate, and calls to
46+
* these functions fail to link. Calling this is only needed when the linker
47+
* could drop the initializer. Safe to call more than once.
4448
*
4549
* @retval true The backend is registered.
4650
*/

‎examples/espressif/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ component from sources in this repository:
2121

2222
The output is a registry-shaped component (`idf_component.yml`,
2323
`CMakeLists.txt`, `Kconfig`, `VERSION`) containing the runtime, the
24-
portable, quantized and Cadence generic kernel libraries, the shared
24+
portable, quantized, Cadence generic and `esp_nn::` kernel libraries,
25+
the ESP-NN delegate, the shared
2526
ESP-IDF PAL from `platform/esp_pal.cpp` (ESP_LOG + `esp_timer` timing),
2627
and a C inference API (`et_model.h`, flash-XIP model bytes with
2728
internal/PSRAM arena placement). Only the kernels a project's models

‎examples/espressif/build_idf_component.sh‎

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
#
1616
# Derived from examples/arduino/build_arduino_library.sh with an ESP-IDF
1717
# output layout and the shared ESP-IDF PAL. The component vendors every
18-
# kernel source of its kernel libraries (portable, quantized and the
19-
# Cadence backend's generic kernels) but registers none of them: a
18+
# kernel source of its kernel libraries (portable, quantized, the Cadence
19+
# backend's generic kernels and the esp_nn:: op library) and the ESP-NN
20+
# delegate, but registers none of them: a
2021
# project's kernel bundle, made by the component's tools/gen_kernels.py
2122
# from its models, selects the kernels at build time.
2223
#
@@ -204,15 +205,24 @@ copy_srcs() { # ET_ROOT-relative paths on stdin; $1 = list file to append to
204205

205206
: > "$SCRATCH/core_srcs.txt"
206207
: > "$SCRATCH/kernel_srcs.txt"
208+
: > "$SCRATCH/espnn_srcs.txt"
207209
bzl_list EXECUTORCH_CORE_SRCS | copy_srcs "$SCRATCH/core_srcs.txt"
208210
# register_prim_ops.cpp is compiled by the build, with the bundle's prim
209211
# selection, only when a project has a kernel bundle.
210212
bzl_list EXECUTORCH_SRCS | grep -v '/register_prim_ops\.cpp$' \
211213
| copy_srcs "$SCRATCH/kernel_srcs.txt"
212214
echo "kernels/prim_ops/register_prim_ops.cpp" | copy_srcs "$SCRATCH/prim_src.txt"
213215
( cd "$ET_ROOT" && find kernels/portable/cpu kernels/quantized/cpu \
214-
backends/cadence/generic -name '*.cpp' -not -path '*/test*/*' | sort ) \
216+
backends/cadence/generic backends/espressif/ops -name '*.cpp' \
217+
-not -path '*/test*/*' | sort ) \
215218
| copy_srcs "$SCRATCH/kernel_srcs.txt"
219+
# The ESP-NN delegate runtime, an ESP-IDF component of its own in-tree.
220+
ESPNN_RUNTIME="$ET_ROOT/backends/espressif/espnn_runtime"
221+
mkdir -p "$OUT_DIR/src/espnn"
222+
for f in "$ESPNN_RUNTIME"/src/*.cpp; do
223+
cp "$f" "$OUT_DIR/src/espnn/"
224+
echo "src/espnn/$(basename "$f")" >> "$SCRATCH/espnn_srcs.txt"
225+
done
216226

217227
echo "[2/5] Runtime, extension and kernel sources copied"
218228

@@ -271,6 +281,10 @@ cp "$ET_ROOT/kernels/prim_ops/selective_build_prim_ops.h" \
271281
mkdir -p "$OUT_DIR/priv_include"
272282
cp -r "$ET_ROOT/third-party/flatbuffers/include/flatbuffers" \
273283
"$OUT_DIR/priv_include/flatbuffers"
284+
# Only espnn_backend.h is the delegate's API; the rest is internal to it.
285+
cp -r "$ESPNN_RUNTIME/include/espnn" "$OUT_DIR/priv_include/espnn"
286+
mkdir -p "$OUT_DIR/include/espnn"
287+
mv "$OUT_DIR/priv_include/espnn/espnn_backend.h" "$OUT_DIR/include/espnn/"
274288

275289
echo "[3/5] Headers and third-party code copied"
276290

@@ -283,6 +297,7 @@ LIBRARIES=(
283297
"portable kernels/portable/functions.yaml"
284298
"quantized kernels/quantized/quantized.yaml"
285299
"cadence backends/cadence/aot/functions.yaml"
300+
"esp_nn backends/espressif/ops/operators.yaml"
286301
)
287302
mkdir -p "$OUT_DIR/tools/libraries" "$OUT_DIR/tools/codegen"
288303
cp "$TEMPLATE_DIR/tools/gen_kernels.py" "$OUT_DIR/tools/"
@@ -315,7 +330,7 @@ cp "$TEMPLATE_DIR/CMakeLists.txt" "$TEMPLATE_DIR/project_include.cmake" \
315330
"$TEMPLATE_DIR/README.md" "$OUT_DIR/"
316331
cp "$TEMPLATE_DIR/include/et_model.h" "$OUT_DIR/include/"
317332
mkdir -p "$OUT_DIR/src/platform"
318-
cp "$TEMPLATE_DIR/src/et_model.cpp" "$OUT_DIR/src/"
333+
cp "$TEMPLATE_DIR/src/et_model.cpp" "$TEMPLATE_DIR/src/espnn_anchor.cpp" "$OUT_DIR/src/"
319334
cp "$ESP_PAL_SRC" "$OUT_DIR/src/platform/"
320335
cp "$ET_ROOT/LICENSE" "$OUT_DIR/LICENSE"
321336

@@ -349,9 +364,18 @@ NOTICE
349364
sort -u "$SCRATCH/kernel_srcs.txt" | sed 's/.*/ "&"/'
350365
echo ")"
351366
echo "set(ET_PRIM_OPS_SRC \"$(cat "$SCRATCH/prim_src.txt")\")"
367+
echo "set(ET_ESPNN_SRCS"
368+
sed 's/.*/ "&"/' "$SCRATCH/espnn_srcs.txt"
369+
echo ")"
352370
} > "$OUT_DIR/et_generated.cmake"
353371

354-
cat > "$OUT_DIR/Kconfig" <<'KCONFIG'
372+
sed -n '/^menu /,/^endmenu/p' "$ESPNN_RUNTIME/Kconfig" > "$SCRATCH/espnn_kconfig"
373+
if [[ ! -s "$SCRATCH/espnn_kconfig" ]]; then
374+
echo "ERROR: $ESPNN_RUNTIME/Kconfig has no menu to include." >&2
375+
exit 1
376+
fi
377+
{
378+
cat <<'KCONFIG'
355379
# GENERATED by examples/espressif/build_idf_component.sh -- do not edit.
356380
menu "ExecuTorch"
357381
@@ -413,8 +437,12 @@ menu "ExecuTorch"
413437
Raise this when the application registers its own kernels.
414438
Must not be below the kernel bundle's kernel count.
415439
416-
endmenu
417440
KCONFIG
441+
# The ESP-NN delegate's options, as a submenu.
442+
sed 's/^./ &/' "$SCRATCH/espnn_kconfig"
443+
echo ""
444+
echo "endmenu"
445+
} > "$OUT_DIR/Kconfig"
418446

419447
sed "s/@ET_COMPONENT_VERSION@/$ET_COMPONENT_VERSION/" \
420448
"$TEMPLATE_DIR/idf_component.yml.in" > "$OUT_DIR/idf_component.yml"

‎examples/espressif/export_smoke_model.py‎

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ def forward(self, x, y):
2626
def main() -> None:
2727
parser = argparse.ArgumentParser(description=__doc__)
2828
parser.add_argument("--output", type=Path, required=True)
29-
parser.add_argument(
30-
"--pte-output", type=Path, help="Also write the plain (unbundled) .pte"
31-
)
3229
args = parser.parse_args()
3330

3431
model = SmokeModel().eval()
@@ -46,10 +43,6 @@ def main() -> None:
4643
args.output.parent.mkdir(parents=True, exist_ok=True)
4744
args.output.write_bytes(serialize_from_bundled_program_to_flatbuffer(bundle))
4845
print(f"Exported {args.output}")
49-
if args.pte_output:
50-
args.pte_output.parent.mkdir(parents=True, exist_ok=True)
51-
args.pte_output.write_bytes(program.buffer)
52-
print(f"Exported {args.pte_output}")
5346

5447

5548
if __name__ == "__main__":

‎examples/espressif/idf_component_smoke/CMakeLists.txt‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66

77
# Minimal ESP-IDF consumer of the generated ExecuTorch component, used by
88
# .ci/scripts/test_espressif_idf_component.sh. Pass the generated component
9-
# directory as ET_COMPONENT_DIR, the plain smoke .pte from export_smoke_model.py
10-
# as ET_SMOKE_PTE, and optionally its kernel bundle from the component's
11-
# tools/gen_kernels.py as ET_SMOKE_KERNELS; without one the build makes it.
12-
# ET_SMOKE_UNDECLARED runs the model without declaring it. Without ET_SMOKE_PTE
13-
# it builds an app that does not use ExecuTorch, for size comparisons.
9+
# directory as ET_COMPONENT_DIR and the .pte files that split_bundled_program.py
10+
# made as the ET_SMOKE_MODELS list; each runs on its bundled test case. Pass a
11+
# kernel bundle from the component's tools/gen_kernels.py as ET_SMOKE_KERNELS,
12+
# or leave it out to have the build make one. With ET_SMOKE_BACKENDS, the models
13+
# are embedded but not declared and only those delegates are, as for models
14+
# delivered over the air; with ET_SMOKE_UNDECLARED they are embedded but not
15+
# declared. Without ET_SMOKE_MODELS it builds an app that does not use
16+
# ExecuTorch, for size comparisons.
1417

1518
cmake_minimum_required(VERSION 3.16)
1619

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Export a quantized mul that runs on the esp_nn:: op library, as a bundled program."""
8+
9+
import argparse
10+
from pathlib import Path
11+
12+
import torch
13+
from executorch.backends.espressif.test.tester import EspTester
14+
from executorch.backends.test.harness.stages import StageType
15+
from executorch.devtools.bundled_program.config import MethodTestCase, MethodTestSuite
16+
from executorch.devtools.bundled_program.core import BundledProgram
17+
from executorch.devtools.bundled_program.serialize import (
18+
serialize_from_bundled_program_to_flatbuffer,
19+
)
20+
from torch.utils._pytree import tree_flatten
21+
22+
23+
class Mul(torch.nn.Module):
24+
def forward(self, x, y):
25+
return x * y
26+
27+
28+
def main():
29+
parser = argparse.ArgumentParser(description=__doc__)
30+
parser.add_argument("--output", type=Path, required=True)
31+
args = parser.parse_args()
32+
33+
inputs = (torch.linspace(-1.5, 2.5, 17), torch.linspace(3, -4, 17))
34+
tester = EspTester(Mul(), inputs)
35+
tester.quantize().export().to_edge().run_passes()
36+
expected, _ = tree_flatten(
37+
tester.get_artifact().exported_program().module()(*inputs)
38+
)
39+
program = tester.to_executorch().get_artifact(StageType.TO_EXECUTORCH)
40+
operators = program.executorch_program.execution_plan[0].operators
41+
if "esp_nn::quantized_mul_int8" not in {op.name for op in operators}:
42+
raise SystemExit("the mul did not lower to esp_nn::quantized_mul_int8")
43+
bundle = BundledProgram(
44+
program, [MethodTestSuite("forward", [MethodTestCase(inputs, expected)])]
45+
)
46+
args.output.write_bytes(serialize_from_bundled_program_to_flatbuffer(bundle))
47+
48+
49+
if __name__ == "__main__":
50+
main()

‎examples/espressif/idf_component_smoke/main/CMakeLists.txt‎

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,55 @@
44
# This source code is licensed under the BSD-style license found in the
55
# LICENSE file in the root directory of this source tree.
66

7-
if(ET_SMOKE_PTE)
8-
set(srcs "main.c" "model.S")
7+
if(ET_SMOKE_MODELS)
8+
set(srcs "main.c" "${CMAKE_CURRENT_BINARY_DIR}/models.S")
99
else()
1010
set(srcs "empty.c")
1111
endif()
1212
idf_component_register(SRCS ${srcs} PRIV_REQUIRES executorch)
13-
if(NOT ET_SMOKE_PTE)
13+
if(NOT ET_SMOKE_MODELS)
1414
return()
1515
endif()
1616

17+
# Embeds each model and the .io file split_bundled_program.py wrote next to it,
18+
# with the table main.c walks.
19+
set(table "")
20+
set(data "")
21+
set(files "")
22+
set(index 0)
23+
foreach(pte IN LISTS ET_SMOKE_MODELS)
24+
get_filename_component(pte "${pte}" ABSOLUTE)
25+
get_filename_component(name "${pte}" NAME_WLE)
26+
get_filename_component(dir "${pte}" DIRECTORY)
27+
set(io "${dir}/${name}.io")
28+
string(
29+
APPEND table
30+
" .word name_${index}, pte_${index}, pte_end_${index}, io_${index}\n"
31+
)
32+
string(
33+
APPEND data "name_${index}:\n .asciz \"${name}\"\n"
34+
" .balign 16\npte_${index}:\n .incbin \"${pte}\"\npte_end_${index}:\n"
35+
" .balign 16\nio_${index}:\n .incbin \"${io}\"\n"
36+
)
37+
list(APPEND files "${pte}" "${io}")
38+
if(NOT ET_SMOKE_BACKENDS AND NOT ET_SMOKE_UNDECLARED)
39+
executorch_add_model(PTE "${pte}")
40+
endif()
41+
math(EXPR index "${index} + 1")
42+
endforeach()
43+
file(
44+
CONFIGURE
45+
OUTPUT
46+
"${CMAKE_CURRENT_BINARY_DIR}/models.S"
47+
CONTENT
48+
" .section .rodata.smoke_models, \"a\"\n .balign 4\n .global smoke_models\nsmoke_models:\n${table} .global smoke_model_count\nsmoke_model_count:\n .word ${index}\n${data}\n .section .note.GNU-stack, \"\", @progbits\n"
49+
)
1750
set_source_files_properties(
18-
model.S PROPERTIES COMPILE_DEFINITIONS "ET_SMOKE_PTE=\"${ET_SMOKE_PTE}\""
51+
"${CMAKE_CURRENT_BINARY_DIR}/models.S" PROPERTIES OBJECT_DEPENDS "${files}"
1952
)
20-
if(NOT ET_SMOKE_UNDECLARED)
21-
executorch_add_model(PTE "${ET_SMOKE_PTE}")
22-
endif()
2353
if(ET_SMOKE_KERNELS)
2454
executorch_add_kernels(DIR "${ET_SMOKE_KERNELS}")
2555
endif()
56+
foreach(backend IN LISTS ET_SMOKE_BACKENDS)
57+
executorch_add_backend(${backend})
58+
endforeach()

0 commit comments

Comments
 (0)