tapewasm 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/calibrate.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Which shape this engine prefers, measured once.
|
|
2
|
+
//
|
|
3
|
+
// `compileTape`'s `"auto"` threshold cannot be right for every engine:
|
|
4
|
+
// straight-line and re-rolled cross over between 8,026 and 24,564 nodes in V8
|
|
5
|
+
// and around 2,000 in SpiderMonkey and JavaScriptCore. The built-in value serves the
|
|
6
|
+
// latter two and costs V8 up to 7.6x on a trace between them. This measures
|
|
7
|
+
// which side the engine is on and returns a threshold to pass to `compileTape`.
|
|
8
|
+
//
|
|
9
|
+
// await init(...);
|
|
10
|
+
// const above = await calibrateReroll();
|
|
11
|
+
// const built = compileTape(text, String(above));
|
|
12
|
+
//
|
|
13
|
+
// The answer is cached: it is a property of the engine, not of the model.
|
|
14
|
+
|
|
15
|
+
import { compileTape } from "./pkg/tapewasm.js";
|
|
16
|
+
|
|
17
|
+
/// What `"auto"` uses. Right for SpiderMonkey and JavaScriptCore.
|
|
18
|
+
export const RE_ROLL_ABOVE = 2_000;
|
|
19
|
+
|
|
20
|
+
/// For an engine that prefers straight-line, as V8 does.
|
|
21
|
+
///
|
|
22
|
+
/// Bracketed by measurement rather than picked: on eleven posteriordb models,
|
|
23
|
+
/// straight-line still wins at 8,026 nodes and has lost by 24,564. A threshold
|
|
24
|
+
/// of 24,000 already costs `low_dim_gauss_mix` 1.21x, so this sits clear of
|
|
25
|
+
/// that edge. At 20,000 the five models whose shape changes get 2.54x in the
|
|
26
|
+
/// geometric mean and none is slower.
|
|
27
|
+
export const V8_RE_ROLL_ABOVE = 20_000;
|
|
28
|
+
|
|
29
|
+
/// Where the two engine families disagree most clearly. At this size V8 prefers
|
|
30
|
+
/// straight-line on every model measured, and the other two prefer loops on all
|
|
31
|
+
/// but one.
|
|
32
|
+
const PROBE_NODES = 4_000;
|
|
33
|
+
|
|
34
|
+
let cached = null;
|
|
35
|
+
|
|
36
|
+
/// A tape of the commonest shape — one accumulated term per observation —
|
|
37
|
+
/// using nothing but arithmetic, so the module it compiles to imports only
|
|
38
|
+
/// memory and the probe needs no maths from the caller.
|
|
39
|
+
function probeTape(terms) {
|
|
40
|
+
// Instruction 0, 1, 2 are the parameters; `at` is the index the next line takes.
|
|
41
|
+
const out = ["n_params 3", "new_var 0.4", "new_var 1.1", "new_var 0.3"];
|
|
42
|
+
let at = 3;
|
|
43
|
+
const push = (line) => {
|
|
44
|
+
out.push(line);
|
|
45
|
+
return at++;
|
|
46
|
+
};
|
|
47
|
+
let acc = null;
|
|
48
|
+
for (let i = 0; i < terms; i++) {
|
|
49
|
+
const x = (0.001 * (i % 997)).toFixed(6);
|
|
50
|
+
const y = (0.5 + 0.001 * (i % 991)).toFixed(6);
|
|
51
|
+
const bx = push(`mul_c 1 ${x}`); // beta * x
|
|
52
|
+
const mu = push(`add ${bx} 0`); // + alpha
|
|
53
|
+
const r = push(`rsub_c ${mu} ${y}`); // y - mu
|
|
54
|
+
const z = push(`div ${r} 2`); // / sigma
|
|
55
|
+
const sq = push(`mul ${z} ${z}`);
|
|
56
|
+
acc = acc === null ? sq : push(`add ${acc} ${sq}`);
|
|
57
|
+
}
|
|
58
|
+
const half = push(`mul_c ${acc} -0.5`);
|
|
59
|
+
out.push(`root ${half}`);
|
|
60
|
+
return out.join("\n") + "\n";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function timeOne(wasm, nParams, scratchInit, rounds) {
|
|
64
|
+
const need = nParams * 16 + scratchInit.length * 8;
|
|
65
|
+
const memory = new WebAssembly.Memory({ initial: Math.ceil(need / 65536) + 2 });
|
|
66
|
+
const { instance } = await WebAssembly.instantiate(wasm, { tapewasm: { memory } });
|
|
67
|
+
const view = new Float64Array(memory.buffer);
|
|
68
|
+
view.set(scratchInit, nParams * 2);
|
|
69
|
+
for (let i = 0; i < nParams; i++) view[i] = 0.1 * (i + 1);
|
|
70
|
+
const lpg = instance.exports.log_prob_grad;
|
|
71
|
+
const call = () => lpg(0, nParams * 8, nParams, nParams * 16);
|
|
72
|
+
|
|
73
|
+
for (let i = 0; i < 50; i++) call(); // past the first tier
|
|
74
|
+
const t0 = performance.now();
|
|
75
|
+
let n = 0;
|
|
76
|
+
while (performance.now() - t0 < 4) { call(); n++; }
|
|
77
|
+
const iters = Math.max(1, n);
|
|
78
|
+
let best = Infinity;
|
|
79
|
+
for (let r = 0; r < rounds; r++) {
|
|
80
|
+
const t = performance.now();
|
|
81
|
+
for (let j = 0; j < iters; j++) call();
|
|
82
|
+
best = Math.min(best, (performance.now() - t) / iters);
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Measure which shape this engine prefers and return a threshold for
|
|
89
|
+
* `compileTape`'s `reroll` argument. Call after `init()`.
|
|
90
|
+
*
|
|
91
|
+
* Cached after the first call — pass `{ force: true }` to measure again.
|
|
92
|
+
* On any failure it returns the built-in threshold, so a caller can use the
|
|
93
|
+
* result without guarding it.
|
|
94
|
+
*/
|
|
95
|
+
export async function calibrateReroll({ force = false, rounds = 7 } = {}) {
|
|
96
|
+
if (cached !== null && !force) return cached.above;
|
|
97
|
+
let result;
|
|
98
|
+
try {
|
|
99
|
+
const text = probeTape(Math.max(1, Math.round(PROBE_NODES / 5)));
|
|
100
|
+
const straight = compileTape(text, "never");
|
|
101
|
+
const looped = compileTape(text, "always");
|
|
102
|
+
// Interleaved, so a drift in machine state lands on both.
|
|
103
|
+
let sBest = Infinity, lBest = Infinity;
|
|
104
|
+
for (let r = 0; r < 2; r++) {
|
|
105
|
+
sBest = Math.min(sBest, await timeOne(straight.wasm, straight.nParams, straight.scratchInit, rounds));
|
|
106
|
+
lBest = Math.min(lBest, await timeOne(looped.wasm, looped.nParams, looped.scratchInit, rounds));
|
|
107
|
+
}
|
|
108
|
+
const prefersStraight = sBest < lBest;
|
|
109
|
+
result = {
|
|
110
|
+
above: prefersStraight ? V8_RE_ROLL_ABOVE : RE_ROLL_ABOVE,
|
|
111
|
+
prefersStraight,
|
|
112
|
+
straightMs: sBest,
|
|
113
|
+
loopedMs: lBest,
|
|
114
|
+
measured: true,
|
|
115
|
+
};
|
|
116
|
+
} catch (e) {
|
|
117
|
+
// A threshold is an optimisation; failing to measure one is not an error.
|
|
118
|
+
result = { above: RE_ROLL_ABOVE, measured: false, error: String(e?.message ?? e) };
|
|
119
|
+
}
|
|
120
|
+
cached = result;
|
|
121
|
+
return result.above;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** What the last `calibrateReroll` measured, or `null` before the first call. */
|
|
125
|
+
export function lastCalibration() {
|
|
126
|
+
return cached;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* `compileTape` at the threshold this engine prefers, measured once.
|
|
131
|
+
*
|
|
132
|
+
* The plain `compileTape` is synchronous and cannot wait for a measurement, so
|
|
133
|
+
* its `"auto"` is the built-in 2,000 — right for SpiderMonkey and
|
|
134
|
+
* JavaScriptCore, and low for V8. On posteriordb's 23 models in Node, that
|
|
135
|
+
* default loses on thirteen of them, by up to 12x. This is the same call with
|
|
136
|
+
* the measured number, and the measurement happens once per process.
|
|
137
|
+
*/
|
|
138
|
+
export async function compileTapeCalibrated(text, opts) {
|
|
139
|
+
const above = await calibrateReroll(opts);
|
|
140
|
+
return compileTape(text, String(above));
|
|
141
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapewasm",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Compile an autodiff tape to a WebAssembly module and sample it with nuts-rs, in the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"./pkg/*": "./pkg/*"
|
|
15
15
|
},
|
|
16
|
-
"files": ["index.js", "index.d.ts", "pkg/", "LICENSE-APACHE", "LICENSE-MIT", "README.md"],
|
|
16
|
+
"files": ["index.js", "index.d.ts", "calibrate.js", "pkg/", "LICENSE-APACHE", "LICENSE-MIT", "README.md"],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"build:wasm": "make -C .. wasm"
|
|
19
19
|
},
|
package/pkg/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "tapewasm",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Compile an autodiff tape to a wasm module and sample it with nuts-rs, in the browser. The wasm-bindgen API over tapewasm-codegen.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.3",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
package/pkg/tapewasm.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/* @ts-self-types="./tapewasm.d.ts" */
|
|
2
|
-
import { aot_evaluate, aot_logp } from './snippets/tapewasm-
|
|
3
|
-
import * as import1 from "./snippets/tapewasm-
|
|
4
|
-
import * as import2 from "./snippets/tapewasm-
|
|
5
|
-
import * as import3 from "./snippets/tapewasm-
|
|
6
|
-
import * as import4 from "./snippets/tapewasm-
|
|
7
|
-
import * as import5 from "./snippets/tapewasm-
|
|
2
|
+
import { aot_evaluate, aot_logp } from './snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js';
|
|
3
|
+
import * as import1 from "./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js"
|
|
4
|
+
import * as import2 from "./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js"
|
|
5
|
+
import * as import3 from "./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js"
|
|
6
|
+
import * as import4 from "./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js"
|
|
7
|
+
import * as import5 from "./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js"
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -563,15 +563,15 @@ function __wbg_get_imports() {
|
|
|
563
563
|
__wbg___wbindgen_throw_5d9e815e6fdf150f: function(arg0, arg1) {
|
|
564
564
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
|
565
565
|
},
|
|
566
|
-
|
|
566
|
+
__wbg_aot_evaluate_90ed2a4aec33a889: function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
|
567
567
|
const ret = aot_evaluate(arg0 >>> 0, arg1 >>> 0, arg2 >>> 0, arg3 >>> 0);
|
|
568
568
|
return ret;
|
|
569
569
|
}, arguments); },
|
|
570
|
-
|
|
570
|
+
__wbg_aot_logp_212144b205cad1c9: function(arg0, arg1, arg2, arg3) {
|
|
571
571
|
const ret = aot_logp(arg0 >>> 0, arg1 >>> 0, arg2 >>> 0, arg3 >>> 0);
|
|
572
572
|
return ret;
|
|
573
573
|
},
|
|
574
|
-
|
|
574
|
+
__wbg_call_ae8ca521a7cc221c: function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5, arg6) {
|
|
575
575
|
var v0 = getArrayF64FromWasm0(arg3, arg4).slice();
|
|
576
576
|
wasm.__wbindgen_free(arg3, arg4 * 8, 8);
|
|
577
577
|
var v1 = getArrayF64FromWasm0(arg5, arg6).slice();
|
|
@@ -614,11 +614,11 @@ function __wbg_get_imports() {
|
|
|
614
614
|
return {
|
|
615
615
|
__proto__: null,
|
|
616
616
|
"./tapewasm_bg.js": import0,
|
|
617
|
-
"./snippets/tapewasm-
|
|
618
|
-
"./snippets/tapewasm-
|
|
619
|
-
"./snippets/tapewasm-
|
|
620
|
-
"./snippets/tapewasm-
|
|
621
|
-
"./snippets/tapewasm-
|
|
617
|
+
"./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js": import1,
|
|
618
|
+
"./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js": import2,
|
|
619
|
+
"./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js": import3,
|
|
620
|
+
"./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js": import4,
|
|
621
|
+
"./snippets/tapewasm-a5eaf252e13afb7a/js/aot_bridge.js": import5,
|
|
622
622
|
};
|
|
623
623
|
}
|
|
624
624
|
|
package/pkg/tapewasm_bg.wasm
CHANGED
|
Binary file
|
/package/pkg/snippets/{tapewasm-c400eec12145fd93 → tapewasm-a5eaf252e13afb7a}/js/aot_bridge.js
RENAMED
|
File without changes
|