diff --git a/.github/workflows/trunk.yml b/.github/workflows/trunk.yml index 670517f836b..fb732a4de4a 100644 --- a/.github/workflows/trunk.yml +++ b/.github/workflows/trunk.yml @@ -174,11 +174,11 @@ jobs: echo "---- ${TARGET} Board ${BOARD} FVP setup ----" run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "" - echo "---- ${TARGET} Create PTE ----" - run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "" - echo "---- ${TARGET} Build and run ----" run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "" + + echo "---- ${TARGET} Build and run Separate PTE ----" + run_command_block_from_readme "${ZEPHYR_SAMPLES_README_PATH}" "" done # MV2 Ethos-U sample (NPU targets only — skips cortex-m55) diff --git a/.gitignore b/.gitignore index c644211f1c0..da1d154b462 100644 --- a/.gitignore +++ b/.gitignore @@ -76,6 +76,8 @@ xcuserdata/ *.dll *.pyd +# Zephyr +zephyr_scratch/* # Agents .claude/*.local.* diff --git a/tools/cmake/ExportModel.cmake b/tools/cmake/ExportModel.cmake new file mode 100644 index 00000000000..7ec6596a1ec --- /dev/null +++ b/tools/cmake/ExportModel.cmake @@ -0,0 +1,158 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +include(${CMAKE_CURRENT_LIST_DIR}/Utils.cmake) + +# Shared setup for Python-based model exporters used by both the configure-time +# and build-time helper entry points below. +function( + _executorch_prepare_python_export + caller + script + output + working_directory + python_executable + out_command_var + out_output_var +) + if(NOT script) + message(FATAL_ERROR "${caller} requires SCRIPT to be set") + endif() + if(NOT output) + message(FATAL_ERROR "${caller} requires OUTPUT to be set") + endif() + if(NOT working_directory) + message(FATAL_ERROR "${caller} requires WORKING_DIRECTORY to be set") + endif() + if(NOT IS_DIRECTORY "${working_directory}") + message( + FATAL_ERROR + "${caller} requires WORKING_DIRECTORY to exist: ${working_directory}" + ) + endif() + + if(NOT python_executable) + resolve_python_executable() + set(python_executable ${PYTHON_EXECUTABLE}) + endif() + + if(NOT EXISTS "${script}") + message(FATAL_ERROR "Python exporter script does not exist: ${script}") + endif() + + if(IS_ABSOLUTE "${output}") + set(_et_export_output "${output}") + else() + # Resolve relative output paths from the exporter's working directory so + # configure-time checks and build-time outputs refer to the same file. + get_filename_component( + _et_export_output "${output}" ABSOLUTE BASE_DIR "${working_directory}" + ) + endif() + + get_filename_component(_et_export_output_dir "${_et_export_output}" DIRECTORY) + if(_et_export_output_dir AND NOT EXISTS "${_et_export_output_dir}") + file(MAKE_DIRECTORY "${_et_export_output_dir}") + endif() + + set(_et_export_command ${python_executable} ${script} ${ARGN}) + set(${out_command_var} + ${_et_export_command} + PARENT_SCOPE + ) + set(${out_output_var} + "${_et_export_output}" + PARENT_SCOPE + ) +endfunction() + +# Run a Python exporter at configure time and require it to produce OUTPUT. +# +# Backend-specific lowering options should be expressed in ARGS and remain owned +# by the Python script. This helper runs the exporter immediately during CMake +# configure, then verifies that the expected output file was written. +function(executorch_run_python_exporter out_var) + cmake_parse_arguments( + ARG "" "SCRIPT;OUTPUT;WORKING_DIRECTORY;PYTHON_EXECUTABLE" "ARGS" ${ARGN} + ) + + _executorch_prepare_python_export( + executorch_run_python_exporter + "${ARG_SCRIPT}" + "${ARG_OUTPUT}" + "${ARG_WORKING_DIRECTORY}" + "${ARG_PYTHON_EXECUTABLE}" + _et_export_command + _et_export_output + ${ARG_ARGS} + ) + + execute_process( + COMMAND ${_et_export_command} + WORKING_DIRECTORY "${ARG_WORKING_DIRECTORY}" + RESULT_VARIABLE _et_export_status + OUTPUT_VARIABLE _et_export_stdout + ERROR_VARIABLE _et_export_stderr + ) + + if(NOT _et_export_status EQUAL 0) + message( + FATAL_ERROR + "Python exporter failed for ${ARG_SCRIPT}\nstdout:\n${_et_export_stdout}\nstderr:\n${_et_export_stderr}" + ) + endif() + + if(NOT EXISTS "${_et_export_output}") + message( + FATAL_ERROR + "Python exporter ${ARG_SCRIPT} completed but did not produce ${_et_export_output}" + ) + endif() + + set(${out_var} + "${_et_export_output}" + PARENT_SCOPE + ) +endfunction() + +# Add a build-time target that runs a Python exporter and materializes OUTPUT. +# Unlike executorch_run_python_exporter(), this helper defers execution until +# the build graph runs and exposes the generated file through a custom target. +function(executorch_add_python_export_target) + cmake_parse_arguments( + ARG "" "TARGET;SCRIPT;OUTPUT;WORKING_DIRECTORY;PYTHON_EXECUTABLE" + "ARGS;DEPENDS" ${ARGN} + ) + + if(NOT ARG_TARGET) + message( + FATAL_ERROR + "executorch_add_python_export_target requires TARGET to be set" + ) + endif() + + _executorch_prepare_python_export( + executorch_add_python_export_target + "${ARG_SCRIPT}" + "${ARG_OUTPUT}" + "${ARG_WORKING_DIRECTORY}" + "${ARG_PYTHON_EXECUTABLE}" + _et_export_command + _et_export_output + ${ARG_ARGS} + ) + + add_custom_command( + OUTPUT ${_et_export_output} + COMMAND ${_et_export_command} + COMMAND_EXPAND_LISTS + DEPENDS ${ARG_SCRIPT} ${ARG_DEPENDS} + WORKING_DIRECTORY "${ARG_WORKING_DIRECTORY}" + COMMENT "Generating model with ${ARG_SCRIPT}" + VERBATIM + ) + + add_custom_target(${ARG_TARGET} DEPENDS ${_et_export_output}) +endfunction() diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index 6b6970f30ce..cad38990fc4 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -11,6 +11,7 @@ if(CONFIG_EXECUTORCH) set(EXECUTORCH_DIR ${ZEPHYR_CURRENT_MODULE_DIR}) message(STATUS "EXECUTORCH_DIR set to: ${EXECUTORCH_DIR}") + include(${EXECUTORCH_DIR}/zephyr/ExecuTorchZephyrModel.cmake) include(${EXECUTORCH_DIR}/tools/cmake/common/preset.cmake) include(${EXECUTORCH_DIR}/tools/cmake/preset/zephyr.cmake) diff --git a/zephyr/ExecuTorchZephyrModel.cmake b/zephyr/ExecuTorchZephyrModel.cmake new file mode 100644 index 00000000000..7d6873419cf --- /dev/null +++ b/zephyr/ExecuTorchZephyrModel.cmake @@ -0,0 +1,376 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +include(${EXECUTORCH_DIR}/tools/cmake/ExportModel.cmake) + +# Resolve a model path from user configuration and handle ET_PTE_FILE_PATH +# and/or CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT. +# +# If CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT is not set, return ET_PTE_FILE_PATH. +# +# If a model was converted by a custom Python exporter script, the output file +# is saved/cached in EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT so reruns can detect if +# the user points ET_PTE_FILE_PATH at the same file and avoid regenerating it. +# If auto export is not used EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT will be +# cleared. +# +# If ET_PTE_FILE_PATH is set and not equal to +# EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT or the name of the generated pte, clear +# the EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT cmake cache value and return +# ET_PTE_FILE_PATH. +# +# Otherwise, try finding the specified model by absolute path, in the CMake +# source, or zephyr source. +# +# If a model is auto-generated, save its output path so later configure-time +# logic can recognize it as build-generated output. +function(executorch_zephyr_resolve_model out_var) + + get_filename_component(_et_zephyr_workspace_dir "${ZEPHYR_BASE}/../" ABSOLUTE) + set(_et_pte_file_path "${ET_PTE_FILE_PATH}") + set(_et_has_export_script FALSE) + if(DEFINED CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT) + if(NOT "${CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT}" STREQUAL "") + set(_et_has_export_script TRUE) + endif() + endif() + + # EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT caches the generated file from the + # previous configure step. Read it first, then clear it so each configure pass + # recomputes the active generated output from the current settings. + set(_et_auto_model_output "${EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT}") + + set(EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT + "" + CACHE INTERNAL "Auto-generated ExecuTorch model path" FORCE + ) + set_property(DIRECTORY PROPERTY EXECUTORCH_ZEPHYR_GENERATED_MODEL_TARGET "") + set_property(DIRECTORY PROPERTY EXECUTORCH_ZEPHYR_GENERATED_MODEL_OUTPUT "") + + if(_et_has_export_script) + set(_et_resolved_export_script "${CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT}") + find_file( + _et_found_export_script + NAMES "${_et_resolved_export_script}" + HINTS "${CMAKE_CURRENT_SOURCE_DIR}" "${_et_zephyr_workspace_dir}" + PATHS "" + ) + if(NOT _et_found_export_script OR NOT EXISTS "${_et_found_export_script}") + message( + FATAL_ERROR + "CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT points to a missing file: ${CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT}" + ) + endif() + + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${_et_found_export_script}" + ) + + if(NOT "${CONFIG_EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES}" STREQUAL "") + separate_arguments( + _et_export_python_dependencies NATIVE_COMMAND + "${CONFIG_EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES}" + ) + + foreach(_et_export_python_dependency IN + LISTS _et_export_python_dependencies + ) + if(IS_ABSOLUTE "${_et_export_python_dependency}") + set(_et_found_export_dependency "${_et_export_python_dependency}") + elseif(EXISTS + "${CMAKE_CURRENT_SOURCE_DIR}/${_et_export_python_dependency}" + ) + get_filename_component( + _et_found_export_dependency + "${CMAKE_CURRENT_SOURCE_DIR}/${_et_export_python_dependency}" + ABSOLUTE + ) + elseif(EXISTS + "${_et_zephyr_workspace_dir}/${_et_export_python_dependency}" + ) + get_filename_component( + _et_found_export_dependency + "${_et_zephyr_workspace_dir}/${_et_export_python_dependency}" + ABSOLUTE + ) + else() + message( + FATAL_ERROR + "CONFIG_EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES points to a missing file: ${_et_export_python_dependency}" + ) + endif() + + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS "${_et_found_export_dependency}" + ) + endforeach() + endif() + + set(_et_export_python_working_directory "${CMAKE_CURRENT_BINARY_DIR}") + if(NOT "${CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT}" STREQUAL "") + if(IS_ABSOLUTE "${CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT}") + set(_et_generated_pte + "${CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT}" + ) + else() + set(_et_generated_pte + "${_et_export_python_working_directory}/${CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT}" + ) + endif() + else() + set(_et_generated_pte "${CMAKE_CURRENT_BINARY_DIR}/configured_model.pte") + endif() + + if(_et_pte_file_path + AND NOT _et_pte_file_path STREQUAL _et_auto_model_output + AND NOT _et_pte_file_path STREQUAL _et_generated_pte + ) + message( + STATUS + "ET_PTE_FILE_PATH is set to a different file than the generated model, using ET_PTE_FILE_PATH: ${_et_pte_file_path} and ignoring CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT: ${CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT}" + ) + else() + set_property( + DIRECTORY PROPERTY EXECUTORCH_ZEPHYR_GENERATED_MODEL_OUTPUT + "${_et_generated_pte}" + ) + + set(_et_export_python_args) + if(NOT "${CONFIG_EXECUTORCH_EXPORT_PYTHON_ARGS}" STREQUAL "") + separate_arguments( + _et_export_python_args NATIVE_COMMAND + "${CONFIG_EXECUTORCH_EXPORT_PYTHON_ARGS}" + ) + endif() + + message( + STATUS + "Generating model from ${CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT} with Python exporter" + ) + executorch_run_python_exporter( + _et_generated_model_path + SCRIPT + "${_et_found_export_script}" + OUTPUT + "${_et_generated_pte}" + WORKING_DIRECTORY + "${_et_export_python_working_directory}" + PYTHON_EXECUTABLE + "${Python3_EXECUTABLE}" + ARGS + ${_et_export_python_args} + ) + + set(_et_pte_file_path "${_et_generated_model_path}") + + if(NOT EXISTS "${_et_generated_model_path}") + message( + FATAL_ERROR + "Generated model file does not exist: ${_et_generated_model_path}" + ) + endif() + set(EXECUTORCH_ZEPHYR_AUTO_MODEL_OUTPUT + "${_et_generated_model_path}" + CACHE INTERNAL "Auto-generated ExecuTorch model path" FORCE + ) + endif() + endif() + + if(_et_pte_file_path AND NOT IS_ABSOLUTE "${_et_pte_file_path}") + if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/${_et_pte_file_path}") + get_filename_component( + _et_pte_file_path "${CMAKE_CURRENT_SOURCE_DIR}/${_et_pte_file_path}" + ABSOLUTE + ) + elseif(EXISTS "${_et_zephyr_workspace_dir}/${_et_pte_file_path}") + get_filename_component( + _et_pte_file_path "${_et_zephyr_workspace_dir}/${_et_pte_file_path}" + ABSOLUTE + ) + else() + get_filename_component( + _et_pte_file_path "${_et_pte_file_path}" ABSOLUTE BASE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}" + ) + endif() + endif() + + set(${out_var} + "${_et_pte_file_path}" + PARENT_SCOPE + ) +endfunction() + +# Convert a model to a .(b)pte file if CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT is +# set, otherwise rely on ET_PTE_FILE_PATH to be set by the user. Also check if +# the model contains any non-delegated ops and configure selective build state. +# +# The first output variable receives the resolved model path. A second output +# variable may be provided to receive whether any non-delegated ops were found. +function(executorch_zephyr_prepare_model_for_build out_path_var) + cmake_parse_arguments(ARG "REQUIRE_BPTE" "" "" ${ARGN}) + set(out_has_ops_var "${ARGV1}") + + executorch_zephyr_resolve_model(_et_pte_file_path) + + if(NOT _et_pte_file_path) + message( + FATAL_ERROR + "Set ET_PTE_FILE_PATH or CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT to provide an ExecuTorch model to embed." + ) + endif() + + if(NOT IS_ABSOLUTE "${_et_pte_file_path}") + message(FATAL_ERROR "ET_PTE_FILE_PATH must resolve to an absolute path") + endif() + + if(NOT EXISTS "${_et_pte_file_path}") + message( + FATAL_ERROR + "Could not find ExecuTorch model at ET_PTE_FILE_PATH: ${_et_pte_file_path}" + ) + endif() + + if(ARG_REQUIRE_BPTE AND NOT _et_pte_file_path MATCHES [[\.bpte$]]) + message( + FATAL_ERROR + "This sample requires a .bpte model, got: ${_et_pte_file_path}" + ) + endif() + + set(ET_PTE_FILE_PATH + "${_et_pte_file_path}" + CACHE FILEPATH "Path to the ExecuTorch .pte (or .bpte) model to embed" + FORCE + ) + + execute_process( + COMMAND + ${Python3_EXECUTABLE} "${EXECUTORCH_DIR}/codegen/tools/gen_oplist.py" + --model_file_path=${_et_pte_file_path} + --output_path=${CMAKE_CURRENT_BINARY_DIR}/temp.yaml + RESULT_VARIABLE _et_gen_oplist_status + OUTPUT_VARIABLE _et_gen_oplist_output + ERROR_VARIABLE _et_gen_oplist_error + ) + + if(NOT _et_gen_oplist_status EQUAL 0) + message( + FATAL_ERROR + "gen_oplist.py failed for model ${_et_pte_file_path}\nstdout:\n${_et_gen_oplist_output}\nstderr:\n${_et_gen_oplist_error}" + ) + endif() + + if(_et_gen_oplist_output MATCHES "aten::" OR _et_gen_oplist_output MATCHES + "dim_order_ops::" + ) + set(_et_found_ops_in_file TRUE) + else() + set(_et_found_ops_in_file FALSE) + endif() + + if(_et_found_ops_in_file) + set(EXECUTORCH_SELECT_OPS_LIST + "" + PARENT_SCOPE + ) + set(EXECUTORCH_SELECT_OPS_MODEL + "${_et_pte_file_path}" + CACHE STRING "Select operators from this ExecuTorch model" FORCE + ) + set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS + ON + PARENT_SCOPE + ) + message( + "gen_oplist: EXECUTORCH_SELECT_OPS_MODEL=${_et_pte_file_path} is used to auto generate ops from" + ) + else() + set(EXECUTORCH_SELECT_OPS_LIST + "" + PARENT_SCOPE + ) + set(EXECUTORCH_SELECT_OPS_MODEL + "" + CACHE STRING "Select operators from this ExecuTorch model" FORCE + ) + set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS + OFF + PARENT_SCOPE + ) + message( + "gen_oplist: No non delegated ops were found in ${_et_pte_file_path}; no portable ops added to build" + ) + endif() + + set(${out_path_var} + "${_et_pte_file_path}" + PARENT_SCOPE + ) + if(out_has_ops_var) + set(${out_has_ops_var} + "${_et_found_ops_in_file}" + PARENT_SCOPE + ) + endif() +endfunction() + +# Generate a model .h header from a .pte file and add it to the specified +# target's include path. +function(executorch_zephyr_add_model_header target_name model_path section) + if(NOT TARGET ${target_name}) + message(FATAL_ERROR "Target '${target_name}' does not exist") + endif() + + if(NOT IS_ABSOLUTE "${model_path}") + message( + FATAL_ERROR + "Model path passed to executorch_zephyr_add_model_header must be absolute: ${model_path}" + ) + endif() + + if(NOT EXISTS "${model_path}") + message(FATAL_ERROR "Model file does not exist: ${model_path}") + endif() + + set(_model_pte_header "${CMAKE_CURRENT_BINARY_DIR}/model_pte.h") + get_property( + _generated_model_target + DIRECTORY + PROPERTY EXECUTORCH_ZEPHYR_GENERATED_MODEL_TARGET + ) + get_property( + _generated_model_output + DIRECTORY + PROPERTY EXECUTORCH_ZEPHYR_GENERATED_MODEL_OUTPUT + ) + set(_model_header_depends + ${model_path} + ${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py + ) + if(_generated_model_target AND _generated_model_output STREQUAL + "${model_path}" + ) + list(APPEND _model_header_depends ${_generated_model_target}) + endif() + add_custom_command( + OUTPUT ${_model_pte_header} + COMMAND + ${Python3_EXECUTABLE} + ${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py --pte + ${model_path} --outdir ${CMAKE_CURRENT_BINARY_DIR} --section ${section} + DEPENDS ${_model_header_depends} + COMMENT "Converting ${model_path} to model_pte.h" + ) + + set(_model_header_target gen_model_header_${target_name}) + add_custom_target(${_model_header_target} DEPENDS ${_model_pte_header}) + add_dependencies(${target_name} ${_model_header_target}) + target_include_directories(${target_name} PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +endfunction() diff --git a/zephyr/Kconfig b/zephyr/Kconfig index ce977c93c67..c9c3bcfb830 100644 --- a/zephyr/Kconfig +++ b/zephyr/Kconfig @@ -1,7 +1,7 @@ # Copyright (c) 2025 Petri Oksanen # Copyright (c) Meta Platforms, Inc. and affiliates. # All rights reserved. -# Copyright 2025 Arm Limited and/or its affiliates. +# Copyright 2025-2026 Arm Limited and/or its affiliates. # # SPDX-License-Identifier: Apache-2.0 @@ -49,4 +49,34 @@ config EXECUTORCH_BUILD_PORTABLE_OPS operator building to include only needed operators, but the underlying kernel implementations will still be available. +config EXECUTORCH_EXPORT_PYTHON_SCRIPT + string "Python exporter script" + default "" + help + Optional Python script run during CMake configure to generate the + model file embedded by the sample or application. + +config EXECUTORCH_EXPORT_PYTHON_ARGS + string "Python exporter arguments" + default "" + help + Optional extra command-line arguments appended when invoking + CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT. Use this for exporter- + specific lowering flags such as backend or target selection. + +config EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES + string "Python exporter dependency files" + default "" + help + Optional space-separated list of files that should trigger CMake + reconfigure when changed. Use this for local Python helper modules or + other side files imported by CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT. + +config EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT + string "Python exporter generated output path" + default "" + help + Optional output path that Zephyr expects the exporter script to + write. Relative paths are resolved from the sample build directory. + endif # EXECUTORCH diff --git a/zephyr/README.md b/zephyr/README.md index 67fa680aa17..0dd09184f62 100644 --- a/zephyr/README.md +++ b/zephyr/README.md @@ -2,7 +2,9 @@ **ExecuTorch** is PyTorch's unified solution for deploying AI models on-device—from smartphones to microcontrollers—built for privacy, performance, and portability. It powers Meta's on-device AI across **Instagram, WhatsApp, Quest 3, Ray-Ban Meta Smart Glasses**, and [more](https://docs.pytorch.org/executorch/main/success-stories.html). -This folder adds ExecuTorch so it can be build and run in the Zephyr project as a external module. This includes an example under zephyr/samples/hello-executorch of running executor runners with Arm® Ethos™-U backend on a Corstone™ FVP, targeting the Zephyr RTOS. +This folder integrates ExecuTorch as a Zephyr module. It also contains sample +applications under `zephyr/samples/`, including +`zephyr/samples/hello-executorch` for embedding and running a `.pte` model. # Requirements @@ -34,11 +36,15 @@ export ZEPHYR_SDK_INSTALL_DIR= # Usage with Zephyr -To pull in ExecuTorch as a Zephyr module, either add it as a West project in the west.yaml file or pull it in by adding a submanifest (e.g. zephyr/submanifests/executorch.yaml) +To pull in ExecuTorch as a Zephyr module, either add it as a West project in +`west.yaml` or include it through a submanifest such as +`zephyr/submanifests/executorch.yaml`. # Create executorch.yaml under zephyr/submanifests -There is an example executorch.yaml in this folder you can copy or just create a new file /zephyr/submanifests/executorch.yaml with the following content: +There is an example `executorch.yaml` in this folder you can copy, or you can +create `/zephyr/submanifests/executorch.yaml` with the +following content: /zephyr/submanifests/executorch.yaml ``` @@ -52,7 +58,7 @@ manifest: ## Run west config and update: -Add ExecuTorch and Ethos-U driver to Zephyr +Add ExecuTorch and Ethos™-U driver to Zephyr ``` west config manifest.project-filter -- -.*,+zephyr,+executorch,+cmsis,+cmsis_6,+cmsis-nn,+hal_ethos_u @@ -71,9 +77,11 @@ git submodule update --init --recursive cd ../../.. ``` -## Prepare Ethos-U tools like Vela compiler and Corstone 300/320 FVP +## Prepare Ethos-U tools like Vela compiler and Corstone™ 300/320 FVP -This is needed to convert python models to PTE files for Ethos-Ux5 and also installs Corstone 300/320 FVP so you can run and test. +This installs the tools needed to export Python models to PTE files for +Ethos-Ux5 and also installs the Corstone 300/320 FVPs used by the sample +flows. Make sure to read and agree to the Corstone eula @@ -89,6 +97,25 @@ modules/lib/executorch/examples/arm/setup.sh --i-agree-to-the-contained-eula Build and run instructions for simple Zephyr minimal example setup is documented in [`zephyr/samples/hello-executorch/README.md`](samples/hello-executorch/README.md). +## Model export config + +Zephyr samples can auto-generate a `.pte` file during CMake configure by using +the `CONFIG_EXECUTORCH_EXPORT_PYTHON_*` Kconfig options in `prj.conf` or in a +board-specific `boards/*.conf` file. + +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT` selects the Python exporter script to + run. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_ARGS` passes extra arguments to that script. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES` lists local helper files that + should trigger reconfigure when changed. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT` tells Zephyr which output + file the script is expected to write. + +These settings can be overridden per board in `boards/.conf`, which is +useful when different targets need different exporter scripts or generated model +file names. The `hello-executorch` sample uses this to select different default +exporters for Corstone-300 and Corstone-320. + ## Notable files # executorch.yaml @@ -97,7 +124,10 @@ Copy this to /zephyr/submanifests/ # module.yml -Do not remove this file. As mentioned in the official Zephyr [documenation](https://docs.zephyrproject.org/latest/develop/modules.html), for Executorch to be built as Zephyr module, the file `zephyr/module.yml` must exist at the top level directory in the project. +Do not remove this file. As described in the official Zephyr +[documentation](https://docs.zephyrproject.org/latest/develop/modules.html), +`zephyr/module.yml` must exist at the top level of the project for ExecuTorch +to be discovered as a Zephyr module. # Reference diff --git a/zephyr/samples/hello-executorch/CMakeLists.txt b/zephyr/samples/hello-executorch/CMakeLists.txt index ca266ead811..20936c93479 100644 --- a/zephyr/samples/hello-executorch/CMakeLists.txt +++ b/zephyr/samples/hello-executorch/CMakeLists.txt @@ -21,70 +21,10 @@ set(ET_PTE_FILE_PATH CACHE FILEPATH "Path to the ExecuTorch .pte (or .bpte) model to embed" ) set(ET_PTE_SECTION - "network_model_sec" + ".rodata" CACHE STRING "Section attribute used for the generated model data" ) -if(NOT ET_PTE_FILE_PATH) - message( - FATAL_ERROR - "ET_PTE_FILE_PATH must point to the ExecuTorch .pte (or .bpte) model to embed." - ) -endif() - -if(NOT IS_ABSOLUTE "${ET_PTE_FILE_PATH}") - get_filename_component( - ET_PTE_FILE_PATH "${ET_PTE_FILE_PATH}" ABSOLUTE BASE_DIR - "${CMAKE_CURRENT_SOURCE_DIR}" - ) -endif() - -if(NOT EXISTS "${ET_PTE_FILE_PATH}") - message( - FATAL_ERROR - "Could not find ExecuTorch model at ET_PTE_FILE_PATH: ${ET_PTE_FILE_PATH}" - ) -endif() - -set(ET_PTE_FILE_PATH - "${ET_PTE_FILE_PATH}" - CACHE FILEPATH "Path to the ExecuTorch .pte (or .bpte) model to embed" - FORCE -) - -execute_process( - COMMAND - python "${CMAKE_CURRENT_LIST_DIR}/../../../codegen/tools/gen_oplist.py" - --model_file_path=${ET_PTE_FILE_PATH} - --output_path=${CMAKE_CURRENT_BINARY_DIR}/temp.yaml - OUTPUT_VARIABLE CMD_RESULT -) - -if(CMD_RESULT MATCHES "aten::" OR CMD_RESULT MATCHES "dim_order_ops::") - set(FOUND_OPS_IN_FILE "true") -else() - set(FOUND_OPS_IN_FILE "false") -endif() - -if(${FOUND_OPS_IN_FILE}) - set(EXECUTORCH_SELECT_OPS_LIST "") - set(EXECUTORCH_SELECT_OPS_MODEL - "${ET_PTE_FILE_PATH}" - CACHE STRING "Select operators from this ExecuTorch model" FORCE - ) - set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS ON) - message( - "gen_oplist: EXECUTORCH_SELECT_OPS_MODEL=${ET_PTE_FILE_PATH} is used to auto generate ops from" - ) -else() - set(EXECUTORCH_SELECT_OPS_LIST "") - set(EXECUTORCH_SELECT_OPS_MODEL "") - set(_EXECUTORCH_GEN_ZEPHYR_PORTABLE_OPS OFF) - message( - "gen_oplist: No non delagated ops was found in ${ET_PTE_FILE_PATH} no ops added to build" - ) -endif() - find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(executorch_executor_runner) @@ -122,6 +62,8 @@ else() message(STATUS "Using predefined EXECUTORCH_DIR=${EXECUTORCH_DIR}") endif() +executorch_zephyr_prepare_model_for_build(ET_PTE_FILE_PATH) + # Set EXECUTORCH_ROOT for the Codegen.cmake file set(EXECUTORCH_ROOT ${EXECUTORCH_DIR}) include(${EXECUTORCH_DIR}/tools/cmake/Utils.cmake) @@ -197,20 +139,7 @@ set(app_sources ) target_sources(app PRIVATE ${app_sources}) -set(_model_pte_header ${CMAKE_CURRENT_BINARY_DIR}/model_pte.h) -add_custom_command( - OUTPUT ${_model_pte_header} - COMMAND - ${Python3_EXECUTABLE} - ${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py --pte - ${ET_PTE_FILE_PATH} --outdir ${CMAKE_CURRENT_BINARY_DIR} --section - ${ET_PTE_SECTION} - DEPENDS ${ET_PTE_FILE_PATH} - ${EXECUTORCH_DIR}/examples/arm/executor_runner/pte_to_header.py - COMMENT "Converting ${ET_PTE_FILE_PATH} to model_pte.h" -) -add_custom_target(gen_model_header DEPENDS ${_model_pte_header}) -add_dependencies(app gen_model_header) +executorch_zephyr_add_model_header(app ${ET_PTE_FILE_PATH} ${ET_PTE_SECTION}) if(DEFINED CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE) target_compile_definitions( @@ -258,6 +187,6 @@ if(TARGET ethosu_core_driver) endif() # Add include directories for sources and generated headers -target_include_directories(app PRIVATE src ${CMAKE_CURRENT_BINARY_DIR}) +target_include_directories(app PRIVATE src) get_target_property(OUT app LINK_LIBRARIES) message(STATUS ${OUT}) diff --git a/zephyr/samples/hello-executorch/README.md b/zephyr/samples/hello-executorch/README.md index 16303303031..443d0f30a0c 100644 --- a/zephyr/samples/hello-executorch/README.md +++ b/zephyr/samples/hello-executorch/README.md @@ -1,14 +1,51 @@ # ExecuTorch Zephyr Samples -This document contains build and run instructions for the examples under -`zephyr/samples/`. +This sample uses a tiny `x + x` model to show the minimum pieces needed to run +an ExecuTorch model in a Zephyr application. + +The model is exported from Python during CMake configure. This sample keeps a +few different exporter scripts under `models/` so it can demonstrate Cortex-M, +Ethos™-U55, and Ethos™-U85 flows. In a real application you would usually keep +just the exporter you need. + +The auto-export path is controlled by these Kconfig options: + +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT` selects the exporter script. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT` names the `.pte` file that + script writes. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_ARGS` can be used to pass extra arguments to + the exporter script. +- `CONFIG_EXECUTORCH_EXPORT_PYTHON_DEPENDENCIES` lists helper files that should + trigger reconfigure when changed. + +The sample sets a default Cortex-M exporter in `prj.conf` and overrides it in +board-specific `boards/*.conf` files for the Corstone™ FVP and Ethos-U boards. + +You can also bypass auto-export entirely and point the build at a prebuilt model +with `-DET_PTE_FILE_PATH=.pte`. + +If you override `CONFIG_EXECUTORCH_EXPORT_PYTHON_*` from the `west build` +command line, remember that they are Kconfig string symbols, so the value must +include embedded double quotes. + +For example, to force the Cortex-M exporter on `mps3/corstone300/fvp`: + +``` +west build -d build-hello-executorch_cortex-m55 -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- '-DCONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT="models/add_cortex-m.py"' '-DCONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT="hello_executorch_cortex-m.pte"' +``` + ## Running a sample application -To run you need to point to the path to the installed Corstone™ FVP for Ethos™-U55/U85 and you can then use west to build and run. You point out the model PTE file you want to run with `-DET_PTE_FILE_PATH=` (see below). +To run the Ethos-U55/U85 sample flows you need to add the directory of the +installed Corstone FVP. The default flow auto-generates the model from +`prj.conf` and any matching board config. For testing you can also use +`-DET_PTE_FILE_PATH=` to point to a prebuilt model PTE file instead. The magic to include and use Ethos-U backend is to set `CONFIG_ETHOS_U=y/n`. -This is done in the example depending on the board you build for so if you build for a different board then the ones below you might want to add a board config file, or add this line to the `prj.conf`. +This is set automatically for the Ethos-U sample boards in `boards/*.conf`. If +you build for a different board, add a matching board config file or put the +setting directly in `prj.conf`. ## Corstone 300 FVP (Ethos-U55) @@ -26,51 +63,66 @@ export ARMFVP_EXTRA_FLAGS="-C mps3_board.uart0.shutdown_on_eot=1 -C ethosu.num_m ### Ethos-U55 +#### Build and run + +Run the Ethos-U55 exporter configured by the project + +``` +west build -d build-hello-executorch_ethos-u55 -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run +``` + #### Prepare a PTE model file -Prepare the Ethos-U55 PTE model +To use a prebuilt model instead of the auto-generated one: + +Prepare and run a separate Ethos-U55 PTE model ``` -python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add.py --quantize --delegate --target=ethos-u55-128 --output=add_u55_128.pte +python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add_ethos-u55.py --quantize --delegate --target=ethos-u55-128 --output=add_u55_128.pte +west build -d build-hello-executorch_ethos-u55 -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_u55_128.pte ``` `--delegate` tells the `aot_arm_compiler` to use Ethos-U backend and `-t ethos-u55-128` specifies the used Ethos-U variant and numbers of macs used, this must match your hardware or FVP config. +### Cortex-M55 (Corstone 300 FVP) + +This sample reuses the Corstone-300 FVP for both Ethos-U55 and Cortex-M55 +examples. Because the board config defaults to the Ethos-U55 exporter, the +Cortex-M55 example overrides the exporter on the command line. + #### Build and run -Run the Ethos-U55 PTE model - +Run the Cortex-M55 exporter + ``` -west build -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_u55_128.pte +west build -d build-hello-executorch_cortex-m55 -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- '-DCONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT="models/add_cortex-m.py"' '-DCONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT="hello_executorch_cortex-m.pte"' ``` -### Cortex-M55 - #### Prepare a PTE model file -Prepare the Cortex-M55 PTE model +To use a prebuilt model instead of the auto-generated one: + +Prepare and run the Cortex-M55 PTE model ``` -python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add.py --quantize --target=cortex-m55+int8 --output=add_m55.pte +python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add_cortex-m.py --quantize --target=cortex-m55+int8 --output=add_m55.pte +west build -d build-hello-executorch_cortex-m55 -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_m55.pte ``` `--target=cortex-m55+int8` selects the Cortex-M/CMSIS-NN portable kernel path (no NPU delegation). This produces a `.pte` optimized for Cortex-M55 with INT8 quantization. -#### Build and run - -Run the Cortex-M55 PTE model - -``` -west build -b mps3/corstone300/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_m55.pte -``` - ## Corstone 320 FVP (Ethos-U85) ### Setup FVP paths Set up FVP paths, libs and macs used, this will also set `shutdown_on_eot` so the FVP auto stops after it has run the example. +These FVP command-line options are passed through the `ARMFVP_EXTRA_FLAGS` +environment variable. The sample does not set `ARMFVP_FLAGS` in its +`CMakeLists.txt`; the base `ARMFVP_FLAGS` come from the selected Zephyr board's +`board.cmake`. + Config Zephyr Corstone320 FVP ``` @@ -82,24 +134,28 @@ export ARMFVP_EXTRA_FLAGS="-C mps4_board.uart0.shutdown_on_eot=1 -C mps4_board.s ### Ethos-U85 -#### Prepare a PTE model file +#### Build and run -Prepare the Ethos-U85 PTE model - +Run the Ethos-U85 exporter configured by the project + ``` -python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add.py --quantize --delegate --target=ethos-u85-256 --output=add_u85_256.pte +west build -d build-hello-executorch_ethos-u85 -b mps4/corstone320/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run ``` -`--delegate` tells the `aot_arm_compiler` to use Ethos-U backend and `-t ethos-u85-256` specifies the used Ethos-U variant and numbers of macs used, this must match your hardware or FVP config. +#### Prepare a PTE model file -#### Build and run +To use a prebuilt model instead of the auto-generated one: -Run the Ethos-U85 PTE model - +Prepare and run a separate Ethos-U85 PTE model + ``` -west build -b mps4/corstone320/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_u85_256.pte +python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add_ethos-u85.py --quantize --delegate --target=ethos-u85-256 --output=add_u85_256.pte +west build -d build-hello-executorch_ethos-u85 -b mps4/corstone320/fvp modules/lib/executorch/zephyr/samples/hello-executorch -t run -- -DET_PTE_FILE_PATH=add_u85_256.pte ``` +`--delegate` tells the `aot_arm_compiler` to use Ethos-U backend and `-t ethos-u85-256` specifies the used Ethos-U variant and numbers of macs used, this must match your hardware or FVP config. + + ## STM Nucleo n657x0_q ### Run west config and update @@ -125,21 +181,13 @@ Also note that the signing tool must be in your path for it to auto sign your el export PATH=$PATH:~/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin ``` -### Prepare a PTE model file - -Prepare the Cortex-M55 PTE model -``` -python -m modules.lib.executorch.backends.arm.scripts.aot_arm_compiler --model_name=modules/lib/executorch/zephyr/samples/hello-executorch/models/add.py --quantize --target=cortex-m55+int8 --output=add_m55.pte -``` - -`--target=cortex-m55+int8` selects the Cortex-M/CMSIS-NN portable kernel path (no NPU delegation). This produces a `.pte` optimized for Cortex-M55 with INT8 quantization. - #### Build and run -Run the Cortex-M55 PTE model +Run the Cortex-M55 sample on the board ``` -west build -b nucleo_n657x0_q modules/lib/executorch/zephyr/samples/hello-executorch -- -DET_PTE_FILE_PATH=add_m55.pte +west build -d build-hello-executorch_nucleo_n6 -b nucleo_n657x0_q modules/lib/executorch/zephyr/samples/hello-executorch west flash ``` -This will run the simple add model on your hardware one and print the output on the serial console. +This will run the simple add model on the board and print the output on the +serial console. diff --git a/zephyr/samples/hello-executorch/boards/mps3_corstone300_fvp.conf b/zephyr/samples/hello-executorch/boards/mps3_corstone300_fvp.conf index bee885286da..23ee689ec8c 100644 --- a/zephyr/samples/hello-executorch/boards/mps3_corstone300_fvp.conf +++ b/zephyr/samples/hello-executorch/boards/mps3_corstone300_fvp.conf @@ -7,3 +7,5 @@ # and use the hardware instance exposed by the board DTS # as the board has one available. CONFIG_ETHOS_U=y +CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT="models/add_ethos-u55.py" +CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT="hello_executorch_ethos-u55.pte" \ No newline at end of file diff --git a/zephyr/samples/hello-executorch/boards/mps4_corstone320_fvp.conf b/zephyr/samples/hello-executorch/boards/mps4_corstone320_fvp.conf index bee885286da..7a382c1e993 100644 --- a/zephyr/samples/hello-executorch/boards/mps4_corstone320_fvp.conf +++ b/zephyr/samples/hello-executorch/boards/mps4_corstone320_fvp.conf @@ -7,3 +7,5 @@ # and use the hardware instance exposed by the board DTS # as the board has one available. CONFIG_ETHOS_U=y +CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT="models/add_ethos-u85.py" +CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT="hello_executorch_ethos-u85.pte" diff --git a/zephyr/samples/hello-executorch/models/add.py b/zephyr/samples/hello-executorch/models/add.py deleted file mode 100644 index aa01b889504..00000000000 --- a/zephyr/samples/hello-executorch/models/add.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright 2026 Arm Limited and/or its affiliates. -# -# This source code is licensed under the BSD-style license found in the -# LICENSE file in the root directory of this source tree. -# -# Example of an external model for the Arm AOT Compiler -# -# Example of an external Python file to be used as a module by the `run.sh` -# (and the `backends/arm/scripts/aot_arm_compiler.py`) script. -# -# Just pass the path of the `add.py` file as `--model_name` -# -# These two variables are picked up by the `aot_arm_compiler.py` and used: -# `ModelUnderTest` should be a `torch.nn.module` instance. -# `ModelInputs` should be a tuple of inputs to the forward function. -# - -import torch - - -class myModelAdd(torch.nn.Module): - def __init__(self): - super().__init__() - - def forward(self, x): - return x + x - - -ModelUnderTest = myModelAdd() -ModelInputs = (torch.ones(5),) diff --git a/zephyr/samples/hello-executorch/models/add_cortex-m.py b/zephyr/samples/hello-executorch/models/add_cortex-m.py new file mode 100644 index 00000000000..c3c9fe6e524 --- /dev/null +++ b/zephyr/samples/hello-executorch/models/add_cortex-m.py @@ -0,0 +1,95 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# This file shows one way to export a small model to a PTE during the Zephyr +# build by pointing CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT at this script. +# +# This variant lowers the model for the Cortex-M backend. See the sibling files +# in this directory for the Ethos-U variants. + +OUTPUT_PTE = "hello_executorch_cortex-m.pte" + +import torch + +from executorch.backends.cortex_m.passes.cortex_m_pass_manager import CortexMPassManager + +from executorch.backends.cortex_m.quantizer.quantizer import CortexMQuantizer +from executorch.exir import ( + EdgeCompileConfig, + ExecutorchBackendConfig, + to_edge_transform_and_lower, +) +from executorch.extension.export_util.utils import save_pte_program +from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e + + +# Model definition. This is intentionally a tiny add model to keep the export +# example focused on the Zephyr integration. +class myModelAdd(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x + x + + +# Define the model and example inputs used by the exporter code below. + +ModelUnderTest = myModelAdd() +ModelInputs = (torch.ones(5),) + +# Export the model to a `.pte` file for Cortex-M. + + +def _to_channels_last(value): + if isinstance(value, torch.Tensor) and value.dim() == 4: + return value.to(memory_format=torch.channels_last) + if isinstance(value, tuple): + return tuple(_to_channels_last(item) for item in value) + return value + + +def _export_cortex_m(pte_file): + model = ModelUnderTest.eval() + example_inputs = tuple(_to_channels_last(value) for value in ModelInputs) + if any( + isinstance(value, torch.Tensor) and value.dim() == 4 for value in example_inputs + ): + model = model.to(memory_format=torch.channels_last) + exported_model = torch.export.export(model, example_inputs, strict=True).module() + + quantizer = CortexMQuantizer() + prepared = prepare_pt2e(exported_model, quantizer) + prepared(*example_inputs) + quantized_model = convert_pt2e(prepared) + exported_program = torch.export.export(quantized_model, example_inputs, strict=True) + + edge_program = to_edge_transform_and_lower( + exported_program, + compile_config=EdgeCompileConfig( + preserve_ops=[ + torch.ops.aten.linear.default, + torch.ops.aten.hardsigmoid.default, + torch.ops.aten.hardsigmoid_.default, + torch.ops.aten.hardswish.default, + torch.ops.aten.hardswish_.default, + ], + _check_ir_validity=False, + ), + ) + pass_manager = CortexMPassManager(edge_program.exported_program()) + edge_program._edge_programs["forward"] = pass_manager.transform() + executorch_program = edge_program.to_executorch( + config=ExecutorchBackendConfig(extract_delegate_segments=False) + ) + save_pte_program(executorch_program, pte_file) + + +def main(): + _export_cortex_m(OUTPUT_PTE) + + +if __name__ == "__main__": + main() diff --git a/zephyr/samples/hello-executorch/models/add_ethos-u55.py b/zephyr/samples/hello-executorch/models/add_ethos-u55.py new file mode 100644 index 00000000000..d2b088a8490 --- /dev/null +++ b/zephyr/samples/hello-executorch/models/add_ethos-u55.py @@ -0,0 +1,82 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# This file shows one way to export a small model to a PTE during the Zephyr +# build by pointing CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT at this script. +# +# This variant lowers the model for Ethos-U55. See the sibling files in this +# directory for the Cortex-M and Ethos-U85 variants. + +OUTPUT_PTE = "hello_executorch_ethos-u55.pte" + +import torch + +from executorch.backends.arm.ethosu import EthosUCompileSpec, EthosUPartitioner +from executorch.backends.arm.quantizer import ( + EthosUQuantizer, + get_symmetric_quantization_config, +) +from executorch.backends.cortex_m.passes.replace_quant_nodes_pass import ( + ReplaceQuantNodesPass, +) +from executorch.exir import ( + EdgeCompileConfig, + ExecutorchBackendConfig, + to_edge_transform_and_lower, +) +from executorch.extension.export_util.utils import save_pte_program +from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e + + +# Model definition. This is intentionally a tiny add model to keep the export +# example focused on the Zephyr integration. +class myModelAdd(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x + x + + +# Define the model and example inputs used by the exporter code below. + +ModelUnderTest = myModelAdd() +ModelInputs = (torch.ones(5),) + +# Export the model to a `.pte` file for Ethos-U55. + + +def _export_ethosu(target, pte_file): + compile_spec = EthosUCompileSpec(target=target) + + model = ModelUnderTest + example_inputs = ModelInputs + exported_model = torch.export.export(model, example_inputs, strict=True).module() + + quantizer = EthosUQuantizer(compile_spec) + quantizer.set_global(get_symmetric_quantization_config()) + prepared = prepare_pt2e(exported_model, quantizer) + prepared(*example_inputs) + quantized_model = convert_pt2e(prepared) + exported_program = torch.export.export(quantized_model, example_inputs, strict=True) + + edge_program = to_edge_transform_and_lower( + exported_program, + partitioner=[EthosUPartitioner(compile_spec)], + compile_config=EdgeCompileConfig(_check_ir_validity=False), + ) + edge_program = edge_program.transform([ReplaceQuantNodesPass()]) + executorch_program = edge_program.to_executorch( + config=ExecutorchBackendConfig(extract_delegate_segments=False) + ) + save_pte_program(executorch_program, pte_file) + + +def main(): + _export_ethosu("ethos-u55-128", OUTPUT_PTE) + + +if __name__ == "__main__": + main() diff --git a/zephyr/samples/hello-executorch/models/add_ethos-u85.py b/zephyr/samples/hello-executorch/models/add_ethos-u85.py new file mode 100644 index 00000000000..1bdd9f9347f --- /dev/null +++ b/zephyr/samples/hello-executorch/models/add_ethos-u85.py @@ -0,0 +1,82 @@ +# Copyright 2026 Arm Limited and/or its affiliates. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# This file shows one way to export a small model to a PTE during the Zephyr +# build by pointing CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT at this script. +# +# This variant lowers the model for Ethos-U85. See the sibling files in this +# directory for the Cortex-M and Ethos-U55 variants. + +OUTPUT_PTE = "hello_executorch_ethos-u85.pte" + +import torch + +from executorch.backends.arm.ethosu import EthosUCompileSpec, EthosUPartitioner +from executorch.backends.arm.quantizer import ( + EthosUQuantizer, + get_symmetric_quantization_config, +) +from executorch.backends.cortex_m.passes.replace_quant_nodes_pass import ( + ReplaceQuantNodesPass, +) +from executorch.exir import ( + EdgeCompileConfig, + ExecutorchBackendConfig, + to_edge_transform_and_lower, +) +from executorch.extension.export_util.utils import save_pte_program +from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e + + +# Model definition. This is intentionally a tiny add model to keep the export +# example focused on the Zephyr integration. +class myModelAdd(torch.nn.Module): + def __init__(self): + super().__init__() + + def forward(self, x): + return x + x + + +# Define the model and example inputs used by the exporter code below. + +ModelUnderTest = myModelAdd() +ModelInputs = (torch.ones(5),) + +# Export the model to a `.pte` file for Ethos-U85. + + +def _export_ethosu(target, pte_file): + compile_spec = EthosUCompileSpec(target=target) + + model = ModelUnderTest + example_inputs = ModelInputs + exported_model = torch.export.export(model, example_inputs, strict=True).module() + + quantizer = EthosUQuantizer(compile_spec) + quantizer.set_global(get_symmetric_quantization_config()) + prepared = prepare_pt2e(exported_model, quantizer) + prepared(*example_inputs) + quantized_model = convert_pt2e(prepared) + exported_program = torch.export.export(quantized_model, example_inputs, strict=True) + + edge_program = to_edge_transform_and_lower( + exported_program, + partitioner=[EthosUPartitioner(compile_spec)], + compile_config=EdgeCompileConfig(_check_ir_validity=False), + ) + edge_program = edge_program.transform([ReplaceQuantNodesPass()]) + executorch_program = edge_program.to_executorch( + config=ExecutorchBackendConfig(extract_delegate_segments=False) + ) + save_pte_program(executorch_program, pte_file) + + +def main(): + _export_ethosu("ethos-u85-256", OUTPUT_PTE) + + +if __name__ == "__main__": + main() diff --git a/zephyr/samples/hello-executorch/prj.conf b/zephyr/samples/hello-executorch/prj.conf index a70f2987a03..7401f8cf651 100644 --- a/zephyr/samples/hello-executorch/prj.conf +++ b/zephyr/samples/hello-executorch/prj.conf @@ -36,7 +36,16 @@ CONFIG_EXECUTORCH_BUILD_PORTABLE_OPS=n CONFIG_FPU=y CONFIG_FP_HARDABI=y -# Add model specific configs +# Model export configuration. The exporter script owns lowering and writes the +# generated .pte file selected by the build. +# +# This sample uses a Cortex-M exporter by default and overrides it in some +# board-specific files. For example, Ethos-U55 uses +# boards/mps3_corstone300_fvp.conf. +# +# In your own project you would normally keep only the exporter you need. +CONFIG_EXECUTORCH_EXPORT_PYTHON_SCRIPT="models/add_cortex-m.py" +CONFIG_EXECUTORCH_EXPORT_PYTHON_GENERATED_OUTPUT="hello_executorch_cortex-m.pte" # Setup memory requirements to scale with the compiled model/network. CONFIG_EXECUTORCH_METHOD_ALLOCATOR_POOL_SIZE=2000 diff --git a/zephyr/samples/hello-executorch/sample.yaml b/zephyr/samples/hello-executorch/sample.yaml new file mode 100644 index 00000000000..d961705cbef --- /dev/null +++ b/zephyr/samples/hello-executorch/sample.yaml @@ -0,0 +1,11 @@ +sample: + name: ExecuTorch Hello + description: Minimal ExecuTorch sample using a tiny x + x model on Zephyr +common: + tags: + - executorch + - ml + - cortex-m + - ethos-u + depends_on: + - executorch diff --git a/zephyr/samples/hello-executorch/src/arm_executor_runner.cpp b/zephyr/samples/hello-executorch/src/arm_executor_runner.cpp index 0a8cec1d158..01bb795d6a3 100644 --- a/zephyr/samples/hello-executorch/src/arm_executor_runner.cpp +++ b/zephyr/samples/hello-executorch/src/arm_executor_runner.cpp @@ -61,10 +61,8 @@ using executorch::runtime::TensorInfo; /** * The method_allocation_pool should be large enough to fit the setup, input * used and other data used like the planned memory pool (e.g. memory-planned - * buffers to use for mutable tensor data) In this example we run on a - * Corstone-3xx FVP so we can use a lot of memory to be able to run and test - * large models if you run on HW this should be lowered to fit into your - * availible memory. + * buffers to use for mutable tensor data) This should be lowered to fit into + * your available memory. */ #if !defined(ET_ARM_METHOD_ALLOCATOR_POOL_SIZE) @@ -267,6 +265,12 @@ int main(int argc, const char* argv[]) { "Setup Method allocator pool. Size: %zu bytes.", method_allocation_pool_size); + /** ArmMemoryAllocator is just a subclass of + * executorch::runtime::MemoryAllocator that adds some info about usage that + * is used for the logs in the end. You can use the MemoryAllocator interface + * directly if you don't need that extra info and want to save a few bytes. + */ + ArmMemoryAllocator method_allocator( method_allocation_pool_size, method_allocation_pool); @@ -281,7 +285,6 @@ int main(int argc, const char* argv[]) { static_cast(method_meta->memory_planned_buffer_size(id).get()); ET_LOG(Info, "Setting up planned buffer %zu, size %zu.", id, buffer_size); - /* Move to it's own allocator when MemoryPlanner is in place. */ uint8_t* buffer = reinterpret_cast(method_allocator.allocate(buffer_size)); ET_CHECK_MSG( @@ -428,61 +431,63 @@ int main(int argc, const char* argv[]) { if (status != Error::Ok) { ET_LOG( - Info, + Error, "Execution of method %s failed with status 0x%" PRIx32, method_name, static_cast(status)); + ET_LOG(Error, "ERROR \04"); + return -1; } else { ET_LOG(Info, "Model executed successfully."); - } - - std::vector outputs(method->outputs_size()); - status = method->get_outputs(outputs.data(), outputs.size()); - ET_CHECK(status == Error::Ok); + std::vector outputs(method->outputs_size()); + status = method->get_outputs(outputs.data(), outputs.size()); + ET_CHECK(status == Error::Ok); - ET_LOG(Info, "Model outputs:"); - for (size_t i = 0; i < outputs.size(); ++i) { - if (!outputs[i].isTensor()) { - ET_LOG(Info, " output[%zu]: non-tensor value", i); - continue; - } - Tensor tensor = outputs[i].toTensor(); - ET_LOG( - Info, - " output[%zu]: tensor scalar_type=%s numel=%zd", - i, - executorch::runtime::toString(tensor.scalar_type()), - tensor.numel()); - switch (tensor.scalar_type()) { - case ScalarType::Int: { - const int* data = tensor.const_data_ptr(); - for (ssize_t j = 0; j < tensor.numel(); ++j) { - ET_LOG(Info, " [%zd] = %d", j, data[j]); - } - break; + ET_LOG(Info, "Model outputs:"); + for (size_t i = 0; i < outputs.size(); ++i) { + if (!outputs[i].isTensor()) { + ET_LOG(Info, " output[%zu]: non-tensor value", i); + continue; } - case ScalarType::Float: { - const float* data = tensor.const_data_ptr(); - for (ssize_t j = 0; j < tensor.numel(); ++j) { - ET_LOG(Info, " [%zd] = %f", j, static_cast(data[j])); + Tensor tensor = outputs[i].toTensor(); + ET_LOG( + Info, + " output[%zu]: tensor scalar_type=%s numel=%zd", + i, + executorch::runtime::toString(tensor.scalar_type()), + tensor.numel()); + switch (tensor.scalar_type()) { + case ScalarType::Int: { + const int* data = tensor.const_data_ptr(); + for (ssize_t j = 0; j < tensor.numel(); ++j) { + ET_LOG(Info, " [%zd] = %d", j, data[j]); + } + break; } - break; - } - case ScalarType::Char: { - const int8_t* data = tensor.const_data_ptr(); - for (ssize_t j = 0; j < tensor.numel(); ++j) { - ET_LOG(Info, " [%zd] = %d", j, data[j]); + case ScalarType::Float: { + const float* data = tensor.const_data_ptr(); + for (ssize_t j = 0; j < tensor.numel(); ++j) { + ET_LOG(Info, " [%zd] = %f", j, static_cast(data[j])); + } + break; } - break; + case ScalarType::Char: { + const int8_t* data = tensor.const_data_ptr(); + for (ssize_t j = 0; j < tensor.numel(); ++j) { + ET_LOG(Info, " [%zd] = %d", j, data[j]); + } + break; + } + default: + ET_LOG( + Info, + " (%s tensor dump skipped)", + executorch::runtime::toString(tensor.scalar_type())); } - default: - ET_LOG( - Info, - " (%s tensor dump skipped)", - executorch::runtime::toString(tensor.scalar_type())); } + ET_LOG(Info, "SUCCESS: Program complete, exiting."); } - ET_LOG(Info, "SUCCESS: Program complete, exiting."); + ET_LOG(Info, "\04"); return 0; }