Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- README.md +8 -4
- index.html +79 -17
- web/bench-main.js +108 -0
- web/bench.html +31 -0
- web/config.js +22 -0
- web/gpu.js +91 -0
- web/index.html +81 -0
- web/kernels.js +1361 -0
- web/main.js +171 -0
- web/model.js +440 -0
- web/npy.js +16 -0
- web/tokenizer.js +122 -0
README.md
CHANGED
|
@@ -1,10 +1,14 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Qwen3.5-2B WebGPU (custom WGSL kernels)
|
| 3 |
+
emoji: ⚡
|
| 4 |
colorFrom: purple
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
+
Qwen3.5-2B (hybrid gated DeltaNet + gated attention) running in the browser
|
| 12 |
+
on hand-written WGSL kernels only — the first DeltaNet hybrid on WebGPU.
|
| 13 |
+
Weights stream from [borkiss/qwen35-2b-webgpu](https://huggingface.co/borkiss/qwen35-2b-webgpu).
|
| 14 |
+
Needs WebGPU with subgroups (Chrome/Edge 125+; shader-f16 optional).
|
index.html
CHANGED
|
@@ -1,19 +1,81 @@
|
|
| 1 |
<!doctype html>
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
</html>
|
|
|
|
| 1 |
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<meta charset="utf-8" />
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 5 |
+
<title>Qwen3.5-2B · WebGPU</title>
|
| 6 |
+
<style>
|
| 7 |
+
:root {
|
| 8 |
+
--bg: #0b0d10; --panel: #14171c; --line: #232830; --fg: #e8ebef;
|
| 9 |
+
--dim: #8b93a1; --accent: #7c5cff; --ok: #3ecf8e; --err: #ff6b6b;
|
| 10 |
+
}
|
| 11 |
+
* { box-sizing: border-box; }
|
| 12 |
+
body {
|
| 13 |
+
margin: 0; background: var(--bg); color: var(--fg);
|
| 14 |
+
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 15 |
+
height: 100dvh; display: flex; flex-direction: column;
|
| 16 |
+
}
|
| 17 |
+
header {
|
| 18 |
+
display: flex; align-items: baseline; gap: 12px; padding: 12px 18px;
|
| 19 |
+
border-bottom: 1px solid var(--line); flex: none;
|
| 20 |
+
}
|
| 21 |
+
.wordmark { font-weight: 700; letter-spacing: .2px; }
|
| 22 |
+
.sub { color: var(--dim); font-size: 12.5px; }
|
| 23 |
+
#gpuname { margin-left: auto; color: var(--dim); font-size: 12.5px; }
|
| 24 |
+
#toksbox { color: var(--dim); font-size: 12.5px; }
|
| 25 |
+
#toks { color: var(--ok); font-variant-numeric: tabular-nums; }
|
| 26 |
+
#chat { flex: 1; overflow-y: auto; padding: 18px; }
|
| 27 |
+
.msg { max-width: 780px; margin: 0 auto 16px; }
|
| 28 |
+
.msg .who { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim); margin-bottom: 4px; }
|
| 29 |
+
.msg.user .who { color: var(--accent); }
|
| 30 |
+
.msg .body { white-space: pre-wrap; word-break: break-word; }
|
| 31 |
+
.stats { font-size: 11.5px; color: var(--dim); margin-top: 6px; }
|
| 32 |
+
.caret { display: inline-block; width: 7px; height: 15px; background: var(--accent); animation: blink 1s steps(1) infinite; vertical-align: -2px; }
|
| 33 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 34 |
+
form { flex: none; display: flex; gap: 10px; padding: 14px 18px 18px; border-top: 1px solid var(--line); }
|
| 35 |
+
#input {
|
| 36 |
+
flex: 1; max-width: 780px; margin-left: auto; resize: none; max-height: 160px;
|
| 37 |
+
background: var(--panel); color: var(--fg); border: 1px solid var(--line);
|
| 38 |
+
border-radius: 10px; padding: 10px 12px; font: inherit; outline: none;
|
| 39 |
+
}
|
| 40 |
+
#input:focus { border-color: var(--accent); }
|
| 41 |
+
#send {
|
| 42 |
+
margin-right: auto; background: var(--accent); border: 0; color: #fff;
|
| 43 |
+
border-radius: 10px; padding: 0 18px; font: inherit; font-weight: 600; cursor: pointer;
|
| 44 |
+
}
|
| 45 |
+
#send:disabled, #input:disabled { opacity: .45; cursor: default; }
|
| 46 |
+
#boot {
|
| 47 |
+
position: fixed; inset: 0; background: var(--bg); display: flex; flex-direction: column;
|
| 48 |
+
align-items: center; justify-content: center; gap: 14px; transition: opacity .4s; z-index: 10;
|
| 49 |
+
}
|
| 50 |
+
#boot.done { opacity: 0; pointer-events: none; }
|
| 51 |
+
#boottitle { font-size: 22px; font-weight: 700; }
|
| 52 |
+
#bootsub { color: var(--dim); font-size: 13px; max-width: 420px; text-align: center; }
|
| 53 |
+
#bar { width: 300px; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; }
|
| 54 |
+
#barfill { width: 100%; height: 100%; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .2s; }
|
| 55 |
+
#log { font: 12px/1.7 ui-monospace, monospace; color: var(--dim); width: 420px; }
|
| 56 |
+
#log .ok { color: var(--ok); }
|
| 57 |
+
#log .err { color: var(--err); }
|
| 58 |
+
</style>
|
| 59 |
+
<body>
|
| 60 |
+
<header>
|
| 61 |
+
<span class="wordmark">Qwen3.5-2B</span>
|
| 62 |
+
<span class="sub">hybrid DeltaNet + attention · hand-written WGSL · int4</span>
|
| 63 |
+
<span id="toksbox"><span id="toks">–</span> tok/s</span>
|
| 64 |
+
<span id="gpuname"></span>
|
| 65 |
+
</header>
|
| 66 |
+
<div id="chat"><div id="msgs"></div></div>
|
| 67 |
+
<form id="form">
|
| 68 |
+
<textarea id="input" rows="1" placeholder="Message… (Enter to send)" disabled></textarea>
|
| 69 |
+
<button id="send" type="submit" disabled>Send</button>
|
| 70 |
+
</form>
|
| 71 |
+
<div id="boot">
|
| 72 |
+
<div id="boottitle">Qwen3.5-2B</div>
|
| 73 |
+
<div id="bootsub">First hybrid gated-DeltaNet LLM in the browser — pure WebGPU, hand-written WGSL kernels.
|
| 74 |
+
Downloading <span id="dlmb">~1.1 GB</span> of int4 weights <span id="dlpct"></span></div>
|
| 75 |
+
<div id="bar"><div id="barfill"></div></div>
|
| 76 |
+
<div id="dlnote" class="sub"></div>
|
| 77 |
+
<div id="log"></div>
|
| 78 |
+
</div>
|
| 79 |
+
<script type="module" src="/web/main.js"></script>
|
| 80 |
+
</body>
|
| 81 |
</html>
|
web/bench-main.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Public benchmark page: runs the standard suite on the visitor's GPU and
|
| 2 |
+
// produces a shareable .log (human-readable + embedded JSON).
|
| 3 |
+
// Community format (as lfm2): window.__done = { ok, meta, results }.
|
| 4 |
+
import { initDevice } from "/web/gpu.js";
|
| 5 |
+
import { loadModel } from "/web/model.js";
|
| 6 |
+
import { Tokenizer } from "/web/tokenizer.js";
|
| 7 |
+
import { resolvePaths } from "/web/config.js";
|
| 8 |
+
|
| 9 |
+
const $ = (id) => document.getElementById(id);
|
| 10 |
+
const median = (a) => a.slice().sort((x, y) => x - y)[a.length >> 1];
|
| 11 |
+
|
| 12 |
+
const lines = [];
|
| 13 |
+
function log(text, cls = "") {
|
| 14 |
+
lines.push(text);
|
| 15 |
+
const el = document.createElement("div");
|
| 16 |
+
if (cls) el.className = cls;
|
| 17 |
+
el.textContent = text;
|
| 18 |
+
if ($("console").textContent === "idle — press Run bench") $("console").textContent = "";
|
| 19 |
+
$("console").appendChild(el);
|
| 20 |
+
$("console").scrollTop = $("console").scrollHeight;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
async function bench() {
|
| 24 |
+
const R = { meta: {}, results: {} };
|
| 25 |
+
R.meta.date = new Date().toISOString();
|
| 26 |
+
R.meta.ua = navigator.userAgent;
|
| 27 |
+
R.meta.model = "Qwen3.5-2B";
|
| 28 |
+
R.meta.quant = new URLSearchParams(location.search).get("quant") ?? "q4";
|
| 29 |
+
|
| 30 |
+
const adapter = await navigator.gpu?.requestAdapter();
|
| 31 |
+
if (!adapter) throw new Error("WebGPU is not available in this browser");
|
| 32 |
+
R.meta.gpu = `${adapter.info.vendor} ${adapter.info.architecture} ${adapter.info.device ?? ""}`.trim();
|
| 33 |
+
R.meta.features = ["shader-f16", "subgroups", "timestamp-query"].filter((f) => adapter.features.has(f));
|
| 34 |
+
log(`gpu: ${R.meta.gpu}`, "hl");
|
| 35 |
+
log(`features: ${R.meta.features.join(", ")}`);
|
| 36 |
+
|
| 37 |
+
const device = await initDevice();
|
| 38 |
+
const paths = await resolvePaths();
|
| 39 |
+
R.meta.weights_source = paths.source;
|
| 40 |
+
log(`weights source: ${paths.source}`);
|
| 41 |
+
|
| 42 |
+
log(`loading Qwen3.5-2B ${R.meta.quant} weights…`);
|
| 43 |
+
let t0 = performance.now();
|
| 44 |
+
const model = await loadModel(device, { base: paths.weights, variant: R.meta.quant });
|
| 45 |
+
R.results.load_s = +((performance.now() - t0) / 1000).toFixed(1);
|
| 46 |
+
log(`loaded in ${R.results.load_s}s${model.weightsFromCache ? " (browser cache)" : ""}`, "ok");
|
| 47 |
+
const tok = await Tokenizer.load(paths.tokenizer);
|
| 48 |
+
|
| 49 |
+
log("warmup (pipeline compilation)…");
|
| 50 |
+
model.startGenerate(tok.encode("warmup run of the benchmark"));
|
| 51 |
+
await model.decodeChunk(4);
|
| 52 |
+
|
| 53 |
+
// prefill: 512 tokens x3
|
| 54 |
+
const P = 512;
|
| 55 |
+
const words = "the quick brown fox jumps over the lazy dog and keeps running through the quiet forest ".repeat(60);
|
| 56 |
+
let ids = tok.encode(words).slice(0, P);
|
| 57 |
+
const pre = [];
|
| 58 |
+
for (let r = 0; r < 3; r++) {
|
| 59 |
+
model.reset();
|
| 60 |
+
t0 = performance.now();
|
| 61 |
+
model.prefill(ids);
|
| 62 |
+
await device.queue.onSubmittedWorkDone();
|
| 63 |
+
pre.push(P / ((performance.now() - t0) / 1000));
|
| 64 |
+
}
|
| 65 |
+
R.results.prefill_tok_s = +median(pre).toFixed(1);
|
| 66 |
+
log(`prefill (${P} tok, median of 3): ${R.results.prefill_tok_s} tok/s`, "ok");
|
| 67 |
+
|
| 68 |
+
// decode: 128 tokens x5, GPU-resident chunks of 16
|
| 69 |
+
const D = 128;
|
| 70 |
+
const runs = [];
|
| 71 |
+
for (let r = 0; r < 5; r++) {
|
| 72 |
+
model.startGenerate(tok.applyChatTemplate([{ role: "user", content: "Tell me a long story about a lighthouse keeper." }]));
|
| 73 |
+
await device.queue.onSubmittedWorkDone();
|
| 74 |
+
t0 = performance.now();
|
| 75 |
+
for (let done = 1; done < D; done += 16) await model.decodeChunk(Math.min(16, D - done));
|
| 76 |
+
runs.push((D - 1) / ((performance.now() - t0) / 1000));
|
| 77 |
+
log(` decode run ${r + 1}: ${runs[r].toFixed(1)} tok/s`);
|
| 78 |
+
}
|
| 79 |
+
R.results.decode_tok_s = +median(runs).toFixed(1);
|
| 80 |
+
R.results.decode_tok_s_best = +Math.max(...runs).toFixed(1);
|
| 81 |
+
log(`decode (${D} tok x5, chunk 16): median ${R.results.decode_tok_s}, best ${R.results.decode_tok_s_best} tok/s`, "ok");
|
| 82 |
+
|
| 83 |
+
log("");
|
| 84 |
+
log(JSON.stringify({ meta: R.meta, results: R.results }), "hl");
|
| 85 |
+
return R;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
let lastR = null;
|
| 89 |
+
$("run").addEventListener("click", async () => {
|
| 90 |
+
$("run").disabled = true;
|
| 91 |
+
try {
|
| 92 |
+
lastR = await bench();
|
| 93 |
+
$("save").disabled = false;
|
| 94 |
+
window.__done = { ok: true, meta: lastR.meta, results: lastR.results };
|
| 95 |
+
} catch (e) {
|
| 96 |
+
log(String(e.message ?? e), "err");
|
| 97 |
+
window.__done = { ok: false, error: String(e.message ?? e) };
|
| 98 |
+
}
|
| 99 |
+
$("run").disabled = false;
|
| 100 |
+
});
|
| 101 |
+
$("save").addEventListener("click", () => {
|
| 102 |
+
const blob = new Blob([lines.join("\n") + "\n"], { type: "text/plain" });
|
| 103 |
+
const a = document.createElement("a");
|
| 104 |
+
a.href = URL.createObjectURL(blob);
|
| 105 |
+
a.download = `qwen35-webgpu-bench-${Date.now()}.log`;
|
| 106 |
+
a.click();
|
| 107 |
+
});
|
| 108 |
+
if (new URLSearchParams(location.search).has("auto")) $("run").click();
|
web/bench.html
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<meta charset="utf-8" />
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 5 |
+
<title>Qwen3.5-2B WebGPU · bench</title>
|
| 6 |
+
<style>
|
| 7 |
+
body { margin: 0; background: #0b0d10; color: #e8ebef; font: 14px/1.6 ui-monospace, monospace; }
|
| 8 |
+
main { max-width: 760px; margin: 40px auto; padding: 0 16px; }
|
| 9 |
+
h1 { font-size: 18px; }
|
| 10 |
+
.sub { color: #8b93a1; font-size: 12.5px; }
|
| 11 |
+
#console { background: #14171c; border: 1px solid #232830; border-radius: 10px; padding: 14px; min-height: 280px; max-height: 60vh; overflow-y: auto; white-space: pre-wrap; margin: 16px 0; }
|
| 12 |
+
#console .ok { color: #3ecf8e; }
|
| 13 |
+
#console .hl { color: #7c5cff; }
|
| 14 |
+
#console .err { color: #ff6b6b; }
|
| 15 |
+
button { background: #7c5cff; border: 0; color: #fff; border-radius: 8px; padding: 9px 16px; font: inherit; font-weight: 600; cursor: pointer; margin-right: 8px; }
|
| 16 |
+
button:disabled { opacity: .45; cursor: default; }
|
| 17 |
+
</style>
|
| 18 |
+
<body>
|
| 19 |
+
<main>
|
| 20 |
+
<h1>Qwen3.5-2B WebGPU benchmark</h1>
|
| 21 |
+
<div class="sub">Runs the standard suite on your GPU (downloads ~1.07 GB of int4 weights once, cached).
|
| 22 |
+
Share the .log — results land in the community table.</div>
|
| 23 |
+
<p>
|
| 24 |
+
<button id="run">Run bench</button>
|
| 25 |
+
<button id="save" disabled>Download .log</button>
|
| 26 |
+
</p>
|
| 27 |
+
<div id="console">idle — press Run bench</div>
|
| 28 |
+
</main>
|
| 29 |
+
<script type="module" src="/web/bench-main.js"></script>
|
| 30 |
+
</body>
|
| 31 |
+
</html>
|
web/config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Where to fetch weights + tokenizer from. A local checkout (run.mjs dev
|
| 2 |
+
// server, or an HF Space carrying the blobs) serves them same-origin;
|
| 3 |
+
// otherwise fall back to the HF model repo — its resolve/ CDN sends
|
| 4 |
+
// Access-Control-Allow-Origin: *, so any static host can pull cross-origin.
|
| 5 |
+
const HF_REPO = "borkiss/qwen35-2b-webgpu";
|
| 6 |
+
const HF = `https://huggingface.co/${HF_REPO}/resolve/main`;
|
| 7 |
+
|
| 8 |
+
let cached = null;
|
| 9 |
+
export async function resolvePaths() {
|
| 10 |
+
if (cached) return cached;
|
| 11 |
+
let local = false;
|
| 12 |
+
for (const m of ["manifest.q4.json", "manifest.f16.json"]) {
|
| 13 |
+
try {
|
| 14 |
+
const r = await fetch(`/web/weights/${m}`, { method: "HEAD" });
|
| 15 |
+
if (r.ok) { local = true; break; }
|
| 16 |
+
} catch {}
|
| 17 |
+
}
|
| 18 |
+
cached = local
|
| 19 |
+
? { weights: "/web/weights/", tokenizer: "/web/weights/tokenizer.json", source: "local" }
|
| 20 |
+
: { weights: `${HF}/weights/`, tokenizer: `${HF}/weights/tokenizer.json`, source: "huggingface" };
|
| 21 |
+
return cached;
|
| 22 |
+
}
|
web/gpu.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Thin WebGPU helpers shared by tests and the model runtime.
|
| 2 |
+
|
| 3 |
+
import { setSubgroups, setF16 } from "./kernels.js";
|
| 4 |
+
|
| 5 |
+
export async function initDevice() {
|
| 6 |
+
const adapter = await navigator.gpu?.requestAdapter({ powerPreference: "high-performance" })
|
| 7 |
+
?? await navigator.gpu?.requestAdapter();
|
| 8 |
+
if (!adapter) throw new Error("WebGPU is not available — use Chrome/Edge 113+ or Safari 26+");
|
| 9 |
+
const params = typeof location !== "undefined" ? new URLSearchParams(location.search) : new URLSearchParams();
|
| 10 |
+
// shader-f16 is a fast path, not a requirement: without it (Dawn gates it
|
| 11 |
+
// on NVIDIA/Linux; Qualcomm mobiles lack it) f16 tensors are converted to
|
| 12 |
+
// f32 at load time and the KV cache lives in f32
|
| 13 |
+
const hasF16 = !params.has("nof16") && adapter.features.has("shader-f16");
|
| 14 |
+
setF16(hasF16);
|
| 15 |
+
const features = hasF16 ? ["shader-f16"] : [];
|
| 16 |
+
// subgroups are a fast path, not a requirement: without them (iOS Safari)
|
| 17 |
+
// the matvec kernels fall back to workgroup-shared reductions
|
| 18 |
+
const hasSubgroups = !params.has("nosg") && adapter.features.has("subgroups");
|
| 19 |
+
if (hasSubgroups) features.push("subgroups");
|
| 20 |
+
setSubgroups(hasSubgroups);
|
| 21 |
+
if (adapter.features.has("timestamp-query")) features.push("timestamp-query");
|
| 22 |
+
if (hasSubgroups && adapter.features.has("chromium-experimental-subgroup-matrix"))
|
| 23 |
+
features.push("chromium-experimental-subgroup-matrix");
|
| 24 |
+
const device = await adapter.requestDevice({
|
| 25 |
+
requiredFeatures: features,
|
| 26 |
+
requiredLimits: {
|
| 27 |
+
maxBufferSize: adapter.limits.maxBufferSize,
|
| 28 |
+
maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
|
| 29 |
+
maxStorageBuffersPerShaderStage: adapter.limits.maxStorageBuffersPerShaderStage,
|
| 30 |
+
maxComputeWorkgroupStorageSize: adapter.limits.maxComputeWorkgroupStorageSize,
|
| 31 |
+
},
|
| 32 |
+
});
|
| 33 |
+
device.hasSubgroups = hasSubgroups;
|
| 34 |
+
device.hasF16 = hasF16;
|
| 35 |
+
device.lost.then((info) => console.error("GPU device lost:", info.reason, info.message));
|
| 36 |
+
return device;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export function createBuffer(device, sizeBytes, { storage = true, uniform = false } = {}) {
|
| 40 |
+
let usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC;
|
| 41 |
+
if (storage) usage |= GPUBufferUsage.STORAGE;
|
| 42 |
+
if (uniform) usage |= GPUBufferUsage.UNIFORM;
|
| 43 |
+
return device.createBuffer({ size: Math.max(sizeBytes, 16), usage });
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export function uploadF32(device, data) {
|
| 47 |
+
const buf = createBuffer(device, data.byteLength);
|
| 48 |
+
device.queue.writeBuffer(buf, 0, data);
|
| 49 |
+
return buf;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export function makePipeline(device, code, label = "") {
|
| 53 |
+
const module = device.createShaderModule({ code, label });
|
| 54 |
+
return device.createComputePipeline({ label, layout: "auto", compute: { module } });
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
export function bind(device, pipeline, entries) {
|
| 58 |
+
return device.createBindGroup({
|
| 59 |
+
layout: pipeline.getBindGroupLayout(0),
|
| 60 |
+
entries: Object.entries(entries).map(([binding, buffer]) => ({
|
| 61 |
+
binding: Number(binding),
|
| 62 |
+
resource: { buffer },
|
| 63 |
+
})),
|
| 64 |
+
});
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export async function readback(device, buffer, sizeBytes, offset = 0) {
|
| 68 |
+
const rb = device.createBuffer({
|
| 69 |
+
size: sizeBytes,
|
| 70 |
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
| 71 |
+
});
|
| 72 |
+
const enc = device.createCommandEncoder();
|
| 73 |
+
enc.copyBufferToBuffer(buffer, offset, rb, 0, sizeBytes);
|
| 74 |
+
device.queue.submit([enc.finish()]);
|
| 75 |
+
await rb.mapAsync(GPUMapMode.READ);
|
| 76 |
+
const data = new Float32Array(rb.getMappedRange().slice(0));
|
| 77 |
+
rb.destroy();
|
| 78 |
+
return data;
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// f16 helpers (Float16Array is available in Chrome 135+)
|
| 82 |
+
export function toF16(f32arr) {
|
| 83 |
+
return new Float16Array(f32arr);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
export function uploadF16(device, f32arr) {
|
| 87 |
+
const f16 = new Float16Array(f32arr);
|
| 88 |
+
const buf = createBuffer(device, f16.byteLength);
|
| 89 |
+
device.queue.writeBuffer(buf, 0, f16.buffer, f16.byteOffset, f16.byteLength);
|
| 90 |
+
return buf;
|
| 91 |
+
}
|
web/index.html
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<meta charset="utf-8" />
|
| 4 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 5 |
+
<title>Qwen3.5-2B · WebGPU</title>
|
| 6 |
+
<style>
|
| 7 |
+
:root {
|
| 8 |
+
--bg: #0b0d10; --panel: #14171c; --line: #232830; --fg: #e8ebef;
|
| 9 |
+
--dim: #8b93a1; --accent: #7c5cff; --ok: #3ecf8e; --err: #ff6b6b;
|
| 10 |
+
}
|
| 11 |
+
* { box-sizing: border-box; }
|
| 12 |
+
body {
|
| 13 |
+
margin: 0; background: var(--bg); color: var(--fg);
|
| 14 |
+
font: 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
| 15 |
+
height: 100dvh; display: flex; flex-direction: column;
|
| 16 |
+
}
|
| 17 |
+
header {
|
| 18 |
+
display: flex; align-items: baseline; gap: 12px; padding: 12px 18px;
|
| 19 |
+
border-bottom: 1px solid var(--line); flex: none;
|
| 20 |
+
}
|
| 21 |
+
.wordmark { font-weight: 700; letter-spacing: .2px; }
|
| 22 |
+
.sub { color: var(--dim); font-size: 12.5px; }
|
| 23 |
+
#gpuname { margin-left: auto; color: var(--dim); font-size: 12.5px; }
|
| 24 |
+
#toksbox { color: var(--dim); font-size: 12.5px; }
|
| 25 |
+
#toks { color: var(--ok); font-variant-numeric: tabular-nums; }
|
| 26 |
+
#chat { flex: 1; overflow-y: auto; padding: 18px; }
|
| 27 |
+
.msg { max-width: 780px; margin: 0 auto 16px; }
|
| 28 |
+
.msg .who { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim); margin-bottom: 4px; }
|
| 29 |
+
.msg.user .who { color: var(--accent); }
|
| 30 |
+
.msg .body { white-space: pre-wrap; word-break: break-word; }
|
| 31 |
+
.stats { font-size: 11.5px; color: var(--dim); margin-top: 6px; }
|
| 32 |
+
.caret { display: inline-block; width: 7px; height: 15px; background: var(--accent); animation: blink 1s steps(1) infinite; vertical-align: -2px; }
|
| 33 |
+
@keyframes blink { 50% { opacity: 0; } }
|
| 34 |
+
form { flex: none; display: flex; gap: 10px; padding: 14px 18px 18px; border-top: 1px solid var(--line); }
|
| 35 |
+
#input {
|
| 36 |
+
flex: 1; max-width: 780px; margin-left: auto; resize: none; max-height: 160px;
|
| 37 |
+
background: var(--panel); color: var(--fg); border: 1px solid var(--line);
|
| 38 |
+
border-radius: 10px; padding: 10px 12px; font: inherit; outline: none;
|
| 39 |
+
}
|
| 40 |
+
#input:focus { border-color: var(--accent); }
|
| 41 |
+
#send {
|
| 42 |
+
margin-right: auto; background: var(--accent); border: 0; color: #fff;
|
| 43 |
+
border-radius: 10px; padding: 0 18px; font: inherit; font-weight: 600; cursor: pointer;
|
| 44 |
+
}
|
| 45 |
+
#send:disabled, #input:disabled { opacity: .45; cursor: default; }
|
| 46 |
+
#boot {
|
| 47 |
+
position: fixed; inset: 0; background: var(--bg); display: flex; flex-direction: column;
|
| 48 |
+
align-items: center; justify-content: center; gap: 14px; transition: opacity .4s; z-index: 10;
|
| 49 |
+
}
|
| 50 |
+
#boot.done { opacity: 0; pointer-events: none; }
|
| 51 |
+
#boottitle { font-size: 22px; font-weight: 700; }
|
| 52 |
+
#bootsub { color: var(--dim); font-size: 13px; max-width: 420px; text-align: center; }
|
| 53 |
+
#bar { width: 300px; height: 6px; background: var(--line); border-radius: 3px; overflow: hidden; }
|
| 54 |
+
#barfill { width: 100%; height: 100%; background: var(--accent); transform: scaleX(0); transform-origin: left; transition: transform .2s; }
|
| 55 |
+
#log { font: 12px/1.7 ui-monospace, monospace; color: var(--dim); width: 420px; }
|
| 56 |
+
#log .ok { color: var(--ok); }
|
| 57 |
+
#log .err { color: var(--err); }
|
| 58 |
+
</style>
|
| 59 |
+
<body>
|
| 60 |
+
<header>
|
| 61 |
+
<span class="wordmark">Qwen3.5-2B</span>
|
| 62 |
+
<span class="sub">hybrid DeltaNet + attention · hand-written WGSL · int4</span>
|
| 63 |
+
<span id="toksbox"><span id="toks">–</span> tok/s</span>
|
| 64 |
+
<span id="gpuname"></span>
|
| 65 |
+
</header>
|
| 66 |
+
<div id="chat"><div id="msgs"></div></div>
|
| 67 |
+
<form id="form">
|
| 68 |
+
<textarea id="input" rows="1" placeholder="Message… (Enter to send)" disabled></textarea>
|
| 69 |
+
<button id="send" type="submit" disabled>Send</button>
|
| 70 |
+
</form>
|
| 71 |
+
<div id="boot">
|
| 72 |
+
<div id="boottitle">Qwen3.5-2B</div>
|
| 73 |
+
<div id="bootsub">First hybrid gated-DeltaNet LLM in the browser — pure WebGPU, hand-written WGSL kernels.
|
| 74 |
+
Downloading <span id="dlmb">~1.1 GB</span> of int4 weights <span id="dlpct"></span></div>
|
| 75 |
+
<div id="bar"><div id="barfill"></div></div>
|
| 76 |
+
<div id="dlnote" class="sub"></div>
|
| 77 |
+
<div id="log"></div>
|
| 78 |
+
</div>
|
| 79 |
+
<script type="module" src="/web/main.js"></script>
|
| 80 |
+
</body>
|
| 81 |
+
</html>
|
web/kernels.js
ADDED
|
@@ -0,0 +1,1361 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// WGSL kernel sources for Qwen3.5 (hybrid gated DeltaNet + gated attention)
|
| 2 |
+
// inference. All shapes are baked in as consts at pipeline-creation time;
|
| 3 |
+
// only per-step values (t, pos0) live in a small step buffer.
|
| 4 |
+
//
|
| 5 |
+
// Conventions (inherited from the lfm2 project):
|
| 6 |
+
// - weights: f16 (f32 when !F16), row-major [out, in], bound as array<vec4<HT>>
|
| 7 |
+
// - activations: f32; DeltaNet recurrent state: always f32
|
| 8 |
+
// - step buffer: { t: u32, pos0: u32, sstate: u32, acc: u32 } in storage so
|
| 9 |
+
// GPU kernels can bump it (GPU-resident decode loop, no CPU roundtrip)
|
| 10 |
+
|
| 11 |
+
// Whether the device supports WGSL subgroups (set by initDevice). Without
|
| 12 |
+
// them (iOS Safari) matvec kernels emit workgroup-shared reductions.
|
| 13 |
+
export let SG = true;
|
| 14 |
+
export function setSubgroups(v) {
|
| 15 |
+
SG = v;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Whether the device supports shader-f16 (set by initDevice). Without it
|
| 19 |
+
// (Dawn gates f16 on NVIDIA/Linux; Qualcomm mobiles lack it) the runtime
|
| 20 |
+
// converts every f16 tensor to f32 at load time; kernels just switch the
|
| 21 |
+
// bound array type, since all reads are already wrapped in f32() casts.
|
| 22 |
+
export let F16 = true;
|
| 23 |
+
export function setF16(v) {
|
| 24 |
+
F16 = v;
|
| 25 |
+
}
|
| 26 |
+
// half-type helpers for kernel templates (do not touch these two lines in
|
| 27 |
+
// bulk replacements — see the lfm2 EF16 regression)
|
| 28 |
+
const HT = () => (F16 ? "f16" : "f32");
|
| 29 |
+
const EF16 = () => (F16 ? "enable f16;" : "");
|
| 30 |
+
|
| 31 |
+
export const STEP_UNIFORM = `
|
| 32 |
+
struct Step { t: u32, pos0: u32, sstate: u32, acc: u32 }
|
| 33 |
+
@group(0) @binding(15) var<storage, read> step: Step;
|
| 34 |
+
`;
|
| 35 |
+
|
| 36 |
+
// ---------------------------------------------------------------------------
|
| 37 |
+
// Gated DeltaNet: recurrent decode step (one token), all heads in one dispatch.
|
| 38 |
+
//
|
| 39 |
+
// Math (per head h, state S[dk, dv], everything f32):
|
| 40 |
+
// qn = l2norm(q) / sqrt(DK); kn = l2norm(k) (eps 1e-6)
|
| 41 |
+
// decay = exp(g_h) (g_h < 0)
|
| 42 |
+
// kv_mem[dv] = sum_dk S[dk,dv]*decay * kn[dk]
|
| 43 |
+
// delta[dv] = (v[dv] - kv_mem[dv]) * beta_h
|
| 44 |
+
// S[dk,dv] = S[dk,dv]*decay + kn[dk]*delta[dv]
|
| 45 |
+
// out[dv] = sum_dk S[dk,dv] * qn[dk]
|
| 46 |
+
//
|
| 47 |
+
// One workgroup per head, WG size = DV (=128), thread li owns column dv=li.
|
| 48 |
+
// Single read pass computes acc_q = S·q_raw, acc_k = S·k_raw per column plus
|
| 49 |
+
// shared reductions of q·q, k·k, q·k; out folds algebraically:
|
| 50 |
+
// out = decay*acc_q*invq + delta * dot(kn,qn)
|
| 51 |
+
// so S is touched twice (read + read-modify-write), never three times.
|
| 52 |
+
//
|
| 53 |
+
// Bindings:
|
| 54 |
+
// 0 qkv [T, QKV] f32 (q | k | v concatenated, conv+silu already applied)
|
| 55 |
+
// 1 g [T, H] f32 (log-decay, precomputed by deltaGates)
|
| 56 |
+
// 2 beta [T, H] f32
|
| 57 |
+
// 3 state [H, DK, DV] f32 read_write
|
| 58 |
+
// 4 out [T, H*DV] f32 (core_attn_out, pre RMSNormGated)
|
| 59 |
+
// 15 step (t = token index into qkv/g/beta/out)
|
| 60 |
+
export function deltaStep({ H = 16, DK = 128, DV = 128 } = {}) {
|
| 61 |
+
const QKV = 2 * H * DK + H * DV;
|
| 62 |
+
return /* wgsl */ `
|
| 63 |
+
@group(0) @binding(0) var<storage, read> qkv: array<f32>;
|
| 64 |
+
@group(0) @binding(1) var<storage, read> g: array<f32>;
|
| 65 |
+
@group(0) @binding(2) var<storage, read> beta: array<f32>;
|
| 66 |
+
@group(0) @binding(3) var<storage, read_write> state: array<f32>;
|
| 67 |
+
@group(0) @binding(4) var<storage, read_write> out: array<f32>;
|
| 68 |
+
${STEP_UNIFORM}
|
| 69 |
+
|
| 70 |
+
const H = ${H}u;
|
| 71 |
+
const DK = ${DK}u;
|
| 72 |
+
const DV = ${DV}u;
|
| 73 |
+
const QKV = ${QKV}u;
|
| 74 |
+
const KOFF = ${H * DK}u;
|
| 75 |
+
const VOFF = ${2 * H * DK}u;
|
| 76 |
+
const SCALE = ${(1 / Math.sqrt(DK)).toPrecision(17)};
|
| 77 |
+
|
| 78 |
+
var<workgroup> qs: array<f32, DK>;
|
| 79 |
+
var<workgroup> ks: array<f32, DK>;
|
| 80 |
+
var<workgroup> red: array<vec3<f32>, DK>;
|
| 81 |
+
|
| 82 |
+
@compute @workgroup_size(${DV})
|
| 83 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 84 |
+
let h = wg.x;
|
| 85 |
+
let ti = step.t - 1u; // step.t = token count; this kernel does the last one
|
| 86 |
+
let base = ti * QKV;
|
| 87 |
+
// stage raw q/k for this head; each thread also keeps its v component
|
| 88 |
+
qs[li] = qkv[base + h * DK + li];
|
| 89 |
+
ks[li] = qkv[base + KOFF + h * DK + li];
|
| 90 |
+
let v_t = qkv[base + VOFF + h * DV + li];
|
| 91 |
+
red[li] = vec3<f32>(qs[li] * qs[li], ks[li] * ks[li], qs[li] * ks[li]);
|
| 92 |
+
workgroupBarrier();
|
| 93 |
+
// tree-reduce (q·q, k·k, q·k); portable, cost is negligible at this size
|
| 94 |
+
for (var s = DK / 2u; s > 0u; s >>= 1u) {
|
| 95 |
+
if (li < s) { red[li] += red[li + s]; }
|
| 96 |
+
workgroupBarrier();
|
| 97 |
+
}
|
| 98 |
+
let sums = red[0];
|
| 99 |
+
let invq = inverseSqrt(sums.x + 1e-6) * SCALE;
|
| 100 |
+
let invk = inverseSqrt(sums.y + 1e-6);
|
| 101 |
+
let dot_kq = sums.z * invq * invk;
|
| 102 |
+
let decay = exp(g[ti * H + h]);
|
| 103 |
+
let beta_h = beta[ti * H + h];
|
| 104 |
+
|
| 105 |
+
// pass 1 over S: projections of the old state onto k and q
|
| 106 |
+
let sbase = h * DK * DV + li;
|
| 107 |
+
var acc_q = 0.0;
|
| 108 |
+
var acc_k = 0.0;
|
| 109 |
+
for (var dk = 0u; dk < DK; dk++) {
|
| 110 |
+
let s = state[sbase + dk * DV];
|
| 111 |
+
acc_q += s * qs[dk];
|
| 112 |
+
acc_k += s * ks[dk];
|
| 113 |
+
}
|
| 114 |
+
let kv_mem = decay * acc_k * invk;
|
| 115 |
+
let delta = (v_t - kv_mem) * beta_h;
|
| 116 |
+
let out_v = decay * acc_q * invq + dot_kq * delta;
|
| 117 |
+
|
| 118 |
+
// pass 2 over S: decay + rank-1 update
|
| 119 |
+
for (var dk = 0u; dk < DK; dk++) {
|
| 120 |
+
let idx = sbase + dk * DV;
|
| 121 |
+
state[idx] = state[idx] * decay + ks[dk] * invk * delta;
|
| 122 |
+
}
|
| 123 |
+
out[ti * H * DV + h * DV + li] = out_v;
|
| 124 |
+
}`;
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
// ---------------------------------------------------------------------------
|
| 128 |
+
// Gated DeltaNet: chunked prefill (WY representation), chunk size 64.
|
| 129 |
+
// Reference: torch_chunk_gated_delta_rule (transformers qwen3_next).
|
| 130 |
+
//
|
| 131 |
+
// Stage 1 (deltaChunkLocal) is parallel over (chunk, head): per chunk it
|
| 132 |
+
// l2-normalizes q/k, cumsums g, builds the strictly-lower matrix
|
| 133 |
+
// A[i][j] = -beta_i * kn_i·kn_j * exp(gc_i - gc_j)
|
| 134 |
+
// runs the forward substitution T = (I - A)^{-1} in workgroup memory, and
|
| 135 |
+
// stores qn, kn, tv = T@(v*beta), kcd = T@(kn*beta*exp(gc)), gc into `loc`.
|
| 136 |
+
//
|
| 137 |
+
// Stage 2 (deltaChunkScan) is the sequential inter-chunk scan, one workgroup
|
| 138 |
+
// per head; thread li owns state column S[:, li] in private registers for the
|
| 139 |
+
// whole segment (state storage is touched once at start and once at end):
|
| 140 |
+
// v_new = tv - kcd @ S
|
| 141 |
+
// out = (qn*e^gc) @ S + tril(qn·kn * e^(gc_r-gc_j)) @ v_new
|
| 142 |
+
// S = S*e^glast + (kn*e^(glast-gc))^T @ v_new
|
| 143 |
+
// v_new columns never cross threads, so they live in private memory too.
|
| 144 |
+
//
|
| 145 |
+
// loc layout per (chunk, head) block, floats:
|
| 146 |
+
// qn 64*128 | kn 64*128 | tv 64*128 | kcd 64*128 | gc 64 (stride 32832)
|
| 147 |
+
// step.t = token count of the segment; chunk count = ceil(t / 64).
|
| 148 |
+
export const DELTA_BLK = 4 * 64 * 128 + 64;
|
| 149 |
+
|
| 150 |
+
export function deltaChunkLocal({ H = 16, DK = 128, DV = 128 } = {}) {
|
| 151 |
+
const QKV = 2 * H * DK + H * DV;
|
| 152 |
+
return /* wgsl */ `
|
| 153 |
+
@group(0) @binding(0) var<storage, read> qkv: array<f32>;
|
| 154 |
+
@group(0) @binding(1) var<storage, read> g: array<f32>;
|
| 155 |
+
@group(0) @binding(2) var<storage, read> beta: array<f32>;
|
| 156 |
+
@group(0) @binding(3) var<storage, read_write> loc: array<f32>;
|
| 157 |
+
${STEP_UNIFORM}
|
| 158 |
+
|
| 159 |
+
const H = ${H}u;
|
| 160 |
+
const DK = ${DK}u;
|
| 161 |
+
const DV = ${DV}u;
|
| 162 |
+
const QKV = ${QKV}u;
|
| 163 |
+
const KOFF = ${H * DK}u;
|
| 164 |
+
const VOFF = ${2 * H * DK}u;
|
| 165 |
+
const BLK = ${DELTA_BLK}u;
|
| 166 |
+
const SCALE = ${(1 / Math.sqrt(DK)).toPrecision(17)};
|
| 167 |
+
|
| 168 |
+
var<workgroup> Tm: array<f32, 4096>;
|
| 169 |
+
var<workgroup> rowbuf: array<f32, 64>;
|
| 170 |
+
var<workgroup> gcs: array<f32, 64>;
|
| 171 |
+
var<workgroup> invq: array<f32, 64>;
|
| 172 |
+
var<workgroup> invk: array<f32, 64>;
|
| 173 |
+
var<workgroup> betas: array<f32, 64>;
|
| 174 |
+
var<workgroup> vrow: array<f32, ${DV}>;
|
| 175 |
+
var<workgroup> krow: array<f32, ${DK}>;
|
| 176 |
+
var<workgroup> wgT: u32;
|
| 177 |
+
|
| 178 |
+
@compute @workgroup_size(128)
|
| 179 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 180 |
+
if (li == 0u) { wgT = step.t; }
|
| 181 |
+
let nT = workgroupUniformLoad(&wgT);
|
| 182 |
+
let c = wg.x / H;
|
| 183 |
+
let h = wg.x % H;
|
| 184 |
+
let blk = wg.x * BLK;
|
| 185 |
+
|
| 186 |
+
// per-token l2 norms, beta, raw g
|
| 187 |
+
if (li < 64u) {
|
| 188 |
+
let tok = c * 64u + li;
|
| 189 |
+
var sq = 0.0;
|
| 190 |
+
var sk = 0.0;
|
| 191 |
+
if (tok < nT) {
|
| 192 |
+
let qb = tok * QKV + h * DK;
|
| 193 |
+
let kb = tok * QKV + KOFF + h * DK;
|
| 194 |
+
for (var d = 0u; d < DK; d++) {
|
| 195 |
+
let qv = qkv[qb + d];
|
| 196 |
+
let kv = qkv[kb + d];
|
| 197 |
+
sq += qv * qv;
|
| 198 |
+
sk += kv * kv;
|
| 199 |
+
}
|
| 200 |
+
invq[li] = inverseSqrt(sq + 1e-6) * SCALE;
|
| 201 |
+
invk[li] = inverseSqrt(sk + 1e-6);
|
| 202 |
+
betas[li] = beta[tok * H + h];
|
| 203 |
+
gcs[li] = g[tok * H + h];
|
| 204 |
+
} else {
|
| 205 |
+
invq[li] = 0.0; invk[li] = 0.0; betas[li] = 0.0; gcs[li] = 0.0;
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
workgroupBarrier();
|
| 209 |
+
if (li == 0u) {
|
| 210 |
+
for (var i = 1u; i < 64u; i++) { gcs[i] += gcs[i - 1u]; }
|
| 211 |
+
}
|
| 212 |
+
workgroupBarrier();
|
| 213 |
+
|
| 214 |
+
// A[i][j] (strictly lower) into Tm
|
| 215 |
+
for (var idx = li; idx < 4096u; idx += 128u) {
|
| 216 |
+
let i = idx >> 6u;
|
| 217 |
+
let j = idx & 63u;
|
| 218 |
+
var val = 0.0;
|
| 219 |
+
if (j < i) {
|
| 220 |
+
let toki = c * 64u + i;
|
| 221 |
+
let tokj = c * 64u + j;
|
| 222 |
+
if (tokj < nT && toki < nT) {
|
| 223 |
+
let kbi = toki * QKV + KOFF + h * DK;
|
| 224 |
+
let kbj = tokj * QKV + KOFF + h * DK;
|
| 225 |
+
var dt = 0.0;
|
| 226 |
+
for (var d = 0u; d < DK; d++) { dt += qkv[kbi + d] * qkv[kbj + d]; }
|
| 227 |
+
val = -betas[i] * dt * invk[i] * invk[j] * exp(gcs[i] - gcs[j]);
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
Tm[idx] = val;
|
| 231 |
+
}
|
| 232 |
+
workgroupBarrier();
|
| 233 |
+
|
| 234 |
+
// forward substitution: T = (I - A)^{-1}, unit lower-triangular
|
| 235 |
+
for (var i = 1u; i < 64u; i++) {
|
| 236 |
+
if (li < 64u) { rowbuf[li] = Tm[i * 64u + li]; }
|
| 237 |
+
workgroupBarrier();
|
| 238 |
+
if (li < i) {
|
| 239 |
+
var acc = rowbuf[li];
|
| 240 |
+
for (var m = li + 1u; m < i; m++) { acc += rowbuf[m] * Tm[m * 64u + li]; }
|
| 241 |
+
Tm[i * 64u + li] = acc;
|
| 242 |
+
}
|
| 243 |
+
workgroupBarrier();
|
| 244 |
+
}
|
| 245 |
+
if (li < 64u) { Tm[li * 64u + li] = 1.0; }
|
| 246 |
+
workgroupBarrier();
|
| 247 |
+
|
| 248 |
+
// tv = T @ (v*beta), kcd = T @ (kn*beta*e^gc); accumulate row-wise while
|
| 249 |
+
// staging one (v, k) token row at a time
|
| 250 |
+
var accV = array<f32, 64>();
|
| 251 |
+
var accK = array<f32, 64>();
|
| 252 |
+
for (var j = 0u; j < 64u; j++) {
|
| 253 |
+
let tok = c * 64u + j;
|
| 254 |
+
if (tok < nT) {
|
| 255 |
+
vrow[li] = qkv[tok * QKV + VOFF + h * DV + li] * betas[j];
|
| 256 |
+
krow[li] = qkv[tok * QKV + KOFF + h * DK + li] * invk[j] * betas[j] * exp(gcs[j]);
|
| 257 |
+
} else {
|
| 258 |
+
vrow[li] = 0.0; krow[li] = 0.0;
|
| 259 |
+
}
|
| 260 |
+
workgroupBarrier();
|
| 261 |
+
for (var r = j; r < 64u; r++) {
|
| 262 |
+
let tj = Tm[r * 64u + j];
|
| 263 |
+
accV[r] += tj * vrow[li];
|
| 264 |
+
accK[r] += tj * krow[li];
|
| 265 |
+
}
|
| 266 |
+
workgroupBarrier();
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
// store qn, kn, tv, kcd, gc
|
| 270 |
+
for (var r = 0u; r < 64u; r++) {
|
| 271 |
+
let tok = c * 64u + r;
|
| 272 |
+
var qn = 0.0;
|
| 273 |
+
var kn = 0.0;
|
| 274 |
+
if (tok < nT) {
|
| 275 |
+
qn = qkv[tok * QKV + h * DK + li] * invq[r];
|
| 276 |
+
kn = qkv[tok * QKV + KOFF + h * DK + li] * invk[r];
|
| 277 |
+
}
|
| 278 |
+
loc[blk + r * 128u + li] = qn;
|
| 279 |
+
loc[blk + 8192u + r * 128u + li] = kn;
|
| 280 |
+
loc[blk + 16384u + r * 128u + li] = accV[r];
|
| 281 |
+
loc[blk + 24576u + r * 128u + li] = accK[r];
|
| 282 |
+
}
|
| 283 |
+
if (li < 64u) { loc[blk + 32768u + li] = gcs[li]; }
|
| 284 |
+
}`;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
export function deltaChunkScan({ H = 16, DK = 128, DV = 128 } = {}) {
|
| 288 |
+
return /* wgsl */ `
|
| 289 |
+
@group(0) @binding(0) var<storage, read> loc: array<f32>;
|
| 290 |
+
@group(0) @binding(1) var<storage, read_write> state: array<f32>;
|
| 291 |
+
@group(0) @binding(2) var<storage, read_write> out: array<f32>;
|
| 292 |
+
${STEP_UNIFORM}
|
| 293 |
+
|
| 294 |
+
const H = ${H}u;
|
| 295 |
+
const DK = ${DK}u;
|
| 296 |
+
const DV = ${DV}u;
|
| 297 |
+
const BLK = ${DELTA_BLK}u;
|
| 298 |
+
|
| 299 |
+
var<workgroup> alocal: array<f32, 4096>;
|
| 300 |
+
var<workgroup> arow: array<f32, ${DK}>;
|
| 301 |
+
var<workgroup> brow: array<f32, ${DK}>;
|
| 302 |
+
var<workgroup> gcs: array<f32, 64>;
|
| 303 |
+
var<workgroup> wgT: u32;
|
| 304 |
+
|
| 305 |
+
@compute @workgroup_size(128)
|
| 306 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 307 |
+
if (li == 0u) { wgT = step.t; }
|
| 308 |
+
let nT = workgroupUniformLoad(&wgT);
|
| 309 |
+
let nC = (nT + 63u) / 64u;
|
| 310 |
+
let h = wg.x;
|
| 311 |
+
|
| 312 |
+
// thread li owns state column S[:, li] for the whole segment
|
| 313 |
+
var s = array<f32, ${DK}>();
|
| 314 |
+
for (var dk = 0u; dk < DK; dk++) { s[dk] = state[h * DK * DV + dk * DV + li]; }
|
| 315 |
+
var vncol = array<f32, 64>();
|
| 316 |
+
|
| 317 |
+
for (var c = 0u; c < nC; c++) {
|
| 318 |
+
let blk = (c * H + h) * BLK;
|
| 319 |
+
if (li < 64u) { gcs[li] = loc[blk + 32768u + li]; }
|
| 320 |
+
workgroupBarrier();
|
| 321 |
+
let glast = gcs[63];
|
| 322 |
+
|
| 323 |
+
// intra-chunk attention: alocal[r][j] = qn_r·kn_j * e^(gc_r-gc_j), j<=r
|
| 324 |
+
for (var idx = li; idx < 4096u; idx += 128u) {
|
| 325 |
+
let r = idx >> 6u;
|
| 326 |
+
let j = idx & 63u;
|
| 327 |
+
var val = 0.0;
|
| 328 |
+
if (j <= r) {
|
| 329 |
+
var dt = 0.0;
|
| 330 |
+
for (var d = 0u; d < DK; d++) {
|
| 331 |
+
dt += loc[blk + r * 128u + d] * loc[blk + 8192u + j * 128u + d];
|
| 332 |
+
}
|
| 333 |
+
val = dt * exp(gcs[r] - gcs[j]);
|
| 334 |
+
}
|
| 335 |
+
alocal[idx] = val;
|
| 336 |
+
}
|
| 337 |
+
workgroupBarrier();
|
| 338 |
+
|
| 339 |
+
// rows in order: v_new, then out (v_new columns stay in this thread)
|
| 340 |
+
for (var r = 0u; r < 64u; r++) {
|
| 341 |
+
arow[li] = loc[blk + 24576u + r * 128u + li]; // kcd row
|
| 342 |
+
brow[li] = loc[blk + r * 128u + li] * exp(gcs[r]); // qn*e^gc row
|
| 343 |
+
workgroupBarrier();
|
| 344 |
+
var vp = 0.0;
|
| 345 |
+
var ai = 0.0;
|
| 346 |
+
for (var dk = 0u; dk < DK; dk++) {
|
| 347 |
+
vp += arow[dk] * s[dk];
|
| 348 |
+
ai += brow[dk] * s[dk];
|
| 349 |
+
}
|
| 350 |
+
let vn = loc[blk + 16384u + r * 128u + li] - vp;
|
| 351 |
+
vncol[r] = vn;
|
| 352 |
+
var o = ai;
|
| 353 |
+
for (var j = 0u; j <= r; j++) { o += alocal[r * 64u + j] * vncol[j]; }
|
| 354 |
+
let tok = c * 64u + r;
|
| 355 |
+
if (tok < nT) { out[tok * H * DV + h * DV + li] = o; }
|
| 356 |
+
workgroupBarrier();
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// S = S*e^glast + sum_r kn_r*e^(glast-gc_r) (outer) v_new_r
|
| 360 |
+
let dl = exp(glast);
|
| 361 |
+
for (var dk = 0u; dk < DK; dk++) { s[dk] *= dl; }
|
| 362 |
+
for (var r = 0u; r < 64u; r++) {
|
| 363 |
+
arow[li] = loc[blk + 8192u + r * 128u + li] * exp(glast - gcs[r]);
|
| 364 |
+
workgroupBarrier();
|
| 365 |
+
let vn = vncol[r];
|
| 366 |
+
for (var dk = 0u; dk < DK; dk++) { s[dk] += arow[dk] * vn; }
|
| 367 |
+
workgroupBarrier();
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
for (var dk = 0u; dk < DK; dk++) { state[h * DK * DV + dk * DV + li] = s[dk]; }
|
| 372 |
+
}`;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
// ---------------------------------------------------------------------------
|
| 376 |
+
// DeltaNet plumbing: gates, causal conv, gated RMSNorm
|
| 377 |
+
// ---------------------------------------------------------------------------
|
| 378 |
+
|
| 379 |
+
// beta = sigmoid(b), g = -exp(A_log) * softplus(a + dt_bias).
|
| 380 |
+
// b|a live in the fused in_proj activation at row stride `row`, offset `off`;
|
| 381 |
+
// params = A_log|dt_bias [2*H] f32.
|
| 382 |
+
export function deltaGates({ H = 16, row = 2 * 16, off = 0 } = {}) {
|
| 383 |
+
return /* wgsl */ `
|
| 384 |
+
${STEP_UNIFORM}
|
| 385 |
+
@group(0) @binding(0) var<storage, read> ba: array<f32>;
|
| 386 |
+
@group(0) @binding(1) var<storage, read> params: array<f32>;
|
| 387 |
+
@group(0) @binding(2) var<storage, read_write> g: array<f32>;
|
| 388 |
+
@group(0) @binding(3) var<storage, read_write> beta: array<f32>;
|
| 389 |
+
|
| 390 |
+
const H = ${H}u;
|
| 391 |
+
|
| 392 |
+
@compute @workgroup_size(64)
|
| 393 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 394 |
+
let i = gid.x;
|
| 395 |
+
if (i >= step.t * H) { return; }
|
| 396 |
+
let t = i / H;
|
| 397 |
+
let h = i % H;
|
| 398 |
+
let b = ba[t * ${row}u + ${off}u + h];
|
| 399 |
+
let a = ba[t * ${row}u + ${off}u + H + h];
|
| 400 |
+
beta[i] = 1.0 / (1.0 + exp(-b));
|
| 401 |
+
let x = a + params[H + h];
|
| 402 |
+
let sp = select(log(1.0 + exp(x)), x, x > 20.0);
|
| 403 |
+
g[i] = -exp(params[h]) * sp;
|
| 404 |
+
}`;
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
// Depthwise causal conv (kernel 4, bias-free) + SiLU over the fused qkv
|
| 408 |
+
// activation (first C of each `xrow`-wide row). state[c, 0..3] = raw inputs
|
| 409 |
+
// at t-3..t (persistent). One thread per channel, sequential over t.
|
| 410 |
+
export function convQkv({ C, xrow = null }) {
|
| 411 |
+
const XROW = xrow ?? C;
|
| 412 |
+
return /* wgsl */ `
|
| 413 |
+
${EF16()}
|
| 414 |
+
${STEP_UNIFORM}
|
| 415 |
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
| 416 |
+
@group(0) @binding(1) var<storage, read> w: array<${HT()}>; // [C, 4]
|
| 417 |
+
@group(0) @binding(2) var<storage, read_write> state: array<f32>; // [C, 4]
|
| 418 |
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>; // [T, C]
|
| 419 |
+
|
| 420 |
+
const C = ${C}u;
|
| 421 |
+
|
| 422 |
+
@compute @workgroup_size(256)
|
| 423 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 424 |
+
let c = gid.x;
|
| 425 |
+
if (c >= C) { return; }
|
| 426 |
+
let w0 = f32(w[c * 4u]);
|
| 427 |
+
let w1 = f32(w[c * 4u + 1u]);
|
| 428 |
+
let w2 = f32(w[c * 4u + 2u]);
|
| 429 |
+
let w3 = f32(w[c * 4u + 3u]);
|
| 430 |
+
var s0 = state[c * 4u];
|
| 431 |
+
var s1 = state[c * 4u + 1u];
|
| 432 |
+
var s2 = state[c * 4u + 2u];
|
| 433 |
+
var s3 = state[c * 4u + 3u];
|
| 434 |
+
for (var t = 0u; t < step.t; t++) {
|
| 435 |
+
let xv = x[t * ${XROW}u + c];
|
| 436 |
+
let acc = w0 * s1 + w1 * s2 + w2 * s3 + w3 * xv;
|
| 437 |
+
y[t * C + c] = acc / (1.0 + exp(-acc));
|
| 438 |
+
s0 = s1; s1 = s2; s2 = s3; s3 = xv;
|
| 439 |
+
}
|
| 440 |
+
state[c * 4u] = s0;
|
| 441 |
+
state[c * 4u + 1u] = s1;
|
| 442 |
+
state[c * 4u + 2u] = s2;
|
| 443 |
+
state[c * 4u + 3u] = s3;
|
| 444 |
+
}`;
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
// Gated RMSNorm per value head (dim DV): y = rmsnorm(x)*w * silu(z).
|
| 448 |
+
// Weight is per head-dim, shared across heads (plain scale, NOT (1+w)).
|
| 449 |
+
// z lives in the fused in_proj activation at row stride zrow, offset zoff.
|
| 450 |
+
// One workgroup per (t, head); wg.x = t*H + h.
|
| 451 |
+
export function gatedNorm({ H = 16, DV = 128, eps = 1e-6, zrow = null, zoff = 0 } = {}) {
|
| 452 |
+
const ZROW = zrow ?? H * DV;
|
| 453 |
+
return /* wgsl */ `
|
| 454 |
+
${EF16()}
|
| 455 |
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
| 456 |
+
@group(0) @binding(1) var<storage, read> z: array<f32>;
|
| 457 |
+
@group(0) @binding(2) var<storage, read> w: array<${HT()}>;
|
| 458 |
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
| 459 |
+
|
| 460 |
+
const DV = ${DV}u;
|
| 461 |
+
const H = ${H}u;
|
| 462 |
+
const EPS = ${eps};
|
| 463 |
+
var<workgroup> red: array<f32, ${DV}>;
|
| 464 |
+
|
| 465 |
+
@compute @workgroup_size(${DV})
|
| 466 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 467 |
+
let base = wg.x * DV;
|
| 468 |
+
let t = wg.x / H;
|
| 469 |
+
let h = wg.x % H;
|
| 470 |
+
let v = x[base + li];
|
| 471 |
+
red[li] = v * v;
|
| 472 |
+
workgroupBarrier();
|
| 473 |
+
for (var s = ${DV / 2}u; s > 0u; s >>= 1u) {
|
| 474 |
+
if (li < s) { red[li] += red[li + s]; }
|
| 475 |
+
workgroupBarrier();
|
| 476 |
+
}
|
| 477 |
+
let inv = inverseSqrt(red[0] / f32(DV) + EPS);
|
| 478 |
+
let zv = z[t * ${ZROW}u + ${zoff}u + h * DV + li];
|
| 479 |
+
y[base + li] = v * inv * f32(w[li]) * (zv / (1.0 + exp(-zv)));
|
| 480 |
+
}`;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
// ---------------------------------------------------------------------------
|
| 484 |
+
// Gated attention (full_attention layers)
|
| 485 |
+
// ---------------------------------------------------------------------------
|
| 486 |
+
|
| 487 |
+
// Post-QKV processing for Qwen3.5 gated attention. Fused projection rows:
|
| 488 |
+
// [ NH * (q 256 | gate 256) | NKV k 256 | NKV v 256 ] (ROW = 5120)
|
| 489 |
+
// Per-head RMSNorm over the full HD on q and k (weights pre-normalized to
|
| 490 |
+
// plain scale by the converter), then neox RoPE on the first RD dims only
|
| 491 |
+
// (theta 1e7), remaining dims pass through. Gates stay in the qkv activation
|
| 492 |
+
// (gateMul reads them after attention). wg.x = t*(NH+NKV)+slot; WG = HD.
|
| 493 |
+
export function qkvRope({ NH = 8, NKV = 2, HD = 256, RD = 64, eps = 1e-6, ropeTheta = 1e7, maxSeq = 4096 } = {}) {
|
| 494 |
+
const ROW = NH * HD * 2 + 2 * NKV * HD;
|
| 495 |
+
const KO = NH * HD * 2;
|
| 496 |
+
const VO = NH * HD * 2 + NKV * HD;
|
| 497 |
+
const RH = RD / 2;
|
| 498 |
+
return /* wgsl */ `
|
| 499 |
+
${EF16()}
|
| 500 |
+
${STEP_UNIFORM}
|
| 501 |
+
@group(0) @binding(0) var<storage, read> qkv: array<f32>; // [T, ${ROW}]
|
| 502 |
+
@group(0) @binding(3) var<storage, read> qw: array<${HT()}>; // [HD]
|
| 503 |
+
@group(0) @binding(4) var<storage, read> kw: array<${HT()}>; // [HD]
|
| 504 |
+
@group(0) @binding(5) var<storage, read_write> qout: array<f32>; // [T, NH*HD]
|
| 505 |
+
@group(0) @binding(6) var<storage, read_write> kcache: array<${HT()}>; // [maxSeq, NKV*HD]
|
| 506 |
+
@group(0) @binding(7) var<storage, read_write> vcache: array<${HT()}>; // [maxSeq, NKV*HD]
|
| 507 |
+
|
| 508 |
+
const NH = ${NH}u;
|
| 509 |
+
const NKV = ${NKV}u;
|
| 510 |
+
const HD = ${HD}u;
|
| 511 |
+
const RD = ${RD}u;
|
| 512 |
+
const RH = ${RH}u;
|
| 513 |
+
const EPS = ${eps};
|
| 514 |
+
const SLOTS = ${NH + NKV}u;
|
| 515 |
+
const ROW = ${ROW}u;
|
| 516 |
+
var<workgroup> red: array<f32, ${HD}>;
|
| 517 |
+
var<workgroup> sn: array<f32, ${HD}>;
|
| 518 |
+
|
| 519 |
+
fn ropeMix(d: u32, pos: u32) -> f32 {
|
| 520 |
+
// normed value at dim d after partial neox rope (reads staged sn[])
|
| 521 |
+
if (d >= RD) { return sn[d]; }
|
| 522 |
+
let p = d % RH;
|
| 523 |
+
let theta = f32(pos) * pow(${ropeTheta}, -f32(p) / f32(RH));
|
| 524 |
+
let c = cos(theta);
|
| 525 |
+
let s = sin(theta);
|
| 526 |
+
if (d < RH) { return sn[d] * c - sn[d + RH] * s; }
|
| 527 |
+
return sn[d] * c + sn[d - RH] * s;
|
| 528 |
+
}
|
| 529 |
+
|
| 530 |
+
@compute @workgroup_size(${HD})
|
| 531 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) d: u32) {
|
| 532 |
+
let t = wg.x / SLOTS;
|
| 533 |
+
if (t >= step.t) { return; }
|
| 534 |
+
let slot = wg.x % SLOTS;
|
| 535 |
+
let pos = step.pos0 + t;
|
| 536 |
+
|
| 537 |
+
if (slot < NH) {
|
| 538 |
+
let base = t * ROW + slot * HD * 2u; // q part of this head
|
| 539 |
+
let v = qkv[base + d];
|
| 540 |
+
red[d] = v * v;
|
| 541 |
+
workgroupBarrier();
|
| 542 |
+
for (var s = ${HD / 2}u; s > 0u; s >>= 1u) {
|
| 543 |
+
if (d < s) { red[d] += red[d + s]; }
|
| 544 |
+
workgroupBarrier();
|
| 545 |
+
}
|
| 546 |
+
let inv = inverseSqrt(red[0] / f32(HD) + EPS);
|
| 547 |
+
sn[d] = v * inv * f32(qw[d]);
|
| 548 |
+
workgroupBarrier();
|
| 549 |
+
qout[t * NH * HD + slot * HD + d] = ropeMix(d, pos);
|
| 550 |
+
} else {
|
| 551 |
+
let h = slot - NH;
|
| 552 |
+
let kbase = t * ROW + ${KO}u + h * HD;
|
| 553 |
+
let vbase = t * ROW + ${VO}u + h * HD;
|
| 554 |
+
let cbase = pos * NKV * HD + h * HD;
|
| 555 |
+
let v = qkv[kbase + d];
|
| 556 |
+
red[d] = v * v;
|
| 557 |
+
workgroupBarrier();
|
| 558 |
+
for (var s = ${HD / 2}u; s > 0u; s >>= 1u) {
|
| 559 |
+
if (d < s) { red[d] += red[d + s]; }
|
| 560 |
+
workgroupBarrier();
|
| 561 |
+
}
|
| 562 |
+
let inv = inverseSqrt(red[0] / f32(HD) + EPS);
|
| 563 |
+
sn[d] = v * inv * f32(kw[d]);
|
| 564 |
+
workgroupBarrier();
|
| 565 |
+
kcache[cbase + d] = ${HT()}(ropeMix(d, pos));
|
| 566 |
+
vcache[cbase + d] = ${HT()}(qkv[vbase + d]);
|
| 567 |
+
}
|
| 568 |
+
}`;
|
| 569 |
+
}
|
| 570 |
+
|
| 571 |
+
// Causal attention over the KV cache (GQA). One workgroup per (t, head).
|
| 572 |
+
// Same structure as the lfm2 kernel, sized for HD=256.
|
| 573 |
+
export function attention({ NH = 8, NKV = 2, HD = 256, maxSeq = 4096 } = {}) {
|
| 574 |
+
const group = NH / NKV;
|
| 575 |
+
const scale = 1 / Math.sqrt(HD);
|
| 576 |
+
const HD4 = HD / 4;
|
| 577 |
+
const KVW4 = (NKV * HD) / 4;
|
| 578 |
+
return /* wgsl */ `
|
| 579 |
+
${EF16()}
|
| 580 |
+
${STEP_UNIFORM}
|
| 581 |
+
@group(0) @binding(0) var<storage, read> q: array<vec4<f32>>; // [T, NH*HD/4]
|
| 582 |
+
@group(0) @binding(1) var<storage, read> kcache: array<vec4<${HT()}>>; // [maxSeq, NKV*HD/4]
|
| 583 |
+
@group(0) @binding(2) var<storage, read> vcache: array<vec4<${HT()}>>; // [maxSeq, NKV*HD/4]
|
| 584 |
+
@group(0) @binding(3) var<storage, read_write> out: array<vec4<f32>>; // [T, NH*HD/4]
|
| 585 |
+
|
| 586 |
+
const NH = ${NH}u;
|
| 587 |
+
const HD4 = ${HD4}u;
|
| 588 |
+
const KVW4 = ${KVW4}u;
|
| 589 |
+
const SCALE = ${scale};
|
| 590 |
+
|
| 591 |
+
var<workgroup> qs4: array<vec4<f32>, ${HD4}>;
|
| 592 |
+
var<workgroup> scores: array<f32, ${maxSeq}>;
|
| 593 |
+
var<workgroup> red: array<f32, ${HD}>;
|
| 594 |
+
var<workgroup> red4: array<vec4<f32>, ${HD}>;
|
| 595 |
+
|
| 596 |
+
@compute @workgroup_size(${HD})
|
| 597 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 598 |
+
let t = wg.x / NH;
|
| 599 |
+
if (t >= step.t) { return; }
|
| 600 |
+
let h = wg.x % NH;
|
| 601 |
+
let kvh = h / ${group}u;
|
| 602 |
+
let S = step.pos0 + t + 1u; // visible keys
|
| 603 |
+
if (li < HD4) { qs4[li] = q[(t * NH + h) * HD4 + li]; }
|
| 604 |
+
workgroupBarrier();
|
| 605 |
+
|
| 606 |
+
var lmax = -3.4e38;
|
| 607 |
+
for (var j = li; j < S; j += ${HD}u) {
|
| 608 |
+
var dp = 0.0;
|
| 609 |
+
let kbase = j * KVW4 + kvh * HD4;
|
| 610 |
+
for (var d = 0u; d < HD4; d++) {
|
| 611 |
+
dp += dot(vec4<f32>(kcache[kbase + d]), qs4[d]);
|
| 612 |
+
}
|
| 613 |
+
let sc = dp * SCALE;
|
| 614 |
+
scores[j] = sc;
|
| 615 |
+
lmax = max(lmax, sc);
|
| 616 |
+
}
|
| 617 |
+
red[li] = lmax;
|
| 618 |
+
workgroupBarrier();
|
| 619 |
+
var stride = ${HD / 2}u;
|
| 620 |
+
while (stride > 0u) {
|
| 621 |
+
if (li < stride) { red[li] = max(red[li], red[li + stride]); }
|
| 622 |
+
workgroupBarrier();
|
| 623 |
+
stride /= 2u;
|
| 624 |
+
}
|
| 625 |
+
let m = red[0];
|
| 626 |
+
workgroupBarrier();
|
| 627 |
+
|
| 628 |
+
var lsum = 0.0;
|
| 629 |
+
for (var j = li; j < S; j += ${HD}u) {
|
| 630 |
+
let e = exp(scores[j] - m);
|
| 631 |
+
scores[j] = e;
|
| 632 |
+
lsum += e;
|
| 633 |
+
}
|
| 634 |
+
red[li] = lsum;
|
| 635 |
+
workgroupBarrier();
|
| 636 |
+
stride = ${HD / 2}u;
|
| 637 |
+
while (stride > 0u) {
|
| 638 |
+
if (li < stride) { red[li] += red[li + stride]; }
|
| 639 |
+
workgroupBarrier();
|
| 640 |
+
stride /= 2u;
|
| 641 |
+
}
|
| 642 |
+
let z = red[0];
|
| 643 |
+
|
| 644 |
+
let d4 = li % HD4;
|
| 645 |
+
let blk = li / HD4;
|
| 646 |
+
var acc = vec4<f32>(0.0);
|
| 647 |
+
for (var j = blk; j < S; j += ${HD / HD4}u) {
|
| 648 |
+
acc += scores[j] * vec4<f32>(vcache[j * KVW4 + kvh * HD4 + d4]);
|
| 649 |
+
}
|
| 650 |
+
red4[li] = acc;
|
| 651 |
+
workgroupBarrier();
|
| 652 |
+
if (li < HD4) {
|
| 653 |
+
var o = red4[li];
|
| 654 |
+
for (var b = 1u; b < ${HD / HD4}u; b++) {
|
| 655 |
+
o += red4[li + b * HD4];
|
| 656 |
+
}
|
| 657 |
+
out[(t * NH + h) * HD4 + li] = o / z;
|
| 658 |
+
}
|
| 659 |
+
}`;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
// attn_out *= sigmoid(gate); gates live in the fused qkv activation at
|
| 663 |
+
// per-head offset h*2*HD + HD. y is [T, NH*HD].
|
| 664 |
+
export function gateMul({ NH = 8, NKV = 2, HD = 256 } = {}) {
|
| 665 |
+
const ROW = NH * HD * 2 + 2 * NKV * HD;
|
| 666 |
+
return /* wgsl */ `
|
| 667 |
+
${STEP_UNIFORM}
|
| 668 |
+
@group(0) @binding(0) var<storage, read> qkv: array<f32>;
|
| 669 |
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
| 670 |
+
|
| 671 |
+
const NH = ${NH}u;
|
| 672 |
+
const HD = ${HD}u;
|
| 673 |
+
|
| 674 |
+
@compute @workgroup_size(256)
|
| 675 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 676 |
+
let i = gid.x;
|
| 677 |
+
if (i >= step.t * NH * HD) { return; }
|
| 678 |
+
let t = i / (NH * HD);
|
| 679 |
+
let c = i % (NH * HD);
|
| 680 |
+
let h = c / HD;
|
| 681 |
+
let d = c % HD;
|
| 682 |
+
let gv = qkv[t * ${ROW}u + h * HD * 2u + HD + d];
|
| 683 |
+
y[i] = y[i] / (1.0 + exp(-gv));
|
| 684 |
+
}`;
|
| 685 |
+
}
|
| 686 |
+
|
| 687 |
+
// ---------------------------------------------------------------------------
|
| 688 |
+
// Generic kernels, copied from the lfm2 project (f16 path)
|
| 689 |
+
// ---------------------------------------------------------------------------
|
| 690 |
+
|
| 691 |
+
// y[row] = dot(W[row, :], x[:]) -- decode-path matvec, one subgroup per row.
|
| 692 |
+
export function gemv({ N, K }) {
|
| 693 |
+
const K4 = K / 4;
|
| 694 |
+
return /* wgsl */ `
|
| 695 |
+
${EF16()}
|
| 696 |
+
enable subgroups;
|
| 697 |
+
@group(0) @binding(0) var<storage, read> w: array<vec4<${HT()}>>;
|
| 698 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 699 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 700 |
+
|
| 701 |
+
const N = ${N}u;
|
| 702 |
+
const K4 = ${K4}u;
|
| 703 |
+
|
| 704 |
+
@compute @workgroup_size(256)
|
| 705 |
+
fn main(
|
| 706 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 707 |
+
@builtin(local_invocation_index) li: u32,
|
| 708 |
+
@builtin(subgroup_invocation_id) sgi: u32,
|
| 709 |
+
@builtin(subgroup_size) sgs: u32,
|
| 710 |
+
) {
|
| 711 |
+
let rows_per_wg = 256u / sgs;
|
| 712 |
+
let row = wg.x * rows_per_wg + li / sgs;
|
| 713 |
+
let base = min(row, N - 1u) * K4;
|
| 714 |
+
var acc = 0.0;
|
| 715 |
+
for (var i = sgi; i < K4; i += sgs) {
|
| 716 |
+
acc += dot(vec4<f32>(w[base + i]), x[i]);
|
| 717 |
+
}
|
| 718 |
+
acc = subgroupAdd(acc);
|
| 719 |
+
if (sgi == 0u && row < N) { y[row] = acc; }
|
| 720 |
+
}`;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
// gemv with fused residual add: y[row] += dot(W[row,:], x).
|
| 724 |
+
export function gemvAdd({ N, K }) {
|
| 725 |
+
const K4 = K / 4;
|
| 726 |
+
return /* wgsl */ `
|
| 727 |
+
${EF16()}
|
| 728 |
+
enable subgroups;
|
| 729 |
+
@group(0) @binding(0) var<storage, read> w: array<vec4<${HT()}>>;
|
| 730 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 731 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 732 |
+
|
| 733 |
+
const N = ${N}u;
|
| 734 |
+
const K4 = ${K4}u;
|
| 735 |
+
|
| 736 |
+
@compute @workgroup_size(256)
|
| 737 |
+
fn main(
|
| 738 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 739 |
+
@builtin(local_invocation_index) li: u32,
|
| 740 |
+
@builtin(subgroup_invocation_id) sgi: u32,
|
| 741 |
+
@builtin(subgroup_size) sgs: u32,
|
| 742 |
+
) {
|
| 743 |
+
let rows_per_wg = 256u / sgs;
|
| 744 |
+
let row = wg.x * rows_per_wg + li / sgs;
|
| 745 |
+
let base = min(row, N - 1u) * K4;
|
| 746 |
+
var acc = 0.0;
|
| 747 |
+
for (var i = sgi; i < K4; i += sgs) {
|
| 748 |
+
acc += dot(vec4<f32>(w[base + i]), x[i]);
|
| 749 |
+
}
|
| 750 |
+
acc = subgroupAdd(acc);
|
| 751 |
+
if (sgi == 0u && row < N) { y[row] = y[row] + acc; }
|
| 752 |
+
}`;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
// fused SwiGLU matvec: y = silu(W1@x) * (W3@x)
|
| 756 |
+
export function gemvSwiglu({ N, K }) {
|
| 757 |
+
const K4 = K / 4;
|
| 758 |
+
return /* wgsl */ `
|
| 759 |
+
${EF16()}
|
| 760 |
+
enable subgroups;
|
| 761 |
+
@group(0) @binding(0) var<storage, read> w1: array<vec4<${HT()}>>;
|
| 762 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 763 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 764 |
+
@group(0) @binding(3) var<storage, read> w3: array<vec4<${HT()}>>;
|
| 765 |
+
|
| 766 |
+
const N = ${N}u;
|
| 767 |
+
const K4 = ${K4}u;
|
| 768 |
+
|
| 769 |
+
@compute @workgroup_size(256)
|
| 770 |
+
fn main(
|
| 771 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 772 |
+
@builtin(local_invocation_index) li: u32,
|
| 773 |
+
@builtin(subgroup_invocation_id) sgi: u32,
|
| 774 |
+
@builtin(subgroup_size) sgs: u32,
|
| 775 |
+
) {
|
| 776 |
+
let row = wg.x * (256u / sgs) + li / sgs;
|
| 777 |
+
let base = min(row, N - 1u) * K4;
|
| 778 |
+
var a = 0.0;
|
| 779 |
+
var b = 0.0;
|
| 780 |
+
for (var i = sgi; i < K4; i += sgs) {
|
| 781 |
+
let xv = x[i];
|
| 782 |
+
a += dot(vec4<f32>(w1[base + i]), xv);
|
| 783 |
+
b += dot(vec4<f32>(w3[base + i]), xv);
|
| 784 |
+
}
|
| 785 |
+
a = subgroupAdd(a);
|
| 786 |
+
b = subgroupAdd(b);
|
| 787 |
+
if (sgi == 0u && row < N) { y[row] = a / (1.0 + exp(-a)) * b; }
|
| 788 |
+
}`;
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
// naive tiled-less GEMM for prefill: y[m, n] = dot(W[n,:], x[m,:])
|
| 792 |
+
export function gemm({ N, K }) {
|
| 793 |
+
const K4 = K / 4;
|
| 794 |
+
return /* wgsl */ `
|
| 795 |
+
${EF16()}
|
| 796 |
+
${STEP_UNIFORM}
|
| 797 |
+
@group(0) @binding(0) var<storage, read> w: array<vec4<${HT()}>>;
|
| 798 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 799 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 800 |
+
|
| 801 |
+
const N = ${N}u;
|
| 802 |
+
const K4 = ${K4}u;
|
| 803 |
+
|
| 804 |
+
@compute @workgroup_size(8, 8)
|
| 805 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 806 |
+
let n = gid.x;
|
| 807 |
+
let m = gid.y;
|
| 808 |
+
if (n >= N || m >= step.t) { return; }
|
| 809 |
+
var acc = 0.0;
|
| 810 |
+
let wbase = n * K4;
|
| 811 |
+
let xbase = m * K4;
|
| 812 |
+
for (var i = 0u; i < K4; i++) {
|
| 813 |
+
acc += dot(vec4<f32>(w[wbase + i]), x[xbase + i]);
|
| 814 |
+
}
|
| 815 |
+
y[m * N + n] = acc;
|
| 816 |
+
}`;
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
// gemm with fused residual add: y[m, n] += dot(W[n,:], x[m,:])
|
| 820 |
+
export function gemmAdd({ N, K }) {
|
| 821 |
+
const K4 = K / 4;
|
| 822 |
+
return /* wgsl */ `
|
| 823 |
+
${EF16()}
|
| 824 |
+
${STEP_UNIFORM}
|
| 825 |
+
@group(0) @binding(0) var<storage, read> w: array<vec4<${HT()}>>;
|
| 826 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 827 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 828 |
+
|
| 829 |
+
const N = ${N}u;
|
| 830 |
+
const K4 = ${K4}u;
|
| 831 |
+
|
| 832 |
+
@compute @workgroup_size(8, 8)
|
| 833 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 834 |
+
let n = gid.x;
|
| 835 |
+
let m = gid.y;
|
| 836 |
+
if (n >= N || m >= step.t) { return; }
|
| 837 |
+
var acc = 0.0;
|
| 838 |
+
let wbase = n * K4;
|
| 839 |
+
let xbase = m * K4;
|
| 840 |
+
for (var i = 0u; i < K4; i++) {
|
| 841 |
+
acc += dot(vec4<f32>(w[wbase + i]), x[xbase + i]);
|
| 842 |
+
}
|
| 843 |
+
y[m * N + n] = y[m * N + n] + acc;
|
| 844 |
+
}`;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
// RMSNorm over rows of length C: y = x / rms(x) * w. One workgroup per row.
|
| 848 |
+
// Weights are pre-normalized by the converter ((1+w) folded in).
|
| 849 |
+
export function rmsnorm({ C, eps }) {
|
| 850 |
+
return /* wgsl */ `
|
| 851 |
+
${EF16()}
|
| 852 |
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
| 853 |
+
@group(0) @binding(1) var<storage, read> w: array<${HT()}>;
|
| 854 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 855 |
+
|
| 856 |
+
const C = ${C}u;
|
| 857 |
+
const EPS = ${eps};
|
| 858 |
+
var<workgroup> partial: array<f32, 256>;
|
| 859 |
+
|
| 860 |
+
@compute @workgroup_size(256)
|
| 861 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 862 |
+
let base = wg.x * C;
|
| 863 |
+
var s = 0.0;
|
| 864 |
+
for (var i = li; i < C; i += 256u) {
|
| 865 |
+
let v = x[base + i];
|
| 866 |
+
s += v * v;
|
| 867 |
+
}
|
| 868 |
+
partial[li] = s;
|
| 869 |
+
workgroupBarrier();
|
| 870 |
+
var stride = 128u;
|
| 871 |
+
while (stride > 0u) {
|
| 872 |
+
if (li < stride) { partial[li] += partial[li + stride]; }
|
| 873 |
+
workgroupBarrier();
|
| 874 |
+
stride /= 2u;
|
| 875 |
+
}
|
| 876 |
+
let inv = inverseSqrt(partial[0] / f32(C) + EPS);
|
| 877 |
+
for (var i = li; i < C; i += 256u) {
|
| 878 |
+
y[base + i] = x[base + i] * inv * f32(w[i]);
|
| 879 |
+
}
|
| 880 |
+
}`;
|
| 881 |
+
}
|
| 882 |
+
|
| 883 |
+
// y = silu(a) * b, elementwise over T*C
|
| 884 |
+
export function swiglu({ C }) {
|
| 885 |
+
return /* wgsl */ `
|
| 886 |
+
${STEP_UNIFORM}
|
| 887 |
+
@group(0) @binding(0) var<storage, read> a: array<f32>;
|
| 888 |
+
@group(0) @binding(1) var<storage, read> b: array<f32>;
|
| 889 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 890 |
+
|
| 891 |
+
@compute @workgroup_size(256)
|
| 892 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 893 |
+
let i = gid.x;
|
| 894 |
+
if (i >= step.t * ${C}u) { return; }
|
| 895 |
+
let v = a[i];
|
| 896 |
+
y[i] = v / (1.0 + exp(-v)) * b[i];
|
| 897 |
+
}`;
|
| 898 |
+
}
|
| 899 |
+
|
| 900 |
+
// y += x, elementwise over T*C
|
| 901 |
+
export function addInPlace({ C }) {
|
| 902 |
+
return /* wgsl */ `
|
| 903 |
+
${STEP_UNIFORM}
|
| 904 |
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
| 905 |
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
| 906 |
+
|
| 907 |
+
@compute @workgroup_size(256)
|
| 908 |
+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
| 909 |
+
let i = gid.x;
|
| 910 |
+
if (i >= step.t * ${C}u) { return; }
|
| 911 |
+
y[i] = y[i] + x[i];
|
| 912 |
+
}`;
|
| 913 |
+
}
|
| 914 |
+
|
| 915 |
+
// out[t, :] = f32(emb[hist[pos0 + t], :]); C % 4 == 0.
|
| 916 |
+
export function embedGather({ C }) {
|
| 917 |
+
const C4 = C / 4;
|
| 918 |
+
return /* wgsl */ `
|
| 919 |
+
${EF16()}
|
| 920 |
+
${STEP_UNIFORM}
|
| 921 |
+
@group(0) @binding(0) var<storage, read> emb: array<vec4<${HT()}>>;
|
| 922 |
+
@group(0) @binding(1) var<storage, read> hist: array<u32>;
|
| 923 |
+
@group(0) @binding(2) var<storage, read_write> y: array<vec4<f32>>;
|
| 924 |
+
|
| 925 |
+
const C4 = ${C4}u;
|
| 926 |
+
|
| 927 |
+
@compute @workgroup_size(256)
|
| 928 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 929 |
+
let t = wg.x;
|
| 930 |
+
if (t >= step.t) { return; }
|
| 931 |
+
let base = hist[step.pos0 + t] * C4;
|
| 932 |
+
for (var i = li; i < C4; i += 256u) {
|
| 933 |
+
y[t * C4 + i] = vec4<f32>(emb[base + i]);
|
| 934 |
+
}
|
| 935 |
+
}`;
|
| 936 |
+
}
|
| 937 |
+
|
| 938 |
+
// ---------------------------------------------------------------------------
|
| 939 |
+
// int4 wire format (from the lfm2 project): symmetric per-group along K,
|
| 940 |
+
// byte-sliced nibbles (u32 word = weights q0..q7: low nibbles of bytes 0..3 =
|
| 941 |
+
// q0..q3, high nibbles = q4..q7), biased by +8. f16 scales [N, K/group].
|
| 942 |
+
// One vec4<u32> = 32 weights.
|
| 943 |
+
// ---------------------------------------------------------------------------
|
| 944 |
+
|
| 945 |
+
const Q8_UNPACK = /* wgsl */ `
|
| 946 |
+
fn unpack_q8(w: u32) -> vec4<f32> {
|
| 947 |
+
return vec4<f32>(
|
| 948 |
+
f32(i32(w << 24u) >> 24u),
|
| 949 |
+
f32(i32(w << 16u) >> 24u),
|
| 950 |
+
f32(i32(w << 8u) >> 24u),
|
| 951 |
+
f32(i32(w) >> 24u));
|
| 952 |
+
}`;
|
| 953 |
+
|
| 954 |
+
const Q4_UNPACK = /* wgsl */ `
|
| 955 |
+
fn q4_lo(w: u32) -> vec4<f32> {
|
| 956 |
+
return vec4<f32>(f32(w & 0xFu), f32((w >> 8u) & 0xFu), f32((w >> 16u) & 0xFu), f32((w >> 24u) & 0xFu)) - vec4<f32>(8.0);
|
| 957 |
+
}
|
| 958 |
+
fn q4_hi(w: u32) -> vec4<f32> {
|
| 959 |
+
return vec4<f32>(f32((w >> 4u) & 0xFu), f32((w >> 12u) & 0xFu), f32((w >> 20u) & 0xFu), f32((w >> 28u) & 0xFu)) - vec4<f32>(8.0);
|
| 960 |
+
}
|
| 961 |
+
fn q4_dot(w: u32, x0: vec4<f32>, x1: vec4<f32>) -> f32 {
|
| 962 |
+
return dot(q4_lo(w), x0) + dot(q4_hi(w), x1);
|
| 963 |
+
}`;
|
| 964 |
+
|
| 965 |
+
// f16-bitfield unpack: (nibble | 0x6400) bitcast to f16 is exactly 1024+q —
|
| 966 |
+
// bit-exact with q4_lo/q4_hi at ~half the unpack ALU (lfm2 measured win on
|
| 967 |
+
// hot layer matvecs; the cache-cold logits matvec prefers the plain unpack).
|
| 968 |
+
const Q4_UNPACK_FT = /* wgsl */ `
|
| 969 |
+
fn q4_dot(w: u32, x0: vec4<f32>, x1: vec4<f32>) -> f32 {
|
| 970 |
+
let p0 = bitcast<vec2<f16>>((w & 0x000F000Fu) | 0x64006400u);
|
| 971 |
+
let p1 = bitcast<vec2<f16>>(((w >> 8u) & 0x000F000Fu) | 0x64006400u);
|
| 972 |
+
let p2 = bitcast<vec2<f16>>(((w >> 4u) & 0x000F000Fu) | 0x64006400u);
|
| 973 |
+
let p3 = bitcast<vec2<f16>>(((w >> 12u) & 0x000F000Fu) | 0x64006400u);
|
| 974 |
+
let lo = vec4<f32>(f32(p0.x), f32(p1.x), f32(p0.y), f32(p1.y)) - vec4<f32>(1032.0);
|
| 975 |
+
let hi = vec4<f32>(f32(p2.x), f32(p3.x), f32(p2.y), f32(p3.y)) - vec4<f32>(1032.0);
|
| 976 |
+
return dot(lo, x0) + dot(hi, x1);
|
| 977 |
+
}`;
|
| 978 |
+
|
| 979 |
+
// q4 matvec, vec4<u32> loads, 2 rows per subgroup; epilogue "plain"/"add".
|
| 980 |
+
// group 32: one scale per chunk. Subgroup-free fallback: 32-lane virtual warps.
|
| 981 |
+
export function gemvQ4({ N, K, epilogue = "plain", group = 32, ft = true }) {
|
| 982 |
+
const KC = K / 32;
|
| 983 |
+
const SIDX = group === 32 ? "c" : "c / 2u";
|
| 984 |
+
const SGRP = K / group;
|
| 985 |
+
const store = (rowExpr, accExpr) =>
|
| 986 |
+
epilogue === "add" ? `y[${rowExpr}] = y[${rowExpr}] + ${accExpr};` : `y[${rowExpr}] = ${accExpr};`;
|
| 987 |
+
const head = SG
|
| 988 |
+
? ` let lane = sgi;
|
| 989 |
+
let warp = li / sgs;
|
| 990 |
+
let wsz = sgs;
|
| 991 |
+
let row0 = (wg.x * (256u / sgs) + warp) * 2u;`
|
| 992 |
+
: ` let lane = li % 32u;
|
| 993 |
+
let warp = li / 32u;
|
| 994 |
+
let wsz = 32u;
|
| 995 |
+
let row0 = (wg.x * 8u + warp) * 2u;`;
|
| 996 |
+
const reduce = SG
|
| 997 |
+
? ` a0 = subgroupAdd(a0);
|
| 998 |
+
a1 = subgroupAdd(a1);`
|
| 999 |
+
: ` red0[li] = a0;
|
| 1000 |
+
red1[li] = a1;
|
| 1001 |
+
workgroupBarrier();
|
| 1002 |
+
var stride = 16u;
|
| 1003 |
+
while (stride > 0u) {
|
| 1004 |
+
if (lane < stride) {
|
| 1005 |
+
red0[li] += red0[li + stride];
|
| 1006 |
+
red1[li] += red1[li + stride];
|
| 1007 |
+
}
|
| 1008 |
+
workgroupBarrier();
|
| 1009 |
+
stride /= 2u;
|
| 1010 |
+
}
|
| 1011 |
+
a0 = red0[li];
|
| 1012 |
+
a1 = red1[li];`;
|
| 1013 |
+
return /* wgsl */ `
|
| 1014 |
+
${EF16()}
|
| 1015 |
+
${SG ? "enable subgroups;" : ""}
|
| 1016 |
+
@group(0) @binding(0) var<storage, read> wq: array<vec4<u32>>;
|
| 1017 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 1018 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 1019 |
+
@group(0) @binding(4) var<storage, read> scales: array<${HT()}>;
|
| 1020 |
+
|
| 1021 |
+
const N = ${N}u;
|
| 1022 |
+
const KC = ${KC}u;
|
| 1023 |
+
${ft && F16 ? Q4_UNPACK_FT : Q4_UNPACK}
|
| 1024 |
+
${SG ? "" : "var<workgroup> red0: array<f32, 256>;\nvar<workgroup> red1: array<f32, 256>;"}
|
| 1025 |
+
|
| 1026 |
+
fn q4_chunk(w: vec4<u32>, c: u32) -> f32 {
|
| 1027 |
+
return q4_dot(w.x, x[c * 8u], x[c * 8u + 1u]) +
|
| 1028 |
+
q4_dot(w.y, x[c * 8u + 2u], x[c * 8u + 3u]) +
|
| 1029 |
+
q4_dot(w.z, x[c * 8u + 4u], x[c * 8u + 5u]) +
|
| 1030 |
+
q4_dot(w.w, x[c * 8u + 6u], x[c * 8u + 7u]);
|
| 1031 |
+
}
|
| 1032 |
+
|
| 1033 |
+
@compute @workgroup_size(256)
|
| 1034 |
+
fn main(
|
| 1035 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 1036 |
+
@builtin(local_invocation_index) li: u32,
|
| 1037 |
+
${SG ? " @builtin(subgroup_invocation_id) sgi: u32,\n @builtin(subgroup_size) sgs: u32," : ""}
|
| 1038 |
+
) {
|
| 1039 |
+
${head}
|
| 1040 |
+
let r = min(row0, N - 2u);
|
| 1041 |
+
let wb0 = r * KC;
|
| 1042 |
+
let wb1 = wb0 + KC;
|
| 1043 |
+
let sb0 = r * ${SGRP}u;
|
| 1044 |
+
let sb1 = sb0 + ${SGRP}u;
|
| 1045 |
+
var a0 = 0.0;
|
| 1046 |
+
var a1 = 0.0;
|
| 1047 |
+
for (var c = lane; c < KC; c += wsz) {
|
| 1048 |
+
a0 += f32(scales[sb0 + ${SIDX}]) * q4_chunk(wq[wb0 + c], c);
|
| 1049 |
+
a1 += f32(scales[sb1 + ${SIDX}]) * q4_chunk(wq[wb1 + c], c);
|
| 1050 |
+
}
|
| 1051 |
+
${reduce}
|
| 1052 |
+
if (lane == 0u && row0 + 2u <= N) {
|
| 1053 |
+
${store("row0", "a0")}
|
| 1054 |
+
${store("row0 + 1u", "a1")}
|
| 1055 |
+
}
|
| 1056 |
+
}`;
|
| 1057 |
+
}
|
| 1058 |
+
|
| 1059 |
+
// q4 fused SwiGLU matvec
|
| 1060 |
+
export function gemvSwigluQ4({ N, K, group = 32 }) {
|
| 1061 |
+
const KC = K / 32;
|
| 1062 |
+
const SIDX = group === 32 ? "c" : "c / 2u";
|
| 1063 |
+
const SGRP = K / group;
|
| 1064 |
+
const head = SG
|
| 1065 |
+
? ` let lane = sgi;
|
| 1066 |
+
let wsz = sgs;
|
| 1067 |
+
let row = wg.x * (256u / sgs) + li / sgs;`
|
| 1068 |
+
: ` let lane = li % 32u;
|
| 1069 |
+
let wsz = 32u;
|
| 1070 |
+
let row = wg.x * 8u + li / 32u;`;
|
| 1071 |
+
const reduce = SG
|
| 1072 |
+
? ` a = subgroupAdd(a);
|
| 1073 |
+
b = subgroupAdd(b);`
|
| 1074 |
+
: ` red0[li] = a;
|
| 1075 |
+
red1[li] = b;
|
| 1076 |
+
workgroupBarrier();
|
| 1077 |
+
var stride = 16u;
|
| 1078 |
+
while (stride > 0u) {
|
| 1079 |
+
if (lane < stride) {
|
| 1080 |
+
red0[li] += red0[li + stride];
|
| 1081 |
+
red1[li] += red1[li + stride];
|
| 1082 |
+
}
|
| 1083 |
+
workgroupBarrier();
|
| 1084 |
+
stride /= 2u;
|
| 1085 |
+
}
|
| 1086 |
+
a = red0[li];
|
| 1087 |
+
b = red1[li];`;
|
| 1088 |
+
return /* wgsl */ `
|
| 1089 |
+
${EF16()}
|
| 1090 |
+
${SG ? "enable subgroups;" : ""}
|
| 1091 |
+
@group(0) @binding(0) var<storage, read> w1q: array<vec4<u32>>;
|
| 1092 |
+
@group(0) @binding(1) var<storage, read> x: array<vec4<f32>>;
|
| 1093 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 1094 |
+
@group(0) @binding(3) var<storage, read> w3q: array<vec4<u32>>;
|
| 1095 |
+
@group(0) @binding(4) var<storage, read> s1: array<${HT()}>;
|
| 1096 |
+
@group(0) @binding(5) var<storage, read> s3: array<${HT()}>;
|
| 1097 |
+
|
| 1098 |
+
const N = ${N}u;
|
| 1099 |
+
const KC = ${KC}u;
|
| 1100 |
+
${F16 ? Q4_UNPACK_FT : Q4_UNPACK}
|
| 1101 |
+
${SG ? "" : "var<workgroup> red0: array<f32, 256>;\nvar<workgroup> red1: array<f32, 256>;"}
|
| 1102 |
+
|
| 1103 |
+
fn q4_chunk(w: vec4<u32>, c: u32) -> f32 {
|
| 1104 |
+
return q4_dot(w.x, x[c * 8u], x[c * 8u + 1u]) +
|
| 1105 |
+
q4_dot(w.y, x[c * 8u + 2u], x[c * 8u + 3u]) +
|
| 1106 |
+
q4_dot(w.z, x[c * 8u + 4u], x[c * 8u + 5u]) +
|
| 1107 |
+
q4_dot(w.w, x[c * 8u + 6u], x[c * 8u + 7u]);
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
@compute @workgroup_size(256)
|
| 1111 |
+
fn main(
|
| 1112 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 1113 |
+
@builtin(local_invocation_index) li: u32,
|
| 1114 |
+
${SG ? " @builtin(subgroup_invocation_id) sgi: u32,\n @builtin(subgroup_size) sgs: u32," : ""}
|
| 1115 |
+
) {
|
| 1116 |
+
${head}
|
| 1117 |
+
let r = min(row, N - 1u);
|
| 1118 |
+
let wbase = r * KC;
|
| 1119 |
+
let sbase = r * ${SGRP}u;
|
| 1120 |
+
var a = 0.0;
|
| 1121 |
+
var b = 0.0;
|
| 1122 |
+
for (var c = lane; c < KC; c += wsz) {
|
| 1123 |
+
let g = sbase + ${SIDX};
|
| 1124 |
+
a += f32(s1[g]) * q4_chunk(w1q[wbase + c], c);
|
| 1125 |
+
b += f32(s3[g]) * q4_chunk(w3q[wbase + c], c);
|
| 1126 |
+
}
|
| 1127 |
+
${reduce}
|
| 1128 |
+
if (lane == 0u && row < N) { y[row] = a / (1.0 + exp(-a)) * b; }
|
| 1129 |
+
}`;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
// tiled prefill GEMM over quantized weights (q4/q8), 64x64 tiles, BK=32.
|
| 1133 |
+
// Unlike the lfm2 original: guards for N not divisible by 64 (our fused
|
| 1134 |
+
// delta in_proj is 8224 rows) and an "add" epilogue for fused residuals.
|
| 1135 |
+
export function gemmTiledQ({ N, K, dtype = "q4", group = 32, xstride = K, epilogue = "plain" }) {
|
| 1136 |
+
const q4 = dtype === "q4";
|
| 1137 |
+
const wordsPerCol = q4 ? 4 : 8;
|
| 1138 |
+
const weightsPerWord = q4 ? 8 : 4;
|
| 1139 |
+
const KWROW = K / weightsPerWord;
|
| 1140 |
+
const SGRP = K / group;
|
| 1141 |
+
const store = (dst, acc) => (epilogue === "add" ? `y[${dst}] = y[${dst}] + ${acc};` : `y[${dst}] = ${acc};`);
|
| 1142 |
+
return /* wgsl */ `
|
| 1143 |
+
${EF16()}
|
| 1144 |
+
${STEP_UNIFORM}
|
| 1145 |
+
@group(0) @binding(0) var<storage, read> wq: array<u32>;
|
| 1146 |
+
@group(0) @binding(1) var<storage, read> x: array<f32>;
|
| 1147 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 1148 |
+
@group(0) @binding(4) var<storage, read> scales: array<${HT()}>;
|
| 1149 |
+
|
| 1150 |
+
const N = ${N}u;
|
| 1151 |
+
const K = ${K}u;
|
| 1152 |
+
${Q8_UNPACK}
|
| 1153 |
+
${Q4_UNPACK}
|
| 1154 |
+
|
| 1155 |
+
var<workgroup> As: array<array<f32, 32>, 64>; // [m][k]
|
| 1156 |
+
var<workgroup> Bs: array<array<f32, 65>, 32>; // [k][n] (+1 pad col)
|
| 1157 |
+
|
| 1158 |
+
@compute @workgroup_size(16, 16)
|
| 1159 |
+
fn main(
|
| 1160 |
+
@builtin(workgroup_id) wg: vec3<u32>,
|
| 1161 |
+
@builtin(local_invocation_id) lid: vec3<u32>,
|
| 1162 |
+
@builtin(local_invocation_index) li: u32,
|
| 1163 |
+
) {
|
| 1164 |
+
let n0 = wg.x * 64u;
|
| 1165 |
+
let m0 = wg.y * 64u;
|
| 1166 |
+
var acc: array<array<f32, 4>, 4>;
|
| 1167 |
+
|
| 1168 |
+
for (var k0 = 0u; k0 < K; k0 += 32u) {
|
| 1169 |
+
for (var e = li; e < 512u; e += 256u) {
|
| 1170 |
+
let m = e / 8u;
|
| 1171 |
+
let k = (e % 8u) * 4u;
|
| 1172 |
+
let mm = m0 + m;
|
| 1173 |
+
if (mm < step.t) {
|
| 1174 |
+
let base = mm * ${xstride}u + k0 + k;
|
| 1175 |
+
As[m][k] = x[base];
|
| 1176 |
+
As[m][k + 1u] = x[base + 1u];
|
| 1177 |
+
As[m][k + 2u] = x[base + 2u];
|
| 1178 |
+
As[m][k + 3u] = x[base + 3u];
|
| 1179 |
+
} else {
|
| 1180 |
+
As[m][k] = 0.0; As[m][k + 1u] = 0.0; As[m][k + 2u] = 0.0; As[m][k + 3u] = 0.0;
|
| 1181 |
+
}
|
| 1182 |
+
}
|
| 1183 |
+
for (var e = li; e < ${64 * wordsPerCol}u; e += 256u) {
|
| 1184 |
+
let n = e / ${wordsPerCol}u;
|
| 1185 |
+
let kw = e % ${wordsPerCol}u;
|
| 1186 |
+
let col = min(n0 + n, N - 1u);
|
| 1187 |
+
let kabs = k0 + kw * ${weightsPerWord}u;
|
| 1188 |
+
let s = f32(scales[col * ${SGRP}u + kabs / ${group}u]);
|
| 1189 |
+
let w = wq[col * ${KWROW}u + kabs / ${weightsPerWord}u];
|
| 1190 |
+
let kk = kw * ${weightsPerWord}u;
|
| 1191 |
+
${
|
| 1192 |
+
q4
|
| 1193 |
+
? ` let lo = q4_lo(w) * s;
|
| 1194 |
+
let hi = q4_hi(w) * s;
|
| 1195 |
+
Bs[kk][n] = lo.x; Bs[kk + 1u][n] = lo.y; Bs[kk + 2u][n] = lo.z; Bs[kk + 3u][n] = lo.w;
|
| 1196 |
+
Bs[kk + 4u][n] = hi.x; Bs[kk + 5u][n] = hi.y; Bs[kk + 6u][n] = hi.z; Bs[kk + 7u][n] = hi.w;`
|
| 1197 |
+
: ` let v = unpack_q8(w) * s;
|
| 1198 |
+
Bs[kk][n] = v.x; Bs[kk + 1u][n] = v.y; Bs[kk + 2u][n] = v.z; Bs[kk + 3u][n] = v.w;`
|
| 1199 |
+
}
|
| 1200 |
+
}
|
| 1201 |
+
workgroupBarrier();
|
| 1202 |
+
|
| 1203 |
+
for (var k = 0u; k < 32u; k++) {
|
| 1204 |
+
let a0 = As[lid.y * 4u][k];
|
| 1205 |
+
let a1 = As[lid.y * 4u + 1u][k];
|
| 1206 |
+
let a2 = As[lid.y * 4u + 2u][k];
|
| 1207 |
+
let a3 = As[lid.y * 4u + 3u][k];
|
| 1208 |
+
let b0 = Bs[k][lid.x * 4u];
|
| 1209 |
+
let b1 = Bs[k][lid.x * 4u + 1u];
|
| 1210 |
+
let b2 = Bs[k][lid.x * 4u + 2u];
|
| 1211 |
+
let b3 = Bs[k][lid.x * 4u + 3u];
|
| 1212 |
+
acc[0][0] += a0 * b0; acc[0][1] += a0 * b1; acc[0][2] += a0 * b2; acc[0][3] += a0 * b3;
|
| 1213 |
+
acc[1][0] += a1 * b0; acc[1][1] += a1 * b1; acc[1][2] += a1 * b2; acc[1][3] += a1 * b3;
|
| 1214 |
+
acc[2][0] += a2 * b0; acc[2][1] += a2 * b1; acc[2][2] += a2 * b2; acc[2][3] += a2 * b3;
|
| 1215 |
+
acc[3][0] += a3 * b0; acc[3][1] += a3 * b1; acc[3][2] += a3 * b2; acc[3][3] += a3 * b3;
|
| 1216 |
+
}
|
| 1217 |
+
workgroupBarrier();
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
for (var i = 0u; i < 4u; i++) {
|
| 1221 |
+
let m = m0 + lid.y * 4u + i;
|
| 1222 |
+
if (m >= step.t) { continue; }
|
| 1223 |
+
for (var j = 0u; j < 4u; j++) {
|
| 1224 |
+
let n = n0 + lid.x * 4u + j;
|
| 1225 |
+
if (n < N) { ${store("m * N + n", "acc[i][j]")} }
|
| 1226 |
+
}
|
| 1227 |
+
}
|
| 1228 |
+
}`;
|
| 1229 |
+
}
|
| 1230 |
+
|
| 1231 |
+
// q4 embedding gather: out[t, :] = dequant(emb[hist[pos0+t], :]).
|
| 1232 |
+
// One workgroup per token; each thread unpacks one u32 word (8 weights).
|
| 1233 |
+
export function embedGatherQ4({ C, group = 32 }) {
|
| 1234 |
+
const CW = C / 8; // u32 words per row
|
| 1235 |
+
const SGRP = C / group;
|
| 1236 |
+
return /* wgsl */ `
|
| 1237 |
+
${EF16()}
|
| 1238 |
+
${STEP_UNIFORM}
|
| 1239 |
+
@group(0) @binding(0) var<storage, read> emb: array<u32>;
|
| 1240 |
+
@group(0) @binding(1) var<storage, read> hist: array<u32>;
|
| 1241 |
+
@group(0) @binding(2) var<storage, read_write> y: array<f32>;
|
| 1242 |
+
@group(0) @binding(4) var<storage, read> scales: array<${HT()}>;
|
| 1243 |
+
|
| 1244 |
+
const CW = ${CW}u;
|
| 1245 |
+
${Q4_UNPACK}
|
| 1246 |
+
|
| 1247 |
+
@compute @workgroup_size(256)
|
| 1248 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 1249 |
+
let t = wg.x;
|
| 1250 |
+
if (t >= step.t) { return; }
|
| 1251 |
+
let row = hist[step.pos0 + t];
|
| 1252 |
+
for (var wi = li; wi < CW; wi += 256u) {
|
| 1253 |
+
let s = f32(scales[row * ${SGRP}u + (wi * 8u) / ${group}u]);
|
| 1254 |
+
let w = emb[row * CW + wi];
|
| 1255 |
+
let lo = q4_lo(w) * s;
|
| 1256 |
+
let hi = q4_hi(w) * s;
|
| 1257 |
+
let base = t * ${C}u + wi * 8u;
|
| 1258 |
+
y[base] = lo.x; y[base + 1u] = lo.y; y[base + 2u] = lo.z; y[base + 3u] = lo.w;
|
| 1259 |
+
y[base + 4u] = hi.x; y[base + 5u] = hi.y; y[base + 6u] = hi.z; y[base + 7u] = hi.w;
|
| 1260 |
+
}
|
| 1261 |
+
}`;
|
| 1262 |
+
}
|
| 1263 |
+
|
| 1264 |
+
// ---------------------------------------------------------------------------
|
| 1265 |
+
// Cascaded greedy argmax (lfm2 design, full-width "direct" mode only —
|
| 1266 |
+
// partial coarse is provably unsafe without tail-norm bounds). The exact
|
| 1267 |
+
// logits are computed by the normal logits matvec; argmaxSliceTop picks the
|
| 1268 |
+
// top-2 of every 64-token slice (the global winner provably survives), and
|
| 1269 |
+
// argmaxCandAdvance reduces candidates straight off the exact logits, writes
|
| 1270 |
+
// hist[pos] and bumps the step buffer — GPU-resident decode, no CPU readback.
|
| 1271 |
+
// ---------------------------------------------------------------------------
|
| 1272 |
+
|
| 1273 |
+
// top-2 of each 64-wide slice of the logits -> 2 candidates per slice.
|
| 1274 |
+
// dispatch: V/64 workgroups.
|
| 1275 |
+
export function argmaxSliceTop({ V }) {
|
| 1276 |
+
return /* wgsl */ `
|
| 1277 |
+
@group(0) @binding(0) var<storage, read> logits: array<f32>;
|
| 1278 |
+
@group(0) @binding(2) var<storage, read_write> cidxs: array<u32>;
|
| 1279 |
+
|
| 1280 |
+
var<workgroup> vals: array<f32, 64>;
|
| 1281 |
+
var<workgroup> idxs: array<u32, 64>;
|
| 1282 |
+
var<workgroup> win: u32;
|
| 1283 |
+
|
| 1284 |
+
@compute @workgroup_size(64)
|
| 1285 |
+
fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_index) li: u32) {
|
| 1286 |
+
let idx = wg.x * 64u + li;
|
| 1287 |
+
let v = logits[idx];
|
| 1288 |
+
for (var round = 0u; round < 2u; round++) {
|
| 1289 |
+
vals[li] = v;
|
| 1290 |
+
idxs[li] = idx;
|
| 1291 |
+
if (round == 1u && idx == win) { vals[li] = -3.4e38; }
|
| 1292 |
+
workgroupBarrier();
|
| 1293 |
+
var stride = 32u;
|
| 1294 |
+
while (stride > 0u) {
|
| 1295 |
+
if (li < stride) {
|
| 1296 |
+
let ov = vals[li + stride];
|
| 1297 |
+
let oi = idxs[li + stride];
|
| 1298 |
+
if (ov > vals[li] || (ov == vals[li] && oi < idxs[li])) {
|
| 1299 |
+
vals[li] = ov;
|
| 1300 |
+
idxs[li] = oi;
|
| 1301 |
+
}
|
| 1302 |
+
}
|
| 1303 |
+
workgroupBarrier();
|
| 1304 |
+
stride /= 2u;
|
| 1305 |
+
}
|
| 1306 |
+
if (li == 0u) {
|
| 1307 |
+
cidxs[wg.x * 2u + round] = idxs[0];
|
| 1308 |
+
win = idxs[0];
|
| 1309 |
+
}
|
| 1310 |
+
workgroupBarrier();
|
| 1311 |
+
}
|
| 1312 |
+
}`;
|
| 1313 |
+
}
|
| 1314 |
+
|
| 1315 |
+
// final reduce over NC candidates read straight from the exact logits
|
| 1316 |
+
// (ties -> lowest token id, matching torch.argmax) + GPU-side advance:
|
| 1317 |
+
// hist[pos0 + t] = winner, step becomes {1, pos0 + t}. dispatch: 1
|
| 1318 |
+
export function argmaxCandAdvance({ NC }) {
|
| 1319 |
+
return /* wgsl */ `
|
| 1320 |
+
struct Step { t: u32, pos0: u32, sstate: u32, acc: u32 }
|
| 1321 |
+
@group(0) @binding(0) var<storage, read> logits: array<f32>;
|
| 1322 |
+
@group(0) @binding(1) var<storage, read_write> step: Step;
|
| 1323 |
+
@group(0) @binding(2) var<storage, read_write> hist: array<u32>;
|
| 1324 |
+
@group(0) @binding(3) var<storage, read> cidxs: array<u32>;
|
| 1325 |
+
|
| 1326 |
+
var<workgroup> vals: array<f32, 256>;
|
| 1327 |
+
var<workgroup> idxs: array<u32, 256>;
|
| 1328 |
+
|
| 1329 |
+
@compute @workgroup_size(256)
|
| 1330 |
+
fn main(@builtin(local_invocation_index) li: u32) {
|
| 1331 |
+
var bv = -3.4e38;
|
| 1332 |
+
var bi = 0xffffffffu;
|
| 1333 |
+
for (var c = li; c < ${NC}u; c += 256u) {
|
| 1334 |
+
let ti = cidxs[c];
|
| 1335 |
+
let v = logits[ti];
|
| 1336 |
+
if (v > bv || (v == bv && ti < bi)) { bv = v; bi = ti; }
|
| 1337 |
+
}
|
| 1338 |
+
vals[li] = bv;
|
| 1339 |
+
idxs[li] = bi;
|
| 1340 |
+
workgroupBarrier();
|
| 1341 |
+
var stride = 128u;
|
| 1342 |
+
while (stride > 0u) {
|
| 1343 |
+
if (li < stride) {
|
| 1344 |
+
let ov = vals[li + stride];
|
| 1345 |
+
let oi = idxs[li + stride];
|
| 1346 |
+
if (ov > vals[li] || (ov == vals[li] && oi < idxs[li])) {
|
| 1347 |
+
vals[li] = ov;
|
| 1348 |
+
idxs[li] = oi;
|
| 1349 |
+
}
|
| 1350 |
+
}
|
| 1351 |
+
workgroupBarrier();
|
| 1352 |
+
stride /= 2u;
|
| 1353 |
+
}
|
| 1354 |
+
if (li == 0u) {
|
| 1355 |
+
let next = step.pos0 + step.t;
|
| 1356 |
+
hist[next] = idxs[0];
|
| 1357 |
+
step.t = 1u;
|
| 1358 |
+
step.pos0 = next;
|
| 1359 |
+
}
|
| 1360 |
+
}`;
|
| 1361 |
+
}
|
web/main.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { initDevice } from "/web/gpu.js";
|
| 2 |
+
import { loadModel } from "/web/model.js";
|
| 3 |
+
import { Tokenizer } from "/web/tokenizer.js";
|
| 4 |
+
import { resolvePaths } from "/web/config.js";
|
| 5 |
+
|
| 6 |
+
const $ = (id) => document.getElementById(id);
|
| 7 |
+
const CHUNK = 8;
|
| 8 |
+
const MAX_NEW = 768;
|
| 9 |
+
|
| 10 |
+
const log = (text, cls = "") => {
|
| 11 |
+
const el = document.createElement("div");
|
| 12 |
+
el.textContent = text;
|
| 13 |
+
if (cls) el.className = cls;
|
| 14 |
+
$("log").appendChild(el);
|
| 15 |
+
while ($("log").children.length > 8) $("log").firstChild.remove();
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
let model, tok, EOS, EOT;
|
| 19 |
+
const history = [];
|
| 20 |
+
const QUANT = new URLSearchParams(location.search).get("quant") ?? "q4";
|
| 21 |
+
const BLOB_GB = { f16: 3.76, q4: 1.07 };
|
| 22 |
+
|
| 23 |
+
async function boot() {
|
| 24 |
+
try {
|
| 25 |
+
log("requesting a WebGPU adapter…");
|
| 26 |
+
const adapter = await navigator.gpu?.requestAdapter();
|
| 27 |
+
if (!adapter) throw new Error("WebGPU is not available in this browser");
|
| 28 |
+
const info = adapter.info;
|
| 29 |
+
$("gpuname").textContent = `${info.vendor} ${info.architecture} · ${QUANT}`.trim();
|
| 30 |
+
|
| 31 |
+
const device = await initDevice();
|
| 32 |
+
const paths = await resolvePaths();
|
| 33 |
+
log(`weights source: ${paths.source}`);
|
| 34 |
+
log("loading the tokenizer (248k BPE)…");
|
| 35 |
+
const tokP = Tokenizer.load(paths.tokenizer);
|
| 36 |
+
|
| 37 |
+
const GB = BLOB_GB[QUANT] ?? 1.07;
|
| 38 |
+
$("dlmb").textContent = `${GB} GB`;
|
| 39 |
+
log(`loading weights: ${GB} GB ${QUANT}…`);
|
| 40 |
+
const t0 = performance.now();
|
| 41 |
+
model = await loadModel(device, {
|
| 42 |
+
base: paths.weights,
|
| 43 |
+
variant: QUANT,
|
| 44 |
+
onProgress: (got, total) => {
|
| 45 |
+
const f = got / total;
|
| 46 |
+
$("barfill").style.transform = `scaleX(${f * 0.9})`;
|
| 47 |
+
$("dlpct").textContent = `· ${Math.round(f * 100)}%`;
|
| 48 |
+
},
|
| 49 |
+
});
|
| 50 |
+
$("dlnote").textContent = model.weightsFromCache
|
| 51 |
+
? "model loaded from the browser cache ✓"
|
| 52 |
+
: "model downloaded ✓ — cached for next time";
|
| 53 |
+
tok = await tokP;
|
| 54 |
+
EOS = tok.eos;
|
| 55 |
+
EOT = tok.eot;
|
| 56 |
+
$("barfill").style.transform = "scaleX(0.95)";
|
| 57 |
+
log(`weights in GPU buffers in ${((performance.now() - t0) / 1000).toFixed(1)} s`, "ok");
|
| 58 |
+
|
| 59 |
+
log("warmup: compiling pipelines…");
|
| 60 |
+
model.startGenerate(tok.encode("warmup"));
|
| 61 |
+
await model.decodeChunk(1);
|
| 62 |
+
model.reset();
|
| 63 |
+
$("barfill").style.transform = "scaleX(1)";
|
| 64 |
+
log("ready.", "ok");
|
| 65 |
+
|
| 66 |
+
setTimeout(() => {
|
| 67 |
+
$("boot").classList.add("done");
|
| 68 |
+
$("input").disabled = false;
|
| 69 |
+
$("send").disabled = false;
|
| 70 |
+
$("input").focus();
|
| 71 |
+
const demo = new URLSearchParams(location.search).get("demo");
|
| 72 |
+
if (demo !== null) {
|
| 73 |
+
$("input").value = demo || "What is the capital of France?";
|
| 74 |
+
$("form").requestSubmit();
|
| 75 |
+
} else {
|
| 76 |
+
window.__done = { ok: true, stage: "loaded" };
|
| 77 |
+
}
|
| 78 |
+
}, 400);
|
| 79 |
+
} catch (e) {
|
| 80 |
+
log(String(e.message ?? e), "err");
|
| 81 |
+
window.__done = { ok: false, error: String(e.message ?? e) };
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
function addMsg(role) {
|
| 86 |
+
const el = document.createElement("div");
|
| 87 |
+
el.className = `msg ${role}`;
|
| 88 |
+
el.innerHTML = `<div class="who">${role === "user" ? "you" : "qwen3.5"}</div><div class="body"></div>`;
|
| 89 |
+
$("msgs").appendChild(el);
|
| 90 |
+
return el.querySelector(".body");
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
let busy = false;
|
| 94 |
+
async function generate(userText) {
|
| 95 |
+
busy = true;
|
| 96 |
+
$("send").disabled = true;
|
| 97 |
+
$("input").disabled = true;
|
| 98 |
+
|
| 99 |
+
addMsg("user").textContent = userText;
|
| 100 |
+
history.push({ role: "user", content: userText });
|
| 101 |
+
const body = addMsg("assistant");
|
| 102 |
+
const caret = document.createElement("span");
|
| 103 |
+
caret.className = "caret";
|
| 104 |
+
body.appendChild(caret);
|
| 105 |
+
|
| 106 |
+
// each turn re-prefills the whole conversation: keeps DeltaNet/KV state
|
| 107 |
+
// exactly right with no post-EOS pollution
|
| 108 |
+
const ids = tok.applyChatTemplate(history);
|
| 109 |
+
const t0 = performance.now();
|
| 110 |
+
model.startGenerate(ids);
|
| 111 |
+
await model.device.queue.onSubmittedWorkDone();
|
| 112 |
+
const prefillMs = performance.now() - t0;
|
| 113 |
+
|
| 114 |
+
let outIds = [];
|
| 115 |
+
let text = "";
|
| 116 |
+
const t1 = performance.now();
|
| 117 |
+
const budget = Math.min(MAX_NEW, model.maxSeq - ids.length - CHUNK - 4);
|
| 118 |
+
let done = 0;
|
| 119 |
+
while (done < budget) {
|
| 120 |
+
const n = Math.min(CHUNK, budget - done);
|
| 121 |
+
const gen = await model.decodeChunk(n);
|
| 122 |
+
done += n;
|
| 123 |
+
const stop = gen.findIndex((id) => id === EOS || id === EOT);
|
| 124 |
+
outIds = stop >= 0 ? gen.slice(0, stop) : gen;
|
| 125 |
+
text = tok.decode(outIds);
|
| 126 |
+
body.textContent = text;
|
| 127 |
+
body.appendChild(caret);
|
| 128 |
+
const dt = (performance.now() - t1) / 1000;
|
| 129 |
+
$("toks").textContent = (done / dt).toFixed(1);
|
| 130 |
+
$("chat").scrollTop = $("chat").scrollHeight;
|
| 131 |
+
if (stop >= 0) break;
|
| 132 |
+
}
|
| 133 |
+
const decodeS = (performance.now() - t1) / 1000;
|
| 134 |
+
|
| 135 |
+
caret.remove();
|
| 136 |
+
history.push({ role: "assistant", content: text });
|
| 137 |
+
const stats = document.createElement("div");
|
| 138 |
+
stats.className = "stats";
|
| 139 |
+
stats.textContent =
|
| 140 |
+
`prefill ${ids.length} tok · ${prefillMs.toFixed(0)} ms — ` +
|
| 141 |
+
`decode ${done} tok · ${(done / decodeS).toFixed(1)} tok/s`;
|
| 142 |
+
body.parentElement.appendChild(stats);
|
| 143 |
+
$("chat").scrollTop = $("chat").scrollHeight;
|
| 144 |
+
|
| 145 |
+
busy = false;
|
| 146 |
+
$("send").disabled = false;
|
| 147 |
+
$("input").disabled = false;
|
| 148 |
+
$("input").focus();
|
| 149 |
+
window.__done = { ok: true, stage: "generated", text, tok_s: +(done / decodeS).toFixed(1) };
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
$("form").addEventListener("submit", (e) => {
|
| 153 |
+
e.preventDefault();
|
| 154 |
+
const text = $("input").value.trim();
|
| 155 |
+
if (!text || busy) return;
|
| 156 |
+
$("input").value = "";
|
| 157 |
+
$("input").style.height = "auto";
|
| 158 |
+
generate(text);
|
| 159 |
+
});
|
| 160 |
+
$("input").addEventListener("keydown", (e) => {
|
| 161 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 162 |
+
e.preventDefault();
|
| 163 |
+
$("form").requestSubmit();
|
| 164 |
+
}
|
| 165 |
+
});
|
| 166 |
+
$("input").addEventListener("input", (e) => {
|
| 167 |
+
e.target.style.height = "auto";
|
| 168 |
+
e.target.style.height = e.target.scrollHeight + "px";
|
| 169 |
+
});
|
| 170 |
+
|
| 171 |
+
boot();
|
web/model.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Qwen3.5-2B WebGPU runtime: weight loader + hybrid forward
|
| 2 |
+
// (18x gated DeltaNet + 6x gated attention), f16 weights / f32 activations.
|
| 3 |
+
import * as K from "./kernels.js";
|
| 4 |
+
import { createBuffer, makePipeline } from "./gpu.js";
|
| 5 |
+
|
| 6 |
+
export const CFG = {
|
| 7 |
+
L: 24, C: 2048, V: 248320, FF: 6144, EPS: 1e-6,
|
| 8 |
+
H: 16, DK: 128, DV: 128, // DeltaNet
|
| 9 |
+
NH: 8, NKV: 2, HD: 256, RD: 64, // attention
|
| 10 |
+
ROPE_THETA: 1e7,
|
| 11 |
+
DELTA_IN: 8224, // qkv 6144 | z 2048 | b 16 | a 16
|
| 12 |
+
CONV_C: 6144,
|
| 13 |
+
ATTN_IN: 5120, // per-head (q|gate)*8 | k*2 | v*2
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
const LAYER_TYPES = Array.from({ length: 24 }, (_, i) => (i % 4 === 3 ? "attn" : "delta"));
|
| 17 |
+
|
| 18 |
+
// Stream the blob straight into per-GPU-buffer segments: the f16 blob (3.76
|
| 19 |
+
// GB) does not fit in a single ArrayBuffer, so JS never holds more than one
|
| 20 |
+
// segment (max ~1 GB, the tied embedding) at a time. q4 tensors contribute
|
| 21 |
+
// two segments: packed weights and f16 scales (key "<name>::scales").
|
| 22 |
+
export async function loadModel(device, { base = "/web/weights/", variant = "q4", onProgress } = {}) {
|
| 23 |
+
const manifest = await (await fetch(`${base}manifest.${variant}.json`)).json();
|
| 24 |
+
const segments = [];
|
| 25 |
+
for (const [name, t] of Object.entries(manifest.tensors)) {
|
| 26 |
+
segments.push({ key: name, offset: t.offset, bytes: t.bytes });
|
| 27 |
+
if (t.scales_offset !== undefined) {
|
| 28 |
+
segments.push({ key: name + "::scales", offset: t.scales_offset, bytes: t.scales_bytes });
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
segments.sort((a, b) => a.offset - b.offset);
|
| 32 |
+
const url = `${base}qwen35-2b.${variant}.bin`;
|
| 33 |
+
// Cache API: q4 (~1 GB) is cached for instant reloads; the f16 blob is too
|
| 34 |
+
// big for most quotas and is fetched fresh every time.
|
| 35 |
+
let resp = null;
|
| 36 |
+
let fromCache = false;
|
| 37 |
+
let cache = null;
|
| 38 |
+
const cacheable = variant !== "f16" && typeof caches !== "undefined";
|
| 39 |
+
if (cacheable) {
|
| 40 |
+
try {
|
| 41 |
+
cache = await caches.open("qwen35-weights");
|
| 42 |
+
const hit = await cache.match(url);
|
| 43 |
+
if (hit) { resp = hit; fromCache = true; }
|
| 44 |
+
} catch {}
|
| 45 |
+
}
|
| 46 |
+
if (!resp) {
|
| 47 |
+
resp = await fetch(url);
|
| 48 |
+
if (resp.ok && cache) {
|
| 49 |
+
try { await cache.put(url, resp.clone()); } catch {}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
if (!resp.ok) throw new Error(`fetch blob: ${resp.status}`);
|
| 53 |
+
const total = Number(resp.headers.get("content-length") ?? manifest.total_bytes);
|
| 54 |
+
const reader = resp.body.getReader();
|
| 55 |
+
const tensors = {};
|
| 56 |
+
let idx = 0;
|
| 57 |
+
let cur = null; // Uint8Array being filled
|
| 58 |
+
let curFill = 0;
|
| 59 |
+
let pos = 0; // absolute blob position
|
| 60 |
+
const finish = () => {
|
| 61 |
+
const seg = segments[idx];
|
| 62 |
+
const buf = createBuffer(device, seg.bytes);
|
| 63 |
+
device.queue.writeBuffer(buf, 0, cur.buffer, 0, seg.bytes);
|
| 64 |
+
tensors[seg.key] = buf;
|
| 65 |
+
cur = null;
|
| 66 |
+
curFill = 0;
|
| 67 |
+
idx++;
|
| 68 |
+
};
|
| 69 |
+
for (;;) {
|
| 70 |
+
const { done, value } = await reader.read();
|
| 71 |
+
if (done) break;
|
| 72 |
+
let off = 0;
|
| 73 |
+
while (off < value.byteLength && idx < segments.length) {
|
| 74 |
+
const seg = segments[idx];
|
| 75 |
+
if (pos < seg.offset) { // alignment gap
|
| 76 |
+
const skip = Math.min(seg.offset - pos, value.byteLength - off);
|
| 77 |
+
pos += skip; off += skip;
|
| 78 |
+
continue;
|
| 79 |
+
}
|
| 80 |
+
if (!cur) cur = new Uint8Array(seg.bytes);
|
| 81 |
+
const take = Math.min(seg.bytes - curFill, value.byteLength - off);
|
| 82 |
+
cur.set(value.subarray(off, off + take), curFill);
|
| 83 |
+
curFill += take; pos += take; off += take;
|
| 84 |
+
if (curFill === seg.bytes) finish();
|
| 85 |
+
}
|
| 86 |
+
pos += value.byteLength - off;
|
| 87 |
+
onProgress?.(pos, total);
|
| 88 |
+
}
|
| 89 |
+
if (idx < segments.length) throw new Error(`blob truncated: ${idx}/${segments.length} segments`);
|
| 90 |
+
const model = new Qwen35(device, manifest, tensors);
|
| 91 |
+
model.weightsFromCache = fromCache;
|
| 92 |
+
return model;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
export class Qwen35 {
|
| 96 |
+
constructor(device, manifest, w) {
|
| 97 |
+
this.device = device;
|
| 98 |
+
this.manifest = manifest;
|
| 99 |
+
this.w = w;
|
| 100 |
+
this.q4 = manifest.quant === "q4";
|
| 101 |
+
this.maxSeq = 4096;
|
| 102 |
+
this.maxT = 512; // prefill segment length
|
| 103 |
+
this.pos = 0;
|
| 104 |
+
this.hist = []; // token ids (CPU mirror of bufHist)
|
| 105 |
+
this.#alloc();
|
| 106 |
+
this.#pipelines();
|
| 107 |
+
this.#bindGroups();
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
#alloc() {
|
| 111 |
+
const d = this.device;
|
| 112 |
+
const { C, V, FF, H, DK, DV, NH, NKV, HD, DELTA_IN, CONV_C, ATTN_IN, L } = CFG;
|
| 113 |
+
const T = this.maxT;
|
| 114 |
+
const hb = 2; // f16 KV cache
|
| 115 |
+
this.bufHist = createBuffer(d, this.maxSeq * 4);
|
| 116 |
+
this.bufStep = createBuffer(d, 16);
|
| 117 |
+
this.bufX = createBuffer(d, T * C * 4);
|
| 118 |
+
this.bufXn = createBuffer(d, T * C * 4);
|
| 119 |
+
this.bufAct = createBuffer(d, T * DELTA_IN * 4);
|
| 120 |
+
this.bufQkv = createBuffer(d, T * CONV_C * 4);
|
| 121 |
+
this.bufG = createBuffer(d, T * H * 4);
|
| 122 |
+
this.bufBeta = createBuffer(d, T * H * 4);
|
| 123 |
+
this.bufCore = createBuffer(d, T * H * DV * 4);
|
| 124 |
+
this.bufGn = createBuffer(d, T * H * DV * 4);
|
| 125 |
+
this.bufQout = createBuffer(d, T * NH * HD * 4);
|
| 126 |
+
this.bufAttnOut = createBuffer(d, T * NH * HD * 4);
|
| 127 |
+
this.bufMlpA = createBuffer(d, T * FF * 4);
|
| 128 |
+
this.bufMlpB = createBuffer(d, T * FF * 4);
|
| 129 |
+
this.bufMlpC = createBuffer(d, T * FF * 4);
|
| 130 |
+
this.bufLoc = createBuffer(d, Math.ceil(T / 64) * H * K.DELTA_BLK * 4);
|
| 131 |
+
this.bufLogits = createBuffer(d, V * 4);
|
| 132 |
+
this.bufCidxs = createBuffer(d, (V / 64) * 2 * 4);
|
| 133 |
+
this.convState = [];
|
| 134 |
+
this.recState = [];
|
| 135 |
+
this.kCache = [];
|
| 136 |
+
this.vCache = [];
|
| 137 |
+
for (let i = 0; i < L; i++) {
|
| 138 |
+
if (LAYER_TYPES[i] === "delta") {
|
| 139 |
+
this.convState[i] = createBuffer(d, CONV_C * 4 * 4);
|
| 140 |
+
this.recState[i] = createBuffer(d, H * DK * DV * 4);
|
| 141 |
+
} else {
|
| 142 |
+
this.kCache[i] = createBuffer(d, this.maxSeq * NKV * HD * hb);
|
| 143 |
+
this.vCache[i] = createBuffer(d, this.maxSeq * NKV * HD * hb);
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
#pipelines() {
|
| 149 |
+
const d = this.device;
|
| 150 |
+
const { C, V, FF, H, DK, DV, NH, NKV, HD, RD, EPS, ROPE_THETA, DELTA_IN, CONV_C, ATTN_IN } = CFG;
|
| 151 |
+
const mk = (code, label) => makePipeline(d, code, label);
|
| 152 |
+
if (this.q4) {
|
| 153 |
+
this.p = {
|
| 154 |
+
embed: mk(K.embedGatherQ4({ C }), "embedQ4"),
|
| 155 |
+
norm: mk(K.rmsnorm({ C, eps: EPS }), "rmsnorm"),
|
| 156 |
+
conv: mk(K.convQkv({ C: CONV_C, xrow: DELTA_IN }), "convQkv"),
|
| 157 |
+
gates: mk(K.deltaGates({ H, row: DELTA_IN, off: DELTA_IN - 2 * H }), "deltaGates"),
|
| 158 |
+
dstep: mk(K.deltaStep({ H, DK, DV }), "deltaStep"),
|
| 159 |
+
dlocal: mk(K.deltaChunkLocal({ H, DK, DV }), "deltaChunkLocal"),
|
| 160 |
+
dscan: mk(K.deltaChunkScan({ H, DK, DV }), "deltaChunkScan"),
|
| 161 |
+
gnorm: mk(K.gatedNorm({ H, DV, eps: EPS, zrow: DELTA_IN, zoff: CONV_C }), "gatedNorm"),
|
| 162 |
+
rope: mk(K.qkvRope({ NH, NKV, HD, RD, eps: EPS, ropeTheta: ROPE_THETA, maxSeq: this.maxSeq }), "qkvRope"),
|
| 163 |
+
attn: mk(K.attention({ NH, NKV, HD, maxSeq: this.maxSeq }), "attention"),
|
| 164 |
+
gateMul: mk(K.gateMul({ NH, NKV, HD }), "gateMul"),
|
| 165 |
+
swiglu: mk(K.swiglu({ C: FF }), "swiglu"),
|
| 166 |
+
gvDeltaIn: mk(K.gemvQ4({ N: DELTA_IN, K: C }), "gemvQ4.deltaIn"),
|
| 167 |
+
gvAttnIn: mk(K.gemvQ4({ N: ATTN_IN, K: C }), "gemvQ4.attnIn"),
|
| 168 |
+
gvOut: mk(K.gemvQ4({ N: C, K: C, epilogue: "add" }), "gemvQ4add.out"),
|
| 169 |
+
gvSwiglu: mk(K.gemvSwigluQ4({ N: FF, K: C }), "gemvSwigluQ4"),
|
| 170 |
+
gvDown: mk(K.gemvQ4({ N: C, K: FF, epilogue: "add" }), "gemvQ4add.down"),
|
| 171 |
+
gvLogits: mk(K.gemvQ4({ N: V, K: C, ft: false }), "gemvQ4.logits"),
|
| 172 |
+
gmDeltaIn: mk(K.gemmTiledQ({ N: DELTA_IN, K: C }), "gemmQ4.deltaIn"),
|
| 173 |
+
gmAttnIn: mk(K.gemmTiledQ({ N: ATTN_IN, K: C }), "gemmQ4.attnIn"),
|
| 174 |
+
gmOut: mk(K.gemmTiledQ({ N: C, K: C, epilogue: "add" }), "gemmQ4add.out"),
|
| 175 |
+
gmFF: mk(K.gemmTiledQ({ N: FF, K: C }), "gemmQ4.ff"),
|
| 176 |
+
gmDown: mk(K.gemmTiledQ({ N: C, K: FF, epilogue: "add" }), "gemmQ4add.down"),
|
| 177 |
+
sliceTop: mk(K.argmaxSliceTop({ V }), "argmaxSliceTop"),
|
| 178 |
+
candAdv: mk(K.argmaxCandAdvance({ NC: (V / 64) * 2 }), "argmaxCandAdvance"),
|
| 179 |
+
};
|
| 180 |
+
return;
|
| 181 |
+
}
|
| 182 |
+
this.p = {
|
| 183 |
+
embed: mk(K.embedGather({ C }), "embed"),
|
| 184 |
+
norm: mk(K.rmsnorm({ C, eps: EPS }), "rmsnorm"),
|
| 185 |
+
conv: mk(K.convQkv({ C: CONV_C, xrow: DELTA_IN }), "convQkv"),
|
| 186 |
+
gates: mk(K.deltaGates({ H, row: DELTA_IN, off: DELTA_IN - 2 * H }), "deltaGates"),
|
| 187 |
+
dstep: mk(K.deltaStep({ H, DK, DV }), "deltaStep"),
|
| 188 |
+
dlocal: mk(K.deltaChunkLocal({ H, DK, DV }), "deltaChunkLocal"),
|
| 189 |
+
dscan: mk(K.deltaChunkScan({ H, DK, DV }), "deltaChunkScan"),
|
| 190 |
+
gnorm: mk(K.gatedNorm({ H, DV, eps: EPS, zrow: DELTA_IN, zoff: CONV_C }), "gatedNorm"),
|
| 191 |
+
rope: mk(K.qkvRope({ NH, NKV, HD, RD, eps: EPS, ropeTheta: ROPE_THETA, maxSeq: this.maxSeq }), "qkvRope"),
|
| 192 |
+
attn: mk(K.attention({ NH, NKV, HD, maxSeq: this.maxSeq }), "attention"),
|
| 193 |
+
gateMul: mk(K.gateMul({ NH, NKV, HD }), "gateMul"),
|
| 194 |
+
swiglu: mk(K.swiglu({ C: FF }), "swiglu"),
|
| 195 |
+
// decode matvecs
|
| 196 |
+
gvDeltaIn: mk(K.gemv({ N: DELTA_IN, K: C }), "gemv.deltaIn"),
|
| 197 |
+
gvAttnIn: mk(K.gemv({ N: ATTN_IN, K: C }), "gemv.attnIn"),
|
| 198 |
+
gvOut: mk(K.gemvAdd({ N: C, K: C }), "gemvAdd.out"),
|
| 199 |
+
gvSwiglu: mk(K.gemvSwiglu({ N: FF, K: C }), "gemvSwiglu"),
|
| 200 |
+
gvDown: mk(K.gemvAdd({ N: C, K: FF }), "gemvAdd.down"),
|
| 201 |
+
gvLogits: mk(K.gemv({ N: V, K: C }), "gemv.logits"),
|
| 202 |
+
// prefill matmuls
|
| 203 |
+
gmDeltaIn: mk(K.gemm({ N: DELTA_IN, K: C }), "gemm.deltaIn"),
|
| 204 |
+
gmAttnIn: mk(K.gemm({ N: ATTN_IN, K: C }), "gemm.attnIn"),
|
| 205 |
+
gmOut: mk(K.gemmAdd({ N: C, K: C }), "gemmAdd.out"),
|
| 206 |
+
gmFF: mk(K.gemm({ N: FF, K: C }), "gemm.ff"),
|
| 207 |
+
gmDown: mk(K.gemmAdd({ N: C, K: FF }), "gemmAdd.down"),
|
| 208 |
+
sliceTop: mk(K.argmaxSliceTop({ V }), "argmaxSliceTop"),
|
| 209 |
+
candAdv: mk(K.argmaxCandAdvance({ NC: (V / 64) * 2 }), "argmaxCandAdvance"),
|
| 210 |
+
};
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
#bind(pipeline, entries) {
|
| 214 |
+
return this.device.createBindGroup({
|
| 215 |
+
layout: pipeline.getBindGroupLayout(0),
|
| 216 |
+
entries: Object.entries(entries).map(([binding, r]) => ({
|
| 217 |
+
binding: Number(binding),
|
| 218 |
+
resource: r instanceof GPUBuffer ? { buffer: r } : r,
|
| 219 |
+
})),
|
| 220 |
+
});
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
#bindGroups() {
|
| 224 |
+
const { C } = CFG;
|
| 225 |
+
const w = this.w;
|
| 226 |
+
const p = this.p;
|
| 227 |
+
const step = this.bufStep;
|
| 228 |
+
// q4 linears carry scales at binding 4 (5 for the swiglu up-matrix)
|
| 229 |
+
const sc = (name, b = 4) => (this.q4 ? { [b]: w[name + "::scales"] } : {});
|
| 230 |
+
this.bgEmbed = this.#bind(p.embed, { 0: w["embed_tokens.weight"], 1: this.bufHist, 2: this.bufX, 15: step, ...sc("embed_tokens.weight") });
|
| 231 |
+
this.bgFinalNorm = this.#bind(p.norm, { 0: this.bufX, 1: w["norm.weight"], 2: this.bufXn });
|
| 232 |
+
this.bgLogits = (tOff) => this.#bind(p.gvLogits, {
|
| 233 |
+
0: w["embed_tokens.weight"],
|
| 234 |
+
1: { buffer: this.bufXn, offset: tOff * C * 4, size: C * 4 },
|
| 235 |
+
2: this.bufLogits,
|
| 236 |
+
...sc("embed_tokens.weight"),
|
| 237 |
+
});
|
| 238 |
+
this.bgSliceTop = this.#bind(p.sliceTop, { 0: this.bufLogits, 2: this.bufCidxs });
|
| 239 |
+
this.bgCandAdv = this.#bind(p.candAdv, { 0: this.bufLogits, 1: step, 2: this.bufHist, 3: this.bufCidxs });
|
| 240 |
+
this.layers = [];
|
| 241 |
+
for (let i = 0; i < CFG.L; i++) {
|
| 242 |
+
const pre = `layers.${i}.`;
|
| 243 |
+
const lg = {
|
| 244 |
+
type: LAYER_TYPES[i],
|
| 245 |
+
bgNorm1: this.#bind(p.norm, { 0: this.bufX, 1: w[pre + "input_layernorm.weight"], 2: this.bufXn }),
|
| 246 |
+
bgNorm2: this.#bind(p.norm, { 0: this.bufX, 1: w[pre + "post_attention_layernorm.weight"], 2: this.bufXn }),
|
| 247 |
+
};
|
| 248 |
+
if (lg.type === "delta") {
|
| 249 |
+
const q = pre + "linear_attn.";
|
| 250 |
+
lg.bgIn = this.#bind(p.gvDeltaIn, { 0: w[q + "in_proj.weight"], 1: this.bufXn, 2: this.bufAct, ...sc(q + "in_proj.weight") });
|
| 251 |
+
lg.bgInP = this.#bind(p.gmDeltaIn, { 0: w[q + "in_proj.weight"], 1: this.bufXn, 2: this.bufAct, 15: step, ...sc(q + "in_proj.weight") });
|
| 252 |
+
lg.bgConv = this.#bind(p.conv, { 0: this.bufAct, 1: w[q + "conv1d.weight"], 2: this.convState[i], 3: this.bufQkv, 15: step });
|
| 253 |
+
lg.bgGates = this.#bind(p.gates, { 0: this.bufAct, 1: w[q + "gate_params"], 2: this.bufG, 3: this.bufBeta, 15: step });
|
| 254 |
+
lg.bgStep = this.#bind(p.dstep, { 0: this.bufQkv, 1: this.bufG, 2: this.bufBeta, 3: this.recState[i], 4: this.bufCore, 15: step });
|
| 255 |
+
lg.bgLocal = this.#bind(p.dlocal, { 0: this.bufQkv, 1: this.bufG, 2: this.bufBeta, 3: this.bufLoc, 15: step });
|
| 256 |
+
lg.bgScan = this.#bind(p.dscan, { 0: this.bufLoc, 1: this.recState[i], 2: this.bufCore, 15: step });
|
| 257 |
+
lg.bgGnorm = this.#bind(p.gnorm, { 0: this.bufCore, 1: this.bufAct, 2: w[q + "norm.weight"], 3: this.bufGn });
|
| 258 |
+
lg.bgOut = this.#bind(p.gvOut, { 0: w[q + "out_proj.weight"], 1: this.bufGn, 2: this.bufX, ...sc(q + "out_proj.weight") });
|
| 259 |
+
lg.bgOutP = this.#bind(p.gmOut, { 0: w[q + "out_proj.weight"], 1: this.bufGn, 2: this.bufX, 15: step, ...sc(q + "out_proj.weight") });
|
| 260 |
+
} else {
|
| 261 |
+
const q = pre + "self_attn.";
|
| 262 |
+
lg.bgIn = this.#bind(p.gvAttnIn, { 0: w[q + "in_proj.weight"], 1: this.bufXn, 2: this.bufAct, ...sc(q + "in_proj.weight") });
|
| 263 |
+
lg.bgInP = this.#bind(p.gmAttnIn, { 0: w[q + "in_proj.weight"], 1: this.bufXn, 2: this.bufAct, 15: step, ...sc(q + "in_proj.weight") });
|
| 264 |
+
lg.bgRope = this.#bind(p.rope, { 0: this.bufAct, 3: w[q + "q_norm.weight"], 4: w[q + "k_norm.weight"], 5: this.bufQout, 6: this.kCache[i], 7: this.vCache[i], 15: step });
|
| 265 |
+
lg.bgAttn = this.#bind(p.attn, { 0: this.bufQout, 1: this.kCache[i], 2: this.vCache[i], 3: this.bufAttnOut, 15: step });
|
| 266 |
+
lg.bgGateMul = this.#bind(p.gateMul, { 0: this.bufAct, 1: this.bufAttnOut, 15: step });
|
| 267 |
+
lg.bgOut = this.#bind(p.gvOut, { 0: w[q + "o_proj.weight"], 1: this.bufAttnOut, 2: this.bufX, ...sc(q + "o_proj.weight") });
|
| 268 |
+
lg.bgOutP = this.#bind(p.gmOut, { 0: w[q + "o_proj.weight"], 1: this.bufAttnOut, 2: this.bufX, 15: step, ...sc(q + "o_proj.weight") });
|
| 269 |
+
}
|
| 270 |
+
const m = pre + "mlp.";
|
| 271 |
+
lg.bgSwiglu = this.#bind(p.gvSwiglu, { 0: w[m + "gate_proj.weight"], 1: this.bufXn, 2: this.bufMlpA, 3: w[m + "up_proj.weight"], ...sc(m + "gate_proj.weight"), ...sc(m + "up_proj.weight", 5) });
|
| 272 |
+
lg.bgDown = this.#bind(p.gvDown, { 0: w[m + "down_proj.weight"], 1: this.bufMlpA, 2: this.bufX, ...sc(m + "down_proj.weight") });
|
| 273 |
+
lg.bgGateP = this.#bind(p.gmFF, { 0: w[m + "gate_proj.weight"], 1: this.bufXn, 2: this.bufMlpA, 15: step, ...sc(m + "gate_proj.weight") });
|
| 274 |
+
lg.bgUpP = this.#bind(p.gmFF, { 0: w[m + "up_proj.weight"], 1: this.bufXn, 2: this.bufMlpB, 15: step, ...sc(m + "up_proj.weight") });
|
| 275 |
+
lg.bgSwP = this.#bind(p.swiglu, { 0: this.bufMlpA, 1: this.bufMlpB, 2: this.bufMlpC, 15: step });
|
| 276 |
+
lg.bgDownP = this.#bind(p.gmDown, { 0: w[m + "down_proj.weight"], 1: this.bufMlpC, 2: this.bufX, 15: step, ...sc(m + "down_proj.weight") });
|
| 277 |
+
this.layers.push(lg);
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
reset() {
|
| 282 |
+
const { CONV_C, H, DK, DV, NKV, HD } = CFG;
|
| 283 |
+
this.pos = 0;
|
| 284 |
+
this.hist = [];
|
| 285 |
+
for (let i = 0; i < CFG.L; i++) {
|
| 286 |
+
if (LAYER_TYPES[i] === "delta") {
|
| 287 |
+
this.device.queue.writeBuffer(this.convState[i], 0, new Float32Array(CONV_C * 4));
|
| 288 |
+
this.device.queue.writeBuffer(this.recState[i], 0, new Float32Array(H * DK * DV));
|
| 289 |
+
}
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
#writeStep(t, pos0) {
|
| 294 |
+
this.device.queue.writeBuffer(this.bufStep, 0, new Uint32Array([t, pos0, 0, 0]));
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
// one full forward over T tokens already in bufHist at [pos0, pos0+T).
|
| 298 |
+
// debug: GPUBuffer to receive per-stage copies of the hidden state
|
| 299 |
+
// (slot 0 = embed, 1+i = after layer i, L+1 = final norm), T*C floats each.
|
| 300 |
+
#encodeForward(T, prefill, { debug = null, cascade = false } = {}) {
|
| 301 |
+
const { C, H, FF, NH, NKV, HD, DELTA_IN, CONV_C, ATTN_IN } = CFG;
|
| 302 |
+
const enc = this.device.createCommandEncoder();
|
| 303 |
+
let pass = enc.beginComputePass();
|
| 304 |
+
const disp = (pipe, bg, x, y = 1) => {
|
| 305 |
+
pass.setPipeline(pipe);
|
| 306 |
+
pass.setBindGroup(0, bg);
|
| 307 |
+
pass.dispatchWorkgroups(x, y);
|
| 308 |
+
};
|
| 309 |
+
const snap = (buf, slot) => {
|
| 310 |
+
if (!debug) return;
|
| 311 |
+
pass.end();
|
| 312 |
+
enc.copyBufferToBuffer(buf, 0, debug, slot * T * C * 4, T * C * 4);
|
| 313 |
+
pass = enc.beginComputePass();
|
| 314 |
+
};
|
| 315 |
+
const p = this.p;
|
| 316 |
+
// f16: gemv 8 rows/wg (sg=32), gemm 8x8 grid; q4: gemv 16 rows/wg
|
| 317 |
+
// (2 rows/subgroup), swiglu 8, tiled gemm 64x64 tiles
|
| 318 |
+
const rows = (n) => Math.ceil(n / (this.q4 ? 16 : 8));
|
| 319 |
+
const rowsSw = (n) => Math.ceil(n / 8);
|
| 320 |
+
const gmX = (n) => Math.ceil(n / (this.q4 ? 64 : 8));
|
| 321 |
+
const cols = Math.ceil(T / (this.q4 ? 64 : 8));
|
| 322 |
+
disp(p.embed, this.bgEmbed, T);
|
| 323 |
+
snap(this.bufX, 0);
|
| 324 |
+
for (let i = 0; i < CFG.L; i++) {
|
| 325 |
+
const lg = this.layers[i];
|
| 326 |
+
disp(p.norm, lg.bgNorm1, T);
|
| 327 |
+
if (lg.type === "delta") {
|
| 328 |
+
if (prefill) disp(p.gmDeltaIn, lg.bgInP, gmX(DELTA_IN), cols);
|
| 329 |
+
else disp(p.gvDeltaIn, lg.bgIn, rows(DELTA_IN));
|
| 330 |
+
disp(p.conv, lg.bgConv, Math.ceil(CONV_C / 256));
|
| 331 |
+
disp(p.gates, lg.bgGates, Math.ceil((T * H) / 64));
|
| 332 |
+
if (prefill) {
|
| 333 |
+
disp(p.dlocal, lg.bgLocal, Math.ceil(T / 64) * H);
|
| 334 |
+
disp(p.dscan, lg.bgScan, H);
|
| 335 |
+
} else {
|
| 336 |
+
disp(p.dstep, lg.bgStep, H);
|
| 337 |
+
}
|
| 338 |
+
disp(p.gnorm, lg.bgGnorm, T * H);
|
| 339 |
+
if (prefill) disp(p.gmOut, lg.bgOutP, gmX(C), cols);
|
| 340 |
+
else disp(p.gvOut, lg.bgOut, rows(C));
|
| 341 |
+
} else {
|
| 342 |
+
if (prefill) disp(p.gmAttnIn, lg.bgInP, gmX(ATTN_IN), cols);
|
| 343 |
+
else disp(p.gvAttnIn, lg.bgIn, rows(ATTN_IN));
|
| 344 |
+
disp(p.rope, lg.bgRope, T * (NH + NKV));
|
| 345 |
+
disp(p.attn, lg.bgAttn, T * NH);
|
| 346 |
+
disp(p.gateMul, lg.bgGateMul, Math.ceil((T * NH * HD) / 256));
|
| 347 |
+
if (prefill) disp(p.gmOut, lg.bgOutP, gmX(C), cols);
|
| 348 |
+
else disp(p.gvOut, lg.bgOut, rows(C));
|
| 349 |
+
}
|
| 350 |
+
disp(p.norm, lg.bgNorm2, T);
|
| 351 |
+
if (prefill) {
|
| 352 |
+
disp(p.gmFF, lg.bgGateP, gmX(FF), cols);
|
| 353 |
+
disp(p.gmFF, lg.bgUpP, gmX(FF), cols);
|
| 354 |
+
disp(p.swiglu, lg.bgSwP, Math.ceil((T * FF) / 256));
|
| 355 |
+
disp(p.gmDown, lg.bgDownP, gmX(C), cols);
|
| 356 |
+
} else {
|
| 357 |
+
disp(p.gvSwiglu, lg.bgSwiglu, rowsSw(FF));
|
| 358 |
+
disp(p.gvDown, lg.bgDown, rows(C));
|
| 359 |
+
}
|
| 360 |
+
snap(this.bufX, 1 + i);
|
| 361 |
+
}
|
| 362 |
+
disp(p.norm, this.bgFinalNorm, T);
|
| 363 |
+
snap(this.bufXn, CFG.L + 1);
|
| 364 |
+
disp(p.gvLogits, this.bgLogits(T - 1), rows(CFG.V));
|
| 365 |
+
if (cascade) {
|
| 366 |
+
disp(p.sliceTop, this.bgSliceTop, CFG.V / 64);
|
| 367 |
+
disp(p.candAdv, this.bgCandAdv, 1);
|
| 368 |
+
}
|
| 369 |
+
pass.end();
|
| 370 |
+
return enc;
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
pushTokens(ids) {
|
| 374 |
+
this.device.queue.writeBuffer(this.bufHist, this.pos * 4, new Uint32Array(ids));
|
| 375 |
+
this.hist.push(...ids);
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// prefill; logits of the last token land in bufLogits
|
| 379 |
+
prefill(ids, opts = {}) {
|
| 380 |
+
let off = 0;
|
| 381 |
+
while (off < ids.length) {
|
| 382 |
+
const seg = ids.slice(off, off + this.maxT);
|
| 383 |
+
this.pushTokens(seg);
|
| 384 |
+
this.#writeStep(seg.length, this.pos);
|
| 385 |
+
this.device.queue.submit([this.#encodeForward(seg.length, true, opts).finish()]);
|
| 386 |
+
this.pos += seg.length;
|
| 387 |
+
off += seg.length;
|
| 388 |
+
}
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
// prefill + GPU-side argmax of the first generated token: after this,
|
| 392 |
+
// hist[P] holds the first token and step = {1, P} (P = prompt length).
|
| 393 |
+
startGenerate(ids) {
|
| 394 |
+
this.reset();
|
| 395 |
+
this.promptLen = ids.length;
|
| 396 |
+
let off = 0;
|
| 397 |
+
while (off < ids.length) {
|
| 398 |
+
const seg = ids.slice(off, off + this.maxT);
|
| 399 |
+
this.pushTokens(seg);
|
| 400 |
+
this.#writeStep(seg.length, this.pos);
|
| 401 |
+
const last = off + seg.length >= ids.length;
|
| 402 |
+
this.device.queue.submit([this.#encodeForward(seg.length, true, { cascade: last }).finish()]);
|
| 403 |
+
this.pos += seg.length;
|
| 404 |
+
off += seg.length;
|
| 405 |
+
}
|
| 406 |
+
this.gen = 1;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
// n GPU-resident decode steps in ONE submit (no CPU roundtrips inside);
|
| 410 |
+
// returns all generated token ids so far (incl. the pending one).
|
| 411 |
+
async decodeChunk(n) {
|
| 412 |
+
const bufs = [];
|
| 413 |
+
for (let i = 0; i < n; i++) {
|
| 414 |
+
bufs.push(this.#encodeForward(1, false, { cascade: true }).finish());
|
| 415 |
+
}
|
| 416 |
+
this.device.queue.submit(bufs);
|
| 417 |
+
this.pos += n;
|
| 418 |
+
this.gen += n;
|
| 419 |
+
const total = this.promptLen + this.gen;
|
| 420 |
+
const rb = this.device.createBuffer({ size: total * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
|
| 421 |
+
const enc = this.device.createCommandEncoder();
|
| 422 |
+
enc.copyBufferToBuffer(this.bufHist, 0, rb, 0, total * 4);
|
| 423 |
+
this.device.queue.submit([enc.finish()]);
|
| 424 |
+
await rb.mapAsync(GPUMapMode.READ);
|
| 425 |
+
const ids = Array.from(new Uint32Array(rb.getMappedRange().slice(0)));
|
| 426 |
+
rb.destroy();
|
| 427 |
+
this.hist = ids;
|
| 428 |
+
return ids.slice(this.promptLen);
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
decodeStep(tokenId, opts = {}) {
|
| 432 |
+
this.pushTokens([tokenId]);
|
| 433 |
+
this.#writeStep(1, this.pos);
|
| 434 |
+
this.device.queue.submit([this.#encodeForward(1, false, opts).finish()]);
|
| 435 |
+
this.pos += 1;
|
| 436 |
+
}
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// GEMM prefill matvec grid note: gemm uses workgroup_size(8,8) over (N, T);
|
| 440 |
+
// rows() above sizes the X dimension in units of 8 columns.
|
web/npy.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Minimal .npy loader (little-endian f32 only).
|
| 2 |
+
export async function loadNpy(url) {
|
| 3 |
+
const buf = await (await fetch(url)).arrayBuffer();
|
| 4 |
+
const view = new DataView(buf);
|
| 5 |
+
if (view.getUint32(0, true) !== 0x4d554e93) throw new Error("not npy: " + url);
|
| 6 |
+
const major = view.getUint8(6);
|
| 7 |
+
const headerLen = major >= 2 ? view.getUint32(8, true) : view.getUint16(8, true);
|
| 8 |
+
const headerStart = major >= 2 ? 12 : 10;
|
| 9 |
+
const header = new TextDecoder().decode(new Uint8Array(buf, headerStart, headerLen));
|
| 10 |
+
const descr = header.match(/'descr':\s*'([^']+)'/)[1];
|
| 11 |
+
if (descr !== "<f4") throw new Error(`unsupported dtype ${descr}`);
|
| 12 |
+
if (/'fortran_order':\s*True/.test(header)) throw new Error(`fortran order not supported: ${url}`);
|
| 13 |
+
const shape = (header.match(/'shape':\s*\(([^)]*)\)/)[1].match(/\d+/g) ?? []).map(Number);
|
| 14 |
+
const data = new Float32Array(buf, headerStart + headerLen);
|
| 15 |
+
return { shape, data };
|
| 16 |
+
}
|
web/tokenizer.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Byte-level BPE tokenizer for Qwen3.5 (HF tokenizer.json format).
|
| 2 |
+
// Differences vs the lfm2 port: NFC normalizer, Qwen pretokenizer regex
|
| 3 |
+
// (\p{M} marks glued to letters, single digits), 26 special tokens, and the
|
| 4 |
+
// Qwen chat template (auto-inserts an empty think block, non-thinking mode).
|
| 5 |
+
|
| 6 |
+
// GPT-2 byte <-> unicode table
|
| 7 |
+
function byteMaps() {
|
| 8 |
+
const bs = [];
|
| 9 |
+
for (let i = 33; i <= 126; i++) bs.push(i);
|
| 10 |
+
for (let i = 161; i <= 172; i++) bs.push(i);
|
| 11 |
+
for (let i = 174; i <= 255; i++) bs.push(i);
|
| 12 |
+
const cs = bs.slice();
|
| 13 |
+
let n = 0;
|
| 14 |
+
for (let b = 0; b < 256; b++) {
|
| 15 |
+
if (!bs.includes(b)) {
|
| 16 |
+
bs.push(b);
|
| 17 |
+
cs.push(256 + n);
|
| 18 |
+
n++;
|
| 19 |
+
}
|
| 20 |
+
}
|
| 21 |
+
const b2c = new Array(256);
|
| 22 |
+
const c2b = new Map();
|
| 23 |
+
bs.forEach((b, i) => {
|
| 24 |
+
b2c[b] = String.fromCharCode(cs[i]);
|
| 25 |
+
c2b.set(cs[i], b);
|
| 26 |
+
});
|
| 27 |
+
return { b2c, c2b };
|
| 28 |
+
}
|
| 29 |
+
const { b2c, c2b } = byteMaps();
|
| 30 |
+
|
| 31 |
+
// JS port of the Qwen pretokenizer regex; (?i:'s|'t|'re|'ve|'m|'ll|'d)
|
| 32 |
+
// rewritten with explicit cases. Source pattern:
|
| 33 |
+
// (?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+
|
| 34 |
+
const PRETOK =
|
| 35 |
+
/'(?:[sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD])|[^\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+/gu;
|
| 36 |
+
|
| 37 |
+
export class Tokenizer {
|
| 38 |
+
static async load(url) {
|
| 39 |
+
const t = new Tokenizer();
|
| 40 |
+
const j = await (await fetch(url)).json();
|
| 41 |
+
t.vocab = new Map(Object.entries(j.model.vocab));
|
| 42 |
+
t.inv = new Map([...t.vocab].map(([s, i]) => [i, s]));
|
| 43 |
+
t.ranks = new Map(j.model.merges.map((m, i) => [Array.isArray(m) ? m.join(" ") : m, i]));
|
| 44 |
+
t.special = new Map();
|
| 45 |
+
for (const a of j.added_tokens) {
|
| 46 |
+
t.special.set(a.content, a.id);
|
| 47 |
+
t.inv.set(a.id, a.content);
|
| 48 |
+
}
|
| 49 |
+
t.specialRe = new RegExp(
|
| 50 |
+
"(" + [...t.special.keys()].map((s) => s.replace(/[|<>]/g, "\\$&")).join("|") + ")"
|
| 51 |
+
);
|
| 52 |
+
t.eos = t.special.get("<|im_end|>");
|
| 53 |
+
t.eot = t.special.get("<|endoftext|>");
|
| 54 |
+
return t;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
#bpe(word) {
|
| 58 |
+
let parts = [...word];
|
| 59 |
+
while (parts.length > 1) {
|
| 60 |
+
let best = null, bestRank = Infinity;
|
| 61 |
+
for (let i = 0; i < parts.length - 1; i++) {
|
| 62 |
+
const r = this.ranks.get(parts[i] + " " + parts[i + 1]);
|
| 63 |
+
if (r !== undefined && r < bestRank) {
|
| 64 |
+
bestRank = r;
|
| 65 |
+
best = i;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
if (best === null) break;
|
| 69 |
+
parts = [...parts.slice(0, best), parts[best] + parts[best + 1], ...parts.slice(best + 2)];
|
| 70 |
+
}
|
| 71 |
+
return parts;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
#encodeText(text) {
|
| 75 |
+
const ids = [];
|
| 76 |
+
const enc = new TextEncoder();
|
| 77 |
+
for (const m of text.matchAll(PRETOK)) {
|
| 78 |
+
const chars = Array.from(enc.encode(m[0]), (b) => b2c[b]).join("");
|
| 79 |
+
for (const tok of this.#bpe(chars)) {
|
| 80 |
+
const id = this.vocab.get(tok);
|
| 81 |
+
if (id === undefined) throw new Error(`token not in vocab: ${JSON.stringify(tok)}`);
|
| 82 |
+
ids.push(id);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
return ids;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
encode(text) {
|
| 89 |
+
const ids = [];
|
| 90 |
+
for (const piece of text.normalize("NFC").split(this.specialRe)) {
|
| 91 |
+
if (!piece) continue;
|
| 92 |
+
if (this.special.has(piece)) ids.push(this.special.get(piece));
|
| 93 |
+
else ids.push(...this.#encodeText(piece));
|
| 94 |
+
}
|
| 95 |
+
return ids;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
decode(ids, { skipSpecial = true } = {}) {
|
| 99 |
+
const bytes = [];
|
| 100 |
+
for (const id of ids) {
|
| 101 |
+
const tok = this.inv.get(id);
|
| 102 |
+
if (tok === undefined) continue;
|
| 103 |
+
if (this.special.has(tok)) {
|
| 104 |
+
if (!skipSpecial) bytes.push(...new TextEncoder().encode(tok));
|
| 105 |
+
continue;
|
| 106 |
+
}
|
| 107 |
+
for (const ch of tok) bytes.push(c2b.get(ch.charCodeAt(0)));
|
| 108 |
+
}
|
| 109 |
+
return new TextDecoder().decode(new Uint8Array(bytes));
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
// chat_template.jinja equivalent for text-only chats (non-thinking mode:
|
| 113 |
+
// the template pre-fills an empty think block in the generation prompt)
|
| 114 |
+
applyChatTemplate(messages, { addGenerationPrompt = true } = {}) {
|
| 115 |
+
let s = "";
|
| 116 |
+
for (const m of messages) {
|
| 117 |
+
s += `<|im_start|>${m.role}\n${m.content}<|im_end|>\n`;
|
| 118 |
+
}
|
| 119 |
+
if (addGenerationPrompt) s += "<|im_start|>assistant\n<think>\n\n</think>\n\n";
|
| 120 |
+
return this.encode(s);
|
| 121 |
+
}
|
| 122 |
+
}
|