rustnn compiled to WebAssembly, building graphs on the browser's WebNN API. Live at https://rustnn.github.io/rustnnwasm-demo/ (needs a browser with WebNN enabled). Two pages:
index.html, Transformers.js with a rustnn backend:pipeline('feature-extraction', provider)using the pluggable inference providers of transformers.js#1732, withall-MiniLM-L6-v2compared against the default ONNX Runtime pipeline.playground.html, graph playground: edit a.webnngraph; rustnn parses it and builds it through the browser'sMLGraphBuilder, and the page runs it.
rustnn does not wire tensors and dispatch for the browser backend yet, so both pages create
tensors and call dispatch from JavaScript. See ../rustnn/docs/integration/webnn-browser.md.
index.html / embeddings.js
│ pipeline('feature-extraction', provider) extractor(texts, { pooling, normalize })
▼
┌──────────────────────────── Transformers.js (PR #1732) ────────────────────────────┐
│ config + tokenizer (Xenova/all-MiniLM-L6-v2) mean pooling, normalization │
│ hub download + browser cache: getModelFile() calls model.forward(inputs) │
└─────────────┬───────────────────────────────────────────────────▲──────────────────┘
│ load(options) forward(inputs) │ { last_hidden_state }
▼ │
┌──────────────────────── rustnn-provider.js (RustnnInferenceProvider) ──────────────┐
│ load: fetch model.webnn, manifest.json, model.weights │
│ (tarekziade/all-MiniLM-L6-v2-webnn) and create an MLContext │
│ forward: pad each row to 128 tokens, writeTensor, dispatch, readTensor, unpad │
└─────────────┬───────────────────────────────────────────────────▲──────────────────┘
│ loadWebnnModel(context, text, manifest, weights) │ MLGraph
▼ │
┌──────────────────────── rustnn (wasm, pkg/ from src/lib.rs) ───────────────────────┐
│ parse .webnn ─► inline weights ─► GraphInfo ─► narrow int64 to int32 if needed │
│ converters::webnn::convert_async: one MLGraphBuilder call per operation, build() │
└─────────────┬──────────────────────────────────────────────────────────────────────┘
│ MLGraphBuilder calls (the provider dispatches the MLGraph)
▼
┌──────────────── browser WebNN (navigator.ml): GPU, NPU or CPU backend ─────────────┐
└────────────────────────────────────────────────────────────────────────────────────┘
cd rustnnwasm-demo
wasm-pack build --target web --release --no-typescript --no-pack # writes pkg/
python3 serve.py # http://127.0.0.1:8137/, no-cache headersThe embeddings page also needs the transformers.js PR branch, built in vendor/ (gitignored)
and mapped with an import map in index.html:
git clone --depth 1 --branch nico/pluggable-inference-providers \
https://github.com/huggingface/transformers.js vendor/transformers.js
cd vendor/transformers.js
npm exec --yes --package=pnpm@10.28.1 -- pnpm install
npm exec --yes --package=pnpm@10.28.1 -- pnpm \
--filter @huggingface/transformers-onnxruntime --filter @huggingface/transformers buildserve.py makes the browser revalidate every file, so a rebuilt pkg/ or edited JavaScript
is picked up on reload (python3 -m http.server lets Chrome cache them).
Open http://localhost:8137/ in a browser with WebNN enabled. For Chrome, either turn on
chrome://flags/#web-machine-learning-neural-network or start a separate instance:
open -na "Google Chrome" --args --user-data-dir=/tmp/chrome-webnn \
--enable-features=WebMachineLearningNeuralNetwork http://localhost:8137/.cargo/config.toml sets the wasm32 target and the RUSTFLAGS the WebNN bindings need
(+reference-types, --cfg=web_sys_unstable_apis).
- The
.webnntext parser depends onpest, which pulls instackerandpsm.psm's assembly does not build for wasm32 with Apple's clang, sosrc/lib.rsprovides the one symbolstackeruses there (rust_psm_stack_pointer). - Chrome on macOS accepts only
float32,float16andint32inputs and has no int64 constants.loadWebnnModelnarrows int64 to int32 whenopSupportLimits()lacks int64; the provider then writesInt32Arrayinputs. - The embeddings page needs rustnn's WebNN converter to lower
squeezeandunsqueeze(not WebNN operations) toreshape, as the specification's emulation does;../rustnncarries that change. - The MiniLM WebNN graph has a fixed
[1, 128]input. The provider runs one row at a time and rejects sequences longer than 128 tokens. - In the playground, constants use
@scalar(v)or@bytes([...])(little-endian), since the page has no weights file.
wasm-pack build --target web --release --no-typescript --no-pack
scripts/publish-pages.sh # --no-push only assembles site/The script copies the pages, pkg/ and the vendor files the import map uses into site/, and
force-pushes them as the gh-pages branch. The wasm is prebuilt locally, not in CI, because it
links the local ../rustnn checkout. Visitors need a browser with WebNN enabled.