Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
e6c7666
README and CMake changes
Conarnar Jul 8, 2025
b322cb8
Added build test script for wasm example
Conarnar Jul 9, 2025
43561d7
Added CI tests
Conarnar Jul 9, 2025
d19c6b4
Emscripten automatically installs node.js
Conarnar Jul 9, 2025
4b0e1af
Use Emscripten provided version of Node.js
Conarnar Jul 10, 2025
a21942d
Applied suggestions
Conarnar Jul 10, 2025
7d15296
Merge branch 'main' into wasm-bindings
Conarnar Jul 14, 2025
5378272
Module test working
Conarnar Jul 16, 2025
0701996
Added unit tests for wasm bindings
Conarnar Jul 17, 2025
f3e3d54
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 17, 2025
160f15f
Applied suggestions
Conarnar Jul 18, 2025
bbc46df
Cleaned up tensor classes
Conarnar Jul 18, 2025
79d2769
Added loading from Uint8Array
Conarnar Jul 18, 2025
be9c771
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 19, 2025
a838685
Added CI tests
Conarnar Jul 19, 2025
9c2b93b
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 19, 2025
e63961c
Fix CI
Conarnar Jul 19, 2025
bfbbfc5
Build now uses --post-js to test load from buffer
Conarnar Jul 20, 2025
b274dae
Changed JsTensor data to return memory view
Conarnar Jul 24, 2025
7de34b9
Applied suggestions
Conarnar Jul 24, 2025
4aab20f
Add making JsTensor from iterator
Conarnar Jul 24, 2025
da58ab2
to_evalue check for undefined
Conarnar Jul 24, 2025
cfa3d4c
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 24, 2025
fb5ae68
Renamed option to build unit tests
Conarnar Jul 24, 2025
f4e7144
Marked wasm bindings API as experimental
Conarnar Jul 24, 2025
aa48efb
Set enum names for convenience
Conarnar Jul 25, 2025
6f38b89
Module load from ArrayBuffer
Conarnar Jul 25, 2025
5cfbc1d
Bump PytorchTorch pin to 0725
Conarnar Jul 25, 2025
c72e574
Merge remote-tracking branch 'origin/export-D78989384' into wasm-bind…
Conarnar Jul 25, 2025
1040b15
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 26, 2025
7926335
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 28, 2025
3345fe9
Applied suggestions, turned on assertions, added dim order strides to…
Conarnar Jul 28, 2025
f3ba749
Added case for error messages longer than 256 characters
Conarnar Jul 28, 2025
f806259
Added assertion to verify object is tensor
Conarnar Jul 29, 2025
ddbc698
Merge branch 'main' into wasm-bindings-js
Conarnar Jul 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,10 @@ if(EXECUTORCH_BUILD_PYBIND)
)
endif()

if(EXECUTORCH_BUILD_WASM)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/wasm)
endif()

if(EXECUTORCH_BUILD_EXTENSION_TRAINING)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/extension/training)
endif()
Expand Down
44 changes: 44 additions & 0 deletions extension/wasm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@

cmake_minimum_required(VERSION 3.24)

project(executorch_wasm)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this directory belongs to a new project so should delete this.


if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()

if(NOT EMSCRIPTEN)
message(FATAL_ERROR "Emscripten is required to build this target")
endif()

# Source root directory for executorch.
if(NOT EXECUTORCH_ROOT)
set(EXECUTORCH_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/../..)
endif()

include(${EXECUTORCH_ROOT}/tools/cmake/Utils.cmake)
set(_common_compile_options -Wno-deprecated-declarations -fPIC)
set(_common_include_directories ${EXECUTORCH_ROOT}/..)

set(link_libraries)
list(
APPEND
link_libraries
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: should follow the naming convention with a prepending _

embind
executorch_core
extension_data_loader
portable_ops_lib
extension_module_static
extension_tensor
extension_runner_util
)

add_library(executorch_wasm OBJECT wasm_bindings.cpp)

target_compile_options(executorch_wasm PUBLIC ${_common_compile_options})
target_include_directories(executorch_wasm PUBLIC ${_common_include_directories})
target_link_libraries(executorch_wasm PUBLIC ${link_libraries})

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should there be a default target_link_options for emscripten?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The link options used for the unit tests are specific for that target. I don't think there are link options that would be widely applicable for this.

if(BUILD_TESTING)
add_subdirectory(test)
endif()
15 changes: 15 additions & 0 deletions extension/wasm/build_wasm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@

