Skip to content

Commit cfc9406

Browse files
committed
Expand DSL→JS transpiler: index/shape symbols, integer inputs, caching
Add P1 (index/shape symbols _i0/_n0/_flat_idx) by emitting them as trailing kernel params and reconstructing per-block global coords in the driver, and P2 (integer inputs with floating output, matching miniexpr's float promotion). Memoize transpile + js.eval so repeated evaluations don't re-parse/re-compile, which closes most of the gap on light kernels. Update tests, the node bench (new P1/P2 kernels, streamed rows), and plans/dsl-js-coverage.md.
1 parent fc8694f commit cfc9406

5 files changed

Lines changed: 393 additions & 84 deletions

File tree

bench/js-transpiler/dsl-js-node.mjs

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
98108
def 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
102116
def 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)
107125
A_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)
111129
A_B2 = blosc2.asarray(A_NP, chunks=_chunks, blocks=_blocks, cparams=_cp)
112130
B_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.
115138
KERNELS = [
@@ -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
123148
def 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

185218
const py = await loadPyodide();
@@ -203,29 +236,36 @@ console.log("blosc2", await py.runPythonAsync("blosc2.__version__"),
203236
"| Pyodide", py.version, "| reps", NFRAMES);
204237

205238
py.FS.writeFile("/kernel_bench.py", new TextEncoder().encode(PYSRC));
206-
const out = await py.runPythonAsync(`
239+
await py.runPythonAsync(`
207240
import sys
208241
if "/" not in sys.path: sys.path.insert(0, "/")
209242
import kernel_bench
210-
kernel_bench.result(${NFRAMES})
211243
`);
212244

213-
const r = JSON.parse(out);
214245
const 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.
221249
console.log("\nper-kernel bench (ms/frame, lower is better; 'default' = prefer-js w/ fallback,");
222250
console.log("'tcc' = miniexpr JIT, 'nojit' = miniexpr interpreter):");
223251
const cols = ["default", "js", "tcc", "nojit", "js/tcc"];
224252
console.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`);
231271
if (bad.length || !fbOk) process.exit(1);

0 commit comments

Comments
 (0)