tinygpt / worker.js
tanushlukha's picture
Deploy the TinyGPT page with model commit 3464ad889fdf
c56718d verified
Raw History Blame Contribute Delete
4.5 kB
// The page's second thread: downloads the models once, then runs them (web/tinygpt.js) without
// freezing the page. Messages in: load, generate, stop, parse. Messages out: progress, ready,
// piece, done, parsed, error.
import { MODEL_BASE } from "./config.js";
import { GPT, Parser, Tokenizer, readSafetensors, stream } from "./tinygpt.js";
const CACHE = "tinygpt-models-v1";
const loaded = {}; // "lm" | "parser" -> Promise of {config, tok, model}
let stopRequested = false;
const post = (message) => self.postMessage(message);
async function openCache() { // Cache Storage: kept between visits (absent in some private windows)
try { return await caches.open(CACHE); } catch { return null; }
}
async function fetchOk(url) {
let res;
try { res = await fetch(url); } catch { throw new Error("the network request failed"); }
if (!res.ok) throw new Error(`the server answered HTTP ${res.status}`);
return res;
}
/** The file's bytes, reporting progress as they arrive. */
async function download(url, onProgress) {
const res = await fetchOk(url);
const total = Number(res.headers.get("content-length")) || 0;
const reader = res.body.getReader(), parts = [];
let got = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
parts.push(value);
got += value.length;
onProgress(got, total);
}
const bytes = new Uint8Array(got);
let at = 0;
for (const p of parts) { bytes.set(p, at); at += p.length; }
return bytes.buffer;
}
async function sha256(buffer) {
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", buffer));
return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
}
/** A bundle as bundle.load_bundle reads it: config, tokenizer, then weights checked against their
* SHA-256. The weights are cached only once they pass, and checked again when read from the cache. */
async function loadBundle(name) {
const base = new URL(`${name}/`, new URL(MODEL_BASE, self.location.href)).href;
const config = await (await fetchOk(base + "config.json")).json();
const tok = new Tokenizer(await (await fetchOk(base + config.tokenizer)).json());
const url = base + "model.safetensors", want = config.sha256["model.safetensors"];
const cache = await openCache();
let buffer = null;
const hit = cache && await cache.match(url);
if (hit) {
buffer = await hit.arrayBuffer();
if (await sha256(buffer) !== want) { buffer = null; await cache.delete(url); } // damaged: download again
}
if (!buffer) {
buffer = await download(url, (got, total) => post({ type: "progress", name, got, total }));
if (await sha256(buffer) !== want) throw new Error("the downloaded weights don't match their checksum");
cache?.put(url, new Response(buffer)).catch(() => {}); // the Response keeps its own copy
}
const model = new GPT(config.model_config, readSafetensors(buffer));
post({ type: "ready", name });
return { config, tok, model };
}
function load(name) { // one download per model; after a failure, the next request tries again
loaded[name] ??= loadBundle(name).catch((e) => { delete loaded[name]; throw e; });
return loaded[name];
}
// a zero-delay pause, so a "stop" message can arrive between two tokens
const channel = new MessageChannel();
const tick = () => new Promise((resolve) => { channel.port1.onmessage = () => resolve(); channel.port2.postMessage(0); });
async function generate({ id, prompt, params }) {
const { model, tok } = await load("lm");
stopRequested = false;
for (const event of stream(model, tok, prompt, params, () => stopRequested)) {
post(event.done ? { type: "done", id, stats: event.done } : { type: "piece", id, text: event.text });
await tick();
}
}
async function parse({ id, text }) {
const { model, tok, config } = await load("parser");
const t0 = performance.now();
const result = new Parser(model, tok, config.task).parse(text);
post({ type: "parsed", id, result, seconds: (performance.now() - t0) / 1000 });
}
self.onmessage = async ({ data }) => {
try {
if (data.type === "load") await load(data.name);
else if (data.type === "stop") stopRequested = true;
else if (data.type === "generate") await generate(data);
else if (data.type === "parse") await parse(data);
} catch (e) {
post({ type: "error", id: data.id, name: data.name ?? (data.type === "parse" ? "parser" : "lm"), message: e.message });
}
};