cd "$(dirname "${BASH_SOURCE[0]}")/../../"
mkdir -p cmake-out-wasm
cd cmake-out-wasm
emcmake cmake -DEXECUTORCH_BUILD_WASM=ON \
-DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \
-DEXECUTORCH_BUILD_EXTENSION_FLAT_TENSOR=ON \
-DEXECUTORCH_BUILD_DEVTOOLS=ON \
-DEXECUTORCH_BUILD_EXTENSION_MODULE=ON \
-DEXECUTORCH_BUILD_EXTENSION_TENSOR=ON \
-DEXECUTORCH_BUILD_EXTENSION_RUNNER_UTIL=ON \
-DBUILD_TESTING=ON \
-DCMAKE_BUILD_TYPE=Release \
..
make executorch_wasm_tests -j32
33 changes: 33 additions & 0 deletions extension/wasm/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

set(MODELS_DIR ${CMAKE_CURRENT_BINARY_DIR}/models/)

add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/models/add_mul.pte ${CMAKE_CURRENT_BINARY_DIR}/models/add.pte
COMMAND ${CMAKE_COMMAND} -E make_directory "${MODELS_DIR}"
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../../..
COMMAND python3 -m examples.portable.scripts.export --model_name="add_mul" --output_dir="${MODELS_DIR}"
COMMAND python3 -m examples.portable.scripts.export --model_name="add" --output_dir="${MODELS_DIR}"
)

add_custom_target(executorch_wasm_test_models DEPENDS ${MODELS_DIR}/add_mul.pte ${MODELS_DIR}/add.pte)

add_executable(executorch_wasm_test_lib)
target_link_libraries(executorch_wasm_test_lib PUBLIC executorch_wasm)
target_link_options(executorch_wasm_test_lib PUBLIC --embed-file "${MODELS_DIR}@/")
add_dependencies(executorch_wasm_test_lib executorch_wasm_test_models)

add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/executorch_wasm.test.js
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/executorch_wasm.test.js ${CMAKE_CURRENT_BINARY_DIR}/executorch_wasm.test.js
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/executorch_wasm.test.js
COMMENT "Copying executorch_wasm.test.js to build output directory"
)

add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/package.json
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/package.json ${CMAKE_CURRENT_BINARY_DIR}/package.json
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/package.json
COMMENT "Copying package.json to build output directory"
)

add_custom_target(executorch_wasm_tests DEPENDS executorch_wasm_test_lib ${CMAKE_CURRENT_BINARY_DIR}/executorch_wasm.test.js ${CMAKE_CURRENT_BINARY_DIR}/package.json)
211 changes: 211 additions & 0 deletions extension/wasm/test/executorch_wasm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@

let et;
beforeAll((done) => {
et = require("./executorch_wasm_test_lib");
et.onRuntimeInitialized = () => {
done();
}
});

describe("Tensor", () => {
test("ones", () => {
const tensor = et.FloatTensor.ones([2, 2]);
expect(tensor.getData()).toEqual([1, 1, 1, 1]);
expect(tensor.getSizes()).toEqual([2, 2]);
tensor.delete();
});

test("zeros", () => {
const tensor = et.FloatTensor.zeros([2, 2]);
expect(tensor.getData()).toEqual([0, 0, 0, 0]);
expect(tensor.getSizes()).toEqual([2, 2]);
tensor.delete();
});

test("fromArray", () => {
const tensor = et.FloatTensor.fromArray([1, 2, 3, 4], [2, 2]);
expect(tensor.getData()).toEqual([1, 2, 3, 4]);
expect(tensor.getSizes()).toEqual([2, 2]);
tensor.delete();
});

test("fromArray wrong size", () => {
expect(() => et.FloatTensor.fromArray([1, 2, 3, 4], [3, 2])).toThrow();
});

test("full", () => {
const tensor = et.FloatTensor.full([2, 2], 3);
expect(tensor.getData()).toEqual([3, 3, 3, 3]);
expect(tensor.getSizes()).toEqual([2, 2]);
tensor.delete();
});
});

