borkiss commited on
Commit
51e8f9a
·
verified ·
1 Parent(s): fcb061c

Upload folder using huggingface_hub

Browse files
Files changed (12) hide show
  1. README.md +8 -4
  2. index.html +79 -17
  3. web/bench-main.js +108 -0
  4. web/bench.html +31 -0
  5. web/config.js +22 -0
  6. web/gpu.js +91 -0
  7. web/index.html +81 -0
  8. web/kernels.js +1361 -0
  9. web/main.js +171 -0
  10. web/model.js +440 -0
  11. web/npy.js +16 -0
  12. web/tokenizer.js +122 -0
README.md CHANGED
@@ -1,10 +1,14 @@
1
  ---
2
- title: Qwen35 2b Webgpu Demo
3
- emoji: 🏆
4
  colorFrom: purple
5
- colorTo: gray
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
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
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }