Skip to content

Add: WASM Emscripten example #347

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

raphaelmenges
Copy link
Contributor

@raphaelmenges raphaelmenges commented Feb 4, 2025

Hello 👋,

I noticed that support for the WASM (WASI?) target in this crate has been officially dropped. However, I recently encountered a use case that requires me running onnxruntime on the Web. This led me to investigate whether ort could work within an Emscripten environment.

I discovered that this crate can be used as-is with the wasm32-unknown-emscripten Rust compilation target! While the setup can get a bit complex—especially when enabling multi-threading in onnxruntime—it works well. I would love to see this example merged into the official repository, but I also understand if it is considered too experimental to be officially endorsed.

Here’s a .gif for motivation, showcasing my example using the YoloV8 model to classify objects in pictures on Chromium:

Screen Recording 2025-02-04 at 11 16 22


The Less Crazy Part

Microsoft does not provide precompiled static libraries for WASM, so I created a GitHub Actions workflow to handle this. The generated libonnxruntime.a can be linked with ort as usual—even when targeting wasm32-unknown-emscripten.

To expose Rust functions to JavaScript, the Rust main file must provide a C interface, which Emscripten can export. I use rust-embed to bundle the .onnx model into the .wasm. An index.html file then incorporates the .js and .wasm outputs, which include the compiled Rust code, onnxruntime, and the model.

Additionally, the Emscripten SDK version used by the Rust compiler must match the exact version used by onnxruntime for successful linking.

The Crazy Part

Things get trickier when enabling multi-threading in onnxruntime. Since v1.19.0, Microsoft recommends enabling multi-threading using the --enable_wasm_threads build flag. This links libonnxruntime.a to pthread, meaning all linked objects—including Rust’s standard library—must also be compiled with pthread support.

However, Rust’s standard library is not compiled this way by default, so you must switch to Rust nightly and compile the Rust standard library with +atomics,+bulk-memory,+mutable-globals.

Additionally, the server must set specific CORS flags, as multi-threaded Emscripten uses SharedArrayBuffer in the Web browser, which requires these settings.

Verdict

I am already opening the pull request as a draft to get early feedback. However, at least following ToDos are pending before a pull request could happen:

  • Enable debug builds of the example.
  • Add CI to automatically test the target.
  • Use/enhance ort's precompiled static libonnxruntime mechanism.

In the future, I would love to see execution providers for the Web to be available in ort:

  • Add XNNPACK execution provider. Cannot be built at the moment.
  • Add WebGL execution provider. Stable but deprecated. Might be the best option for now? Seems to exposed to the JavaScript world, only.
  • Add WebGPU execution provider. Still requires Firefox Nightly, but is the most promising future option. However, might not yet be ready.
  • Add WebNN execution provider. WebNN is not yet a standard.
Siteproxy

This comment was marked as off-topic.

@raphaelmenges raphaelmenges marked this pull request as draft February 4, 2025 11:13
@raphaelmenges
Copy link
Contributor Author

raphaelmenges commented Feb 6, 2025

  • Add CI to automatically test the target.

I imagine an even more minimal example / test setup than YoloV8 that is first compiled and then run in the CI within a headless Chromium. We could check the developer console output to determine whether the model was correctly inferred. What do you think? Which model would be suited? Any good example how to use headless Chromium in GitHub workflows? Would this become its own workflow or rather a job in an existing workflow?

PS: I guess we could use puppeteer to execute JavaScript code without any .html?

Siteproxy
@decahedron1
Copy link
Member

YOLOv8 is already a pretty good example model - not too simple like the MNIST model, but not too large like GPT-2. I don't know off the top of my head if either supports WASM threads but we should give Node.js or ideally Deno a shot before jumping straight to headless Chromium for CI.

Btw, great work here! Until now I didn't think it was even possible to get wasm32-unknown-emscripten working 😅

@raphaelmenges
Copy link
Contributor Author

I don't know off the top of my head if either supports WASM threads but we should give Node.js or ideally Deno a shot before jumping straight to headless Chromium for CI.

Sure, I can use Deno to test the .wasm build. I am wondering what would be the best approach to integrate such a test into this repository:

  • We require Rust code that is compiled into the .wasm. Where should it live? I guess inside the tests directory, only Rust tests should live? Should I reuse the code from the example for the test? Feels also strange to use an example project for testing. Perhaps I can create a new top-level directory tests-wasm and put all relevant files there?
  • In the workflow, the Rust code should be compiled into the .wasm. Should that happen in an existing workflow like the test.yml and there be a job on its own?

@raphaelmenges raphaelmenges force-pushed the wasm-emscripten-example branch from cbf5276 to 4337eca Compare February 18, 2025 09:15
@raphaelmenges
Copy link
Contributor Author

  • Use/enhance ort's precompiled static libonnxruntime mechanism.

