Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ default-members = [ '.' ]
exclude = [
'backends/candle',
'backends/tract',
'backends/web',
'examples/async-gpt2-api',
'examples/cudarc',
'examples/custom-ops',
Expand Down
3 changes: 3 additions & 0 deletions backends/web/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"rust-analyzer.cargo.target": "wasm32-unknown-unknown"
}
44 changes: 44 additions & 0 deletions backends/web/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
[workspace]
resolver = "2"

[package]
name = "ort-web"
description = "ONNX Runtime on the web 🌐 - An alternative backend for ort"
version = "0.1.0+1.22"
edition = "2024"
rust-version = "1.88"
license = "MIT OR Apache-2.0"
repository = "https://github.com/pykeio/ort"
homepage = "https://ort.pyke.io/backends/web"
keywords = [ "machine-learning", "ai", "ml", "web", "wasm" ]
categories = [ "algorithms", "mathematics", "science", "web-programming", "wasm" ]
authors = [
"pyke.io <[email protected]>"
]

[lib]
name = "ort_web"
path = "lib.rs"

[dependencies]
js-sys = "0.3"
ort = { path = "../../", version = "=2.0.0-rc.10", default-features = false, features = [ "alternative-backend" ] }
ort-sys = { path = "../../ort-sys", version = "=2.0.0-rc.10", default-features = false, features = [ "disable-linking" ] }
serde = { version = "1.0", features = [ "derive" ] }
serde-wasm-bindgen = "0.6"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

[dependencies.web-sys]
version = "0.3"
features = [
"console",
"ImageData",
"HtmlImageElement",
"ImageBitmap",
"WebGlTexture",
"GpuBuffer"
]

[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = [ 'cfg(web_sys_unstable_apis)' ] }
144 changes: 144 additions & 0 deletions backends/web/_loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
const INIT_SYMBOL = Symbol('@ort-web.init');

const FEATURES_NONE = 0;
const FEATURES_WEBGL = 1 << 0;
const FEATURES_WEBGPU = 1 << 1;
const FEATURES_ALL = FEATURES_WEBGL | FEATURES_WEBGPU;

/**
* @typedef {Object} Dist
* @property {string} baseUrl
* @property {string} scriptName
* @property {string} binaryName
* @property {string} [wrapperName] defaults to `binaryName` s/\.wasm$/.mjs
* @property {Record<'main' | 'wrapper' | 'binary', string>} integrities
*/

const DIST_BASE = 'https://cdn.pyke.io/0/pyke:ort-rs/[email protected]/';

/** @type {Record<number, Dist>} */
const DIST = {
[FEATURES_NONE]: {
baseUrl: DIST_BASE,
scriptName: 'ort.wasm.min.js',
binaryName: 'ort-wasm-simd-threaded.wasm',
integrities: {
main: 'Uvpo3KshAzID7bmsY+Pz2/tiNWwl6Y5XeDTPpktDx73e0o/1TdssZDScTVHxpLYv',
wrapper: 'Y/ZaWdP4FERyRvi+anEVDVDDhMJKldzf33TRb2MiCALo054swqCUe6aM/tD8XL6g',
binary: '9UMXJFWi2zyn9PbGgXmJjEYM4hu8T8zmqmgxX6zQ08ZmNBOso3IT0cTp3M3oU7DU'
}
},
[FEATURES_WEBGL]: {
baseUrl: DIST_BASE,
scriptName: 'ort.webgl.min.js',
binaryName: 'ort-wasm-simd-threaded.wasm',
integrities: {
main: 'pD9jsAlDhP5yhHaVikKM6mXw/E4HPB+4kc/rf3lrMctGWwT0XpIxiTdH/XDHR7Pr',
wrapper: 'Y/ZaWdP4FERyRvi+anEVDVDDhMJKldzf33TRb2MiCALo054swqCUe6aM/tD8XL6g',
binary: '9UMXJFWi2zyn9PbGgXmJjEYM4hu8T8zmqmgxX6zQ08ZmNBOso3IT0cTp3M3oU7DU'
}
},
[FEATURES_WEBGPU]: {
baseUrl: DIST_BASE,
scriptName: 'ort.webgpu.min.js',
binaryName: 'ort-wasm-simd-threaded.jsep.wasm',
integrities: {
main: 'rY/SpyGuo298HuKPNCTIhlm3xc022++95XwJnuGVpKaW4yEzMTTDvgXoRQdiicvj',
wrapper: 'Liv6LVoHkWBuJEPAGGmpzPGesXdc9YN5Eu0UaA9a9qChwB0H21V86UFBLhnIBieb',
binary: 'jVPVL8reOtRz4+v3ZZAWg8bO5m7HGJr7tsMxmvNae28TztYbHZIk8JXHeZ/82yST'
}
},
[FEATURES_ALL]: {
baseUrl: DIST_BASE,
scriptName: 'ort.all.min.js',
binaryName: 'ort-wasm-simd-threaded.jsep.wasm',
integrities: {
main: 'VVNyVdgdgHOM/8agRDy7rVx66N+/9T1vkYzwYtSS/u36YVzaln3cMtxt24ozySvr',
wrapper: 'Liv6LVoHkWBuJEPAGGmpzPGesXdc9YN5Eu0UaA9a9qChwB0H21V86UFBLhnIBieb',
binary: 'jVPVL8reOtRz4+v3ZZAWg8bO5m7HGJr7tsMxmvNae28TztYbHZIk8JXHeZ/82yST'
}
}
};

