mirror of
https://github.com/saymrwulf/onnxruntime.git
synced 2026-05-17 21:10:43 +00:00
### Description This PR rewrite the backend resolve logic to support specifying multiple EPs. #### Backend The first version of ONNX Runtime Web actually carried some existing code from [ONNX.js](https://github.com/microsoft/onnxjs), which includes the "backend" concept. The original "backend" in ONNX.js is designed in a way assuming there is only one backend from user's backend hint list will be used. For example, in ONNX.js, if user specify a backend hint as `['webgl', 'wasm']`, ONNX.js will first try to use WebGL backend - if it loads successfully (the browser supports webgl), then "webgl" backend will be used and "wasm" will be ignored; otherwise, "webgl" will be ignored and try to load "wasm" backend. In short: only one backend will be used when initializing a session. #### Execution Provider Execution Provider, or EP, in ONNX Runtime is a different concept. One of the differences is that users are allow to specify multiple EPs, and if one does not support a particular kernel, it can fallback to other EP. This is a very common case when using a GPU EP in ONNX Runtime. #### Current Status: Backend v.s. EP Because of the history reasons mentioned above, the current status is quite confusing. There are **real backend**s, which means it's different implementation in code; and there are **backend hint**s, which are used as string names for backend hint; and there are **EP**s of the ONNX Runtime concepts. currently there are only 2 **backend**s in our code base: The "onnxjs backend", and the "wasm backend". The "onnxjs backend" currently only powers backend hint "webgl", which go into the old onnx.js code path. All other backend hints including "wasm", "cpu"(alias to wasm), "webgpu" and "webnn" are all powered by "wasm backend". And because ORT Web treat "backend" as an internal concept and want to align with ONNX Runtime, so those names of backend hints are becoming EP names. The following table shows today's status: | Execution Provider Name (public) / Backend Hint (internal) | Backend | EP in ORT | -------- | ------- | ------- | | "wasm"/"cpu" | WasmBackend | CPU EP | "webgl" | OnnxjsBackend | \* technically not an EP | "webgpu" | WasmBackend | JSEP | "webnn" | WasmBackend | WebNN EP #### Problem While the API allows to specify multiple EPs, the backend resolving only allows one backend. This causes issues when user specify multiple EP names in session options, the backend resolve behavior and EP registration behavior is inconsistent. Specifically, in this issue: https://github.com/microsoft/onnxruntime/issues/15796#issuecomment-1925363908: EP list `['webgpu', 'wasm']` on a browser without WebGPU support resolves to 'wasm' backend, but the full EP list is passed in session options, so JSEP is still enabled, causing the runtime error. #### Solution Since we still need WebGL backend, we cannot totally remove the backend register/resolve system. In this PR I made the following changes: - initialize every backend from the EP list, instead of only do that for the first successful one. - for the first resolved backend, filter all EP using the exact same backend. Remove all EPs not using this backend from session options - for every explicitly specified EP, if it's removed, show a warning message in console
161 lines
5.3 KiB
TypeScript
161 lines
5.3 KiB
TypeScript
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// Licensed under the MIT License.
|
|
|
|
import {Backend} from './backend.js';
|
|
import {InferenceSession} from './inference-session.js';
|
|
|
|
interface BackendInfo {
|
|
backend: Backend;
|
|
priority: number;
|
|
|
|
initPromise?: Promise<void>;
|
|
initialized?: boolean;
|
|
aborted?: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
const backends: Map<string, BackendInfo> = new Map();
|
|
const backendsSortedByPriority: string[] = [];
|
|
|
|
/**
|
|
* Register a backend.
|
|
*
|
|
* @param name - the name as a key to lookup as an execution provider.
|
|
* @param backend - the backend object.
|
|
* @param priority - an integer indicating the priority of the backend. Higher number means higher priority. if priority
|
|
* < 0, it will be considered as a 'beta' version and will not be used as a fallback backend by default.
|
|
*
|
|
* @ignore
|
|
*/
|
|
export const registerBackend = (name: string, backend: Backend, priority: number): void => {
|
|
if (backend && typeof backend.init === 'function' && typeof backend.createInferenceSessionHandler === 'function') {
|
|
const currentBackend = backends.get(name);
|
|
if (currentBackend === undefined) {
|
|
backends.set(name, {backend, priority});
|
|
} else if (currentBackend.priority > priority) {
|
|
// same name is already registered with a higher priority. skip registeration.
|
|
return;
|
|
} else if (currentBackend.priority === priority) {
|
|
if (currentBackend.backend !== backend) {
|
|
throw new Error(`cannot register backend "${name}" using priority ${priority}`);
|
|
}
|
|
}
|
|
|
|
if (priority >= 0) {
|
|
const i = backendsSortedByPriority.indexOf(name);
|
|
if (i !== -1) {
|
|
backendsSortedByPriority.splice(i, 1);
|
|
}
|
|
|
|
for (let i = 0; i < backendsSortedByPriority.length; i++) {
|
|
if (backends.get(backendsSortedByPriority[i])!.priority <= priority) {
|
|
backendsSortedByPriority.splice(i, 0, name);
|
|
return;
|
|
}
|
|
}
|
|
backendsSortedByPriority.push(name);
|
|
}
|
|
return;
|
|
}
|
|
|
|
throw new TypeError('not a valid backend');
|
|
};
|
|
|
|
/**
|
|
* Try to resolve and initialize a backend.
|
|
*
|
|
* @param backendName - the name of the backend.
|
|
* @returns the backend instance if resolved and initialized successfully, or an error message if failed.
|
|
*/
|
|
const tryResolveAndInitializeBackend = async(backendName: string): Promise<Backend|string> => {
|
|
const backendInfo = backends.get(backendName);
|
|
if (!backendInfo) {
|
|
return 'backend not found.';
|
|
}
|
|
|
|
if (backendInfo.initialized) {
|
|
return backendInfo.backend;
|
|
} else if (backendInfo.aborted) {
|
|
return backendInfo.error!;
|
|
} else {
|
|
const isInitializing = !!backendInfo.initPromise;
|
|
try {
|
|
if (!isInitializing) {
|
|
backendInfo.initPromise = backendInfo.backend.init(backendName);
|
|
}
|
|
await backendInfo.initPromise;
|
|
backendInfo.initialized = true;
|
|
return backendInfo.backend;
|
|
} catch (e) {
|
|
if (!isInitializing) {
|
|
backendInfo.error = `${e}`;
|
|
backendInfo.aborted = true;
|
|
}
|
|
return backendInfo.error!;
|
|
} finally {
|
|
delete backendInfo.initPromise;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Resolve execution providers from the specific session options.
|
|
*
|
|
* @param options - the session options object.
|
|
* @returns a promise that resolves to a tuple of an initialized backend instance and a session options object with
|
|
* filtered EP list.
|
|
*
|
|
* @ignore
|
|
*/
|
|
export const resolveBackendAndExecutionProviders = async(options: InferenceSession.SessionOptions):
|
|
Promise<[backend: Backend, options: InferenceSession.SessionOptions]> => {
|
|
// extract backend hints from session options
|
|
const eps = options.executionProviders || [];
|
|
const backendHints = eps.map(i => typeof i === 'string' ? i : i.name);
|
|
const backendNames = backendHints.length === 0 ? backendsSortedByPriority : backendHints;
|
|
|
|
// try to resolve and initialize all requested backends
|
|
let backend: Backend|undefined;
|
|
const errors = [];
|
|
const availableBackendNames = new Set<string>();
|
|
for (const backendName of backendNames) {
|
|
const resolveResult = await tryResolveAndInitializeBackend(backendName);
|
|
if (typeof resolveResult === 'string') {
|
|
errors.push({name: backendName, err: resolveResult});
|
|
} else {
|
|
if (!backend) {
|
|
backend = resolveResult;
|
|
}
|
|
if (backend === resolveResult) {
|
|
availableBackendNames.add(backendName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// if no backend is available, throw error.
|
|
if (!backend) {
|
|
throw new Error(`no available backend found. ERR: ${errors.map(e => `[${e.name}] ${e.err}`).join(', ')}`);
|
|
}
|
|
|
|
// for each explicitly requested backend, if it's not available, output warning message.
|
|
for (const {name, err} of errors) {
|
|
if (backendHints.includes(name)) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`removing requested execution provider "${
|
|
name}" from session options because it is not available: ${err}`);
|
|
}
|
|
}
|
|
|
|
const filteredEps = eps.filter(i => availableBackendNames.has(typeof i === 'string' ? i : i.name));
|
|
|
|
return [
|
|
backend, new Proxy(options, {
|
|
get: (target, prop) => {
|
|
if (prop === 'executionProviders') {
|
|
return filteredEps;
|
|
}
|
|
return Reflect.get(target, prop);
|
|
}
|
|
})
|
|
];
|
|
};
|