alias3-web: 4-bit WebGPU graph, readout files, library and demo
Browse files- .gitattributes +5 -0
- README.md +210 -0
- aliasweb.mjs +211 -0
- assets/bis-overview.png +3 -0
- assets/demo.png +3 -0
- heads/head.bin +3 -0
- heads/norm.bin +3 -0
- index.html +199 -0
- model/chat_template.jinja +158 -0
- model/genai_config.json +68 -0
- model/heads/head.bin +3 -0
- model/model.onnx +3 -0
- model/model.onnx.data +3 -0
- model/tok/tokenizer.json +3 -0
- model/tok/tokenizer_config.json +34 -0
- model/tokenizer.json +3 -0
- model/tokenizer_config.json +34 -0
- serve.py +69 -0
- vendor/ct0-edge.LICENSE +21 -0
- vendor/ct0.js +2246 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/bis-overview.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/demo.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
model/model.onnx.data filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
model/tok/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
model/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
base_model:
|
| 6 |
+
- aliasrobotics/alias3-micro-1026
|
| 7 |
+
base_model_relation: finetune
|
| 8 |
+
pipeline_tag: text-generation
|
| 9 |
+
library_name: onnx
|
| 10 |
+
tags:
|
| 11 |
+
- cybersecurity
|
| 12 |
+
- security
|
| 13 |
+
- onnx
|
| 14 |
+
- onnxruntime-genai
|
| 15 |
+
- webgpu
|
| 16 |
+
- browser
|
| 17 |
+
- int4
|
| 18 |
+
- dual-system
|
| 19 |
+
- decision-head
|
| 20 |
+
- system-1
|
| 21 |
+
- system-2
|
| 22 |
+
- phishing
|
| 23 |
+
- malware-detection
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
# alias3-web
|
| 27 |
+
|
| 28 |
+
**A cybersecurity transformer that runs in the browser.** It answers a typed security question in about
|
| 29 |
+
150 ms and writes an explanation on request, on the user's GPU through WebGPU. The page under test never
|
| 30 |
+
leaves the machine.
|
| 31 |
+
|
| 32 |
+
<p align="center">
|
| 33 |
+
<img src="assets/bis-overview.png" alt="alias3-web inside the Browser Immune System: a page is reduced to typed observations, the model answers one directed question per threat class, and explains a verdict on request" width="920">
|
| 34 |
+
</p>
|
| 35 |
+
|
| 36 |
+
alias3-web is the model behind the **Browser Immune System (BIS)**, a browser extension that screens every
|
| 37 |
+
page a user visits. Both are described in the paper *Browser Immune System (BIS): Innate and Adaptive
|
| 38 |
+
Threat Detection in the Browser with a Cybersecurity Transformer* (Mayoral-Vilches and Worrel, 2026), where
|
| 39 |
+
the model is called `alias-web`. The figure above is the paper's overview: the model sits in the centre lane.
|
| 40 |
+
|
| 41 |
+
It serves two regimes from one loaded set of 4-bit weights:
|
| 42 |
+
|
| 43 |
+
| Regime | What it does | Cost on a laptop GPU |
|
| 44 |
+
|---|---|---|
|
| 45 |
+
| **System 1** | One typed question, one forward pass, no token decoded: a probability per option. | 152 ms per question |
|
| 46 |
+
| **System 2** | Ordinary text generation from the same weights, on request. | 10.5 tokens/s |
|
| 47 |
+
|
| 48 |
+
> **Research artefact.** Released to make the paper reproducible. Read the limits below before relying on
|
| 49 |
+
> its answers.
|
| 50 |
+
|
| 51 |
+
## What is in this repository
|
| 52 |
+
|
| 53 |
+
| Path | What it is |
|
| 54 |
+
|---|---|
|
| 55 |
+
| `model/model.onnx`, `model/model.onnx.data` | The 4-bit graph for WebGPU (ONNX Runtime GenAI, 2,440,274,944 bytes of weights). It returns logits and the hidden states at requested positions. |
|
| 56 |
+
| `model/tokenizer.json`, `model/tokenizer_config.json`, `model/chat_template.jinja`, `model/genai_config.json` | Tokenizer and runtime configuration. `model/tok/` and `model/heads/` hold copies where the runner looks for them. |
|
| 57 |
+
| `heads/head.bin` | The complete pointer head (1.3 M parameters, fp16, 2,622,545 bytes): weights and biases of both projections. |
|
| 58 |
+
| `heads/norm.bin` | The weight of the model's final RMSNorm (10,289 bytes). The graph exports hidden states before that norm and the head was trained after it. |
|
| 59 |
+
| `aliasweb.mjs` | The library: the typed encoding, the norm, the head, and the calls `ask()` and `generate()`. |
|
| 60 |
+
| `index.html`, `serve.py` | A demo page and a local server with HTTP Range support. |
|
| 61 |
+
| `vendor/ct0.js`, `vendor/ct0-edge.LICENSE` | The prefill engine: the distribution of [ct0-edge](https://github.com/eduardoworrel/ct0-edge) by Eduardo Worrel (MIT), unchanged. |
|
| 62 |
+
|
| 63 |
+
## Quick start
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
hf download aliasrobotics/alias3-web --local-dir alias3-web
|
| 67 |
+
cd alias3-web && python3 serve.py 8924
|
| 68 |
+
# open http://127.0.0.1:8924 in Chrome or Edge and press "Load model"
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
The first load reads 2.44 GB from the local server and takes a few seconds. The demo page fetches its two
|
| 72 |
+
runtime modules (ONNX Runtime Web 1.30.0 and Transformers.js 3.7.5) from a public CDN; the model, the
|
| 73 |
+
material you paste and the answers stay on your machine.
|
| 74 |
+
|
| 75 |
+
From your own page, with the same import map as `index.html`:
|
| 76 |
+
|
| 77 |
+
```js
|
| 78 |
+
import { AliasWeb } from "./aliasweb.mjs";
|
| 79 |
+
const model = await AliasWeb.load({ base: "./" });
|
| 80 |
+
|
| 81 |
+
// System 1: a directed yes/no question about a piece of evidence
|
| 82 |
+
const r = await model.ask({
|
| 83 |
+
kind: "noul",
|
| 84 |
+
state: `const q = new URLSearchParams(location.search).get("q");
|
| 85 |
+
document.getElementById("out").innerHTML = "result: " + q;`,
|
| 86 |
+
q: "Is user input written into the DOM without escaping?",
|
| 87 |
+
});
|
| 88 |
+
console.log(r.p, r.ms); // 0.822, about 150 ms
|
| 89 |
+
|
| 90 |
+
// System 2: an explanation, only when it is worth seconds
|
| 91 |
+
const t = await model.generate({
|
| 92 |
+
state: "...the same evidence...",
|
| 93 |
+
prompt: "You are a security reviewer. Name the vulnerability in this code and the fix, in 2-3 sentences.",
|
| 94 |
+
maxNewTokens: 90,
|
| 95 |
+
});
|
| 96 |
+
console.log(t.text, t.tokPerS);
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## The typed interface
|
| 100 |
+
|
| 101 |
+
A decision is one causal sequence, `<state> s <q> q <opt> o1 </opt> … <opt> oK </opt> <decide>`. The markers
|
| 102 |
+
are not new vocabulary: they are five special tokens of the base tokenizer, and the library writes them by
|
| 103 |
+
id.
|
| 104 |
+
|
| 105 |
+
| Marker | Token | Id | Role |
|
| 106 |
+
|---|---|---|---|
|
| 107 |
+
| `<state>` | `<\|fim_prefix\|>` | 248060 | opens the evidence |
|
| 108 |
+
| `<q>` | `<\|fim_middle\|>` | 248061 | opens the question |
|
| 109 |
+
| `<opt>` | `<\|box_start\|>` | 248049 | opens an option |
|
| 110 |
+
| `</opt>` | `<\|box_end\|>` | 248050 | closes an option: the key is read here |
|
| 111 |
+
| `<decide>` | `<\|fim_suffix\|>` | 248062 | closes the row: the query is read here |
|
| 112 |
+
|
| 113 |
+
The head scores option *i* as `q(h_decide) · k(h_opt_i) / (sqrt(256) · T)` on the normalised hidden states
|
| 114 |
+
and a softmax over the options gives the probabilities. Text supplied by the caller is sanitised (a literal
|
| 115 |
+
`<|name|>` becomes `<¦name¦>`), so that the evidence cannot forge an option boundary.
|
| 116 |
+
|
| 117 |
+
| `kind` | Options | Returns | Status |
|
| 118 |
+
|---|---|---|---|
|
| 119 |
+
| `noul` | Yes, No | `p`, the probability of yes | the kind to use |
|
| 120 |
+
| `choice` | your list | `choice`, `top` | answers in the categories of this checkpoint's training, see the limits |
|
| 121 |
+
| `score` | 1 to 5 | `level`, `expected` | orders severity only weakly, see the limits |
|
| 122 |
+
|
| 123 |
+
## Measurements
|
| 124 |
+
|
| 125 |
+
All on one Apple M3 Max laptop (128 GB) in Chrome with WebGPU, the weights served from the local server.
|
| 126 |
+
|
| 127 |
+
| Quantity | Value |
|
| 128 |
+
|---|---|
|
| 129 |
+
| Model ready, second load in the same browser | 2.33 s |
|
| 130 |
+
| System 1, per question on short code snippets | 152 ms mean of twelve (110 to 200 ms); the head itself takes 1.18 ms |
|
| 131 |
+
| System 2 | 10.5 tokens/s, about 96 ms per token |
|
| 132 |
+
| A generated answer against a decision on the same snippet | 6 to 39 times the cost |
|
| 133 |
+
|
| 134 |
+
One directed yes/no question per snippet:
|
| 135 |
+
|
| 136 |
+
| Snippet | Question | P(yes) |
|
| 137 |
+
|---|---|---|
|
| 138 |
+
| DOM XSS: `innerHTML` with a query parameter | Is user input written into the DOM without escaping? | **0.822** |
|
| 139 |
+
| SQL injection: string concatenation in `db.query` | Does this code concatenate user input into a SQL query? | **1.000** |
|
| 140 |
+
| Command injection: `exec("curl " + url)` | Does this code pass user input to a shell command? | **1.000** |
|
| 141 |
+
| Path traversal: `sendFile("/var/data/" + f)` | Can user input escape the intended directory in this file path? | 0.428 |
|
| 142 |
+
| Hard-coded secret: an API key in the source | Does this code contain a hardcoded secret? | **1.000** |
|
| 143 |
+
| Safe: pure arithmetic | Is user input written into the DOM without escaping? | **0.000** |
|
| 144 |
+
|
| 145 |
+
Before publication the files of this repository were loaded in Chrome for Testing 157 and asked the same
|
| 146 |
+
six questions: every probability above was reproduced to the third decimal. A first load in a fresh
|
| 147 |
+
browser profile took 2.5 to 4.0 s.
|
| 148 |
+
|
| 149 |
+
The question is the lever. On the DOM XSS snippet the generic question "Does this code contain a security
|
| 150 |
+
vulnerability?" returns 0.040 and the directed one 0.822: same head, same evidence. Ask one directed
|
| 151 |
+
question per class.
|
| 152 |
+
|
| 153 |
+
In BIS, a battery of seven such questions returned the expected page verdict on 7 of the 8 sites of the
|
| 154 |
+
paper's benchmark, in 0.9 to 6.4 s per page, with identical verdicts across samples and across Chrome and
|
| 155 |
+
Firefox.
|
| 156 |
+
|
| 157 |
+
## Limits
|
| 158 |
+
|
| 159 |
+
- **WebGPU only.** The 4-bit graph has no CPU path. Measured in Chrome; Firefox Nightly with a window runs
|
| 160 |
+
it 5.9 times slower; Safari and Windows are unmeasured.
|
| 161 |
+
- **Text inside the evidence can move the answer.** In the paper's benchmark a comment that declared a
|
| 162 |
+
script harmless took a miner detection from 1.00 to 0.00. Treat the state as untrusted input.
|
| 163 |
+
- **Questions need calibration before they filter anything.** Two of the seven BIS questions, one about
|
| 164 |
+
scripts loaded from another server and one about keystroke capture asked of a login form, answer yes on
|
| 165 |
+
ordinary pages.
|
| 166 |
+
- **`choice` and `score` speak the language of this checkpoint's training,** the context categories of
|
| 167 |
+
capture-the-flag campaigns. Asked to pick among weakness classes, `choice` returns the same label for any
|
| 168 |
+
injectable snippet, and `score` is not a calibrated severity. Use `noul`.
|
| 169 |
+
- **System 2 needs a forced prefix.** The 4-bit checkpoint does not close its deliberation block on its
|
| 170 |
+
own; `generate()` starts the answer after the closing marker. A directive prompt and a temperature of 0.3
|
| 171 |
+
avoid narrated pseudo-deliberation.
|
| 172 |
+
- **Agreement with the full-precision weights was checked on one prompt,** where the 4-bit graph returned
|
| 173 |
+
the same decision direction. How often the two precisions agree is not measured.
|
| 174 |
+
- **One machine.** There is no measurement on a modest integrated GPU.
|
| 175 |
+
|
| 176 |
+
## Provenance
|
| 177 |
+
|
| 178 |
+
- **Parent.** [alias3-micro](https://huggingface.co/aliasrobotics/alias3-micro-1026), a 4B cybersecurity
|
| 179 |
+
transformer on a `Qwen3.5-4B` backbone (32 layers, hidden width 2560, a 248,320-token vocabulary),
|
| 180 |
+
described in the first paper cited below.
|
| 181 |
+
- **What alias3-web adds to the weights.** Two rank-16 adapters, fused: the parent's decision adapter,
|
| 182 |
+
trained together with the pointer head, and a context-category sensor trained for two epochs on 12,576
|
| 183 |
+
replayed decision examples and 4,172 context-category examples, with the head frozen.
|
| 184 |
+
- **The graph.** ONNX Runtime GenAI model builder 0.18 with `-p int4 -e webgpu` and
|
| 185 |
+
`include_hidden_states=true exclude_mtp=true`, run on Linux x86_64, following the recipe of the edge
|
| 186 |
+
study by Eduardo Worrel that this work reproduces and extends.
|
| 187 |
+
- **Not included.** The fused full-precision checkpoint (8.4 GB) and the BIS extension are not part of this
|
| 188 |
+
repository.
|
| 189 |
+
|
| 190 |
+
## License
|
| 191 |
+
|
| 192 |
+
Apache-2.0 for the model files, the library, the demo page and the server, as for the parent model. The
|
| 193 |
+
vendored runner in `vendor/` is MIT; its licence file is beside it.
|
| 194 |
+
|
| 195 |
+
## Citation
|
| 196 |
+
|
| 197 |
+
```bibtex
|
| 198 |
+
@article{mayoralvilches2026bis,
|
| 199 |
+
author = {Mayoral-Vilches, V{\'i}ctor and Worrel, Eduardo},
|
| 200 |
+
title = {Browser Immune System ({BIS}): Innate and Adaptive Threat Detection in the Browser with a Cybersecurity Transformer},
|
| 201 |
+
year = {2026}
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
@article{mayoralvilches2026transformer,
|
| 205 |
+
author = {Mayoral-Vilches, V{\'i}ctor},
|
| 206 |
+
title = {{CT0}: Cybersecurity Transformer},
|
| 207 |
+
journal = {arXiv preprint arXiv:2610.16550},
|
| 208 |
+
year = {2026}
|
| 209 |
+
}
|
| 210 |
+
```
|
aliasweb.mjs
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// alias3-web (alias-web in the paper) · a cybersecurity transformer in the browser (WebGPU).
|
| 2 |
+
//
|
| 3 |
+
// Two regimes from one loaded 4-bit graph:
|
| 4 |
+
// System 1 ask() one typed question, one prefill, no token decoded: a probability per option
|
| 5 |
+
// System 2 generate() text from the same weights, token by token, on request
|
| 6 |
+
//
|
| 7 |
+
// The prefill engine is the vendored runner (vendor/ct0.js: ct0-edge by Eduardo Worrel, MIT, see
|
| 8 |
+
// vendor/ct0-edge.LICENSE), unchanged. This file adds the readout the model was trained with:
|
| 9 |
+
//
|
| 10 |
+
// · the System 1 markers as the model's own special tokens, by id:
|
| 11 |
+
// <state> = <|fim_prefix|> <q> = <|fim_middle|> <opt> = <|box_start|>
|
| 12 |
+
// </opt> = <|box_end|> <decide> = <|fim_suffix|>
|
| 13 |
+
// · the final RMSNorm (heads/norm.bin): the graph exports hidden states before it
|
| 14 |
+
// · the complete pointer head (heads/head.bin), read as in training:
|
| 15 |
+
// z_i = q(h_<decide>) · k(h_</opt>_i) / (sqrt(256) · T), softmax over the options
|
| 16 |
+
// · yes/no and severity as choices over ("Yes", "No") and ("1" … "5"): one mechanism for every question
|
| 17 |
+
//
|
| 18 |
+
// The page that imports this module needs an import map for the runner and its two runtimes; see index.html.
|
| 19 |
+
|
| 20 |
+
export { AliasWeb };
|
| 21 |
+
|
| 22 |
+
const MARKERS = {
|
| 23 |
+
state: "<|fim_prefix|>",
|
| 24 |
+
q: "<|fim_middle|>",
|
| 25 |
+
opt: "<|box_start|>",
|
| 26 |
+
optEnd: "<|box_end|>",
|
| 27 |
+
decide: "<|fim_suffix|>",
|
| 28 |
+
};
|
| 29 |
+
const LEVELS = ["1", "2", "3", "4", "5"];
|
| 30 |
+
export const DEFAULT_SCORE_Q = "How severe is the security risk in this content?";
|
| 31 |
+
|
| 32 |
+
// ── head.bin: [u32 lenJSON][json][fp16 q][fp16 qb][fp16 k][fp16 kb] ────────
|
| 33 |
+
function f16(h) {
|
| 34 |
+
const s = (h & 0x8000) >> 15, e = (h & 0x7c00) >> 10, f = h & 0x03ff;
|
| 35 |
+
if (e === 0) return (s ? -1 : 1) * Math.pow(2, -14) * (f / 1024);
|
| 36 |
+
if (e === 0x1f) return f ? NaN : s ? -Infinity : Infinity;
|
| 37 |
+
return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024);
|
| 38 |
+
}
|
| 39 |
+
export function loadHead(buf) {
|
| 40 |
+
const dv = new DataView(buf);
|
| 41 |
+
const jl = dv.getUint32(0, true);
|
| 42 |
+
const meta = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, jl)));
|
| 43 |
+
let off = 4 + jl;
|
| 44 |
+
const take = (n) => { const f = new Float32Array(n); for (let i = 0; i < n; i++) f[i] = f16(dv.getUint16(off + i * 2, true)); off += n * 2; return f; };
|
| 45 |
+
const d = meta.headDim, h = meta.hiddenSize;
|
| 46 |
+
return { q: take(d * h), qb: take(d), k: take(d * h), kb: take(d), headDim: d, hiddenSize: h,
|
| 47 |
+
temperature: meta.temperature ?? 1.0, scale: meta.scale ?? 1 / Math.sqrt(d), scoreOffset: meta.score_offset ?? 0 };
|
| 48 |
+
}
|
| 49 |
+
// norm.bin: [u32 lenJSON][json {hiddenSize}][fp32 hiddenSize]
|
| 50 |
+
async function loadNorm(buf) {
|
| 51 |
+
const dv = new DataView(buf);
|
| 52 |
+
const jl = dv.getUint32(0, true);
|
| 53 |
+
const meta = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, jl)));
|
| 54 |
+
const n = meta.hiddenSize, off = 4 + jl, f = new Float32Array(n);
|
| 55 |
+
for (let i = 0; i < n; i++) f[i] = dv.getFloat32(off + i * 4, true);
|
| 56 |
+
return f;
|
| 57 |
+
}
|
| 58 |
+
function proj(W, b, x, dim, hidden) {
|
| 59 |
+
const y = new Float32Array(dim);
|
| 60 |
+
for (let d = 0; d < dim; d++) { let a = b[d]; const r = d * hidden; for (let i = 0; i < hidden; i++) a += W[r + i] * x[i]; y[d] = a; }
|
| 61 |
+
return y;
|
| 62 |
+
}
|
| 63 |
+
const dot = (a, b) => { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; };
|
| 64 |
+
const headLogit = (hd, hD, hO) => dot(proj(hd.q, hd.qb, hD, hd.headDim, hd.hiddenSize), proj(hd.k, hd.kb, hO, hd.headDim, hd.hiddenSize)) * hd.scale / hd.temperature + (hd.scoreOffset || 0);
|
| 65 |
+
const softmax = (xs) => {
|
| 66 |
+
const mx = Math.max(...xs.filter(Number.isFinite));
|
| 67 |
+
const e = xs.map((x) => (Number.isFinite(x) ? Math.exp(x - mx) : 0));
|
| 68 |
+
const z = e.reduce((a, b) => a + b, 0) || 1;
|
| 69 |
+
return e.map((v) => v / z);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
// text supplied by the caller can never produce a delimiter: <|name|> becomes <¦name¦>, as in training
|
| 73 |
+
const SPECIAL_RE = /<\|([A-Za-z0-9_]+)\|>/g;
|
| 74 |
+
const sanitize = (t) => t.replace(SPECIAL_RE, "<¦$1¦>");
|
| 75 |
+
|
| 76 |
+
class AliasWeb {
|
| 77 |
+
constructor(tok, prefill, head, mid, normW) { this.tok = tok; this.prefill = prefill; this.head = head; this.mid = mid; this.normW = normW; }
|
| 78 |
+
|
| 79 |
+
// the graph exports hidden_states before the final norm; the head was trained on the normalised
|
| 80 |
+
// state, so the model's final RMSNorm is applied here
|
| 81 |
+
norm(h) {
|
| 82 |
+
if (!this.normW) return h;
|
| 83 |
+
let s = 0; for (let i = 0; i < h.length; i++) s += h[i] * h[i];
|
| 84 |
+
const inv = 1 / Math.sqrt(s / h.length);
|
| 85 |
+
const o = new Float32Array(h.length);
|
| 86 |
+
for (let i = 0; i < h.length; i++) o[i] = h[i] * inv * this.normW[i];
|
| 87 |
+
return o;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
static async load(opts = {}) {
|
| 91 |
+
const base = opts.base ?? "./";
|
| 92 |
+
const status = opts.status ?? (() => { });
|
| 93 |
+
status("loading the runner and the 4-bit graph (2.4 GB)…");
|
| 94 |
+
const edge = await import("ct0-edge");
|
| 95 |
+
const inner = await edge.CT0.load({
|
| 96 |
+
runtime: "genai",
|
| 97 |
+
genaiBaseUrl: base + "model/",
|
| 98 |
+
genaiModel: "model.onnx",
|
| 99 |
+
baseUrl: base + "model/",
|
| 100 |
+
status: () => { },
|
| 101 |
+
verifyEp: false, // placement was verified once against this graph in Chrome
|
| 102 |
+
skipVersionCheck: true, // the import map pins the ONNX Runtime version
|
| 103 |
+
genai: { warmup: false },
|
| 104 |
+
});
|
| 105 |
+
const be = inner.backend ?? inner.be;
|
| 106 |
+
if (!be?.prefill) throw new Error("the runner did not expose its prefill backend");
|
| 107 |
+
status("loading the head and the final norm…");
|
| 108 |
+
const head = loadHead(await (await fetch(base + "heads/head.bin")).arrayBuffer());
|
| 109 |
+
const normW = await loadNorm(await (await fetch(base + "heads/norm.bin")).arrayBuffer());
|
| 110 |
+
// marker ids from the tokenizer's added_tokens
|
| 111 |
+
const tk = await (await fetch(base + "model/tokenizer.json")).json();
|
| 112 |
+
const added = new Map((tk.added_tokens ?? []).map((t) => [t.content, t.id]));
|
| 113 |
+
const mid = {};
|
| 114 |
+
for (const [k, m] of Object.entries(MARKERS)) {
|
| 115 |
+
const id = added.get(m);
|
| 116 |
+
if (typeof id !== "number") throw new Error("marker without a token id: " + m);
|
| 117 |
+
mid[k] = id;
|
| 118 |
+
}
|
| 119 |
+
const model = new AliasWeb(inner.tok?.tokenizer ?? inner.tok, (ids, positions) => be.prefill(ids, { positions }), head, mid, normW);
|
| 120 |
+
model.be = be; model.base = base;
|
| 121 |
+
status("warm-up…");
|
| 122 |
+
const w = model.encode("warmup", "ok?", ["Yes", "No"]);
|
| 123 |
+
await model.prefill(w.ids, []);
|
| 124 |
+
status("ready");
|
| 125 |
+
return model;
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
encode(state, question, options) {
|
| 129 |
+
const t = this.tok;
|
| 130 |
+
const idsOf = (text) => Array.from(t.encode(sanitize(text), { add_special_tokens: false }), Number);
|
| 131 |
+
const ids = [this.mid.state, ...idsOf(state), this.mid.q, ...idsOf(question)];
|
| 132 |
+
const optEnds = [];
|
| 133 |
+
for (const o of options) {
|
| 134 |
+
ids.push(this.mid.opt, ...idsOf(o));
|
| 135 |
+
optEnds.push(ids.length);
|
| 136 |
+
ids.push(this.mid.optEnd);
|
| 137 |
+
}
|
| 138 |
+
ids.push(this.mid.decide);
|
| 139 |
+
return { ids, decide: ids.length - 1, optEnds };
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
async ask(q) {
|
| 143 |
+
const t0 = performance.now();
|
| 144 |
+
let options, question, labels;
|
| 145 |
+
if (q.kind === "noul") { options = ["Yes", "No"]; question = q.q ?? ""; labels = ["yes", "no"]; }
|
| 146 |
+
else if (q.kind === "score") { options = LEVELS; question = q.q ?? DEFAULT_SCORE_Q; labels = LEVELS; }
|
| 147 |
+
else { options = q.options; question = q.q ?? ""; labels = options; }
|
| 148 |
+
const enc = this.encode(q.state, question, options);
|
| 149 |
+
const p0 = performance.now();
|
| 150 |
+
const { hiddens } = await this.prefill(enc.ids, [enc.decide, ...enc.optEnds]);
|
| 151 |
+
const prefillMs = performance.now() - p0;
|
| 152 |
+
const hDecide = this.norm(hiddens[0]), perOpt = hiddens.slice(1).map((h) => this.norm(h));
|
| 153 |
+
const raws = perOpt.map((h) => headLogit(this.head, hDecide, h));
|
| 154 |
+
const ps = softmax(raws);
|
| 155 |
+
const out = { kind: q.kind, ms: performance.now() - t0, prefillMs, nTokens: enc.ids.length, raws: {} };
|
| 156 |
+
labels.forEach((l, i) => out.raws[l] = raws[i]);
|
| 157 |
+
if (q.kind === "noul") out.p = ps[0];
|
| 158 |
+
else if (q.kind === "score") {
|
| 159 |
+
out.per = Object.fromEntries(labels.map((l, i) => [l, ps[i]]));
|
| 160 |
+
out.level = 1 + ps.indexOf(Math.max(...ps));
|
| 161 |
+
out.expected = ps.reduce((a, p, i) => a + p * (i + 1), 0);
|
| 162 |
+
} else {
|
| 163 |
+
out.choice = Object.fromEntries(labels.map((l, i) => [l, ps[i]]));
|
| 164 |
+
out.top = labels[ps.indexOf(Math.max(...ps))];
|
| 165 |
+
out.p = Math.max(...ps);
|
| 166 |
+
}
|
| 167 |
+
return out;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
// ── System 2: text from the same 4-bit weights ─────────────────────────
|
| 171 |
+
// The graph exposes logits; the runner's backend samples from them (end tokens <|im_end|> and
|
| 172 |
+
// <|endoftext|>).
|
| 173 |
+
async generate(q) {
|
| 174 |
+
const t0 = performance.now();
|
| 175 |
+
const state = q.state ?? "";
|
| 176 |
+
const prompt = (q.prompt ?? "").trim();
|
| 177 |
+
const user = state ? prompt + "\n\n" + state : prompt;
|
| 178 |
+
const t = this.tok;
|
| 179 |
+
const idsOf = (text) => Array.from(t.encode(sanitize(text), { add_special_tokens: false }), Number);
|
| 180 |
+
if (!this._added) {
|
| 181 |
+
const tk = await (await fetch(this.base + "model/tokenizer.json")).json();
|
| 182 |
+
this._added = new Map((tk.added_tokens ?? []).map((x) => [x.content, x.id]));
|
| 183 |
+
}
|
| 184 |
+
const imS = this._added.get("<|im_start|>"), imE = this._added.get("<|im_end|>");
|
| 185 |
+
// The base family deliberates by default and this checkpoint in 4-bit does not close the block:
|
| 186 |
+
// the assistant turn is forced to start after the closing marker. The special tokens go in by id,
|
| 187 |
+
// never through idsOf(): the sanitiser would rewrite them.
|
| 188 |
+
const body = idsOf("user\n" + user);
|
| 189 |
+
const asst = q.forceNoThink === false
|
| 190 |
+
? [imS, ...idsOf("assistant\n")]
|
| 191 |
+
: [imS, ...idsOf("assistant\n</think>\n\n")];
|
| 192 |
+
const ids = [imS, ...body, imE, ...asst];
|
| 193 |
+
const out = [];
|
| 194 |
+
const tt0 = performance.now();
|
| 195 |
+
for await (const tokId of this.be.generate(ids, {
|
| 196 |
+
maxNewTokens: q.maxNewTokens ?? 96,
|
| 197 |
+
temperature: q.temperature ?? 0.7,
|
| 198 |
+
topP: q.topP ?? 0.9,
|
| 199 |
+
})) {
|
| 200 |
+
out.push(tokId);
|
| 201 |
+
if (q.onToken) q.onToken(t.decode([tokId], { skip_special_tokens: true }), out.length);
|
| 202 |
+
}
|
| 203 |
+
const genMs = performance.now() - tt0;
|
| 204 |
+
return {
|
| 205 |
+
text: t.decode(out, { skip_special_tokens: true, clean_up_tokenization_spaces: false }).trim(),
|
| 206 |
+
tokens: out.length, genMs,
|
| 207 |
+
tokPerS: +(out.length / (genMs / 1000)).toFixed(1),
|
| 208 |
+
ms: performance.now() - t0,
|
| 209 |
+
};
|
| 210 |
+
}
|
| 211 |
+
}
|
assets/bis-overview.png
ADDED
|
Git LFS Details
|
assets/demo.png
ADDED
|
Git LFS Details
|
heads/head.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f586cb80f9f369c0d2ca00763e9402a883d7c759b712d2f81434a6a2e7e7495b
|
| 3 |
+
size 2622545
|
heads/norm.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a0ae161d2fa1902a031434126a904fe665a5ebff488b8740d0de495b93b90ce
|
| 3 |
+
size 10289
|
index.html
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 6 |
+
<title>alias3-web · a cybersecurity transformer in your browser</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --ink:#16202A; --muted:#5B6875; --rule:#D6DCE2; --bg:#F4F6F8; --card:#FFF;
|
| 9 |
+
--accent:#1F4E8C; --accent-soft:#E3EBF6; --good:#0ca30c; --mono:ui-monospace,Menlo,Consolas,monospace; }
|
| 10 |
+
* { box-sizing:border-box }
|
| 11 |
+
body { margin:0; background:var(--bg); color:var(--ink);
|
| 12 |
+
font:15px/1.5 -apple-system,"Segoe UI",Roboto,sans-serif; }
|
| 13 |
+
main { max-width:900px; margin:0 auto; padding:28px 18px 80px }
|
| 14 |
+
h1 { font-size:1.5rem; margin:.2em 0 .3em }
|
| 15 |
+
h1 small { color:var(--muted); font-weight:400; font-size:.95rem }
|
| 16 |
+
.sub { color:var(--muted); margin:0 0 18px }
|
| 17 |
+
.card { background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:16px 18px; margin:14px 0 }
|
| 18 |
+
label { font-size:.85rem; color:var(--muted); display:block; margin-bottom:4px }
|
| 19 |
+
textarea, input[type=text] { width:100%; border:1px solid var(--rule); border-radius:8px; padding:8px 10px;
|
| 20 |
+
font:12.5px/1.45 var(--mono); background:#fbfcfd; color:var(--ink) }
|
| 21 |
+
textarea { min-height:120px; resize:vertical }
|
| 22 |
+
.row { display:flex; gap:10px; flex-wrap:wrap; align-items:flex-end }
|
| 23 |
+
.row > div { flex:1; min-width:180px }
|
| 24 |
+
button { background:var(--accent); color:#fff; border:0; border-radius:8px; padding:10px 18px;
|
| 25 |
+
font-size:.95rem; cursor:pointer; margin:12px 8px 0 0 }
|
| 26 |
+
button.secondary { background:var(--accent-soft); color:var(--accent) }
|
| 27 |
+
button:disabled { opacity:.45; cursor:wait }
|
| 28 |
+
#status { font-size:.9rem; color:var(--muted); margin-top:10px }
|
| 29 |
+
.result { margin-top:14px }
|
| 30 |
+
.bar-row { display:flex; align-items:center; gap:10px; margin:5px 0 }
|
| 31 |
+
.bar-label { width:190px; font-size:.85rem; text-align:right; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
|
| 32 |
+
.bar-track { flex:1; background:#eef1f4; border-radius:6px; height:20px; position:relative }
|
| 33 |
+
.bar-fill { height:100%; border-radius:6px; background:var(--accent); transition:width .25s }
|
| 34 |
+
.bar-fill.win { background:var(--good) }
|
| 35 |
+
.bar-val { width:64px; font-size:.85rem; font-family:var(--mono) }
|
| 36 |
+
.kpis { display:flex; gap:12px; flex-wrap:wrap; margin-top:12px }
|
| 37 |
+
.kpi { background:var(--accent-soft); border-radius:10px; padding:10px 14px; min-width:120px }
|
| 38 |
+
.kpi b { display:block; font-size:1.5rem; color:var(--accent) }
|
| 39 |
+
.kpi span { font-size:.8rem; color:var(--muted) }
|
| 40 |
+
.pill { display:inline-block; background:var(--accent-soft); color:var(--accent); border-radius:20px;
|
| 41 |
+
padding:2px 10px; font-size:.78rem; margin:2px 4px 2px 0; font-family:var(--mono) }
|
| 42 |
+
details { margin-top:10px } summary { cursor:pointer; color:var(--muted); font-size:.85rem }
|
| 43 |
+
pre { background:#eef1f4; border-radius:8px; padding:10px; font-size:.75rem; overflow:auto }
|
| 44 |
+
.err { color:#b00020; font-family:var(--mono); font-size:.8rem; margin-top:8px; white-space:pre-wrap }
|
| 45 |
+
</style>
|
| 46 |
+
</head>
|
| 47 |
+
<body>
|
| 48 |
+
<main>
|
| 49 |
+
<h1>alias3-web <small>· typed security decisions and explanations, on your GPU (WebGPU)</small></h1>
|
| 50 |
+
<p class="sub">A 4B cybersecurity transformer quantised to 4 bits (ONNX Runtime GenAI, 2.44 GB) with the
|
| 51 |
+
<b>complete trained head</b> and the <b>model's own marker tokens</b>. Nothing leaves this page.
|
| 52 |
+
Prefill engine: <a href="https://github.com/eduardoworrel/ct0-edge">ct0-edge</a> by Eduardo Worrel (MIT), vendored.</p>
|
| 53 |
+
|
| 54 |
+
<div class="card">
|
| 55 |
+
<div class="row">
|
| 56 |
+
<div><label>State (the material to judge: code, a log, the source of a page…)</label>
|
| 57 |
+
<textarea id="state">const q = new URLSearchParams(location.search).get("q");
|
| 58 |
+
document.getElementById("out").innerHTML = "result: " + q;</textarea></div>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="row" style="margin-top:10px">
|
| 61 |
+
<div><label>Question (yes/no and choice)</label>
|
| 62 |
+
<input type="text" id="q" value="Is user input written into the DOM without escaping?"></div>
|
| 63 |
+
<div><label>Options (choice, separated by ;)</label>
|
| 64 |
+
<input type="text" id="opts" value="dom-xss; sql-injection; ssrf; path-traversal"></div>
|
| 65 |
+
</div>
|
| 66 |
+
<button id="btn-load" class="secondary">1 · Load model (2.4 GB)</button>
|
| 67 |
+
<button id="btn-noul" disabled>Yes / No</button>
|
| 68 |
+
<button id="btn-choice" disabled>Choice</button>
|
| 69 |
+
<button id="btn-score" disabled>Severity 1-5</button>
|
| 70 |
+
<button id="btn-s2" disabled>Explanation (System 2)</button>
|
| 71 |
+
<div id="status">Press “Load model”. Needs Chrome or Edge with WebGPU.</div>
|
| 72 |
+
<div id="err" class="err"></div>
|
| 73 |
+
<details>
|
| 74 |
+
<summary>the marker tokens and the readout</summary>
|
| 75 |
+
<span class="pill"><state>→<|fim_prefix|></span><span class="pill"><q>→<|fim_middle|></span>
|
| 76 |
+
<span class="pill"><opt>→<|box_start|></span><span class="pill"></opt>→<|box_end|></span>
|
| 77 |
+
<span class="pill"><decide>→<|fim_suffix|></span>
|
| 78 |
+
<p style="font-size:.82rem;color:var(--muted)">readout: q(h<decide>)·k(h</opt>ᵢ)·√256⁻¹/T, softmax over the options.
|
| 79 |
+
A yes/no question is a choice over (Yes, No) and severity a choice over (1..5): one mechanism, as trained.</p>
|
| 80 |
+
</details>
|
| 81 |
+
</div>
|
| 82 |
+
|
| 83 |
+
<div class="card result" id="result" style="display:none">
|
| 84 |
+
<div class="kpis">
|
| 85 |
+
<div class="kpi"><b id="k-top">·</b><span id="k-top-lbl">answer</span></div>
|
| 86 |
+
<div class="kpi"><b id="k-ms">·</b><span>total latency</span></div>
|
| 87 |
+
<div class="kpi"><b id="k-pref">·</b><span>prefill</span></div>
|
| 88 |
+
<div class="kpi"><b id="k-tok">·</b><span>tokens</span></div>
|
| 89 |
+
</div>
|
| 90 |
+
<div id="bars"></div>
|
| 91 |
+
<details><summary>raw</summary><pre id="raw"></pre></details>
|
| 92 |
+
</div>
|
| 93 |
+
</main>
|
| 94 |
+
|
| 95 |
+
<script type="importmap">
|
| 96 |
+
{ "imports": {
|
| 97 |
+
"ct0-edge": "./vendor/ct0.js",
|
| 98 |
+
"@huggingface/transformers": "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.7.5",
|
| 99 |
+
"onnxruntime-web/webgpu": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.30.0/dist/ort.webgpu.bundle.min.mjs",
|
| 100 |
+
"onnxruntime-web": "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.30.0/dist/ort.webgpu.bundle.min.mjs"
|
| 101 |
+
} }
|
| 102 |
+
</script>
|
| 103 |
+
<script type="module">
|
| 104 |
+
import { AliasWeb } from "./aliasweb.mjs";
|
| 105 |
+
const $ = (id) => document.getElementById(id);
|
| 106 |
+
let model = null;
|
| 107 |
+
|
| 108 |
+
function bars(entries) {
|
| 109 |
+
$("bars").innerHTML = "";
|
| 110 |
+
const max = Math.max(...entries.map(([, v]) => v));
|
| 111 |
+
for (const [label, v] of entries) {
|
| 112 |
+
const row = document.createElement("div"); row.className = "bar-row";
|
| 113 |
+
const l = document.createElement("div"); l.className = "bar-label"; l.textContent = label; l.title = label;
|
| 114 |
+
const t = document.createElement("div"); t.className = "bar-track";
|
| 115 |
+
const f = document.createElement("div"); f.className = "bar-fill" + (v === max ? " win" : "");
|
| 116 |
+
f.style.width = (v * 100).toFixed(1) + "%";
|
| 117 |
+
const val = document.createElement("div"); val.className = "bar-val"; val.textContent = (v * 100).toFixed(1) + "%";
|
| 118 |
+
t.appendChild(f); row.append(l, t, val); $("bars").appendChild(row);
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
function show(r, topLabel, tok) {
|
| 123 |
+
$("result").style.display = "";
|
| 124 |
+
$("k-top").textContent = topLabel;
|
| 125 |
+
$("k-top-lbl").textContent = r.kind === "noul" ? "P(yes)" : r.kind === "score" ? "level" : "top";
|
| 126 |
+
$("k-ms").textContent = r.ms.toFixed(0) + " ms";
|
| 127 |
+
$("k-pref").textContent = r.prefillMs.toFixed(0) + " ms";
|
| 128 |
+
$("k-tok").textContent = tok;
|
| 129 |
+
const entries = r.kind === "noul" ? [["Yes", r.p], ["No", 1 - r.p]]
|
| 130 |
+
: r.kind === "score" ? Object.entries(r.per).map(([l, p]) => ["level " + l, p])
|
| 131 |
+
: Object.entries(r.choice);
|
| 132 |
+
bars(entries);
|
| 133 |
+
$("raw").textContent = JSON.stringify({ ...r, lastLogits: undefined }, null, 1).slice(0, 2000);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
async function guard(fn) {
|
| 137 |
+
$("err").textContent = "";
|
| 138 |
+
if (!model) { $("err").textContent = "load the model first"; return; }
|
| 139 |
+
try { await fn(); } catch (e) { $("err").textContent = String(e?.stack || e); }
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
$("btn-load").onclick = async () => {
|
| 143 |
+
$("btn-load").disabled = true; $("err").textContent = "";
|
| 144 |
+
try {
|
| 145 |
+
model = await AliasWeb.load({ base: "./", status: (s) => $("status").textContent = s });
|
| 146 |
+
window.aliasweb = model;
|
| 147 |
+
for (const b of ["btn-noul", "btn-choice", "btn-score", "btn-s2"]) $(b).disabled = false;
|
| 148 |
+
$("status").textContent = "model ready · WebGPU active";
|
| 149 |
+
} catch (e) { $("err").textContent = String(e?.stack || e); $("btn-load").disabled = false; }
|
| 150 |
+
};
|
| 151 |
+
|
| 152 |
+
$("btn-noul").onclick = () => guard(async () => {
|
| 153 |
+
const state = $("state").value, q = $("q").value;
|
| 154 |
+
const r = await model.ask({ kind: "noul", state, q });
|
| 155 |
+
const enc = model.encode(state, q, ["Yes", "No"]);
|
| 156 |
+
show(r, (r.p * 100).toFixed(1) + "%", enc.ids.length);
|
| 157 |
+
});
|
| 158 |
+
|
| 159 |
+
$("btn-choice").onclick = () => guard(async () => {
|
| 160 |
+
const state = $("state").value;
|
| 161 |
+
const options = $("opts").value.split(";").map((s) => s.trim()).filter(Boolean);
|
| 162 |
+
const q = "Which vulnerability class does this code contain?";
|
| 163 |
+
const r = await model.ask({ kind: "choice", state, q, options });
|
| 164 |
+
const enc = model.encode(state, q, options);
|
| 165 |
+
show(r, r.top, enc.ids.length);
|
| 166 |
+
});
|
| 167 |
+
|
| 168 |
+
$("btn-s2").onclick = () => guard(async () => {
|
| 169 |
+
const state = $("state").value;
|
| 170 |
+
$("status").textContent = "System 2 is writing (about 10 tokens/s)…";
|
| 171 |
+
const r = await model.generate({
|
| 172 |
+
state,
|
| 173 |
+
prompt: "You are a security reviewer. Name the vulnerability in this code and the fix, in 2-3 sentences.",
|
| 174 |
+
maxNewTokens: 90, temperature: 0.7,
|
| 175 |
+
});
|
| 176 |
+
$("status").textContent = "model ready · WebGPU active";
|
| 177 |
+
$("result").style.display = "";
|
| 178 |
+
$("k-top").textContent = r.tokens + " tok";
|
| 179 |
+
$("k-top-lbl").textContent = "S2 · " + r.tokPerS + " tok/s";
|
| 180 |
+
$("k-ms").textContent = (r.ms / 1000).toFixed(1) + " s";
|
| 181 |
+
$("k-pref").textContent = "≈" + (r.ms / r.tokens).toFixed(0) + " ms/tok";
|
| 182 |
+
$("k-tok").textContent = "vs 0.15 s S1";
|
| 183 |
+
$("bars").innerHTML = "";
|
| 184 |
+
const p = document.createElement("p");
|
| 185 |
+
p.style.cssText = "font-size:.95rem;line-height:1.6";
|
| 186 |
+
p.textContent = r.text;
|
| 187 |
+
$("bars").appendChild(p);
|
| 188 |
+
$("raw").textContent = JSON.stringify({ tokens: r.tokens, tokPerS: r.tokPerS, ms: Math.round(r.ms) }, null, 1);
|
| 189 |
+
});
|
| 190 |
+
|
| 191 |
+
$("btn-score").onclick = () => guard(async () => {
|
| 192 |
+
const state = $("state").value;
|
| 193 |
+
const r = await model.ask({ kind: "score", state });
|
| 194 |
+
const enc = model.encode(state, "", ["1", "2", "3", "4", "5"]);
|
| 195 |
+
show(r, r.level + " / 5", enc.ids.length);
|
| 196 |
+
});
|
| 197 |
+
</script>
|
| 198 |
+
</body>
|
| 199 |
+
</html>
|
model/chat_template.jinja
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{%- set image_count = namespace(value=0) %}
|
| 2 |
+
{%- set video_count = namespace(value=0) %}
|
| 3 |
+
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
|
| 4 |
+
{%- if content is string %}
|
| 5 |
+
{{- content }}
|
| 6 |
+
{%- elif content is iterable and content is not mapping %}
|
| 7 |
+
{%- for item in content %}
|
| 8 |
+
{%- if 'image' in item or 'image_url' in item or item.type == 'image' %}
|
| 9 |
+
{%- if is_system_content %}
|
| 10 |
+
{{- raise_exception('System message cannot contain images.') }}
|
| 11 |
+
{%- endif %}
|
| 12 |
+
{%- if do_vision_count %}
|
| 13 |
+
{%- set image_count.value = image_count.value + 1 %}
|
| 14 |
+
{%- endif %}
|
| 15 |
+
{%- if add_vision_id %}
|
| 16 |
+
{{- 'Picture ' ~ image_count.value ~ ': ' }}
|
| 17 |
+
{%- endif %}
|
| 18 |
+
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
|
| 19 |
+
{%- elif 'video' in item or item.type == 'video' %}
|
| 20 |
+
{%- if is_system_content %}
|
| 21 |
+
{{- raise_exception('System message cannot contain videos.') }}
|
| 22 |
+
{%- endif %}
|
| 23 |
+
{%- if do_vision_count %}
|
| 24 |
+
{%- set video_count.value = video_count.value + 1 %}
|
| 25 |
+
{%- endif %}
|
| 26 |
+
{%- if add_vision_id %}
|
| 27 |
+
{{- 'Video ' ~ video_count.value ~ ': ' }}
|
| 28 |
+
{%- endif %}
|
| 29 |
+
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
|
| 30 |
+
{%- elif 'text' in item %}
|
| 31 |
+
{{- item.text }}
|
| 32 |
+
{%- else %}
|
| 33 |
+
{{- raise_exception('Unexpected item type in content.') }}
|
| 34 |
+
{%- endif %}
|
| 35 |
+
{%- endfor %}
|
| 36 |
+
{%- elif content is none or content is undefined %}
|
| 37 |
+
{{- '' }}
|
| 38 |
+
{%- else %}
|
| 39 |
+
{{- raise_exception('Unexpected content type.') }}
|
| 40 |
+
{%- endif %}
|
| 41 |
+
{%- endmacro %}
|
| 42 |
+
{%- if not messages %}
|
| 43 |
+
{{- raise_exception('No messages provided.') }}
|
| 44 |
+
{%- endif %}
|
| 45 |
+
{%- set num_sys = 0 %}
|
| 46 |
+
{%- set merged_system = '' %}
|
| 47 |
+
{%- if messages[0].role == 'system' or messages[0].role == 'developer' %}
|
| 48 |
+
{%- set first = render_content(messages[0].content, false, true)|trim %}
|
| 49 |
+
{%- if messages|length > 1 and (messages[1].role == 'system' or messages[1].role == 'developer') %}
|
| 50 |
+
{%- set second = render_content(messages[1].content, false, true)|trim %}
|
| 51 |
+
{%- set merged_system = first + '\n' + second %}
|
| 52 |
+
{%- set num_sys = 2 %}
|
| 53 |
+
{%- else %}
|
| 54 |
+
{%- set merged_system = first %}
|
| 55 |
+
{%- set num_sys = 1 %}
|
| 56 |
+
{%- endif %}
|
| 57 |
+
{%- endif %}
|
| 58 |
+
{%- if tools and tools is iterable and tools is not mapping %}
|
| 59 |
+
{{- '<|im_start|>system\n' }}
|
| 60 |
+
{{- "# Tools\n\nYou have access to the following functions:\n\n<tools>" }}
|
| 61 |
+
{%- for tool in tools %}
|
| 62 |
+
{{- "\n" }}
|
| 63 |
+
{{- tool | tojson }}
|
| 64 |
+
{%- endfor %}
|
| 65 |
+
{{- "\n</tools>" }}
|
| 66 |
+
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n</IMPORTANT>' }}
|
| 67 |
+
{%- if merged_system %}
|
| 68 |
+
{{- '\n\n' + merged_system }}
|
| 69 |
+
{%- endif %}
|
| 70 |
+
{{- '<|im_end|>\n' }}
|
| 71 |
+
{%- else %}
|
| 72 |
+
{%- if merged_system %}
|
| 73 |
+
{{- '<|im_start|>system\n' + merged_system + '<|im_end|>\n' }}
|
| 74 |
+
{%- endif %}
|
| 75 |
+
{%- endif %}
|
| 76 |
+
{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
|
| 77 |
+
{%- for message in messages[::-1] %}
|
| 78 |
+
{%- set index = (messages|length - 1) - loop.index0 %}
|
| 79 |
+
{%- if ns.multi_step_tool and message.role == "user" %}
|
| 80 |
+
{%- set content = render_content(message.content, false)|trim %}
|
| 81 |
+
{%- if not(content.startswith('<tool_response>') and content.endswith('</tool_response>')) %}
|
| 82 |
+
{%- set ns.multi_step_tool = false %}
|
| 83 |
+
{%- set ns.last_query_index = index %}
|
| 84 |
+
{%- endif %}
|
| 85 |
+
{%- endif %}
|
| 86 |
+
{%- endfor %}
|
| 87 |
+
{%- for message in messages %}
|
| 88 |
+
{%- if loop.index0 >= num_sys and message.role != "system" and message.role != "developer" %}
|
| 89 |
+
{%- set content = render_content(message.content, true)|trim %}
|
| 90 |
+
{%- if message.role == "user" %}
|
| 91 |
+
{{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
|
| 92 |
+
{%- elif message.role == "assistant" %}
|
| 93 |
+
{%- set reasoning_content = '' %}
|
| 94 |
+
{%- if message.reasoning_content is string %}
|
| 95 |
+
{%- set reasoning_content = message.reasoning_content %}
|
| 96 |
+
{%- else %}
|
| 97 |
+
{%- if '</think>' in content %}
|
| 98 |
+
{%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
|
| 99 |
+
{%- set content = content.split('</think>')[-1].lstrip('\n') %}
|
| 100 |
+
{%- endif %}
|
| 101 |
+
{%- endif %}
|
| 102 |
+
{%- set reasoning_content = reasoning_content|trim %}
|
| 103 |
+
{%- if loop.index0 > ns.last_query_index %}
|
| 104 |
+
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
|
| 105 |
+
{%- else %}
|
| 106 |
+
{{- '<|im_start|>' + message.role + '\n' + content }}
|
| 107 |
+
{%- endif %}
|
| 108 |
+
{%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
|
| 109 |
+
{%- for tool_call in message.tool_calls %}
|
| 110 |
+
{%- if tool_call.function is defined %}
|
| 111 |
+
{%- set tool_call = tool_call.function %}
|
| 112 |
+
{%- endif %}
|
| 113 |
+
{%- if loop.first %}
|
| 114 |
+
{%- if content|trim %}
|
| 115 |
+
{{- '\n\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 116 |
+
{%- else %}
|
| 117 |
+
{{- '<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 118 |
+
{%- endif %}
|
| 119 |
+
{%- else %}
|
| 120 |
+
{{- '\n<tool_call>\n<function=' + tool_call.name + '>\n' }}
|
| 121 |
+
{%- endif %}
|
| 122 |
+
{%- if tool_call.arguments is mapping %}
|
| 123 |
+
{%- for args_name in tool_call.arguments %}
|
| 124 |
+
{%- set args_value = tool_call.arguments[args_name] %}
|
| 125 |
+
{{- '<parameter=' + args_name + '>\n' }}
|
| 126 |
+
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
|
| 127 |
+
{{- args_value }}
|
| 128 |
+
{{- '\n</parameter>\n' }}
|
| 129 |
+
{%- endfor %}
|
| 130 |
+
{%- endif %}
|
| 131 |
+
{{- '</function>\n</tool_call>' }}
|
| 132 |
+
{%- endfor %}
|
| 133 |
+
{%- endif %}
|
| 134 |
+
{{- '<|im_end|>\n' }}
|
| 135 |
+
{%- elif message.role == "tool" %}
|
| 136 |
+
{%- if loop.previtem and loop.previtem.role != "tool" %}
|
| 137 |
+
{{- '<|im_start|>user' }}
|
| 138 |
+
{%- endif %}
|
| 139 |
+
{{- '\n<tool_response>\n' }}
|
| 140 |
+
{{- content }}
|
| 141 |
+
{{- '\n</tool_response>' }}
|
| 142 |
+
{%- if not loop.last and loop.nextitem.role != "tool" %}
|
| 143 |
+
{{- '<|im_end|>\n' }}
|
| 144 |
+
{%- elif loop.last %}
|
| 145 |
+
{{- '<|im_end|>\n' }}
|
| 146 |
+
{%- endif %}
|
| 147 |
+
{%- endif %}
|
| 148 |
+
{%- endif %}
|
| 149 |
+
{%- endfor %}
|
| 150 |
+
{%- if add_generation_prompt %}
|
| 151 |
+
{{- '<|im_start|>assistant\n' }}
|
| 152 |
+
{%- if enable_thinking is defined and enable_thinking is false %}
|
| 153 |
+
{{- '<think>\n\n</think>\n\n' }}
|
| 154 |
+
{%- else %}
|
| 155 |
+
{{- '<think>\n' }}
|
| 156 |
+
{%- endif %}
|
| 157 |
+
{%- endif %}
|
| 158 |
+
{#- Unsloth fixes - developer role, tool calling #}
|
model/genai_config.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": {
|
| 3 |
+
"bos_token_id": 1,
|
| 4 |
+
"context_length": 262144,
|
| 5 |
+
"decoder": {
|
| 6 |
+
"session_options": {
|
| 7 |
+
"log_id": "onnxruntime-genai",
|
| 8 |
+
"provider_options": [
|
| 9 |
+
{
|
| 10 |
+
"webgpu": {
|
| 11 |
+
"enableGraphCapture": "0",
|
| 12 |
+
"validationMode": "basic"
|
| 13 |
+
}
|
| 14 |
+
}
|
| 15 |
+
]
|
| 16 |
+
},
|
| 17 |
+
"filename": "model.onnx",
|
| 18 |
+
"head_size": 256,
|
| 19 |
+
"hidden_size": 2560,
|
| 20 |
+
"inputs": {
|
| 21 |
+
"input_ids": "input_ids",
|
| 22 |
+
"attention_mask": "attention_mask",
|
| 23 |
+
"position_ids": "position_ids",
|
| 24 |
+
"past_key_names": "past_key_values.%d.key",
|
| 25 |
+
"past_value_names": "past_key_values.%d.value",
|
| 26 |
+
"past_conv_names": "past.%d.conv",
|
| 27 |
+
"past_recurrent_names": "past.%d.recurrent"
|
| 28 |
+
},
|
| 29 |
+
"outputs": {
|
| 30 |
+
"logits": "logits",
|
| 31 |
+
"present_key_names": "present.%d.key",
|
| 32 |
+
"present_value_names": "present.%d.value",
|
| 33 |
+
"present_conv_names": "present.%d.conv",
|
| 34 |
+
"present_recurrent_names": "present.%d.recurrent"
|
| 35 |
+
},
|
| 36 |
+
"num_attention_heads": 16,
|
| 37 |
+
"num_hidden_layers": 32,
|
| 38 |
+
"num_key_value_heads": 4
|
| 39 |
+
},
|
| 40 |
+
"eos_token_id": [
|
| 41 |
+
248046,
|
| 42 |
+
248044
|
| 43 |
+
],
|
| 44 |
+
"pad_token_id": 248055,
|
| 45 |
+
"type": "qwen3_5_text",
|
| 46 |
+
"vocab_size": 248320,
|
| 47 |
+
"bot_token_id": 248058,
|
| 48 |
+
"eot_token_id": 248059,
|
| 49 |
+
"bor_token_id": 248068,
|
| 50 |
+
"eor_token_id": 248069
|
| 51 |
+
},
|
| 52 |
+
"search": {
|
| 53 |
+
"diversity_penalty": 0.0,
|
| 54 |
+
"do_sample": false,
|
| 55 |
+
"early_stopping": true,
|
| 56 |
+
"length_penalty": 1.0,
|
| 57 |
+
"max_length": 262144,
|
| 58 |
+
"min_length": 0,
|
| 59 |
+
"no_repeat_ngram_size": 0,
|
| 60 |
+
"num_beams": 1,
|
| 61 |
+
"num_return_sequences": 1,
|
| 62 |
+
"past_present_share_buffer": true,
|
| 63 |
+
"repetition_penalty": 1.0,
|
| 64 |
+
"temperature": 1.0,
|
| 65 |
+
"top_k": 50,
|
| 66 |
+
"top_p": 1.0
|
| 67 |
+
}
|
| 68 |
+
}
|
model/heads/head.bin
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f586cb80f9f369c0d2ca00763e9402a883d7c759b712d2f81434a6a2e7e7495b
|
| 3 |
+
size 2622545
|
model/model.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:94d67f6e1777aa45e6e99ef39df9c2b568801ab8c5eda91c697b18d8a8f0885d
|
| 3 |
+
size 489574
|
model/model.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6563d99a1dc847ea4ce02e0ff51da15b88fe532fae4cb3b2f6a24f0d988e0a0e
|
| 3 |
+
size 2440274944
|
model/tok/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
|
| 3 |
+
size 19989325
|
model/tok/tokenizer_config.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"audio_bos_token": "<|audio_start|>",
|
| 4 |
+
"audio_eos_token": "<|audio_end|>",
|
| 5 |
+
"audio_token": "<|audio_pad|>",
|
| 6 |
+
"backend": "tokenizers",
|
| 7 |
+
"bos_token": null,
|
| 8 |
+
"clean_up_tokenization_spaces": false,
|
| 9 |
+
"eos_token": "<|im_end|>",
|
| 10 |
+
"errors": "replace",
|
| 11 |
+
"image_token": "<|image_pad|>",
|
| 12 |
+
"is_local": true,
|
| 13 |
+
"local_files_only": false,
|
| 14 |
+
"model_max_length": 262144,
|
| 15 |
+
"model_specific_special_tokens": {
|
| 16 |
+
"audio_bos_token": "<|audio_start|>",
|
| 17 |
+
"audio_eos_token": "<|audio_end|>",
|
| 18 |
+
"audio_token": "<|audio_pad|>",
|
| 19 |
+
"image_token": "<|image_pad|>",
|
| 20 |
+
"video_token": "<|video_pad|>",
|
| 21 |
+
"vision_bos_token": "<|vision_start|>",
|
| 22 |
+
"vision_eos_token": "<|vision_end|>"
|
| 23 |
+
},
|
| 24 |
+
"pad_token": "<|vision_pad|>",
|
| 25 |
+
"padding_side": "left",
|
| 26 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 27 |
+
"processor_class": "Qwen3VLProcessor",
|
| 28 |
+
"split_special_tokens": false,
|
| 29 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 30 |
+
"unk_token": null,
|
| 31 |
+
"video_token": "<|video_pad|>",
|
| 32 |
+
"vision_bos_token": "<|vision_start|>",
|
| 33 |
+
"vision_eos_token": "<|vision_end|>"
|
| 34 |
+
}
|
model/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:06b9509352d2af50381ab2247e083b80d32d5c0aba91c272ca9ff729b6a0e523
|
| 3 |
+
size 19989325
|
model/tokenizer_config.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": false,
|
| 3 |
+
"audio_bos_token": "<|audio_start|>",
|
| 4 |
+
"audio_eos_token": "<|audio_end|>",
|
| 5 |
+
"audio_token": "<|audio_pad|>",
|
| 6 |
+
"backend": "tokenizers",
|
| 7 |
+
"bos_token": null,
|
| 8 |
+
"clean_up_tokenization_spaces": false,
|
| 9 |
+
"eos_token": "<|im_end|>",
|
| 10 |
+
"errors": "replace",
|
| 11 |
+
"image_token": "<|image_pad|>",
|
| 12 |
+
"is_local": true,
|
| 13 |
+
"local_files_only": false,
|
| 14 |
+
"model_max_length": 262144,
|
| 15 |
+
"model_specific_special_tokens": {
|
| 16 |
+
"audio_bos_token": "<|audio_start|>",
|
| 17 |
+
"audio_eos_token": "<|audio_end|>",
|
| 18 |
+
"audio_token": "<|audio_pad|>",
|
| 19 |
+
"image_token": "<|image_pad|>",
|
| 20 |
+
"video_token": "<|video_pad|>",
|
| 21 |
+
"vision_bos_token": "<|vision_start|>",
|
| 22 |
+
"vision_eos_token": "<|vision_end|>"
|
| 23 |
+
},
|
| 24 |
+
"pad_token": "<|vision_pad|>",
|
| 25 |
+
"padding_side": "left",
|
| 26 |
+
"pretokenize_regex": "(?i:'s|'t|'re|'ve|'m|'ll|'d)|[^\\r\\n\\p{L}\\p{N}]?[\\p{L}\\p{M}]+|\\p{N}| ?[^\\s\\p{L}\\p{M}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+",
|
| 27 |
+
"processor_class": "Qwen3VLProcessor",
|
| 28 |
+
"split_special_tokens": false,
|
| 29 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 30 |
+
"unk_token": null,
|
| 31 |
+
"video_token": "<|video_pad|>",
|
| 32 |
+
"vision_bos_token": "<|vision_start|>",
|
| 33 |
+
"vision_eos_token": "<|vision_end|>"
|
| 34 |
+
}
|
serve.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Static server for the alias-web bundle, with HTTP Range support for the 2.4 GB weight file.
|
| 3 |
+
|
| 4 |
+
python3 serve.py [port] # default 8924; then open http://127.0.0.1:<port> in Chrome or Edge
|
| 5 |
+
|
| 6 |
+
Binds to 127.0.0.1 only. Responses are marked no-store so that a changed module is never served stale;
|
| 7 |
+
CORS is open so that a local browser extension can fetch the model from this server.
|
| 8 |
+
"""
|
| 9 |
+
import http.server, os, re, socketserver, sys
|
| 10 |
+
|
| 11 |
+
ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Handler(http.server.SimpleHTTPRequestHandler):
|
| 15 |
+
def __init__(self, *a, **kw):
|
| 16 |
+
super().__init__(*a, directory=ROOT, **kw)
|
| 17 |
+
|
| 18 |
+
def send_head(self):
|
| 19 |
+
self._remaining = None
|
| 20 |
+
path = self.translate_path(self.path)
|
| 21 |
+
rng = self.headers.get("Range")
|
| 22 |
+
if not rng or not os.path.isfile(path):
|
| 23 |
+
return super().send_head()
|
| 24 |
+
size = os.path.getsize(path)
|
| 25 |
+
m = re.match(r"bytes=(\d*)-(\d*)$", rng.strip())
|
| 26 |
+
if not m or not (m.group(1) or m.group(2)):
|
| 27 |
+
self.send_error(416)
|
| 28 |
+
return None
|
| 29 |
+
if m.group(1):
|
| 30 |
+
start, end = int(m.group(1)), min(int(m.group(2)) if m.group(2) else size - 1, size - 1)
|
| 31 |
+
else: # suffix form: the last N bytes
|
| 32 |
+
start, end = max(0, size - int(m.group(2))), size - 1
|
| 33 |
+
if start > end:
|
| 34 |
+
self.send_error(416)
|
| 35 |
+
return None
|
| 36 |
+
f = open(path, "rb")
|
| 37 |
+
f.seek(start)
|
| 38 |
+
self._remaining = end - start + 1
|
| 39 |
+
self.send_response(206)
|
| 40 |
+
self.send_header("Content-Type", self.guess_type(path))
|
| 41 |
+
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
|
| 42 |
+
self.send_header("Content-Length", str(self._remaining))
|
| 43 |
+
self.end_headers()
|
| 44 |
+
return f
|
| 45 |
+
|
| 46 |
+
def copyfile(self, source, outputfile):
|
| 47 |
+
if self._remaining is None:
|
| 48 |
+
return super().copyfile(source, outputfile)
|
| 49 |
+
left = self._remaining
|
| 50 |
+
while left > 0:
|
| 51 |
+
chunk = source.read(min(1 << 20, left))
|
| 52 |
+
if not chunk:
|
| 53 |
+
break
|
| 54 |
+
outputfile.write(chunk)
|
| 55 |
+
left -= len(chunk)
|
| 56 |
+
|
| 57 |
+
def end_headers(self):
|
| 58 |
+
self.send_header("Accept-Ranges", "bytes")
|
| 59 |
+
self.send_header("Cache-Control", "no-store")
|
| 60 |
+
self.send_header("Access-Control-Allow-Origin", "*")
|
| 61 |
+
super().end_headers()
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
socketserver.TCPServer.allow_reuse_address = True
|
| 65 |
+
if __name__ == "__main__":
|
| 66 |
+
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8924
|
| 67 |
+
with socketserver.ThreadingTCPServer(("127.0.0.1", port), Handler) as s:
|
| 68 |
+
print(f"serving {ROOT} on http://127.0.0.1:{port}")
|
| 69 |
+
s.serve_forever()
|
vendor/ct0-edge.LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 eduardoworrel
|
| 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.
|
vendor/ct0.js
ADDED
|
@@ -0,0 +1,2246 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// src/backend.ts
|
| 2 |
+
var GENERATE_DEFAULTS = { maxNewTokens: 512, temperature: 0.7, topP: 0.9, topK: 40, repetitionPenalty: 1.1, seed: 0 };
|
| 3 |
+
function resolveGenerateOptions(o = {}) {
|
| 4 |
+
const num = (v, d) => typeof v === "number" && Number.isFinite(v) ? v : d;
|
| 5 |
+
const D = GENERATE_DEFAULTS;
|
| 6 |
+
const temperature = num(o.temperature, D.temperature);
|
| 7 |
+
return {
|
| 8 |
+
maxNewTokens: Math.max(0, Math.floor(num(o.maxNewTokens, D.maxNewTokens))),
|
| 9 |
+
temperature,
|
| 10 |
+
topP: num(o.topP, D.topP),
|
| 11 |
+
topK: num(o.topK, D.topK),
|
| 12 |
+
repetitionPenalty: num(o.repetitionPenalty, temperature <= 0 ? 1 : D.repetitionPenalty),
|
| 13 |
+
seed: num(o.seed, D.seed),
|
| 14 |
+
signal: o.signal
|
| 15 |
+
};
|
| 16 |
+
}
|
| 17 |
+
function checkContinuation(name, h, ids, positions) {
|
| 18 |
+
const k = h.ids.length;
|
| 19 |
+
if (ids.length <= k || h.ids.some((x, i) => ids[i] !== x)) throw new Error(`${name}: prefillFrom ids must extend the snapshot prefix (${k} tokens) by at least one token`);
|
| 20 |
+
for (const p of positions) if (!Number.isInteger(p) || p < k || p >= ids.length) throw new Error(`${name}: position ${p} outside the continuation [${k},${ids.length})`);
|
| 21 |
+
return k;
|
| 22 |
+
}
|
| 23 |
+
function createGate(name) {
|
| 24 |
+
let tail = Promise.resolve();
|
| 25 |
+
let disposed = false;
|
| 26 |
+
let disposing = null;
|
| 27 |
+
const err = () => new Error(`${name}: backend already disposed`);
|
| 28 |
+
const lock = async () => {
|
| 29 |
+
const prev = tail;
|
| 30 |
+
let rel;
|
| 31 |
+
tail = new Promise((r) => {
|
| 32 |
+
rel = r;
|
| 33 |
+
});
|
| 34 |
+
await prev;
|
| 35 |
+
return rel;
|
| 36 |
+
};
|
| 37 |
+
return {
|
| 38 |
+
get disposed() {
|
| 39 |
+
return disposed;
|
| 40 |
+
},
|
| 41 |
+
assertLive() {
|
| 42 |
+
if (disposed) throw err();
|
| 43 |
+
},
|
| 44 |
+
async acquire() {
|
| 45 |
+
if (disposed) throw err();
|
| 46 |
+
return lock();
|
| 47 |
+
},
|
| 48 |
+
dispose(release) {
|
| 49 |
+
if (!disposing) {
|
| 50 |
+
disposed = true;
|
| 51 |
+
disposing = (async () => {
|
| 52 |
+
const rel = await lock();
|
| 53 |
+
try {
|
| 54 |
+
await release();
|
| 55 |
+
} finally {
|
| 56 |
+
rel();
|
| 57 |
+
}
|
| 58 |
+
})();
|
| 59 |
+
}
|
| 60 |
+
return disposing;
|
| 61 |
+
}
|
| 62 |
+
};
|
| 63 |
+
}
|
| 64 |
+
var gates = /* @__PURE__ */ new WeakMap();
|
| 65 |
+
function gateFor(shared, name) {
|
| 66 |
+
let g = gates.get(shared);
|
| 67 |
+
if (!g) {
|
| 68 |
+
g = createGate(name);
|
| 69 |
+
gates.set(shared, g);
|
| 70 |
+
}
|
| 71 |
+
return g;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
// src/ortVersion.ts
|
| 75 |
+
function checkOrtVersion(v, min = [1, 30]) {
|
| 76 |
+
const m = /^(\d+)\.(\d+)/.exec(v ?? "");
|
| 77 |
+
if (!m) return;
|
| 78 |
+
if (Number(m[1]) < min[0] || Number(m[1]) === min[0] && Number(m[2]) < min[1])
|
| 79 |
+
throw new Error(`ct0-edge: onnxruntime-web ${v} is too old; requires >= ${min.join(".")} (WebGPU returns NaN below that; the genai backend needs the LinearAttention/CausalConvWithState kernels). Pass skipVersionCheck to ignore.`);
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
// src/backends/decodeOnly.ts
|
| 83 |
+
var EOS = /* @__PURE__ */ new Set([248046, 248044]);
|
| 84 |
+
var HIDDEN_NAMES = ["/model/norm/Mul_1_output_0", "mul_13450", "hidden_states", "type_as_80"];
|
| 85 |
+
var F16 = globalThis.Float16Array;
|
| 86 |
+
function mulberry32(seed) {
|
| 87 |
+
let a = seed >>> 0 || 1;
|
| 88 |
+
return () => {
|
| 89 |
+
a |= 0;
|
| 90 |
+
a = a + 1831565813 | 0;
|
| 91 |
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
| 92 |
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
| 93 |
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
| 94 |
+
};
|
| 95 |
+
}
|
| 96 |
+
function halfToNumber(h) {
|
| 97 |
+
const s = h & 32768 ? -1 : 1, e = (h & 31744) >> 10, f = h & 1023;
|
| 98 |
+
return e === 0 ? s * Math.pow(2, -14) * (f / 1024) : e === 31 ? f ? NaN : s * Infinity : s * Math.pow(2, e - 15) * (1 + f / 1024);
|
| 99 |
+
}
|
| 100 |
+
function tensorToF32(data, type, off = 0, n = data.length - off) {
|
| 101 |
+
const out = new Float32Array(n);
|
| 102 |
+
const isF16Arr = typeof F16 !== "undefined" && data instanceof F16;
|
| 103 |
+
if (data instanceof Float32Array || isF16Arr) {
|
| 104 |
+
for (let i = 0; i < n; i++) out[i] = Number(data[off + i]);
|
| 105 |
+
} else if (type === "float16" || data instanceof Uint16Array) {
|
| 106 |
+
for (let i = 0; i < n; i++) out[i] = halfToNumber(data[off + i]);
|
| 107 |
+
} else for (let i = 0; i < n; i++) out[i] = Number(data[off + i]);
|
| 108 |
+
return out;
|
| 109 |
+
}
|
| 110 |
+
function lastRow(L) {
|
| 111 |
+
const V = L.dims.at(-1), off = (L.dims.at(-2) - 1) * V;
|
| 112 |
+
return tensorToF32(L.data, L.type, off, V);
|
| 113 |
+
}
|
| 114 |
+
function sample(logits, gen, rand, o) {
|
| 115 |
+
const { temperature: temp, topP, topK, repetitionPenalty: rep } = o;
|
| 116 |
+
if (rep !== 1) for (const t of new Set(gen)) logits[t] = logits[t] > 0 ? logits[t] / rep : logits[t] * rep;
|
| 117 |
+
if (temp <= 0) {
|
| 118 |
+
let bi = 0, bv = -1e30;
|
| 119 |
+
for (let i = 0; i < logits.length; i++) if (logits[i] > bv) {
|
| 120 |
+
bv = logits[i];
|
| 121 |
+
bi = i;
|
| 122 |
+
}
|
| 123 |
+
return bi;
|
| 124 |
+
}
|
| 125 |
+
let idx = Array.from(logits.keys());
|
| 126 |
+
idx.sort((a, b) => logits[b] - logits[a]);
|
| 127 |
+
if (topK > 0) idx = idx.slice(0, topK);
|
| 128 |
+
const mx = logits[idx[0]];
|
| 129 |
+
let probs = idx.map((i) => Math.exp((logits[i] - mx) / temp));
|
| 130 |
+
let s = probs.reduce((a, b) => a + b, 0);
|
| 131 |
+
probs = probs.map((p) => p / s);
|
| 132 |
+
if (topP < 1) {
|
| 133 |
+
let c = 0, cut = idx.length;
|
| 134 |
+
for (let i = 0; i < idx.length; i++) {
|
| 135 |
+
c += probs[i];
|
| 136 |
+
if (c >= topP) {
|
| 137 |
+
cut = i + 1;
|
| 138 |
+
break;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
idx = idx.slice(0, cut);
|
| 142 |
+
probs = probs.slice(0, cut);
|
| 143 |
+
const z = probs.reduce((a, b) => a + b, 0);
|
| 144 |
+
probs = probs.map((p) => p / z);
|
| 145 |
+
}
|
| 146 |
+
let r = rand(), acc = 0;
|
| 147 |
+
for (let i = 0; i < idx.length; i++) {
|
| 148 |
+
acc += probs[i];
|
| 149 |
+
if (r <= acc) return idx[i];
|
| 150 |
+
}
|
| 151 |
+
return idx[idx.length - 1];
|
| 152 |
+
}
|
| 153 |
+
function pastNameFor(out) {
|
| 154 |
+
let m;
|
| 155 |
+
if (m = out.match(/^present_conv\.(\d+)$/)) return "past_conv." + m[1];
|
| 156 |
+
if (m = out.match(/^present_recurrent\.(\d+)$/)) return "past_recurrent." + m[1];
|
| 157 |
+
if (m = out.match(/^present\.(\d+)\.(key|value)$/)) return `past_key_values.${m[1]}.${m[2]}`;
|
| 158 |
+
if (m = out.match(/^present\.(\d+)\.(conv|recurrent)$/)) return `past.${m[1]}.${m[2]}`;
|
| 159 |
+
if (m = out.match(/^so(\d+)$/)) return "si" + m[1];
|
| 160 |
+
return null;
|
| 161 |
+
}
|
| 162 |
+
function cacheDims(name, shape, pastLen) {
|
| 163 |
+
const isKV = /^past_key_values\./.test(name) || shape.length === 4 && !/conv|recurrent/.test(name) && typeof shape[2] !== "number";
|
| 164 |
+
return shape.map((d, i) => typeof d === "number" && d >= 0 ? d : isKV && i === 2 ? pastLen : 1);
|
| 165 |
+
}
|
| 166 |
+
async function createDecodeOnly(session, tok, opts) {
|
| 167 |
+
if (!opts.skipVersionCheck) checkOrtVersion(opts.ort.env?.versions?.web);
|
| 168 |
+
const T = opts.ort.Tensor;
|
| 169 |
+
const pastLen = opts.pastLen ?? 1;
|
| 170 |
+
const inNames = session.inputNames, outNames = session.outputNames;
|
| 171 |
+
if (inNames.includes("attention_mask")) throw new Error("decode-only: a graph with attention_mask is not decode-only");
|
| 172 |
+
const meta = Array.isArray(session.inputMetadata) ? session.inputMetadata : Object.entries(session.inputMetadata ?? {}).map(([name, m]) => ({ name, ...m }));
|
| 173 |
+
const metaBy = new Map(meta.map((m) => [m.name, m]));
|
| 174 |
+
const specs = [];
|
| 175 |
+
for (const n of inNames) {
|
| 176 |
+
if (n === "input_ids" || n === "cache_position") continue;
|
| 177 |
+
const m = metaBy.get(n);
|
| 178 |
+
if (!m?.shape) throw new Error("decode-only: no shape metadata for input " + n);
|
| 179 |
+
specs.push({ name: n, type: m.type, dims: cacheDims(n, m.shape, pastLen) });
|
| 180 |
+
}
|
| 181 |
+
const pastOf = /* @__PURE__ */ new Map();
|
| 182 |
+
for (const o of outNames) {
|
| 183 |
+
const p = pastNameFor(o);
|
| 184 |
+
if (p && inNames.includes(p)) pastOf.set(o, p);
|
| 185 |
+
}
|
| 186 |
+
const missing = specs.filter((s) => ![...pastOf.values()].includes(s.name));
|
| 187 |
+
if (missing.length) throw new Error("decode-only: cache inputs without a matching present output: " + missing.slice(0, 3).map((s) => s.name).join(", "));
|
| 188 |
+
const hiddenName = HIDDEN_NAMES.find((n) => outNames.includes(n)) ?? null;
|
| 189 |
+
const hasCachePos = inNames.includes("cache_position");
|
| 190 |
+
const zeros = (s) => {
|
| 191 |
+
const n = s.dims.reduce((a, b) => a * b, 1);
|
| 192 |
+
if (s.type === "float32") return new T("float32", new Float32Array(n), s.dims);
|
| 193 |
+
if (s.type === "float16") return new T("float16", F16 ? new F16(n) : new Uint16Array(n), s.dims);
|
| 194 |
+
throw new Error(`decode-only: unsupported cache dtype: ${s.type} (${s.name})`);
|
| 195 |
+
};
|
| 196 |
+
const initState = () => {
|
| 197 |
+
const st = {};
|
| 198 |
+
for (const s of specs) st[s.name] = zeros(s);
|
| 199 |
+
return st;
|
| 200 |
+
};
|
| 201 |
+
const fetchBase = ["logits", ...pastOf.keys()];
|
| 202 |
+
async function step(tid, pos, st, wantHidden) {
|
| 203 |
+
const feed2 = { input_ids: new T("int64", BigInt64Array.from([BigInt(tid)]), [1, 1]), ...st };
|
| 204 |
+
if (hasCachePos) feed2.cache_position = new T("int64", BigInt64Array.from([BigInt(pos)]), [1]);
|
| 205 |
+
const names = wantHidden && hiddenName ? [...fetchBase, hiddenName] : fetchBase;
|
| 206 |
+
const r = await session.run(feed2, names);
|
| 207 |
+
const ns = {};
|
| 208 |
+
for (const [o, p] of pastOf) ns[p] = r[o];
|
| 209 |
+
const hidden = wantHidden && hiddenName ? tensorToF32(r[hiddenName].data, r[hiddenName].type) : void 0;
|
| 210 |
+
return { logits: r.logits, st: ns, hidden };
|
| 211 |
+
}
|
| 212 |
+
async function feed(ids, from, st, want) {
|
| 213 |
+
const wanted = new Set(want.positions), at = /* @__PURE__ */ new Map();
|
| 214 |
+
let last = null;
|
| 215 |
+
for (let i = from; i < ids.length; i++) {
|
| 216 |
+
const r = await step(ids[i], i, st, wanted.has(i));
|
| 217 |
+
st = r.st;
|
| 218 |
+
last = r.logits;
|
| 219 |
+
if (r.hidden) at.set(i, r.hidden);
|
| 220 |
+
}
|
| 221 |
+
return { hiddens: want.positions.map((p) => at.get(p)), lastLogits: lastRow(last) };
|
| 222 |
+
}
|
| 223 |
+
const checkHidden = (want) => {
|
| 224 |
+
if (want.positions.length && !hiddenName) throw new Error("decode-only: the graph does not expose a hidden state (outputs: " + outNames.slice(-3).join(", ") + ")");
|
| 225 |
+
};
|
| 226 |
+
const gate = createGate("decode-only");
|
| 227 |
+
const snaps = /* @__PURE__ */ new WeakMap();
|
| 228 |
+
return {
|
| 229 |
+
kind: "decode-only",
|
| 230 |
+
hiddenName,
|
| 231 |
+
dispose() {
|
| 232 |
+
return gate.dispose(() => session.release?.());
|
| 233 |
+
},
|
| 234 |
+
async prefill(ids, want) {
|
| 235 |
+
gate.assertLive();
|
| 236 |
+
checkHidden(want);
|
| 237 |
+
for (const p of new Set(want.positions)) if (p < 0 || p >= ids.length) throw new Error(`prefill: position ${p} out of [0,${ids.length})`);
|
| 238 |
+
if (!ids.length) throw new Error("prefill: empty ids");
|
| 239 |
+
const rel = await gate.acquire();
|
| 240 |
+
try {
|
| 241 |
+
return await feed(ids, 0, initState(), want);
|
| 242 |
+
} finally {
|
| 243 |
+
rel();
|
| 244 |
+
}
|
| 245 |
+
},
|
| 246 |
+
async snapshot(ids) {
|
| 247 |
+
gate.assertLive();
|
| 248 |
+
if (!ids.length) throw new Error("decode-only: snapshot of empty ids");
|
| 249 |
+
const rel = await gate.acquire();
|
| 250 |
+
try {
|
| 251 |
+
let st = initState();
|
| 252 |
+
for (let i = 0; i < ids.length; i++) st = (await step(ids[i], i, st, false)).st;
|
| 253 |
+
const h = { ids: ids.slice(), dispose() {
|
| 254 |
+
snaps.delete(h);
|
| 255 |
+
} };
|
| 256 |
+
snaps.set(h, st);
|
| 257 |
+
return h;
|
| 258 |
+
} finally {
|
| 259 |
+
rel();
|
| 260 |
+
}
|
| 261 |
+
},
|
| 262 |
+
async prefillFrom(h, ids, want) {
|
| 263 |
+
gate.assertLive();
|
| 264 |
+
const st = snaps.get(h);
|
| 265 |
+
if (!st) throw new Error("decode-only: unknown or released snapshot handle");
|
| 266 |
+
const k = checkContinuation("decode-only", h, ids, want.positions);
|
| 267 |
+
checkHidden(want);
|
| 268 |
+
const rel = await gate.acquire();
|
| 269 |
+
try {
|
| 270 |
+
return await feed(ids, k, st, want);
|
| 271 |
+
} finally {
|
| 272 |
+
rel();
|
| 273 |
+
}
|
| 274 |
+
},
|
| 275 |
+
async *generate(ids, o) {
|
| 276 |
+
gate.assertLive();
|
| 277 |
+
const g = resolveGenerateOptions(o);
|
| 278 |
+
const rand = mulberry32(g.seed);
|
| 279 |
+
if (!ids.length) throw new Error("generate: empty ids");
|
| 280 |
+
if (g.maxNewTokens === 0) return;
|
| 281 |
+
const rel = await gate.acquire();
|
| 282 |
+
try {
|
| 283 |
+
let st = initState(), pos = 0, last = null;
|
| 284 |
+
for (const t of ids) {
|
| 285 |
+
if (g.signal?.aborted) return;
|
| 286 |
+
({ logits: last, st } = await step(t, pos, st, false));
|
| 287 |
+
pos++;
|
| 288 |
+
}
|
| 289 |
+
const gen = [];
|
| 290 |
+
for (let s = 0; s < g.maxNewTokens; s++) {
|
| 291 |
+
if (g.signal?.aborted) break;
|
| 292 |
+
const tid = sample(lastRow(last), gen, rand, g);
|
| 293 |
+
if (EOS.has(tid)) break;
|
| 294 |
+
gen.push(tid);
|
| 295 |
+
yield tid;
|
| 296 |
+
if (s + 1 >= g.maxNewTokens || g.signal?.aborted) break;
|
| 297 |
+
({ logits: last, st } = await step(tid, pos, st, false));
|
| 298 |
+
pos++;
|
| 299 |
+
}
|
| 300 |
+
} finally {
|
| 301 |
+
rel();
|
| 302 |
+
}
|
| 303 |
+
}
|
| 304 |
+
};
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// src/backends/genai.ts
|
| 308 |
+
function parsePlacement(lines) {
|
| 309 |
+
const out = {};
|
| 310 |
+
for (const l of lines) {
|
| 311 |
+
const m = /Node\(s\) placed on \[(\w+)\]\.\s*Number of nodes:\s*(\d+)/.exec(l);
|
| 312 |
+
if (m) out[m[1]] = (out[m[1]] ?? 0) + Number(m[2]);
|
| 313 |
+
}
|
| 314 |
+
return out;
|
| 315 |
+
}
|
| 316 |
+
function assertWebGpuPlacement(p, maxCpuNodes = 32) {
|
| 317 |
+
const gpu = Object.entries(p).filter(([k]) => /webgpu/i.test(k)).reduce((a, [, n]) => a + n, 0);
|
| 318 |
+
const cpu = p.CPUExecutionProvider ?? 0;
|
| 319 |
+
if (!Object.keys(p).length) throw new Error("genai: could not verify the EP placement (no 'Node(s) placed on' logs); reject a bundle without WebGPU");
|
| 320 |
+
if (gpu === 0) throw new Error(`genai: no node on the WebGPU EP (placement: ${JSON.stringify(p)}); LinearAttention does not run on GPU`);
|
| 321 |
+
if (cpu > maxCpuNodes) throw new Error(`genai: ${cpu} nodes on CPU (> ${maxCpuNodes}); probably a bundle without the WebGPU kernels (use onnxruntime-web/webgpu). Placement: ${JSON.stringify(p)}`);
|
| 322 |
+
}
|
| 323 |
+
function genaiFromSession(session, ort, opts = {}) {
|
| 324 |
+
const T = ort.Tensor;
|
| 325 |
+
const eos = opts.eos ?? EOS;
|
| 326 |
+
const inNames = session.inputNames, outNames = session.outputNames;
|
| 327 |
+
for (const need of ["input_ids"]) if (!inNames.includes(need)) throw new Error(`genai: graph without input ${need}`);
|
| 328 |
+
if (!outNames.includes("logits")) throw new Error("genai: graph without a logits output");
|
| 329 |
+
const hasMask = inNames.includes("attention_mask"), hasPos = inNames.includes("position_ids");
|
| 330 |
+
const meta = Array.isArray(session.inputMetadata) ? session.inputMetadata : Object.entries(session.inputMetadata ?? {}).map(([name, m]) => ({ name, ...m }));
|
| 331 |
+
const metaBy = new Map(meta.map((m) => [m.name, m]));
|
| 332 |
+
const specs = [];
|
| 333 |
+
for (const n of inNames) {
|
| 334 |
+
if (n === "input_ids" || n === "attention_mask" || n === "position_ids") continue;
|
| 335 |
+
const m = metaBy.get(n);
|
| 336 |
+
if (!m?.shape) throw new Error("genai: no shape metadata for input " + n);
|
| 337 |
+
const dims = cacheDims(n, m.shape, 0);
|
| 338 |
+
if (/^past_key_values\./.test(n) && typeof m.shape[3] !== "number") dims[3] = opts.kvHeadDim ?? 256;
|
| 339 |
+
specs.push({ name: n, type: m.type, dims });
|
| 340 |
+
}
|
| 341 |
+
const pastOf = /* @__PURE__ */ new Map();
|
| 342 |
+
for (const o of outNames) {
|
| 343 |
+
const p = pastNameFor(o);
|
| 344 |
+
if (p && inNames.includes(p)) pastOf.set(o, p);
|
| 345 |
+
}
|
| 346 |
+
const covered = new Set(pastOf.values());
|
| 347 |
+
const missing = specs.filter((s) => !covered.has(s.name));
|
| 348 |
+
if (missing.length) throw new Error("genai: cache inputs without a present output: " + missing.slice(0, 3).map((s) => s.name).join(", "));
|
| 349 |
+
const hiddenName = HIDDEN_NAMES.find((n) => outNames.includes(n)) ?? null;
|
| 350 |
+
const posRows = (() => {
|
| 351 |
+
const sh = metaBy.get("position_ids")?.shape;
|
| 352 |
+
return typeof sh?.[0] === "number" ? sh[0] : 3;
|
| 353 |
+
})();
|
| 354 |
+
const zeros = (s) => {
|
| 355 |
+
const n = s.dims.reduce((a, b) => a * b, 1);
|
| 356 |
+
if (s.type === "float16") return new T("float16", new Uint16Array(n), s.dims);
|
| 357 |
+
if (s.type === "float32") return new T("float32", new Float32Array(n), s.dims);
|
| 358 |
+
throw new Error(`genai: unsupported cache dtype: ${s.type}`);
|
| 359 |
+
};
|
| 360 |
+
const initState = () => {
|
| 361 |
+
const st = {};
|
| 362 |
+
for (const s of specs) st[s.name] = zeros(s);
|
| 363 |
+
return st;
|
| 364 |
+
};
|
| 365 |
+
const free = (st) => {
|
| 366 |
+
if (st) {
|
| 367 |
+
for (const t of Object.values(st)) if (t?.location === "gpu-buffer") {
|
| 368 |
+
try {
|
| 369 |
+
t.dispose?.();
|
| 370 |
+
} catch {
|
| 371 |
+
}
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
};
|
| 375 |
+
const gate = createGate("genai");
|
| 376 |
+
async function run(chunk, past, st, wantLogits, wantHidden, keep = false) {
|
| 377 |
+
const S = chunk.length;
|
| 378 |
+
const feed = { input_ids: new T("int64", BigInt64Array.from(chunk.map(BigInt)), [1, S]), ...st };
|
| 379 |
+
if (hasMask) feed.attention_mask = new T("int64", new BigInt64Array(past + S).fill(1n), [1, past + S]);
|
| 380 |
+
if (hasPos) {
|
| 381 |
+
const pos = new BigInt64Array(posRows * S);
|
| 382 |
+
for (let r2 = 0; r2 < posRows; r2++) for (let i = 0; i < S; i++) pos[r2 * S + i] = BigInt(past + i);
|
| 383 |
+
feed.position_ids = new T("int64", pos, [posRows, 1, S]);
|
| 384 |
+
}
|
| 385 |
+
const fetches = [...pastOf.keys()];
|
| 386 |
+
if (wantLogits) fetches.unshift("logits");
|
| 387 |
+
if (wantHidden && hiddenName) fetches.push(hiddenName);
|
| 388 |
+
const r = await session.run(feed, fetches, { logSeverityLevel: 3 });
|
| 389 |
+
const ns = {};
|
| 390 |
+
for (const [o, p] of pastOf) ns[p] = r[o];
|
| 391 |
+
if (!keep) free(st);
|
| 392 |
+
const out = {};
|
| 393 |
+
if (wantLogits) out.logits = r.logits;
|
| 394 |
+
if (wantHidden && hiddenName) out.hidden = r[hiddenName];
|
| 395 |
+
return { st: ns, out };
|
| 396 |
+
}
|
| 397 |
+
const read = async (t) => typeof t.getData === "function" ? await t.getData(true) : t.data;
|
| 398 |
+
async function feedAll(ids, wanted, wantLast, signal, from) {
|
| 399 |
+
let st = from?.st ?? initState(), past = from?.past ?? 0;
|
| 400 |
+
const kept = from?.st;
|
| 401 |
+
const hid = /* @__PURE__ */ new Map();
|
| 402 |
+
let lastLogits;
|
| 403 |
+
try {
|
| 404 |
+
while (past < ids.length) {
|
| 405 |
+
if (signal?.aborted) return { st, past, hid, lastLogits, aborted: true };
|
| 406 |
+
const chunk = ids.slice(past, past + self.chunk), last = past + chunk.length >= ids.length;
|
| 407 |
+
const need = [...wanted].filter((p) => p >= past && p < past + chunk.length);
|
| 408 |
+
const r = await run(chunk, past, st, last && wantLast, need.length > 0, st === kept);
|
| 409 |
+
st = r.st;
|
| 410 |
+
if (r.out.hidden) {
|
| 411 |
+
const H = r.out.hidden, D = H.dims.at(-1), d = await read(H);
|
| 412 |
+
for (const p of need) hid.set(p, tensorToF32(d, H.type, (p - past) * D, D));
|
| 413 |
+
}
|
| 414 |
+
if (r.out.logits) {
|
| 415 |
+
const L = r.out.logits, V = L.dims.at(-1), d = await read(L);
|
| 416 |
+
lastLogits = tensorToF32(d, L.type, (L.dims.at(-2) - 1) * V, V);
|
| 417 |
+
}
|
| 418 |
+
past += chunk.length;
|
| 419 |
+
}
|
| 420 |
+
} catch (e) {
|
| 421 |
+
if (st !== kept) free(st);
|
| 422 |
+
throw e;
|
| 423 |
+
}
|
| 424 |
+
return { st, past, hid, lastLogits, aborted: false };
|
| 425 |
+
}
|
| 426 |
+
const snaps = /* @__PURE__ */ new WeakMap();
|
| 427 |
+
const self = {
|
| 428 |
+
kind: "genai",
|
| 429 |
+
chunk: Math.max(1, opts.chunk ?? 128),
|
| 430 |
+
hiddenName,
|
| 431 |
+
dispose() {
|
| 432 |
+
return gate.dispose(() => session.release?.());
|
| 433 |
+
},
|
| 434 |
+
async prefill(ids, want) {
|
| 435 |
+
gate.assertLive();
|
| 436 |
+
for (const p of want.positions) if (!Number.isInteger(p) || p < 0 || p >= ids.length) throw new Error(`genai: position ${p} out of [0,${ids.length})`);
|
| 437 |
+
if (!ids.length) throw new Error("prefill: empty ids");
|
| 438 |
+
if (want.positions.length && !hiddenName) throw new Error("genai: the graph does not expose a hidden state; use method 'logits'");
|
| 439 |
+
const rel = await gate.acquire();
|
| 440 |
+
try {
|
| 441 |
+
const f = await feedAll(ids, new Set(want.positions), true);
|
| 442 |
+
free(f.st);
|
| 443 |
+
return { hiddens: want.positions.map((p) => f.hid.get(p)), lastLogits: f.lastLogits };
|
| 444 |
+
} finally {
|
| 445 |
+
rel();
|
| 446 |
+
}
|
| 447 |
+
},
|
| 448 |
+
async snapshot(ids) {
|
| 449 |
+
gate.assertLive();
|
| 450 |
+
if (!ids.length) throw new Error("genai: snapshot of empty ids");
|
| 451 |
+
const rel = await gate.acquire();
|
| 452 |
+
try {
|
| 453 |
+
const f = await feedAll(ids, /* @__PURE__ */ new Set(), false);
|
| 454 |
+
let st = f.st;
|
| 455 |
+
const h = { ids: ids.slice(), dispose: async () => {
|
| 456 |
+
if (!st) return;
|
| 457 |
+
const s0 = st;
|
| 458 |
+
st = null;
|
| 459 |
+
snaps.delete(h);
|
| 460 |
+
let r = null;
|
| 461 |
+
try {
|
| 462 |
+
r = await gate.acquire();
|
| 463 |
+
} catch {
|
| 464 |
+
}
|
| 465 |
+
try {
|
| 466 |
+
free(s0);
|
| 467 |
+
} finally {
|
| 468 |
+
r?.();
|
| 469 |
+
}
|
| 470 |
+
} };
|
| 471 |
+
snaps.set(h, st);
|
| 472 |
+
return h;
|
| 473 |
+
} finally {
|
| 474 |
+
rel();
|
| 475 |
+
}
|
| 476 |
+
},
|
| 477 |
+
async prefillFrom(h, ids, want) {
|
| 478 |
+
gate.assertLive();
|
| 479 |
+
const st = snaps.get(h);
|
| 480 |
+
if (!st) throw new Error("genai: unknown or released snapshot handle");
|
| 481 |
+
const k = checkContinuation("genai", h, ids, want.positions);
|
| 482 |
+
if (want.positions.length && !hiddenName) throw new Error("genai: the graph does not expose a hidden state; use method 'logits'");
|
| 483 |
+
const rel = await gate.acquire();
|
| 484 |
+
try {
|
| 485 |
+
const f = await feedAll(ids, new Set(want.positions), true, void 0, { st, past: k });
|
| 486 |
+
free(f.st);
|
| 487 |
+
return { hiddens: want.positions.map((p) => f.hid.get(p)), lastLogits: f.lastLogits };
|
| 488 |
+
} finally {
|
| 489 |
+
rel();
|
| 490 |
+
}
|
| 491 |
+
},
|
| 492 |
+
async *generate(ids, o) {
|
| 493 |
+
gate.assertLive();
|
| 494 |
+
const g = resolveGenerateOptions(o);
|
| 495 |
+
const rand = mulberry32(g.seed);
|
| 496 |
+
if (!ids.length) throw new Error("generate: empty ids");
|
| 497 |
+
if (g.maxNewTokens === 0) return;
|
| 498 |
+
const rel = await gate.acquire();
|
| 499 |
+
let st = null;
|
| 500 |
+
try {
|
| 501 |
+
if (o.signal?.aborted) return;
|
| 502 |
+
const f = await feedAll(ids, /* @__PURE__ */ new Set(), true, o.signal);
|
| 503 |
+
st = f.st;
|
| 504 |
+
if (f.aborted) return;
|
| 505 |
+
let past = f.past, logits = f.lastLogits;
|
| 506 |
+
const gen = [];
|
| 507 |
+
for (let s = 0; s < g.maxNewTokens; s++) {
|
| 508 |
+
if (o.signal?.aborted) break;
|
| 509 |
+
const tid = sample(logits, gen, rand, g);
|
| 510 |
+
if (eos.has(tid)) break;
|
| 511 |
+
gen.push(tid);
|
| 512 |
+
yield tid;
|
| 513 |
+
if (o.signal?.aborted || s + 1 >= g.maxNewTokens) break;
|
| 514 |
+
const r = await run([tid], past, st, true, false);
|
| 515 |
+
st = r.st;
|
| 516 |
+
past++;
|
| 517 |
+
const L = r.out.logits, V = L.dims.at(-1);
|
| 518 |
+
logits = tensorToF32(await read(L), L.type, (L.dims.at(-2) - 1) * V, V);
|
| 519 |
+
}
|
| 520 |
+
} finally {
|
| 521 |
+
free(st);
|
| 522 |
+
rel();
|
| 523 |
+
}
|
| 524 |
+
}
|
| 525 |
+
};
|
| 526 |
+
return self;
|
| 527 |
+
}
|
| 528 |
+
function hashStr(x) {
|
| 529 |
+
let h = 2166136261;
|
| 530 |
+
for (let i = 0; i < x.length; i++) {
|
| 531 |
+
h ^= x.charCodeAt(i);
|
| 532 |
+
h = Math.imul(h, 16777619) >>> 0;
|
| 533 |
+
}
|
| 534 |
+
return h.toString(16).padStart(8, "0");
|
| 535 |
+
}
|
| 536 |
+
async function adapterTag(opts) {
|
| 537 |
+
if (typeof opts.adapterTag === "string") return opts.adapterTag;
|
| 538 |
+
if (typeof opts.adapterTag === "function") return opts.adapterTag();
|
| 539 |
+
try {
|
| 540 |
+
const gpu = globalThis.navigator?.gpu;
|
| 541 |
+
const ad = await gpu?.requestAdapter?.();
|
| 542 |
+
const i = ad?.info ?? await ad?.requestAdapterInfo?.();
|
| 543 |
+
if (!i) return "?";
|
| 544 |
+
return hashStr([i.vendor, i.architecture, i.device, i.description].join("|"));
|
| 545 |
+
} catch {
|
| 546 |
+
return "?";
|
| 547 |
+
}
|
| 548 |
+
}
|
| 549 |
+
function epStore(opts) {
|
| 550 |
+
if (opts.epCache !== void 0) return opts.epCache;
|
| 551 |
+
try {
|
| 552 |
+
return globalThis.localStorage ?? null;
|
| 553 |
+
} catch {
|
| 554 |
+
return null;
|
| 555 |
+
}
|
| 556 |
+
}
|
| 557 |
+
async function probePlacement(ort, modelUrl, sessionOpts, maxCpuNodes) {
|
| 558 |
+
const captured = [];
|
| 559 |
+
const names = ["log", "info", "warn", "error", "debug"];
|
| 560 |
+
const saved = {};
|
| 561 |
+
const prevLevel = ort.env?.logLevel;
|
| 562 |
+
if (ort.env) ort.env.logLevel = "verbose";
|
| 563 |
+
for (const n of names) {
|
| 564 |
+
saved[n] = console[n];
|
| 565 |
+
console[n] = (...a) => {
|
| 566 |
+
const s = a.join(" ");
|
| 567 |
+
if (s.includes("Node(s) placed on")) captured.push(s);
|
| 568 |
+
else if (!s.includes("[V:onnxruntime") && !/WebGPU Shader code|^\s*$/.test(s)) saved[n].apply(console, a);
|
| 569 |
+
};
|
| 570 |
+
}
|
| 571 |
+
let probe;
|
| 572 |
+
try {
|
| 573 |
+
probe = await ort.InferenceSession.create(modelUrl, sessionOpts(0));
|
| 574 |
+
} finally {
|
| 575 |
+
for (const n of names) console[n] = saved[n];
|
| 576 |
+
if (ort.env) ort.env.logLevel = prevLevel;
|
| 577 |
+
}
|
| 578 |
+
try {
|
| 579 |
+
assertWebGpuPlacement(parsePlacement(captured), maxCpuNodes);
|
| 580 |
+
} finally {
|
| 581 |
+
await probe.release?.();
|
| 582 |
+
}
|
| 583 |
+
}
|
| 584 |
+
async function createGenai(modelUrl, dataUrl, opts = {}) {
|
| 585 |
+
const providers = opts.providers ?? ["webgpu"];
|
| 586 |
+
if (providers[0] !== "webgpu") throw new Error("genai: the graph requires the webgpu EP as first provider (the LinearAttention/CausalConvWithState kernels only exist on WebGPU)");
|
| 587 |
+
const ort = opts.ort ?? await import("onnxruntime-web/webgpu");
|
| 588 |
+
const verify = opts.verifyEp ?? true;
|
| 589 |
+
const sessionOpts = (level) => ({
|
| 590 |
+
executionProviders: providers.map((n) => n === "webgpu" ? { name: "webgpu", validationMode: "disabled" } : n),
|
| 591 |
+
externalData: [{ path: "model.onnx.data", data: dataUrl }],
|
| 592 |
+
graphOptimizationLevel: "all",
|
| 593 |
+
enableGraphCapture: false,
|
| 594 |
+
preferredOutputLocation: "gpu-buffer",
|
| 595 |
+
logSeverityLevel: level
|
| 596 |
+
});
|
| 597 |
+
if (!opts.skipVersionCheck) checkOrtVersion(ort.env?.versions?.web);
|
| 598 |
+
if (verify) {
|
| 599 |
+
const store = epStore(opts);
|
| 600 |
+
const maxCpu = opts.maxCpuNodes ?? 32;
|
| 601 |
+
const key = store ? `ct0-edge:genai-ep:${ort.env?.versions?.web ?? "?"}:${await adapterTag(opts)}:${modelUrl}:${dataUrl}:${maxCpu}` : "";
|
| 602 |
+
let cached = false;
|
| 603 |
+
if (verify !== "force" && store) {
|
| 604 |
+
try {
|
| 605 |
+
cached = store.getItem(key) === "ok";
|
| 606 |
+
} catch {
|
| 607 |
+
}
|
| 608 |
+
}
|
| 609 |
+
if (!cached) {
|
| 610 |
+
await probePlacement(ort, modelUrl, sessionOpts, maxCpu);
|
| 611 |
+
if (store) {
|
| 612 |
+
try {
|
| 613 |
+
store.setItem(key, "ok");
|
| 614 |
+
} catch {
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
}
|
| 618 |
+
}
|
| 619 |
+
const session = await ort.InferenceSession.create(modelUrl, sessionOpts(3));
|
| 620 |
+
try {
|
| 621 |
+
const be = genaiFromSession(session, ort, opts);
|
| 622 |
+
if (opts.warmup ?? true) await be.prefill([248045], { positions: [] });
|
| 623 |
+
return be;
|
| 624 |
+
} catch (e) {
|
| 625 |
+
try {
|
| 626 |
+
await session.release?.();
|
| 627 |
+
} catch {
|
| 628 |
+
}
|
| 629 |
+
throw e;
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
+
|
| 633 |
+
// src/backends/transformersjs.ts
|
| 634 |
+
function transformersJsFromModel(model, Tensor) {
|
| 635 |
+
const sess = model.sessions?.model ?? Object.values(model.sessions ?? {})[0];
|
| 636 |
+
const outs = sess?.outputNames ?? [];
|
| 637 |
+
const hiddenName = HIDDEN_NAMES.find((n) => outs.includes(n)) ?? null;
|
| 638 |
+
const toTensors = (ids) => ({
|
| 639 |
+
input_ids: new Tensor("int64", BigInt64Array.from(ids.map(BigInt)), [1, ids.length]),
|
| 640 |
+
attention_mask: new Tensor("int64", new BigInt64Array(ids.length).fill(1n), [1, ids.length])
|
| 641 |
+
});
|
| 642 |
+
const gate = createGate("scan");
|
| 643 |
+
return {
|
| 644 |
+
kind: "scan",
|
| 645 |
+
hiddenName,
|
| 646 |
+
async prefill(ids, want) {
|
| 647 |
+
gate.assertLive();
|
| 648 |
+
const n = ids.length;
|
| 649 |
+
if (!n) throw new Error("prefill: empty ids");
|
| 650 |
+
for (const p of want.positions) if (!Number.isInteger(p) || p < 0 || p >= n) throw new Error(`transformers.js: position ${p} out of range [0,${n})`);
|
| 651 |
+
if (want.positions.length && !hiddenName) throw new Error("transformers.js: graph has no hidden output; use method 'logits'");
|
| 652 |
+
const rel = await gate.acquire();
|
| 653 |
+
try {
|
| 654 |
+
const out = await model.forward(toTensors(ids));
|
| 655 |
+
const L = out.logits, V = L.dims.at(-1);
|
| 656 |
+
const lastLogits = tensorToF32(L.data, L.type, (L.dims.at(-2) - 1) * V, V);
|
| 657 |
+
const hiddens = [];
|
| 658 |
+
if (want.positions.length) {
|
| 659 |
+
const H = out[hiddenName], D = H.dims.at(-1);
|
| 660 |
+
if (H.dims.at(-2) !== n) throw new Error(`transformers.js: hidden seq ${H.dims.at(-2)} != ${n}`);
|
| 661 |
+
for (const p of want.positions) hiddens.push(tensorToF32(H.data, H.type, p * D, D));
|
| 662 |
+
}
|
| 663 |
+
return { hiddens, lastLogits };
|
| 664 |
+
} finally {
|
| 665 |
+
rel();
|
| 666 |
+
}
|
| 667 |
+
},
|
| 668 |
+
dispose() {
|
| 669 |
+
return gate.dispose(() => model.dispose?.());
|
| 670 |
+
},
|
| 671 |
+
async *generate(ids, o) {
|
| 672 |
+
gate.assertLive();
|
| 673 |
+
const g = resolveGenerateOptions(o);
|
| 674 |
+
const max = g.maxNewTokens, temp = g.temperature;
|
| 675 |
+
if (!ids.length) throw new Error("generate: empty ids");
|
| 676 |
+
if (max === 0) return;
|
| 677 |
+
const rel = await gate.acquire();
|
| 678 |
+
const queue = [];
|
| 679 |
+
let done = false, wake = null, err = null, first = true, stop = false;
|
| 680 |
+
const ping = () => {
|
| 681 |
+
const w = wake;
|
| 682 |
+
wake = null;
|
| 683 |
+
w?.();
|
| 684 |
+
};
|
| 685 |
+
const streamer = {
|
| 686 |
+
put(v) {
|
| 687 |
+
if (first) {
|
| 688 |
+
first = false;
|
| 689 |
+
return;
|
| 690 |
+
}
|
| 691 |
+
if (stop) return;
|
| 692 |
+
for (const t of v.flat ? v.flat() : v) queue.push(Number(t));
|
| 693 |
+
ping();
|
| 694 |
+
},
|
| 695 |
+
end() {
|
| 696 |
+
done = true;
|
| 697 |
+
ping();
|
| 698 |
+
}
|
| 699 |
+
};
|
| 700 |
+
const criterion = (batch) => batch.map(() => stop || !!o.signal?.aborted);
|
| 701 |
+
const args = { ...toTensors(ids), max_new_tokens: max, streamer, stopping_criteria: [criterion], do_sample: temp > 0 };
|
| 702 |
+
if (temp > 0) {
|
| 703 |
+
args.temperature = temp;
|
| 704 |
+
args.top_p = g.topP;
|
| 705 |
+
args.top_k = g.topK;
|
| 706 |
+
}
|
| 707 |
+
args.repetition_penalty = g.repetitionPenalty;
|
| 708 |
+
const run = Promise.resolve().then(() => model.generate(args)).then(() => {
|
| 709 |
+
done = true;
|
| 710 |
+
ping();
|
| 711 |
+
}, (e) => {
|
| 712 |
+
err = e;
|
| 713 |
+
done = true;
|
| 714 |
+
ping();
|
| 715 |
+
});
|
| 716 |
+
let emitted = 0;
|
| 717 |
+
try {
|
| 718 |
+
while (true) {
|
| 719 |
+
if (o.signal?.aborted) break;
|
| 720 |
+
if (queue.length) {
|
| 721 |
+
const t = queue.shift();
|
| 722 |
+
if (EOS.has(t)) break;
|
| 723 |
+
yield t;
|
| 724 |
+
if (++emitted >= max) break;
|
| 725 |
+
} else if (done) break;
|
| 726 |
+
else await new Promise((r) => {
|
| 727 |
+
wake = r;
|
| 728 |
+
if (queue.length || done) ping();
|
| 729 |
+
});
|
| 730 |
+
}
|
| 731 |
+
} finally {
|
| 732 |
+
stop = true;
|
| 733 |
+
try {
|
| 734 |
+
await run;
|
| 735 |
+
} finally {
|
| 736 |
+
rel();
|
| 737 |
+
}
|
| 738 |
+
}
|
| 739 |
+
if (err && !o.signal?.aborted) throw err;
|
| 740 |
+
}
|
| 741 |
+
};
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
// src/positions.ts
|
| 745 |
+
var CHOICE_Q = "Which vulnerability class does this code contain?";
|
| 746 |
+
var SEV_Q = "How severe is the security risk in this content? (1=none, 5=critical)";
|
| 747 |
+
function renderPrompt(state, question) {
|
| 748 |
+
return "<|im_start|>system\nYou are a security decision function. Answer with exactly one word: Yes or No.<|im_end|>\n<|im_start|>user\n<state>" + state + "</state>\n<q>" + question + "</q><decide><|im_end|>\n<|im_start|>assistant\n";
|
| 749 |
+
}
|
| 750 |
+
var KINDS = ["noul", "choice", "score"];
|
| 751 |
+
function assertProbe(p) {
|
| 752 |
+
const q = p;
|
| 753 |
+
if (!q || typeof q !== "object") throw new Error("probe: expected an object {kind, state, ...}");
|
| 754 |
+
if (!KINDS.includes(q.kind)) {
|
| 755 |
+
const hint = q.kind === void 0 && "type" in q ? ` (the field is \`kind\`, not \`type\`: {kind: ${JSON.stringify(q.type)}, state, q})` : "";
|
| 756 |
+
throw new Error(`probe: invalid kind ${JSON.stringify(q.kind)}; use "noul", "choice" or "score"${hint}`);
|
| 757 |
+
}
|
| 758 |
+
if (typeof q.state !== "string") throw new Error("probe: state must be a string");
|
| 759 |
+
if (q.kind === "choice") {
|
| 760 |
+
const o = q.options;
|
| 761 |
+
if (!Array.isArray(o) || !o.length) throw new Error("probe: choice requires options (non-empty list)");
|
| 762 |
+
const seen = /* @__PURE__ */ new Set();
|
| 763 |
+
for (const x of o) {
|
| 764 |
+
if (typeof x !== "string" || !x.trim()) throw new Error("probe: option label empty or not a string: " + JSON.stringify(x));
|
| 765 |
+
if (/<\/?opt>/.test(x)) throw new Error("probe: option label cannot contain <opt> / </opt>: " + JSON.stringify(x));
|
| 766 |
+
if (seen.has(x)) throw new Error("probe: duplicate option label: " + JSON.stringify(x));
|
| 767 |
+
seen.add(x);
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
}
|
| 771 |
+
function promptFor(p) {
|
| 772 |
+
assertProbe(p);
|
| 773 |
+
if (p.kind === "noul") return renderPrompt(p.state, p.q ?? "");
|
| 774 |
+
if (p.kind === "choice") {
|
| 775 |
+
return renderPrompt(p.state, CHOICE_Q + " " + (p.options ?? []).map((o) => "<opt>" + o + "</opt>").join(""));
|
| 776 |
+
}
|
| 777 |
+
return renderPrompt(p.state, SEV_Q);
|
| 778 |
+
}
|
| 779 |
+
function decidePos(tokens) {
|
| 780 |
+
const imEnd = [];
|
| 781 |
+
tokens.forEach((s, i) => {
|
| 782 |
+
if (s === "<|im_end|>") imEnd.push(i);
|
| 783 |
+
});
|
| 784 |
+
if (!imEnd.length) throw new Error("decidePos: no <|im_end|> in " + JSON.stringify(tokens.slice(-8)));
|
| 785 |
+
let lastAsst = -1;
|
| 786 |
+
tokens.forEach((s, i) => {
|
| 787 |
+
if (s.trim() === "assistant") lastAsst = i;
|
| 788 |
+
});
|
| 789 |
+
const cand = lastAsst >= 0 ? imEnd.filter((i) => i < lastAsst) : [];
|
| 790 |
+
const end = cand.length ? cand[cand.length - 1] : imEnd[imEnd.length - 1];
|
| 791 |
+
if (end === 0) throw new Error("decidePos: <|im_end|> at position 0, no preceding token");
|
| 792 |
+
if (tokens[end - 1] !== ">") {
|
| 793 |
+
throw new Error("decidePos: token before <|im_end|> is " + JSON.stringify(tokens[end - 1]) + ", expected '>' (end of <decide>)");
|
| 794 |
+
}
|
| 795 |
+
return end - 1;
|
| 796 |
+
}
|
| 797 |
+
function optPositions(prompt, offsets) {
|
| 798 |
+
const out = [];
|
| 799 |
+
const from = Math.max(0, prompt.lastIndexOf("<q>"));
|
| 800 |
+
for (const m of prompt.slice(from).matchAll(/<\/opt>/g)) {
|
| 801 |
+
const c = from + m.index + m[0].length - 1;
|
| 802 |
+
const hit = offsets.findIndex(([s, e]) => s <= c && c < e);
|
| 803 |
+
if (hit < 0) throw new Error("optPositions: no token covers char " + c);
|
| 804 |
+
out.push(hit);
|
| 805 |
+
}
|
| 806 |
+
return out;
|
| 807 |
+
}
|
| 808 |
+
function optPositionsFor(prompt, offsets, nOptions) {
|
| 809 |
+
const pos = optPositions(prompt, offsets);
|
| 810 |
+
if (nOptions === void 0) nOptions = (prompt.slice(Math.max(0, prompt.lastIndexOf("<q>"))).match(/<opt>/g) ?? []).length;
|
| 811 |
+
if (pos.length !== nOptions) throw new Error(`positions: ${pos.length} </opt> markers after <q> for ${nOptions} options (label containing </opt>?)`);
|
| 812 |
+
return pos;
|
| 813 |
+
}
|
| 814 |
+
function tokenOffsets(tok, prompt) {
|
| 815 |
+
const ids = Array.from(tok.encode(prompt, { add_special_tokens: false }), (x) => Number(x));
|
| 816 |
+
const dec = (a) => tok.decode(a, { skip_special_tokens: false, clean_up_tokenization_spaces: false });
|
| 817 |
+
const tokens = ids.map((i) => dec([i]));
|
| 818 |
+
const offsets = [];
|
| 819 |
+
let prev = 0;
|
| 820 |
+
let full = "";
|
| 821 |
+
for (let i = 0; i < ids.length; i++) {
|
| 822 |
+
full = dec(ids.slice(0, i + 1));
|
| 823 |
+
let end = full.length;
|
| 824 |
+
while (end > prev && full.charCodeAt(end - 1) === 65533) end--;
|
| 825 |
+
offsets.push([prev, end]);
|
| 826 |
+
prev = end;
|
| 827 |
+
}
|
| 828 |
+
if (full !== prompt) throw new Error("tokenOffsets: reconstruction != prompt (offsets not reliable)");
|
| 829 |
+
return { ids, tokens, offsets };
|
| 830 |
+
}
|
| 831 |
+
|
| 832 |
+
// src/systemone.ts
|
| 833 |
+
function loadHead(buf) {
|
| 834 |
+
const dv = new DataView(buf);
|
| 835 |
+
const jsonLen = dv.getUint32(0, true);
|
| 836 |
+
const meta = JSON.parse(new TextDecoder().decode(new Uint8Array(buf, 4, jsonLen)));
|
| 837 |
+
let off = 4 + jsonLen;
|
| 838 |
+
const take = (n) => {
|
| 839 |
+
const avail = Math.floor((buf.byteLength - off) / 2);
|
| 840 |
+
if (n > avail) throw new Error(`head binary too short: needs ${n} fp16, ${avail} available`);
|
| 841 |
+
const f32 = new Float32Array(n);
|
| 842 |
+
for (let i = 0; i < n; i++) f32[i] = f16ToF32(dv.getUint16(off + i * 2, true));
|
| 843 |
+
off += n * 2;
|
| 844 |
+
return f32;
|
| 845 |
+
};
|
| 846 |
+
return {
|
| 847 |
+
q: take(meta.headDim * meta.hiddenSize),
|
| 848 |
+
qb: take(meta.headDim),
|
| 849 |
+
k: take(meta.headDim * meta.hiddenSize),
|
| 850 |
+
kb: take(meta.headDim),
|
| 851 |
+
headDim: meta.headDim,
|
| 852 |
+
hiddenSize: meta.hiddenSize,
|
| 853 |
+
temperature: meta.temperature,
|
| 854 |
+
scoreOffset: meta.score_offset ?? 0
|
| 855 |
+
};
|
| 856 |
+
}
|
| 857 |
+
function f16ToF32(h) {
|
| 858 |
+
const s = (h & 32768) >> 15, e = (h & 31744) >> 10, f = h & 1023;
|
| 859 |
+
if (e === 0) return (s ? -1 : 1) * Math.pow(2, -14) * (f / 1024);
|
| 860 |
+
if (e === 31) return f ? NaN : s ? -Infinity : Infinity;
|
| 861 |
+
return (s ? -1 : 1) * Math.pow(2, e - 15) * (1 + f / 1024);
|
| 862 |
+
}
|
| 863 |
+
function proj(W, b, x, dim, hidden) {
|
| 864 |
+
const y = new Float32Array(dim);
|
| 865 |
+
for (let d = 0; d < dim; d++) {
|
| 866 |
+
let acc = b[d];
|
| 867 |
+
const row = d * hidden;
|
| 868 |
+
for (let i = 0; i < hidden; i++) acc += W[row + i] * x[i];
|
| 869 |
+
y[d] = acc;
|
| 870 |
+
}
|
| 871 |
+
return y;
|
| 872 |
+
}
|
| 873 |
+
function dot(a, b) {
|
| 874 |
+
let s = 0;
|
| 875 |
+
for (let i = 0; i < a.length; i++) s += a[i] * b[i];
|
| 876 |
+
return s;
|
| 877 |
+
}
|
| 878 |
+
var sigmoid = (z) => 1 / (1 + Math.exp(-z));
|
| 879 |
+
function headScore(hd, hq, hk) {
|
| 880 |
+
if (!hq || !hk || hq.length < hd.hiddenSize || hk.length < hd.hiddenSize) return NaN;
|
| 881 |
+
const a = proj(hd.q, hd.qb, hq, hd.headDim, hd.hiddenSize);
|
| 882 |
+
const b = proj(hd.k, hd.kb, hk, hd.headDim, hd.hiddenSize);
|
| 883 |
+
const s = dot(a, b) / hd.temperature + (hd.scoreOffset || 0);
|
| 884 |
+
return Number.isFinite(s) ? s : NaN;
|
| 885 |
+
}
|
| 886 |
+
function answer(hd, q, hiddens) {
|
| 887 |
+
if (q.type === "noul") {
|
| 888 |
+
const raw2 = headScore(hd, hiddens.atDecide, hiddens.atLast);
|
| 889 |
+
return { type: "noul", noul: sigmoid(raw2), raw: raw2 };
|
| 890 |
+
}
|
| 891 |
+
if (q.type === "choice") {
|
| 892 |
+
const opts = q.options || [];
|
| 893 |
+
const per = hiddens.perOption || [];
|
| 894 |
+
if (!per.length) return { type: "choice", choice: {} };
|
| 895 |
+
const choice = {};
|
| 896 |
+
const raws = [];
|
| 897 |
+
for (let i = 0; i < opts.length; i++) {
|
| 898 |
+
const h = per[i];
|
| 899 |
+
if (!h) {
|
| 900 |
+
choice[opts[i]] = 0;
|
| 901 |
+
raws.push(-Infinity);
|
| 902 |
+
continue;
|
| 903 |
+
}
|
| 904 |
+
const r = headScore(hd, hiddens.atDecide, h);
|
| 905 |
+
raws.push(r);
|
| 906 |
+
choice[opts[i]] = r;
|
| 907 |
+
}
|
| 908 |
+
const mx = Math.max(...raws.filter(Number.isFinite));
|
| 909 |
+
let z = 0;
|
| 910 |
+
const exps = raws.map((r) => Number.isFinite(r) ? Math.exp(r - mx) : 0);
|
| 911 |
+
z = exps.reduce((a, b) => a + b, 0) || 1;
|
| 912 |
+
for (let i = 0; i < opts.length; i++) choice[opts[i]] = exps[i] / z;
|
| 913 |
+
return { type: "choice", choice };
|
| 914 |
+
}
|
| 915 |
+
const K = q.levels || 5;
|
| 916 |
+
const raw = headScore(hd, hiddens.atDecide, hiddens.atLast);
|
| 917 |
+
const p = sigmoid(raw);
|
| 918 |
+
const score = Math.max(1, Math.min(K, Math.round(p * (K - 1) + 1)));
|
| 919 |
+
return { type: "score", score, raw };
|
| 920 |
+
}
|
| 921 |
+
|
| 922 |
+
// src/ask.ts
|
| 923 |
+
var YES_ID = 9175;
|
| 924 |
+
var NO_ID = 2665;
|
| 925 |
+
var DIGIT_IDS = [16, 17, 18, 19, 20];
|
| 926 |
+
var DEFAULT_METHOD = { noul: "logits", choice: "head", score: "head" };
|
| 927 |
+
var sigmoid2 = (z) => 1 / (1 + Math.exp(-z));
|
| 928 |
+
function softmax(xs) {
|
| 929 |
+
const mx = Math.max(...xs);
|
| 930 |
+
const e = xs.map((x) => Math.exp(x - mx));
|
| 931 |
+
const z = e.reduce((a, b) => a + b, 0) || 1;
|
| 932 |
+
return e.map((v) => v / z);
|
| 933 |
+
}
|
| 934 |
+
function argmax(xs) {
|
| 935 |
+
let b = 0;
|
| 936 |
+
for (let i = 1; i < xs.length; i++) if (xs[i] > xs[b]) b = i;
|
| 937 |
+
return b;
|
| 938 |
+
}
|
| 939 |
+
function toChoice(options, probs) {
|
| 940 |
+
const choice = {};
|
| 941 |
+
options.forEach((o, i) => {
|
| 942 |
+
choice[o] = probs[i];
|
| 943 |
+
});
|
| 944 |
+
return { choice, top: options[argmax(probs)] };
|
| 945 |
+
}
|
| 946 |
+
function levelFromCuts(x, cuts) {
|
| 947 |
+
let l = 1;
|
| 948 |
+
for (const c of cuts) if (x >= c) l++;
|
| 949 |
+
return l;
|
| 950 |
+
}
|
| 951 |
+
function checkCuts(cuts, n, read) {
|
| 952 |
+
if (cuts === void 0) return void 0;
|
| 953 |
+
if (!Array.isArray(cuts) || cuts.length !== n || !cuts.every(Number.isFinite) || cuts.some((c, i) => i > 0 && c <= cuts[i - 1]))
|
| 954 |
+
throw new Error(`ask: scoreCuts for the ${read} read must be ${n} finite ascending numbers, got ${JSON.stringify(cuts)}`);
|
| 955 |
+
return cuts;
|
| 956 |
+
}
|
| 957 |
+
function planAsk(be, tok, heads, q, opts) {
|
| 958 |
+
const maxSafe = opts.maxSafeTokens ?? 2048;
|
| 959 |
+
const prompt = promptFor(q);
|
| 960 |
+
const type = q.kind;
|
| 961 |
+
const method = opts.method ?? DEFAULT_METHOD[type];
|
| 962 |
+
if (method !== "head" && method !== "logits" && method !== "both") throw new Error(`ask: unknown method ${JSON.stringify(method)}; use "head", "logits" or "both"`);
|
| 963 |
+
const options = q.options ?? [];
|
| 964 |
+
const t = tokenOffsets(tok, prompt);
|
| 965 |
+
if (t.ids.length > maxSafe) throw new Error(`ask: prompt has ${t.ids.length} tokens, over maxSafeTokens (${maxSafe})`);
|
| 966 |
+
let headCuts, logitCuts;
|
| 967 |
+
if (type === "score" && opts.scoreCuts !== void 0) {
|
| 968 |
+
const sc = opts.scoreCuts;
|
| 969 |
+
if (Array.isArray(sc)) {
|
| 970 |
+
if (method === "both") throw new Error('ask: method "both" reads two scales; pass scoreCuts as { head, logits }');
|
| 971 |
+
if (method === "head") headCuts = sc;
|
| 972 |
+
else logitCuts = sc;
|
| 973 |
+
} else ({ head: headCuts, logits: logitCuts } = sc ?? {});
|
| 974 |
+
headCuts = checkCuts(headCuts, (q.levels || 5) - 1, "head");
|
| 975 |
+
logitCuts = checkCuts(logitCuts, DIGIT_IDS.length - 1, "logits");
|
| 976 |
+
}
|
| 977 |
+
const optFirst = type === "choice" ? options.map((o) => Number(tok.encode(o, { add_special_tokens: false })[0])) : [];
|
| 978 |
+
const fromLogits = (L) => {
|
| 979 |
+
if (type === "noul") {
|
| 980 |
+
const raw = L[YES_ID] - L[NO_ID];
|
| 981 |
+
return { type, method: "logits", raw, p: sigmoid2(raw) };
|
| 982 |
+
}
|
| 983 |
+
if (type === "choice") return { type, method: "logits", ...toChoice(options, softmax(optFirst.map((id) => L[id]))) };
|
| 984 |
+
const d = DIGIT_IDS.map((i) => L[i]);
|
| 985 |
+
const expected = softmax(d).reduce((a, p, i) => a + (i + 1) * p, 0);
|
| 986 |
+
return { type, method: "logits", level: logitCuts ? levelFromCuts(expected, logitCuts) : argmax(d) + 1, expected };
|
| 987 |
+
};
|
| 988 |
+
if (method === "logits") return { ids: t.ids, positions: [], read: (r) => fromLogits(r.lastLogits) };
|
| 989 |
+
if (!be.hiddenName) throw new Error(`ask: method '${method}' needs a graph with a hidden output (backend.hiddenName is null); use method 'logits'`);
|
| 990 |
+
const hd = type === "score" ? heads.score : heads.noulChoice;
|
| 991 |
+
if (!hd) throw new Error(type === "score" ? `ask: method '${method}' for score needs the score head (head_score.bin); load it via loadHeads({score})` : `ask: method '${method}' needs a loaded head for '${type}'`);
|
| 992 |
+
const dp = decidePos(t.tokens);
|
| 993 |
+
const withLogits = (r, L) => method === "both" ? { ...r, logits: fromLogits(L) } : r;
|
| 994 |
+
if (type === "choice") {
|
| 995 |
+
const op = optPositionsFor(prompt, t.offsets, options.length);
|
| 996 |
+
return { ids: t.ids, positions: [dp, ...op], read: ({ hiddens, lastLogits }) => {
|
| 997 |
+
const perOption = hiddens.slice(1);
|
| 998 |
+
const a = answer(hd, { type: "choice", options }, { atDecide: hiddens[0], atLast: perOption[perOption.length - 1], perOption });
|
| 999 |
+
return withLogits({ type, method: "head", ...toChoice(options, options.map((o) => a.choice[o])) }, lastLogits);
|
| 1000 |
+
} };
|
| 1001 |
+
}
|
| 1002 |
+
return { ids: t.ids, positions: [dp, t.ids.length - 1], read: ({ hiddens, lastLogits }) => {
|
| 1003 |
+
const a = answer(hd, { type, levels: q.levels }, { atDecide: hiddens[0], atLast: hiddens[1] });
|
| 1004 |
+
if (type === "noul") return withLogits({ type, method: "head", raw: a.raw, p: a.noul }, lastLogits);
|
| 1005 |
+
const level = headCuts && Number.isFinite(a.raw) ? levelFromCuts(a.raw, headCuts) : a.score;
|
| 1006 |
+
return withLogits({ type, method: "head", raw: a.raw, level }, lastLogits);
|
| 1007 |
+
} };
|
| 1008 |
+
}
|
| 1009 |
+
async function ask(be, tok, heads, q, opts = {}) {
|
| 1010 |
+
const p = planAsk(be, tok, heads, q, opts);
|
| 1011 |
+
return p.read(await be.prefill(p.ids, { positions: p.positions }));
|
| 1012 |
+
}
|
| 1013 |
+
function sharedPrefix(plans) {
|
| 1014 |
+
let k = Math.min(...plans.map((p) => p.ids.length - 1), ...plans.flatMap((p) => p.positions));
|
| 1015 |
+
const a = plans[0].ids;
|
| 1016 |
+
for (const p of plans.slice(1)) {
|
| 1017 |
+
let i = 0;
|
| 1018 |
+
while (i < k && p.ids[i] === a[i]) i++;
|
| 1019 |
+
k = i;
|
| 1020 |
+
}
|
| 1021 |
+
return k;
|
| 1022 |
+
}
|
| 1023 |
+
function askMany(be, tok, heads, state, qs, opts = {}) {
|
| 1024 |
+
return askManyWith(be, tok, heads, state, qs, () => opts);
|
| 1025 |
+
}
|
| 1026 |
+
async function askManyWith(be, tok, heads, state, qs, optsFor) {
|
| 1027 |
+
if (typeof state !== "string") throw new Error("askMany: state must be a string");
|
| 1028 |
+
const plans = qs.map((q0) => {
|
| 1029 |
+
if (q0?.state !== void 0 && q0.state !== state) throw new Error("askMany: a question carries a state different from the shared state");
|
| 1030 |
+
const q = { ...q0, state };
|
| 1031 |
+
return planAsk(be, tok, heads, q, optsFor(q));
|
| 1032 |
+
});
|
| 1033 |
+
const out = [];
|
| 1034 |
+
const k = plans.length > 1 && be.snapshot && be.prefillFrom ? sharedPrefix(plans) : 0;
|
| 1035 |
+
if (k < 1) {
|
| 1036 |
+
for (const p of plans) out.push(p.read(await be.prefill(p.ids, { positions: p.positions })));
|
| 1037 |
+
return out;
|
| 1038 |
+
}
|
| 1039 |
+
const h = await be.snapshot(plans[0].ids.slice(0, k));
|
| 1040 |
+
try {
|
| 1041 |
+
for (const p of plans) out.push(p.read(await be.prefillFrom(h, p.ids, { positions: p.positions })));
|
| 1042 |
+
} finally {
|
| 1043 |
+
await h.dispose();
|
| 1044 |
+
}
|
| 1045 |
+
return out;
|
| 1046 |
+
}
|
| 1047 |
+
|
| 1048 |
+
// src/webllm.ts
|
| 1049 |
+
function logitCapture() {
|
| 1050 |
+
let armed = false, cap = null, sampled = [];
|
| 1051 |
+
return {
|
| 1052 |
+
processor: {
|
| 1053 |
+
processLogits(l) {
|
| 1054 |
+
if (armed) {
|
| 1055 |
+
cap = new Float32Array(l);
|
| 1056 |
+
armed = false;
|
| 1057 |
+
}
|
| 1058 |
+
return l;
|
| 1059 |
+
},
|
| 1060 |
+
processSampledToken(t) {
|
| 1061 |
+
sampled.push(t);
|
| 1062 |
+
},
|
| 1063 |
+
resetState() {
|
| 1064 |
+
}
|
| 1065 |
+
},
|
| 1066 |
+
arm() {
|
| 1067 |
+
armed = true;
|
| 1068 |
+
cap = null;
|
| 1069 |
+
},
|
| 1070 |
+
take() {
|
| 1071 |
+
const c = cap;
|
| 1072 |
+
cap = null;
|
| 1073 |
+
armed = false;
|
| 1074 |
+
return c;
|
| 1075 |
+
},
|
| 1076 |
+
drain() {
|
| 1077 |
+
const s = sampled;
|
| 1078 |
+
sampled = [];
|
| 1079 |
+
return s;
|
| 1080 |
+
},
|
| 1081 |
+
reset() {
|
| 1082 |
+
armed = false;
|
| 1083 |
+
cap = null;
|
| 1084 |
+
sampled = [];
|
| 1085 |
+
}
|
| 1086 |
+
};
|
| 1087 |
+
}
|
| 1088 |
+
function normalizeRecovery(r) {
|
| 1089 |
+
const N = r.rows.length;
|
| 1090 |
+
if (!N || r.P.length % N) throw new Error(`webllm: P (${r.P.length}) is not a multiple of rows (${N})`);
|
| 1091 |
+
let P;
|
| 1092 |
+
if (r.P instanceof Uint16Array) {
|
| 1093 |
+
P = new Float32Array(r.P.length);
|
| 1094 |
+
for (let i = 0; i < P.length; i++) P[i] = halfToNumber(r.P[i]);
|
| 1095 |
+
} else P = r.P;
|
| 1096 |
+
return { rows: r.rows, P, D: P.length / N };
|
| 1097 |
+
}
|
| 1098 |
+
function recoverHidden(rec, logits) {
|
| 1099 |
+
const r = "D" in rec ? rec : normalizeRecovery(rec);
|
| 1100 |
+
const N = r.rows.length, sub = new Float32Array(N), h = new Float32Array(r.D);
|
| 1101 |
+
for (let n = 0; n < N; n++) sub[n] = logits[r.rows[n]];
|
| 1102 |
+
for (let d = 0; d < r.D; d++) {
|
| 1103 |
+
let acc = 0;
|
| 1104 |
+
const off = d * N;
|
| 1105 |
+
for (let n = 0; n < N; n++) acc += r.P[off + n] * sub[n];
|
| 1106 |
+
h[d] = acc;
|
| 1107 |
+
}
|
| 1108 |
+
return h;
|
| 1109 |
+
}
|
| 1110 |
+
async function loadRecovery(rowsUrl, pUrl, format = pUrl.endsWith(".f16") ? "f16" : "f32", fetchFn = fetch) {
|
| 1111 |
+
const [rb, pb] = await Promise.all([
|
| 1112 |
+
fetchFn(rowsUrl).then((r) => {
|
| 1113 |
+
if (!r.ok) throw new Error("webllm: " + rowsUrl + " " + r.status);
|
| 1114 |
+
return r.arrayBuffer();
|
| 1115 |
+
}),
|
| 1116 |
+
fetchFn(pUrl).then((r) => {
|
| 1117 |
+
if (!r.ok) throw new Error("webllm: " + pUrl + " " + r.status);
|
| 1118 |
+
return r.arrayBuffer();
|
| 1119 |
+
})
|
| 1120 |
+
]);
|
| 1121 |
+
return { rows: new Int32Array(rb), P: format === "f16" ? new Uint16Array(pb) : new Float32Array(pb) };
|
| 1122 |
+
}
|
| 1123 |
+
async function tryLoadRecovery(base, files, fetchFn = fetch) {
|
| 1124 |
+
try {
|
| 1125 |
+
const rec = await loadRecovery(new URL(files.rows, base).href, new URL(files.P, base).href, files.P.endsWith(".f16") ? "f16" : "f32", fetchFn);
|
| 1126 |
+
const N = rec.rows.length;
|
| 1127 |
+
if (!N || rec.P.length % N) throw new Error(`P (${rec.P.length}) is not a multiple of rows (${N})`);
|
| 1128 |
+
return { recovery: rec };
|
| 1129 |
+
} catch (e) {
|
| 1130 |
+
return { reason: e?.message ?? String(e) };
|
| 1131 |
+
}
|
| 1132 |
+
}
|
| 1133 |
+
function webllmBackend(engine, capture, opts) {
|
| 1134 |
+
if (!opts.tok?.decode) throw new Error("webllm: opts.tok (tokenizer with decode) is required; WebLLM only accepts text");
|
| 1135 |
+
const eos = opts.eos ?? EOS;
|
| 1136 |
+
let rec = opts.recovery ? normalizeRecovery(opts.recovery) : null;
|
| 1137 |
+
const gate = gateFor(engine, "webllm");
|
| 1138 |
+
const decode = (ids) => opts.tok.decode(ids, { skip_special_tokens: false, clean_up_tokenization_spaces: false });
|
| 1139 |
+
async function logitsAt(ids, k) {
|
| 1140 |
+
await engine.resetChat();
|
| 1141 |
+
capture.reset();
|
| 1142 |
+
capture.arm();
|
| 1143 |
+
const r = await engine.completions.create({ prompt: decode(ids.slice(0, k + 1)), max_tokens: 1, temperature: 0 });
|
| 1144 |
+
const L = capture.take();
|
| 1145 |
+
if (!L) throw new Error("webllm: the LogitProcessor captured no logits (engine created without this capture's logitProcessorRegistry, or a Web Worker?)");
|
| 1146 |
+
const n = r?.usage?.prompt_tokens;
|
| 1147 |
+
if (n !== void 0 && n !== k + 1) throw new Error(`webllm: the prompt truncated at position ${k} re-tokenized to ${n} tokens (expected ${k + 1}); the position read would be wrong`);
|
| 1148 |
+
return L;
|
| 1149 |
+
}
|
| 1150 |
+
return {
|
| 1151 |
+
kind: "webllm",
|
| 1152 |
+
hiddenName: rec ? "recovered-from-logits" : null,
|
| 1153 |
+
dispose() {
|
| 1154 |
+
return gate.dispose(async () => {
|
| 1155 |
+
rec = null;
|
| 1156 |
+
await engine.unload?.();
|
| 1157 |
+
});
|
| 1158 |
+
},
|
| 1159 |
+
async prefill(ids, want) {
|
| 1160 |
+
gate.assertLive();
|
| 1161 |
+
if (!ids.length) throw new Error("prefill: empty ids");
|
| 1162 |
+
for (const p of want.positions) if (!Number.isInteger(p) || p < 0 || p >= ids.length) throw new Error(`webllm: position ${p} out of [0,${ids.length})`);
|
| 1163 |
+
if (want.positions.length && !rec) throw new Error("webllm: without opts.recovery the backend cannot recover hidden states; use method 'logits'");
|
| 1164 |
+
const rel = await gate.acquire();
|
| 1165 |
+
try {
|
| 1166 |
+
const last = ids.length - 1;
|
| 1167 |
+
const lastLogits = await logitsAt(ids, last);
|
| 1168 |
+
const at = /* @__PURE__ */ new Map();
|
| 1169 |
+
for (const p of new Set(want.positions)) at.set(p, recoverHidden(rec, p === last ? lastLogits : await logitsAt(ids, p)));
|
| 1170 |
+
return { hiddens: want.positions.map((p) => at.get(p).slice()), lastLogits };
|
| 1171 |
+
} finally {
|
| 1172 |
+
rel();
|
| 1173 |
+
}
|
| 1174 |
+
},
|
| 1175 |
+
async *generate(ids, o) {
|
| 1176 |
+
gate.assertLive();
|
| 1177 |
+
const g = resolveGenerateOptions(o);
|
| 1178 |
+
if (!ids.length) throw new Error("generate: empty ids");
|
| 1179 |
+
if (g.maxNewTokens === 0) return;
|
| 1180 |
+
const rel = await gate.acquire();
|
| 1181 |
+
let iter = null, finished = false;
|
| 1182 |
+
try {
|
| 1183 |
+
if (o.signal?.aborted) return;
|
| 1184 |
+
await engine.resetChat();
|
| 1185 |
+
capture.reset();
|
| 1186 |
+
const stream = await engine.completions.create({
|
| 1187 |
+
prompt: decode(ids),
|
| 1188 |
+
stream: true,
|
| 1189 |
+
max_tokens: g.maxNewTokens,
|
| 1190 |
+
temperature: g.temperature,
|
| 1191 |
+
top_p: g.topP,
|
| 1192 |
+
seed: g.seed,
|
| 1193 |
+
repetition_penalty: g.repetitionPenalty
|
| 1194 |
+
});
|
| 1195 |
+
iter = stream[Symbol.asyncIterator]();
|
| 1196 |
+
let n = 0, stop = false;
|
| 1197 |
+
while (!stop) {
|
| 1198 |
+
if (o.signal?.aborted) break;
|
| 1199 |
+
const nx = await iter.next();
|
| 1200 |
+
const toks = capture.drain();
|
| 1201 |
+
if (nx.done) finished = true;
|
| 1202 |
+
for (const t of toks) {
|
| 1203 |
+
if (eos.has(t)) {
|
| 1204 |
+
stop = true;
|
| 1205 |
+
break;
|
| 1206 |
+
}
|
| 1207 |
+
if (n >= g.maxNewTokens) {
|
| 1208 |
+
stop = true;
|
| 1209 |
+
break;
|
| 1210 |
+
}
|
| 1211 |
+
n++;
|
| 1212 |
+
yield t;
|
| 1213 |
+
if (o.signal?.aborted) {
|
| 1214 |
+
stop = true;
|
| 1215 |
+
break;
|
| 1216 |
+
}
|
| 1217 |
+
}
|
| 1218 |
+
if (nx.done) break;
|
| 1219 |
+
}
|
| 1220 |
+
} finally {
|
| 1221 |
+
if (iter) {
|
| 1222 |
+
try {
|
| 1223 |
+
if (!finished) {
|
| 1224 |
+
await engine.interruptGenerate();
|
| 1225 |
+
for (let i = 0; i < 1e5; i++) {
|
| 1226 |
+
const nx = await iter.next();
|
| 1227 |
+
if (nx.done) break;
|
| 1228 |
+
}
|
| 1229 |
+
}
|
| 1230 |
+
} catch {
|
| 1231 |
+
}
|
| 1232 |
+
try {
|
| 1233 |
+
await iter.return?.();
|
| 1234 |
+
} catch {
|
| 1235 |
+
}
|
| 1236 |
+
}
|
| 1237 |
+
capture.reset();
|
| 1238 |
+
rel();
|
| 1239 |
+
}
|
| 1240 |
+
}
|
| 1241 |
+
};
|
| 1242 |
+
}
|
| 1243 |
+
async function createWebllm(webllm, opts) {
|
| 1244 |
+
const capture = logitCapture();
|
| 1245 |
+
const appConfig = opts.appConfig ?? { model_list: [{ model: opts.modelUrl, model_id: opts.modelId, model_lib: opts.libUrl }] };
|
| 1246 |
+
const recovery = opts.recovery ?? (opts.recoveryUrls ? await loadRecovery(opts.recoveryUrls.rows, opts.recoveryUrls.P, opts.recoveryUrls.format) : void 0);
|
| 1247 |
+
const engine = await webllm.CreateMLCEngine(opts.modelId, {
|
| 1248 |
+
appConfig,
|
| 1249 |
+
logitProcessorRegistry: /* @__PURE__ */ new Map([[opts.modelId, capture.processor]]),
|
| 1250 |
+
initProgressCallback: opts.initProgressCallback
|
| 1251 |
+
});
|
| 1252 |
+
return Object.assign(webllmBackend(engine, capture, { tok: opts.tok, recovery, eos: opts.eos }), { engine, capture });
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
// src/backends/litert.ts
|
| 1256 |
+
var LITERTJS_VERSION = "2.6.0";
|
| 1257 |
+
var LITERT_WARNING = `ct0-edge: runtime "litert" is opt-in (LiteRT.js @litertjs/core ${LITERTJS_VERSION}, WebGPU with streamed weights; about 10x slower per decision than "genai"). Pass quiet: true to silence this note.`;
|
| 1258 |
+
function h2f(h) {
|
| 1259 |
+
const s = h & 32768 ? -1 : 1, e = h >> 10 & 31, f = h & 1023;
|
| 1260 |
+
return e === 0 ? s * 2 ** -14 * (f / 1024) : e === 31 ? f ? NaN : s * Infinity : s * 2 ** (e - 15) * (1 + f / 1024);
|
| 1261 |
+
}
|
| 1262 |
+
function embedFromF16Reader(read, hidden) {
|
| 1263 |
+
return {
|
| 1264 |
+
async rows(ids) {
|
| 1265 |
+
const out = new Float32Array(ids.length * hidden), got = /* @__PURE__ */ new Map();
|
| 1266 |
+
await Promise.all([...new Set(ids)].map(async (id) => {
|
| 1267 |
+
const b = await read(id * hidden * 2, hidden * 2);
|
| 1268 |
+
const c = b.byteOffset % 2 ? b.slice() : b;
|
| 1269 |
+
got.set(id, new Uint16Array(c.buffer, c.byteOffset, hidden));
|
| 1270 |
+
}));
|
| 1271 |
+
ids.forEach((id, r) => {
|
| 1272 |
+
const q = got.get(id);
|
| 1273 |
+
for (let j = 0; j < hidden; j++) out[r * hidden + j] = h2f(q[j]);
|
| 1274 |
+
});
|
| 1275 |
+
return out;
|
| 1276 |
+
}
|
| 1277 |
+
};
|
| 1278 |
+
}
|
| 1279 |
+
function httpRangeReader(url, fetchFn = fetch) {
|
| 1280 |
+
return async (offset, length) => {
|
| 1281 |
+
const r = await fetchFn(url, { headers: { Range: `bytes=${offset}-${offset + length - 1}` } });
|
| 1282 |
+
if (!r.ok) throw new Error(`litert: ${url} -> HTTP ${r.status}`);
|
| 1283 |
+
const b = new Uint8Array(await r.arrayBuffer());
|
| 1284 |
+
return r.status === 206 ? b : b.subarray(offset, offset + length);
|
| 1285 |
+
};
|
| 1286 |
+
}
|
| 1287 |
+
var MASK_OFF = globalThis.__CT0_MASK_OFF ?? -1e30;
|
| 1288 |
+
function buildMask(pos, cache, dtype) {
|
| 1289 |
+
const rows = pos.length;
|
| 1290 |
+
if (dtype === "bool") {
|
| 1291 |
+
const m2 = new Uint8Array(rows * cache);
|
| 1292 |
+
for (let r = 0; r < rows; r++) m2.fill(1, r * cache, r * cache + pos[r] + 1);
|
| 1293 |
+
return m2;
|
| 1294 |
+
}
|
| 1295 |
+
const m = new Float32Array(rows * cache).fill(MASK_OFF);
|
| 1296 |
+
for (let r = 0; r < rows; r++) m.fill(0, r * cache, r * cache + pos[r] + 1);
|
| 1297 |
+
return m;
|
| 1298 |
+
}
|
| 1299 |
+
function prefillChunks(n, P) {
|
| 1300 |
+
const out = [];
|
| 1301 |
+
for (let s = 0; s < n; s += P) out.push({ start: s, len: Math.min(P, n - s) });
|
| 1302 |
+
return out;
|
| 1303 |
+
}
|
| 1304 |
+
var isState = (n) => n.startsWith("kv_cache_");
|
| 1305 |
+
var zerosOf = (io) => {
|
| 1306 |
+
const n = io.shape.reduce((a, b) => a * b, 1);
|
| 1307 |
+
return io.dtype === "int32" ? new Int32Array(n) : new Float32Array(n);
|
| 1308 |
+
};
|
| 1309 |
+
async function createLiteRt(lrt, graph, manifest, embed, opts = {}) {
|
| 1310 |
+
if (!opts.quiet) console.warn(LITERT_WARNING);
|
| 1311 |
+
const accelerator = opts.accelerator ?? "webgpu";
|
| 1312 |
+
const sigNames = Object.keys(manifest.signatures);
|
| 1313 |
+
const prefillName = sigNames.filter((s) => /^prefill_\d+$/.test(s)).sort((a, b) => +b.split("_")[1] - +a.split("_")[1])[0];
|
| 1314 |
+
if (!prefillName || !manifest.signatures.decode) throw new Error(`litert: the graph needs prefill_<N> and decode signatures (has ${sigNames.join(", ")})`);
|
| 1315 |
+
const P = +prefillName.split("_")[1];
|
| 1316 |
+
const dec = manifest.signatures.decode;
|
| 1317 |
+
const stateIO = dec.inputs.filter((i) => isState(i.name));
|
| 1318 |
+
const maskIO = dec.inputs.find((i) => i.name === "mask");
|
| 1319 |
+
if (!maskIO) throw new Error("litert: decode signature has no mask input");
|
| 1320 |
+
const cache = maskIO.shape[maskIO.shape.length - 1];
|
| 1321 |
+
const H = manifest.hidden;
|
| 1322 |
+
const paramIO = dec.inputs.find((i) => i.name === "param_tensor");
|
| 1323 |
+
const inplace = !!paramIO;
|
| 1324 |
+
const hiddenOut = (dec.outputs.find((o) => o.name === "hidden") ?? dec.outputs.find((o) => /(^|[_/])hidden(_states)?$/.test(o.name)))?.name ?? null;
|
| 1325 |
+
const gpuOptions = { precision: opts.gpuPrecision ?? "fp32" };
|
| 1326 |
+
const copyState = opts.copyState ?? accelerator === "webgpu";
|
| 1327 |
+
const model = accelerator === "webgpu" && opts.weights ? await lrt.loadModelAndWeights(graph, { getReader: () => opts.weights().getReader() }, { accelerator: "webgpu", gpuOptions }) : await lrt.loadAndCompile(graph, accelerator === "webgpu" ? { accelerator, gpuOptions } : { accelerator });
|
| 1328 |
+
const T = (data, shape, dtype) => dtype ? new lrt.Tensor(data, shape, dtype) : new lrt.Tensor(data, shape);
|
| 1329 |
+
const del = (t) => {
|
| 1330 |
+
try {
|
| 1331 |
+
t?.delete?.();
|
| 1332 |
+
} catch {
|
| 1333 |
+
}
|
| 1334 |
+
};
|
| 1335 |
+
const zeroStateHost = () => {
|
| 1336 |
+
const st = {};
|
| 1337 |
+
for (const io of stateIO) st[io.name] = T(zerosOf(io), io.shape);
|
| 1338 |
+
return st;
|
| 1339 |
+
};
|
| 1340 |
+
async function freshState() {
|
| 1341 |
+
const st = zeroStateHost();
|
| 1342 |
+
if (inplace && accelerator === "webgpu") for (const k in st) {
|
| 1343 |
+
const g = await st[k].moveTo("webgpu");
|
| 1344 |
+
st[k] = g;
|
| 1345 |
+
}
|
| 1346 |
+
return st;
|
| 1347 |
+
}
|
| 1348 |
+
const freeState = (st) => {
|
| 1349 |
+
for (const k in st) del(st[k]);
|
| 1350 |
+
};
|
| 1351 |
+
async function run(sig, emb, rows, pos, st, valid = rows) {
|
| 1352 |
+
const feed = { ...st, embeddings: T(emb, [1, rows, H]), input_pos: T(pos, [rows]), mask: T(buildMask(pos, cache, maskIO.dtype), [1, 1, rows, cache], maskIO.dtype === "bool" ? "bool" : void 0) };
|
| 1353 |
+
if (paramIO) {
|
| 1354 |
+
const p = new Int32Array(paramIO.shape.reduce((a, b) => a * b, 1));
|
| 1355 |
+
p[0] = pos[0];
|
| 1356 |
+
p[1] = pos[0] + valid;
|
| 1357 |
+
p[2] = pos[0] + valid;
|
| 1358 |
+
feed.param_tensor = T(p, paramIO.shape);
|
| 1359 |
+
}
|
| 1360 |
+
const out = await model.run(sig, feed);
|
| 1361 |
+
del(feed.embeddings);
|
| 1362 |
+
del(feed.input_pos);
|
| 1363 |
+
del(feed.mask);
|
| 1364 |
+
del(feed.param_tensor);
|
| 1365 |
+
if (inplace) return { st, logits: out.logits, hidden: hiddenOut ? out[hiddenOut] : void 0 };
|
| 1366 |
+
const ns = {};
|
| 1367 |
+
for (const io of stateIO) {
|
| 1368 |
+
const t = out[io.name];
|
| 1369 |
+
if (!t) throw new Error(`litert: ${sig} did not return ${io.name}`);
|
| 1370 |
+
if (copyState && t.copyTo) {
|
| 1371 |
+
ns[io.name] = await t.copyTo("wasm");
|
| 1372 |
+
del(t);
|
| 1373 |
+
} else ns[io.name] = t;
|
| 1374 |
+
}
|
| 1375 |
+
freeState(st);
|
| 1376 |
+
return { st: ns, logits: out.logits, hidden: hiddenOut ? out[hiddenOut] : void 0 };
|
| 1377 |
+
}
|
| 1378 |
+
async function readLogits(L) {
|
| 1379 |
+
const c = L.moveTo ? await L.moveTo("wasm") : L;
|
| 1380 |
+
const a = Float32Array.from(c.toTypedArray());
|
| 1381 |
+
if (c !== L) del(c);
|
| 1382 |
+
del(L);
|
| 1383 |
+
return a.subarray(a.length - manifest.vocab);
|
| 1384 |
+
}
|
| 1385 |
+
async function collectHidden(hid, firstPos, rows, want, at) {
|
| 1386 |
+
if (!hid || !want.size) {
|
| 1387 |
+
del(hid);
|
| 1388 |
+
return;
|
| 1389 |
+
}
|
| 1390 |
+
const c = hid.moveTo ? await hid.moveTo("wasm") : hid;
|
| 1391 |
+
const a = c.toTypedArray();
|
| 1392 |
+
for (let r = 0; r < rows; r++) {
|
| 1393 |
+
const p = firstPos + r;
|
| 1394 |
+
if (want.has(p)) at.set(p, Float32Array.from(a.subarray(r * H, r * H + H)));
|
| 1395 |
+
}
|
| 1396 |
+
if (c !== hid) del(c);
|
| 1397 |
+
del(hid);
|
| 1398 |
+
}
|
| 1399 |
+
async function prefillTokens(ids, from, end, st, want, at) {
|
| 1400 |
+
const grab = async (r, firstPos, rows) => {
|
| 1401 |
+
if (want && at) await collectHidden(r.hidden, firstPos, rows, want, at);
|
| 1402 |
+
else del(r.hidden);
|
| 1403 |
+
};
|
| 1404 |
+
if (opts.decodeOnlyPrefill) {
|
| 1405 |
+
for (let i = from; i < end; i++) {
|
| 1406 |
+
const r = await run("decode", await embed.rows([ids[i]]), 1, Int32Array.from([i]), st);
|
| 1407 |
+
st = r.st;
|
| 1408 |
+
del(r.logits);
|
| 1409 |
+
await grab(r, i, 1);
|
| 1410 |
+
}
|
| 1411 |
+
return st;
|
| 1412 |
+
}
|
| 1413 |
+
const remDec = opts.remainderViaDecode ?? accelerator === "webgpu";
|
| 1414 |
+
for (const c of prefillChunks(end - from, P)) {
|
| 1415 |
+
if (remDec && c.len < P) {
|
| 1416 |
+
for (let i = from + c.start; i < from + c.start + c.len; i++) {
|
| 1417 |
+
const r2 = await run("decode", await embed.rows([ids[i]]), 1, Int32Array.from([i]), st);
|
| 1418 |
+
st = r2.st;
|
| 1419 |
+
del(r2.logits);
|
| 1420 |
+
await grab(r2, i, 1);
|
| 1421 |
+
}
|
| 1422 |
+
continue;
|
| 1423 |
+
}
|
| 1424 |
+
const toks = ids.slice(from + c.start, from + c.start + c.len);
|
| 1425 |
+
const emb = new Float32Array(P * H);
|
| 1426 |
+
emb.set(await embed.rows(toks));
|
| 1427 |
+
const pos = new Int32Array(P);
|
| 1428 |
+
for (let i = 0; i < c.len; i++) pos[i] = from + c.start + i;
|
| 1429 |
+
const r = await run(prefillName, emb, P, pos, st, c.len);
|
| 1430 |
+
st = r.st;
|
| 1431 |
+
await grab(r, from + c.start, c.len);
|
| 1432 |
+
}
|
| 1433 |
+
return st;
|
| 1434 |
+
}
|
| 1435 |
+
async function decodeOne(tid, p, st) {
|
| 1436 |
+
const r = await run("decode", await embed.rows([tid]), 1, Int32Array.from([p]), st);
|
| 1437 |
+
return { st: r.st, logits: await readLogits(r.logits), hidden: r.hidden };
|
| 1438 |
+
}
|
| 1439 |
+
async function feedFrom(ids, from, st, positions) {
|
| 1440 |
+
const want = new Set(positions);
|
| 1441 |
+
const at = /* @__PURE__ */ new Map();
|
| 1442 |
+
const last = ids.length - 1;
|
| 1443 |
+
st = await prefillTokens(ids, from, last, st, want, at);
|
| 1444 |
+
const r = await decodeOne(ids[last], last, st);
|
| 1445 |
+
await collectHidden(r.hidden, last, 1, want, at);
|
| 1446 |
+
freeState(r.st);
|
| 1447 |
+
return { lastLogits: r.logits, hiddens: positions.map((p) => at.get(p)) };
|
| 1448 |
+
}
|
| 1449 |
+
const checkHidden = (want) => {
|
| 1450 |
+
if (want.positions.length && !hiddenOut) throw new Error('litert: this graph has no hidden-state output; use ask() with method "logits"');
|
| 1451 |
+
};
|
| 1452 |
+
const gate = createGate("litert");
|
| 1453 |
+
const snaps = /* @__PURE__ */ new WeakMap();
|
| 1454 |
+
const snapshotState = async (st) => Promise.all(stateIO.map(async (io) => {
|
| 1455 |
+
const t = st[io.name];
|
| 1456 |
+
const c = t.moveTo ? await t.moveTo("wasm") : t;
|
| 1457 |
+
const a = Float32Array.from(c.toTypedArray());
|
| 1458 |
+
if (c !== t) del(c);
|
| 1459 |
+
return a;
|
| 1460 |
+
}));
|
| 1461 |
+
const fromCopy = async (arrs) => {
|
| 1462 |
+
const st = {};
|
| 1463 |
+
stateIO.forEach((io, i) => {
|
| 1464 |
+
st[io.name] = T(arrs[i].slice(), io.shape);
|
| 1465 |
+
});
|
| 1466 |
+
if (inplace && accelerator === "webgpu") for (const k in st) st[k] = await st[k].moveTo("webgpu");
|
| 1467 |
+
return st;
|
| 1468 |
+
};
|
| 1469 |
+
return {
|
| 1470 |
+
kind: "litert",
|
| 1471 |
+
hiddenName: hiddenOut,
|
| 1472 |
+
dispose() {
|
| 1473 |
+
return gate.dispose(() => model.delete?.());
|
| 1474 |
+
},
|
| 1475 |
+
async prefill(ids, want) {
|
| 1476 |
+
gate.assertLive();
|
| 1477 |
+
checkHidden(want);
|
| 1478 |
+
if (!ids.length) throw new Error("prefill: empty ids");
|
| 1479 |
+
const rel = await gate.acquire();
|
| 1480 |
+
try {
|
| 1481 |
+
return await feedFrom(ids, 0, await freshState(), want.positions);
|
| 1482 |
+
} finally {
|
| 1483 |
+
rel();
|
| 1484 |
+
}
|
| 1485 |
+
},
|
| 1486 |
+
async snapshot(ids) {
|
| 1487 |
+
gate.assertLive();
|
| 1488 |
+
if (!ids.length) throw new Error("litert: snapshot of empty ids");
|
| 1489 |
+
const rel = await gate.acquire();
|
| 1490 |
+
try {
|
| 1491 |
+
const st = await prefillTokens(ids, 0, ids.length, await freshState());
|
| 1492 |
+
const h = { ids: ids.slice(), dispose() {
|
| 1493 |
+
snaps.delete(h);
|
| 1494 |
+
} };
|
| 1495 |
+
snaps.set(h, await snapshotState(st));
|
| 1496 |
+
freeState(st);
|
| 1497 |
+
return h;
|
| 1498 |
+
} finally {
|
| 1499 |
+
rel();
|
| 1500 |
+
}
|
| 1501 |
+
},
|
| 1502 |
+
async prefillFrom(h, ids, want) {
|
| 1503 |
+
gate.assertLive();
|
| 1504 |
+
checkHidden(want);
|
| 1505 |
+
const arrs = snaps.get(h);
|
| 1506 |
+
if (!arrs) throw new Error("litert: unknown or released snapshot handle");
|
| 1507 |
+
const k = checkContinuation("litert", h, ids, want.positions);
|
| 1508 |
+
const rel = await gate.acquire();
|
| 1509 |
+
try {
|
| 1510 |
+
return await feedFrom(ids, k, await fromCopy(arrs), want.positions);
|
| 1511 |
+
} finally {
|
| 1512 |
+
rel();
|
| 1513 |
+
}
|
| 1514 |
+
},
|
| 1515 |
+
async *generate(ids, o) {
|
| 1516 |
+
gate.assertLive();
|
| 1517 |
+
const g = resolveGenerateOptions(o), rand = mulberry32(g.seed);
|
| 1518 |
+
if (!ids.length) throw new Error("generate: empty ids");
|
| 1519 |
+
if (g.maxNewTokens === 0) return;
|
| 1520 |
+
const rel = await gate.acquire();
|
| 1521 |
+
let st = null;
|
| 1522 |
+
try {
|
| 1523 |
+
st = await prefillTokens(ids, 0, ids.length - 1, await freshState());
|
| 1524 |
+
let r = await decodeOne(ids[ids.length - 1], ids.length - 1, st);
|
| 1525 |
+
st = r.st;
|
| 1526 |
+
const gen = [];
|
| 1527 |
+
for (let s = 0, pos = ids.length; s < g.maxNewTokens; s++, pos++) {
|
| 1528 |
+
if (g.signal?.aborted || pos >= cache) break;
|
| 1529 |
+
const tid = sample(r.logits, gen, rand, g);
|
| 1530 |
+
if (EOS.has(tid)) break;
|
| 1531 |
+
gen.push(tid);
|
| 1532 |
+
yield tid;
|
| 1533 |
+
if (s + 1 >= g.maxNewTokens || g.signal?.aborted) break;
|
| 1534 |
+
r = await decodeOne(tid, pos, st);
|
| 1535 |
+
st = r.st;
|
| 1536 |
+
}
|
| 1537 |
+
} finally {
|
| 1538 |
+
if (st) freeState(st);
|
| 1539 |
+
rel();
|
| 1540 |
+
}
|
| 1541 |
+
}
|
| 1542 |
+
};
|
| 1543 |
+
}
|
| 1544 |
+
|
| 1545 |
+
// src/runtime.ts
|
| 1546 |
+
var DEFAULT_REPOS = {
|
| 1547 |
+
genai: "eduardoworrel/alias3-micro-1026-ONNX-genai",
|
| 1548 |
+
decodeOnly: "eduardoworrel/alias3-micro-1026-ONNX-decode-only",
|
| 1549 |
+
onnx: "eduardoworrel/alias3-micro-1026-ONNX",
|
| 1550 |
+
mlcQ4f16: "eduardoworrel/alias3-micro-1026-MLC-q4f16_1",
|
| 1551 |
+
mlcQ4f32: "eduardoworrel/alias3-micro-1026-MLC-q4f32_1",
|
| 1552 |
+
litert: "eduardoworrel/alias3-micro-1026-LiteRT"
|
| 1553 |
+
};
|
| 1554 |
+
var LITERT_QUANTS = ["int4", "int8", "int4-b64"];
|
| 1555 |
+
function litertBundleBase(o = {}) {
|
| 1556 |
+
if (o.litertBaseUrl) return o.litertBaseUrl;
|
| 1557 |
+
const q = o.litertQuant ?? "int4";
|
| 1558 |
+
if (!LITERT_QUANTS.includes(q)) throw new Error(`ct0-edge: litertQuant must be one of ${LITERT_QUANTS.join(", ")} (got ${JSON.stringify(q)})`);
|
| 1559 |
+
return `https://huggingface.co/${o.litertRepo ?? DEFAULT_REPOS.litert}/resolve/main/litert/${q}/`;
|
| 1560 |
+
}
|
| 1561 |
+
var DEFAULT_FILES = {
|
| 1562 |
+
head: "heads/head.bin",
|
| 1563 |
+
headScore: "heads/head_score.bin"
|
| 1564 |
+
};
|
| 1565 |
+
async function detectWebGPU(nav = globalThis.navigator) {
|
| 1566 |
+
try {
|
| 1567 |
+
const gpu = nav?.gpu;
|
| 1568 |
+
if (!gpu?.requestAdapter) return false;
|
| 1569 |
+
return !!await gpu.requestAdapter();
|
| 1570 |
+
} catch {
|
| 1571 |
+
return false;
|
| 1572 |
+
}
|
| 1573 |
+
}
|
| 1574 |
+
var RUNTIMES = ["genai", "decode-only", "transformers.js", "webllm", "litert"];
|
| 1575 |
+
var DEVICES = ["webgpu", "wasm", "cpu", "cuda"];
|
| 1576 |
+
var RUNTIME_LIMITS = {
|
| 1577 |
+
genai: { maxSafeTokens: 2048, byDevice: { webgpu: 7200 } },
|
| 1578 |
+
"decode-only": { maxSafeTokens: 2048, byDevice: { webgpu: 3600, cpu: 7200 } },
|
| 1579 |
+
"transformers.js": { maxSafeTokens: 2048 },
|
| 1580 |
+
webllm: { maxSafeTokens: 2048 },
|
| 1581 |
+
litert: { maxSafeTokens: 2e3 }
|
| 1582 |
+
};
|
| 1583 |
+
function maxSafeTokensFor(runtime, device) {
|
| 1584 |
+
const l = RUNTIME_LIMITS[runtime];
|
| 1585 |
+
return l.byDevice?.[device] ?? l.maxSafeTokens;
|
| 1586 |
+
}
|
| 1587 |
+
var MLC_Q4F32_LIB = "https://raw.githubusercontent.com/mlc-ai/binary-mlc-llm-libs/main/web-llm-models/v0_2_86/base/Qwen3.5-4B-q4f32_1_cs1k-webgpu.wasm";
|
| 1588 |
+
var RUNTIME_DEFAULTS = {
|
| 1589 |
+
genai: {
|
| 1590 |
+
repo: DEFAULT_REPOS.genai,
|
| 1591 |
+
files: { webgpu: "webgpu/int4/model.onnx", cuda: "cuda/int4/model.onnx" },
|
| 1592 |
+
heads: true,
|
| 1593 |
+
methods: DEFAULT_METHOD
|
| 1594 |
+
},
|
| 1595 |
+
"decode-only": {
|
| 1596 |
+
repo: DEFAULT_REPOS.decodeOnly,
|
| 1597 |
+
file: "onnx/model_q4.onnx",
|
| 1598 |
+
heads: true,
|
| 1599 |
+
methods: DEFAULT_METHOD
|
| 1600 |
+
},
|
| 1601 |
+
"transformers.js": {
|
| 1602 |
+
repo: DEFAULT_REPOS.onnx,
|
| 1603 |
+
dtype: "q4",
|
| 1604 |
+
heads: true,
|
| 1605 |
+
methods: DEFAULT_METHOD
|
| 1606 |
+
},
|
| 1607 |
+
webllm: {
|
| 1608 |
+
builds: {
|
| 1609 |
+
q4f16_1: {
|
| 1610 |
+
repo: DEFAULT_REPOS.mlcQ4f16,
|
| 1611 |
+
modelId: "alias3-micro-1026-q4f16_1",
|
| 1612 |
+
lib: "model-lib.wasm",
|
| 1613 |
+
recovery: { rows: "recovery/P_q4f16_rows.i32", P: "recovery/P_q4f16_8192.f16" }
|
| 1614 |
+
},
|
| 1615 |
+
q4f32_1: {
|
| 1616 |
+
repo: DEFAULT_REPOS.mlcQ4f32,
|
| 1617 |
+
modelId: "alias3-micro-1026-q4f32_1",
|
| 1618 |
+
lib: MLC_Q4F32_LIB,
|
| 1619 |
+
recovery: { rows: "recovery/P_q4f32_rows.i32", P: "recovery/P_q4f32_8192.f16" }
|
| 1620 |
+
}
|
| 1621 |
+
},
|
| 1622 |
+
build: "q4f32_1",
|
| 1623 |
+
heads: true,
|
| 1624 |
+
methods: DEFAULT_METHOD,
|
| 1625 |
+
logitsOnlyMethods: { noul: "logits", choice: "logits", score: "logits" }
|
| 1626 |
+
},
|
| 1627 |
+
litert: {
|
| 1628 |
+
repo: DEFAULT_REPOS.litert,
|
| 1629 |
+
files: { manifest: "litert/manifest.json", graph: "litert/model.graph.tflite", weights: "litert/model.weights.bin", embed: "litert/embed.f16.bin" },
|
| 1630 |
+
wasm: "https://cdn.jsdelivr.net/npm/@litertjs/core@2.6.0/wasm/",
|
| 1631 |
+
heads: false,
|
| 1632 |
+
methods: { noul: "logits", choice: "logits", score: "logits" }
|
| 1633 |
+
}
|
| 1634 |
+
};
|
| 1635 |
+
function defaultMethods(runtime, opts = {}) {
|
| 1636 |
+
if (runtime === "webllm" && !opts.webllmRecovery) return RUNTIME_DEFAULTS.webllm.logitsOnlyMethods;
|
| 1637 |
+
if (runtime === "litert") return RUNTIME_DEFAULTS.litert.methods;
|
| 1638 |
+
return DEFAULT_METHOD;
|
| 1639 |
+
}
|
| 1640 |
+
function resolveRuntimeChoice(o, fallback = "auto") {
|
| 1641 |
+
if (o.runtime === void 0) return { choice: fallback };
|
| 1642 |
+
if (o.runtime !== "auto" && !RUNTIMES.includes(o.runtime))
|
| 1643 |
+
throw new Error(`ct0-edge: unknown runtime "${o.runtime}". Use "auto", ${RUNTIMES.map((r) => `"${r}"`).join(", ")}.`);
|
| 1644 |
+
return { choice: o.runtime };
|
| 1645 |
+
}
|
| 1646 |
+
function detectPlatform(g = globalThis) {
|
| 1647 |
+
const isNode = !!g.process?.versions?.node;
|
| 1648 |
+
const hasDom = typeof g.window !== "undefined" && typeof g.document !== "undefined";
|
| 1649 |
+
const isWorker = typeof g.WorkerGlobalScope !== "undefined" && typeof g.importScripts === "function";
|
| 1650 |
+
return isNode && !hasDom && !isWorker ? "node" : "browser";
|
| 1651 |
+
}
|
| 1652 |
+
var GENAI_NODE_CPU_ERROR = 'ct0-edge: runtime "genai" does not run on onnxruntime-node CPU (the int4 graph fails with "not enough space: expected 49152, got 0"). In Node use runtime "decode-only" (CPU) or "transformers.js", or run genai on a CUDA machine (device "cuda").';
|
| 1653 |
+
function checkDevice(platform, device) {
|
| 1654 |
+
if (!DEVICES.includes(device)) throw new Error(`ct0-edge: unknown device "${device}". Use "auto", ${DEVICES.map((d) => `"${d}"`).join(", ")}.`);
|
| 1655 |
+
if (platform === "browser" && device === "cuda") throw new Error('ct0-edge: device "cuda" is Node-only. In the browser use "webgpu" or "wasm".');
|
| 1656 |
+
if (platform === "node" && (device === "webgpu" || device === "wasm")) throw new Error(`ct0-edge: device "${device}" is browser-only. In Node use "cpu" or "cuda".`);
|
| 1657 |
+
}
|
| 1658 |
+
function deviceFor(runtime, want, caps) {
|
| 1659 |
+
const { platform } = caps;
|
| 1660 |
+
const dev = platform === "browser" && want === "cpu" ? "wasm" : want;
|
| 1661 |
+
if (dev !== "auto") checkDevice(platform, dev);
|
| 1662 |
+
const needGpu = (d) => {
|
| 1663 |
+
if (d === "webgpu" && !caps.webgpu) throw new Error(`ct0-edge: runtime "${runtime}" on WebGPU, but WebGPU is not available in this browser.`);
|
| 1664 |
+
return d;
|
| 1665 |
+
};
|
| 1666 |
+
const needCuda = (d) => {
|
| 1667 |
+
if (d === "cuda" && !caps.cuda) throw new Error(`ct0-edge: runtime "${runtime}" on CUDA, but onnxruntime-node lists no CUDA execution provider here.`);
|
| 1668 |
+
return d;
|
| 1669 |
+
};
|
| 1670 |
+
switch (runtime) {
|
| 1671 |
+
case "genai":
|
| 1672 |
+
if (platform === "browser") {
|
| 1673 |
+
if (dev === "wasm") throw new Error('ct0-edge: runtime "genai" needs WebGPU in the browser (its LinearAttention kernels have no WASM build). Use runtime "decode-only" on "wasm".');
|
| 1674 |
+
return needGpu("webgpu");
|
| 1675 |
+
}
|
| 1676 |
+
if (dev === "cpu") throw new Error(GENAI_NODE_CPU_ERROR);
|
| 1677 |
+
if (!caps.cuda) throw new Error(GENAI_NODE_CPU_ERROR + " No CUDA execution provider was found.");
|
| 1678 |
+
return "cuda";
|
| 1679 |
+
case "webllm":
|
| 1680 |
+
if (platform === "node") throw new Error('ct0-edge: runtime "webllm" runs only in the browser (WebGPU). In Node use "decode-only" or "transformers.js".');
|
| 1681 |
+
if (dev === "wasm") throw new Error('ct0-edge: runtime "webllm" needs WebGPU.');
|
| 1682 |
+
return needGpu("webgpu");
|
| 1683 |
+
case "litert":
|
| 1684 |
+
if (platform === "node") throw new Error('ct0-edge: runtime "litert" runs in the browser (LiteRT.js streams the weights into WebGPU buffers). In Node use "decode-only" or "transformers.js".');
|
| 1685 |
+
if (dev === "wasm") throw new Error('ct0-edge: runtime "litert" needs WebGPU (the CT0 weights exceed the 2 GiB LiteRT.js Wasm heap).');
|
| 1686 |
+
return needGpu("webgpu");
|
| 1687 |
+
case "decode-only":
|
| 1688 |
+
case "transformers.js":
|
| 1689 |
+
if (dev !== "auto") return platform === "browser" ? needGpu(dev) : needCuda(dev);
|
| 1690 |
+
return platform === "browser" ? caps.webgpu ? "webgpu" : "wasm" : "cpu";
|
| 1691 |
+
}
|
| 1692 |
+
}
|
| 1693 |
+
function planRuntime(choice, device = "auto", caps) {
|
| 1694 |
+
if (choice !== "auto") {
|
| 1695 |
+
if (!RUNTIMES.includes(choice)) throw new Error(`ct0-edge: unknown runtime "${String(choice)}".`);
|
| 1696 |
+
return [{ runtime: choice, device: deviceFor(choice, device, caps) }];
|
| 1697 |
+
}
|
| 1698 |
+
const { platform } = caps;
|
| 1699 |
+
const dev = platform === "browser" && device === "cpu" ? "wasm" : device;
|
| 1700 |
+
if (dev !== "auto") checkDevice(platform, dev);
|
| 1701 |
+
if (platform === "browser") {
|
| 1702 |
+
if (dev === "wasm") return [{ runtime: "decode-only", device: "wasm" }];
|
| 1703 |
+
if (dev === "webgpu" && !caps.webgpu) throw new Error('ct0-edge: device "webgpu" requested, but WebGPU is not available in this browser.');
|
| 1704 |
+
if (caps.webgpu) return [{ runtime: "genai", device: "webgpu" }, { runtime: "decode-only", device: "webgpu" }];
|
| 1705 |
+
return [{ runtime: "decode-only", device: "wasm" }];
|
| 1706 |
+
}
|
| 1707 |
+
const nodeCpu = [{ runtime: "transformers.js", device: "cpu" }, { runtime: "decode-only", device: "cpu" }];
|
| 1708 |
+
if (dev === "cpu") return nodeCpu;
|
| 1709 |
+
if (dev === "cuda") {
|
| 1710 |
+
if (!caps.cuda) throw new Error('ct0-edge: device "cuda" requested, but onnxruntime-node lists no CUDA execution provider here.');
|
| 1711 |
+
return [{ runtime: "genai", device: "cuda" }, { runtime: "transformers.js", device: "cuda" }];
|
| 1712 |
+
}
|
| 1713 |
+
if (caps.cuda) return [{ runtime: "genai", device: "cuda" }, { runtime: "transformers.js", device: "cuda" }, ...nodeCpu];
|
| 1714 |
+
return nodeCpu;
|
| 1715 |
+
}
|
| 1716 |
+
async function selectRuntime(choice, device, deps) {
|
| 1717 |
+
const status = deps.onStatus ?? (() => {
|
| 1718 |
+
});
|
| 1719 |
+
const plans = planRuntime(choice, device, deps.caps);
|
| 1720 |
+
if (choice !== "auto") return { ...plans[0], backend: await deps.load(plans[0]) };
|
| 1721 |
+
const reasons = [];
|
| 1722 |
+
if (plans[0].runtime !== "genai") {
|
| 1723 |
+
const { platform, webgpu, cuda } = deps.caps;
|
| 1724 |
+
reasons.push(platform === "browser" ? webgpu ? `device "${plans[0].device}" requested` : "WebGPU not available" : cuda ? `device "${plans[0].device}" requested` : "no CUDA execution provider (genai does not run on onnxruntime-node CPU)");
|
| 1725 |
+
}
|
| 1726 |
+
for (const [i, plan] of plans.entries()) {
|
| 1727 |
+
if (i > 0) status(`${reasons.at(-1)} -> ${plan.runtime} (${plan.device})`);
|
| 1728 |
+
try {
|
| 1729 |
+
const backend = await deps.load(plan);
|
| 1730 |
+
return { ...plan, backend, fallbackReason: reasons.length ? reasons.join("; ") : void 0 };
|
| 1731 |
+
} catch (e) {
|
| 1732 |
+
reasons.push(`${plan.runtime} (${plan.device}) unavailable: ${e?.message ?? String(e)}`);
|
| 1733 |
+
if (i === plans.length - 1) {
|
| 1734 |
+
const err = new Error(`ct0-edge: no runtime loaded. ${reasons.join("; ")}`);
|
| 1735 |
+
err.cause = e;
|
| 1736 |
+
throw err;
|
| 1737 |
+
}
|
| 1738 |
+
}
|
| 1739 |
+
}
|
| 1740 |
+
throw new Error("ct0-edge: no runtime candidate");
|
| 1741 |
+
}
|
| 1742 |
+
function packagesFor(runtime, platform) {
|
| 1743 |
+
const ort = platform === "node" ? "onnxruntime-node" : "onnxruntime-web";
|
| 1744 |
+
if (runtime === "genai" || runtime === "decode-only") return ["@huggingface/transformers", ort];
|
| 1745 |
+
if (runtime === "webllm") return ["@huggingface/transformers", "@mlc-ai/web-llm"];
|
| 1746 |
+
if (runtime === "litert") return ["@huggingface/transformers", "@litertjs/core"];
|
| 1747 |
+
return ["@huggingface/transformers"];
|
| 1748 |
+
}
|
| 1749 |
+
function isNotFound(e, pkg) {
|
| 1750 |
+
const msg = String(e?.message ?? e);
|
| 1751 |
+
if (e?.code === "ERR_MODULE_NOT_FOUND" || e?.code === "MODULE_NOT_FOUND" || /Cannot find (package|module)/.test(msg)) return msg.includes(pkg);
|
| 1752 |
+
return /Failed to resolve module specifier|Failed to fetch dynamically imported module|error loading dynamically imported module|Importing a module script failed/i.test(msg);
|
| 1753 |
+
}
|
| 1754 |
+
async function importOptional(pkg, importer, why) {
|
| 1755 |
+
try {
|
| 1756 |
+
return await importer();
|
| 1757 |
+
} catch (e) {
|
| 1758 |
+
if (!isNotFound(e, pkg)) throw e;
|
| 1759 |
+
const err = new Error(`ct0-edge: ${why} needs the package "${pkg}", which is not installed. Install it: npm i ${pkg}`);
|
| 1760 |
+
err.cause = e;
|
| 1761 |
+
throw err;
|
| 1762 |
+
}
|
| 1763 |
+
}
|
| 1764 |
+
function detectCuda(ortNode) {
|
| 1765 |
+
try {
|
| 1766 |
+
return !!ortNode?.listSupportedBackends?.().some((b) => b?.name === "cuda");
|
| 1767 |
+
} catch {
|
| 1768 |
+
return false;
|
| 1769 |
+
}
|
| 1770 |
+
}
|
| 1771 |
+
|
| 1772 |
+
// src/nodeFiles.ts
|
| 1773 |
+
var nodeImport = (spec) => import(spec);
|
| 1774 |
+
async function defaultCacheDir() {
|
| 1775 |
+
const env = globalThis.process?.env ?? {};
|
| 1776 |
+
if (env.CT0_CACHE) return env.CT0_CACHE;
|
| 1777 |
+
const os = await nodeImport("node:os"), path = await nodeImport("node:path");
|
| 1778 |
+
return path.join(env.XDG_CACHE_HOME ?? path.join(os.homedir(), ".cache"), "ct0-edge");
|
| 1779 |
+
}
|
| 1780 |
+
async function localFile(url, cacheDir, onStatus = () => {
|
| 1781 |
+
}) {
|
| 1782 |
+
const { fileURLToPath } = await nodeImport("node:url");
|
| 1783 |
+
if (url.startsWith("file:")) return fileURLToPath(url);
|
| 1784 |
+
if (!/^https?:/i.test(url)) return url;
|
| 1785 |
+
const fs = await nodeImport("node:fs"), path = await nodeImport("node:path");
|
| 1786 |
+
const u = new URL(url);
|
| 1787 |
+
const dest = path.join(cacheDir, u.hostname, ...decodeURIComponent(u.pathname).split("/").filter((s) => s && s !== ".."));
|
| 1788 |
+
if (fs.existsSync(dest)) return dest;
|
| 1789 |
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
| 1790 |
+
const part = `${dest}.part-${globalThis.process?.pid ?? 0}`;
|
| 1791 |
+
const name = path.basename(dest);
|
| 1792 |
+
try {
|
| 1793 |
+
const res = await get(url);
|
| 1794 |
+
const total = Number(res.headers["content-length"]) || 0;
|
| 1795 |
+
let got = 0, shown = -1;
|
| 1796 |
+
res.on("data", (c) => {
|
| 1797 |
+
got += c.length;
|
| 1798 |
+
const pct = total ? Math.floor(100 * got / total / 10) * 10 : -1;
|
| 1799 |
+
if (pct !== shown && pct >= 0) {
|
| 1800 |
+
shown = pct;
|
| 1801 |
+
onStatus(`downloading ${name}: ${pct}% of ${(total / 1e9).toFixed(2)} GB`);
|
| 1802 |
+
}
|
| 1803 |
+
});
|
| 1804 |
+
const { pipeline } = await nodeImport("node:stream/promises");
|
| 1805 |
+
await pipeline(res, fs.createWriteStream(part));
|
| 1806 |
+
if (total && got !== total) throw new Error(`ct0-edge: download truncated: ${url} (${got} of ${total} bytes)`);
|
| 1807 |
+
fs.renameSync(part, dest);
|
| 1808 |
+
} catch (e) {
|
| 1809 |
+
try {
|
| 1810 |
+
fs.rmSync(part, { force: true });
|
| 1811 |
+
} catch {
|
| 1812 |
+
}
|
| 1813 |
+
throw e;
|
| 1814 |
+
}
|
| 1815 |
+
return dest;
|
| 1816 |
+
}
|
| 1817 |
+
async function get(url, hops = 0) {
|
| 1818 |
+
const mod = await nodeImport(url.startsWith("https:") ? "node:https" : "node:http");
|
| 1819 |
+
const res = await new Promise((resolve, reject) => mod.get(url, { headers: { "user-agent": "ct0-edge" } }, resolve).on("error", reject));
|
| 1820 |
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && hops < 10) {
|
| 1821 |
+
res.resume();
|
| 1822 |
+
return get(new URL(res.headers.location, url).href, hops + 1);
|
| 1823 |
+
}
|
| 1824 |
+
if (res.statusCode !== 200) {
|
| 1825 |
+
res.resume();
|
| 1826 |
+
throw new Error(`ct0-edge: download failed: ${url} -> HTTP ${res.statusCode}`);
|
| 1827 |
+
}
|
| 1828 |
+
return res;
|
| 1829 |
+
}
|
| 1830 |
+
async function readLocalBytes(url) {
|
| 1831 |
+
const { fileURLToPath } = await nodeImport("node:url");
|
| 1832 |
+
const fs = await nodeImport("node:fs");
|
| 1833 |
+
const b = fs.readFileSync(url.startsWith("file:") ? fileURLToPath(url) : url);
|
| 1834 |
+
return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);
|
| 1835 |
+
}
|
| 1836 |
+
|
| 1837 |
+
// src/ct0.ts
|
| 1838 |
+
var CT0 = class _CT0 {
|
| 1839 |
+
maxContext = 262144;
|
| 1840 |
+
maxSafeTokens = 2048;
|
| 1841 |
+
tok;
|
| 1842 |
+
backend;
|
| 1843 |
+
yesIds = [];
|
| 1844 |
+
noIds = [];
|
| 1845 |
+
constructor(tok, backend) {
|
| 1846 |
+
this.tok = tok;
|
| 1847 |
+
this.backend = backend;
|
| 1848 |
+
}
|
| 1849 |
+
heads = {};
|
| 1850 |
+
methods = defaultMethods("decode-only");
|
| 1851 |
+
runtime;
|
| 1852 |
+
device;
|
| 1853 |
+
fallbackReason;
|
| 1854 |
+
dispose() {
|
| 1855 |
+
return this.backend.dispose();
|
| 1856 |
+
}
|
| 1857 |
+
get hiddenOutput() {
|
| 1858 |
+
return this.backend.hiddenName;
|
| 1859 |
+
}
|
| 1860 |
+
get defaultMethod() {
|
| 1861 |
+
return this.methods;
|
| 1862 |
+
}
|
| 1863 |
+
static async load(opts = {}) {
|
| 1864 |
+
const status = opts.onStatus ?? (() => {
|
| 1865 |
+
});
|
| 1866 |
+
const platform = detectPlatform();
|
| 1867 |
+
const { choice } = resolveRuntimeChoice(opts, opts.baseUrl && !opts.genaiBaseUrl && !opts.genaiRepo ? "decode-only" : "auto");
|
| 1868 |
+
const device = opts.device ?? "auto";
|
| 1869 |
+
const webgpu = platform === "browser" ? await detectWebGPU() : false;
|
| 1870 |
+
planRuntime(choice, device, { platform, webgpu, cuda: true });
|
| 1871 |
+
const mods = opts.modules ?? {};
|
| 1872 |
+
const needsOrt = choice === "auto" || choice === "genai" || choice === "decode-only";
|
| 1873 |
+
const loadOrt = async () => {
|
| 1874 |
+
if (mods.ort) return mods.ort;
|
| 1875 |
+
if (platform === "node") return importOptional("onnxruntime-node", () => import(NODE_ORT), `runtime "${choice}" in Node`);
|
| 1876 |
+
return importOptional("onnxruntime-web", () => import("onnxruntime-web/webgpu"), `runtime "${choice}" in the browser`);
|
| 1877 |
+
};
|
| 1878 |
+
let ort = null;
|
| 1879 |
+
let cuda = false;
|
| 1880 |
+
if (platform === "node" && (needsOrt || device === "cuda")) {
|
| 1881 |
+
try {
|
| 1882 |
+
ort = await loadOrt();
|
| 1883 |
+
cuda = detectCuda(ort);
|
| 1884 |
+
} catch (e) {
|
| 1885 |
+
if (needsOrt) throw e;
|
| 1886 |
+
}
|
| 1887 |
+
}
|
| 1888 |
+
const caps = { platform, webgpu, cuda };
|
| 1889 |
+
status("loading tokenizer\u2026");
|
| 1890 |
+
const tjs = mods.transformers ?? await importOptional("@huggingface/transformers", () => import("@huggingface/transformers"), "ct0-edge (tokenizer)");
|
| 1891 |
+
const env = tjs.env;
|
| 1892 |
+
const pageBase = () => {
|
| 1893 |
+
if (typeof location !== "undefined") return location.href;
|
| 1894 |
+
const cwd = globalThis.process?.cwd?.();
|
| 1895 |
+
return cwd ? "file://" + encodeURI(cwd.replace(/\\/g, "/").replace(/^(?!\/)/, "/")) + "/" : void 0;
|
| 1896 |
+
};
|
| 1897 |
+
const absBase = (b) => new URL(b, pageBase()).href;
|
| 1898 |
+
const at = (p, b) => new URL(p, absBase(b)).href;
|
| 1899 |
+
const mlc = opts.mlc ?? {};
|
| 1900 |
+
const mlcBuild = RUNTIME_DEFAULTS.webllm.builds[mlc.build ?? RUNTIME_DEFAULTS.webllm.build];
|
| 1901 |
+
if (choice === "webllm" && !mlcBuild) throw new Error(`ct0-edge: unknown MLC build "${mlc.build}" (use "q4f32_1" or "q4f16_1")`);
|
| 1902 |
+
const repo = opts.repo ?? (choice === "webllm" ? mlcBuild.repo : opts.baseUrl ? void 0 : DEFAULT_REPOS.decodeOnly);
|
| 1903 |
+
let tok, base;
|
| 1904 |
+
if (repo) {
|
| 1905 |
+
env.allowRemoteModels = true;
|
| 1906 |
+
env.allowLocalModels = false;
|
| 1907 |
+
tok = await tjs.AutoTokenizer.from_pretrained(repo, { local_files_only: false });
|
| 1908 |
+
base = opts.baseUrl ?? `https://huggingface.co/${repo}/resolve/main/`;
|
| 1909 |
+
} else {
|
| 1910 |
+
base = absBase(opts.baseUrl);
|
| 1911 |
+
env.allowRemoteModels = false;
|
| 1912 |
+
env.allowLocalModels = true;
|
| 1913 |
+
const bu = new URL(base);
|
| 1914 |
+
if (bu.protocol === "file:") {
|
| 1915 |
+
const { fileURLToPath } = await import(NODE_URL);
|
| 1916 |
+
env.localModelPath = fileURLToPath(bu);
|
| 1917 |
+
} else env.localModelPath = typeof location !== "undefined" && bu.origin === location.origin ? bu.pathname : base;
|
| 1918 |
+
tok = await tjs.AutoTokenizer.from_pretrained("tok");
|
| 1919 |
+
}
|
| 1920 |
+
const genaiBase = opts.genaiBaseUrl ?? `https://huggingface.co/${opts.genaiRepo ?? DEFAULT_REPOS.genai}/resolve/main/`;
|
| 1921 |
+
const tjsRepo = opts.transformersRepo ?? RUNTIME_DEFAULTS["transformers.js"].repo;
|
| 1922 |
+
const tjsBase = `https://huggingface.co/${tjsRepo}/resolve/main/`;
|
| 1923 |
+
let cacheDir;
|
| 1924 |
+
const fileFor = async (url) => {
|
| 1925 |
+
if (platform !== "node") return url;
|
| 1926 |
+
cacheDir ??= opts.cacheDir ?? await defaultCacheDir();
|
| 1927 |
+
return localFile(url, cacheDir, status);
|
| 1928 |
+
};
|
| 1929 |
+
const loadDecodeOnly = async (dev) => {
|
| 1930 |
+
ort ??= await loadOrt();
|
| 1931 |
+
if (platform === "browser" && !opts.skipVersionCheck) checkOrtVersion(ort.env?.versions?.web);
|
| 1932 |
+
const file = opts.decodeModel ?? RUNTIME_DEFAULTS["decode-only"].file;
|
| 1933 |
+
const dataPath = opts.externalDataFile ?? file + "_data";
|
| 1934 |
+
const providers = opts.providers ?? [dev];
|
| 1935 |
+
const isLocal = /^(\.\/|\/)/.test(dataPath) || /^(\.\/|\/)/.test(file);
|
| 1936 |
+
const modelUrl = isLocal ? at(file, pageBase()) : at(file, base);
|
| 1937 |
+
const dataUrl = isLocal ? at(dataPath, pageBase()) : at(dataPath, base);
|
| 1938 |
+
status(`loading decode-only graph (${providers.join(",")})\u2026`);
|
| 1939 |
+
let S;
|
| 1940 |
+
if (platform === "node") {
|
| 1941 |
+
await fileFor(dataUrl);
|
| 1942 |
+
S = await ort.InferenceSession.create(await fileFor(modelUrl), { executionProviders: providers, logSeverityLevel: 3 });
|
| 1943 |
+
} else {
|
| 1944 |
+
S = await ort.InferenceSession.create(modelUrl, {
|
| 1945 |
+
executionProviders: providers,
|
| 1946 |
+
externalData: [{ path: dataPath.split("/").pop(), data: dataUrl }]
|
| 1947 |
+
});
|
| 1948 |
+
}
|
| 1949 |
+
try {
|
| 1950 |
+
return await createDecodeOnly(S, tok, { ort, pastLen: opts.pastLen, skipVersionCheck: opts.skipVersionCheck });
|
| 1951 |
+
} catch (e) {
|
| 1952 |
+
try {
|
| 1953 |
+
await S.release?.();
|
| 1954 |
+
} catch {
|
| 1955 |
+
}
|
| 1956 |
+
throw e;
|
| 1957 |
+
}
|
| 1958 |
+
};
|
| 1959 |
+
const loadGenai = async (dev) => {
|
| 1960 |
+
ort ??= await loadOrt();
|
| 1961 |
+
const file = opts.genaiModel ?? (dev === "cuda" ? RUNTIME_DEFAULTS.genai.files.cuda : RUNTIME_DEFAULTS.genai.files.webgpu);
|
| 1962 |
+
status(`loading genai int4 graph (${dev})\u2026`);
|
| 1963 |
+
if (platform === "browser")
|
| 1964 |
+
return createGenai(
|
| 1965 |
+
at(file, genaiBase),
|
| 1966 |
+
at(file + ".data", genaiBase),
|
| 1967 |
+
{ ...opts.genai, verifyEp: opts.verifyEp ?? opts.genai?.verifyEp ?? false, ort, skipVersionCheck: opts.skipVersionCheck ?? opts.genai?.skipVersionCheck }
|
| 1968 |
+
);
|
| 1969 |
+
await fileFor(at(file + ".data", genaiBase));
|
| 1970 |
+
const S = await ort.InferenceSession.create(await fileFor(at(file, genaiBase)), { executionProviders: [dev], graphOptimizationLevel: "all", logSeverityLevel: 3 });
|
| 1971 |
+
try {
|
| 1972 |
+
const be = genaiFromSession(S, ort, opts.genai);
|
| 1973 |
+
if (opts.genai?.warmup ?? true) await be.prefill([248045], { positions: [] });
|
| 1974 |
+
return be;
|
| 1975 |
+
} catch (e) {
|
| 1976 |
+
try {
|
| 1977 |
+
await S.release?.();
|
| 1978 |
+
} catch {
|
| 1979 |
+
}
|
| 1980 |
+
throw e;
|
| 1981 |
+
}
|
| 1982 |
+
};
|
| 1983 |
+
const loadTransformers = async (dev) => {
|
| 1984 |
+
const dtype = opts.dtype ?? RUNTIME_DEFAULTS["transformers.js"].dtype;
|
| 1985 |
+
status(`loading transformers.js graph (${dtype}, ${dev})\u2026`);
|
| 1986 |
+
env.allowRemoteModels = true;
|
| 1987 |
+
env.allowLocalModels = false;
|
| 1988 |
+
if (platform === "node" && opts.cacheDir) env.cacheDir = opts.cacheDir;
|
| 1989 |
+
const model = await tjs.AutoModelForCausalLM.from_pretrained(tjsRepo, { dtype, device: dev });
|
| 1990 |
+
return transformersJsFromModel(model, tjs.Tensor);
|
| 1991 |
+
};
|
| 1992 |
+
let mlcBase = null, mlcNote, webllmRecovery = false;
|
| 1993 |
+
const loadWebllm = async () => {
|
| 1994 |
+
const webllm = mods.webllm ?? await importOptional("@mlc-ai/web-llm", () => import(WEBLLM_PKG), 'runtime "webllm"');
|
| 1995 |
+
const build = mlcBuild;
|
| 1996 |
+
const modelUrl = mlc.modelUrl ?? `https://huggingface.co/${build.repo}/`;
|
| 1997 |
+
const modelLib = mlc.modelLib ?? (/^https?:/.test(build.lib) ? build.lib : new URL("resolve/main/" + build.lib, modelUrl).href);
|
| 1998 |
+
mlcBase = new URL("resolve/main/", modelUrl).href;
|
| 1999 |
+
let recovery = mlc.recovery || void 0;
|
| 2000 |
+
if (!recovery && mlc.recovery !== false) {
|
| 2001 |
+
status("loading WebLLM hidden-state recovery\u2026");
|
| 2002 |
+
const files = mlc.recoveryUrls ?? build.recovery;
|
| 2003 |
+
const r = await tryLoadRecovery(mlcBase, files);
|
| 2004 |
+
recovery = r.recovery;
|
| 2005 |
+
if (!recovery) {
|
| 2006 |
+
mlcNote = `webllm recovery unavailable (${r.reason}): every question type reads the logits`;
|
| 2007 |
+
status(mlcNote);
|
| 2008 |
+
}
|
| 2009 |
+
}
|
| 2010 |
+
webllmRecovery = !!recovery;
|
| 2011 |
+
status(`loading WebLLM build ${mlc.build ?? RUNTIME_DEFAULTS.webllm.build}\u2026`);
|
| 2012 |
+
return createWebllm(webllm, {
|
| 2013 |
+
modelId: mlc.modelId ?? build.modelId,
|
| 2014 |
+
modelUrl,
|
| 2015 |
+
libUrl: modelLib,
|
| 2016 |
+
appConfig: mlc.appConfig,
|
| 2017 |
+
tok,
|
| 2018 |
+
recovery,
|
| 2019 |
+
initProgressCallback: mlc.initProgressCallback
|
| 2020 |
+
});
|
| 2021 |
+
};
|
| 2022 |
+
const loadLitert = async (dev) => {
|
| 2023 |
+
const lrt = mods.litert ?? await importOptional("@litertjs/core", () => import(LITERT_PKG), 'runtime "litert"');
|
| 2024 |
+
const D = RUNTIME_DEFAULTS.litert;
|
| 2025 |
+
const lbase = absBase(litertBundleBase(opts));
|
| 2026 |
+
const name = (f) => f.replace(/^litert\//, "");
|
| 2027 |
+
status(`loading LiteRT.js ${LITERTJS_VERSION} runtime\u2026`);
|
| 2028 |
+
try {
|
| 2029 |
+
await lrt.loadLiteRt(opts.litertWasmUrl ?? D.wasm, { jspi: !!globalThis.WebAssembly?.Suspending });
|
| 2030 |
+
} catch (e) {
|
| 2031 |
+
if (!/already loading|already loaded/i.test(String(e?.message ?? e))) throw e;
|
| 2032 |
+
}
|
| 2033 |
+
const manifest = await (await fetch(at(name(D.files.manifest), lbase))).json();
|
| 2034 |
+
const graph = new Uint8Array(await (await fetch(at(name(D.files.graph), lbase))).arrayBuffer());
|
| 2035 |
+
const weightsUrl = at(name(D.files.weights), lbase);
|
| 2036 |
+
status("streaming LiteRT weights into WebGPU\u2026");
|
| 2037 |
+
return createLiteRt(lrt, graph, manifest, embedFromF16Reader(httpRangeReader(at(name(D.files.embed), lbase)), manifest.hidden), {
|
| 2038 |
+
accelerator: dev === "webgpu" ? "webgpu" : "wasm",
|
| 2039 |
+
gpuPrecision: opts.litertPrecision,
|
| 2040 |
+
weights: manifest.externalized === false ? void 0 : () => {
|
| 2041 |
+
let body = null;
|
| 2042 |
+
return new ReadableStream({
|
| 2043 |
+
async pull(c) {
|
| 2044 |
+
body ??= (await fetch(weightsUrl)).body.getReader();
|
| 2045 |
+
const { done, value } = await body.read();
|
| 2046 |
+
if (done) c.close();
|
| 2047 |
+
else c.enqueue(value);
|
| 2048 |
+
},
|
| 2049 |
+
cancel() {
|
| 2050 |
+
body?.cancel();
|
| 2051 |
+
}
|
| 2052 |
+
});
|
| 2053 |
+
}
|
| 2054 |
+
});
|
| 2055 |
+
};
|
| 2056 |
+
const load = (p) => {
|
| 2057 |
+
switch (p.runtime) {
|
| 2058 |
+
case "genai":
|
| 2059 |
+
return loadGenai(p.device);
|
| 2060 |
+
case "decode-only":
|
| 2061 |
+
return loadDecodeOnly(p.device);
|
| 2062 |
+
case "transformers.js":
|
| 2063 |
+
return loadTransformers(p.device);
|
| 2064 |
+
case "webllm":
|
| 2065 |
+
return loadWebllm();
|
| 2066 |
+
case "litert":
|
| 2067 |
+
return loadLitert(p.device);
|
| 2068 |
+
}
|
| 2069 |
+
};
|
| 2070 |
+
const sel = await selectRuntime(choice, device, { caps, load, onStatus: status });
|
| 2071 |
+
const ct0 = new _CT0(tok, sel.backend);
|
| 2072 |
+
ct0.runtime = sel.runtime;
|
| 2073 |
+
ct0.device = sel.device;
|
| 2074 |
+
ct0.fallbackReason = [sel.fallbackReason, sel.runtime === "webllm" ? mlcNote : void 0].filter(Boolean).join("; ") || void 0;
|
| 2075 |
+
ct0.methods = defaultMethods(sel.runtime, { webllmRecovery });
|
| 2076 |
+
ct0.maxSafeTokens = opts.maxSafeTokens ?? maxSafeTokensFor(sel.runtime, sel.device);
|
| 2077 |
+
status(`runtime: ${sel.runtime} (${sel.device})`);
|
| 2078 |
+
const headsBase = sel.runtime === "genai" ? genaiBase : sel.runtime === "decode-only" ? base : sel.runtime === "transformers.js" ? tjsBase : sel.runtime === "webllm" && webllmRecovery ? mlcBase : null;
|
| 2079 |
+
if ((opts.heads ?? true) && headsBase) {
|
| 2080 |
+
try {
|
| 2081 |
+
await ct0.loadHeads({ noul: at(DEFAULT_FILES.head, headsBase), score: at(DEFAULT_FILES.headScore, headsBase) });
|
| 2082 |
+
} catch (e) {
|
| 2083 |
+
status("heads not loaded (" + (e?.message ?? e) + "); ask() with method head will throw");
|
| 2084 |
+
}
|
| 2085 |
+
}
|
| 2086 |
+
const firstId = (s) => {
|
| 2087 |
+
const a = ct0.ids(s);
|
| 2088 |
+
return a.length ? a[0] : null;
|
| 2089 |
+
};
|
| 2090 |
+
ct0.yesIds = [...new Set(["Yes", " Yes", "yes", " yes", "YES"].map(firstId).filter((x) => x != null))];
|
| 2091 |
+
ct0.noIds = [...new Set(["No", " No", "no", " no", "NO"].map(firstId).filter((x) => x != null))];
|
| 2092 |
+
status("ready.");
|
| 2093 |
+
return ct0;
|
| 2094 |
+
}
|
| 2095 |
+
ids(text) {
|
| 2096 |
+
return this.tok(text, { add_special_tokens: false }).input_ids.tolist()[0].map(Number);
|
| 2097 |
+
}
|
| 2098 |
+
countTokens(text) {
|
| 2099 |
+
return this.ids(text).length;
|
| 2100 |
+
}
|
| 2101 |
+
fitContext(text, opts = {}) {
|
| 2102 |
+
const reserve = opts.reserve ?? 300;
|
| 2103 |
+
const budget = Math.max(64, this.maxSafeTokens - reserve);
|
| 2104 |
+
const toks = this.ids(text);
|
| 2105 |
+
if (toks.length <= budget) return text;
|
| 2106 |
+
const head = Math.floor(budget * 0.6), tail = budget - head;
|
| 2107 |
+
const kept = [...toks.slice(0, head), ...toks.slice(toks.length - tail)];
|
| 2108 |
+
opts.onTrim?.({ original: toks.length, kept: budget });
|
| 2109 |
+
return this.tok.decode(kept, { skip_special_tokens: true });
|
| 2110 |
+
}
|
| 2111 |
+
async scanLong(text, question, opts = {}) {
|
| 2112 |
+
const win = opts.windowTokens ?? this.maxSafeTokens - 400;
|
| 2113 |
+
const overlap = opts.overlapTokens ?? 128;
|
| 2114 |
+
const thr = opts.threshold ?? 0.5;
|
| 2115 |
+
const toks = this.ids(text);
|
| 2116 |
+
const hits = [];
|
| 2117 |
+
if (toks.length <= win) {
|
| 2118 |
+
const p = await this.decide(text, question);
|
| 2119 |
+
opts.onChunk?.(0, p, 1);
|
| 2120 |
+
hits.push({ i: 0, start: 0, p, preview: text.slice(0, 100) });
|
| 2121 |
+
} else {
|
| 2122 |
+
const total = Math.ceil((toks.length - overlap) / (win - overlap));
|
| 2123 |
+
let i = 0, idx = 0;
|
| 2124 |
+
while (i < toks.length) {
|
| 2125 |
+
const chunk = this.tok.decode(toks.slice(i, i + win), { skip_special_tokens: true });
|
| 2126 |
+
const p = await this.decide(chunk, question);
|
| 2127 |
+
hits.push({ i: idx, start: i, p, preview: chunk.slice(0, 100) });
|
| 2128 |
+
opts.onChunk?.(idx, p, total);
|
| 2129 |
+
idx++;
|
| 2130 |
+
i += win - overlap;
|
| 2131 |
+
}
|
| 2132 |
+
}
|
| 2133 |
+
return { chunks: hits.length, max: Math.max(...hits.map((h) => h.p)), hits, flagged: hits.filter((h) => h.p >= thr) };
|
| 2134 |
+
}
|
| 2135 |
+
static inspectPage(doc) {
|
| 2136 |
+
const q = (s, f) => [...doc.querySelectorAll(s)].map(f);
|
| 2137 |
+
const comments = [];
|
| 2138 |
+
const w = doc.createTreeWalker(doc.body || doc.documentElement, 128);
|
| 2139 |
+
while (w.nextNode()) comments.push((w.currentNode.nodeValue || "").trim().slice(0, 180));
|
| 2140 |
+
const store = {};
|
| 2141 |
+
try {
|
| 2142 |
+
const ls = (doc.defaultView || window).localStorage;
|
| 2143 |
+
for (let i = 0; i < ls.length; i++) {
|
| 2144 |
+
const k = ls.key(i);
|
| 2145 |
+
store[k] = (ls.getItem(k) || "").slice(0, 80);
|
| 2146 |
+
}
|
| 2147 |
+
} catch {
|
| 2148 |
+
}
|
| 2149 |
+
return {
|
| 2150 |
+
title: doc.title,
|
| 2151 |
+
url: (doc.defaultView || {}).location?.search || "/",
|
| 2152 |
+
forms: q("form", (f) => ({ action: f.getAttribute("action"), method: f.method, inputs: [...f.querySelectorAll("input")].map((i) => ({ name: i.name, type: i.type, value: i.type === "hidden" ? i.value : void 0, autocomplete: i.autocomplete })) })),
|
| 2153 |
+
inputs: q("input,textarea", (i) => ({ sel: i.id ? "#" + i.id : i.name ? `[name=${i.name}]` : i.tagName.toLowerCase(), type: i.type })),
|
| 2154 |
+
buttons: q("button", (b) => ({ sel: b.id ? "#" + b.id : null, text: b.textContent.trim() })),
|
| 2155 |
+
links: q("a[href]", (a) => a.getAttribute("href")),
|
| 2156 |
+
texts: q("pre,code,[class*='debug'],[class*='error'],[class*='stack']", (e) => (e.textContent || "").trim().slice(0, 300)).filter(Boolean),
|
| 2157 |
+
comments,
|
| 2158 |
+
inlineScripts: q("script:not([src])", (s) => s.textContent.trim().slice(0, 220)).filter(Boolean),
|
| 2159 |
+
localStorage: store
|
| 2160 |
+
};
|
| 2161 |
+
}
|
| 2162 |
+
static pageSource(doc, max = 2500) {
|
| 2163 |
+
return (doc.body || doc.documentElement).outerHTML.slice(0, max);
|
| 2164 |
+
}
|
| 2165 |
+
turn(msg, system, think) {
|
| 2166 |
+
let t = "";
|
| 2167 |
+
if (system) t += "<|im_start|>system\n" + system + "<|im_end|>\n";
|
| 2168 |
+
t += "<|im_start|>user\n" + msg + "<|im_end|>\n<|im_start|>assistant\n";
|
| 2169 |
+
return t + (think ? "<think>\n" : "<think>\n\n</think>\n\n");
|
| 2170 |
+
}
|
| 2171 |
+
async *generate(prompt, opts = {}) {
|
| 2172 |
+
yield* this.stream(this.turn(prompt, opts.system, opts.think ?? true), opts);
|
| 2173 |
+
}
|
| 2174 |
+
async *stream(prompt, opts = {}) {
|
| 2175 |
+
for await (const tid of this.backend.generate(this.ids(prompt), opts)) yield this.tok.decode([tid], { skip_special_tokens: true });
|
| 2176 |
+
}
|
| 2177 |
+
async decide(state, question) {
|
| 2178 |
+
const sys = "You are a security decision function. Given a STATE and a yes/no QUESTION, answer with exactly one word: Yes or No.";
|
| 2179 |
+
const text = "<|im_start|>system\n" + sys + "<|im_end|>\n<|im_start|>user\nSTATE:\n" + state + "\n\nQUESTION: " + question + "\nAnswer Yes or No.<|im_end|>\n<|im_start|>assistant\n<think>\n\n</think>\n\n";
|
| 2180 |
+
const { lastLogits: L } = await this.backend.prefill(this.ids(text), { positions: [] });
|
| 2181 |
+
const all = [...this.yesIds, ...this.noIds];
|
| 2182 |
+
if (!all.length) return NaN;
|
| 2183 |
+
const mx = Math.max(...all.map((i) => L[i]));
|
| 2184 |
+
const Z = all.reduce((a, i) => a + Math.exp(L[i] - mx), 0);
|
| 2185 |
+
return this.yesIds.reduce((a, i) => a + Math.exp(L[i] - mx), 0) / Z;
|
| 2186 |
+
}
|
| 2187 |
+
async loadHeads(src) {
|
| 2188 |
+
const get2 = async (x) => {
|
| 2189 |
+
if (typeof x !== "string") return loadHead(x);
|
| 2190 |
+
if (x.startsWith("file:")) return loadHead(await readLocalBytes(x));
|
| 2191 |
+
const r = await fetch(x);
|
| 2192 |
+
if (!r.ok) throw new Error(`loadHeads: ${x} -> HTTP ${r.status}`);
|
| 2193 |
+
return loadHead(await r.arrayBuffer());
|
| 2194 |
+
};
|
| 2195 |
+
const nc = src.noul ?? src.choice;
|
| 2196 |
+
if (nc) this.heads.noulChoice = await get2(nc);
|
| 2197 |
+
if (src.score) this.heads.score = await get2(src.score);
|
| 2198 |
+
}
|
| 2199 |
+
ask(q, opts = {}) {
|
| 2200 |
+
return ask(this.backend, this.tok, this.heads, q, this.askOpts(q, opts));
|
| 2201 |
+
}
|
| 2202 |
+
askMany(state, qs, opts = {}) {
|
| 2203 |
+
return askManyWith(this.backend, this.tok, this.heads, state, qs, (q) => this.askOpts(q, opts));
|
| 2204 |
+
}
|
| 2205 |
+
askOpts(q, opts) {
|
| 2206 |
+
return { ...opts, method: opts.method ?? this.methods[q.kind], maxSafeTokens: opts.maxSafeTokens ?? this.maxSafeTokens };
|
| 2207 |
+
}
|
| 2208 |
+
async prefillHiddens(prompt) {
|
| 2209 |
+
if (!this.backend.hiddenName) return null;
|
| 2210 |
+
const t = tokenOffsets(this.tok, prompt);
|
| 2211 |
+
const dp = decidePos(t.tokens), lp = t.ids.length - 1, op = optPositionsFor(prompt, t.offsets);
|
| 2212 |
+
const { hiddens } = await this.backend.prefill(t.ids, { positions: [dp, lp, ...op] });
|
| 2213 |
+
return { atDecide: hiddens[0], atLast: hiddens[1], atOpts: hiddens.slice(2) };
|
| 2214 |
+
}
|
| 2215 |
+
};
|
| 2216 |
+
var NODE_ORT = "onnxruntime-node";
|
| 2217 |
+
var NODE_URL = "node:url";
|
| 2218 |
+
var WEBLLM_PKG = "@mlc-ai/web-llm";
|
| 2219 |
+
var LITERT_PKG = "@litertjs/core";
|
| 2220 |
+
export {
|
| 2221 |
+
CT0,
|
| 2222 |
+
DEFAULT_FILES,
|
| 2223 |
+
DEFAULT_METHOD,
|
| 2224 |
+
DEFAULT_REPOS,
|
| 2225 |
+
GENAI_NODE_CPU_ERROR,
|
| 2226 |
+
GENERATE_DEFAULTS,
|
| 2227 |
+
LITERT_QUANTS,
|
| 2228 |
+
MLC_Q4F32_LIB,
|
| 2229 |
+
RUNTIMES,
|
| 2230 |
+
RUNTIME_DEFAULTS,
|
| 2231 |
+
RUNTIME_LIMITS,
|
| 2232 |
+
askMany,
|
| 2233 |
+
defaultMethods,
|
| 2234 |
+
detectCuda,
|
| 2235 |
+
detectPlatform,
|
| 2236 |
+
detectWebGPU,
|
| 2237 |
+
importOptional,
|
| 2238 |
+
levelFromCuts,
|
| 2239 |
+
litertBundleBase,
|
| 2240 |
+
maxSafeTokensFor,
|
| 2241 |
+
packagesFor,
|
| 2242 |
+
planRuntime,
|
| 2243 |
+
resolveGenerateOptions,
|
| 2244 |
+
resolveRuntimeChoice,
|
| 2245 |
+
selectRuntime
|
| 2246 |
+
};
|