@@ -93,15 +93,33 @@ def deepar_dsl(a, b):
9393 acc = acc * 0.5 + t * 0.25 + 0.1
9494 return acc + t
9595
96- # Fallback-path kernels (used only by fallback_check, not the float sweep):
97- @blosc2.dsl_kernel # int output/operands -> default must fall back to miniexpr (float64 bridge unsafe)
96+ @blosc2.dsl_kernel # P1: index/shape symbols -> per-element global coords (radial gradient)
97+ def idxgrad_dsl(a):
98+ dx = float(_i0) - _n0 * 0.5 # noqa: F821
99+ dy = float(_i1) - _n1 * 0.5 # noqa: F821
100+ return a + sqrt(dx * dx + dy * dy) # noqa: F821
101+
102+ @blosc2.dsl_kernel # P2: integer inputs, float output (the bridge float64-converts the operands)
103+ def intmix_dsl(a, b):
104+ return sqrt(a * a + b * b) * 0.25 + (a - b) # noqa: F821
105+
106+ # Path-coverage kernels (used only by path_check, not the float sweep):
107+ @blosc2.dsl_kernel # int *output* -> default must fall back to miniexpr (float64 bridge unsafe)
98108def int_dsl(a, b):
99109 return a * 2 + b * 3
100110
101- @blosc2.dsl_kernel # index symbol -> transpiler rejects -> default must fall back to miniexpr
111+ @blosc2.dsl_kernel # int *inputs*, float output -> JS is safe (bridge float64-converts operands)
112+ def intin_dsl(a, b):
113+ return (a + b) * 0.5
114+
115+ @blosc2.dsl_kernel # index/shape symbols -> JS reconstructs global coords per block
102116def idx_dsl(a):
103117 return a + float(_i0) # noqa: F821
104118
119+ @blosc2.dsl_kernel # expm1() is valid DSL/miniexpr but outside the JS Math.* set -> falls back
120+ def unsup_dsl(a, b):
121+ return expm1(a + b) * 0.5 # noqa: F821
122+
105123_x = np.linspace(-SPANX / 2, SPANX / 2, WIDTH, dtype=DTYPE)
106124_y = np.linspace(-SPANX * ASPECT / 2, SPANX * ASPECT / 2, HEIGHT, dtype=DTYPE)
107125A_NP, B_NP = np.meshgrid(_x, _y)
@@ -110,6 +128,11 @@ _blocks = (max(1, _chunks[0] // 4), max(1, _chunks[1] // 3))
110128_cp = blosc2.CParams(codec=blosc2.Codec.LZ4, clevel=1)
111129A_B2 = blosc2.asarray(A_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
112130B_B2 = blosc2.asarray(B_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
131+ # Small-magnitude integer operands for the P2 (int in / float out) kernels.
132+ AI_NP = (A_NP * 10).astype(np.int64)
133+ BI_NP = (B_NP * 10).astype(np.int64)
134+ AI_B2 = blosc2.asarray(AI_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
135+ BI_B2 = blosc2.asarray(BI_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
113136
114137# (name, kernel, operand tuple). Fixed inputs -> each rep does identical work.
115138KERNELS = [
@@ -118,6 +141,8 @@ KERNELS = [
118141 ("trans", trans_dsl, (A_B2, B_B2)),
119142 ("deep", deep_dsl, (A_B2, B_B2)),
120143 ("deepar", deepar_dsl, (A_B2, B_B2)),
144+ ("idxgrad", idxgrad_dsl, (A_B2,)), # P1: index/shape symbols (float out)
145+ ("intmix", intmix_dsl, (AI_B2, BI_B2)), # P2: integer inputs, float out
121146]
122147
123148def run(func, ops, backend, dtype=DTYPE):
@@ -153,33 +178,41 @@ def debug_bridge():
153178 t = time.perf_counter(); bridge(inp, out, 0); best = min(best, (time.perf_counter() - t) * 1000)
154179 return {"first_ms": first, "warm_ms": best}
155180
156- def fallback_check():
157- # Default backend must transparently fall back to miniexpr where js can't go, with no
158- # error and the same result. int dtype -> dtype-gated; index symbol -> transpiler rejects.
159- Ai = blosc2.asarray((A_NP * 10).astype(np.int64), chunks=_chunks, blocks=_blocks, cparams=_cp)
160- Bi = blosc2.asarray((B_NP * 10).astype(np.int64), chunks=_chunks, blocks=_blocks, cparams=_cp)
161- int_def = run(int_dsl, (Ai, Bi), "default", dtype=np.int64)
162- int_tcc = run(int_dsl, (Ai, Bi), "tcc", dtype=np.int64)
163- idx_def = run(idx_dsl, (A_B2,), "default")
181+ def path_check():
182+ # The default backend must agree with miniexpr (tcc) on every kernel, whether it runs
183+ # the kernel through JS (index symbols, int inputs+float out) or transparently falls back
184+ # to miniexpr where JS can't go (int output, unsupported constructs) -- with no error.
185+ int_def = run(int_dsl, (AI_B2, BI_B2), "default", dtype=np.int64) # -> falls back to miniexpr
186+ int_tcc = run(int_dsl, (AI_B2, BI_B2), "tcc", dtype=np.int64)
187+ intin_def = run(intin_dsl, (AI_B2, BI_B2), "default") # -> JS (int in, float out)
188+ intin_tcc = run(intin_dsl, (AI_B2, BI_B2), "tcc")
189+ idx_def = run(idx_dsl, (A_B2,), "default") # -> JS (index symbols)
164190 idx_tcc = run(idx_dsl, (A_B2,), "tcc")
191+ unsup_def = run(unsup_dsl, (A_B2, B_B2), "default") # -> falls back to miniexpr
192+ unsup_tcc = run(unsup_dsl, (A_B2, B_B2), "tcc")
165193 return {
166194 "int_ok": bool(np.array_equal(int_def, int_tcc)),
195+ "intin_ok": bool(np.allclose(intin_def, intin_tcc)),
167196 "idx_ok": bool(np.allclose(idx_def, idx_tcc)),
197+ "unsup_ok": bool(np.allclose(unsup_def, unsup_tcc)),
168198 }
169199
170- def result(reps):
200+ def kernel_names():
201+ return json.dumps([name for name, _f, _o in KERNELS])
202+
203+ def bench_kernel(i, reps):
204+ # One kernel at a time so the driver can print each row as soon as it is computed.
171205 import math
172- kernels = []
173- for name, func, ops in KERNELS:
174- rj = run(func, ops, "js")
175- rtcc = run(func, ops, "tcc")
176- diff = float(np.max(np.abs(rj - rtcc)))
177- diff = diff if math.isfinite(diff) else 1e30 # keep JSON valid; flags as mismatch
178- ms = {b: bench(func, ops, b, reps) for b in ("default", "js", "tcc", "nojit")}
179- kernels.append({"name": name, "ms": ms, "diff": diff})
180- return json.dumps({
181- "kernels": kernels, "bridge": debug_bridge(), "fallback": fallback_check(), "reps": reps,
182- })
206+ name, func, ops = KERNELS[i]
207+ rj = run(func, ops, "js")
208+ rtcc = run(func, ops, "tcc")
209+ diff = float(np.max(np.abs(rj - rtcc)))
210+ diff = diff if math.isfinite(diff) else 1e30 # keep JSON valid; flags as mismatch
211+ ms = {b: bench(func, ops, b, reps) for b in ("default", "js", "tcc", "nojit")}
212+ return json.dumps({"name": name, "ms": ms, "diff": diff})
213+
214+ def summary():
215+ return json.dumps({"bridge": debug_bridge(), "paths": path_check()})
183216` ;
184217
185218const py = await loadPyodide ( ) ;
@@ -203,29 +236,36 @@ console.log("blosc2", await py.runPythonAsync("blosc2.__version__"),
203236 "| Pyodide" , py . version , "| reps" , NFRAMES ) ;
204237
205238py . FS . writeFile ( "/kernel_bench.py" , new TextEncoder ( ) . encode ( PYSRC ) ) ;
206- const out = await py . runPythonAsync ( `
239+ await py . runPythonAsync ( `
207240import sys
208241if "/" not in sys.path: sys.path.insert(0, "/")
209242import kernel_bench
210- kernel_bench.result(${ NFRAMES } )
211243` ) ;
212244
213- const r = JSON . parse ( out ) ;
214245const fmt = ( x , w ) => String ( x ) . padStart ( w ) ;
215- const bad = r . kernels . filter ( ( k ) => k . diff > 1e-5 ) ;
216- console . log ( `\ncorrectness (js vs tcc maxdiff): ${ bad . length ? "MISMATCH " + bad . map ( ( k ) => k . name ) : "OK" } ` ) ;
217- const fb = r . fallback ;
218- const fbOk = fb . int_ok && fb . idx_ok ;
219- console . log ( `default fallback (no jit_backend): int=${ fb . int_ok ? "ok" : "FAIL" } ` +
220- `index-symbol=${ fb . idx_ok ? "ok" : "FAIL" } -> ${ fbOk ? "falls back cleanly" : "BROKEN" } ` ) ;
246+ const names = JSON . parse ( await py . runPythonAsync ( "kernel_bench.kernel_names()" ) ) ;
247+
248+ // Stream the table: print each row as soon as its kernel finishes benchmarking.
221249console . log ( "\nper-kernel bench (ms/frame, lower is better; 'default' = prefer-js w/ fallback," ) ;
222250console . log ( "'tcc' = miniexpr JIT, 'nojit' = miniexpr interpreter):" ) ;
223251const cols = [ "default" , "js" , "tcc" , "nojit" , "js/tcc" ] ;
224252console . log ( " " + "kernel" . padEnd ( 8 ) + cols . map ( ( c ) => fmt ( c , 8 ) ) . join ( "" ) ) ;
225- for ( const k of r . kernels ) {
253+ const bad = [ ] ;
254+ for ( let i = 0 ; i < names . length ; i ++ ) {
255+ const k = JSON . parse ( await py . runPythonAsync ( `kernel_bench.bench_kernel(${ i } , ${ NFRAMES } )` ) ) ;
226256 const { default : def , js, tcc, nojit } = k . ms ;
227257 const cells = [ def , js , tcc , nojit ] . map ( ( v ) => v . toFixed ( 1 ) ) . concat ( ( tcc / js ) . toFixed ( 2 ) + "x" ) ;
228258 console . log ( " " + k . name . padEnd ( 8 ) + cells . map ( ( c ) => fmt ( c , 8 ) ) . join ( "" ) ) ;
259+ if ( k . diff > 1e-5 ) bad . push ( k . name ) ;
229260}
230- console . log ( `\nnewton bridge probe (no blosc2 machinery): first=${ r . bridge . first_ms . toFixed ( 1 ) } ms warm=${ r . bridge . warm_ms . toFixed ( 1 ) } ms` ) ;
261+
262+ const s = JSON . parse ( await py . runPythonAsync ( "kernel_bench.summary()" ) ) ;
263+ console . log ( `\ncorrectness (js vs tcc maxdiff): ${ bad . length ? "MISMATCH " + bad : "OK" } ` ) ;
264+ const fb = s . paths ;
265+ const fbOk = fb . int_ok && fb . intin_ok && fb . idx_ok && fb . unsup_ok ;
266+ console . log ( `default backend (no jit_backend) vs miniexpr: ` +
267+ `int-out=${ fb . int_ok ? "ok" : "FAIL" } int-in=${ fb . intin_ok ? "ok" : "FAIL" } ` +
268+ `index-symbol=${ fb . idx_ok ? "ok" : "FAIL" } unsupported=${ fb . unsup_ok ? "ok" : "FAIL" } ` +
269+ ` -> ${ fbOk ? "all paths agree" : "BROKEN" } ` ) ;
270+ console . log ( `\nnewton bridge probe (no blosc2 machinery): first=${ s . bridge . first_ms . toFixed ( 1 ) } ms warm=${ s . bridge . warm_ms . toFixed ( 1 ) } ms` ) ;
231271if ( bad . length || ! fbOk ) process . exit ( 1 ) ;
0 commit comments