I have observed that you have added mechanisms to provide a precompiled static libonnxruntime for the Emscripten environment. However, when I try to use the download-binaries feature, cargo build panics with "downloaded binaries not available for target wasm32-unknown-emscripten". Am I doing something wrong, or do require any contribution in that regard from me or should I leave the current mechanism to get static libonnxruntime in the example code as-is?

Siteproxy
@decahedron1
Copy link
Member

decahedron1 commented Feb 18, 2025

Like I said in #349 I couldn't get binaries for 1.20.2 but they should be available for 1.21. Keep the current binary mechanism, and don't worry about CI, I'll handle those when 1.21 rolls around 👍

@raphaelmenges
Copy link
Contributor Author

FYI, onnxruntime main branch is now using Emscripten v4.0.3: microsoft/onnxruntime@e6ae1bb

@raphaelmenges raphaelmenges force-pushed the wasm-emscripten-example branch from c63d5ad to 78b0f6f Compare February 21, 2025 08:13
@raphaelmenges
Copy link
Contributor Author

I have pulled the latest changes from main to my branch and tried to enable the WebGPU ep. However, I get the following error at linking:

 = note: wasm-ld: error: /var/folders/z1/_strmfhd7dlcf0ypdhm9vxvm0000gn/T/rustczlLYdj/libort_sys-77bcb0b41cbb8fd7.rlib(graph.cc.o): undefined symbol: std::__2::__fs::filesystem::path::__parent_path() const
          wasm-ld: error: /var/folders/z1/_strmfhd7dlcf0ypdhm9vxvm0000gn/T/rustczlLYdj/libort_sys-77bcb0b41cbb8fd7.rlib(graph.cc.o): undefined symbol: std::__2::__fs::filesystem::path::__root_directory() const
          wasm-ld: error: /var/folders/z1/_strmfhd7dlcf0ypdhm9vxvm0000gn/T/rustczlLYdj/libort_sys-77bcb0b41cbb8fd7.rlib(graph.cc.o): undefined symbol: std::__2::__fs::filesystem::path::__filename() const
          wasm-ld: error: /var/folders/z1/_strmfhd7dlcf0ypdhm9vxvm0000gn/T/rustczlLYdj/libort_sys-77bcb0b41cbb8fd7.rlib(graph_partitioner.cc.o): undefined symbol: std::__2::__fs::filesystem::__status(std::__2::__fs::filesystem::path const&, std::__2::error_code*)

I have removed the default-features of ort, which should make it free of std? Thus, I do not understand why it tries to link something from std at all.

Another cause for the issue could be the upgrade to Emscripten v4.0.3.

Any glue?

@decahedron1
Copy link
Member

Those symbols are C++'s std, not Rust. It's definitely caused by the Emscripten upgrade. Was the ONNX Runtime binary compiled with Emscripten 4.0.3?

@raphaelmenges
Copy link
Contributor Author

raphaelmenges commented Feb 21, 2025

It should have been, according to the log files.

Update: Soon it must be Emscripten 4.0.4 :)

@xixioba
Copy link

xixioba commented Mar 3, 2025

Is there any progress? I tried to reproduce but failed. I am looking forward to the final merge.
Also, can WASI be supported, such as using wasmtime to run ort

@decahedron1
Copy link
Member

Also, can WASI be supported, such as using wasmtime to run ort

Pure WASI is a no, unfortunately. wasmtime specifically has an ort backend for their WASI-NN implementation though 😊

@decahedron1
Copy link
Member

decahedron1 commented Mar 8, 2025

@raphaelmenges Did you ever get to the bottom of that linking issue? And do you need my help on anything here? Looks like ONNX Runtime v1.21 is coming up soon out now and I'd like to get that + WASM out in rc.10 😊

@raphaelmenges
Copy link
Contributor Author

I have not further investigated the issue, yet. I can do so next week sometime, if this is early enough for you!

@raphaelmenges raphaelmenges force-pushed the wasm-emscripten-example branch from 862a70b to 8a174a9 Compare March 10, 2025 07:25
@raphaelmenges
Copy link
Contributor Author

I have upgraded my code to include your latest changes and your precompiled onnxruntime from pyke.io. However, I get the following linker error:

  = note: wasm-ld: error: unable to find library -lwebgpu_dawn

