Skip to content

Commit 9f5d85d

Browse files
committed
feat(bb): BrowserStack-driven MSM-webgpu bench harness
Adds a remote-device bench loop for the MSM-webgpu dev pages so the tree-reduce work can validate against real WebGPU hardware (Apple M2, Snapdragon 8 Elite, Tensor G4) from a workstation without a local GPU. - vite.config.ts: results/progress POST endpoints write JSONL to files named by MSM_WEBGPU_RESULTS_FILE / MSM_WEBGPU_PROGRESS_FILE; allow .trycloudflare.com so the dev server is reachable via Cloudflare Quick Tunnel. - results_post.ts: tiny in-page client used by bench/sanity pages to POST progress + final-state payloads (no keepalive — the page is alive when the bench completes). - bench-batch-affine.ts: post per-batch progress and a terminal done/error row. - scripts/run-browserstack.mjs: spawn vite + cloudflared, drive a BS worker through the REST API, watchdog-tail the JSONL with first-progress / stall / deadline budgets. - scripts/bs-targets.mjs: macOS Sequoia Chrome, S25 Ultra, Pixel 9 Pro XL presets (WebGPU stable). iPhone 15 Pro listed but flagged as needs-iOS-26-or-newer. Validated against macOS Sequoia Chrome 148 (Apple M2, hc=8) on ?total=8192&sizes=64,256,1024: B=64 ns/pair=305.2 median=2.500ms B=256 ns/pair=146.5 median=1.200ms B=1024 ns/pair=219.7 median=1.800ms
1 parent d3fdb6b commit 9f5d85d

5 files changed

Lines changed: 771 additions & 8 deletions

File tree

barretenberg/ts/dev/msm-webgpu/bench-batch-affine.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { BN254_CURVE_CONFIG } from '../../src/msm_webgpu/cuzk/curve_config.js';
1313
import { get_device } from '../../src/msm_webgpu/cuzk/gpu.js';
1414
import { compute_misc_params } from '../../src/msm_webgpu/cuzk/utils.js';
1515
import { BN254_BASE_FIELD } from '../../src/msm_webgpu/cuzk/bn254.js';
16+
import { makeResultsClient } from './results_post.js';
1617

1718
// TOTAL_PAIRS defaults to 65536 (2^16). Overridable via ?total=N for
1819
// smoke-tests on small slices (NOT used in the headline sweep, but
@@ -117,6 +118,21 @@ const benchState: BenchState = {
117118
};
118119
(window as unknown as { __bench: BenchState }).__bench = benchState;
119120

