Spaces:
Running
Running
discrete-GPU validation ?bench=discrete: bandwidth + live + spec-flip in one page
Browse files- index.html +74 -0
index.html
CHANGED
|
@@ -220,6 +220,79 @@ async function runPerfBench() {
|
|
| 220 |
log.innerHTML = tbl; st.textContent = "live decode profile · done"; console.log("[perfbench]", runs);
|
| 221 |
}
|
| 222 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
// ── PER-PASS GPU TRACE (?bench=trace) ── where does a token's ~40ms go? Runs a profiled forward through the
|
| 224 |
// step() path (window.__profile → timestamp-query'd ns per pass) and dumps the breakdown sorted by cost, so the
|
| 225 |
// 83% non-weight overhead is named exactly (attention / argmax / lm_head / norms / dispatch count) — no guessing.
|
|
@@ -265,6 +338,7 @@ try {
|
|
| 265 |
if (params.get("bench") === "spec") { globalThis.__spec = false; await runSpecBench(); }
|
| 266 |
else if (params.get("bench") === "perf") { await runPerfBench(); }
|
| 267 |
else if (params.get("bench") === "trace") { await runTraceBench(); }
|
|
|
|
| 268 |
else if (pending) { const w = [...log.querySelectorAll(".a")].reverse().find((x) => x.dataset.pending); if (w) w.remove(); const p = pending; pending = null; generate(p, true); }
|
| 269 |
else await proactiveGreeting();
|
| 270 |
} catch (e) { st.textContent = "⚠ " + e.message; bubble("a", "Could not start: " + e.message); }
|
|
|
|
| 220 |
log.innerHTML = tbl; st.textContent = "live decode profile · done"; console.log("[perfbench]", runs);
|
| 221 |
}
|
| 222 |
|
| 223 |
+
// ── DISCRETE-GPU VALIDATION (?bench=discrete) ── the whole thesis in one page, on whatever GPU opens it:
|
| 224 |
+
// (1) real VRAM bandwidth + decode roofline, (2) live BitNet tok/s warmed, (3) does spec-decode FLIP from the
|
| 225 |
+
// iGPU loss to a win once bandwidth-bound? Confirms the discrete GPU (bandwidth >350 GB/s) is actually in use.
|
| 226 |
+
const BW_FILL = `@group(0) @binding(0) var<storage,read_write> d: array<u32>;
|
| 227 |
+
@group(0) @binding(1) var<uniform> P: vec4<u32>;
|
| 228 |
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g:vec3<u32>){ let n=P.x; var i=g.x; loop{ if(i>=n){break;} d[i]=(i*2654435761u+1u); i=i+P.y; } }`;
|
| 229 |
+
const BW_READ = `@group(0) @binding(0) var<storage,read> d: array<vec4<u32>>;
|
| 230 |
+
@group(0) @binding(1) var<storage,read_write> sink: array<u32>;
|
| 231 |
+
@group(0) @binding(2) var<uniform> P: vec4<u32>;
|
| 232 |
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g:vec3<u32>){ let n=P.x; let stride=P.y; var acc=vec4<u32>(0u); var i=g.x; loop{ if(i>=n){break;} acc=acc^d[i]; i=i+stride; } sink[g.x]=acc.x^acc.y^acc.z^acc.w; }`;
|
| 233 |
+
async function measureVramBW(dev) {
|
| 234 |
+
const L = dev.limits;
|
| 235 |
+
const bytes = Math.floor(Math.min(L.maxStorageBufferBindingSize, L.maxBufferSize, 512 * 1024 * 1024) / 16) * 16, nVec = bytes / 16;
|
| 236 |
+
const buf = dev.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE });
|
| 237 |
+
const wg = Math.min(L.maxComputeWorkgroupsPerDimension, 65535), TOTAL = wg * 256;
|
| 238 |
+
const sink = dev.createBuffer({ size: TOTAL * 4, usage: GPUBufferUsage.STORAGE });
|
| 239 |
+
const P = dev.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
|
| 240 |
+
dev.queue.writeBuffer(P, 0, new Uint32Array([bytes / 4, TOTAL, 0, 0]));
|
| 241 |
+
const fp = dev.createComputePipeline({ layout: "auto", compute: { module: dev.createShaderModule({ code: BW_FILL }), entryPoint: "main" } });
|
| 242 |
+
const fbg = dev.createBindGroup({ layout: fp.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: buf } }, { binding: 1, resource: { buffer: P } }] });
|
| 243 |
+
{ const e = dev.createCommandEncoder(); const p = e.beginComputePass(); p.setPipeline(fp); p.setBindGroup(0, fbg); p.dispatchWorkgroups(wg); p.end(); dev.queue.submit([e.finish()]); await dev.queue.onSubmittedWorkDone(); }
|
| 244 |
+
dev.queue.writeBuffer(P, 0, new Uint32Array([nVec, TOTAL, 0, 0]));
|
| 245 |
+
const rp = dev.createComputePipeline({ layout: "auto", compute: { module: dev.createShaderModule({ code: BW_READ }), entryPoint: "main" } });
|
| 246 |
+
const rbg = dev.createBindGroup({ layout: rp.getBindGroupLayout(0), entries: [{ binding: 0, resource: { buffer: buf } }, { binding: 1, resource: { buffer: sink } }, { binding: 2, resource: { buffer: P } }] });
|
| 247 |
+
const run = async (passes) => { const e = dev.createCommandEncoder(); for (let k = 0; k < passes; k++) { const p = e.beginComputePass(); p.setPipeline(rp); p.setBindGroup(0, rbg); p.dispatchWorkgroups(wg); p.end(); } const t0 = performance.now(); dev.queue.submit([e.finish()]); await dev.queue.onSubmittedWorkDone(); return performance.now() - t0; };
|
| 248 |
+
await run(4); let best = 1e9; for (let k = 0; k < 5; k++) best = Math.min(best, await run(32));
|
| 249 |
+
buf.destroy(); sink.destroy();
|
| 250 |
+
return (bytes * 32 / 1073741824) / (best / 1000);
|
| 251 |
+
}
|
| 252 |
+
async function runDiscreteBench() {
|
| 253 |
+
log.innerHTML = ""; input.disabled = send.disabled = true;
|
| 254 |
+
const rep = m.rep ?? 1.3, gpu = engine._gpu, dev = gpu && gpu._dev && gpu._dev();
|
| 255 |
+
let adapterStr = "unknown";
|
| 256 |
+
try { const a = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); const inf = (a && a.info) || {}; adapterStr = ((inf.vendor || "") + " " + (inf.architecture || "") + " " + (inf.device || "")).trim() || "unknown"; } catch {}
|
| 257 |
+
st.textContent = "measuring VRAM bandwidth…";
|
| 258 |
+
let gbps = 0; try { gbps = await measureVramBW(dev); } catch (e) { console.warn("bw", e); }
|
| 259 |
+
const roofTok = gbps / 0.69, discrete = gbps > 350;
|
| 260 |
+
st.textContent = "warming + measuring live decode…";
|
| 261 |
+
globalThis.__spec = false;
|
| 262 |
+
engine.reset(); await engine.generate(engine.tokenize(engine.frameTurn("Write one sentence about the sea.", false)), { maxNew: 48, repPenalty: rep });
|
| 263 |
+
let live = 0;
|
| 264 |
+
for (let i = 0; i < 3; i++) { engine.reset(); const r = await engine.generate(engine.tokenize(engine.frameTurn("Write a detailed paragraph about ocean currents.", false)), { maxNew: 128, repPenalty: rep }); live = Math.max(live, (r.stats && r.stats.tokps) || 0); }
|
| 265 |
+
st.textContent = "measuring spec-decode flip…";
|
| 266 |
+
const sIds = engine.tokenize(engine.frameTurn("Passage: \"The quick brown fox jumps over the lazy dog near the river bank at dawn.\" Repeat that passage back to me word for word.", false));
|
| 267 |
+
globalThis.__spec = false; engine.reset(); let t0 = performance.now(); const rb = await engine.generate(sIds.slice(), { maxNew: 192, repPenalty: rep }); const bTok = rb.outIds.length / ((performance.now() - t0) / 1000);
|
| 268 |
+
let sTok = 0, perVerify = 0, accept = 0, same = true, hasSpec = !!engine.specAvailable;
|
| 269 |
+
if (hasSpec) {
|
| 270 |
+
globalThis.__spec = true; engine.reset(); t0 = performance.now(); const rs = await engine.generate(sIds.slice(), { maxNew: 192, repPenalty: rep }); sTok = rs.outIds.length / ((performance.now() - t0) / 1000); globalThis.__spec = false;
|
| 271 |
+
const sp = (rs.stats && rs.stats.spec) || null;
|
| 272 |
+
perVerify = sp && sp.windows ? 1 + sp.accepted / sp.windows : 0; accept = sp && sp.drafted ? 100 * sp.accepted / sp.drafted : 0;
|
| 273 |
+
same = rb.outIds.length === rs.outIds.length && rb.outIds.every((x, i) => x === rs.outIds[i]);
|
| 274 |
+
}
|
| 275 |
+
const flip = hasSpec && sTok > bTok * 1.1, spRatio = bTok ? sTok / bTok : 0;
|
| 276 |
+
const row = (k, v) => `<tr style="border-top:1px solid var(--line)"><td style="padding:7px 8px;color:var(--dim)">${k}</td><td style="padding:7px 8px;font-weight:600">${v}</td></tr>`;
|
| 277 |
+
log.innerHTML = `<div style="font-family:ui-monospace,monospace;font-size:13px;max-width:820px;margin:0 auto;padding:8px">
|
| 278 |
+
<div style="font-size:18px;font-weight:700;margin-bottom:4px">Discrete-GPU validation</div>
|
| 279 |
+
<div style="color:var(--dim);margin-bottom:12px">adapter: ${adapterStr} · BitNet-2B</div>
|
| 280 |
+
<table style="width:100%;border-collapse:collapse">
|
| 281 |
+
${row("GPU in use", `<span style="color:${discrete ? "#48c26c" : "#e0a94a"}">${discrete ? "DISCRETE ✓" : "integrated (bandwidth "+gbps.toFixed(0)+" GB/s — not a discrete GPU)"}</span>`)}
|
| 282 |
+
${row("VRAM bandwidth", `${gbps.toFixed(0)} GB/s`)}
|
| 283 |
+
${row("Decode roofline", `${roofTok.toFixed(0)} tok/s`)}
|
| 284 |
+
${row("Live decode (warmed)", `${live.toFixed(0)} tok/s · ${(100*live/roofTok).toFixed(0)}% of roofline`)}
|
| 285 |
+
${row("Spec-decode (retrieval)", hasSpec ? `${sTok.toFixed(0)} vs ${bTok.toFixed(0)} baseline · <b style="color:${flip ? "#48c26c" : "#e0a94a"}">${spRatio.toFixed(2)}×</b> · ${perVerify.toFixed(2)} tok/verify · ${accept.toFixed(0)}% accept · ${same ? "byte-exact ✓" : "DIVERGED ✗"}` : "unavailable")}
|
| 286 |
+
</table>
|
| 287 |
+
<div style="margin-top:14px;font-size:15px;font-weight:600;color:${flip ? "#48c26c" : "#e0a94a"}">${!discrete
|
| 288 |
+
? "⚠ This is still an integrated GPU (bandwidth ≤350 GB/s). Open on a machine with a discrete GPU to validate the >1000 path — the browser may be picking the iGPU for power saving."
|
| 289 |
+
: flip
|
| 290 |
+
? `✓ CONFIRMED: on discrete silicon spec-decode FLIPPED to a ${spRatio.toFixed(2)}× win (byte-exact), and the roofline is ${roofTok.toFixed(0)} tok/s. The code we built delivers the high-throughput path unchanged on this hardware.`
|
| 291 |
+
: `Spec-decode is ${spRatio.toFixed(2)}× here — not yet a clear win. Bandwidth ${gbps.toFixed(0)} GB/s (roofline ${roofTok.toFixed(0)}); the forward may still be compute-bound on this GPU. Send me the numbers.`}</div>
|
| 292 |
+
</div>`;
|
| 293 |
+
st.textContent = "discrete validation · done"; console.log("[discrete]", { adapterStr, gbps, roofTok, live, bTok, sTok, perVerify, accept, same });
|
| 294 |
+
}
|
| 295 |
+
|
| 296 |
// ── PER-PASS GPU TRACE (?bench=trace) ── where does a token's ~40ms go? Runs a profiled forward through the
|
| 297 |
// step() path (window.__profile → timestamp-query'd ns per pass) and dumps the breakdown sorted by cost, so the
|
| 298 |
// 83% non-weight overhead is named exactly (attention / argmax / lm_head / norms / dispatch count) — no guessing.
|
|
|
|
| 338 |
if (params.get("bench") === "spec") { globalThis.__spec = false; await runSpecBench(); }
|
| 339 |
else if (params.get("bench") === "perf") { await runPerfBench(); }
|
| 340 |
else if (params.get("bench") === "trace") { await runTraceBench(); }
|
| 341 |
+
else if (params.get("bench") === "discrete") { globalThis.__spec = false; await runDiscreteBench(); }
|
| 342 |
else if (pending) { const w = [...log.querySelectorAll(".a")].reverse().find((x) => x.dataset.pending); if (w) w.remove(); const p = pending; pending = null; generate(p, true); }
|
| 343 |
else await proactiveGreeting();
|
| 344 |
} catch (e) { st.textContent = "⚠ " + e.message; bubble("a", "Could not start: " + e.message); }
|