describe("Module", () => {
test("getMethods has foward", () => {
const module = et.Module.load("add.pte");
const methods = module.getMethods();
expect(methods).toEqual(["forward"]);
module.delete();
});

test("loadMethod forward", () => {
const module = et.Module.load("add.pte");
expect(() => module.loadMethod("forward")).not.toThrow();
module.delete();
});

test("loadMethod does not exist", () => {
const module = et.Module.load("add.pte");
expect(() => module.loadMethod("does_not_exist")).toThrow();
module.delete();
});

describe("MethodMeta", () => {
test("name is forward", () => {
const module = et.Module.load("add_mul.pte");
const methodMeta = module.getMethodMeta("forward");
expect(methodMeta.name).toEqual("forward");
methodMeta.delete();
module.delete();
});

test("numInputs is 3", () => {
const module = et.Module.load("add_mul.pte");
const methodMeta = module.getMethodMeta("forward");
expect(methodMeta.numInputs).toEqual(3);
methodMeta.delete();
module.delete();
});

test("method does not exist", () => {
const module = et.Module.load("add_mul.pte");
expect(() => module.getMethodMeta("does_not_exist")).toThrow();
module.delete();
});

describe("TensorInfo", () => {
test("sizes is 2x2", () => {
const module = et.Module.load("add_mul.pte");
const methodMeta = module.getMethodMeta("forward");
for (var i = 0; i < methodMeta.numInputs; i++) {
const tensorInfo = methodMeta.inputTensorMeta(i);
expect(tensorInfo.sizes).toEqual([2, 2]);
tensorInfo.delete();
}
methodMeta.delete();
module.delete();
});

test("out of range", () => {
const module = et.Module.load("add_mul.pte");
const methodMeta = module.getMethodMeta("forward");
expect(() => methodMeta.inputTensorMeta(3)).toThrow();
methodMeta.delete();
module.delete();
});
});
});

describe("execute", () => {
test("add normally", () => {
const module = et.Module.load("add.pte");
const inputs = [et.FloatTensor.ones([1]), et.FloatTensor.ones([1])];
const output = module.execute("forward", inputs);

expect(output.length).toEqual(1);
expect(output[0].getData()).toEqual([2]);
expect(output[0].getSizes()).toEqual([1]);

inputs.forEach((input) => input.delete());
output.forEach((output) => output.delete());
module.delete();
});

test("add_mul normally", () => {
const module = et.Module.load("add_mul.pte");
const inputs = [et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2])];
const output = module.execute("forward", inputs);

expect(output.length).toEqual(1);
expect(output[0].getData()).toEqual([3, 3, 3, 3]);
expect(output[0].getSizes()).toEqual([2, 2]);

inputs.forEach((input) => input.delete());
output.forEach((output) => output.delete());
module.delete();
});

test("forward directly", () => {
const module = et.Module.load("add_mul.pte");
const inputs = [et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2])];
const output = module.forward(inputs);

expect(output.length).toEqual(1);
expect(output[0].getData()).toEqual([3, 3, 3, 3]);
expect(output[0].getSizes()).toEqual([2, 2]);

inputs.forEach((input) => input.delete());
output.forEach((output) => output.delete());
module.delete();
});

test("wrong number of inputs", () => {
const module = et.Module.load("add_mul.pte");
const inputs = [et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2])];
expect(() => module.execute("forward", inputs)).toThrow();

inputs.forEach((input) => input.delete());
module.delete();
});

test("wrong input size", () => {
const module = et.Module.load("add.pte");
const inputs = [et.FloatTensor.ones([2, 1]), et.FloatTensor.ones([2, 1])];
expect(() => module.execute("forward", inputs)).toThrow();

inputs.forEach((input) => input.delete());
module.delete();
});

test("wrong input type", () => {
const module = et.Module.load("add.pte");
const inputs = [et.FloatTensor.ones([1]), et.IntTensor.ones([1])];
expect(() => module.execute("forward", inputs)).toThrow();

inputs.forEach((input) => input.delete());
module.delete();
});

test("method does not exist", () => {
const module = et.Module.load("add.pte");
const inputs = [et.FloatTensor.ones([1]), et.FloatTensor.ones([1])];
expect(() => module.execute("does_not_exist", inputs)).toThrow();

inputs.forEach((input) => input.delete());
module.delete();
});

test("output tensor can be reused", () => {
const module = et.Module.load("add_mul.pte");
const inputs = [et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2]), et.FloatTensor.ones([2, 2])];
const output = module.forward(inputs);

expect(output.length).toEqual(1);
expect(output[0].getData()).toEqual([3, 3, 3, 3]);
expect(output[0].getSizes()).toEqual([2, 2]);

const inputs2 = [output[0], output[0], output[0]];
const output2 = module.forward(inputs2);

expect(output2.length).toEqual(1);
expect(output2[0].getData()).toEqual([21, 21, 21, 21]);
expect(output2[0].getSizes()).toEqual([2, 2]);

inputs.forEach((input) => input.delete());
output.forEach((output) => output.delete());
output2.forEach((output) => output.delete());
module.delete();
});
});
});
5 changes: 5 additions & 0 deletions extension/wasm/test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"scripts": {
"test": "jest"
}
}
Loading
Loading