Register devices when the ONNX runtime is injected via Symbol.for('onnxruntime') - #1743
Register devices when the ONNX runtime is injected via Symbol.for('onnxruntime')#1743astefanutti wants to merge 2 commits into
Conversation
…nxruntime')
`backends/onnx.js` documents an injection point — "if the JS runtime exposes
their own ONNX runtime, use it" — but that branch sets `ONNX` and then falls
past both branches that populate `supportedDevices` and `defaultDevices`.
Every other path sets the module and the device list together.
The result is that an embedder using the documented seam gets an empty
device list, and every `device` option is rejected before anything loads:
Error: Unsupported device: "webgpu". Should be one of:
with nothing after the colon.
Reproduced in a browser with an alternative onnxruntime-web build:
globalThis[Symbol.for('onnxruntime')] = await import('onnxruntime-web/jspi');
const { pipeline } = await import('@huggingface/transformers');
await pipeline('text-generation', model, { device: 'webgpu', dtype: 'q4f16' });
The fix keeps the environment in charge of the device list and lets the
injection decide only which module is used, so a Node embedder and a browser
embedder each get the list that applies to them. With it, the same call
loads a 4.9 GB model on the GPU:
Ready on WebGPU, q4f16 weights, loaded in 11.5s, 24 KV layers kept
between turns
No behaviour change when nothing is injected: `injectedONNX` is undefined and
both branches fall through to the module they chose before.
|
Hi @astefanutti, thank you so much for looking into this! This looks like the right fix and keeps the normal Node/browser paths unchanged. I don't see a blocker. A focused injected-runtime test and provider-contract clarification would be useful follow-ups. |
|
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
|
Hi @astefanutti 👋 thanks for the PR! Currently, the PR has an issue which basically assumes that an You do, however, correctly point out that the current approach doesn't allow for these official Let me know what you think! :) I think we could design an API-level "devices" getter which can solve this, and can tie in nicely with #1753 (comment) |
…s it runs in
The previous commit let the environment decide the device list for an injected runtime, which
assumed two things that do not hold: that an injected module is one of the official packages, and
that it is the one the process would have bundled. Custom runtimes exist (Supabase's edge runtime
injects `{ Tensor, InferenceSession, env: {} }`), and embedders inject `onnxruntime-web` from
processes where `IS_NODE_ENV` is true — Electron renderers, Bun, Node fallbacks — which would have
been handed the node device list.
Every official package stamps its `env`: onnxruntime-web sets `env.versions.web`, onnxruntime-node
sets `env.versions.node` (`Env.versions` in onnxruntime-common), and a custom runtime sets neither.
So the stamp decides: `web` takes the browser list, `node` the platform list, and an unstamped
injected runtime gets no list at all — the behaviour it had before, left to apply its own defaults.
Without an injected runtime the environment decides, as before.
The test evaluates the module afresh under each of the three shapes and checks the list it resolves.
|
Hi @xenova, thanks a lot for the review, that makes perfect sense. That's right custom runtimes like Supabase's edge runtime injects a shim: Also Injectors in the wild do the opposite of what I assumed: an Electron app injects Your suggestion fixes both, because every official package stamps itself: const injected = ORT_SYMBOL in globalThis ? globalThis[ORT_SYMBOL] : undefined;
const versions = injected?.env?.versions;
const flavour = versions?.node ? 'node'
: versions?.web ? 'web'
: injected ? 'custom'
: apis.IS_NODE_ENV ? 'node' : 'web';
ONNX = injected ?? (flavour === 'node' ? ONNX_NODE : ONNX_WEB);
// 'node' → the platform switch as today; 'web' → the WebNN/WebGPU checks as today;
// 'custom' → no device list, exactly the current behaviour for an injected runtime.The bundled paths are untouched, and an unstamped injected module keeps today's semantics. I've updated the PR with that and added the focused test @nico-martin suggested: inject a stub stamped versions.web, one stamped versions.node, and a bare shim, and assert the resulting device list for each. I'd keep the API-level part as a follow-up so this PR stays a fix: (a) let a custom runtime declare its devices via an optional One thing (a) has to carry with it: the default device would need to come from the backend rather than from |
Problem
backends/onnx.jsdocuments an injection point:That branch sets
ONNXand then falls past both of the branches that populatesupportedDevicesanddefaultDevices. Every other path sets the module and the device list together.The result is that an embedder using the documented seam gets an empty device list, and every
deviceoption is rejected before anything loads:
— with nothing after the colon.
Reproduction
In a browser, with any alternative
onnxruntime-webbuild:Fix
Let the injected runtime decide which device list applies, by the package it is — not by the process it runs in, and not by assuming it is an official package at all.
Every official package stamps its
env:onnxruntime-websetsenv.versions.web,onnxruntime-nodesetsenv.versions.node(Env.versionsinonnxruntime-common). A custom runtime that only implements the API surface (Tensor,InferenceSession, …) sets neither. So the stamp tells the three cases apart:onnxruntime-webgets the browser list wherever it runs — including processes whereIS_NODE_ENVis true, which is exactly where embedders inject it (Electron renderers, Bun, Node fallbacks).onnxruntime-nodegets the platform list.executionProvidersforced on it.Validation
tests/injected_runtime.test.jsevaluates the module afresh under each of the three shapes - a modulestamped
versions.web, one stampedversions.node, and a bare{ Tensor, InferenceSession, env: {} }and checks the device list each resolves to.With this branch applied and built, the same call above loads a real 4.9 GB model (
onnx-community/gemma-4-E4B-it-ONNX, q4f16) on the GPU under an injected JSPI runtime: