concept-collection / matmul-bench
matmul-bench: browser GEMM benchmark (JS, WebGPU, custom C WASM, libFLAME/BLIS WASM)
- Plain JS/TS, WebGPU (f32), custom C blocked+SIMD (single + threaded), libFLAME/BLIS compiled to WASM (single + threaded), and a native OpenBLAS reference table. - Threaded methods use WASM pthreads; cross-origin isolation via a vendored coi-serviceworker so it works on GitHub Pages. - CI builds the custom C kernels with emsdk; the BLIS WASM is committed as a vendored binary (it links prebuilt libraries not buildable on the runner).
Jeremy Magland <jmagland@flatironinstitute.org> committed commit 666db680aa5e Browse files
42 changed files+4188−0
.github/workflows/deploy.ymladded+64−0View file
@@ -0,0 +1,64 @@
1+name: Deploy to GitHub Pages
2+
3+on:
4+ push:
5+ branches:
6+ - main
7+ workflow_dispatch:
8+
9+permissions:
10+ contents: read
11+ pages: write
12+ id-token: write
13+
14+concurrency:
15+ group: "pages"
16+ cancel-in-progress: false
17+
18+jobs:
19+ build:
20+ runs-on: ubuntu-latest
21+ steps:
22+ - name: Checkout
23+ uses: actions/checkout@v4
24+
25+ - name: Setup Node
26+ uses: actions/setup-node@v4
27+ with:
28+ node-version: '20'
29+ cache: 'npm'
30+
31+ - name: Install dependencies
32+ run: npm ci
33+
34+ - name: Setup emsdk
35+ uses: mymindstorm/setup-emsdk@v14
36+ with:
37+ version: 5.0.4
38+ actions-cache-folder: 'emsdk-cache'
39+
40+ # Builds the custom C kernels (wasm/dist/matmul.* + public/matmul/matmul_mt.*).
41+ # The libFLAME/BLIS modules (public/matmul/matmul_blis_*) are committed
42+ # vendored binaries — they link prebuilt libraries not available here — so
43+ # nothing outside this repo is needed to produce the full site.
44+ - name: Build custom wasm kernels
45+ run: bash wasm/build-wasm.sh
46+
47+ - name: Build
48+ run: npm run build
49+
50+ - name: Upload artifact
51+ uses: actions/upload-pages-artifact@v3
52+ with:
53+ path: ./dist
54+
55+ deploy:
56+ environment:
57+ name: github-pages
58+ url: ${{ steps.deployment.outputs.page_url }}
59+ runs-on: ubuntu-latest
60+ needs: build
61+ steps:
62+ - name: Deploy to GitHub Pages
63+ id: deployment
64+ uses: actions/deploy-pages@v4
.gitignoreadded+33−0View file
@@ -0,0 +1,33 @@
1+# Logs
2+logs
3+*.log
4+npm-debug.log*
5+yarn-debug.log*
6+yarn-error.log*
7+pnpm-debug.log*
8+lerna-debug.log*
9+
10+node_modules
11+dist
12+dist-ssr
13+*.local
14+
15+# built artifacts (regenerated by wasm/build-wasm.sh / native/build.sh)
16+wasm/dist
17+native/bench_native
18+# CI-built threaded custom module (from wasm/matmul.c + emsdk, like wasm/dist).
19+# The BLIS modules in public/matmul/ (matmul_blis_*) ARE committed — they link
20+# prebuilt .a files CI can't rebuild — so don't ignore the whole dir.
21+public/matmul/matmul_mt.js
22+public/matmul/matmul_mt.wasm
23+
24+# Editor directories and files
25+.vscode/*
26+!.vscode/extensions.json
27+.idea
28+.DS_Store
29+*.suo
30+*.ntvs*
31+*.njsproj
32+*.sln
33+*.sw?
.oxlintrc.jsonadded+9−0View file
@@ -0,0 +1,9 @@
1+{
2+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3+ "plugins": ["react", "typescript", "oxc"],
4+ "ignorePatterns": ["public/matmul/matmul_*.js", "wasm/dist/**", "public/coi-serviceworker.js"],
5+ "rules": {
6+ "react/rules-of-hooks": "error",
7+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
8+ }
9+}
CLAUDE.mdadded+96−0View file
@@ -0,0 +1,96 @@
1+# CLAUDE.md
2+
3+Tips for future agents working in this repo.
4+
5+## Architecture
6+
7+```
8+wasm/matmul.c naive + blocked/SIMD (f64x2) kernels, plus a pthreads
9+ matmul_blocked_mt (rows split across threads, -DMATMUL_MT).
10+ wasm/build-wasm.sh (emsdk) builds it twice:
11+ - st ES module -> wasm/dist/matmul.{js,wasm}
12+ - threaded -> public/matmul/matmul_mt.{js,wasm}
13+blis/matmul_blis.c thin wrapper over libFLAME's dgemm_ (BLIS-backed); built
14+ st + mt (pthreads) by blis/build-blis-wasm.sh, linking the
15+ prebuilt .a libraries from the sibling libflame2wasm
16+ checkout (../../../libflame2wasm), into
17+ public/matmul/matmul_blis_{st,mt}.{js,wasm} (committed)
18+native/bench_native.c same style dgemm benchmark, linked against OpenBLAS,
19+ run standalone outside the browser
20+src/methods/ one file per method, all implementing the MatmulMethod
21+ interface in types.ts (id, precision, available(), run?())
22+src/methods/benchWorker.ts + workerClient.ts
23+ JS and both non-threaded custom WASM kernels run inside a
24+ Vite module Web Worker — naive JS/WASM at n=2048 take ~10s
25+ and would freeze the tab on the main thread. WebGPU runs on
26+ the main thread (async). The worker regenerates inputs from
27+ (n, seed) rather than receiving them over postMessage.
28+src/methods/threadedClient.ts + public/matmul/worker.js
29+ ALL pthread-capable modules (threaded custom C, BLIS st +
30+ mt) run in ONE shared classic worker (not a Vite module
31+ worker) — threaded builds spawn pthread workers from it
32+ (nested workers), and importScripts of the emscripten glue
33+ is the proven path (mirrors libflame2wasm's bench_worker.js).
34+ worker.js has a kind->module REGISTRY; every module exports
35+ a (a,b,c,n,nthreads) function (st builds ignore nthreads).
36+ Served verbatim from public/, out of Vite's module graph.
37+src/components/ BenchmarkRunner (orchestrates runs, thread selector) +
38+ ResultsTable
39+src/data/nativeReference.ts hardcoded numbers from native/bench_native,
40+ hand-copied in — no runtime fetch
41+public/coi-serviceworker.js vendored (gzuidhof, MIT) — adds COOP/COEP so
42+ SharedArrayBuffer works on GitHub Pages (no header control)
43+```
44+
45+## Key gotchas
46+
47+- **WASM .wasm URL resolution.** `wasm/dist/matmul.js` (emscripten glue) is
48+ imported directly; its sibling `matmul.wasm` is resolved via Vite's `?url`
49+ import and passed through `locateFile` — same pattern qhull-wasm-demo uses
50+ for `qhull-wasm/dist/qhull.wasm`. Don't rely on emscripten's default
51+ `import.meta.url`-relative lookup working under Vite's bundler.
52+- **Two threaded builds, one classic worker.** The threaded custom kernel
53+ (`matmul_mt`, from `wasm/matmul.c` with `-pthread -DMATMUL_MT`) and the
54+ threaded BLIS build (`matmul_blis_mt`) both use pthreads/SharedArrayBuffer
55+ and both dispatch through `public/matmul/worker.js`. The non-threaded custom
56+ kernels (`matmul.c` st build) have no `-pthread` and run in the Vite module
57+ worker instead.
58+- **Cross-origin isolation for threaded methods.** `matmul_mt` and
59+ `matmul_blis_mt` need `crossOriginIsolated === true` (COOP: same-origin,
60+ COEP: require-corp). Vite dev/preview set these headers (see vite.config.ts).
61+ GitHub Pages can't set headers, so `public/coi-serviceworker.js` installs
62+ them via a service worker (page reloads once on first visit to gain control).
63+ Their `available()` gates on `crossOriginIsolated`; without it the cell = n/a.
64+- **What's committed vs CI-built under public/matmul/.** The BLIS modules
65+ (`matmul_blis_{st,mt}.*`) are committed — `blis/build-blis-wasm.sh` links the
66+ ~30 MB of prebuilt `.a` files in `../../../libflame2wasm`, which aren't in
67+ this repo and can't be rebuilt in CI. The threaded custom module
68+ (`matmul_mt.*`) is gitignored and CI-built (only needs `matmul.c` + emsdk,
69+ like `wasm/dist`). `worker.js` is a committed source file. Rebuild BLIS
70+ locally after changing `matmul_blis.c` or the upstream libs.
71+- **Row-major via operand swap.** dgemm_ is column-major; `matmul_blis` calls
72+ it with operands swapped — `dgemm_(B, A)` computes row-major C = A*B in the
73+ same flat buffer — so BLIS `c[0]` matches the other (row-major) methods.
74+- **Deterministic seeded inputs, not transferred arrays.** Every method for
75+ a given `n` multiplies bit-identical A/B (from `generateMatrix(n, seed)`);
76+ the worker regenerates them from the same seed rather than receiving the
77+ arrays over `postMessage`, since structured-cloning multi-megabyte
78+ `Float64Array`s per run would be slower than regenerating.
79+- Rebuilding `wasm/dist/*` requires emsdk on PATH or at `~/emsdk`
80+ (`wasm/build-wasm.sh` sources `~/emsdk/emsdk_env.sh` if `emcc` isn't found).
81+ CI installs it via `mymindstorm/setup-emsdk`.
82+
83+## Testing
84+
85+- `npm run dev`, click through each method's run button at a small size
86+ (n=128) first and check the cross-check panel — the f64 methods (JS, the
87+ three WASM kernels, both BLIS builds) should agree to ~1e-9, WebGPU (f32)
88+ should be close but not identical. The "cross-origin isolated ✓" chip must
89+ be green for the threaded methods to be available.
90+- `wasm/build-wasm.sh && npm run build` to verify the CI path locally
91+ (build-wasm.sh emits both the st module and the threaded matmul_mt).
92+- `blis/build-blis-wasm.sh` rebuilds the committed BLIS artifacts (needs the
93+ sibling libflame2wasm checkout + emsdk).
94+- `native/build.sh && native/bench_native` regenerates the values pasted into
95+ `src/data/nativeReference.ts` (no automated round-trip — hand-copy after
96+ running).
README.mdadded+50−0View file
@@ -0,0 +1,50 @@
1+# matmul-bench
2+
3+Compares matrix-matrix multiply (GEMM) performance across implementations
4+running in the browser, plus a native reference measured outside it.
5+
6+**Live:** https://concept-collection.github.io/matmul-bench/
7+
8+Methods:
9+
10+- **JavaScript/TypeScript** — plain triple loop over `Float64Array` (f64), no
11+ special-casing. The "no special effort" baseline.
12+- **WebGPU** — a tiled compute shader (f32; WGSL has no double precision).
13+- **WASM from custom C** — naive, blocked+SIMD (`f64x2`, `-msimd128`), and a
14+ threaded (WASM pthreads) version of the blocked+SIMD kernel, built with
15+ emscripten (f64). The threaded one needs cross-origin isolation.
16+- **libFLAME/BLIS in WASM** — a real BLAS (`dgemm` via libFLAME + BLIS)
17+ compiled to WebAssembly, single-threaded and multi-threaded (WASM pthreads),
18+ from [libflame2wasm](https://github.com/magland/libflame2wasm) (f64). The
19+ threaded build needs cross-origin isolation (SharedArrayBuffer), supplied on
20+ GitHub Pages by a vendored service worker.
21+- **Native LAPACK/OpenBLAS** — a fixed reference table from `dgemm` run
22+ outside the browser (`native/`).
23+
24+## Develop
25+
26+```bash
27+npm install
28+npm run dev # local dev server (sends COOP/COEP for threaded methods)
29+wasm/build-wasm.sh # rebuild wasm/dist/matmul.* + public/matmul/matmul_mt.*
30+ # (needs emsdk)
31+blis/build-blis-wasm.sh # rebuild public/matmul/matmul_blis_{st,mt}.* (needs emsdk
32+ # + the prebuilt .a files from a sibling libflame2wasm)
33+npm run build # tsc -b && vite build
34+```
35+
36+The libFLAME/BLIS WASM artifacts under `public/matmul/` are committed (they link
37+~30 MB of prebuilt static libraries that live in a separate
38+[libflame2wasm](https://github.com/magland/libflame2wasm) checkout), so the
39+GitHub Pages build doesn't rebuild them. The threaded custom module
40+(`public/matmul/matmul_mt.*`) is built in CI from `wasm/matmul.c`.
41+
42+## Native reference
43+
44+```bash
45+native/build.sh
46+OPENBLAS_NUM_THREADS=1 native/bench_native 128 256 512 1024 2048
47+OPENBLAS_NUM_THREADS=12 native/bench_native 128 256 512 1024 2048
48+```
49+
50+Hand-copy the results into `src/data/nativeReference.ts`.
blis/build-blis-wasm.shadded+55−0View file
@@ -0,0 +1,55 @@
1+#!/usr/bin/env bash
2+# Builds matmul_blis.c to public/matmul/matmul_blis_{st,mt}.{js,wasm}, linking
3+# the prebuilt libflame + BLIS WASM static libraries from ../../../libflame2wasm.
4+#
5+# Those .a files come from libflame2wasm's own build scripts:
6+# (in ../../libflame2wasm) ./build-wasm.sh -> install/lib/libflame.a
7+# PTHREAD=1 ./build-wasm.sh -> install/lib/libflame-mt.a
8+# ./build-blis-wasm.sh -> install/lib/libblis-st.a
9+# THREADING=pthreads ./build-blis-wasm.sh -> blis/lib/generic/libblis.a
10+#
11+# The outputs are committed to the repo (see .gitignore) so GitHub Pages deploy
12+# doesn't need to rebuild ~30 MB of BLIS/libflame in CI.
13+#
14+# Requires: emsdk (expected at ~/emsdk, or already on PATH).
15+set -euo pipefail
16+
17+cd "$(dirname "$0")"
18+LIBFLAME=${LIBFLAME:-../../../libflame2wasm}
19+
20+if [ ! -f "$LIBFLAME/install/lib/libflame.a" ]; then
21+ echo "error: prebuilt libraries not found under $LIBFLAME (set LIBFLAME=...)" >&2
22+ exit 1
23+fi
24+
25+if ! command -v emcc >/dev/null 2>&1; then
26+ source ~/emsdk/emsdk_env.sh
27+fi
28+
29+OUT=../public/matmul
30+mkdir -p "$OUT"
31+
32+COMMON="-O2 -sMODULARIZE -sENVIRONMENT=web,worker,node \
33+ -sEXPORTED_FUNCTIONS=_matmul_blis,_malloc,_free \
34+ -sEXPORTED_RUNTIME_METHODS=HEAPF64"
35+
36+# Single-threaded: BLIS(st) linked first so its BLAS symbols win over
37+# libflame's f2c reference BLAS. No threads -> no SharedArrayBuffer needed.
38+emcc $COMMON matmul_blis.c \
39+ -sEXPORT_NAME=createMatmulBlisST \
40+ -sALLOW_MEMORY_GROWTH \
41+ "$LIBFLAME/install/lib/libblis-st.a" \
42+ "$LIBFLAME/install/lib/libflame.a" \
43+ -o "$OUT/matmul_blis_st.js"
44+
45+# Threaded: everything compiled with -pthread; fixed shared memory (growth is
46+# costly with shared memory). Pool must cover max threads + margin.
47+emcc $COMMON -pthread -DBLIS_MT matmul_blis.c \
48+ -sEXPORT_NAME=createMatmulBlisMT \
49+ -sPTHREAD_POOL_SIZE=20 \
50+ -sINITIAL_MEMORY=768MB \
51+ "$LIBFLAME/blis/lib/generic/libblis.a" \
52+ "$LIBFLAME/install/lib/libflame-mt.a" \
53+ -o "$OUT/matmul_blis_mt.js"
54+
55+echo "Built public/matmul/matmul_blis_{st,mt}.{js,wasm}"
blis/matmul_blis.cadded+35−0View file
@@ -0,0 +1,35 @@
1+/*
2+ * Thin matmul wrapper over libflame's LAPACK-compat dgemm_ (backed by BLIS),
3+ * built to WASM by build-blis-wasm.sh against the prebuilt libraries in
4+ * ../../libflame2wasm. Two builds: single-threaded (st) and pthreads (mt).
5+ *
6+ * The rest of matmul-bench works in row-major; dgemm_ is column-major
7+ * (Fortran). Row-major C = A*B is obtained from a column-major GEMM by
8+ * swapping the operands: passing (B, A) computes C^T = B^T * A^T in
9+ * column-major, which is exactly row-major C = A*B in the same flat buffer.
10+ * So c[0] matches the other (row-major) methods bit-for-bit-ish.
11+ */
12+#include <emscripten.h>
13+
14+extern int dgemm_( char* transa, char* transb, int* m, int* n, int* k,
15+ double* alpha, double* a, int* lda, double* b, int* ldb,
16+ double* beta, double* c, int* ldc );
17+
18+#ifdef BLIS_MT
19+/* dim_t is int32 in this BLIS build (--int-size=32). */
20+extern void bli_thread_set_num_threads( int n_threads );
21+#endif
22+
23+EMSCRIPTEN_KEEPALIVE
24+void matmul_blis( double* a, double* b, double* c, int n, int nthreads )
25+{
26+#ifdef BLIS_MT
27+ if ( nthreads > 0 ) bli_thread_set_num_threads( nthreads );
28+#else
29+ (void) nthreads;
30+#endif
31+ char tr = 'N';
32+ double alpha = 1.0, beta = 0.0;
33+ /* Operand swap: (B, A) column-major => row-major C = A*B. */
34+ dgemm_( &tr, &tr, &n, &n, &n, &alpha, b, &n, a, &n, &beta, c, &n );
35+}
index.htmladded+17−0View file
@@ -0,0 +1,17 @@
1+<!doctype html>
2+<html lang="en">
3+ <head>
4+ <meta charset="UTF-8" />
5+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+ <title>matmul-bench</title>
8+ <!-- Adds COOP/COEP via a service worker so SharedArrayBuffer (WASM threads,
9+ for the threaded BLIS method) works on GitHub Pages, which can't set
10+ response headers. No-op when the server already sends them (dev). -->
11+ <script src="%BASE_URL%coi-serviceworker.js"></script>
12+ </head>
13+ <body>
14+ <div id="root"></div>
15+ <script type="module" src="/src/main.tsx"></script>
16+ </body>
17+</html>
native/bench_native.cadded+78−0View file
@@ -0,0 +1,78 @@
1+/*
2+ * Native dgemm benchmark, for a reference point outside the browser.
3+ * Same flop-count and timing convention as the browser benchmarks.
4+ *
5+ * ./build.sh
6+ * OPENBLAS_NUM_THREADS=1 ./bench_native 128 256 512 1024 2048
7+ * OPENBLAS_NUM_THREADS=$(nproc) ./bench_native 128 256 512 1024 2048
8+ */
9+#include <stdio.h>
10+#include <stdlib.h>
11+#include <string.h>
12+#include <time.h>
13+
14+extern void dgemm_( const char* transa, const char* transb, const int* m,
15+ const int* n, const int* k, const double* alpha,
16+ const double* a, const int* lda, const double* b,
17+ const int* ldb, const double* beta, double* c,
18+ const int* ldc );
19+
20+static double now_sec( void )
21+{
22+ struct timespec ts;
23+ clock_gettime( CLOCK_MONOTONIC, &ts );
24+ return ts.tv_sec + 1e-9 * ts.tv_nsec;
25+}
26+
27+/* Same deterministic PRNG as the browser methods, for an identical checksum. */
28+static unsigned long long rng_state = 12345;
29+static double frand( void )
30+{
31+ rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
32+ return ( ( rng_state >> 33 ) & 0xffffff ) / (double) 0x1000000 - 0.5;
33+}
34+
35+static void fill_random( double* a, int n2 )
36+{
37+ rng_state = 12345;
38+ for ( int i = 0; i < n2; i++ ) a[i] = frand();
39+}
40+
41+static void bench_dgemm( int n )
42+{
43+ double *a = malloc( (size_t)n * n * sizeof(double) );
44+ double *b = malloc( (size_t)n * n * sizeof(double) );
45+ double *c = malloc( (size_t)n * n * sizeof(double) );
46+ double alpha = 1.0, beta = 0.0, t, gflops;
47+ char tr = 'N';
48+
49+ fill_random( a, n * n );
50+ fill_random( b, n * n );
51+ memset( c, 0, (size_t)n * n * sizeof(double) );
52+
53+ t = now_sec();
54+ dgemm_( &tr, &tr, &n, &n, &n, &alpha, a, &n, b, &n, &beta, c, &n );
55+ t = now_sec() - t;
56+
57+ gflops = 2.0 * n * (double) n * n / t / 1e9;
58+ printf( "dgemm n=%5d %10.4f s %8.2f GFLOP/s (check c[0]=%.6f)\n",
59+ n, t, gflops, c[0] );
60+
61+ free( a ); free( b ); free( c );
62+}
63+
64+int main( int argc, char** argv )
65+{
66+ int sizes_default[] = { 128, 256, 512, 1024, 2048 };
67+ int *sizes = sizes_default, nsizes = 5;
68+
69+ if ( argc > 1 ) {
70+ nsizes = argc - 1;
71+ sizes = malloc( nsizes * sizeof(int) );
72+ for ( int i = 0; i < nsizes; i++ ) sizes[i] = atoi( argv[i + 1] );
73+ }
74+
75+ for ( int i = 0; i < nsizes; i++ ) bench_dgemm( sizes[i] );
76+
77+ return 0;
78+}
native/build.shadded+5−0View file
@@ -0,0 +1,5 @@
1+#!/usr/bin/env bash
2+set -euo pipefail
3+cd "$(dirname "$0")"
4+gcc -O2 bench_native.c -o bench_native $(pkg-config --cflags --libs openblas)
5+echo "Built native/bench_native"
package-lock.jsonadded+2391−0View file
This diff is 2,396 lines long and is not shown.
package.jsonadded+28−0View file
@@ -0,0 +1,28 @@
1+{
2+ "name": "matmul-bench",
3+ "private": true,
4+ "version": "0.0.0",
5+ "type": "module",
6+ "scripts": {
7+ "dev": "vite",
8+ "build": "tsc -b && vite build",
9+ "lint": "oxlint",
10+ "preview": "vite preview"
11+ },
12+ "dependencies": {
13+ "@emotion/react": "^11.14.0",
14+ "@emotion/styled": "^11.14.1",
15+ "@mui/material": "^9.2.0",
16+ "react": "^19.2.7",
17+ "react-dom": "^19.2.7"
18+ },
19+ "devDependencies": {
20+ "@types/node": "^24.13.2",
21+ "@types/react": "^19.2.17",
22+ "@types/react-dom": "^19.2.3",
23+ "@vitejs/plugin-react": "^6.0.3",
24+ "oxlint": "^1.71.0",
25+ "typescript": "~6.0.2",
26+ "vite": "^8.1.1"
27+ }
28+}
public/coi-serviceworker.jsadded+146−0View file
@@ -0,0 +1,146 @@
1+/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
2+let coepCredentialless = false;
3+if (typeof window === 'undefined') {
4+ self.addEventListener("install", () => self.skipWaiting());
5+ self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim()));
6+
7+ self.addEventListener("message", (ev) => {
8+ if (!ev.data) {
9+ return;
10+ } else if (ev.data.type === "deregister") {
11+ self.registration
12+ .unregister()
13+ .then(() => {
14+ return self.clients.matchAll();
15+ })
16+ .then(clients => {
17+ clients.forEach((client) => client.navigate(client.url));
18+ });
19+ } else if (ev.data.type === "coepCredentialless") {
20+ coepCredentialless = ev.data.value;
21+ }
22+ });
23+
24+ self.addEventListener("fetch", function (event) {
25+ const r = event.request;
26+ if (r.cache === "only-if-cached" && r.mode !== "same-origin") {
27+ return;
28+ }
29+
30+ const request = (coepCredentialless && r.mode === "no-cors")
31+ ? new Request(r, {
32+ credentials: "omit",
33+ })
34+ : r;
35+ event.respondWith(
36+ fetch(request)
37+ .then((response) => {
38+ if (response.status === 0) {
39+ return response;
40+ }
41+
42+ const newHeaders = new Headers(response.headers);
43+ newHeaders.set("Cross-Origin-Embedder-Policy",
44+ coepCredentialless ? "credentialless" : "require-corp"
45+ );
46+ if (!coepCredentialless) {
47+ newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
48+ }
49+ newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
50+
51+ return new Response(response.body, {
52+ status: response.status,
53+ statusText: response.statusText,
54+ headers: newHeaders,
55+ });
56+ })
57+ .catch((e) => console.error(e))
58+ );
59+ });
60+
61+} else {
62+ (() => {
63+ const reloadedBySelf = window.sessionStorage.getItem("coiReloadedBySelf");
64+ window.sessionStorage.removeItem("coiReloadedBySelf");
65+ const coepDegrading = (reloadedBySelf == "coepdegrade");
66+
67+ // You can customize the behavior of this script through a global `coi` variable.
68+ const coi = {
69+ shouldRegister: () => !reloadedBySelf,
70+ shouldDeregister: () => false,
71+ coepCredentialless: () => true,
72+ coepDegrade: () => true,
73+ doReload: () => window.location.reload(),
74+ quiet: false,
75+ ...window.coi
76+ };
77+
78+ const n = navigator;
79+ const controlling = n.serviceWorker && n.serviceWorker.controller;
80+
81+ // Record the failure if the page is served by serviceWorker.
82+ if (controlling && !window.crossOriginIsolated) {
83+ window.sessionStorage.setItem("coiCoepHasFailed", "true");
84+ }
85+ const coepHasFailed = window.sessionStorage.getItem("coiCoepHasFailed");
86+
87+ if (controlling) {
88+ // Reload only on the first failure.
89+ const reloadToDegrade = coi.coepDegrade() && !(
90+ coepDegrading || window.crossOriginIsolated
91+ );
92+ n.serviceWorker.controller.postMessage({
93+ type: "coepCredentialless",
94+ value: (reloadToDegrade || coepHasFailed && coi.coepDegrade())
95+ ? false
96+ : coi.coepCredentialless(),
97+ });
98+ if (reloadToDegrade) {
99+ !coi.quiet && console.log("Reloading page to degrade COEP.");
100+ window.sessionStorage.setItem("coiReloadedBySelf", "coepdegrade");
101+ coi.doReload("coepdegrade");
102+ }
103+
104+ if (coi.shouldDeregister()) {
105+ n.serviceWorker.controller.postMessage({ type: "deregister" });
106+ }
107+ }
108+
109+ // If we're already coi: do nothing. Perhaps it's due to this script doing its job, or COOP/COEP are
110+ // already set from the origin server. Also if the browser has no notion of crossOriginIsolated, just give up here.
111+ if (window.crossOriginIsolated !== false || !coi.shouldRegister()) return;
112+
113+ if (!window.isSecureContext) {
114+ !coi.quiet && console.log("COOP/COEP Service Worker not registered, a secure context is required.");
115+ return;
116+ }
117+
118+ // In some environments (e.g. Firefox private mode) this won't be available
119+ if (!n.serviceWorker) {
120+ !coi.quiet && console.error("COOP/COEP Service Worker not registered, perhaps due to private mode.");
121+ return;
122+ }
123+
124+ n.serviceWorker.register(window.document.currentScript.src).then(
125+ (registration) => {
126+ !coi.quiet && console.log("COOP/COEP Service Worker registered", registration.scope);
127+
128+ registration.addEventListener("updatefound", () => {
129+ !coi.quiet && console.log("Reloading page to make use of updated COOP/COEP Service Worker.");
130+ window.sessionStorage.setItem("coiReloadedBySelf", "updatefound");
131+ coi.doReload();
132+ });
133+
134+ // If the registration is active, but it's not controlling the page
135+ if (registration.active && !n.serviceWorker.controller) {
136+ !coi.quiet && console.log("Reloading page to make use of COOP/COEP Service Worker.");
137+ window.sessionStorage.setItem("coiReloadedBySelf", "notcontrolling");
138+ coi.doReload();
139+ }
140+ },
141+ (err) => {
142+ !coi.quiet && console.error("COOP/COEP Service Worker failed to register:", err);
143+ }
144+ );
145+ })();
146+}
public/favicon.svgadded+1−0View file
@@ -0,0 +1 @@
1+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
\ No newline at end of file
public/matmul/matmul_blis_mt.jsadded+2−0View file
@@ -0,0 +1,2 @@
1+var createMatmulBlisMT=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var ENVIRONMENT_IS_PTHREAD=ENVIRONMENT_IS_WORKER&&globalThis.name=="em-pthread";if(ENVIRONMENT_IS_NODE){var worker_threads=require("node:worker_threads");global.Worker=worker_threads.Worker;ENVIRONMENT_IS_WORKER=!worker_threads.isMainThread;ENVIRONMENT_IS_PTHREAD=ENVIRONMENT_IS_WORKER&&worker_threads.workerData=="em-pthread"}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}if(!ENVIRONMENT_IS_NODE){if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var defaultPrint=console.log.bind(console);var defaultPrintErr=console.error.bind(console);if(ENVIRONMENT_IS_NODE){var utils=require("node:util");var stringify=a=>typeof a=="object"?utils.inspect(a):a;defaultPrint=(...args)=>fs.writeSync(1,args.map(stringify).join(" ")+"\n");defaultPrintErr=(...args)=>fs.writeSync(2,args.map(stringify).join(" ")+"\n")}var out=defaultPrint;var err=defaultPrintErr;var wasmBinary;var wasmModule;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;if(ENVIRONMENT_IS_NODE&&ENVIRONMENT_IS_PTHREAD){globalThis.self=globalThis;var parentPort=worker_threads.parentPort;if(!globalThis.postMessage){parentPort.on("message",msg=>globalThis.onmessage?.({data:msg}));globalThis.postMessage=msg=>parentPort.postMessage(msg)}process.on("uncaughtException",err=>{postMessage({cmd:"uncaughtException",error:err});process.exit(1)})}var startWorker;if(ENVIRONMENT_IS_PTHREAD){var initializedJS=false;self.onunhandledrejection=e=>{throw e.reason||e};function handleMessage(e){try{var msgData=e["data"];var cmd=msgData.cmd;if(cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);startWorker=()=>{postMessage({cmd:"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};for(const handler of msgData.handlers){if(!Module[handler]||Module[handler].proxy){Module[handler]=(...args)=>{postMessage({cmd:"callHandler",handler,args})};if(handler=="print")out=Module[handler];if(handler=="printErr")err=Module[handler]}}wasmMemory=msgData.wasmMemory;updateMemoryViews();wasmModule=msgData.wasmModule;createWasm();run()}else if(cmd==="run"){establishStackSpace(msgData.pthread_ptr);__emscripten_thread_init(msgData.pthread_ptr,0,0,1,0,0);PThread.threadInitTLS();__emscripten_thread_mailbox_await(msgData.pthread_ptr);if(!initializedJS){initializedJS=true}try{invokeEntryPoint(msgData.start_routine,msgData.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(msgData.target==="setimmediate"){}else if(cmd==="checkMailbox"){if(initializedJS){checkMailbox()}}else if(cmd){err(`worker: received unknown command ${cmd}`);err(msgData)}}catch(ex){__emscripten_thread_crashed();throw ex}}self.onmessage=handleMessage}var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function initMemory(){if(ENVIRONMENT_IS_PTHREAD){return}if(Module["wasmMemory"]){wasmMemory=Module["wasmMemory"]}else{var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||805306368;wasmMemory=new WebAssembly.Memory({initial:INITIAL_MEMORY/65536,maximum:INITIAL_MEMORY/65536,shared:true})}updateMemoryViews()}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(ENVIRONMENT_IS_PTHREAD)return startWorker();wasmExports["__wasm_call_ctors"]()}function postRun(){if(ENVIRONMENT_IS_PTHREAD){return}if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("matmul_blis_mt.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){assignWasmImports();var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;registerTLSInit(wasmExports["_emscripten_tls_init"]);assignWasmExports(wasmExports);wasmModule=module;return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"],result["module"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}if(ENVIRONMENT_IS_PTHREAD){var instance=new WebAssembly.Instance(wasmModule,getWasmImports());return receiveInstance(instance,wasmModule)}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var terminateWorker=worker=>{worker.terminate();worker.onmessage=e=>{}};var cleanupThread=pthread_ptr=>{var worker=PThread.pthreads[pthread_ptr];PThread.returnWorkerToPool(worker)};var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var spawnThread=threadParams=>{var worker=PThread.getNewWorker();if(!worker){return 6}PThread.runningWorkers.push(worker);PThread.pthreads[threadParams.pthread_ptr]=worker;worker.pthread_ptr=threadParams.pthread_ptr;var msg={cmd:"run",start_routine:threadParams.startRoutine,arg:threadParams.arg,pthread_ptr:threadParams.pthread_ptr};if(ENVIRONMENT_IS_NODE){worker.unref()}worker.postMessage(msg,threadParams.transferList);return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var stackSave=()=>_emscripten_stack_get_current();var stackRestore=val=>__emscripten_stack_restore(val);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var proxyToMainThread=(funcIndex,emAsmAddr,proxyMode,...callArgs)=>{var bufSize=8*callArgs.length*2;var sp=stackSave();var args=stackAlloc(bufSize);var b=args>>3;for(var arg of callArgs){if(typeof arg=="bigint"){HEAP64[b++]=1n;HEAP64[b++]=arg}else{HEAP64[b++]=0n;HEAPF64[b++]=arg}}var rtn=__emscripten_run_js_on_main_thread(funcIndex,emAsmAddr,bufSize,args,proxyMode);stackRestore(sp);return rtn};function _proc_exit(code){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(0,0,1,code);EXITSTATUS=code;if(!keepRuntimeAlive()){PThread.terminateAllThreads();Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))}function exitOnMainThread(returnCode){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(1,0,0,returnCode);_exit(returnCode)}var exitJS=(status,implicit)=>{EXITSTATUS=status;if(ENVIRONMENT_IS_PTHREAD){exitOnMainThread(status);throw"unwind"}_proc_exit(status)};var _exit=exitJS;var PThread={unusedWorkers:[],runningWorkers:[],tlsInitFunctions:[],pthreads:{},init(){if(!ENVIRONMENT_IS_PTHREAD){PThread.initMainThread()}},initMainThread(){var pthreadPoolSize=20;while(pthreadPoolSize--){PThread.allocateUnusedWorker()}addOnPreRun(async()=>{var pthreadPoolReady=PThread.loadWasmModuleToAllWorkers();addRunDependency("loading-workers");await pthreadPoolReady;removeRunDependency("loading-workers")})},terminateAllThreads:()=>{for(var worker of PThread.runningWorkers){terminateWorker(worker)}for(var worker of PThread.unusedWorkers){terminateWorker(worker)}PThread.unusedWorkers=[];PThread.runningWorkers=[];PThread.pthreads={}},returnWorkerToPool:worker=>{var pthread_ptr=worker.pthread_ptr;delete PThread.pthreads[pthread_ptr];PThread.unusedWorkers.push(worker);PThread.runningWorkers.splice(PThread.runningWorkers.indexOf(worker),1);worker.pthread_ptr=0;__emscripten_thread_free_data(pthread_ptr)},threadInitTLS(){PThread.tlsInitFunctions.forEach(f=>f())},loadWasmModuleToWorker:worker=>new Promise(onFinishedLoading=>{worker.onmessage=e=>{var d=e["data"];var cmd=d.cmd;if(d.targetThread&&d.targetThread!=_pthread_self()){var targetWorker=PThread.pthreads[d.targetThread];if(targetWorker){targetWorker.postMessage(d,d.transferList)}else{err(`Internal error! Worker sent a message "${cmd}" to target pthread ${d.targetThread}, but that thread no longer exists!`)}return}if(cmd==="checkMailbox"){checkMailbox()}else if(cmd==="spawnThread"){spawnThread(d)}else if(cmd==="cleanupThread"){callUserCallback(()=>cleanupThread(d.thread))}else if(cmd==="loaded"){worker.loaded=true;if(ENVIRONMENT_IS_NODE&&!worker.pthread_ptr){worker.unref()}onFinishedLoading(worker)}else if(d.target==="setimmediate"){worker.postMessage(d)}else if(cmd==="uncaughtException"){worker.onerror(d.error)}else if(cmd==="callHandler"){Module[d.handler](...d.args)}else if(cmd){err(`worker sent an unknown command ${cmd}`)}};worker.onerror=e=>{var message="worker sent an error!";err(`${message} ${e.filename}:${e.lineno}: ${e.message}`);throw e};if(ENVIRONMENT_IS_NODE){worker.on("message",data=>worker.onmessage({data}));worker.on("error",e=>worker.onerror(e))}var handlers=[];var knownHandlers=["onExit","onAbort","print","printErr"];for(var handler of knownHandlers){if(Module.propertyIsEnumerable(handler)){handlers.push(handler)}}worker.postMessage({cmd:"load",handlers,wasmMemory,wasmModule})}),async loadWasmModuleToAllWorkers(){if(ENVIRONMENT_IS_PTHREAD){return}let pthreadPoolReady=Promise.all(PThread.unusedWorkers.map(PThread.loadWasmModuleToWorker));return pthreadPoolReady},allocateUnusedWorker(){var worker;var pthreadMainJs=_scriptName;if(Module["mainScriptUrlOrBlob"]){pthreadMainJs=Module["mainScriptUrlOrBlob"];if(typeof pthreadMainJs!="string"){pthreadMainJs=URL.createObjectURL(pthreadMainJs)}}worker=new Worker(pthreadMainJs,{workerData:"em-pthread",name:"em-pthread"});PThread.unusedWorkers.push(worker)},getNewWorker(){if(PThread.unusedWorkers.length==0){PThread.allocateUnusedWorker();PThread.loadWasmModuleToWorker(PThread.unusedWorkers[0])}return PThread.unusedWorkers.pop()}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);function establishStackSpace(pthread_ptr){var stackHigh=HEAPU32[pthread_ptr+48>>2];var stackSize=HEAPU32[pthread_ptr+52>>2];var stackLow=stackHigh-stackSize;_emscripten_stack_set_limits(stackHigh,stackLow);stackRestore(stackHigh)}var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var invokeEntryPoint=(ptr,arg)=>{runtimeKeepaliveCounter=0;noExitRuntime=0;var result=getWasmTableEntry(ptr)(arg);function finish(result){if(keepRuntimeAlive()){EXITSTATUS=result;return}__emscripten_thread_exit(result)}finish(result)};var noExitRuntime=true;var registerTLSInit=tlsInitFunc=>PThread.tlsInitFunctions.push(tlsInitFunc);var wasmMemory;function pthreadCreateProxied(pthread_ptr,attr,startRoutine,arg){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(2,0,1,pthread_ptr,attr,startRoutine,arg);return ___pthread_create_js(pthread_ptr,attr,startRoutine,arg)}var _emscripten_has_threading_support=()=>!!globalThis.SharedArrayBuffer;var ___pthread_create_js=(pthread_ptr,attr,startRoutine,arg)=>{if(!_emscripten_has_threading_support()){return 6}var transferList=[];var error=0;if(ENVIRONMENT_IS_PTHREAD&&(transferList.length===0||error)){return pthreadCreateProxied(pthread_ptr,attr,startRoutine,arg)}if(error)return error;var threadParams={startRoutine,pthread_ptr,arg,transferList};if(ENVIRONMENT_IS_PTHREAD){threadParams.cmd="spawnThread";postMessage(threadParams,transferList);return 0}return spawnThread(threadParams)};var __abort_js=()=>abort("");var __emscripten_init_main_thread_js=tb=>{__emscripten_thread_init(tb,!ENVIRONMENT_IS_WORKER,1,!ENVIRONMENT_IS_WEB,65536,false);PThread.threadInitTLS()};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var maybeExit=()=>{if(!keepRuntimeAlive()){try{if(ENVIRONMENT_IS_PTHREAD){if(_pthread_self())__emscripten_thread_exit(EXITSTATUS);return}_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var waitAsyncPolyfilled=!Atomics.waitAsync||globalThis.navigator?.userAgent&&Number((navigator.userAgent.match(/Chrom(e|ium)\/([0-9]+)\./)||[])[2])<91;var __emscripten_thread_mailbox_await=pthread_ptr=>{if(!waitAsyncPolyfilled){var wait=Atomics.waitAsync(HEAP32,pthread_ptr>>2,pthread_ptr);wait.value.then(checkMailbox);var waitingAsync=pthread_ptr+120;Atomics.store(HEAP32,waitingAsync>>2,1)}};var checkMailbox=()=>callUserCallback(()=>{var pthread_ptr=_pthread_self();if(pthread_ptr){__emscripten_thread_mailbox_await(pthread_ptr);__emscripten_check_mailbox()}});var __emscripten_notify_mailbox_postmessage=(targetThread,currThreadId)=>{if(targetThread==currThreadId){setTimeout(checkMailbox)}else if(ENVIRONMENT_IS_PTHREAD){postMessage({targetThread,cmd:"checkMailbox"})}else{var worker=PThread.pthreads[targetThread];if(!worker){return}worker.postMessage({cmd:"checkMailbox"})}};var proxiedJSCallArgs=[];var __emscripten_receive_on_main_thread_js=(funcIndex,emAsmAddr,callingThread,bufSize,args,ctx,ctxArgs)=>{proxiedJSCallArgs.length=0;var b=args>>3;var end=args+bufSize>>3;while(b<end){var arg;if(HEAP64[b++]){arg=HEAP64[b++]}else{arg=HEAPF64[b++]}proxiedJSCallArgs.push(arg)}var func=proxiedFunctionTable[funcIndex];PThread.currentProxiedOperationCallerThread=callingThread;var rtn=func(...proxiedJSCallArgs);PThread.currentProxiedOperationCallerThread=0;if(ctx){rtn.then(rtn=>__emscripten_run_js_on_main_thread_done(ctx,ctxArgs,rtn));return}return rtn};var __emscripten_thread_cleanup=thread=>{if(!ENVIRONMENT_IS_PTHREAD)cleanupThread(thread);else postMessage({cmd:"cleanupThread",thread})};var __emscripten_thread_set_strongref=thread=>{if(ENVIRONMENT_IS_NODE){PThread.pthreads[thread].ref()}};var _emscripten_get_now=()=>performance.timeOrigin+performance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>num<INT53_MIN||num>INT53_MAX?NaN:Number(num);function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var _emscripten_check_blocking_allowed=()=>{};var runtimeKeepalivePush=()=>{runtimeKeepaliveCounter+=1};var _emscripten_exit_with_live_runtime=()=>{runtimeKeepalivePush();throw"unwind"};var abortOnCannotGrowMemory=requestedSize=>{abort("OOM")};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;abortOnCannotGrowMemory(requestedSize)};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function _environ_get(__environ,environ_buf){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(3,0,1,__environ,environ_buf);var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0}var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};function _environ_sizes_get(penviron_count,penviron_buf_size){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(4,0,1,penviron_count,penviron_buf_size);var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0}function _fd_close(fd){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(5,0,1,fd);return 52}function _fd_seek(fd,offset,whence,newOffset){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(6,0,1,fd,offset,whence,newOffset);offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.buffer instanceof ArrayBuffer?heapOrArray.subarray(idx,endPtr):heapOrArray.slice(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";function _fd_write(fd,iov,iovcnt,pnum){if(ENVIRONMENT_IS_PTHREAD)return proxyToMainThread(7,0,1,fd,iov,iovcnt,pnum);var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0}PThread.init();{initMemory();if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var proxiedFunctionTable=[_proc_exit,exitOnMainThread,pthreadCreateProxied,_environ_get,_environ_sizes_get,_fd_close,_fd_seek,_fd_write];var _matmul_blis,_malloc,_free,__emscripten_tls_init,_pthread_self,__emscripten_thread_init,__emscripten_thread_crashed,__emscripten_run_js_on_main_thread_done,__emscripten_run_js_on_main_thread,__emscripten_thread_free_data,__emscripten_thread_exit,__emscripten_check_mailbox,_emscripten_stack_set_limits,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,__indirect_function_table,wasmTable;function assignWasmExports(wasmExports){_matmul_blis=Module["_matmul_blis"]=wasmExports["matmul_blis"];_malloc=Module["_malloc"]=wasmExports["malloc"];_free=Module["_free"]=wasmExports["free"];__emscripten_tls_init=wasmExports["_emscripten_tls_init"];_pthread_self=wasmExports["pthread_self"];__emscripten_thread_init=wasmExports["_emscripten_thread_init"];__emscripten_thread_crashed=wasmExports["_emscripten_thread_crashed"];__emscripten_run_js_on_main_thread_done=wasmExports["_emscripten_run_js_on_main_thread_done"];__emscripten_run_js_on_main_thread=wasmExports["_emscripten_run_js_on_main_thread"];__emscripten_thread_free_data=wasmExports["_emscripten_thread_free_data"];__emscripten_thread_exit=wasmExports["_emscripten_thread_exit"];__emscripten_check_mailbox=wasmExports["_emscripten_check_mailbox"];_emscripten_stack_set_limits=wasmExports["emscripten_stack_set_limits"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];__indirect_function_table=wasmTable=wasmExports["__indirect_function_table"]}var wasmImports;function assignWasmImports(){wasmImports={__pthread_create_js:___pthread_create_js,_abort_js:__abort_js,_emscripten_init_main_thread_js:__emscripten_init_main_thread_js,_emscripten_notify_mailbox_postmessage:__emscripten_notify_mailbox_postmessage,_emscripten_receive_on_main_thread_js:__emscripten_receive_on_main_thread_js,_emscripten_thread_cleanup:__emscripten_thread_cleanup,_emscripten_thread_mailbox_await:__emscripten_thread_mailbox_await,_emscripten_thread_set_strongref:__emscripten_thread_set_strongref,clock_time_get:_clock_time_get,emscripten_check_blocking_allowed:_emscripten_check_blocking_allowed,emscripten_exit_with_live_runtime:_emscripten_exit_with_live_runtime,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,exit:_exit,fd_close:_fd_close,fd_seek:_fd_seek,fd_write:_fd_write,memory:wasmMemory}}function run(){if(runDependencies>0){dependenciesFulfilled=run;return}if(ENVIRONMENT_IS_PTHREAD){readyPromiseResolve?.(Module);initRuntime();return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;if(!ENVIRONMENT_IS_PTHREAD){wasmExports=await (createWasm());run()}if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}
2+;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMatmulBlisMT;module.exports.default=createMatmulBlisMT}else if(typeof define==="function"&&define["amd"])define([],()=>createMatmulBlisMT);var isPthread=globalThis.name=="em-pthread";var isNode=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(isNode)isPthread=require("node:worker_threads").workerData==="em-pthread";isPthread&&createMatmulBlisMT();
public/matmul/matmul_blis_mt.wasmadded+0−0View file
Binary file not shown.
public/matmul/matmul_blis_st.jsadded+2−0View file
@@ -0,0 +1,2 @@
1+var createMatmulBlisST=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else if(ENVIRONMENT_IS_WORKER){_scriptName=self.location.href}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("matmul_blis_st.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i<str.length;++i){var u=str.codePointAt(i);if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i<str.length;++i){var c=str.charCodeAt(i);if(c<=127){len++}else if(c<=2047){len+=2}else if(c>=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var _fd_close=fd=>52;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>num<INT53_MIN||num>INT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx<endPtr){var u0=heapOrArray[idx++];if(!(u0&128)){str+=String.fromCharCode(u0);continue}var u1=heapOrArray[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}var u2=heapOrArray[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u0=(u0&7)<<18|u1<<12|u2<<6|heapOrArray[idx++]&63}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}return str};var printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(curr===0||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i<iovcnt;i++){var ptr=HEAPU32[iov>>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j<len;j++){printChar(fd,HEAPU8[ptr+j])}num+=len}HEAPU32[pnum>>2]=num;return 0};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}var _matmul_blis,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_matmul_blis=Module["_matmul_blis"]=wasmExports["matmul_blis"];_malloc=Module["_malloc"]=wasmExports["malloc"];_free=Module["_free"]=wasmExports["free"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={_abort_js:__abort_js,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_seek:_fd_seek,fd_write:_fd_write};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})}
2+;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createMatmulBlisST;module.exports.default=createMatmulBlisST}else if(typeof define==="function"&&define["amd"])define([],()=>createMatmulBlisST);
public/matmul/matmul_blis_st.wasmadded+0−0View file
Binary file not shown.
public/matmul/worker.jsadded+85−0View file
@@ -0,0 +1,85 @@
1+// Classic worker (NOT a module worker) that runs the pthread-capable WASM
2+// matmul modules off the main thread. Threaded builds spawn their pthread
3+// workers from here as nested workers — which is why this is a plain
4+// importScripts worker served from public/, kept out of Vite's module graph.
5+// (Mirrors the proven approach in libflame2wasm/web/bench_worker.js.)
6+'use strict';
7+
8+// Modules and this worker all live in public/matmul/; resolve siblings
9+// relative to it so each module and its pthread workers load from here.
10+const DIR = self.location.href.replace(/[^/]*$/, '');
11+
12+// kind -> { script, factory export name, exported C function }. Every func has
13+// signature (aPtr, bPtr, cPtr, n, nthreads) — single-threaded builds ignore
14+// nthreads, so the call is uniform.
15+const REGISTRY = {
16+ 'matmul-mt': { file: 'matmul_mt.js', name: 'createMatmulMT', fn: '_matmul_blocked_mt' },
17+ 'blis-st': { file: 'matmul_blis_st.js', name: 'createMatmulBlisST', fn: '_matmul_blis' },
18+ 'blis-mt': { file: 'matmul_blis_mt.js', name: 'createMatmulBlisMT', fn: '_matmul_blis' },
19+};
20+
21+// Same deterministic PRNG as src/methods/random.ts, inlined so this worker is
22+// self-contained and every method multiplies bit-identical inputs per size.
23+function mulberry32(seed) {
24+ let a = seed;
25+ return () => {
26+ a |= 0;
27+ a = (a + 0x6d2b79f5) | 0;
28+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
29+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
30+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
31+ };
32+}
33+
34+function generateMatrix(n, seed) {
35+ const rand = mulberry32(seed);
36+ const m = new Float64Array(n * n);
37+ for (let i = 0; i < m.length; i++) m[i] = rand() - 0.5;
38+ return m;
39+}
40+
41+const instances = {}; // kind -> { module, fn }
42+
43+async function getInstance(kind) {
44+ if (!instances[kind]) {
45+ const reg = REGISTRY[kind];
46+ if (!reg) throw new Error(`Unknown kind: ${kind}`);
47+ importScripts(DIR + reg.file);
48+ const factory = self[reg.name];
49+ const module = await factory({
50+ locateFile: (path) => DIR + path,
51+ mainScriptUrlOrBlob: DIR + reg.file,
52+ });
53+ instances[kind] = { module, fn: reg.fn };
54+ }
55+ return instances[kind];
56+}
57+
58+onmessage = async (e) => {
59+ const { id, kind, n, seedA, seedB, threads } = e.data;
60+ try {
61+ const { module, fn } = await getInstance(kind);
62+ const a = generateMatrix(n, seedA);
63+ const b = generateMatrix(n, seedB);
64+ const bytes = n * n * 8;
65+ const aPtr = module._malloc(bytes);
66+ const bPtr = module._malloc(bytes);
67+ const cPtr = module._malloc(bytes);
68+ try {
69+ new Float64Array(module.HEAPF64.buffer, aPtr, n * n).set(a);
70+ new Float64Array(module.HEAPF64.buffer, bPtr, n * n).set(b);
71+ const t0 = performance.now();
72+ module[fn](aPtr, bPtr, cPtr, n, threads);
73+ const ms = performance.now() - t0;
74+ // st builds allow memory growth (buffer may be replaced) — re-view.
75+ const sample = new Float64Array(module.HEAPF64.buffer, cPtr, n * n)[0];
76+ postMessage({ id, ms, sample });
77+ } finally {
78+ module._free(aPtr);
79+ module._free(bPtr);
80+ module._free(cPtr);
81+ }
82+ } catch (err) {
83+ postMessage({ id, error: err && err.message ? err.message : String(err) });
84+ }
85+};
src/App.tsxadded+92−0View file
@@ -0,0 +1,92 @@
1+import {
2+ AppBar, Container, createTheme, CssBaseline, Link, Paper, Table, TableBody,
3+ TableCell, TableContainer, TableHead, TableRow, ThemeProvider, Toolbar, Typography,
4+} from '@mui/material'
5+import { BenchmarkRunner } from './components/BenchmarkRunner'
6+import { NATIVE_REFERENCE, NATIVE_MAX_THREADS } from './data/nativeReference'
7+
8+const theme = createTheme({
9+ palette: { mode: 'light', primary: { main: '#1565c0' } },
10+})
11+
12+function App() {
13+ return (
14+ <ThemeProvider theme={theme}>
15+ <CssBaseline />
16+ <AppBar position="static" elevation={0}>
17+ <Toolbar>
18+ <Typography variant="h6" sx={{ flexGrow: 1 }}>matmul-bench</Typography>
19+ <Link href="https://github.com/concept-collection/matmul-bench" target="_blank" rel="noreferrer" color="inherit" underline="hover">
20+ GitHub
21+ </Link>
22+ </Toolbar>
23+ </AppBar>
24+
25+ <Container maxWidth="md" sx={{ py: 3 }}>
26+ <Typography variant="body1" sx={{ mb: 2 }}>
27+ Compares n×n matrix-matrix multiply (GEMM) across implementations
28+ running in the browser — plain JavaScript/TypeScript, a WebGPU
29+ compute shader, hand-optimized C compiled to WebAssembly (including a
30+ multi-threaded build), and libFLAME/BLIS (a real BLAS) compiled to
31+ WebAssembly, single- and multi-threaded — plus a fixed reference
32+ table from native LAPACK/OpenBLAS running outside the browser.
33+ </Typography>
34+
35+ <BenchmarkRunner />
36+
37+ <Typography variant="h6" sx={{ mt: 4, mb: 1 }}>Native reference: OpenBLAS dgemm</Typography>
38+ <Typography variant="caption" color="text.secondary" component="div" sx={{ mb: 1 }}>
39+ Measured with <code>native/bench_native.c</code> outside the browser on one
40+ Linux desktop — hardware-dependent, for rough comparison only. Regenerate with{' '}
41+ <code>native/build.sh && native/bench_native</code>.
42+ </Typography>
43+ {NATIVE_REFERENCE.length > 0 ? (
44+ <TableContainer component={Paper} variant="outlined" sx={{ maxWidth: 480 }}>
45+ <Table size="small">
46+ <TableHead>
47+ <TableRow>
48+ <TableCell>n</TableCell>
49+ <TableCell align="right">1 thread (GFLOP/s)</TableCell>
50+ <TableCell align="right">{NATIVE_MAX_THREADS} threads (GFLOP/s)</TableCell>
51+ </TableRow>
52+ </TableHead>
53+ <TableBody>
54+ {NATIVE_REFERENCE.map((r) => (
55+ <TableRow key={r.n}>
56+ <TableCell>{r.n}</TableCell>
57+ <TableCell align="right">{r.gflops1t.toFixed(1)}</TableCell>
58+ <TableCell align="right">{r.gflopsNt.toFixed(1)}</TableCell>
59+ </TableRow>
60+ ))}
61+ </TableBody>
62+ </Table>
63+ </TableContainer>
64+ ) : (
65+ <Typography variant="body2" color="text.secondary">(not yet captured)</Typography>
66+ )}
67+ <Typography variant="caption" color="text.secondary" component="div" sx={{ mt: 1 }}>
68+ Threading overhead dominates at small n — the {NATIVE_MAX_THREADS}-thread
69+ column is slower than 1 thread below n≈512 on this machine.
70+ </Typography>
71+
72+ <Typography variant="body2" color="text.secondary" sx={{ mt: 3 }}>
73+ The threaded methods (custom C and libFLAME/BLIS) use WASM pthreads
74+ (Web Workers + SharedArrayBuffer) and need cross-origin isolation,
75+ provided here by a service worker; pick the thread count above. The
76+ libFLAME/BLIS methods link a real BLAS (BLIS generic C kernels with{' '}
77+ <code>-msimd128</code>) compiled to WebAssembly — see{' '}
78+ <Link href="https://github.com/magland/libflame2wasm" target="_blank" rel="noreferrer">libflame2wasm</Link>{' '}
79+ — and get far closer to native single-threaded OpenBLAS than the
80+ hand-written C kernels.
81+ </Typography>
82+
83+ <Typography variant="caption" color="text.secondary" component="div" sx={{ mt: 4 }}>
84+ Part of the{' '}
85+ <Link href="https://github.com/concept-collection" target="_blank" rel="noreferrer">concept-collection</Link>.
86+ </Typography>
87+ </Container>
88+ </ThemeProvider>
89+ )
90+}
91+
92+export default App
src/components/BenchmarkRunner.tsxadded+204−0View file
@@ -0,0 +1,204 @@
1+import { useMemo, useState } from 'react'
2+import {
3+ Box, Button, Checkbox, Chip, FormControl, FormControlLabel, InputLabel,
4+ LinearProgress, MenuItem, Select, Stack, Typography,
5+} from '@mui/material'
6+import { jsMatmul } from '../methods/jsMatmul'
7+import { webgpuMatmul } from '../methods/webgpuMatmul'
8+import { wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul } from '../methods/wasmMatmul'
9+import { blisStMatmul, blisMtMatmul } from '../methods/blisMatmul'
10+import { runInWorker } from '../methods/workerClient'
11+import { runInThreadedWorker } from '../methods/threadedClient'
12+import { generateMatrix } from '../methods/random'
13+import { gflops, type MatmulMethod } from '../methods/types'
14+import { ResultsTable, type CellState } from './ResultsTable'
15+
16+const METHODS: MatmulMethod[] = [
17+ jsMatmul, webgpuMatmul, wasmNaiveMatmul, wasmBlockedMatmul, wasmBlockedMtMatmul,
18+ blisStMatmul, blisMtMatmul,
19+]
20+const ALL_SIZES = [128, 256, 512, 1024, 2048]
21+
22+const HW = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4
23+const THREAD_OPTIONS = [...new Set([1, 2, 4, 8, HW])].filter((t) => t <= HW).sort((a, b) => a - b)
24+const isolated = typeof globalThis !== 'undefined' && globalThis.crossOriginIsolated === true
25+
26+const yield_ = () => new Promise((r) => setTimeout(r, 0))
27+
28+// Deterministic per-size seeds so every method (in this run) multiplies
29+// identical A and B — the `sample` (C[0]) values are then directly comparable.
30+function seedsFor(n: number) {
31+ return { seedA: n * 7 + 1, seedB: n * 13 + 2 }
32+}
33+
34+type Results = Record<string, Record<number, CellState>>
35+
36+export function BenchmarkRunner() {
37+ const [sizes, setSizes] = useState<Set<number>>(new Set(ALL_SIZES.filter((n) => n <= 1024)))
38+ const [threads, setThreads] = useState<number>(Math.min(8, HW))
39+ const [results, setResults] = useState<Results>({})
40+ const [running, setRunning] = useState(false)
41+ const [progress, setProgress] = useState(0)
42+
43+ // Threaded column headers reflect the currently selected thread count:
44+ // the word "threaded" in the label becomes "N threads".
45+ const displayMethods = useMemo<MatmulMethod[]>(
46+ () => METHODS.map((m) => (
47+ m.label.includes('threaded')
48+ ? { ...m, label: m.label.replace('threaded', `${threads} threads`) }
49+ : m
50+ )),
51+ [threads],
52+ )
53+
54+ const setCell = (methodId: string, n: number, state: CellState) => {
55+ setResults((prev) => ({
56+ ...prev,
57+ [methodId]: { ...prev[methodId], [n]: state },
58+ }))
59+ }
60+
61+ const runOne = async (method: MatmulMethod, n: number) => {
62+ if (!method.available()) {
63+ setCell(method.id, n, 'unavailable')
64+ return
65+ }
66+ setCell(method.id, n, 'pending')
67+ await yield_()
68+ try {
69+ const { seedA, seedB } = seedsFor(n)
70+ let result
71+ if (method.threadedKind) {
72+ result = await runInThreadedWorker(method.threadedKind, n, seedA, seedB, threads)
73+ } else if (method.worker) {
74+ result = await runInWorker(method.id, n, seedA, seedB)
75+ } else {
76+ result = await method.run!(n, generateMatrix(n, seedA), generateMatrix(n, seedB))
77+ }
78+ setCell(method.id, n, { gflops: gflops(n, result.ms), ms: result.ms, sample: result.sample })
79+ } catch {
80+ setCell(method.id, n, 'error')
81+ }
82+ }
83+
84+ const activeSizes = ALL_SIZES.filter((n) => sizes.has(n))
85+
86+ const runAll = async () => {
87+ setRunning(true)
88+ const total = activeSizes.length * METHODS.length
89+ let done = 0
90+ for (const n of activeSizes) {
91+ for (const method of METHODS) {
92+ await runOne(method, n)
93+ done++
94+ setProgress((done / total) * 100)
95+ }
96+ }
97+ setRunning(false)
98+ }
99+
100+ const runMethod = async (method: MatmulMethod) => {
101+ setRunning(true)
102+ const total = activeSizes.length
103+ let done = 0
104+ for (const n of activeSizes) {
105+ await runOne(method, n)
106+ done++
107+ setProgress((done / total) * 100)
108+ }
109+ setRunning(false)
110+ }
111+
112+ const toggleSize = (n: number) => {
113+ setSizes((prev) => {
114+ const next = new Set(prev)
115+ if (next.has(n)) next.delete(n)
116+ else next.add(n)
117+ return next
118+ })
119+ }
120+
121+ return (
122+ <Stack spacing={2}>
123+ <Typography variant="body2" color="text.secondary">
124+ Multiplies two random n×n matrices with each method and reports GFLOP/s
125+ (2n³ ÷ time). All methods use identical inputs per size — see the
126+ cross-check below. <strong>WebGPU runs in single precision (f32)</strong>;
127+ every other in-browser method uses double precision (f64).
128+ </Typography>
129+
130+ <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
131+ <Chip
132+ size="small"
133+ color={isolated ? 'success' : 'default'}
134+ label={isolated ? 'cross-origin isolated ✓' : 'not cross-origin isolated'}
135+ />
136+ <Typography variant="caption" color="text.secondary">
137+ {isolated
138+ ? 'WASM threads (SharedArrayBuffer) available — threaded methods enabled.'
139+ : 'Threaded methods need SharedArrayBuffer; they will show as n/a here.'}
140+ </Typography>
141+ </Stack>
142+
143+ <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
144+ {ALL_SIZES.map((n) => (
145+ <FormControlLabel
146+ key={n}
147+ control={<Checkbox size="small" checked={sizes.has(n)} onChange={() => toggleSize(n)} disabled={running} />}
148+ label={`n=${n}`}
149+ />
150+ ))}
151+ <FormControl size="small" sx={{ minWidth: 180 }} disabled={running}>
152+ <InputLabel id="threads-label">threads (mt methods)</InputLabel>
153+ <Select
154+ labelId="threads-label"
155+ label="threads (mt methods)"
156+ value={threads}
157+ onChange={(e) => setThreads(Number(e.target.value))}
158+ >
159+ {THREAD_OPTIONS.map((t) => (
160+ <MenuItem key={t} value={t}>{t} thread{t > 1 ? 's' : ''}</MenuItem>
161+ ))}
162+ </Select>
163+ </FormControl>
164+ </Stack>
165+
166+ <Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
167+ <Button variant="contained" onClick={runAll} disabled={running || activeSizes.length === 0}>
168+ {running ? 'Running…' : 'Run all methods'}
169+ </Button>
170+ {displayMethods.map((m) => (
171+ <Button key={m.id} size="small" variant="outlined" onClick={() => runMethod(m)} disabled={running || activeSizes.length === 0}>
172+ Run {m.label}
173+ </Button>
174+ ))}
175+ </Stack>
176+ {running && <LinearProgress variant="determinate" value={progress} />}
177+
178+ <ResultsTable
179+ methods={displayMethods}
180+ sizes={activeSizes}
181+ cell={(methodId, n) => results[methodId]?.[n]}
182+ />
183+
184+ <Box>
185+ <Typography variant="subtitle2">Cross-check (C[0] for identical inputs)</Typography>
186+ <Typography variant="caption" color="text.secondary" component="div" sx={{ mb: 1 }}>
187+ The f64 methods (JS, both WASM kernels, both BLIS builds) should agree
188+ to ~1e-9; WebGPU (f32) will be close but not identical.
189+ </Typography>
190+ <Stack spacing={0.5}>
191+ {activeSizes.map((n) => (
192+ <Typography key={n} variant="caption" component="div">
193+ n={n}: {displayMethods.map((m) => {
194+ const r = results[m.id]?.[n]
195+ const v = r && typeof r === 'object' ? r.sample.toFixed(6) : '—'
196+ return `${m.label}=${v}`
197+ }).join(' · ')}
198+ </Typography>
199+ ))}
200+ </Stack>
201+ </Box>
202+ </Stack>
203+ )
204+}
src/components/ResultsTable.tsxadded+65−0View file
@@ -0,0 +1,65 @@
1+import {
2+ Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography,
3+} from '@mui/material'
4+import type { MatmulMethod } from '../methods/types'
5+
6+export interface CellResult {
7+ gflops: number
8+ ms: number
9+ sample: number
10+}
11+
12+export type CellState = CellResult | 'pending' | 'unavailable' | 'error' | undefined
13+
14+export interface ResultsTableProps {
15+ methods: MatmulMethod[]
16+ sizes: number[]
17+ cell(methodId: string, n: number): CellState
18+}
19+
20+function CellContent({ state }: { state: CellState }) {
21+ if (state === undefined) return <span>—</span>
22+ if (state === 'pending') return <span>…</span>
23+ if (state === 'unavailable') return <Typography variant="caption" color="text.secondary">n/a</Typography>
24+ if (state === 'error') return <Typography variant="caption" color="error">error</Typography>
25+ return (
26+ <>
27+ <div>{state.gflops.toFixed(2)} GFLOP/s</div>
28+ <Typography variant="caption" color="text.secondary">{state.ms.toFixed(1)} ms</Typography>
29+ </>
30+ )
31+}
32+
33+export function ResultsTable({ methods, sizes, cell }: ResultsTableProps) {
34+ return (
35+ <TableContainer component={Paper} variant="outlined">
36+ <Table size="small">
37+ <TableHead>
38+ <TableRow>
39+ <TableCell>n</TableCell>
40+ {methods.map((m) => (
41+ <TableCell key={m.id} align="right">
42+ {m.label}
43+ <Typography variant="caption" color="text.secondary" component="div">
44+ {m.precision}
45+ </Typography>
46+ </TableCell>
47+ ))}
48+ </TableRow>
49+ </TableHead>
50+ <TableBody>
51+ {sizes.map((n) => (
52+ <TableRow key={n}>
53+ <TableCell>{n}</TableCell>
54+ {methods.map((m) => (
55+ <TableCell key={m.id} align="right">
56+ <CellContent state={cell(m.id, n)} />
57+ </TableCell>
58+ ))}
59+ </TableRow>
60+ ))}
61+ </TableBody>
62+ </Table>
63+ </TableContainer>
64+ )
65+}
src/data/nativeReference.tsadded+18−0View file
@@ -0,0 +1,18 @@
1+// Reference timings from running native/bench_native (dgemm via OpenBLAS) on
2+// a Linux desktop, outside the browser. Hardware-dependent — for rough
3+// comparison only. Regenerate with: native/build.sh && native/bench_native
4+export interface NativeReferenceRow {
5+ n: number
6+ gflops1t: number
7+ gflopsNt: number
8+}
9+
10+export const NATIVE_MAX_THREADS = 12
11+
12+export const NATIVE_REFERENCE: NativeReferenceRow[] = [
13+ { n: 128, gflops1t: 6.78, gflopsNt: 0.33 },
14+ { n: 256, gflops1t: 9.88, gflopsNt: 2.78 },
15+ { n: 512, gflops1t: 27.94, gflopsNt: 12.20 },
16+ { n: 1024, gflops1t: 55.34, gflopsNt: 71.43 },
17+ { n: 2048, gflops1t: 55.23, gflopsNt: 116.04 },
18+]
src/index.cssadded+13−0View file
@@ -0,0 +1,13 @@
1+:root {
2+ font-family: Roboto, system-ui, Avenir, Helvetica, Arial, sans-serif;
3+ line-height: 1.5;
4+}
5+
6+* {
7+ box-sizing: border-box;
8+}
9+
10+html, body, #root {
11+ margin: 0;
12+ padding: 0;
13+}
src/main.tsxadded+10−0View file
@@ -0,0 +1,10 @@
1+import { StrictMode } from 'react'
2+import { createRoot } from 'react-dom/client'
3+import './index.css'
4+import App from './App.tsx'
5+
6+createRoot(document.getElementById('root')!).render(
7+ <StrictMode>
8+ <App />
9+ </StrictMode>,
10+)
src/methods/benchWorker.tsadded+41−0View file
@@ -0,0 +1,41 @@
1+// Runs the CPU-bound methods (plain JS and both WASM kernels) off the main
2+// thread — naive JS/WASM at n=2048 take long enough (~10s) to freeze the tab
3+// otherwise. Regenerates inputs from the (n, seed) pair rather than
4+// receiving them over postMessage, since that's cheaper than structured-
5+// cloning multi-megabyte Float64Arrays for every run.
6+import { generateMatrix } from './random'
7+import { jsMatmul } from './jsMatmul'
8+import { wasmNaiveMatmul, wasmBlockedMatmul } from './wasmMatmul'
9+import type { MatmulMethod } from './types'
10+
11+const METHODS: Record<string, MatmulMethod> = {
12+ js: jsMatmul,
13+ 'wasm-naive': wasmNaiveMatmul,
14+ 'wasm-blocked': wasmBlockedMatmul,
15+}
16+
17+export interface WorkerRequest {
18+ id: number
19+ methodId: string
20+ n: number
21+ seedA: number
22+ seedB: number
23+}
24+
25+export type WorkerResponse =
26+ | { id: number; ms: number; sample: number }
27+ | { id: number; error: string }
28+
29+self.onmessage = async (e: MessageEvent<WorkerRequest>) => {
30+ const { id, methodId, n, seedA, seedB } = e.data
31+ try {
32+ const method = METHODS[methodId]
33+ if (!method?.run) throw new Error(`Unknown method: ${methodId}`)
34+ const a = generateMatrix(n, seedA)
35+ const b = generateMatrix(n, seedB)
36+ const { ms, sample } = await method.run(n, a, b)
37+ self.postMessage({ id, ms, sample } satisfies WorkerResponse)
38+ } catch (err) {
39+ self.postMessage({ id, error: err instanceof Error ? err.message : String(err) } satisfies WorkerResponse)
40+ }
41+}
src/methods/blisMatmul.tsadded+21−0View file
@@ -0,0 +1,21 @@
1+import type { MatmulMethod } from './types'
2+
3+// libFLAME's LAPACK-compat dgemm_ backed by BLIS, compiled to WASM. Two
4+// builds: single-threaded and pthreads (needs SharedArrayBuffer, i.e.
5+// crossOriginIsolated). Both run through threadedClient's dedicated worker.
6+export const blisStMatmul: MatmulMethod = {
7+ id: 'blis-st',
8+ label: 'libFLAME/BLIS (1 thread)',
9+ precision: 'f64',
10+ threadedKind: 'blis-st',
11+ available: () => typeof WebAssembly !== 'undefined',
12+}
13+
14+export const blisMtMatmul: MatmulMethod = {
15+ id: 'blis-mt',
16+ label: 'libFLAME/BLIS (threaded)',
17+ precision: 'f64',
18+ threadedKind: 'blis-mt',
19+ note: 'WASM threads — needs cross-origin isolation (SharedArrayBuffer)',
20+ available: () => typeof WebAssembly !== 'undefined' && globalThis.crossOriginIsolated === true,
21+}
src/methods/jsMatmul.tsadded+34−0View file
@@ -0,0 +1,34 @@
1+import type { MatmulMethod } from './types'
2+
3+// Plain triple loop, ikj order (row of A and row of C stay hot across the
4+// inner loop) — the "no special effort" baseline the other methods are
5+// measured against.
6+function multiply(a: Float64Array, b: Float64Array, n: number): Float64Array {
7+ const c = new Float64Array(n * n)
8+ for (let i = 0; i < n; i++) {
9+ const ai = i * n
10+ for (let k = 0; k < n; k++) {
11+ const aik = a[ai + k]
12+ if (aik === 0) continue
13+ const bk = k * n
14+ for (let j = 0; j < n; j++) {
15+ c[ai + j] += aik * b[bk + j]
16+ }
17+ }
18+ }
19+ return c
20+}
21+
22+export const jsMatmul: MatmulMethod = {
23+ id: 'js',
24+ label: 'JavaScript/TS',
25+ precision: 'f64',
26+ worker: true,
27+ available: () => true,
28+ async run(n, a, b) {
29+ const t0 = performance.now()
30+ const c = multiply(a, b, n)
31+ const ms = performance.now() - t0
32+ return { ms, sample: c[0] }
33+ },
34+}
src/methods/random.tsadded+18−0View file
@@ -0,0 +1,18 @@
1+// Deterministic PRNG so every method multiplies identical inputs for a given size.
2+function mulberry32(seed: number) {
3+ let a = seed
4+ return () => {
5+ a |= 0
6+ a = (a + 0x6d2b79f5) | 0
7+ let t = Math.imul(a ^ (a >>> 15), 1 | a)
8+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
9+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296
10+ }
11+}
12+
13+export function generateMatrix(n: number, seed: number): Float64Array {
14+ const rand = mulberry32(seed)
15+ const m = new Float64Array(n * n)
16+ for (let i = 0; i < m.length; i++) m[i] = rand() - 0.5
17+ return m
18+}
src/methods/threadedClient.tsadded+43−0View file
@@ -0,0 +1,43 @@
1+import type { MatmulResult } from './types'
2+
3+// Dedicated classic worker for the pthread-capable WASM modules (threaded
4+// custom C, libFLAME/BLIS st + mt). It's a classic worker served verbatim
5+// from public/matmul/ (see worker.js for why it can't be a Vite module
6+// worker). Resolve it relative to the deployed base.
7+const WORKER_URL = new URL(
8+ `${import.meta.env.BASE_URL}matmul/worker.js`,
9+ window.location.href,
10+)
11+
12+let worker: Worker | null = null
13+let nextId = 0
14+const pending = new Map<number, { resolve: (r: MatmulResult) => void; reject: (e: Error) => void }>()
15+
16+function getWorker(): Worker {
17+ if (!worker) {
18+ worker = new Worker(WORKER_URL) // classic worker (no { type: 'module' })
19+ worker.onmessage = (e: MessageEvent<{ id: number; ms?: number; sample?: number; error?: string }>) => {
20+ const p = pending.get(e.data.id)
21+ if (!p) return
22+ pending.delete(e.data.id)
23+ if (e.data.error) p.reject(new Error(e.data.error))
24+ else p.resolve({ ms: e.data.ms!, sample: e.data.sample! })
25+ }
26+ }
27+ return worker
28+}
29+
30+export function runInThreadedWorker(
31+ kind: string,
32+ n: number,
33+ seedA: number,
34+ seedB: number,
35+ threads: number,
36+): Promise<MatmulResult> {
37+ const w = getWorker()
38+ const id = nextId++
39+ return new Promise((resolve, reject) => {
40+ pending.set(id, { resolve, reject })
41+ w.postMessage({ id, kind, n, seedA, seedB, threads })
42+ })
43+}
src/methods/types.tsadded+31−0View file
@@ -0,0 +1,31 @@
1+export type Precision = 'f32' | 'f64'
2+
3+export interface MatmulResult {
4+ ms: number
5+ /** C[0], for a cheap cross-method sanity check. */
6+ sample: number
7+}
8+
9+export interface MatmulMethod {
10+ id: string
11+ label: string
12+ precision: Precision
13+ /** Short note shown next to the method (e.g. a precision caveat). */
14+ note?: string
15+ /** Runs synchronously long enough (n=2048 naive JS: ~10s+) to freeze the
16+ * tab, so BenchmarkRunner dispatches it to a Web Worker instead of
17+ * calling run() on the main thread. */
18+ worker?: boolean
19+ /** Methods backed by a pthread-capable WASM module run in a dedicated
20+ * classic worker (threadedClient), keyed by this registry kind
21+ * ('matmul-mt' | 'blis-st' | 'blis-mt'); BenchmarkRunner dispatches these
22+ * specially and their run() is unused. Threaded kinds also require
23+ * crossOriginIsolated. */
24+ threadedKind?: 'matmul-mt' | 'blis-st' | 'blis-mt'
25+ available(): boolean
26+ run?(n: number, a: Float64Array, b: Float64Array): Promise<MatmulResult>
27+}
28+
29+export function gflops(n: number, ms: number): number {
30+ return (2 * n * n * n) / (ms / 1000) / 1e9
31+}
src/methods/wasmMatmul.tsadded+68−0View file
@@ -0,0 +1,68 @@
1+// In a Vite bundle the .wasm cannot be located next to the glue at runtime, so
2+// we resolve its URL via Vite's `?url` import and feed it to Emscripten's
3+// `locateFile` (same pattern qhull-wasm-demo uses for qhull-wasm).
4+import createMatmulModule, { type MatmulExports } from '../../wasm/dist/matmul.js'
5+import wasmUrl from '../../wasm/dist/matmul.wasm?url'
6+import type { MatmulMethod } from './types'
7+
8+let modulePromise: Promise<MatmulExports> | null = null
9+
10+function getModule(): Promise<MatmulExports> {
11+ if (!modulePromise) {
12+ modulePromise = createMatmulModule({ locateFile: () => wasmUrl })
13+ }
14+ return modulePromise
15+}
16+
17+async function run(kernel: '_matmul_naive' | '_matmul_blocked', n: number, a: Float64Array, b: Float64Array) {
18+ const mod = await getModule()
19+ const bytes = n * n * 8
20+ const aPtr = mod._malloc(bytes)
21+ const bPtr = mod._malloc(bytes)
22+ const cPtr = mod._malloc(bytes)
23+ try {
24+ new Float64Array(mod.HEAPF64.buffer, aPtr, n * n).set(a)
25+ new Float64Array(mod.HEAPF64.buffer, bPtr, n * n).set(b)
26+ const t0 = performance.now()
27+ mod[kernel](aPtr, bPtr, cPtr, n)
28+ const ms = performance.now() - t0
29+ // Memory may have grown (and the buffer been replaced) during the call,
30+ // so re-view HEAPF64 rather than reuse a view captured before the call.
31+ const sample = new Float64Array(mod.HEAPF64.buffer, cPtr, n * n)[0]
32+ return { ms, sample }
33+ } finally {
34+ mod._free(aPtr)
35+ mod._free(bPtr)
36+ mod._free(cPtr)
37+ }
38+}
39+
40+export const wasmNaiveMatmul: MatmulMethod = {
41+ id: 'wasm-naive',
42+ label: 'WASM (C, naive)',
43+ precision: 'f64',
44+ worker: true,
45+ available: () => typeof WebAssembly !== 'undefined',
46+ run: (n, a, b) => run('_matmul_naive', n, a, b),
47+}
48+
49+export const wasmBlockedMatmul: MatmulMethod = {
50+ id: 'wasm-blocked',
51+ label: 'WASM (C, blocked+SIMD)',
52+ precision: 'f64',
53+ worker: true,
54+ available: () => typeof WebAssembly !== 'undefined',
55+ run: (n, a, b) => run('_matmul_blocked', n, a, b),
56+}
57+
58+// The same blocked+SIMD kernel, parallelized over rows with WASM pthreads.
59+// Runs through threadedClient's classic worker (see blisMatmul for the shared
60+// mechanism); needs crossOriginIsolated for SharedArrayBuffer.
61+export const wasmBlockedMtMatmul: MatmulMethod = {
62+ id: 'wasm-blocked-mt',
63+ label: 'WASM (C, blocked+SIMD, threaded)',
64+ precision: 'f64',
65+ threadedKind: 'matmul-mt',
66+ note: 'WASM threads — needs cross-origin isolation (SharedArrayBuffer)',
67+ available: () => typeof WebAssembly !== 'undefined' && globalThis.crossOriginIsolated === true,
68+}
src/methods/webgpu-globals.d.tsadded+20−0View file
@@ -0,0 +1,20 @@
1+// lib.dom.d.ts has the WebGPU interface types (GPUDevice, GPUBuffer, ...) but
2+// not these runtime flag namespaces, so tsc can't see them even though every
3+// browser with navigator.gpu provides them.
4+declare const GPUBufferUsage: {
5+ MAP_READ: number
6+ MAP_WRITE: number
7+ COPY_SRC: number
8+ COPY_DST: number
9+ INDEX: number
10+ VERTEX: number
11+ UNIFORM: number
12+ STORAGE: number
13+ INDIRECT: number
14+ QUERY_RESOLVE: number
15+}
16+
17+declare const GPUMapMode: {
18+ READ: number
19+ WRITE: number
20+}
src/methods/webgpuMatmul.tsadded+135−0View file
@@ -0,0 +1,135 @@
1+import type { MatmulMethod } from './types'
2+
3+const TILE = 16
4+
5+// Tiled sgemm: each workgroup computes one TILE×TILE block of C, staging
6+// TILE×TILE tiles of A and B through workgroup (shared) memory.
7+const SHADER = /* wgsl */ `
8+struct Dims { n: u32 }
9+@group(0) @binding(0) var<uniform> dims: Dims;
10+@group(0) @binding(1) var<storage, read> A: array<f32>;
11+@group(0) @binding(2) var<storage, read> B: array<f32>;
12+@group(0) @binding(3) var<storage, read_write> C: array<f32>;
13+
14+var<workgroup> tileA: array<array<f32, ${TILE}>, ${TILE}>;
15+var<workgroup> tileB: array<array<f32, ${TILE}>, ${TILE}>;
16+
17+@compute @workgroup_size(${TILE}, ${TILE})
18+fn main(
19+ @builtin(global_invocation_id) gid: vec3<u32>,
20+ @builtin(local_invocation_id) lid: vec3<u32>,
21+) {
22+ let n = dims.n;
23+ let row = gid.y;
24+ let col = gid.x;
25+ var sum: f32 = 0.0;
26+ let numTiles = (n + ${TILE}u - 1u) / ${TILE}u;
27+
28+ for (var t: u32 = 0u; t < numTiles; t = t + 1u) {
29+ let aCol = t * ${TILE}u + lid.x;
30+ let bRow = t * ${TILE}u + lid.y;
31+ tileA[lid.y][lid.x] = select(0.0, A[row * n + aCol], row < n && aCol < n);
32+ tileB[lid.y][lid.x] = select(0.0, B[bRow * n + col], bRow < n && col < n);
33+ workgroupBarrier();
34+ for (var k: u32 = 0u; k < ${TILE}u; k = k + 1u) {
35+ sum = sum + tileA[lid.y][k] * tileB[k][lid.x];
36+ }
37+ workgroupBarrier();
38+ }
39+
40+ if (row < n && col < n) {
41+ C[row * n + col] = sum;
42+ }
43+}
44+`
45+
46+let devicePromise: Promise<GPUDevice> | null = null
47+
48+function getDevice(): Promise<GPUDevice> {
49+ if (!devicePromise) {
50+ devicePromise = (async () => {
51+ if (!navigator.gpu) throw new Error('WebGPU not supported')
52+ const adapter = await navigator.gpu.requestAdapter()
53+ if (!adapter) throw new Error('No WebGPU adapter available')
54+ return adapter.requestDevice()
55+ })()
56+ }
57+ return devicePromise
58+}
59+
60+let pipelinePromise: Promise<{ device: GPUDevice; pipeline: GPUComputePipeline }> | null = null
61+
62+function getPipeline() {
63+ if (!pipelinePromise) {
64+ pipelinePromise = getDevice().then((device) => {
65+ const module = device.createShaderModule({ code: SHADER })
66+ const pipeline = device.createComputePipeline({
67+ layout: 'auto',
68+ compute: { module, entryPoint: 'main' },
69+ })
70+ return { device, pipeline }
71+ })
72+ }
73+ return pipelinePromise
74+}
75+
76+export const webgpuMatmul: MatmulMethod = {
77+ id: 'webgpu',
78+ label: 'WebGPU',
79+ precision: 'f32',
80+ note: 'single precision only — WGSL has no f64',
81+ available: () => typeof navigator !== 'undefined' && !!navigator.gpu,
82+ async run(n, af64, bf64) {
83+ const { device, pipeline } = await getPipeline()
84+
85+ const a = Float32Array.from(af64)
86+ const b = Float32Array.from(bf64)
87+ const bytes = n * n * 4
88+
89+ const dimsBuf = device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST })
90+ device.queue.writeBuffer(dimsBuf, 0, new Uint32Array([n]))
91+
92+ const aBuf = device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST })
93+ const bBuf = device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST })
94+ const cBuf = device.createBuffer({ size: bytes, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC })
95+ const readBuf = device.createBuffer({ size: bytes, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ })
96+
97+ device.queue.writeBuffer(aBuf, 0, a)
98+ device.queue.writeBuffer(bBuf, 0, b)
99+
100+ const bindGroup = device.createBindGroup({
101+ layout: pipeline.getBindGroupLayout(0),
102+ entries: [
103+ { binding: 0, resource: { buffer: dimsBuf } },
104+ { binding: 1, resource: { buffer: aBuf } },
105+ { binding: 2, resource: { buffer: bBuf } },
106+ { binding: 3, resource: { buffer: cBuf } },
107+ ],
108+ })
109+
110+ const t0 = performance.now()
111+ const encoder = device.createCommandEncoder()
112+ const pass = encoder.beginComputePass()
113+ pass.setPipeline(pipeline)
114+ pass.setBindGroup(0, bindGroup)
115+ const groups = Math.ceil(n / TILE)
116+ pass.dispatchWorkgroups(groups, groups)
117+ pass.end()
118+ encoder.copyBufferToBuffer(cBuf, 0, readBuf, 0, bytes)
119+ device.queue.submit([encoder.finish()])
120+
121+ await readBuf.mapAsync(GPUMapMode.READ)
122+ // Timing includes the GPU->CPU readback, since that's what a real caller pays.
123+ const ms = performance.now() - t0
124+ const sample = new Float32Array(readBuf.getMappedRange())[0]
125+ readBuf.unmap()
126+
127+ dimsBuf.destroy()
128+ aBuf.destroy()
129+ bBuf.destroy()
130+ cBuf.destroy()
131+ readBuf.destroy()
132+
133+ return { ms, sample }
134+ },
135+}
src/methods/workerClient.tsadded+29−0View file
@@ -0,0 +1,29 @@
1+import type { MatmulResult } from './types'
2+import type { WorkerRequest, WorkerResponse } from './benchWorker'
3+
4+let worker: Worker | null = null
5+let nextId = 0
6+const pending = new Map<number, { resolve: (r: MatmulResult) => void; reject: (e: Error) => void }>()
7+
8+function getWorker(): Worker {
9+ if (!worker) {
10+ worker = new Worker(new URL('./benchWorker.ts', import.meta.url), { type: 'module' })
11+ worker.onmessage = (e: MessageEvent<WorkerResponse>) => {
12+ const p = pending.get(e.data.id)
13+ if (!p) return
14+ pending.delete(e.data.id)
15+ if ('error' in e.data) p.reject(new Error(e.data.error))
16+ else p.resolve({ ms: e.data.ms, sample: e.data.sample })
17+ }
18+ }
19+ return worker
20+}
21+
22+export function runInWorker(methodId: string, n: number, seedA: number, seedB: number): Promise<MatmulResult> {
23+ const w = getWorker()
24+ const id = nextId++
25+ return new Promise((resolve, reject) => {
26+ pending.set(id, { resolve, reject })
27+ w.postMessage({ id, methodId, n, seedA, seedB } satisfies WorkerRequest)
28+ })
29+}
tsconfig.app.jsonadded+26−0View file
@@ -0,0 +1,26 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023", "DOM"],
6+ "module": "esnext",
7+ "types": ["vite/client"],
8+ "allowArbitraryExtensions": true,
9+ "skipLibCheck": true,
10+
11+ /* Bundler mode */
12+ "moduleResolution": "bundler",
13+ "allowImportingTsExtensions": true,
14+ "verbatimModuleSyntax": true,
15+ "moduleDetection": "force",
16+ "noEmit": true,
17+ "jsx": "react-jsx",
18+
19+ /* Linting */
20+ "noUnusedLocals": true,
21+ "noUnusedParameters": true,
22+ "erasableSyntaxOnly": true,
23+ "noFallthroughCasesInSwitch": true
24+ },
25+ "include": ["src"]
26+}
tsconfig.jsonadded+7−0View file
@@ -0,0 +1,7 @@
1+{
2+ "files": [],
3+ "references": [
4+ { "path": "./tsconfig.app.json" },
5+ { "path": "./tsconfig.node.json" }
6+ ]
7+}
tsconfig.node.jsonadded+23−0View file
@@ -0,0 +1,23 @@
1+{
2+ "compilerOptions": {
3+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4+ "target": "es2023",
5+ "lib": ["ES2023"],
6+ "types": ["node"],
7+ "skipLibCheck": true,
8+
9+ /* Bundler mode */
10+ "module": "nodenext",
11+ "allowImportingTsExtensions": true,
12+ "verbatimModuleSyntax": true,
13+ "moduleDetection": "force",
14+ "noEmit": true,
15+
16+ /* Linting */
17+ "noUnusedLocals": true,
18+ "noUnusedParameters": true,
19+ "erasableSyntaxOnly": true,
20+ "noFallthroughCasesInSwitch": true
21+ },
22+ "include": ["vite.config.ts"]
23+}
vite.config.tsadded+19−0View file
@@ -0,0 +1,19 @@
1+import { defineConfig } from 'vite'
2+import react from '@vitejs/plugin-react'
3+
4+// Cross-origin isolation headers so SharedArrayBuffer (WASM threads, used by
5+// the threaded BLIS method) is available during local dev/preview. In
6+// production on GitHub Pages these can't be set as headers, so the app also
7+// ships public/coi-serviceworker.js (registered from index.html).
8+const coopCoep = {
9+ 'Cross-Origin-Opener-Policy': 'same-origin',
10+ 'Cross-Origin-Embedder-Policy': 'require-corp',
11+}
12+
13+// https://vite.dev/config/
14+export default defineConfig({
15+ plugins: [react()],
16+ base: './',
17+ server: { headers: coopCoep },
18+ preview: { headers: coopCoep },
19+})
wasm/build-wasm.shadded+45−0View file
@@ -0,0 +1,45 @@
1+#!/usr/bin/env bash
2+# Builds wasm/matmul.c two ways:
3+# - single-threaded ES module -> wasm/dist/matmul.{js,wasm}
4+# (naive + blocked kernels; loaded through Vite's module worker)
5+# - threaded (pthreads) module -> public/matmul/matmul_mt.{js,wasm}
6+# (blocked+SIMD parallelized over rows; loaded by public/matmul/worker.js,
7+# a classic worker, since it spawns nested pthread workers)
8+# Requires: emsdk (expected at ~/emsdk, or already on PATH).
9+set -euo pipefail
10+
11+cd "$(dirname "$0")"
12+
13+if ! command -v emcc >/dev/null 2>&1; then
14+ source ~/emsdk/emsdk_env.sh
15+fi
16+
17+mkdir -p dist ../public/matmul
18+
19+# Single-threaded ES module.
20+emcc -O3 -msimd128 matmul.c \
21+ -sMODULARIZE=1 \
22+ -sEXPORT_ES6=1 \
23+ -sEXPORT_NAME=createMatmulModule \
24+ -sENVIRONMENT=web,worker \
25+ -sALLOW_MEMORY_GROWTH=1 \
26+ -sEXPORTED_FUNCTIONS=_matmul_naive,_matmul_blocked,_malloc,_free \
27+ -sEXPORTED_RUNTIME_METHODS=HEAPF64 \
28+ -o dist/matmul.js
29+
30+cp matmul.d.ts dist/matmul.d.ts
31+
32+# Threaded (pthreads) module. Classic (non-ES6) module so a classic worker can
33+# importScripts it and spawn pthread workers. Fixed shared memory (growth is
34+# costly with shared memory); pool must cover max threads + margin.
35+emcc -O3 -msimd128 -pthread -DMATMUL_MT matmul.c \
36+ -sMODULARIZE=1 \
37+ -sEXPORT_NAME=createMatmulMT \
38+ -sENVIRONMENT=web,worker,node \
39+ -sPTHREAD_POOL_SIZE=20 \
40+ -sINITIAL_MEMORY=512MB \
41+ -sEXPORTED_FUNCTIONS=_matmul_blocked_mt,_malloc,_free \
42+ -sEXPORTED_RUNTIME_METHODS=HEAPF64 \
43+ -o ../public/matmul/matmul_mt.js
44+
45+echo "Built wasm/dist/matmul.{js,wasm,d.ts} + public/matmul/matmul_mt.{js,wasm}"
wasm/matmul.cadded+115−0View file
@@ -0,0 +1,115 @@
1+/*
2+ * Double-precision n×n matrix multiply, compiled to WASM with emscripten.
3+ * Row-major C = A * B. Kernels:
4+ * - matmul_naive: triple loop (sanity baseline)
5+ * - matmul_blocked: blocked + SIMD (the "with optimizations" method)
6+ * - matmul_blocked_mt: the blocked+SIMD kernel parallelized over rows with
7+ * pthreads (only in the -DMATMUL_MT / -pthread build)
8+ */
9+#include <emscripten.h>
10+#include <string.h>
11+
12+#ifdef __wasm_simd128__
13+#include <wasm_simd128.h>
14+#endif
15+
16+EMSCRIPTEN_KEEPALIVE
17+void matmul_naive(const double* a, const double* b, double* c, int n) {
18+ for (int i = 0; i < n; i++) {
19+ for (int j = 0; j < n; j++) {
20+ double sum = 0.0;
21+ for (int k = 0; k < n; k++) sum += a[i * n + k] * b[k * n + j];
22+ c[i * n + j] = sum;
23+ }
24+ }
25+}
26+
27+/* Block size tuned for L1/L2 cache residency of three double blocks. */
28+#define BS 64
29+
30+/* Blocked+SIMD kernel over rows [i0, i1) of C. Each row range is independent
31+ * (disjoint output rows), so threads can own disjoint ranges without locking. */
32+static void blocked_range(const double* a, const double* b, double* c, int n,
33+ int i0, int i1) {
34+ memset(c + (size_t)i0 * n, 0, (size_t)(i1 - i0) * n * sizeof(double));
35+
36+ for (int jj = 0; jj < n; jj += BS) {
37+ int jmax = jj + BS < n ? jj + BS : n;
38+ for (int kk = 0; kk < n; kk += BS) {
39+ int kmax = kk + BS < n ? kk + BS : n;
40+ for (int i = i0; i < i1; i++) {
41+ const double* arow = a + (size_t)i * n;
42+ double* crow = c + (size_t)i * n;
43+ for (int k = kk; k < kmax; k++) {
44+ double aik = arow[k];
45+ const double* brow = b + (size_t)k * n;
46+#ifdef __wasm_simd128__
47+ v128_t vaik = wasm_f64x2_splat(aik);
48+ int j = jj;
49+ for (; j + 1 < jmax; j += 2) {
50+ v128_t vb = wasm_v128_load(brow + j);
51+ v128_t vc = wasm_v128_load(crow + j);
52+ vc = wasm_f64x2_add(vc, wasm_f64x2_mul(vaik, vb));
53+ wasm_v128_store(crow + j, vc);
54+ }
55+ for (; j < jmax; j++) crow[j] += aik * brow[j];
56+#else
57+ for (int j = jj; j < jmax; j++) crow[j] += aik * brow[j];
58+#endif
59+ }
60+ }
61+ }
62+ }
63+}
64+
65+EMSCRIPTEN_KEEPALIVE
66+void matmul_blocked(const double* a, const double* b, double* c, int n) {
67+ blocked_range(a, b, c, n, 0, n);
68+}
69+
70+#ifdef MATMUL_MT
71+#include <pthread.h>
72+
73+typedef struct {
74+ const double* a;
75+ const double* b;
76+ double* c;
77+ int n, i0, i1;
78+} mm_task;
79+
80+static void* mm_worker(void* arg) {
81+ mm_task* t = (mm_task*) arg;
82+ blocked_range(t->a, t->b, t->c, t->n, t->i0, t->i1);
83+ return NULL;
84+}
85+
86+/* Same blocked+SIMD kernel, but the rows of C are split across nthreads
87+ * pthreads (reused from emscripten's pthread pool). */
88+EMSCRIPTEN_KEEPALIVE
89+void matmul_blocked_mt(const double* a, const double* b, double* c, int n,
90+ int nthreads) {
91+ if (nthreads < 1) nthreads = 1;
92+ if (nthreads > n) nthreads = n;
93+
94+ pthread_t th[nthreads];
95+ mm_task tasks[nthreads];
96+ int rows = (n + nthreads - 1) / nthreads;
97+ int nt = 0;
98+
99+ for (int i = 0; i < nthreads; i++) {
100+ int i0 = i * rows;
101+ if (i0 >= n) break;
102+ int i1 = i0 + rows;
103+ if (i1 > n) i1 = n;
104+ tasks[nt] = (mm_task){ a, b, c, n, i0, i1 };
105+ if (pthread_create(&th[nt], NULL, mm_worker, &tasks[nt]) == 0) {
106+ nt++;
107+ } else {
108+ /* Pool exhausted — run this chunk inline rather than dropping it. */
109+ blocked_range(a, b, c, n, i0, i1);
110+ }
111+ }
112+
113+ for (int i = 0; i < nt; i++) pthread_join(th[i], NULL);
114+}
115+#endif
wasm/matmul.d.tsadded+14−0View file
@@ -0,0 +1,14 @@
1+// Hand-written type declaration for matmul.js (emscripten's generated glue
2+// has none). Copied next to dist/matmul.js by build-wasm.sh — TS picks up a
3+// .d.ts colocated with a same-named .js file automatically.
4+export interface MatmulExports {
5+ _matmul_naive(a: number, b: number, c: number, n: number): void
6+ _matmul_blocked(a: number, b: number, c: number, n: number): void
7+ _malloc(bytes: number): number
8+ _free(ptr: number): void
9+ HEAPF64: Float64Array
10+}
11+
12+export default function createMatmulModule(opts?: {
13+ locateFile?: (path: string) => string
14+}): Promise<MatmulExports>