/ proof-of-concept / steelsim41
Sign in
proof-of-concept / steelsim41
Add a throughput benchmark script
Jeremy Magland <jeremy.magland@gmail.com> committed commit 19b58d01f3a9 parent 6edbaa3 Browse files
3 changed files+107−1
README.mdmodified+4−0View file
@@ -55,6 +55,10 @@ The default grid spacing of 8 mm keeps the simulation interactive but lowers the
5555 - `tests/design.test.ts` checks the design script: compilation, the checks and their messages, the helpers, the mask of a disc with a hole, the removal of the mean plane, the mount patch, and that every preset evaluates on the default grid.
5656 - `tests/gpu.test.ts` serves the app with Vite and drives a headless Chromium through Playwright. It compares one step of the WGSL kernel with the float64 reference on flat, bowed, tapered, mounted, and elliptical presets (`?selftest=1` in the browser shows the same numbers), runs a short strike on a free plate and on a mounted cymbal to completion, and checks that a script with an error is reported and cannot start. It needs Playwright's Chromium (`npx playwright install chromium`) and a machine whose Chromium has WebGPU; `npm run test:cpu` runs only the first two groups.
5757
58+## Benchmark
59+
60+`npm run bench` runs the crash preset in headless Chromium at several grid spacings and with several concurrent runs and prints steps per second and the wall time per second of audio (`--quick` for a short version, `--browsers N` to spread the concurrent runs over N Chromium instances; on Node 22 add `--experimental-strip-types`). The kernel is latency-bound on grids of a few thousand nodes: an RTX 6000 Ada runs the 16 inch crash at 8 mm at about 140k steps per second, 2.2 times an Intel Iris Xe, and at 4 mm at about the same rate, 5 times the integrated GPU; concurrent runs on one card add only about 40 percent in total, since the card's dispatch rate is the limit.
61+
5862 ## Layout
5963
6064 - `src/params.ts`: simulation parameters, grid and mask, the plate as evaluated on the grid, material functions, patches of the strike and the mount, grid dispersion estimate.
package.jsonmodified+2−1View file
@@ -10,7 +10,8 @@
1010 "preview": "vite preview",
1111 "test": "node --test tests/*.test.ts",
1212 "test:cpu": "node --test tests/cpuRef.test.ts tests/design.test.ts",
13- "test:gpu": "node --test tests/gpu.test.ts"
13+ "test:gpu": "node --test tests/gpu.test.ts",
14+ "bench": "node scripts/bench.ts"
1415 },
1516 "devDependencies": {
1617 "@types/node": "^24.0.0",
scripts/bench.tsadded+101−0View file
@@ -0,0 +1,101 @@
1+// Throughput benchmark: serves the app with Vite, opens it in a headless
2+// Chromium with WebGPU, and runs the crash preset at several grid spacings,
3+// alone and with several concurrent runs, reporting steps per second and
4+// the wall time per second of audio. The numbers describe the machine's
5+// GPU as seen through Chromium; the kernel is latency-bound on small grids,
6+// so concurrency and finer grids are the interesting axes.
7+//
8+// node scripts/bench.ts [--quick] [--preset crash-16] [--gpu-flags "..."] [--browsers N]
9+//
10+// --browsers N spreads the concurrent runs over N independent Chromium
11+// instances (each with its own GPU process), which tells whether a plateau
12+// in total throughput is the GPU's or the browser's.
13+
14+import { fileURLToPath } from "node:url";
15+import { dirname, join } from "node:path";
16+import { createServer } from "vite";
17+import { chromium, type Browser } from "playwright";
18+
19+const root = join(dirname(fileURLToPath(import.meta.url)), "..");
20+const args = process.argv.slice(2);
21+const quick = args.includes("--quick");
22+const preset = args.includes("--preset") ? args[args.indexOf("--preset") + 1] : "crash-16";
23+const extraFlags = args.includes("--gpu-flags") ? args[args.indexOf("--gpu-flags") + 1].split(" ") : [];
24+const nBrowsers = args.includes("--browsers") ? parseInt(args[args.indexOf("--browsers") + 1]) : 1;
25+
26+const ARGS = ["--enable-unsafe-webgpu", "--enable-features=Vulkan", "--use-angle=vulkan", "--ignore-gpu-blocklist", "--no-sandbox", ...extraFlags];
27+
28+interface Status { phase: string; totalSteps: number; dt: number; nx: number; ny: number; wmax: number; peakPressure: number }
29+
30+interface Case { spacing: number; duration: number; concurrency: number }
31+
32+const concOnly = args.includes("--conc");
33+const cases: Case[] = concOnly
34+ ? [{ spacing: 0.008, duration: 2, concurrency: 4 }, { spacing: 0.008, duration: 2, concurrency: 8 }]
35+ : quick
36+ ? [{ spacing: 0.008, duration: 1, concurrency: 1 }, { spacing: 0.004, duration: 0.25, concurrency: 1 }, { spacing: 0.008, duration: 1, concurrency: 4 }]
37+ : [
38+ { spacing: 0.008, duration: 2, concurrency: 1 },
39+ { spacing: 0.006, duration: 1, concurrency: 1 },
40+ { spacing: 0.004, duration: 0.5, concurrency: 1 },
41+ { spacing: 0.003, duration: 0.25, concurrency: 1 },
42+ { spacing: 0.008, duration: 2, concurrency: 2 },
43+ { spacing: 0.008, duration: 2, concurrency: 4 },
44+ { spacing: 0.008, duration: 2, concurrency: 8 },
45+ { spacing: 0.004, duration: 0.5, concurrency: 4 },
46+ ];
47+
48+/** Runs one simulation to completion in a fresh page; resolves with its status and wall time. */
49+async function runOne(browser: Browser, url: string, c: Case): Promise<{ st: Status; wall: number; adapter: string }> {
50+ const page = await browser.newPage();
51+ const errors: string[] = [];
52+ page.on("pageerror", (e) => errors.push(e.message));
53+ page.on("console", (m) => { if (m.type() === "error") errors.push(m.text()); });
54+ await page.goto(`${url}?autostart=1&preset=${preset}&spacing=${c.spacing}&duration=${c.duration}`);
55+ const t0 = performance.now();
56+ let st: Status | null = null;
57+ for (let i = 0; i < 4000; i++) {
58+ await page.waitForTimeout(100);
59+ st = await page.evaluate<Status | null>("window.steelsim ? window.steelsim.status() : null");
60+ const noGpu = await page.evaluate<string | null>("window.steelsim ? window.steelsim.noGpu() : null");
61+ if (noGpu) throw new Error(noGpu);
62+ if (st && st.phase === "done") break;
63+ }
64+ const wall = (performance.now() - t0) / 1000;
65+ const adapter = await page.evaluate<string>(`navigator.gpu.requestAdapter().then(a => a ? (a.info ? a.info.vendor + " " + a.info.architecture + " " + a.info.device + " " + a.info.description : "adapter") : "none")`);
66+ await page.close();
67+ if (errors.length) throw new Error(errors.join("\n"));
68+ if (!st || st.phase !== "done") throw new Error("did not finish");
69+ return { st, wall, adapter };
70+}
71+
72+async function main(): Promise<void> {
73+ const server = await createServer({ root, logLevel: "silent", server: { port: 0, strictPort: false } });
74+ await server.listen();
75+ const url = server.resolvedUrls!.local[0];
76+ const browsers = await Promise.all(Array.from({ length: nBrowsers }, () => chromium.launch({ headless: true, args: ARGS })));
77+ try {
78+ let adapter = "";
79+ console.log(`preset ${preset}, ${nBrowsers} browser instance${nBrowsers > 1 ? "s" : ""}`);
80+ console.log("spacing grid dt conc audio s wall s ksteps/s (each) ksteps/s (total) wall per audio s (each)");
81+ for (const c of cases) {
82+ const results = await Promise.all(Array.from({ length: c.concurrency }, (_, k) => runOne(browsers[k % nBrowsers], url, c)));
83+ adapter = results[0].adapter;
84+ const wall = Math.max(...results.map((r) => r.wall));
85+ const steps = results[0].st.totalSteps;
86+ const each = steps / wall / 1e3;
87+ const total = (steps * c.concurrency) / wall / 1e3;
88+ const st = results[0].st;
89+ console.log(
90+ `${(c.spacing * 1e3).toFixed(0).padStart(4)} mm ${`${st.nx}x${st.ny}`.padEnd(9)} ${(st.dt * 1e6).toFixed(2).padStart(5)} us ${String(c.concurrency).padStart(3)} ${c.duration.toFixed(2).padStart(5)} ${wall.toFixed(1).padStart(6)} ${each.toFixed(0).padStart(8)} ${total.toFixed(0).padStart(8)} ${(wall / c.duration).toFixed(1).padStart(6)}x`,
91+ );
92+ if (!isFinite(st.wmax) || st.wmax > 0.05) console.log(" (diverged)");
93+ }
94+ console.log(`adapter: ${adapter}`);
95+ } finally {
96+ for (const b of browsers) await b.close();
97+ await server.close();
98+ }
99+}
100+
101+void main();
↑↓ move↵openescclose