concept-collection / fastandaccurate
fastandaccurate / scripts / check-gpu.mjs
138 lines · 4.6 KBBlameHistoryRaw
1/**
2 * End-to-end check of the WebGPU solver in a real browser: serves dist/,
3 * opens the problem page in headless Chrome, picks mfs-gpu in the Solution
4 * section, computes one point through the worker, and asserts the reported
5 * error is what single precision gives. The node suite
6 * (test/gpu-test.ts) covers accuracy across the instances; what this adds
7 * is that the same code reaches a device from inside a page and a worker,
8 * which is the only place the site's "Run in this browser" button lives.
9 *
10 * Usage: npm run build && node scripts/check-gpu.mjs
11 * Exits 2, without failing, when no WebGPU device can be had here.
12 */
13import { createServer } from "node:http";
14import { readFile } from "node:fs/promises";
15import { join, extname } from "node:path";
16import puppeteer from "puppeteer-core";
18const root = new URL("../dist", import.meta.url).pathname;
19const types = {
20 ".html": "text/html",
21 ".js": "text/javascript",
22 ".css": "text/css",
23 ".wasm": "application/wasm",
24 ".json": "application/json",
25};
27const server = createServer(async (req, res) => {
28 const path = req.url === "/" ? "/index.html" : (req.url ?? "/").split("?")[0];
29 try {
30 const data = await readFile(join(root, path));
31 res.writeHead(200, {
32 "content-type": types[extname(path)] ?? "application/octet-stream",
33 });
34 res.end(data);
35 } catch {
36 res.writeHead(404);
37 res.end("not found");
38 }
39});
40await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
41const port = server.address().port;
43// Hardware WebGPU first, then the software adapter, as turing-surface does.
44const flagSets = [
45 ["--headless=new", "--no-sandbox", "--enable-unsafe-webgpu", "--enable-features=Vulkan"],
46 [
47 "--headless=new",
48 "--no-sandbox",
49 "--enable-unsafe-webgpu",
50 "--use-webgpu-adapter=swiftshader",
51 "--enable-unsafe-swiftshader",
52 ],
53];
55/** f32 puts mfs-gpu's error on star-hard in this range; the point of the
56 * check is that it computed at all, so the window is generous. */
57const MIN_ERROR = 1e-9;
58const MAX_ERROR = 1e-1;
60let outcome = null;
61for (const flags of flagSets) {
62 const browser = await puppeteer.launch({
63 executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome",
64 args: [...flags],
65 protocolTimeout: 600_000,
66 });
67 try {
68 const page = await browser.newPage();
69 const pageErrors = [];
70 page.on("pageerror", (err) => pageErrors.push(String(err)));
71 await page.goto(`http://127.0.0.1:${port}/#/problem/laplace-dirichlet-2d`, {
72 waitUntil: "networkidle2",
73 });
74 await page.waitForFunction(
75 () => document.body.innerText.includes("Work-precision results"),
76 { timeout: 60_000 }
77 );
79 const gpu = await page.evaluate(() => "gpu" in navigator);
80 if (!gpu) throw new Error("navigator.gpu absent");
82 // The Solution section's solver select, switched to mfs-gpu.
83 const picked = await page.evaluate(() => {
84 for (const sel of document.querySelectorAll("select")) {
85 if ([...sel.options].some((o) => o.value === "mfs-gpu")) {
86 sel.value = "mfs-gpu";
87 sel.dispatchEvent(new Event("change", { bubbles: true }));
88 return true;
89 }
90 }
91 return false;
92 });
93 if (!picked) throw new Error("no solver select offering mfs-gpu");
95 const buttons = await page.$$("button");
96 let compute = null;
97 for (const b of buttons) {
98 const t = await b.evaluate((el) => el.textContent);
99 if (t && t.includes("Compute in this browser")) compute = b;
100 }
101 if (!compute) throw new Error("compute button not found");
102 await compute.click();
104 const text = await page.waitForFunction(
105 () => {
106 const t = document.body.innerText;
107 const m = /rel max error[^0-9eE.+-]*([0-9.]+e[+-][0-9]+)/i.exec(t);
108 return m ? m[1] : false;
109 },
110 { timeout: 300_000 }
111 );
112 const relMax = Number(await text.jsonValue());
113 if (pageErrors.length > 0) throw new Error(`page errors: ${pageErrors[0]}`);
114 outcome = { flags, relMax };
115 } catch (e) {
116 console.error(`run with [${flags.join(" ")}] failed: ${e.message}`);
117 } finally {
118 await browser.close();
119 }
120 if (outcome) break;
121 console.log("retrying with the software adapter…");
123server.close();
125if (!outcome) {
126 console.error("check-gpu: no WebGPU device in this browser; not run");
127 process.exit(2);
129if (!(outcome.relMax > MIN_ERROR && outcome.relMax < MAX_ERROR)) {
130 console.error(
131 `check-gpu: FAIL, mfs-gpu in the browser reported relMax ${outcome.relMax}, ` +
132 `outside [${MIN_ERROR}, ${MAX_ERROR}]`
133 );
134 process.exit(1);
136console.log(
137 `check-gpu: mfs-gpu ran in the browser (relMax ${outcome.relMax.toExponential(2)})`
138);