Skip to content

Commit 196a4aa

Browse files
psiddhGasoonjiamergennachinclaude
authored
Zephyr: Add MobileNetV2 image classification sample with Ethos-U NPU (#19131)
Add a new Zephyr sample that runs a quantized INT8 MobileNetV2 model on Arm Ethos-U NPU using ExecuTorch. The sample classifies a static 224x224x3 RGB test image into 1000 ImageNet classes and prints the top-5 predictions. Validated end-to-end on Alif Ensemble E8 DevKit (Cortex-M55 + Ethos-U55 256 MAC) achieving 19ms inference with 100% NPU delegation (110 ops). This addresses part of #17654 (Zephyr: Expand samples and documentation) by adding a second sample app (MV2) beyond the existing hello-executorch. --------- Co-authored-by: Gasoonjia <gasoonjia@icloud.com> Co-authored-by: Mergen Nachin <mnachin@meta.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8bda9ac commit 196a4aa

11 files changed

Lines changed: 13530 additions & 0 deletions

File tree

.github/workflows/trunk.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ on:
1111
paths:
1212
- .ci/docker/ci_commit_pins/pytorch.txt
1313
- .ci/scripts/**
14+
- zephyr/**
1415
workflow_dispatch:
1516

1617
concurrency:
@@ -179,6 +180,39 @@ jobs:
179180
run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "<!-- RUN test_${TARGET}_build_and_run -->"
180181
done
181182
183+
# MV2 Ethos-U sample (NPU targets only — skips cortex-m55)
184+
MV2_README_PATH="zephyr/samples/mv2-ethosu/README.md"
185+
186+
for TARGET in "${TARGETS[@]}"; do
187+
TARGET="$(echo "$TARGET" | xargs)"
188+
189+
if [[ ${TARGET} == "cortex-m55" ]]; then
190+
echo "---- Skipping MV2 for ${TARGET} (NPU-only sample) ----"
191+
continue
192+
fi
193+
194+
echo "---- MV2 ${TARGET} ----"
195+
rm -Rf build
196+
197+
if [[ ${TARGET} == "ethos-u55" ]]; then
198+
BOARD="corstone300"
199+
elif [[ ${TARGET} == "ethos-u85" ]]; then
200+
BOARD="corstone320"
201+
else
202+
echo "Fail unsupported target selection ${TARGET}"
203+
exit 1
204+
fi
205+
206+
echo "---- MV2 ${TARGET} Board ${BOARD} FVP setup ----"
207+
run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "<!-- RUN setup_${BOARD}_fvp -->"
208+
209+
echo "---- MV2 ${TARGET} Create PTE ----"
210+
run_command_block_from_readme "${MV2_README_PATH}" "<!-- RUN test_mv2_${TARGET}_generate_pte -->"
211+
212+
echo "---- MV2 ${TARGET} Build and run ----"
213+
run_command_block_from_readme "${MV2_README_PATH}" "<!-- RUN test_mv2_${TARGET}_build_and_run -->"
214+
done
215+
182216
test-models-linux-aarch64:
183217
name: test-models-linux-aarch64
184218
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
# Copyright 2025-2026 Arm Limited and/or its affiliates.
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
cmake_minimum_required(VERSION 3.24)
8+
9+
set(CMAKE_SKIP_INSTALL_RULES
10+
ON
11+
CACHE BOOL "" FORCE
12+
)
13+
14+
set(ET_PTE_FILE_PATH
15+
""
16+
CACHE FILEPATH "Path to the ExecuTorch .pte (or .bpte) model to embed"
17+
)
18+
set(ET_PTE_SECTION
19+
"network_model_sec"
20+
CACHE STRING "Section attribute used for the generated model data"
21+
)
22+
23+
if(NOT ET_PTE_FILE_PATH)
24+
message(
25+
FATAL_ERROR
26+
"ET_PTE_FILE_PATH must point to the ExecuTorch .pte (or .bpte) model to embed."
27+
)
28+
endif()
29+
30+
if(NOT IS_ABSOLUTE "${ET_PTE_FILE_PATH}")
31+
get_filename_component(
32+
ET_PTE_FILE_PATH "${ET_PTE_FILE_PATH}" ABSOLUTE BASE_DIR
33+
"${CMAKE_CURRENT_SOURCE_DIR}"
34+
)
35+
endif()
36+
37+
if(NOT EXISTS "${ET_PTE_FILE_PATH}")
38+
message(
39+
FATAL_ERROR
40+
"Could not find ExecuTorch model at ET_PTE_FILE_PATH: ${ET_PTE_FILE_PATH}"
41+
)
42+
endif()
43+
44+
set(ET_PTE_FILE_PATH
45+
"${ET_PTE_FILE_PATH}"
46+
CACHE FILEPATH "Path to the ExecuTorch .pte (or .bpte) model to embed"
47+
FORCE
48+
)
49+
50+
find_package(
51+
Python3
52+
COMPONENTS Interpreter
53+
REQUIRED
54+
)
55+
56+
execute_process(
57+
COMMAND
58+
${Python3_EXECUTABLE}
59+
"${CMAKE_CURRENT_LIST_DIR}/../../../codegen/tools/gen_oplist.py"
60+
--model_file_path=${ET_PTE_FILE_PATH}
61+
--output_path=${CMAKE_CURRENT_BINARY_DIR}/temp.yaml
62+
RESULT_VARIABLE GEN_OPLIST_RESULT
63+
OUTPUT_VARIABLE CMD_RESULT
64+
ERROR_VARIABLE GEN_OPLIST_ERROR
65+
)
66+
67+
if(NOT GEN_OPLIST_RESULT EQUAL 0)
68+
message(
69+
FATAL_ERROR
70+
"gen_oplist.py failed with exit code ${GEN_OPLIST_RESULT}: ${GEN_OPLIST_ERROR}"
71+
)
72+
endif()
73+
74+
if(CMD_RESULT MATCHES "aten::" OR CMD_RESULT MATCHES "dim_order_ops::")
75+
set(FOUND_OPS_IN_FILE "true")
76+
else()
77+
set(FOUND_OPS_IN_FILE "false")
78+
endif()
79+
80+
if(${FOUND_OPS_IN_FILE})
81+
set(EXECUTORCH_SELECT_OPS_LIST "")
82+
set(EXECUTORCH_SELECT_OPS_MODEL
83+
"${ET_PTE_FILE_PATH}"
84+
CACHE STRING "Select operators from this ExecuTorch model" FORCE
85+
)
86+
set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS ON)
87+
message(
88+
"gen_oplist: EXECUTORCH_SELECT_OPS_MODEL=${ET_PTE_FILE_PATH} is used to auto generate ops from"
89+
)
90+
else()
91+
set(EXECUTORCH_SELECT_OPS_LIST "")
92+
set(EXECUTORCH_SELECT_OPS_MODEL "")
93+
set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS OFF)
94+
message(
95+
"gen_oplist: No non-delegated ops were found in ${ET_PTE_FILE_PATH}; no ops added to build"
96+
)
97+
endif()
98+
99+
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
100+
project(executorch_mv2_ethosu)
101+
102+
set(CMAKE_CXX_FLAGS
103+
"${CMAKE_CXX_FLAGS} -Wall -Wno-switch -Wno-float-conversion -Wno-double-promotion -ffunction-sections -fdata-sections"
104+
)
105+
106+
if(NOT DEFINED EXECUTORCH_DIR)
107+
message(
108+
STATUS
109+
"ZEPHYR_EXECUTORCH_MODULE_DIR set to : ${ZEPHYR_EXECUTORCH_MODULE_DIR}"
110+
)
111+
if(DEFINED ZEPHYR_EXECUTORCH_MODULE_DIR)
112+
set(EXECUTORCH_DIR ${ZEPHYR_EXECUTORCH_MODULE_DIR})
113+
message(
114+
STATUS "Using Zephyr module discovery: EXECUTORCH_DIR=${EXECUTORCH_DIR}"
115+
)
116+
else()
117+
message(
118+
FATAL_ERROR
119+
"ExecuTorch module not found. Ensure it's properly configured in your Zephyr workspace."
120+
)
121+
endif()
122+
else()
123+
message(STATUS "Using predefined EXECUTORCH_DIR=${EXECUTORCH_DIR}")
124+
endif()
125+
126+
set(EXECUTORCH_ROOT ${EXECUTORCH_DIR})
127+
include(${EXECUTORCH_DIR}/tools/cmake/Utils.cmake)
128+
129+
if(NOT TARGET portable_kernels)
130+
set(EXECUTORCH_PORTABLE_BUILD_KERNELS_ONLY ON)
131+
add_subdirectory(
132+
${EXECUTORCH_DIR}/kernels/portable
133+
${CMAKE_CURRENT_BINARY_DIR}/executorch/kernels/portable
134+
)
135+
unset(EXECUTORCH_PORTABLE_BUILD_KERNELS_ONLY)
136+
endif()
137+
set(EXECUTORCH_OPS_LIB "")
138+
if(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS)
139+
include(${EXECUTORCH_DIR}/tools/cmake/Codegen.cmake)
140+
if(NOT DEFINED EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD)
141+
set(EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD "")
142+
endif()
143+
gen_selected_ops(
144+
LIB_NAME
145+
"cpu_portable_ops_lib"
146+
OPS_SCHEMA_YAML
147+
""
148+
ROOT_OPS
149+
"${EXECUTORCH_SELECT_OPS_LIST}"
150+
INCLUDE_ALL_OPS
151+
""
152+
OPS_FROM_MODEL
153+
"${EXECUTORCH_SELECT_OPS_MODEL}"
154+
DTYPE_SELECTIVE_BUILD
155+
"${EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD}"
156+
)
157+
generate_bindings_for_kernels(
158+
LIB_NAME "cpu_portable_ops_lib" FUNCTIONS_YAML
159+
${EXECUTORCH_DIR}/kernels/portable/functions.yaml DTYPE_SELECTIVE_BUILD
160+
"${EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD}"
161+
)
162+
gen_operators_lib(
163+
LIB_NAME
164+
"cpu_portable_ops_lib"
165+
KERNEL_LIBS
166+
portable_kernels
167+
DEPS
168+
executorch
169+
DTYPE_SELECTIVE_BUILD
170+
"${EXECUTORCH_ENABLE_DTYPE_SELECTIVE_BUILD}"
171+
)
172+
set(EXECUTORCH_OPS_LIB "cpu_portable_ops_lib")
173+
endif()
174+
175+
set(_local_flatcc_root ${CMAKE_BINARY_DIR}/flatcc_src)
176+
if(NOT EXISTS ${_local_flatcc_root}/CMakeLists.txt)
177+
file(MAKE_DIRECTORY ${_local_flatcc_root})
178+
execute_process(
179+
COMMAND ${CMAKE_COMMAND} -E copy_directory
180+
${EXECUTORCH_DIR}/third-party/flatcc ${_local_flatcc_root}
181+
)
182+
endif()
183+
set(EXECUTORCH_FLATCC_SOURCE_ROOT
184+
${_local_flatcc_root}
185+
CACHE PATH "" FORCE
186+
)
187+
set(EXECUTORCH_FLATCC_INSTALL_ROOT
188+
${_local_flatcc_root}
189+
CACHE PATH "" FORCE
190+
)
191+
192+
set(app_sources
193+
src/main.cpp
194+
${EXECUTORCH_DIR}/examples/arm/executor_runner/arm_memory_allocator.cpp
195+
)
196+
target_sources(app PRIVATE ${app_sources})
197+
198+
set(_model_pte_header ${CMAKE_CURRENT_BINARY_DIR}/model_pte.h)
199+
add_custom_command(
200+
OUTPUT ${_model_pte_header}
201+
COMMAND
202+
${Python3_EXECUTABLE}
203+
${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py --pte
204+
${ET_PTE_FILE_PATH} --outdir ${CMAKE_CURRENT_BINARY_DIR} --section
205+
${ET_PTE_SECTION}
206+
DEPENDS ${ET_PTE_FILE_PATH}
207+
${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py
208+
COMMENT "Converting ${ET_PTE_FILE_PATH} to model_pte.h"
209+
)
210+
add_custom_target(gen_model_header DEPENDS ${_model_pte_header})
211+
add_dependencies(app gen_model_header)
212+
213+
if(DEFINED CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE)
214+
target_compile_definitions(
215+
app
216+
PRIVATE
217+
ET_ARM_METHOD_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE}
218+
)
219+
endif()
220+
if(DEFINED CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE)
221+
target_compile_definitions(
222+
app
223+
PRIVATE
224+
ET_ARM_BAREMETAL_SCRATCH_TEMP_ALLOCATOR_POOL_SIZE=${CONFIG_EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE}
225+
)
226+
endif()
227+
228+
target_link_libraries(app PRIVATE libexecutorch)
229+
if(EXECUTORCH_OPS_LIB)
230+
target_link_libraries(app PRIVATE ${EXECUTORCH_OPS_LIB})
231+
endif()
232+
if(CONFIG_CPU_CORTEX_M)
233+
if(TARGET cortex_m_ops_lib)
234+
target_link_libraries(app PRIVATE cortex_m_ops_lib)
235+
endif()
236+
if(TARGET cortex_m_kernels)
237+
executorch_target_link_options_shared_lib(cortex_m_kernels)
238+
target_link_libraries(app PRIVATE cortex_m_kernels)
239+
endif()
240+
endif()
241+
if(TARGET quantized_kernels)
242+
executorch_target_link_options_shared_lib(quantized_kernels)
243+
target_link_libraries(app PRIVATE quantized_kernels)
244+
endif()
245+
if(TARGET portable_kernels)
246+
executorch_target_link_options_shared_lib(portable_kernels)
247+
target_link_libraries(app PRIVATE portable_kernels)
248+
endif()
249+
if(TARGET executorch_delegate_ethos_u)
250+
executorch_target_link_options_shared_lib(executorch_delegate_ethos_u)
251+
target_link_libraries(app PRIVATE executorch_delegate_ethos_u)
252+
endif()
253+
if(TARGET ethosu_core_driver)
254+
target_link_libraries(app PRIVATE ethosu_core_driver)
255+
endif()
256+
257+
target_include_directories(app PRIVATE src ${CMAKE_CURRENT_BINARY_DIR})
258+
get_target_property(OUT app LINK_LIBRARIES)
259+
message(STATUS ${OUT})

zephyr/samples/mv2-ethosu/Kconfig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
# Copyright 2025-2026 Arm Limited and/or its affiliates.
4+
#
5+
# SPDX-License-Identifier: Apache-2.0
6+
7+
source "Kconfig.zephyr"
8+
9+
menu "ExecuTorch MobileNetV2 sample configuration"
10+
11+
config EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE
12+
int "Method allocator pool size in bytes"
13+
default 1572864
14+
depends on EXECUTORCH
15+
help
16+
Size of the method allocator pool in bytes. MobileNetV2 requires
17+
more memory than simple models. Default is 1.5MB which is sufficient
18+
for a fully NPU-delegated INT8 MobileNetV2 model.
19+
20+
config EXECUTORCH_TEMP_ALLOCATOR_POOL_SIZE
21+
int "Temporary allocator pool size in bytes"
22+
default 1572864
23+
depends on EXECUTORCH
24+
help
25+
Size of the temporary allocator pool in bytes. Default is 1.5MB
26+
which provides sufficient scratch space for MobileNetV2 inference
27+
on Ethos-U NPU.
28+
29+
endmenu

0 commit comments

Comments
 (0)