libflame + BLIS compiled to WebAssembly
Reproducible build scripts that clone, patch, and compile libflame (dense
linear algebra / LAPACK) and BLIS (BLAS backend) to WASM with Emscripten.
build-all-wasm.sh produces the single- and multi-threaded static libraries
downstream projects (e.g. matmul-bench) link against. Only source and scripts
are committed; upstream clones and build outputs are regenerated.
14 changed files+1157−0
.gitignoreadded+16−0View file
@@ -0,0 +1,16 @@
1+# Upstream clones (re-cloned + patched by the build scripts)
2+/libflame/
3+/blis/
4+
5+# Build outputs
6+/install/
7+web/dist/
8+
9+# Compiled benchmark/demo artifacts (regenerated by the build scripts)
10+bench/*.js
11+bench/*.wasm
12+bench/bench_native
13+demo/*.js
14+demo/*.wasm
15+
16+*.o
README.mdadded+239−0View file
@@ -0,0 +1,239 @@
1+# libflame → WebAssembly
2+
3+Compiling [libflame](https://github.com/flame/libflame) (dense linear algebra /
4+LAPACK) to WASM with Emscripten. **Status: working.** The static library builds,
5+links, and passes numerical tests under Node.
6+
7+## Layout
8+
9+- `build-all-wasm.sh` — builds every artifact downstream projects need (see below)
10+- `build-wasm.sh` — end-to-end reproducible libflame build (clone → patch → configure → make → install)
11+- `build-blis-wasm.sh` — builds BLIS (generic config) to WASM as a fast BLAS backend
12+- `libflame/` — upstream clone, re-cloned + patched by `build-wasm.sh` (gitignored)
13+- `blis/` — upstream BLIS clone, re-cloned + patched by `build-blis-wasm.sh` (gitignored)
14+- `install/` — built artifacts: `lib/libflame.a` (~15 MB, 4812 objects) and a
15+ single flattened `include/FLAME.h` (gitignored)
16+- `demo/` — test program exercising the native FLAME/C API (`FLA_Chol`) and the
17+ LAPACK compatibility layer (`dgesv_`)
18+- `bench/` — benchmark comparing the WASM build against native OpenBLAS
19+ (same source compiled both ways)
20+- `web/` — browser benchmark page (see "Running in the browser")
21+
22+Only source (the build scripts, `*.c`, `web/*`, `demo/*.c`, this README) is
23+committed; the upstream clones and all build outputs are gitignored and
24+regenerated by the scripts.
25+
26+## Build
27+
28+`build-wasm.sh` alone builds single-threaded libflame. To build **every** WASM
29+static library downstream projects link against — used e.g. by
30+[concept-collection/matmul-bench](https://github.com/concept-collection/matmul-bench),
31+which clones this repo in CI and runs it — use:
32+
33+```sh
34+./build-all-wasm.sh
35+```
36+
37+which produces (building libflame and BLIS both single- and multi-threaded):
38+
39+- `install/lib/libflame.a` — single-threaded libflame
40+- `install/lib/libflame-mt.a` — libflame compiled with `-pthread`
41+- `install/lib/libblis-st.a` — single-threaded BLIS
42+- `blis/lib/generic/libblis.a` — BLIS compiled with pthreads
43+
44+This is slow (libflame is compiled twice); downstream CI should cache the outputs
45+keyed on this repo's commit SHA. For just the single-threaded libflame:
46+
47+```sh
48+./build-wasm.sh
49+```
50+
51+## Demo
52+
53+```sh
54+cd demo
55+emcc demo.c -I ../install/include ../install/lib/libflame.a -O2 -o demo.js
56+node demo.js
57+```
58+
59+Output:
60+
61+```
62+libflame WASM demo
63+FLA_Chol: L(0,0) = 2.039608 (expected 2.039608) OK
64+dgesv_: info = 0, x = [0.800000, 1.400000] (expected [0.8, 1.4]) OK
65+ALL TESTS PASSED
66+```
67+
68+The linked demo (`FLA_Init` + Cholesky + `dgesv_` and their transitive
69+dependencies) comes out to ~1 MB of wasm — dead code elimination keeps only
70+what you call.
71+
72+## What was needed to make it work
73+
74+1. **No Fortran.** Emscripten has no Fortran compiler, so configure runs with
75+ `--disable-autodetect-f77-*`. The build stays all-C:
76+ `--enable-builtin-blas` uses libflame's f2c-translated reference BLAS, and
77+ `--enable-lapack2flame --enable-legacy-lapack` adds a complete LAPACK API
78+ from f2c'd C sources.
79+
80+2. **No `--host` triple.** The bundled `config.sub` predates wasm targets and
81+ rejects `wasm32-unknown-emscripten`. Setting `CC=emcc` (via `emconfigure`)
82+ is sufficient; configure even recognizes `emcc` as a compiler vendor —
83+ though it doesn't know its optimization flags, so `-O2` is injected into
84+ `config.mk` after configure.
85+
86+3. **`void` vs `int` prototype mismatch (the real WASM blocker).** libflame's
87+ internal headers declare Fortran BLAS routines as returning `void`, while
88+ the f2c'd built-in BLAS defines them returning `int`. On native targets this
89+ ABI mismatch is silently harmless; on WebAssembly call sites and definitions
90+ must agree exactly, so the module **fails wasm validation at link time**
91+ (`wasm-ld: function signature mismatch` → `wasm-validator error`). Fix: a
92+ one-line sed changing the 78 `void F77_*` prototypes to `int` in
93+ `src/base/flamec/blis/include/blis_prototypes_blas.h`.
94+
95+4. **Stale-archive quirk.** The Makefile's `--enable-max-arg-list-hack`
96+ archiving appends object paths to `ar_obj_list` per compile, so `make` after
97+ an incremental rebuild can produce a stale or even empty `libflame.a`. The
98+ build script re-archives from the full `obj/` tree with `emar crs`.
99+
100+## Performance vs native OpenBLAS
101+
102+`bench/bench.c` runs `dgemm`, `dpotrf`, and `dgetrf` through the same
103+Fortran-style interface in all builds (12-core machine, OpenBLAS 0.3.29,
104+emcc 5.0.4 under Node 24; results numerically identical across builds).
105+GFLOP/s:
106+
107+| routine | n | WASM f2c BLAS | WASM BLIS | OpenBLAS 1 thread | OpenBLAS 12 threads |
108+|---------|------|---------------|-----------|-------------------|---------------------|
109+| dgemm | 2000 | 3.0 | 11.7 | 46.8 | 90.1 |
110+| dgemm | 4000 | 1.8 | 12.1 | 53.5 | 183.0 |
111+| dpotrf | 2000 | 3.9 | 10.9 | 44.3 | 8.2* |
112+| dpotrf | 4000 | 4.2 | 11.2 | 51.3 | 142.5 |
113+| dgetrf | 2000 | 3.9 | 10.1 | 40.8 | 58.0 |
114+| dgetrf | 4000 | 4.2 | 10.0 | 48.3 | 71.0 |
115+
116+\* multithreaded numbers at smaller sizes are noisy (thread-pool warmup).
117+
118+Takeaways:
119+
120+- With the built-in f2c BLAS, the WASM build runs at ~1.8–4.2 GFLOP/s — the
121+ f2c reference BLAS is scalar C with no SIMD or cache blocking, and reference
122+ `dgemm` collapses at n=4000 when the working set falls out of cache
123+ (libflame's blocked factorizations hold ~4.2 even then).
124+- Swapping in **WASM-built BLIS** (generic C kernels + `-msimd128`) lifts
125+ everything to **~10–12 GFLOP/s** — a 2.5–7× improvement, now only **4–5×
126+ slower than single-threaded** native OpenBLAS (and ~7–17× slower than all
127+ 12 cores, which WASM can't use single-threaded).
128+- Concretely, at n=4000: LU factorization takes 4.3 s (was 10.2 s with f2c),
129+ and dgemm takes 10.6 s (was 73 s), vs 0.9 s / 2.4 s native single-threaded.
130+
131+## Using BLIS as the BLAS backend
132+
133+`./build-blis-wasm.sh` builds `blis/lib/generic/libblis.a`. Then link it
134+**before** `libflame.a` — the linker resolves every BLAS symbol from BLIS and
135+libflame's f2c BLAS members are never pulled in (they remain as fallback for
136+the banded/packed level-2 routines BLIS doesn't provide):
137+
138+```sh
139+emcc app.c -I install/include blis/lib/generic/libblis.a install/lib/libflame.a \
140+ -sALLOW_MEMORY_GROWTH -o app.js
141+```
142+
143+(`-sALLOW_MEMORY_GROWTH` is required: BLIS allocates memory pools beyond the
144+default 16 MB heap.)
145+
146+### Threading (wasm pthreads)
147+
148+Building both libraries with `-pthread` and BLIS with
149+`--enable-threading=pthreads` enables real multithreading via Web Workers +
150+SharedArrayBuffer:
151+
152+```sh
153+PTHREAD=1 ./build-wasm.sh # → install/lib/libflame-mt.a
154+THREADING=pthreads ./build-blis-wasm.sh # → blis/lib/generic/libblis.a (mt)
155+
156+emcc -O2 -pthread app.c blis/lib/generic/libblis.a install/lib/libflame-mt.a \
157+ -sPTHREAD_POOL_SIZE=14 -sINITIAL_MEMORY=1024MB -o app.js
158+```
159+
160+Every object linked into a shared-memory wasm module must be compiled with
161+`-pthread` (atomics + bulk-memory), hence the libflame rebuild. Set the thread
162+count at runtime with `bli_thread_set_num_threads(n)` (or `BLIS_NUM_THREADS`).
163+GFLOP/s at n=4000 under Node (12-core machine):
164+
165+| routine | 1t | 4t | 8t | 12t |
166+|---------|------|------|------|------|
167+| dgemm | 12.1 | 31.0 | 46.1 | 33.2 |
168+| dpotrf | 11.2 | 23.3 | 29.3 | 5.9 |
169+| dgetrf | 10.0 | 22.7 | 29.1 | 3.4 |
170+
171+- **8 threads is the sweet spot**: threaded WASM `dgemm` (46 GFLOP/s) matches
172+ *single-threaded native OpenBLAS* (53.5), and the factorizations land within
173+ ~1.7× of it — dgetrf is just 2.4× off native OpenBLAS using all 12 cores.
174+- Full thread counts (12) collapse, badly for the factorizations: BLIS
175+ spawns/joins threads per BLAS call, wasm worker scheduling is expensive, and
176+ the main thread + 12 workers oversubscribe 12 cores. Leave headroom.
177+- Browser deployment requires cross-origin isolation (COOP/COEP headers) for
178+ SharedArrayBuffer; Node needs nothing special.
179+
180+Making BLIS coexist with libflame under WASM's exact-signature rules required
181+patches (all scripted in `build-blis-wasm.sh`):
182+
183+1. BLAS interface functions flipped from `void` to `int` returns (f2c
184+ convention, matching every declaration in libflame's f2c code). Bare
185+ `return;` → `return 0;` (a hard error in C23).
186+2. BLIS's f2c-derived compat sources (banded/packed level-2, `lsame_`,
187+ `xerbla_`) deleted — libflame already provides them, and BLIS's versions
188+ use 4-arg `lsame_` / 3-arg `xerbla_` (hidden Fortran string lengths) while
189+ libflame's 1,000+ call sites use the 2-arg form. BLIS's internal calls had
190+ the `(ftnlen)` args stripped to match.
191+3. `defined(EMSCRIPTEN)` → `defined(__EMSCRIPTEN__)` in `bli_system.h`
192+ (BLIS has an Emscripten branch, but tests the obsolete macro name).
193+4. Built with `CC_VENDOR=clang` (the generic config rejects the "emcc" vendor
194+ string; emcc is clang underneath).
195+
196+## Running in the browser
197+
198+`web/` contains an interactive benchmark page:
199+
200+```sh
201+cd web
202+./build-web.sh # builds dist/bench_st.{js,wasm} and dist/bench_mt.{js,wasm}
203+python3 serve.py # serves on http://localhost:8123 with COOP/COEP headers
204+```
205+
206+Then open <http://localhost:8123>. Pick build (single-threaded / pthreads),
207+routine, matrix size, and thread count; results accumulate in a table.
208+
209+Notes:
210+- The threaded build needs cross-origin isolation (SharedArrayBuffer), which
211+ is why `serve.py` sets `Cross-Origin-Opener-Policy: same-origin` and
212+ `Cross-Origin-Embedder-Policy: require-corp`. The page shows a
213+ `crossOriginIsolated` badge; without isolation it falls back to the
214+ single-threaded build.
215+- The benchmark runs in a Web Worker, so the UI stays responsive; the threaded
216+ build spawns its pthread workers from that worker (nested workers — fine in
217+ Chrome/Firefox, may fail in older Safari).
218+- The threaded module reserves 1 GB of shared memory up front; the first
219+ threaded run includes worker-pool startup cost, so run twice for steady-state
220+ numbers.
221+
222+Measured in-browser results (Chrome-family, same 12-core machine, n=2000,
223+GFLOP/s) — essentially identical to Node, and threaded dgemm **beats
224+single-threaded native OpenBLAS** (46.8):
225+
226+| routine | browser 1t | browser 8t | native OpenBLAS 1T |
227+|---------|------------|------------|--------------------|
228+| dgemm | 12.7 | 50.3 | 46.8 |
229+| dpotrf | 10.9 | 21.9 | 44.3 |
230+| dgetrf | 10.2 | 19.4 | 40.8 |
231+
232+## Caveats
233+
234+- Single-threaded (no `--enable-multithreading`); SuperMatrix and SSE
235+ intrinsics disabled. BLIS built with `--disable-threading`.
236+- wasm32: 32-bit `int`/pointers, 4 GB memory ceiling. LAPACK integer arguments
237+ are C `int` (LP32-compatible).
238+- Two archive members define `lsame_` upstream; the manual re-archive keeps one
239+ (they're the same trivial routine).
bench/bench.cadded+154−0View file
@@ -0,0 +1,154 @@
1+/*
2+ * Benchmark: dgemm / dpotrf / dgetrf through the Fortran-style BLAS/LAPACK
3+ * interface. Compiles unchanged against native OpenBLAS and against the
4+ * WASM libflame build (f2c built-in BLAS + lapack2flame).
5+ *
6+ * native: gcc -O2 bench.c -o bench_native -lopenblas
7+ * wasm: emcc -O2 bench.c ../install/lib/libflame.a -sALLOW_MEMORY_GROWTH -o bench.js
8+ */
9+#include <stdio.h>
10+#include <stdlib.h>
11+#include <string.h>
12+#include <time.h>
13+
14+/* The WASM libflame defines these with f2c's int return; native OpenBLAS
15+ (gfortran) uses void. Signatures must match exactly under wasm. */
16+#ifdef __EMSCRIPTEN__
17+#define BLASRET int
18+#else
19+#define BLASRET void
20+#endif
21+
22+extern BLASRET dgemm_( char* transa, char* transb, int* m, int* n, int* k,
23+ double* alpha, double* a, int* lda, double* b, int* ldb,
24+ double* beta, double* c, int* ldc );
25+extern int dpotrf_( char* uplo, int* n, double* a, int* lda, int* info );
26+extern int dgetrf_( int* m, int* n, double* a, int* lda, int* ipiv, int* info );
27+
28+static double now_sec( void )
29+{
30+ struct timespec ts;
31+ clock_gettime( CLOCK_MONOTONIC, &ts );
32+ return ts.tv_sec + 1e-9 * ts.tv_nsec;
33+}
34+
35+/* Deterministic pseudo-random fill so both builds do identical work. */
36+static unsigned long long rng_state = 12345;
37+static double frand( void )
38+{
39+ rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
40+ return ( ( rng_state >> 33 ) & 0xffffff ) / (double) 0x1000000 - 0.5;
41+}
42+
43+static void fill_random( double* a, int n2 )
44+{
45+ int i;
46+ rng_state = 12345;
47+ for ( i = 0; i < n2; i++ ) a[i] = frand();
48+}
49+
50+/* Diagonally dominant SPD matrix for Cholesky. */
51+static void fill_spd( double* a, int n )
52+{
53+ int i, j;
54+ rng_state = 12345;
55+ for ( j = 0; j < n; j++ )
56+ for ( i = 0; i < n; i++ )
57+ a[ j*n + i ] = ( i == j ) ? n : 0.5 * frand();
58+ for ( j = 0; j < n; j++ )
59+ for ( i = 0; i < j; i++ ) {
60+ double v = 0.5 * ( a[ j*n + i ] + a[ i*n + j ] );
61+ a[ j*n + i ] = a[ i*n + j ] = v;
62+ }
63+}
64+
65+static void bench_dgemm( int n )
66+{
67+ double *a = malloc( (size_t)n*n*sizeof(double) );
68+ double *b = malloc( (size_t)n*n*sizeof(double) );
69+ double *c = malloc( (size_t)n*n*sizeof(double) );
70+ double alpha = 1.0, beta = 0.0, t, gflops;
71+ char tr = 'N';
72+
73+ fill_random( a, n*n );
74+ fill_random( b, n*n );
75+ memset( c, 0, (size_t)n*n*sizeof(double) );
76+
77+ t = now_sec();
78+ dgemm_( &tr, &tr, &n, &n, &n, &alpha, a, &n, b, &n, &beta, c, &n );
79+ t = now_sec() - t;
80+
81+ gflops = 2.0 * n * (double)n * n / t / 1e9;
82+ printf( "dgemm n=%5d %10.3f s %8.2f GFLOP/s (check c[0]=%.6f)\n",
83+ n, t, gflops, c[0] );
84+ free( a ); free( b ); free( c );
85+}
86+
87+static void bench_dpotrf( int n )
88+{
89+ double *a = malloc( (size_t)n*n*sizeof(double) );
90+ double t, gflops;
91+ char lo = 'L';
92+ int info = 0;
93+
94+ fill_spd( a, n );
95+ t = now_sec();
96+ dpotrf_( &lo, &n, a, &n, &info );
97+ t = now_sec() - t;
98+
99+ gflops = ( (double)n * n * n / 3.0 ) / t / 1e9;
100+ printf( "dpotrf n=%5d %10.3f s %8.2f GFLOP/s (info=%d, L00=%.6f)\n",
101+ n, t, gflops, info, a[0] );
102+ free( a );
103+}
104+
105+static void bench_dgetrf( int n )
106+{
107+ double *a = malloc( (size_t)n*n*sizeof(double) );
108+ int *ipiv = malloc( (size_t)n*sizeof(int) );
109+ double t, gflops;
110+ int info = 0;
111+
112+ fill_random( a, n*n );
113+ t = now_sec();
114+ dgetrf_( &n, &n, a, &n, ipiv, &info );
115+ t = now_sec() - t;
116+
117+ gflops = ( 2.0 * n * (double)n * n / 3.0 ) / t / 1e9;
118+ printf( "dgetrf n=%5d %10.3f s %8.2f GFLOP/s (info=%d)\n",
119+ n, t, gflops, info );
120+ free( a ); free( ipiv );
121+}
122+
123+#ifdef BLIS_MT
124+/* dim_t is int32 in this BLIS build (--int-size=32). */
125+extern void bli_thread_set_num_threads( int n_threads );
126+#endif
127+
128+int main( int argc, char** argv )
129+{
130+ int sizes_default[] = { 500, 1000, 2000 };
131+ int *sizes = sizes_default, nsizes = 3, i;
132+
133+ /* Optional first arg "tN" sets the BLIS thread count (BLIS_MT builds). */
134+ if ( argc > 1 && argv[1][0] == 't' ) {
135+#ifdef BLIS_MT
136+ int nt = atoi( argv[1] + 1 );
137+ if ( nt > 0 ) bli_thread_set_num_threads( nt );
138+ printf( "BLIS threads: %d\n", nt );
139+#endif
140+ argc--; argv++;
141+ }
142+
143+ if ( argc > 1 ) {
144+ nsizes = argc - 1;
145+ sizes = malloc( nsizes * sizeof(int) );
146+ for ( i = 0; i < nsizes; i++ ) sizes[i] = atoi( argv[i+1] );
147+ }
148+
149+ for ( i = 0; i < nsizes; i++ ) bench_dgemm( sizes[i] );
150+ for ( i = 0; i < nsizes; i++ ) bench_dpotrf( sizes[i] );
151+ for ( i = 0; i < nsizes; i++ ) bench_dgetrf( sizes[i] );
152+
153+ return 0;
154+}
bench/wasm_results.txtadded+10−0View file
@@ -0,0 +1,10 @@
1+dgemm n= 500 0.143 s 1.75 GFLOP/s (check c[0]=1.454343)
2+dgemm n= 1000 0.470 s 4.26 GFLOP/s (check c[0]=-0.136566)
3+dgemm n= 2000 5.275 s 3.03 GFLOP/s (check c[0]=0.380984)
4+dpotrf n= 500 0.026 s 1.63 GFLOP/s (info=0, L00=22.360680)
5+dpotrf n= 1000 0.086 s 3.87 GFLOP/s (info=0, L00=31.622777)
6+dpotrf n= 2000 0.687 s 3.88 GFLOP/s (info=0, L00=44.721360)
7+dgetrf n= 500 0.032 s 2.63 GFLOP/s (info=0)
8+dgetrf n= 1000 0.193 s 3.46 GFLOP/s (info=0)
9+dgetrf n= 2000 1.383 s 3.86 GFLOP/s (info=0)
10+EXIT: 0
bench/wasm_results_4000.txtadded+4−0View file
@@ -0,0 +1,4 @@
1+dgemm n= 4000 73.259 s 1.75 GFLOP/s (check c[0]=6.004976)
2+dpotrf n= 4000 5.121 s 4.17 GFLOP/s (info=0, L00=63.245553)
3+dgetrf n= 4000 10.225 s 4.17 GFLOP/s (info=0)
4+EXIT: 0
build-all-wasm.shadded+49−0View file
@@ -0,0 +1,49 @@
1+#!/bin/bash
2+# Build every WASM artifact downstream projects need, from a clean checkout.
3+# Produces the four static libraries that (e.g.) concept-collection/matmul-bench
4+# links against:
5+#
6+# install/lib/libflame.a single-threaded libflame
7+# install/lib/libflame-mt.a libflame compiled with -pthread
8+# install/lib/libblis-st.a single-threaded BLIS (BLAS backend)
9+# blis/lib/generic/libblis.a BLIS compiled with pthreads
10+#
11+# Each variant is built in a freshly re-cloned tree (rm -rf before each build):
12+# the single- and multi-threaded objects differ only by compile flags, and
13+# libflame's make does not rebuild objects when just config.mk changes, so a
14+# clean tree is the only reliable way to get correct -pthread archives.
15+#
16+# This is slow (libflame is built twice, BLIS twice). Downstream CI should
17+# cache the outputs keyed on this repo's commit SHA.
18+#
19+# Requires: emsdk (expected at ~/emsdk, or already on PATH), git, a C toolchain.
20+set -euo pipefail
21+
22+cd "$(dirname "$0")"
23+
24+if ! command -v emcc >/dev/null 2>&1; then
25+ source ~/emsdk/emsdk_env.sh
26+fi
27+
28+echo "==> [1/4] libflame (single-threaded)"
29+rm -rf libflame
30+./build-wasm.sh
31+
32+echo "==> [2/4] libflame (pthreads)"
33+rm -rf libflame
34+PTHREAD=1 ./build-wasm.sh
35+
36+echo "==> [3/4] BLIS (single-threaded)"
37+rm -rf blis
38+./build-blis-wasm.sh
39+mkdir -p install/lib
40+cp blis/lib/generic/libblis.a install/lib/libblis-st.a
41+
42+echo "==> [4/4] BLIS (pthreads)"
43+rm -rf blis
44+THREADING=pthreads ./build-blis-wasm.sh
45+
46+echo
47+echo "Done. Artifacts:"
48+ls -la install/lib/libflame.a install/lib/libflame-mt.a \
49+ install/lib/libblis-st.a blis/lib/generic/libblis.a
build-blis-wasm.shadded+79−0View file
@@ -0,0 +1,79 @@
1+#!/bin/bash
2+# Build BLIS (generic config) as a WebAssembly static library to serve as a
3+# fast BLAS backend for the WASM libflame build.
4+#
5+# Link order matters: put libblis.a BEFORE libflame.a so the linker resolves
6+# BLAS symbols from BLIS; libflame's built-in f2c BLAS members are then never
7+# pulled in (they remain only as fallback for routines BLIS doesn't provide,
8+# e.g. banded/packed level-2).
9+#
10+# Usage: ./build-blis-wasm.sh (single-threaded)
11+# THREADING=pthreads ./build-blis-wasm.sh (wasm-threads build;
12+# link apps with -pthread -sPTHREAD_POOL_SIZE=N, and all other
13+# linked objects must also be compiled with -pthread)
14+set -euo pipefail
15+
16+THREADING=${THREADING:-single}
17+EXTRA_CFLAGS=""
18+if [ "$THREADING" = "pthreads" ]; then
19+ EXTRA_CFLAGS="-pthread"
20+fi
21+
22+cd "$(dirname "$0")"
23+
24+if ! command -v emcc >/dev/null 2>&1; then
25+ source ~/emsdk/emsdk_env.sh
26+fi
27+
28+if [ ! -d blis ]; then
29+ git clone --depth 1 https://github.com/flame/blis
30+fi
31+cd blis
32+
33+# --- Patches to make BLIS's BLAS interface WASM-compatible with libflame ---
34+#
35+# WASM requires call sites and definitions to agree on exact signatures.
36+# libflame's f2c-derived code declares BLAS subroutines returning int and
37+# calls lsame_/xerbla_ with 2 args (no hidden Fortran string lengths).
38+# Stock BLIS uses void returns and 4-arg lsame_ / 3-arg xerbla_.
39+
40+# 1. Drop BLIS's f2c-derived compat sources (banded/packed level-2 routines,
41+# lsame_, xerbla_). libflame.a already provides all of them in the
42+# 2-arg/int f2c convention. Keep cabs1 (no conflicts) and all headers.
43+ls frame/compat/f2c/*.c | grep -v cabs1 | xargs rm -f
44+rm -f frame/compat/f2c/bla_xerbla_array.h
45+sed -i '/#include "bla_xerbla_array.h"/d' frame/compat/bli_blas_defs.h
46+
47+# 2. Prototype lsame_/xerbla_ with 2 args to match libflame's definitions.
48+sed -i 's/, long ca_len, long cb_len//; s/, int ca_len, int cb_len//' frame/compat/f2c/bla_lsame.h
49+sed -i 's/, ftnlen srname_len//' frame/compat/f2c/bla_xerbla.h
50+
51+# 3. Strip hidden string-length args from BLIS's internal lsame_/xerbla_ calls.
52+sed -i 's/, *(ftnlen)[0-9]*//g' frame/compat/check/*.h
53+
54+# 4. Give every BLAS interface function an int return (f2c convention), so
55+# signatures match libflame's declarations. Bare `return;` must become
56+# `return 0;` (a hard error in C23 otherwise).
57+sed -i 's/void PASTEF77/int PASTEF77/g' \
58+ frame/compat/*.c frame/compat/*.h frame/compat/f2c/*.h \
59+ frame/compat/extra/*.c frame/compat/extra/*.h frame/compat/amd/*.c
60+sed -i 's/\breturn; \\/return 0; \\/g; s/^\( *\)return;$/\1return 0;/' \
61+ frame/compat/*.c frame/compat/check/*.h frame/compat/extra/*.c frame/compat/amd/*.c
62+
63+# 5. BLIS tests the obsolete EMSCRIPTEN macro; modern emcc defines __EMSCRIPTEN__.
64+sed -i 's/defined(EMSCRIPTEN)/defined(__EMSCRIPTEN__)/' frame/include/bli_system.h
65+
66+# --- Configure and build ---
67+# -msimd128 lets the generic C kernels autovectorize to wasm SIMD.
68+CC=emcc AR=emar RANLIB=emranlib CFLAGS="-msimd128 $EXTRA_CFLAGS" ./configure \
69+ --disable-shared --enable-static \
70+ --enable-blas --disable-cblas \
71+ --int-size=32 --blas-int-size=32 \
72+ --complex-return=gnu \
73+ --enable-threading="$THREADING" \
74+ generic
75+
76+# The generic make_defs.mk rejects the "emcc" vendor string; emcc is clang.
77+make -j"$(nproc)" CC_VENDOR=clang
78+
79+echo "Done. Library: $(pwd)/lib/generic/libblis.a"
build-wasm.shadded+75−0View file
@@ -0,0 +1,75 @@
1+#!/bin/bash
2+# Build libflame as a WebAssembly static library using Emscripten.
3+#
4+# Usage: ./build-wasm.sh (single-threaded)
5+# PTHREAD=1 ./build-wasm.sh (compile with -pthread so the library
6+# can link into shared-memory/threaded wasm modules; installs as
7+# libflame-mt.a alongside the single-threaded libflame.a)
8+# Requires: emsdk (expected at ~/emsdk, or already on PATH)
9+set -euo pipefail
10+
11+PTHREAD=${PTHREAD:-0}
12+EXTRA_CFLAGS=""
13+if [ "$PTHREAD" = "1" ]; then
14+ EXTRA_CFLAGS=" -pthread"
15+fi
16+
17+cd "$(dirname "$0")"
18+ROOT=$(pwd)
19+PREFIX=$ROOT/install
20+
21+if ! command -v emcc >/dev/null 2>&1; then
22+ source ~/emsdk/emsdk_env.sh
23+fi
24+
25+# 1. Clone
26+if [ ! -d libflame ]; then
27+ git clone --depth 1 https://github.com/flame/libflame
28+fi
29+cd libflame
30+
31+# 2. Patch: libflame declares the Fortran-style BLAS as returning void, but the
32+# f2c'd built-in BLAS defines those routines returning int. Harmless on
33+# native targets, but a fatal signature mismatch under wasm-ld (calls
34+# through mismatched signatures fail wasm validation).
35+sed -i 's/^void F77_/int F77_/' src/base/flamec/blis/include/blis_prototypes_blas.h
36+
37+# 3. Configure. Notes:
38+# - No --host triple: the bundled config.sub predates wasm; emcc via CC is enough.
39+# - Fortran autodetection must be off (Emscripten has no Fortran compiler).
40+# - builtin-blas uses the f2c C translations, so the library is self-contained.
41+# - lapack2flame + legacy-lapack adds a full LAPACK API (f2c C sources, no Fortran).
42+emconfigure ./configure \
43+ --prefix="$PREFIX" \
44+ --disable-autodetect-f77-ldflags \
45+ --disable-autodetect-f77-name-mangling \
46+ --enable-builtin-blas \
47+ --enable-lapack2flame \
48+ --enable-legacy-lapack \
49+ --disable-dynamic-build \
50+ --enable-static-build \
51+ --enable-vector-intrinsics=none \
52+ CC=emcc AR=emar RANLIB=emranlib
53+
54+# 4. configure doesn't know optimization flags for the "emcc" vendor; add -O2.
55+sed -i "s/^COPTFLAGS := *\$/COPTFLAGS := -O2$EXTRA_CFLAGS/" config/*/config.mk
56+
57+# 5. Build.
58+make -j"$(nproc)"
59+
60+# 6. Re-archive from the full object tree. The Makefile's incremental
61+# archiving (ar_obj_list) only covers objects compiled in the current make
62+# run, which can leave the archive stale or incomplete across rebuilds.
63+LIBDIR=$(dirname "$(find lib -name libflame.a)")
64+rm -f "$LIBDIR/libflame.a"
65+find obj -name '*.o' | sort | xargs -n 500 emar crs "$LIBDIR/libflame.a"
66+
67+# 7. Install (single flattened FLAME.h + libflame.a).
68+if [ "$PTHREAD" = "1" ]; then
69+ mkdir -p "$PREFIX/lib"
70+ cp "$LIBDIR/libflame.a" "$PREFIX/lib/libflame-mt.a"
71+ echo "Done. Library: $PREFIX/lib/libflame-mt.a"
72+else
73+ make install
74+ echo "Done. Library: $PREFIX/lib/libflame.a Header: $PREFIX/include/FLAME.h"
75+fi
demo/demo.cadded+86−0View file
@@ -0,0 +1,86 @@
1+/*
2+ * Demo: exercise libflame compiled to WebAssembly.
3+ *
4+ * 1. Native FLAME/C API: Cholesky factorization of an SPD matrix.
5+ * 2. LAPACK compatibility layer (lapack2flame): solve A x = b with dgesv_.
6+ */
7+#include <stdio.h>
8+#include <math.h>
9+#include "FLAME.h"
10+
11+/* LAPACK-style prototype provided by the lapack2flame layer. */
12+extern int dgesv_( int* n, int* nrhs, double* a, int* lda,
13+ int* ipiv, double* b, int* ldb, int* info );
14+
15+static int test_flame_cholesky( void )
16+{
17+ int n = 4, i, j;
18+ FLA_Obj A;
19+ double* buf;
20+
21+ /* SPD matrix: A = M^T M + n*I built by hand (column-major). */
22+ double A0[16] = {
23+ 4.16, -3.12, 0.56, -0.10,
24+ -3.12, 5.03, -0.83, 1.18,
25+ 0.56, -0.83, 0.76, 0.34,
26+ -0.10, 1.18, 0.34, 1.18
27+ };
28+
29+ FLA_Obj_create( FLA_DOUBLE, n, n, 0, 0, &A );
30+ buf = (double*) FLA_Obj_buffer_at_view( A );
31+ for ( j = 0; j < n; j++ )
32+ for ( i = 0; i < n; i++ )
33+ buf[ j * n + i ] = A0[ j * n + i ];
34+
35+ if ( FLA_Chol( FLA_LOWER_TRIANGULAR, A ) != FLA_SUCCESS ) {
36+ printf( "FLA_Chol: FAILED (matrix not SPD?)\n" );
37+ FLA_Obj_free( &A );
38+ return 1;
39+ }
40+
41+ /* Check L(0,0) = sqrt(4.16) */
42+ double expect = sqrt( 4.16 );
43+ double got = buf[0];
44+ printf( "FLA_Chol: L(0,0) = %.6f (expected %.6f) %s\n",
45+ got, expect, fabs( got - expect ) < 1e-12 ? "OK" : "MISMATCH" );
46+
47+ FLA_Obj_free( &A );
48+ return fabs( got - expect ) >= 1e-12;
49+}
50+
51+static int test_lapack_dgesv( void )
52+{
53+ /* Solve A x = b where A = [[2,1],[1,3]], b = [3,5] -> x = [0.8, 1.4] */
54+ int n = 2, nrhs = 1, lda = 2, ldb = 2, info = 0;
55+ int ipiv[2];
56+ double A[4] = { 2.0, 1.0, /* column 0 */
57+ 1.0, 3.0 }; /* column 1 */
58+ double b[2] = { 3.0, 5.0 };
59+
60+ dgesv_( &n, &nrhs, A, &lda, ipiv, b, &ldb, &info );
61+
62+ printf( "dgesv_: info = %d, x = [%.6f, %.6f] (expected [0.8, 1.4]) %s\n",
63+ info, b[0], b[1],
64+ ( info == 0 && fabs( b[0] - 0.8 ) < 1e-12
65+ && fabs( b[1] - 1.4 ) < 1e-12 ) ? "OK" : "MISMATCH" );
66+
67+ return !( info == 0 && fabs( b[0] - 0.8 ) < 1e-12
68+ && fabs( b[1] - 1.4 ) < 1e-12 );
69+}
70+
71+int main( void )
72+{
73+ int failures = 0;
74+
75+ FLA_Init();
76+ printf( "libflame WASM demo\n" );
77+
78+ failures += test_flame_cholesky();
79+ failures += test_lapack_dgesv();
80+
81+ FLA_Finalize();
82+
83+ printf( failures == 0 ? "ALL TESTS PASSED\n" : "%d TEST(S) FAILED\n",
84+ failures );
85+ return failures;
86+}
web/bench_web.cadded+144−0View file
@@ -0,0 +1,144 @@
1+/*
2+ * Browser benchmark entry point for libflame+BLIS WASM.
3+ * Exposes run_bench(routine, n, nthreads) for calling from JavaScript;
4+ * returns GFLOP/s (or -1 on error) and printf's a detail line (captured
5+ * by the page via Module.print).
6+ *
7+ * Built two ways by build-web.sh:
8+ * single-threaded: BLIS st + libflame.a
9+ * threaded: -pthread -DBLIS_MT, BLIS pthreads + libflame-mt.a
10+ */
11+#include <stdio.h>
12+#include <stdlib.h>
13+#include <string.h>
14+#include <time.h>
15+#include <emscripten.h>
16+
17+extern int dgemm_( char* transa, char* transb, int* m, int* n, int* k,
18+ double* alpha, double* a, int* lda, double* b, int* ldb,
19+ double* beta, double* c, int* ldc );
20+extern int dpotrf_( char* uplo, int* n, double* a, int* lda, int* info );
21+extern int dgetrf_( int* m, int* n, double* a, int* lda, int* ipiv, int* info );
22+
23+#ifdef BLIS_MT
24+/* dim_t is int32 in this BLIS build (--int-size=32). */
25+extern void bli_thread_set_num_threads( int n_threads );
26+#endif
27+
28+static double now_sec( void )
29+{
30+ struct timespec ts;
31+ clock_gettime( CLOCK_MONOTONIC, &ts );
32+ return ts.tv_sec + 1e-9 * ts.tv_nsec;
33+}
34+
35+static unsigned long long rng_state = 12345;
36+static double frand( void )
37+{
38+ rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
39+ return ( ( rng_state >> 33 ) & 0xffffff ) / (double) 0x1000000 - 0.5;
40+}
41+
42+static void fill_random( double* a, int n2 )
43+{
44+ int i;
45+ rng_state = 12345;
46+ for ( i = 0; i < n2; i++ ) a[i] = frand();
47+}
48+
49+static void fill_spd( double* a, int n )
50+{
51+ int i, j;
52+ rng_state = 12345;
53+ for ( j = 0; j < n; j++ )
54+ for ( i = 0; i < n; i++ )
55+ a[ j*n + i ] = ( i == j ) ? n : 0.5 * frand();
56+ for ( j = 0; j < n; j++ )
57+ for ( i = 0; i < j; i++ ) {
58+ double v = 0.5 * ( a[ j*n + i ] + a[ i*n + j ] );
59+ a[ j*n + i ] = a[ i*n + j ] = v;
60+ }
61+}
62+
63+static double bench_dgemm( int n )
64+{
65+ double *a = malloc( (size_t)n*n*sizeof(double) );
66+ double *b = malloc( (size_t)n*n*sizeof(double) );
67+ double *c = malloc( (size_t)n*n*sizeof(double) );
68+ double alpha = 1.0, beta = 0.0, t, gflops;
69+ char tr = 'N';
70+
71+ if ( !a || !b || !c ) { free(a); free(b); free(c); return -1.0; }
72+ fill_random( a, n*n );
73+ fill_random( b, n*n );
74+ memset( c, 0, (size_t)n*n*sizeof(double) );
75+
76+ t = now_sec();
77+ dgemm_( &tr, &tr, &n, &n, &n, &alpha, a, &n, b, &n, &beta, c, &n );
78+ t = now_sec() - t;
79+
80+ gflops = 2.0 * n * (double)n * n / t / 1e9;
81+ printf( "dgemm n=%5d %8.3f s %7.2f GFLOP/s (check c[0]=%.6f)\n",
82+ n, t, gflops, c[0] );
83+ free( a ); free( b ); free( c );
84+ return gflops;
85+}
86+
87+static double bench_dpotrf( int n )
88+{
89+ double *a = malloc( (size_t)n*n*sizeof(double) );
90+ double t, gflops;
91+ char lo = 'L';
92+ int info = 0;
93+
94+ if ( !a ) return -1.0;
95+ fill_spd( a, n );
96+ t = now_sec();
97+ dpotrf_( &lo, &n, a, &n, &info );
98+ t = now_sec() - t;
99+
100+ gflops = ( (double)n * n * n / 3.0 ) / t / 1e9;
101+ printf( "dpotrf n=%5d %8.3f s %7.2f GFLOP/s (info=%d, L00=%.6f)\n",
102+ n, t, gflops, info, a[0] );
103+ free( a );
104+ return info == 0 ? gflops : -1.0;
105+}
106+
107+static double bench_dgetrf( int n )
108+{
109+ double *a = malloc( (size_t)n*n*sizeof(double) );
110+ int *ipiv = malloc( (size_t)n*sizeof(int) );
111+ double t, gflops;
112+ int info = 0;
113+
114+ if ( !a || !ipiv ) { free(a); free(ipiv); return -1.0; }
115+ fill_random( a, n*n );
116+ t = now_sec();
117+ dgetrf_( &n, &n, a, &n, ipiv, &info );
118+ t = now_sec() - t;
119+
120+ gflops = ( 2.0 * n * (double)n * n / 3.0 ) / t / 1e9;
121+ printf( "dgetrf n=%5d %8.3f s %7.2f GFLOP/s (info=%d)\n",
122+ n, t, gflops, info );
123+ free( a ); free( ipiv );
124+ return info == 0 ? gflops : -1.0;
125+}
126+
127+EMSCRIPTEN_KEEPALIVE
128+double run_bench( int routine, int n, int nthreads )
129+{
130+ if ( n < 2 || n > 8000 ) return -1.0;
131+
132+#ifdef BLIS_MT
133+ if ( nthreads > 0 ) bli_thread_set_num_threads( nthreads );
134+#else
135+ (void) nthreads;
136+#endif
137+
138+ switch ( routine ) {
139+ case 0: return bench_dgemm( n );
140+ case 1: return bench_dpotrf( n );
141+ case 2: return bench_dgetrf( n );
142+ default: return -1.0;
143+ }
144+}
web/bench_worker.jsadded+39−0View file
@@ -0,0 +1,39 @@
1+// Runs the wasm benchmark modules off the main thread. The threaded (mt)
2+// variant spawns its pthread workers from here (nested workers).
3+'use strict';
4+
5+const instances = {}; // variant -> { module, run }
6+
7+async function getInstance(variant) {
8+ if (!instances[variant]) {
9+ importScripts(`dist/bench_${variant}.js`);
10+ const factory = variant === 'mt' ? createBenchMT : createBenchST;
11+ const module = await factory({
12+ // Loaded via importScripts, the module would otherwise resolve the
13+ // .wasm (and the script pthread workers re-load) relative to THIS
14+ // worker's URL — point both back into dist/.
15+ locateFile: (path) => 'dist/' + path,
16+ mainScriptUrlOrBlob: `dist/bench_${variant}.js`,
17+ print: (line) => postMessage({ type: 'log', line }),
18+ printErr: (line) => postMessage({ type: 'log', line: '[stderr] ' + line }),
19+ });
20+ instances[variant] = {
21+ module,
22+ run: module.cwrap('run_bench', 'number', ['number', 'number', 'number']),
23+ };
24+ }
25+ return instances[variant];
26+}
27+
28+onmessage = async (e) => {
29+ const { id, variant, routine, n, threads } = e.data;
30+ try {
31+ const inst = await getInstance(variant);
32+ const t0 = performance.now();
33+ const gflops = inst.run(routine, n, threads);
34+ const seconds = (performance.now() - t0) / 1000;
35+ postMessage({ type: 'result', id, gflops, seconds });
36+ } catch (err) {
37+ postMessage({ type: 'error', id, message: String(err && err.message || err) });
38+ }
39+};
web/build-web.shadded+36−0View file
@@ -0,0 +1,36 @@
1+#!/bin/bash
2+# Build the browser benchmark modules (single-threaded and pthreads variants)
3+# into web/dist/. Requires the libraries built by ../build-wasm.sh (+ PTHREAD=1)
4+# and ../build-blis-wasm.sh (+ THREADING=pthreads).
5+set -euo pipefail
6+
7+cd "$(dirname "$0")"
8+ROOT=$(cd .. && pwd)
9+
10+if ! command -v emcc >/dev/null 2>&1; then
11+ source ~/emsdk/emsdk_env.sh
12+fi
13+
14+mkdir -p dist
15+
16+COMMON="-O2 -sMODULARIZE -sENVIRONMENT=web,worker,node \
17+ -sEXPORTED_FUNCTIONS=_run_bench -sEXPORTED_RUNTIME_METHODS=cwrap"
18+
19+# Single-threaded: BLIS(st) first so its BLAS symbols win over libflame's f2c BLAS.
20+emcc $COMMON bench_web.c \
21+ -sEXPORT_NAME=createBenchST \
22+ -sALLOW_MEMORY_GROWTH \
23+ "$ROOT/install/lib/libblis-st.a" \
24+ "$ROOT/install/lib/libflame.a" \
25+ -o dist/bench_st.js
26+
27+# Threaded: everything compiled with -pthread; fixed shared memory (growth is
28+# costly with shared memory); worker pool must cover max threads + margin.
29+emcc $COMMON -pthread -DBLIS_MT bench_web.c \
30+ -sEXPORT_NAME=createBenchMT \
31+ -sPTHREAD_POOL_SIZE=14 -sINITIAL_MEMORY=1024MB \
32+ "$ROOT/blis/lib/generic/libblis.a" \
33+ "$ROOT/install/lib/libflame-mt.a" \
34+ -o dist/bench_mt.js
35+
36+echo "Done: web/dist/bench_st.{js,wasm} and web/dist/bench_mt.{js,wasm}"
web/index.htmladded+197−0View file
@@ -0,0 +1,197 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+<head>
4+<meta charset="utf-8">
5+<meta name="viewport" content="width=device-width, initial-scale=1">
6+<title>libflame + BLIS — WebAssembly benchmark</title>
7+<style>
8+ :root {
9+ --bg: #ffffff; --fg: #1a1a1a; --muted: #666; --border: #d8d8d8;
10+ --accent: #0b62d6; --ok: #1a7f37; --bad: #c0392b; --panel: #f6f7f9;
11+ }
12+ @media (prefers-color-scheme: dark) {
13+ :root {
14+ --bg: #14161a; --fg: #e8e8e8; --muted: #9aa0a6; --border: #33363c;
15+ --accent: #5b9cf5; --ok: #4cc26a; --bad: #e06c5c; --panel: #1d2025;
16+ }
17+ }
18+ * { box-sizing: border-box; }
19+ body {
20+ margin: 0 auto; max-width: 880px; padding: 24px 16px 64px;
21+ background: var(--bg); color: var(--fg);
22+ font: 15px/1.5 system-ui, sans-serif;
23+ }
24+ h1 { font-size: 1.35rem; margin: 0 0 4px; }
25+ .sub { color: var(--muted); margin: 0 0 20px; }
26+ .badges { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px; }
27+ .badge {
28+ padding: 3px 10px; border-radius: 20px; font-size: 0.85rem;
29+ border: 1px solid var(--border); background: var(--panel);
30+ }
31+ .badge b.ok { color: var(--ok); } .badge b.bad { color: var(--bad); }
32+ fieldset {
33+ border: 1px solid var(--border); border-radius: 8px; background: var(--panel);
34+ padding: 14px 16px; margin: 0 0 16px; display: flex; gap: 18px;
35+ flex-wrap: wrap; align-items: end;
36+ }
37+ label { display: flex; flex-direction: column; gap: 4px; font-size: 0.85rem; color: var(--muted); }
38+ select, button {
39+ font: inherit; padding: 6px 10px; border-radius: 6px;
40+ border: 1px solid var(--border); background: var(--bg); color: var(--fg);
41+ }
42+ button {
43+ background: var(--accent); color: #fff; border: none;
44+ padding: 8px 22px; cursor: pointer; font-weight: 600;
45+ }
46+ button:disabled { opacity: 0.5; cursor: default; }
47+ .warn { color: var(--bad); font-size: 0.9rem; margin: -8px 0 16px; }
48+ table { border-collapse: collapse; width: 100%; margin: 8px 0 20px; font-variant-numeric: tabular-nums; }
49+ th, td { border: 1px solid var(--border); padding: 6px 10px; text-align: right; }
50+ th:first-child, td:first-child, th:nth-child(2), td:nth-child(2) { text-align: left; }
51+ th { background: var(--panel); font-size: 0.85rem; }
52+ td.g { font-weight: 700; }
53+ pre {
54+ background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
55+ padding: 12px; overflow-x: auto; font-size: 0.82rem; min-height: 3em;
56+ }
57+ .ref { color: var(--muted); font-size: 0.85rem; }
58+ h2 { font-size: 1.05rem; margin: 26px 0 6px; }
59+</style>
60+</head>
61+<body>
62+<h1>libflame + BLIS — WebAssembly benchmark</h1>
63+<p class="sub">dgemm / dpotrf / dgetrf via the LAPACK interface, running entirely in your browser.</p>
64+
65+<div class="badges">
66+ <span class="badge">crossOriginIsolated: <b id="isoBadge"></b></span>
67+ <span class="badge">hardwareConcurrency: <b id="hcBadge"></b></span>
68+ <span class="badge">wasm SIMD: <b>required</b></span>
69+</div>
70+<p class="warn" id="isoWarn" hidden>
71+ Threads unavailable: page is not cross-origin isolated. Serve it with
72+ <code>python3 serve.py</code> (sets COOP/COEP headers), not a plain file:// or generic server.
73+</p>
74+
75+<fieldset>
76+ <label>Build
77+ <select id="variant">
78+ <option value="st">single-threaded</option>
79+ <option value="mt" selected>threaded (pthreads)</option>
80+ </select>
81+ </label>
82+ <label>Routine
83+ <select id="routine">
84+ <option value="all" selected>all three</option>
85+ <option value="0">dgemm</option>
86+ <option value="1">dpotrf (Cholesky)</option>
87+ <option value="2">dgetrf (LU)</option>
88+ </select>
89+ </label>
90+ <label>Matrix size n
91+ <select id="size">
92+ <option>500</option>
93+ <option>1000</option>
94+ <option selected>2000</option>
95+ <option>4000</option>
96+ </select>
97+ </label>
98+ <label>Threads
99+ <select id="threads">
100+ <option>1</option><option>2</option><option>4</option>
101+ <option selected>8</option><option>12</option>
102+ </select>
103+ </label>
104+ <button id="runBtn">Run</button>
105+</fieldset>
106+
107+<table id="results">
108+ <thead>
109+ <tr><th>build</th><th>routine</th><th>n</th><th>threads</th><th>time (s)</th><th>GFLOP/s</th></tr>
110+ </thead>
111+ <tbody></tbody>
112+</table>
113+
114+<h2>Log</h2>
115+<pre id="log"></pre>
116+
117+<h2>Reference numbers (same code, 12-core x86-64 Linux)</h2>
118+<p class="ref">GFLOP/s at n=4000 — wasm under Node: single-threaded ≈ 10–12; 8 threads ≈ 29–46.
119+Native OpenBLAS: 1 thread ≈ 48–54; 12 threads ≈ 71–183. First threaded run includes worker-pool startup.</p>
120+
121+<script>
122+'use strict';
123+const $ = (id) => document.getElementById(id);
124+const ROUTINE_NAMES = { 0: 'dgemm', 1: 'dpotrf', 2: 'dgetrf' };
125+
126+const iso = self.crossOriginIsolated === true;
127+$('isoBadge').textContent = String(iso);
128+$('isoBadge').className = iso ? 'ok' : 'bad';
129+$('hcBadge').textContent = navigator.hardwareConcurrency || '?';
130+if (!iso) {
131+ $('isoWarn').hidden = false;
132+ $('variant').value = 'st';
133+ $('variant').querySelector('option[value=mt]').disabled = true;
134+}
135+
136+const worker = new Worker('bench_worker.js');
137+let nextId = 1;
138+const pending = new Map();
139+
140+worker.onmessage = (e) => {
141+ const m = e.data;
142+ if (m.type === 'log') {
143+ $('log').textContent += m.line + '\n';
144+ } else if (pending.has(m.id)) {
145+ const { resolve, reject } = pending.get(m.id);
146+ pending.delete(m.id);
147+ m.type === 'result' ? resolve(m) : reject(new Error(m.message));
148+ }
149+};
150+worker.onerror = (e) => { $('log').textContent += 'worker error: ' + e.message + '\n'; };
151+
152+function runOne(variant, routine, n, threads) {
153+ const id = nextId++;
154+ return new Promise((resolve, reject) => {
155+ pending.set(id, { resolve, reject });
156+ worker.postMessage({ id, variant, routine, n, threads });
157+ });
158+}
159+
160+function addRow(variant, routine, n, threads, seconds, gflops) {
161+ const tr = document.createElement('tr');
162+ tr.innerHTML =
163+ `<td>${variant === 'mt' ? 'threaded' : 'single-threaded'}</td>` +
164+ `<td>${ROUTINE_NAMES[routine]}</td><td>${n}</td>` +
165+ `<td>${variant === 'mt' ? threads : 1}</td>` +
166+ `<td>${seconds.toFixed(3)}</td><td class="g">${gflops.toFixed(2)}</td>`;
167+ $('results').tBodies[0].appendChild(tr);
168+}
169+
170+$('runBtn').onclick = async () => {
171+ const variant = $('variant').value;
172+ const n = parseInt($('size').value, 10);
173+ const threads = parseInt($('threads').value, 10);
174+ const sel = $('routine').value;
175+ const routines = sel === 'all' ? [0, 1, 2] : [parseInt(sel, 10)];
176+
177+ $('runBtn').disabled = true;
178+ try {
179+ for (const r of routines) {
180+ $('log').textContent += `running ${ROUTINE_NAMES[r]} n=${n} (${variant}` +
181+ (variant === 'mt' ? `, ${threads} threads` : '') + `)...\n`;
182+ const res = await runOne(variant, r, n, threads);
183+ if (res.gflops < 0) {
184+ $('log').textContent += ` failed (out of memory or bad input?)\n`;
185+ } else {
186+ addRow(variant, r, n, threads, res.seconds, res.gflops);
187+ }
188+ }
189+ } catch (err) {
190+ $('log').textContent += 'error: ' + err.message + '\n';
191+ } finally {
192+ $('runBtn').disabled = false;
193+ }
194+};
195+</script>
196+</body>
197+</html>
web/serve.pyadded+29−0View file
@@ -0,0 +1,29 @@
1+#!/usr/bin/env python3
2+"""Serve the benchmark page with the cross-origin-isolation headers that
3+SharedArrayBuffer (wasm threads) requires.
4+
5+Usage: python3 serve.py [port] (default port 8123)
6+"""
7+import sys
8+from http.server import HTTPServer, SimpleHTTPRequestHandler
9+
10+
11+class Handler(SimpleHTTPRequestHandler):
12+ extensions_map = {
13+ **SimpleHTTPRequestHandler.extensions_map,
14+ ".wasm": "application/wasm",
15+ ".js": "text/javascript",
16+ ".mjs": "text/javascript",
17+ }
18+
19+ def end_headers(self):
20+ self.send_header("Cross-Origin-Opener-Policy", "same-origin")
21+ self.send_header("Cross-Origin-Embedder-Policy", "require-corp")
22+ self.send_header("Cache-Control", "no-store")
23+ super().end_headers()
24+
25+
26+if __name__ == "__main__":
27+ port = int(sys.argv[1]) if len(sys.argv) > 1 else 8123
28+ print(f"Serving on http://localhost:{port} (COOP/COEP enabled)")
29+ HTTPServer(("127.0.0.1", port), Handler).serve_forever()