/**
* @param {string} url
* @param {'fetch' | 'script' | 'module'} as
* @param {string} [type]
* @param {string} [integrity]
*/
function preload(url, as, type, integrity) {
const el = document.createElement('link');
el.href = url;
if (as !== 'module') {
el.rel = 'preload';
el.setAttribute('as', as);
} else {
el.rel = 'modulepreload';
}
if (type) {
el.setAttribute('type', type);
}
if (integrity) {
el.setAttribute('integrity', `sha384-${integrity}`);
}
document.head.appendChild(el);
}

/**
* @param {number} features
* @returns {Promise<boolean>}
*/
export function initRuntime(features) {
if ('ort' in window && /** @type {any} */(window).ort[INIT_SYMBOL]) {
return Promise.resolve(false);
}

if (!(features in DIST)) {
return Promise.reject(new Error('Unsupported feature set'));
}

const dist = DIST[features];
/** @param {string} file */
const relative = file => new URL(file, dist.baseUrl).toString();

return new Promise((resolve, reject) => {
// since the order is load main script -> imports wrapper script -> fetches wasm, now would be a good time to
// start fetching those
preload(
relative(dist.binaryName),
'fetch',
'application/wasm',
dist.integrities.binary
);
preload(
relative(dist.wrapperName || dist.binaryName.replace(/\.wasm$/, '.mjs')),
'module',
undefined,
dist.integrities.wrapper
);

const script = document.createElement('script');
script.src = new URL(dist.binaryName, dist.baseUrl).toString();
if (dist.integrities.main) {
script.setAttribute('integrity', `sha384-${dist.integrities.main}`);
}
script.addEventListener('load', () => {
if (!('ort' in window)) {
return reject(new Error('script loaded but ort not defined'));
}

Object.defineProperty(window.ort, INIT_SYMBOL, {
value: true,
configurable: false,
enumerable: false,
writable: false
});

resolve(true);
});
script.addEventListener('error', e => {
reject(e.error);
});
document.head.appendChild(script);
});
}
52 changes: 52 additions & 0 deletions backends/web/_telemetry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const EVENT_URL = 'https://signal.pyke.io/beacon/9f5be487-d137-455a-9938-2fc7ecaa9de3/vVOv73JqP3iYRqXMBNm';

const IS_LOCALHOST = /^localhost$|^127(\.[0-9]+){0,2}\.[0-9]+$|^\[::1?\]$/;

/** @param {Uint8Array<ArrayBuffer>} payload */
function track(payload) {
if (IS_LOCALHOST.test(location.hostname) || location.protocol === 'file:') {
return false;
}
if (navigator.webdriver || 'Cypress' in window) {
return false;
}

return navigator.sendBeacon(EVENT_URL, payload.buffer);
}

/** @param {Uint8Array<ArrayBuffer>[]} chunks */
function concat(...chunks) {
const concatenated = new Uint8Array(chunks.reduce((a, b) => a + b.byteLength, 0));
let offset = 0;
for (const chunk of chunks) {
concatenated.set(chunk, offset);
offset += chunk.byteLength;
}
return concatenated;
}

/** @param {number} x */
function asUint32(x) {
const view = new DataView(new ArrayBuffer(4));
view.setUint32(0, x, true);
return new Uint8Array(view.buffer);
}

const encoder = new TextEncoder();

let hasInitializedSession = false;
export function trackSessionInit() {
if (hasInitializedSession) {
return true;
}

hasInitializedSession = true;

const hostname = location.hostname;
return track(concat(
new Uint8Array([ 0x01 ]),
new Uint8Array([ 0x90, 0x63, 0x8A, 0xE7 ]),
asUint32(hostname.length),
encoder.encode(hostname)
));
}
Loading
Loading