I then removed linking to webgpu_dawn from ort-sysbecause I assumed that Dawn is only required on Desktop and Mobile to provide a WebGPU environment. However, then I am confronted with the following linker errors:

  = note: wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuInstanceRequestAdapter
          wasm-ld: error: lto.tmp: undefined symbol: wgpuAdapterHasFeature
          wasm-ld: error: lto.tmp: undefined symbol: wgpuAdapterHasFeature
          wasm-ld: error: lto.tmp: undefined symbol: wgpuAdapterHasFeature
          wasm-ld: error: lto.tmp: undefined symbol: wgpuAdapterHasFeature
          wasm-ld: error: lto.tmp: undefined symbol: wgpuAdapterGetLimits
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuAdapterRequestDevice
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: lto.tmp: undefined symbol: emwgpuDelete
          wasm-ld: error: too many errors emitted, stopping now (use -error-limit=0 to see all errors)

I have then checked the logs of your precompiled onnxruntime build and indeed, Dawn seems to be used in the Emscripten build as well. I could not find a webgpu_dawn library in the outputs, though. I am left confused.


In the release note of v1.21 it is also mentioned that onnxruntime now supports WASM64. Should we do something about that?

@decahedron1
Copy link
Member

Dawn is indeed required for Emscripten too. Does the -sUSE_WEBGPU emcc flag help with that linker error at all? If not we can forego WebGPU on WASM for now and stick to a vanilla build - after all, with the new alternative backends feature, there could be a wonnx backend which also gives us the benefit of WebGPU for wasm32-unknown-unknown.

Let's get WASM32 working before looking into WASM64. Browser support doesn't seem too good anyways (though better than WebGPU to be fair 😶)

@raphaelmenges
Copy link
Contributor Author

I tried wonnx some months ago and the development appears to me rather slow or even dead, sadly. I am not sure whether integrating it into ort will bring real benefit to anyone but increase your maintenance effort.

Anyway, I am very fine if you decide to not to include the WebGPU ep in the WASM example at the moment. I am also happy if you take over this pull request now and make any desired changes you want, i.e., remove the WebGPU ep code etc!

@decahedron1
Copy link
Member

Hmm, those emwgpu functions are defined in Dawn's own version of the script included by -sUSE_WEBGPU. I don't know how difficult it'll be to include that script but I'm starting a non-WebGPU build for wasm32-unknown-emscripten just in case. We can always revisit WebGPU on WASM at a later date, maybe when it's no longer so experimental (unless its like ort's rc situation 🫠)

@raphaelmenges
Copy link
Contributor Author

In another context I just realized that the YoloV8 model you provide has now a different URL: https://cdn.pyke.io/0/pyke:ort-rs/example-models@0.0.0/yolov8m.onnx

Can you fix that link in the WASM example code?

@decahedron1 decahedron1 marked this pull request as ready for review March 10, 2025 18:45
@decahedron1
Copy link
Member

decahedron1 commented Mar 10, 2025

I don't think I can push to your branch because it's an organization-owned fork, but I updated the model URL and did another minor tweak to fix the name of the output .js file and it's all working! Next is WebGPU =)

image

EDIT: yeah, no WebGPU for now. It links fine and acknowledges the EP but complains of a Rust panic during the call to SessionOptionsAppendExecutionProvider with an 'empty' message, which makes no sense. Debugging WASM is a nightmare and each cargo build takes 5 minutes so I'm throwing in the towel. At least the CPU EP works great!

EDIT 2: seems like the WebGPU build is just broken in general as removing the registration makes it panic at a different point. I still don't know where it would be panicking with an empty message. This type of weirdness only ever happens on WASM 🙃

@raphaelmenges
Copy link
Contributor Author

EDIT 2: seems like the WebGPU build is just broken in general as removing the registration makes it panic at a different point. I still don't know where it would be panicking with an empty message. This type of weirdness only ever happens on WASM 🙃

This might be the pull request to track about the status of the WebGPU ep in WASM: microsoft/onnxruntime#23697

I am using WebGPU ep on macOS already successfully btw. :)

@decahedron1
Copy link
Member

Opened #363 with my changes, thank you for all your hard work here!

decahedron1 added a commit that referenced this pull request Mar 11, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Co-authored-by: r.menges <Raphael.Menges@alfatraining.de>
@raphaelmenges
Copy link
Contributor Author

EDIT 2: seems like the WebGPU build is just broken in general as removing the registration makes it panic at a different point. I still don't know where it would be panicking with an empty message. This type of weirdness only ever happens on WASM 🙃

According to @fs-eire, the WebGPU ep should work now in WASM: microsoft/onnxruntime#23072 (comment)

vali-reinfer pushed a commit to reinfer/ort that referenced this pull request Apr 29, 2025

Partially verified

This commit is signed with the committer’s verified signature.
vali-reinfer’s contribution has been verified via GPG key.
We cannot verify signatures from co-authors, and some of the co-authors attributed to this commit require their commits to be signed.
Co-authored-by: r.menges <Raphael.Menges@alfatraining.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants