33// working tree's pure-Python (src/blosc2/dsl_js.py + lazyexpr.py) on top of it before
44// importing blosc2 -- so the wired path runs without waiting for a new wheel.
55//
6+ // Benches a spread of kernel shapes to show how the js-vs-JIT ratio depends on the kernel:
7+ // branchy + early-exit (newton), branch-free light (poly), transcendental-heavy (trans),
8+ // deep no-exit loop (deep). Reports js / jit / no-jit per kernel.
9+ //
610// npm i # pulls pyodide@314 (see package.json)
7- // node bench/js-transpiler/dsl-js-node.mjs # correctness + 24-frame bench
8- // node bench/js-transpiler/dsl-js-node.mjs 48 # bench with N frames
11+ // node bench/js-transpiler/dsl-js-node.mjs # correctness + bench, 12 reps
12+ // node bench/js-transpiler/dsl-js-node.mjs 24 # N reps
913import { loadPyodide } from "pyodide" ;
1014import { readFileSync } from "node:fs" ;
1115import { fileURLToPath } from "node:url" ;
1216
1317// Resolve paths from this file, so the harness runs from any CWD (repo root is ../../).
1418const ROOT = fileURLToPath ( new URL ( "../../" , import . meta. url ) ) ;
15- const NFRAMES = Number ( process . argv [ 2 ] ) || 24 ;
19+ const NFRAMES = Number ( process . argv [ 2 ] ) || 12 ;
1620
17- // Kernel + bench live in a real module file: @blosc2.dsl_kernel runs inspect.getsource(),
21+ // Kernels + bench live in a real module file: @blosc2.dsl_kernel runs inspect.getsource(),
1822// which needs the function to be file-backed (not exec'd from a string).
1923const PYSRC = String . raw `
2024import json, time
@@ -26,7 +30,8 @@ SPANX = 3.4
2630ASPECT = HEIGHT / WIDTH
2731DTYPE = np.float64
2832
29- @blosc2.dsl_kernel
33+ # --- kernels spanning the cost/control-flow spectrum -------------------------------------
34+ @blosc2.dsl_kernel # branchy, deep, per-pixel early exit
3035def newton_dsl(a, b, max_iter, relax):
3136 za = a
3237 zb = b
@@ -59,27 +64,34 @@ def newton_dsl(a, b, max_iter, relax):
5964 root = 2.0
6065 return root + 0.9 * (it / mif)
6166
62- def newton_numpy(a, b, max_iter, relax):
63- za = a.copy(); zb = b.copy()
64- mif = float(max_iter)
65- it = np.full(a.shape, mif)
66- alive = np.ones(a.shape, dtype=bool)
67- for k in range(max_iter):
68- a2 = za * za; b2 = zb * zb
69- fr = za * a2 - 3.0 * za * b2 - 1.0; fi = 3.0 * a2 * zb - zb * b2
70- dr = 3.0 * (a2 - b2); di = 6.0 * za * zb; den = dr * dr + di * di + 1e-12
71- qr = relax * (fr * dr + fi * di) / den; qi = relax * (fi * dr - fr * di) / den
72- za = za - qr; zb = zb - qi
73- c = alive & ((qr * qr + qi * qi) < 1e-6); it[c] = k; alive &= ~c
74- if not alive.any():
75- break
76- d0 = (za - 1.0) ** 2 + zb ** 2
77- d1 = (za + 0.5) ** 2 + (zb - 0.8660254) ** 2
78- d2 = (za + 0.5) ** 2 + (zb + 0.8660254) ** 2
79- root = np.zeros(a.shape); md = d0.copy()
80- m = d1 < md; md = np.where(m, d1, md); root = np.where(m, 1.0, root)
81- m = d2 < md; root = np.where(m, 2.0, root)
82- return root + 0.9 * (it / mif)
67+ @blosc2.dsl_kernel # light, branch-free, vectorizable arithmetic
68+ def poly_dsl(a, b):
69+ a2 = a * a
70+ b2 = b * b
71+ return a2 * a - 3.0 * a * b2 + 2.0 * b2 * b - a + 0.5 * b
72+
73+ @blosc2.dsl_kernel # transcendental-heavy (exercises each engine's libm). miniexpr wants
74+ def trans_dsl(a, b): # bare sin/cos/... (not np.sin); the transpiler maps both to Math.*
75+ msq = a * a + b * b
76+ sc = sin(a * 3.0) * cos(b * 2.0)
77+ ex = exp(msq * -0.5)
78+ return sc + ex + sqrt(msq + 1.0) + log(msq + 2.0)
79+
80+ @blosc2.dsl_kernel # deep fixed loop, transcendental-bound (libm sin every iter)
81+ def deep_dsl(a, b):
82+ acc = a
83+ for k in range(64):
84+ acc = acc * 0.99 + sin(acc + b)
85+ return acc
86+
87+ @blosc2.dsl_kernel # deep fixed loop, pure arithmetic (no libm, no branches); contractive
88+ def deepar_dsl(a, b):
89+ acc = a * 0.1
90+ t = b * 0.1
91+ for k in range(64):
92+ t = t * 0.5 - acc * 0.25 + 0.1
93+ acc = acc * 0.5 + t * 0.25 + 0.1
94+ return acc + t
8395
8496_x = np.linspace(-SPANX / 2, SPANX / 2, WIDTH, dtype=DTYPE)
8597_y = np.linspace(-SPANX * ASPECT / 2, SPANX * ASPECT / 2, HEIGHT, dtype=DTYPE)
@@ -90,40 +102,36 @@ _cp = blosc2.CParams(codec=blosc2.Codec.LZ4, clevel=1)
90102A_B2 = blosc2.asarray(A_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
91103B_B2 = blosc2.asarray(B_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
92104
93- def run(relax, backend):
105+ # (name, kernel, operand tuple). Fixed inputs -> each rep does identical work.
106+ KERNELS = [
107+ ("newton", newton_dsl, (A_B2, B_B2, MAXITER, 1.37)),
108+ ("poly", poly_dsl, (A_B2, B_B2)),
109+ ("trans", trans_dsl, (A_B2, B_B2)),
110+ ("deep", deep_dsl, (A_B2, B_B2)),
111+ ("deepar", deepar_dsl, (A_B2, B_B2)),
112+ ]
113+
114+ def run(func, ops, backend):
94115 kw = {"dtype": DTYPE, "cparams": _cp}
95116 if backend == "js":
96117 kw["jit_backend"] = "js"
97118 elif backend == "jit":
98119 kw["jit"] = True
99120 else:
100121 kw["jit"] = False
101- return blosc2.lazyudf(newton_dsl, (A_B2, B_B2, MAXITER, relax), **kw)[:]
102-
103- def correctness():
104- rx = 1.37
105- ref = newton_numpy(A_NP, B_NP, MAXITER, rx)
106- return {
107- "diff_js": float(np.max(np.abs(run(rx, "js") - ref))),
108- "diff_jit": float(np.max(np.abs(run(rx, "jit") - ref))),
109- }
110-
111- def bench(nframes):
112- relaxes = [float(v) for v in np.linspace(1.0, 1.85, nframes)]
113- def sweep_ms(backend):
114- for r in relaxes[:2]:
115- run(r, backend) # warm
116- best = float("inf")
117- for _ in range(3):
118- t = time.perf_counter()
119- for r in relaxes:
120- run(r, backend)
121- best = min(best, (time.perf_counter() - t) * 1000)
122- return best
123- return {b: sweep_ms(b) for b in ("js", "jit", "nojit")}
122+ return blosc2.lazyudf(func, ops, **kw)[:]
123+
124+ def bench(func, ops, backend, reps):
125+ run(func, ops, backend) # warm
126+ best = float("inf")
127+ for _ in range(3):
128+ t = time.perf_counter()
129+ for _ in range(reps):
130+ run(func, ops, backend)
131+ best = min(best, (time.perf_counter() - t) * 1000 / reps)
132+ return best
124133
125134def debug_bridge():
126- # Call the JS bridge directly (no lazyudf/blosc2 machinery) to isolate marshal+compile+compute.
127135 import blosc2.dsl_js as dj
128136 bridge = dj.js_kernel(newton_dsl)
129137 out = np.empty((HEIGHT, WIDTH), dtype=DTYPE)
@@ -134,12 +142,17 @@ def debug_bridge():
134142 t = time.perf_counter(); bridge(inp, out, 0); best = min(best, (time.perf_counter() - t) * 1000)
135143 return {"first_ms": first, "warm_ms": best}
136144
137- def result(nframes):
138- out = correctness()
139- out["ms"] = bench(nframes)
140- out["bridge"] = debug_bridge()
141- out["nframes"] = nframes
142- return json.dumps(out)
145+ def result(reps):
146+ import math
147+ kernels = []
148+ for name, func, ops in KERNELS:
149+ rj = run(func, ops, "js")
150+ rjit = run(func, ops, "jit")
151+ diff = float(np.max(np.abs(rj - rjit)))
152+ diff = diff if math.isfinite(diff) else 1e30 # keep JSON valid; flags as mismatch
153+ ms = {b: bench(func, ops, b, reps) for b in ("js", "jit", "nojit")}
154+ kernels.append({"name": name, "ms": ms, "diff": diff})
155+ return json.dumps({"kernels": kernels, "bridge": debug_bridge(), "reps": reps})
143156` ;
144157
145158const py = await loadPyodide ( ) ;
@@ -160,27 +173,28 @@ import sys, blosc2
160173assert hasattr(sys.modules["blosc2.lazyexpr"], "_as_js_udf"), "overlay did not take"
161174` ) ;
162175console . log ( "blosc2" , await py . runPythonAsync ( "blosc2.__version__" ) ,
163- "| Pyodide" , py . version , "| frames " , NFRAMES ) ;
176+ "| Pyodide" , py . version , "| reps " , NFRAMES ) ;
164177
165- py . FS . writeFile ( "/newton_bench .py" , new TextEncoder ( ) . encode ( PYSRC ) ) ;
178+ py . FS . writeFile ( "/kernel_bench .py" , new TextEncoder ( ) . encode ( PYSRC ) ) ;
166179const out = await py . runPythonAsync ( `
167180import sys
168181if "/" not in sys.path: sys.path.insert(0, "/")
169- import newton_bench
170- newton_bench .result(${ NFRAMES } )
182+ import kernel_bench
183+ kernel_bench .result(${ NFRAMES } )
171184` ) ;
172185
173186const r = JSON . parse ( out ) ;
174- const ok = r . diff_js < 1e-9 && r . diff_jit < 1e-9 ;
175- console . log ( `\ncorrectness vs numpy: js maxdiff=${ r . diff_js . toExponential ( 2 ) } ` +
176- `jit maxdiff=${ r . diff_jit . toExponential ( 2 ) } ${ ok ? "OK" : "MISMATCH" } ` ) ;
177- const per = ms => `${ ms . toFixed ( 0 ) } ms total (${ ( ms / r . nframes ) . toFixed ( 2 ) } ms/frame)` ;
178- console . log ( `\nperf (${ r . nframes } -frame relax sweep, best of 3):` ) ;
179- console . log ( ` jit_backend="js" : ${ per ( r . ms . js ) } ` ) ;
180- console . log ( ` jit (miniexpr) : ${ per ( r . ms . jit ) } ` ) ;
181- console . log ( ` no-JIT : ${ per ( r . ms . nojit ) } ` ) ;
182- console . log ( ` js vs jit : ${ ( r . ms . jit / r . ms . js ) . toFixed ( 2 ) } x ` +
183- `js vs no-jit : ${ ( r . ms . nojit / r . ms . js ) . toFixed ( 2 ) } x` ) ;
184- console . log ( `\nbridge probe (direct call, no blosc2 machinery): ` +
185- `first=${ r . bridge . first_ms . toFixed ( 1 ) } ms warm=${ r . bridge . warm_ms . toFixed ( 1 ) } ms` ) ;
186- if ( ! ok ) process . exit ( 1 ) ;
187+ const fmt = ( x , w ) => String ( x ) . padStart ( w ) ;
188+ const bad = r . kernels . filter ( ( k ) => k . diff > 1e-5 ) ;
189+ console . log ( `\ncorrectness (js vs JIT maxdiff): ${ bad . length ? "MISMATCH " + bad . map ( ( k ) => k . name ) : "OK" } ` ) ;
190+ console . log ( "\nper-kernel bench (ms/frame, lower is better):" ) ;
191+ console . log ( " kernel js jit nojit js/jit js/nojit diff" ) ;
192+ for ( const k of r . kernels ) {
193+ const { js, jit, nojit } = k . ms ;
194+ console . log (
195+ ` ${ k . name . padEnd ( 7 ) } ${ fmt ( js . toFixed ( 1 ) , 6 ) } ${ fmt ( jit . toFixed ( 1 ) , 6 ) } ${ fmt ( nojit . toFixed ( 1 ) , 7 ) } ` +
196+ ` ${ fmt ( ( jit / js ) . toFixed ( 2 ) + "x" , 6 ) } ${ fmt ( ( nojit / js ) . toFixed ( 2 ) + "x" , 8 ) } ${ k . diff . toExponential ( 1 ) } ` ,
197+ ) ;
198+ }
199+ console . log ( `\nnewton bridge probe (no blosc2 machinery): first=${ r . bridge . first_ms . toFixed ( 1 ) } ms warm=${ r . bridge . warm_ms . toFixed ( 1 ) } ms` ) ;
200+ if ( bad . length ) process . exit ( 1 ) ;
0 commit comments