From c646872e42cfb1aac107f6b6c72459ed7b99f98c Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Fri, 3 Apr 2026 17:17:59 +0100 Subject: [PATCH 1/2] feat(playground): wire WasmIML methods into debug probe Add evalLoss, inferBatch, and getLayerStats to the window.__nisps debug probe so Playwright tests and dev console can access the new WasmIML capabilities. InputHeatmap (Phase 3) is not yet wired into a-app.js, so batch inference heatmap integration is deferred. --- playground/js/a-app.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/playground/js/a-app.js b/playground/js/a-app.js index eb1332e..1cd334d 100644 --- a/playground/js/a-app.js +++ b/playground/js/a-app.js @@ -914,6 +914,9 @@ async function init() { updateStatus(); }, saveState: () => saveState(), + evalLoss: () => iml.evalLoss(), + inferBatch: (points) => iml.inferBatch(points), + getLayerStats:() => iml.getLayerStats(), }; } From 0fc2d940499623eaf2645dc59b11c5f3d85045eb Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Fri, 3 Apr 2026 17:18:01 +0100 Subject: [PATCH 2/2] feat(playground): batch inference support in input heatmap InputHeatmap.update() now accepts options.inferBatchFn to evaluate all grid points (plus the divergence center point) in a single WASM call instead of 256 separate round-trips. The per-point inferFn path is preserved as a fallback when inferBatchFn is not provided. --- playground/js/ui/input-heatmap.js | 72 +++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/playground/js/ui/input-heatmap.js b/playground/js/ui/input-heatmap.js index 4f2a06c..13d2bbf 100644 --- a/playground/js/ui/input-heatmap.js +++ b/playground/js/ui/input-heatmap.js @@ -10,8 +10,9 @@ * - variance: output variance -> saturation (shows "interesting" vs "flat") * - divergence: difference from center point output (how each region diverges) * - * Performance: 16x16 = 256 inferences at ~20us each = ~5ms. - * Throttled to max 5 updates/sec by default. + * Performance: 16x16 = 256 inferences. When inferBatchFn is provided, + * all points are evaluated in a single WASM call. Falls back to per-point + * inferFn (~20us each = ~5ms). Throttled to max 5 updates/sec by default. * * @module input-heatmap */ @@ -107,6 +108,9 @@ export class InputHeatmap { * @param {object} [options] * @param {object} [options.zoomWindow] - { x1, y1, x2, y2 } in [0,1] space * @param {number} [options.resolution] - override resolution for this update + * @param {function} [options.inferBatchFn] - (inputPoints: number[][]) => number[][] + * Batch inference: takes array of [x,y] pairs, returns array of output arrays. + * When provided, used instead of per-point inferFn for better performance. */ update(inferFn, options = {}) { if (!this._enabled) return; @@ -117,26 +121,60 @@ export class InputHeatmap { const res = clamp(options.resolution ?? this._resolution, MIN_RESOLUTION, MAX_RESOLUTION); const zw = options.zoomWindow || { x1: 0, y1: 0, x2: 1, y2: 1 }; + const inferBatchFn = options.inferBatchFn || null; // Sample grid const grid = new Float32Array(res * res); - const outputs = []; + let outputs; - // Pre-compute center output for divergence mode - if (this._colorMode === 'divergence') { - const cx = (zw.x1 + zw.x2) / 2; - const cy = (zw.y1 + zw.y2) / 2; - this._centerOutput = inferFn([cx, cy]); - } + if (inferBatchFn) { + // ---- Batch path: build all input points, call once ---- + const needsCenter = this._colorMode === 'divergence'; + const points = []; - // Collect all outputs for normalization - for (let gy = 0; gy < res; gy++) { - for (let gx = 0; gx < res; gx++) { - // Map grid cell to input space - const inputX = zw.x1 + (gx + 0.5) / res * (zw.x2 - zw.x1); - const inputY = zw.y1 + (gy + 0.5) / res * (zw.y2 - zw.y1); - const out = inferFn([inputX, inputY]); - outputs.push(out); + // If divergence mode, first point is the center + if (needsCenter) { + const cx = (zw.x1 + zw.x2) / 2; + const cy = (zw.y1 + zw.y2) / 2; + points.push([cx, cy]); + } + + // Grid points + for (let gy = 0; gy < res; gy++) { + for (let gx = 0; gx < res; gx++) { + const inputX = zw.x1 + (gx + 0.5) / res * (zw.x2 - zw.x1); + const inputY = zw.y1 + (gy + 0.5) / res * (zw.y2 - zw.y1); + points.push([inputX, inputY]); + } + } + + const allOutputs = inferBatchFn(points); + + if (needsCenter) { + this._centerOutput = allOutputs[0]; + outputs = allOutputs.slice(1); + } else { + outputs = allOutputs; + } + } else { + // ---- Per-point fallback path ---- + outputs = []; + + // Pre-compute center output for divergence mode + if (this._colorMode === 'divergence') { + const cx = (zw.x1 + zw.x2) / 2; + const cy = (zw.y1 + zw.y2) / 2; + this._centerOutput = inferFn([cx, cy]); + } + + // Collect all outputs for normalization + for (let gy = 0; gy < res; gy++) { + for (let gx = 0; gx < res; gx++) { + const inputX = zw.x1 + (gx + 0.5) / res * (zw.x2 - zw.x1); + const inputY = zw.y1 + (gy + 0.5) / res * (zw.y2 - zw.y1); + const out = inferFn([inputX, inputY]); + outputs.push(out); + } } }