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 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
- # Temporary Holding Version
1
+ <!--
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) 2024-2026 Soumya Debnath
4
+ -->
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
+ [![Status](https://img.shields.io/badge/status-pre--release-orange.svg)]()
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.