From e6c76666b2e81f768d97c08ddf4699c019737000 Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Tue, 8 Jul 2025 15:14:13 -0700 Subject: [PATCH 1/6] README and CMake changes --- CMakeLists.txt | 10 ++++++ examples/wasm/README.md | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 examples/wasm/README.md diff --git a/CMakeLists.txt b/CMakeLists.txt index d3c81fd1b38..3f6133af74f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -726,6 +726,16 @@ if(EXECUTORCH_BUILD_EXECUTOR_RUNNER) endif() target_link_libraries(executor_runner ${_executor_runner_libs}) target_compile_options(executor_runner PUBLIC ${_common_compile_options}) + + if(EMSCRIPTEN) + # Directory of model pte files to embed in the wasm binary. + if(NOT DEFINED WASM_MODEL_DIR) + set(WASM_MODEL_DIR "${CMAKE_SOURCE_DIR}/models/") + endif() + + set(CMAKE_EXECUTABLE_SUFFIX ".html") + target_link_options(executor_runner PUBLIC -sALLOW_MEMORY_GROWTH --embed-file "${WASM_MODEL_DIR}@/") + endif() endif() if(EXECUTORCH_BUILD_VULKAN) diff --git a/examples/wasm/README.md b/examples/wasm/README.md new file mode 100644 index 00000000000..ecc65896920 --- /dev/null +++ b/examples/wasm/README.md @@ -0,0 +1,72 @@ +# ExecuTorch Wasm Build + +This guide describes how to build ExecuTorch for WebAssembly (Wasm). + +## Directory Structure + +``` +examples/wasm +└── README.md # This file +``` + +## Prerequisites + +- [emscripten](https://emscripten.org/docs/getting_started/Tutorial.html) +- [Node.js](https://nodejs.org/en/) (Optional) + +## Generate Models + +JavaScript does not have access to the filesystem. To load a model, it needs to be preloaded or embedded into the virtual filesystem. In this example, models in the `./models/` directory are embedded by default. We will then build `executorch_runner` in Wasm. + +1. Following the setup guide in [Setting up ExecuTorch](https://pytorch.org/executorch/main/getting-started-setup) +you should be able to get the basic development environment for ExecuTorch working. + +2. Using the script `portable/scripts/export.py` generate a model binary file by selecting a +model name from the list of available models in the `examples/models` dir. + +```bash +cd executorch # To the top level dir + +mkdir models + +# To get a list of example models +python3 -m examples.portable.script.export -h + +# To generate a specific pte model into the models/ directory +python3 -m examples.portable.scripts.export --model_name="mv2" --output_dir="models/" # for MobileNetv2 + +# This should generate ./models/mv2.pte file, if successful. +``` + +Use -h (or --help) to see all the supported models. For the browser example, make sure you have a model with the file name `model.pte` in the `./models/` directory. + +3. Once we have the model binaries (.pte) in `./models/`, we can build `executor_runner` in Wasm with Emscripten. When calling `emcmake cmake`, you can pass the `-DWASM_MODEL_DIR=` option to specify the directory containing the model files instead of `./models/`. + +```bash +./install_executorch.sh --clean +(mkdir cmake-out-wasm \ + && cd cmake-out-wasm \ + && emcmake cmake -DEXECUTORCH_PAL_DEFAULT=posix ..) \ + && cmake --build cmake-out-wasm -j32 --target executor_runner +``` + +If you need to rebuild `executor_runner` after modifying the contents of `./models/`, you can run the following command + +```bash +cmake --build cmake-out-wasm -j32 --target executor_runner --clean-first +``` + +4. Run the model with Node.js. + +```bash +# Run the tool on the generated model. +node cmake-out-wasm/executor_runner.js --model_path mv2.pte +``` + +5. You can also run the model in the browser. Note that you cannot pass command line arguments to the browser version of the tool. By default, the program will load the model `model.pte` and run it. Several browsers do not support `file://` XHR requests to load the Wasm file. To get around this, you can use a local web server. For example, with Python: + +```bash +python3 -m http.server --directory cmake-out-wasm +``` + +The page will be available at http://localhost:8000/executor_runner.html. From b322cb84e4cf45c61c83560f03d02a1180ea3c83 Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Tue, 8 Jul 2025 17:21:20 -0700 Subject: [PATCH 2/6] Added build test script for wasm example --- examples/wasm/test_build_wasm.sh | 37 ++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/wasm/test_build_wasm.sh diff --git a/examples/wasm/test_build_wasm.sh b/examples/wasm/test_build_wasm.sh new file mode 100644 index 00000000000..87ef91ce4b2 --- /dev/null +++ b/examples/wasm/test_build_wasm.sh @@ -0,0 +1,37 @@ + +set -e + +source "$(dirname "${BASH_SOURCE[0]}")/../../.ci/scripts/utils.sh" + +test_build_wasm() { + local model_name=$1 + local model_export_name="${model_name}.pte" + local model_dir_name="./models_test/" + echo "Exporting ${model_name}" + mkdir -p "${model_dir_name}" + ${PYTHON_EXECUTABLE} -m examples.portable.scripts.export --model_name="${model_name}" --output_dir="$model_dir_name" + + local example_dir=examples/wasm + local build_dir=cmake-out/${example_dir} + rm -rf ${build_dir} + retry emcmake cmake -DWASM_MODEL_DIR="$(realpath "${model_dir_name}")" -B${build_dir} . + + echo "Building ${example_dir}" + cmake --build ${build_dir} -j9 --target executor_runner + + echo "Removing ${model_dir_name}" + rm -rf "${model_dir_name}" + + echo 'Running wasm build test' + node ${build_dir}/executor_runner.js --model_path="${model_export_name}" +} + +if [[ -z $PYTHON_EXECUTABLE ]]; +then + PYTHON_EXECUTABLE=python3 +fi + +cmake_install_executorch_lib + +test_build_wasm add_mul +test_build_wasm mv2 From 43561d7a8a3b25af97eb705d35f4aae19169b20f Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Wed, 9 Jul 2025 11:15:43 -0700 Subject: [PATCH 3/6] Added CI tests --- .ci/scripts/setup-emscripten.sh | 20 ++++++++++++++++++++ .github/workflows/pull.yml | 28 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .ci/scripts/setup-emscripten.sh diff --git a/.ci/scripts/setup-emscripten.sh b/.ci/scripts/setup-emscripten.sh new file mode 100644 index 00000000000..6ecdad37007 --- /dev/null +++ b/.ci/scripts/setup-emscripten.sh @@ -0,0 +1,20 @@ + +set -ex + +install_nodejs() { + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash + \. "$HOME/.nvm/nvm.sh" + nvm install 22 +} + +install_emscripten() { + git clone https://github.com/emscripten-core/emsdk.git + pushd emsdk || return + ./emsdk install 4.0.10 + ./emsdk activate 4.0.10 + source ./emsdk_env.sh + popd || return +} + +install_nodejs +install_emscripten diff --git a/.github/workflows/pull.yml b/.github/workflows/pull.yml index df254b7f409..02903918fe5 100644 --- a/.github/workflows/pull.yml +++ b/.github/workflows/pull.yml @@ -734,3 +734,31 @@ jobs: PYTHON_EXECUTABLE=python bash .ci/scripts/setup-openvino.sh PYTHON_EXECUTABLE=python bash .ci/scripts/test_openvino.sh + + test-build-wasm-linux: + name: test-build-wasm-linux + uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + with: + runner: linux.2xlarge + docker-image: executorch-ubuntu-22.04-clang12 + submodules: 'recursive' + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + timeout: 90 + script: | + # The generic Linux job chooses to use base env, not the one setup by the image + CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]") + conda activate "${CONDA_ENV}" + + BUILD_TOOL="cmake" + PYTHON_EXECUTABLE=python bash .ci/scripts/setup-linux.sh --build-tool "${BUILD_TOOL}" + + # Install Node.js and Emscripten + source .ci/scripts/setup-emscripten.sh + + # Test selective build + PYTHON_EXECUTABLE=python bash examples/wasm/test_build_wasm.sh From d19c6b497d0f496fcf05588af970b260a710b8c0 Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Wed, 9 Jul 2025 15:29:17 -0700 Subject: [PATCH 4/6] Emscripten automatically installs node.js --- examples/wasm/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/wasm/README.md b/examples/wasm/README.md index ecc65896920..cb0395d3376 100644 --- a/examples/wasm/README.md +++ b/examples/wasm/README.md @@ -12,11 +12,10 @@ examples/wasm ## Prerequisites - [emscripten](https://emscripten.org/docs/getting_started/Tutorial.html) -- [Node.js](https://nodejs.org/en/) (Optional) ## Generate Models -JavaScript does not have access to the filesystem. To load a model, it needs to be preloaded or embedded into the virtual filesystem. In this example, models in the `./models/` directory are embedded by default. We will then build `executorch_runner` in Wasm. +JavaScript does not have direct access to the host file system. To load a model, it needs to be preloaded or embedded into the virtual file system. In this example, models in the `./models/` directory are embedded by default. We will then build `executorch_runner` in Wasm. 1. Following the setup guide in [Setting up ExecuTorch](https://pytorch.org/executorch/main/getting-started-setup) you should be able to get the basic development environment for ExecuTorch working. @@ -56,7 +55,7 @@ If you need to rebuild `executor_runner` after modifying the contents of `./mode cmake --build cmake-out-wasm -j32 --target executor_runner --clean-first ``` -4. Run the model with Node.js. +4. Run the model with Node.js (automatically installed with Emscripten). ```bash # Run the tool on the generated model. From 4b0e1afd2d21dd99254b430566bc3605a1492c2c Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Thu, 10 Jul 2025 13:24:50 -0700 Subject: [PATCH 5/6] Use Emscripten provided version of Node.js --- .ci/scripts/setup-emscripten.sh | 7 ------- examples/wasm/README.md | 11 ++--------- examples/wasm/test_build_wasm.sh | 2 +- 3 files changed, 3 insertions(+), 17 deletions(-) diff --git a/.ci/scripts/setup-emscripten.sh b/.ci/scripts/setup-emscripten.sh index 6ecdad37007..637f3cbda0d 100644 --- a/.ci/scripts/setup-emscripten.sh +++ b/.ci/scripts/setup-emscripten.sh @@ -1,12 +1,6 @@ set -ex -install_nodejs() { - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash - \. "$HOME/.nvm/nvm.sh" - nvm install 22 -} - install_emscripten() { git clone https://github.com/emscripten-core/emsdk.git pushd emsdk || return @@ -16,5 +10,4 @@ install_emscripten() { popd || return } -install_nodejs install_emscripten diff --git a/examples/wasm/README.md b/examples/wasm/README.md index cb0395d3376..f9eec78984e 100644 --- a/examples/wasm/README.md +++ b/examples/wasm/README.md @@ -2,16 +2,9 @@ This guide describes how to build ExecuTorch for WebAssembly (Wasm). -## Directory Structure - -``` -examples/wasm -└── README.md # This file -``` - ## Prerequisites -- [emscripten](https://emscripten.org/docs/getting_started/Tutorial.html) +- [Emscripten](https://emscripten.org/docs/getting_started/Tutorial.html) ## Generate Models @@ -55,7 +48,7 @@ If you need to rebuild `executor_runner` after modifying the contents of `./mode cmake --build cmake-out-wasm -j32 --target executor_runner --clean-first ``` -4. Run the model with Node.js (automatically installed with Emscripten). +4. Run the model with Node.js. Emscripten should come preinstalled with a compatible version of Node.js. If you have an incompatible version of Node.js installed, you can use the Emscripten-provided version by running `$EMSDK_NODE` instead of `node`. ```bash # Run the tool on the generated model. diff --git a/examples/wasm/test_build_wasm.sh b/examples/wasm/test_build_wasm.sh index 87ef91ce4b2..8218a0ebb9b 100644 --- a/examples/wasm/test_build_wasm.sh +++ b/examples/wasm/test_build_wasm.sh @@ -23,7 +23,7 @@ test_build_wasm() { rm -rf "${model_dir_name}" echo 'Running wasm build test' - node ${build_dir}/executor_runner.js --model_path="${model_export_name}" + $EMSDK_NODE ${build_dir}/executor_runner.js --model_path="${model_export_name}" } if [[ -z $PYTHON_EXECUTABLE ]]; From a21942d31585f434ef2d3bbff6da20f243d07d7a Mon Sep 17 00:00:00 2001 From: Conan Jeffrey Truong Date: Thu, 10 Jul 2025 16:49:06 -0700 Subject: [PATCH 6/6] Applied suggestions --- CMakeLists.txt | 1 + examples/wasm/README.md | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3f6133af74f..5071cca55b4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -727,6 +727,7 @@ if(EXECUTORCH_BUILD_EXECUTOR_RUNNER) target_link_libraries(executor_runner ${_executor_runner_libs}) target_compile_options(executor_runner PUBLIC ${_common_compile_options}) + # Automatically set when using `emcmake cmake` for Wasm build. if(EMSCRIPTEN) # Directory of model pte files to embed in the wasm binary. if(NOT DEFINED WASM_MODEL_DIR) diff --git a/examples/wasm/README.md b/examples/wasm/README.md index f9eec78984e..15ce07493d1 100644 --- a/examples/wasm/README.md +++ b/examples/wasm/README.md @@ -2,6 +2,18 @@ This guide describes how to build ExecuTorch for WebAssembly (Wasm). +## Quick Start + +To quickly test the build, you can run the following commands + +```bash +cd executorch # To the top level dir + +source .ci/scripts/setup-emscripten.sh # Install Emscripten and set up the environment variables + +bash examples/wasm/test_build_wasm.sh # Run the test build script +``` + ## Prerequisites - [Emscripten](https://emscripten.org/docs/getting_started/Tutorial.html) @@ -62,3 +74,24 @@ python3 -m http.server --directory cmake-out-wasm ``` The page will be available at http://localhost:8000/executor_runner.html. + +## Common Issues + +### CompileError: WebAssembly.instantiate() [...] failed: expected table index 0... + +This seems to be an issue with Node.js v16. Emscripten should come preinstalled with a compatible version of Node.js. You can use the Emscripten-provided version by running `$EMSDK_NODE` instead of `node`. + +```bash +echo $EMSDK_NODE +.../emsdk/node/22.16.0_64bit/bin/node # example output +``` + +### Failed to open [...]: No such file or directory (44) + +The file may not have been present while building the Wasm binary. You can rebuild with the following command + +```bash +cmake --build cmake-out-wasm -j32 --target executor_runner --clean-first +``` + +The path may also be incorrect. The files in the `WASM_MODEL_DIR` are placed into the root directory of the virtual file system, so you would use `--model_path mv2.pte` instead of `--model_path models/mv2.pte`, for example.