edgeinfer 0.0.0-stage → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +363 -2
- package/dist/index.d.mts +590 -0
- package/dist/index.d.ts +590 -0
- package/dist/index.js +1827 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1769 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -4
- package/src/download.ts +39 -0
- package/src/edgeinfer.ts +710 -0
- package/src/events.ts +17 -0
- package/src/image-processor.ts +35 -0
- package/src/index.ts +15 -0
- package/src/matryoshka.ts +441 -0
- package/src/model-cache.ts +46 -0
- package/src/onnx-lm.ts +104 -0
- package/src/runtime-manager.ts +338 -0
- package/src/speculative.ts +294 -0
- package/src/tokenizer.ts +419 -0
- package/src/types.ts +19 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 Soumya Debnath
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,364 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) 2024-2026 Soumya Debnath
|
|
4
|
+
-->
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
# EdgeInfer
|
|
7
|
+
|
|
8
|
+
<div align="center">
|
|
9
|
+
<p><strong>EdgeInfer runs small ONNX models inside the browser on WebGPU or WebAssembly, so inference costs nothing per call and user data never leaves the device.</strong></p>
|
|
10
|
+
|
|
11
|
+
[](https://opensource.org/licenses/MIT)
|
|
12
|
+
[]()
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
> **Pre-release software. Not published to npm. No production adopters yet. See [Known Limitations](#known-limitations).**
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What is EdgeInfer?
|
|
22
|
+
|
|
23
|
+
EdgeInfer is a TypeScript library that runs ONNX models in the browser using ONNX Runtime Web, with automatic fallback from WebGPU to WebAssembly SIMD to plain WASM. It includes a Tokenizer, ImageProcessor, and ModelCache for common ML preprocessing tasks.
|
|
24
|
+
|
|
25
|
+
Real exported symbols: `EdgeInfer`, `EventEmitter`, `ImageProcessor`, `ModelCache`, `RuntimeManager`, `Tokenizer`. Nothing is published under a separate npm package.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## What's new in v2
|
|
30
|
+
|
|
31
|
+
- **Two-stage Matryoshka retrieval** with an INT8 coarse scan (`d=64`) and FP32
|
|
32
|
+
re-ranking (`d=768`), persisted in IndexedDB or an in-memory store.
|
|
33
|
+
- **Speculative decoding** with `gamma=5` draft tokens verified in one target
|
|
34
|
+
forward pass, residual resampling on rejection, and cache-aware state
|
|
35
|
+
truncation.
|
|
36
|
+
- **ONNX Runtime Web causal-LM adapter** (`OnnxCausalLM`) for standard
|
|
37
|
+
`input_ids`/`attention_mask` exports.
|
|
38
|
+
- MIT licensed, no license key, 106 tests.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
EdgeInfer is **not published on npm**. `npm install edgeinfer` will fail. Install from source:
|
|
43
|
+
|
|
44
|
+
> **EdgeInfer requires `onnxruntime-web` at runtime.** It is a real dependency, not
|
|
45
|
+
> bundled: `dist/index.mjs` contains `import * as ort from "onnxruntime-web"`. Because
|
|
46
|
+
> that is a *bare module specifier*, a browser cannot resolve it on its own — loading
|
|
47
|
+
> `dist/index.mjs` straight from a CDN fails with
|
|
48
|
+
> `TypeError: Failed to resolve module specifier "onnxruntime-web"`.
|
|
49
|
+
> Use **onnxruntime-web >= 1.21.0**: in 1.17–1.20 the package's root entry point
|
|
50
|
+
> registers only the `cpu` and `wasm` execution providers, so requesting `webgpu`
|
|
51
|
+
> fails and EdgeInfer silently falls back to WASM.
|
|
52
|
+
|
|
53
|
+
**Option 1 — jsDelivr CDN (no build step). An import map is required:**
|
|
54
|
+
```html
|
|
55
|
+
<script type="importmap">
|
|
56
|
+
{
|
|
57
|
+
"imports": {
|
|
58
|
+
"onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.21.0/dist/ort.bundle.min.mjs"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
</script>
|
|
62
|
+
<script type="module">
|
|
63
|
+
import { EdgeInfer } from 'https://cdn.jsdelivr.net/gh/itsoumya-d/edgeinfer@main/dist/index.mjs';
|
|
64
|
+
</script>
|
|
65
|
+
```
|
|
66
|
+
The import map must appear **before** the module script. Without it the import throws.
|
|
67
|
+
|
|
68
|
+
**Option 2 — Clone and build (bundler resolves onnxruntime-web for you):**
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/itsoumya-d/edgeinfer.git
|
|
71
|
+
cd edgeinfer
|
|
72
|
+
npm install
|
|
73
|
+
npm run build
|
|
74
|
+
```
|
|
75
|
+
Then import from `./dist/index.mjs` or `./dist/index.js`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Release process
|
|
80
|
+
|
|
81
|
+
The package metadata is npm-ready (`exports`, `publishConfig.provenance`, `pack:check`). Publishing is tag-driven and requires the repository secret `NPM_TOKEN` with publish rights:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
git tag v2.0.0
|
|
85
|
+
git push origin v2.0.0
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`.github/workflows/release.yml` verifies that the tag matches `package.json`, runs typecheck, tests, build and `npm pack --dry-run`, publishes with npm provenance, and creates a GitHub release. The package remains **pre-release and unpublished** until that workflow is run with a valid token; no npm publication has been performed yet.
|
|
89
|
+
|
|
90
|
+
## Quick Start
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { EdgeInfer } from './dist/index.mjs';
|
|
94
|
+
|
|
95
|
+
// 1. Check WebGPU availability (optional — loads fall back to WASM automatically)
|
|
96
|
+
const caps = await EdgeInfer.getCapabilities();
|
|
97
|
+
console.log(`Running on: ${caps.provider}`); // 'webgpu' | 'wasm'
|
|
98
|
+
|
|
99
|
+
// 2. Load a model (URL must return an ONNX binary)
|
|
100
|
+
const model = await EdgeInfer.load('https://example.com/models/sentiment.onnx', {
|
|
101
|
+
tokenizerUrl: 'https://example.com/models/tokenizer.json',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// 3. Run inference
|
|
105
|
+
const result = await model.sentiment('This is great!');
|
|
106
|
+
console.log(result); // { label: 'positive', score: 0.98 }
|
|
107
|
+
|
|
108
|
+
// 4. Release resources
|
|
109
|
+
model.dispose();
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## API Reference
|
|
115
|
+
|
|
116
|
+
### `EdgeInfer` Class
|
|
117
|
+
|
|
118
|
+
#### `static async load(modelUrl: string, options?: EdgeInferOptions): Promise<EdgeInfer>`
|
|
119
|
+
Downloads (and caches via Cache API), then creates an inference session. `options.executionProviders` overrides auto-detection.
|
|
120
|
+
|
|
121
|
+
#### `static async fromBuffer(buffer: ArrayBuffer, options?: EdgeInferOptions): Promise<EdgeInfer>`
|
|
122
|
+
Creates a session from a pre-loaded model buffer. Rejects with `Error` if the buffer is not a valid ONNX model.
|
|
123
|
+
|
|
124
|
+
#### `static async isWebGPUAvailable(): Promise<boolean>`
|
|
125
|
+
Returns `true` if `navigator.gpu` is present and the adapter request succeeds.
|
|
126
|
+
|
|
127
|
+
#### `static async getCapabilities(): Promise<GPUCapabilities>`
|
|
128
|
+
Returns `{ provider, hasWebGPU, hasWebNN, estimatedVRAM, adapterLimits, vramProbe, recommendedQuantization }`. `estimatedVRAM` is the largest single storage buffer the empirical probe actually allocated, not total VRAM.
|
|
129
|
+
|
|
130
|
+
#### `async predict(inputs, shapes?): Promise<Record<string, Float32Array>>`
|
|
131
|
+
Low-level: pass raw typed arrays keyed by ONNX input name, receive raw output tensors.
|
|
132
|
+
`shapes` optionally supplies the ONNX tensor shape per input name. Inputs without an
|
|
133
|
+
explicit shape default to `[1, data.length]`, which is correct for 2-D sequence inputs
|
|
134
|
+
(`input_ids`, `attention_mask`) but **wrong for higher-rank inputs** — a vision model
|
|
135
|
+
declaring `pixel_values: [1,3,224,224]` will reject a rank-2 tensor with
|
|
136
|
+
`Invalid rank for input`. Pass `shapes` for those:
|
|
137
|
+
```js
|
|
138
|
+
await model.predict({ pixel_values: chw }, { pixel_values: [1, 3, 224, 224] });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Note on input dtype: `classify()`, `embed()` and `sentiment()` select **`tensor(int64)`**
|
|
142
|
+
automatically when the ONNX session declares `input_ids` as int64, or when
|
|
143
|
+
`int64Inputs: true` is passed at load time; otherwise they use `tensor(int32)`.
|
|
144
|
+
`predict()` still accepts either `Int32Array` or `BigInt64Array` directly.
|
|
145
|
+
|
|
146
|
+
#### `async classify(text: string): Promise<ClassificationResult[]>`
|
|
147
|
+
Requires a tokenizer (pass `tokenizerUrl` or `tokenizerConfig` in options, or call `setTokenizer()`). Throws `Error` with a clear message if no tokenizer is configured.
|
|
148
|
+
|
|
149
|
+
#### `async embed(text: string): Promise<Float32Array>`
|
|
150
|
+
Returns an L2-normalized embedding vector from the first model output. Requires tokenizer
|
|
151
|
+
and a `float32` output with one of these ONNX shapes:
|
|
152
|
+
|
|
153
|
+
- `[hidden]` or `[1, hidden]`: already pooled; preserves every embedding dimension.
|
|
154
|
+
- `[1, sequence, hidden]`: mean-pools non-masked tokens, then normalizes. The sequence
|
|
155
|
+
length must match the tokenizer's attention mask. An all-masked sequence returns zeros.
|
|
156
|
+
|
|
157
|
+
Other shapes, batched outputs, non-`float32` outputs, and sequence/mask mismatches throw
|
|
158
|
+
clear errors rather than guessing the shape from the flattened array length. Use
|
|
159
|
+
`predict()` for model-specific postprocessing; its raw flattened output API is unchanged.
|
|
160
|
+
|
|
161
|
+
#### `async embedMatryoshka(text: string, dimension: number): Promise<Float32Array>`
|
|
162
|
+
Truncates the embedding to `dimension` and re-normalizes (Matryoshka MRL).
|
|
163
|
+
|
|
164
|
+
#### `async generate(prompt: string, options?: GenerateOptions): Promise<GenerationResult>`
|
|
165
|
+
Greedy (`temperature: 0`, default) or temperature-sampled autoregressive decoding.
|
|
166
|
+
When the session exposes `present.*` outputs and matching `past_key_values.*` inputs,
|
|
167
|
+
those tensors are reused across steps (KV cache). Otherwise the full sequence is
|
|
168
|
+
re-run each step.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const result = await model.generate('Explain KV caching.', {
|
|
172
|
+
maxNewTokens: 64,
|
|
173
|
+
temperature: 0,
|
|
174
|
+
topK: 0,
|
|
175
|
+
eosTokenId: 2,
|
|
176
|
+
});
|
|
177
|
+
// result: { text: string, tokenIds: number[] }
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The decoder must expose a `logits` output (or an output whose name contains
|
|
181
|
+
`logits`). Input dtype is selected from ONNX metadata, with `int64Inputs` as an
|
|
182
|
+
explicit override.
|
|
183
|
+
|
|
184
|
+
#### `async *generateStream(prompt, options?): AsyncGenerator<GenerationEvent>`
|
|
185
|
+
Yields `{ tokenId, text, index }` as each token is chosen. The KV-cache loop is
|
|
186
|
+
the same as `generate()`; an optional `signal` aborts between decode steps.
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
for await (const event of model.generateStream('Explain KV caching.', {
|
|
190
|
+
maxNewTokens: 64,
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
})) {
|
|
193
|
+
process.stdout.write(event.text);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
#### `async chat(messages, options?): Promise<ChatResult>`
|
|
198
|
+
Collects a chat completion from `ChatMessage[]` (`system`, `user`, `assistant`).
|
|
199
|
+
The default prompt template is `<|role|>content` lines followed by
|
|
200
|
+
`<|assistant|>`; pass `chatTemplate` to match the target tokenizer.
|
|
201
|
+
|
|
202
|
+
#### `async *chatStream(messages, options?): AsyncGenerator<GenerationEvent>`
|
|
203
|
+
Streaming form of `chat()`.
|
|
204
|
+
|
|
205
|
+
#### `async sentiment(text: string): Promise<{ label: string; score: number }>`
|
|
206
|
+
Returns `{ label: 'positive'|'negative'|'neutral', score }`. Requires tokenizer.
|
|
207
|
+
|
|
208
|
+
#### `async classifyImage(imageData: ImageData): Promise<ClassificationResult[]>`
|
|
209
|
+
Expects browser `ImageData`. Converts to `Float32Array` via `ImageProcessor`.
|
|
210
|
+
|
|
211
|
+
#### `async detectObjects(imageData: ImageData): Promise<Detection[]>`
|
|
212
|
+
Returns `{ bbox, label, score }[]` for detections above 0.5 confidence.
|
|
213
|
+
|
|
214
|
+
#### `dispose(): void`
|
|
215
|
+
Releases the ONNX session and resets the provider cache.
|
|
216
|
+
|
|
217
|
+
### `RuntimeManager`
|
|
218
|
+
|
|
219
|
+
#### `static async detectCapabilities(): Promise<GPUCapabilities>`
|
|
220
|
+
In Node.js (no `navigator.gpu`): returns `{ provider: 'wasm', hasWebGPU: false, ... }` without throwing.
|
|
221
|
+
|
|
222
|
+
#### `static resetCache(): void`
|
|
223
|
+
Clears the cached capabilities so the next call re-probes the environment.
|
|
224
|
+
|
|
225
|
+
### `Tokenizer`
|
|
226
|
+
|
|
227
|
+
Constructor takes `{ vocab, merges?, specialTokens?, maxLength?, padTokenId?, unkTokenId?, clsTokenId?, sepTokenId?, bpeSpaceMarker? }`. Note: `vocab` is the key — not `vocabulary`.
|
|
228
|
+
|
|
229
|
+
`encode(text, { addSpecialTokens, maxLength, padding })` returns
|
|
230
|
+
`{ inputIds: Int32Array, attentionMask: Int32Array, inputIdsInt64: BigInt64Array, attentionMaskInt64: BigInt64Array }`.
|
|
231
|
+
Both dtypes are always produced, so callers can feed either `tensor(int32)` or
|
|
232
|
+
`tensor(int64)` models without re-encoding.
|
|
233
|
+
|
|
234
|
+
When `merges` is present the tokenizer applies rank-ordered BPE merges and
|
|
235
|
+
**preserves case by default**; without merges it uses WordPiece and retains the
|
|
236
|
+
legacy uncased default. Pass `lowercase: true` to force uncased input, or
|
|
237
|
+
`bpeSpaceMarker: '▁' | 'Ġ'` for SentencePiece/byte-level vocabularies. Empty input
|
|
238
|
+
returns a single pad token rather than an empty array (which would crash the ONNX
|
|
239
|
+
session).
|
|
240
|
+
|
|
241
|
+
### `ImageProcessor`
|
|
242
|
+
|
|
243
|
+
`static imageDataToFloat32Array(imageData, options?)` converts an RGBA `ImageData`-like object to a CHW `Float32Array` in `[0,1]` range. Works in Node with a mock `ImageData` struct `{ data: Uint8ClampedArray, width, height }`.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Browser-local retrieval and decoding
|
|
248
|
+
|
|
249
|
+
### Matryoshka hybrid RAG
|
|
250
|
+
|
|
251
|
+
`MatryoshkaIndex` stores full FP32 unit vectors and a truncated INT8 view. A
|
|
252
|
+
query scans every chunk at 64 dimensions, then re-scores the top 50 candidates
|
|
253
|
+
at 768 dimensions. Vectors can be persisted in IndexedDB:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import { MatryoshkaIndex, IndexedDbVectorStore } from 'edgeinfer';
|
|
257
|
+
|
|
258
|
+
const store = new IndexedDbVectorStore();
|
|
259
|
+
const index = new MatryoshkaIndex({ dimensions: 768, coarseDimension: 64, store });
|
|
260
|
+
await index.build(records); // records: { id, text, embedding: Float32Array(768) }[]
|
|
261
|
+
|
|
262
|
+
const results = index.search(queryEmbedding, { topK: 10 });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
For a text query, wrap any embedding function:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { createMatryoshkaEmbedder } from 'edgeinfer';
|
|
269
|
+
const embed = createMatryoshkaEmbedder((text) => engine.embed(text));
|
|
270
|
+
const query64 = await embed('refund policy', 64);
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Synthetic benchmark on an Apple Silicon laptop (Node 26, 100,000 chunks at 768
|
|
274
|
+
dimensions): build 1.76 s; coarse scan + re-rank p50 25.1 ms / p95 59.4 ms;
|
|
275
|
+
top-10 hit rate 0.90; Recall@10 rises from 0.725 at `d=64` to 1.000 at `d=768`.
|
|
276
|
+
This is a synthetic workload, not an MTEB evaluation. Reproduce with
|
|
277
|
+
`npm run bench:matryoshka`.
|
|
278
|
+
|
|
279
|
+
### Speculative decoding
|
|
280
|
+
|
|
281
|
+
`SpeculativeDecoder` samples `gamma` tokens from a draft `LanguageModel`, asks
|
|
282
|
+
the target for one forward pass over those tokens, accepts each token with
|
|
283
|
+
probability `min(1, p_target/q_draft)`, resamples from the residual
|
|
284
|
+
`max(0, p_target - q_draft)` on rejection, and truncates both model states.
|
|
285
|
+
`MockLanguageModel` is included for tests and throughput analysis. `OnnxCausalLM`
|
|
286
|
+
adapts a standard causal-LM ONNX session; it recomputes the full sequence per
|
|
287
|
+
call, so a cache-aware adapter can implement `LanguageModel.truncate` for KV
|
|
288
|
+
caching without changing the decoder.
|
|
289
|
+
|
|
290
|
+
Mock benchmark (`gamma=5`, draft 0.2 ms/forward, target 2.0 ms/forward):
|
|
291
|
+
68.4% acceptance, 0.45 target calls per generated token, and a modeled 1.64x
|
|
292
|
+
speedup over target-only decoding. This is an analytical mock result, not a
|
|
293
|
+
WebGPU measurement. Reproduce with `npm run bench:speculative`.
|
|
294
|
+
|
|
295
|
+
## Performance
|
|
296
|
+
|
|
297
|
+
**Claimed benchmark (from original README): M2 MacBook Air, Chrome 120, WebGPU**
|
|
298
|
+
- MobileBERT: ~4ms | MiniLM: ~8ms | MobileNetV3: ~6ms | YOLOv8 Nano: ~15ms | Phi-3-mini: ~25 tok/s
|
|
299
|
+
|
|
300
|
+
**These numbers cannot be reproduced in this environment.** WebGPU is not available in Node.js. A meaningful benchmark requires a real browser with WebGPU support and a loaded ONNX model. The claims above are removed from the main README to avoid misleading users. If you have browser benchmark results to contribute, open a PR with your hardware spec, browser version, and methodology.
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Known Limitations
|
|
305
|
+
|
|
306
|
+
- **Pre-release, no npm package.** Use jsDelivr or clone from source.
|
|
307
|
+
- **WebGPU requires a real browser.** `RuntimeManager.detectCapabilities()` returns `{ provider: 'wasm', hasWebGPU: false }` in Node.js — not an error, just WASM fallback.
|
|
308
|
+
- **No WebGPU = WASM execution.** On browsers without WebGPU (Firefox without flag, older Chromium), all inference runs on WASM. This is slower but still functional.
|
|
309
|
+
- **Model loading requires fetch.** `EdgeInfer.load()` calls `fetch()`. In Node.js, you need Node 18+ (native fetch) or a polyfill. WASM file loading may also require the `wasm-unsafe-eval` Content-Security-Policy directive in the browser.
|
|
310
|
+
- **No text methods without a tokenizer.** `classify()`, `embed()`, `sentiment()` throw a clear `Error` if no tokenizer is configured: `EdgeInfer: No tokenizer configured. Either pass tokenizerUrl/tokenizerConfig...`. This is not a silent hang.
|
|
311
|
+
- **Performance claims deleted.** The original README listed latency numbers (`~4ms`, `~25 tokens/sec`) measured on an M2 MacBook with Chrome WebGPU. These cannot be verified in a CI environment with no GPU. The table has been removed.
|
|
312
|
+
- **Bundle size claim "< 5KB" is incorrect.** `dist/index.mjs` is ~24KB before gzip. `onnxruntime-web` is a real runtime **dependency** (not a peer dependency) and adds ~1.5MB plus the `.wasm` binaries.
|
|
313
|
+
- **CDN use needs an import map.** See Installation. `dist/index.mjs` imports the bare specifier `onnxruntime-web`, which browsers cannot resolve unaided.
|
|
314
|
+
- **`onnxruntime-web` must be >= 1.21.0** for the WebGPU execution provider to be registered by the root entry point; `package.json` declares `^1.21.0`.
|
|
315
|
+
- **`estimatedVRAM` is an allocation probe, not total VRAM.** `RuntimeManager.probeVRAM()` binary-searches the largest single storage buffer the device will allocate, bounded by `adapter.limits.maxBufferSize`, and destroys each buffer immediately. The result is a maximum-tensor-size budget. WebGPU exposes no total-VRAM API, and browser baselines are commonly 256 MiB regardless of the physical card. The probe is unit-tested with a mocked adapter; a real WebGPU measurement is not available in this CI environment.
|
|
316
|
+
- **Generation supports cache-aware ONNX exports.** `generate()`, `generateStream()`, `chat()`, and `chatStream()` reuse `present.*` / `past_key_values.*` tensors when the model exposes them. Architectures with a different cache layout need a custom adapter. The loop is unit-tested with a deterministic mock session; no real decoder model is benchmarked in CI.
|
|
317
|
+
- **Paged GPUBuffer-resident KV is not implemented.** ONNX Runtime Web owns the tensors it returns; the JS SDK cannot pin `past_key_values` in a `GPUBuffer` with zero CPU readback. A true paged-attention path needs a custom WebGPU transformer kernel, which is outside this ONNX-runtime wrapper. The current path is cache reuse at the ONNX tensor boundary, not a custom paged kernel.
|
|
318
|
+
- **No quantization.** `recommendedQuantization` is a string hint; EdgeInfer never quantizes weights. Models must be pre-quantized.
|
|
319
|
+
- **No GGUF, no audio.** ONNX only. No Whisper/speech pipeline exists.
|
|
320
|
+
- **Tokenizer covers WordPiece and merge-table BPE.** Byte-level pre-tokenization (GPT-2 regex and `Ġ` handling) and full SentencePiece normalization pipelines are not reproduced; supply `bpeSpaceMarker` and a compatible vocabulary. Unsupported normalizers fail rather than silently change token IDs. BPE merges are applied; the earlier "BPE not implemented" note was stale.
|
|
321
|
+
- **Downloads have per-transfer deadlines, not whole-load deadlines.** Model/tokenizer headers and bodies default to 120 seconds each and support cancellation. Runtime initialization and inference are not covered. Partial HTTP model responses are rejected; range assembly is not implemented.
|
|
322
|
+
- **No production adopters yet.** APIs may change without notice.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Competitor Comparison
|
|
327
|
+
|
|
328
|
+
| Feature | EdgeInfer | TensorFlow.js | Transformers.js |
|
|
329
|
+
|---|---|---|---|
|
|
330
|
+
| Core Engine | ONNX Runtime Web | Custom / WebGL | ONNX Runtime Web |
|
|
331
|
+
| WebGPU Support | First-class | Experimental | First-class |
|
|
332
|
+
| npm availability | Not published | Published | Published |
|
|
333
|
+
| Production-ready | Pre-release | Yes | Yes |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## 📄 License
|
|
338
|
+
|
|
339
|
+
**[MIT](LICENSE)** — free to use, copy, modify, merge, publish, distribute, sublicense, and sell,
|
|
340
|
+
including in commercial and closed-source products. Keep the copyright and permission notice.
|
|
341
|
+
|
|
342
|
+
Contributions are accepted under the MIT License.
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
### Bounded downloads and tokenizer casing
|
|
346
|
+
|
|
347
|
+
`EdgeInfer.load(url, { signal, downloadTimeoutMs })` passes cancellation and a
|
|
348
|
+
per-transfer deadline to model and tokenizer downloads. `ModelCache.getModel`
|
|
349
|
+
accepts these options as its third argument and `Tokenizer.fromUrl` as its second.
|
|
350
|
+
The default is 120,000 ms per transfer, including response headers and the full
|
|
351
|
+
body. Use a finite positive value no larger than 2,147,483,647 ms. This is not an
|
|
352
|
+
inference/session-creation timeout. A tokenizer failure or cancellation after
|
|
353
|
+
session creation releases the session. Browser Cache API operations cannot be
|
|
354
|
+
physically cancelled, but callers still receive cancellation and no partial
|
|
355
|
+
network body is committed; an already-started complete cache write may finish.
|
|
356
|
+
|
|
357
|
+
WordPiece tokenization remains lowercase by default. Set `lowercase: false` in
|
|
358
|
+
`TokenizerConfig` to preserve case. Loaded HuggingFace JSON supports an explicit
|
|
359
|
+
null normalizer (identity), `Lowercase`, or a `BertNormalizer` with explicit
|
|
360
|
+
`lowercase` and all of `strip_accents`, `clean_text`, `handle_chinese_chars` false.
|
|
361
|
+
Other normalizers are rejected, not silently approximated. Omitted normalizers
|
|
362
|
+
retain the legacy lowercase default. Merge-table BPE is implemented; accent
|
|
363
|
+
stripping, Unicode normalization, and full HuggingFace tokenizer compatibility are
|
|
364
|
+
not.
|