121+
const resultsClient = makeResultsClient({ page: 'bench-batch-affine' });
122+
(window as unknown as { __runId: string }).__runId = resultsClient.runId;
123+
124+
async function postFinal(): Promise<void> {
125+
await resultsClient.postResults({
126+
state: benchState.state,
127+
params: benchState.params,
128+
results: benchState.results,
129+
error: benchState.error,
130+
log: benchState.log,
131+
userAgent: navigator.userAgent,
132+
hardwareConcurrency: navigator.hardwareConcurrency,
133+
});
134+
}
135+
120136
const $log = document.getElementById('log') as HTMLDivElement;
121137
function log(level: 'info' | 'ok' | 'err' | 'warn', msg: string) {
122138
const cls = level === 'ok' ? 'ok' : level === 'err' ? 'err' : level === 'warn' ? 'warn' : '';
@@ -396,6 +412,7 @@ async function main() {
396412
try {
397413
const r = await runOne(device, sm, B, params.reps, R, p);
398414
benchState.results.push(r);
415+
resultsClient.postProgress({ kind: 'batch_done', batch_size: B, median_ms: r.median_ms, ns_per_pair: r.ns_per_pair });
399416
} catch (e) {
400417
const msg = e instanceof Error ? e.message : String(e);
401418
log('err', `B=${B} failed: ${msg} — STOPPING sweep at first failure`);
@@ -415,9 +432,13 @@ async function main() {
415432
}
416433
}
417434

418-
main().catch(e => {
419-
const msg = e instanceof Error ? e.message : String(e);
420-
log('err', `unhandled: ${msg}`);
421-
benchState.state = 'error';
422-
benchState.error = msg;
423-
});
435+
main()
436+
.catch(e => {
437+
const msg = e instanceof Error ? e.message : String(e);
438+
log('err', `unhandled: ${msg}`);
439+
benchState.state = 'error';
440+
benchState.error = msg;
441+
})
442+
.finally(() => {
443+
postFinal().catch(() => {});
444+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Helper used by the dev pages to POST progress and final-result
2+
// payloads back to the local dev server's `/progress` and `/results`
3+
// endpoints (see `vite.config.ts`). The BrowserStack runner tails the
4+
// resulting JSONL to apply stall/deadline watchdogs without polling
5+
// `window.__bench` / `window.__sanity` from outside the browser.
6+
//
7+
// The page can run untouched (no harness) — every call is fire-and-forget
8+
// and swallows errors so a missing endpoint never blocks the bench loop.
9+
10+
export interface ResultsClientConfig {
11+
// Page identifier — e.g. `bench-batch-affine`, `sanity`. Becomes the
12+
// `page` field on every JSONL row.
13+
page: string;
14+
// Optional run id. Defaults to a random hex string per page load.
15+
runId?: string;
16+
}
17+
18+
function makeRunId(): string {
19+
return Array.from(crypto.getRandomValues(new Uint8Array(8)))
20+
.map(b => b.toString(16).padStart(2, "0"))
21+
.join("");
22+
}
23+
24+
export function makeResultsClient(cfg: ResultsClientConfig): {
25+
postProgress(event: Record<string, unknown>): void;
26+
postResults(payload: Record<string, unknown>): Promise<void>;
27+
readonly runId: string;
28+
} {
29+
const runId = cfg.runId ?? makeRunId();
30+
const base = `${window.location.origin}`;
31+
32+
function tryPost(path: string, payload: unknown): Promise<Response> | null {
33+
try {
34+
return fetch(base + path, {
35+
method: "POST",
36+
headers: { "Content-Type": "application/json" },
37+
body: JSON.stringify({ page: cfg.page, runId, ...((payload as object) ?? {}) }),
38+
});
39+
} catch {
40+
return null;
41+
}
42+
}
43+
44+
return {
45+
runId,
46+
postProgress(event) {
47+
const p = tryPost("/progress", { event });
48+
if (p) p.catch(() => {});
49+
},
50+
async postResults(payload) {
51+
console.log(`[results_post] POST /results runId=${runId} bytes≈${JSON.stringify(payload).length}`);
52+
const p = tryPost("/results", payload);
53+
if (p) {
54+
try {
55+
const r = await p;
56+
console.log(`[results_post] /results -> HTTP ${r.status}`);
57+
} catch (e) {
58+
console.log(`[results_post] /results threw: ${(e as Error).message}`);
59+
}
60+
} else {
61+
console.log(`[results_post] /results: tryPost returned null`);
62+
}
63+
},
64+
};
65+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// BrowserStack capability presets for WebGPU MSM bench runs. Mirrors the
2+
// shape used by `barretenberg/wasm-bench/scripts/run-browserstack.mjs` so
3+
// the two harnesses share a target vocabulary (`--target macos`,
4+
// `--target s25-ultra`, …).
5+
//
6+
// WebGPU support matrix (as of 2026-05-17):
7+
// - macOS Sequoia · Chrome 148: WebGPU GA since Chrome 113.
8+
// - Android Chrome (S25 Ultra, Pixel 9 Pro XL): WebGPU shipped in stable
9+
// Chrome on Android in 2024. Latest = "for_real" newest on the device.
10+
// - iOS Safari 26+ on iPhone 15 Pro: WebGPU shipped in Safari 26.0. iOS 17
11+
// Safari does NOT expose `navigator.gpu` — BrowserStack must have a 26+
12+
// image for that target to be usable. We expose the preset but flag it.
13+
14+
export const TARGETS = {
15+
macos: {
16+
label: "macOS Sequoia · Chrome 148",
17+
worker: {
18+
browser: "chrome",
19+
browser_version: "148.0",
20+
os: "OS X",
21+
os_version: "Sequoia",
22+
resolution: "1920x1080",
23+
},
24+
webgpu: "supported",
25+
firstProgressMs: 90_000,
26+
notes: "Reference desktop. Apple M2 base.",
27+
},
28+
"s25-ultra": {
29+
label: "Galaxy S25 Ultra · Android 15 · Chrome",
30+
worker: {
31+
browser: "android",
32+
browserName: "chrome",
33+
os: "android",
34+
os_version: "15.0",
35+
device: "Samsung Galaxy S25 Ultra",
36+
real_mobile: "true",
37+
},
38+
webgpu: "supported",
39+
firstProgressMs: 150_000,
40+
notes: "Snapdragon 8 Elite.",
41+
},
42+
"pixel-9-pro-xl": {
43+
label: "Pixel 9 Pro XL · Android 15 · Chrome",
44+
worker: {
45+
browser: "android",
46+
browserName: "chrome",
47+
os: "android",
48+
os_version: "15.0",
49+
device: "Google Pixel 9 Pro XL",
50+
real_mobile: "true",
51+
},
52+
webgpu: "supported",
53+
firstProgressMs: 150_000,
54+
notes: "Tensor G4.",
55+
},
56+
"iphone-15-pro": {
57+
label: "iPhone 15 Pro · iOS 26 · Safari",
58+
worker: {
59+
browser: "iphone",
60+
browserName: "safari",
61+
os: "ios",
62+
os_version: "26",
63+
device: "iPhone 15 Pro",
64+
real_mobile: "true",
65+
},
66+
webgpu: "needs-ios-26-or-newer",
67+
firstProgressMs: 180_000,
68+
notes:
69+
"Requires iOS 26+ Safari for WebGPU. BS may not have a 26 image yet.",
70+
},
71+
};
72+
73+
export function listTargets() {
74+
return Object.entries(TARGETS).map(([k, v]) => ({
75+
key: k,
76+
label: v.label,
77+
webgpu: v.webgpu,
78+
notes: v.notes ?? "",
79+
}));
80+
}
81+
82+
export function buildWorkerBody(targetKey, url, { name, build, timeoutSec }) {
83+
const t = TARGETS[targetKey];
84+
if (!t) {
85+
throw new Error(
86+
`unknown --target ${targetKey}. Known: ${Object.keys(TARGETS).join(", ")}`,
87+
);
88+
}
89+
return {
90+
...t.worker,
91+
url,
92+
timeout: timeoutSec,
93+
name,
94+
build,
95+
};
96+
}

0 commit comments

Comments
 (0)