@audio/neural-separate 0.3.0 → 0.5.0

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.
@@ -0,0 +1,351 @@
1
+ #!/usr/bin/env python3
2
+ """Compact an exported separation graph: its weights stored in fewer bits, its compute left in float32.
3
+
4
+ pip install onnx onnxruntime numpy
5
+ python3 compact.py --model scnet-large --verify # scnet-large.int8.onnx
6
+ python3 compact.py --model tiger --as fp16 --verify # tiger.fp16.onnx
7
+
8
+ Reads <dir>/<model>.onnx (an export-*.py output; <dir> the neural cache's <model>/) and writes <model>.<as>.onnx beside
9
+ it. The weights are the initializers a Conv, ConvTranspose, MatMul, Gemm, LSTM or GRU takes as its weight, of over 1024
10
+ values (biases, norms and constants stay float32):
11
+ int8 stored as int8 with a float32 scale per output channel (symmetric, the channel's largest magnitude to 127; an
12
+ LSTM's or GRU's per gate row of each direction), Cast and multiplied by its scale in the graph; all of them when
13
+ their rounding together costs --budget dB under the output or less (40; 0: no bound) on the calibration mix,
14
+ else int8 for the weights cheapest per byte saved (each rounded alone, the rest float32) while their summed
15
+ cost stays within the budget, float16 for the rest (separate weights' costs add: the sum predicts the whole
16
+ to a fraction of a dB)
17
+ fp16 stored as float16, Cast to float32 in the graph
18
+ fp32 as they are
19
+ onnxruntime folds the Cast and the product at load (both of constants): the session holds float32 weights and computes
20
+ as the float32 graph does, on every execution provider. DequantizeLinear would not: onnxruntime 1.30 fuses it with the
21
+ MatMul it feeds into MatMulNBits, whose default accuracy level quantizes the activations to int8 on a CPU (MRX's int8
22
+ graph 38.4 dB from float32 so, 40.4 dB as stored here).
23
+
24
+ Every variant is also folded and named short, neither of which changes a value. With each input's shape fixed (SCNet's
25
+ and TIGER's graphs; MRX's takes any length and is left as it is), every value computable from the initializers and the
26
+ shapes alone is computed once and stored as the graph computes it (SCNet's DFT matrices, made in float64 from a Range,
27
+ which onnxruntime-web's WebGPU session neither places nor folds), but for a fill (ConstantOfShape: SCNet's LSTM zero
28
+ states), kept with its shape stored. Identical initializers are stored once; node and value names become base-36
29
+ counters (TIGER's 56,652 nodes carried 11.7 MB of names), the graph's inputs and outputs keep theirs.
30
+
31
+ --verify runs the variant and the float32 graph on the calibration mix (default <dir>/test.f32, scripts/reference.py's;
32
+ --calibrate another: planar float32 stereo at 44.1 kHz), cut and transformed as the package does it, and prints each
33
+ output's largest difference over its peak and its SNR against the float32 graph's (MRX's masks as the spectra they
34
+ mask).
35
+ """
36
+ import argparse
37
+ import hashlib
38
+ import os
39
+ import sys
40
+ from pathlib import Path
41
+
42
+ import numpy as np
43
+
44
+ WEIGHT = {"Conv": (1,), "ConvTranspose": (1,), "MatMul": (1,), "Gemm": (1,), "LSTM": (1, 2), "GRU": (1, 2)}
45
+ MIN, SMALL = 1024, 1024
46
+
47
+
48
+ def users_of(g):
49
+ users = {}
50
+ for n in g.node:
51
+ for k, name in enumerate(n.input): users.setdefault(name, []).append((n, k))
52
+ return users
53
+
54
+
55
+ def weights(g):
56
+ """{name: node} of the float32 initializers of over MIN values that every user takes as its weight."""
57
+ from onnx import TensorProto
58
+
59
+ users, out = users_of(g), {}
60
+ for i in g.initializer:
61
+ uses = users.get(i.name, [])
62
+ if i.data_type != TensorProto.FLOAT or int(np.prod(i.dims)) <= MIN or not uses: continue
63
+ if all(n.op_type in WEIGHT and k in WEIGHT[n.op_type] for n, k in uses) and len({n.op_type for n, _ in uses}) == 1:
64
+ out[i.name] = uses[0][0]
65
+ return out
66
+
67
+
68
+ def q8(node, w):
69
+ """Symmetric int8 per output channel (an LSTM's or GRU's per gate row of each direction), the channel's largest
70
+ magnitude to 127: q in w's layout and its scale, shaped to broadcast against it."""
71
+ keep = (0, 1) if node.op_type in ("LSTM", "GRU") else ({"ConvTranspose": 1, "MatMul": w.ndim - 1}.get(node.op_type, 0),)
72
+ if node.op_type == "Gemm": keep = (0 if any(x.name == "transB" and x.i for x in node.attribute) else 1,)
73
+ scale = np.maximum(np.abs(w).max(axis=tuple(k for k in range(w.ndim) if k not in keep), keepdims=True), 1e-30) / 127
74
+ return np.clip(np.round(w / scale), -127, 127).astype(np.int8), scale.astype(np.float32)
75
+
76
+
77
+ def rounded(node, w):
78
+ """w through q8 and back, in float32: what the int8 graph computes with."""
79
+ q, scale = q8(node, w)
80
+ return q.astype(np.float32) * scale
81
+
82
+
83
+ def prepend(g, nodes):
84
+ body = list(g.node)
85
+ del g.node[:]
86
+ g.node.extend(nodes + body)
87
+
88
+
89
+ def store(m, kinds):
90
+ """Each weight as kinds[name] says: 'fp16' (Cast), 'int8' (Cast, Mul by its scale), else float32."""
91
+ from onnx import helper, numpy_helper, TensorProto
92
+
93
+ g = m.graph
94
+ inits = {i.name: i for i in g.initializer}
95
+ nodes = []
96
+ for name, node in weights(g).items():
97
+ kind = kinds.get(name)
98
+ if kind not in ("fp16", "int8"): continue
99
+ w = numpy_helper.to_array(inits[name])
100
+ g.initializer.remove(inits[name])
101
+ if kind == "fp16":
102
+ g.initializer.append(numpy_helper.from_array(w.astype(np.float16), name + "_h"))
103
+ nodes.append(helper.make_node("Cast", [name + "_h"], [name], to=TensorProto.FLOAT))
104
+ continue
105
+ q, scale = q8(node, w)
106
+ g.initializer.extend([numpy_helper.from_array(q, name + "_q"), numpy_helper.from_array(scale, name + "_s")])
107
+ nodes += [helper.make_node("Cast", [name + "_q"], [name + "_c"], to=TensorProto.FLOAT), helper.make_node("Mul", [name + "_c", name + "_s"], [name])]
108
+ prepend(g, nodes)
109
+ return m
110
+
111
+
112
+ def static(g):
113
+ return all(d.dim_value > 0 for i in g.input for d in i.type.tensor_type.shape.dim)
114
+
115
+
116
+ def fold(m):
117
+ """Values computable from the initializers and the input shapes alone: stored when small, else made at load."""
118
+ import onnx
119
+ import onnxruntime as ort
120
+ from onnx import helper, numpy_helper, shape_inference
121
+
122
+ g = m.graph
123
+ if not static(g): return dedupe(m)
124
+ const, cnodes = {i.name for i in g.initializer}, []
125
+ for n in g.node:
126
+ ins = [x for x in n.input if x]
127
+ if n.op_type in ("Constant", "Shape") or (ins and all(x in const for x in ins) and not n.op_type.startswith("Random")):
128
+ cnodes.append(n)
129
+ const.update(n.output)
130
+ computed = [o for n in cnodes for o in n.output]
131
+ types = {v.name: v.type.tensor_type.elem_type for v in shape_inference.infer_shapes(m).graph.value_info}
132
+ for n in cnodes:
133
+ if n.op_type == "Constant": types[n.output[0]] = next(a.t.data_type for a in n.attribute if a.name == "value")
134
+ probe = onnx.ModelProto()
135
+ probe.CopyFrom(m)
136
+ probe.graph.output.extend(helper.make_tensor_value_info(x, types[x], None) for x in computed)
137
+ s = session(probe.SerializeToString())
138
+ shapes = {i.name: [d.dim_value for d in i.type.tensor_type.shape.dim] for i in g.input}
139
+ vals = dict(zip(computed, s.run(computed, {k: np.zeros(v, np.float32) for k, v in shapes.items()})))
140
+ # the same from another input, or the graph holds a shape that depends on the data: left as it is
141
+ other = s.run(computed, {k: np.random.default_rng(0).standard_normal(v).astype(np.float32) for k, v in shapes.items()})
142
+ if any(not np.array_equal(vals[x], y, equal_nan=vals[x].dtype.kind == "f") for x, y in zip(computed, other)): return dedupe(m)
143
+ # what the rest of the graph reads, stored; a fill (ConstantOfShape) kept, its shape stored
144
+ producer = {o: n for n in cnodes for o in n.output}
145
+ inner = {id(n) for n in cnodes}
146
+ keep, stored = set(), {}
147
+ want = [x for n in g.node if id(n) not in inner for x in n.input] + [o.name for o in g.output]
148
+ while want:
149
+ x = want.pop()
150
+ if x not in producer or x in stored or id(producer[x]) in keep: continue
151
+ if producer[x].op_type == "ConstantOfShape" and vals[x].size > SMALL:
152
+ keep.add(id(producer[x]))
153
+ want += list(producer[x].input)
154
+ else: stored[x] = vals[x]
155
+ body = [n for n in g.node if id(n) not in inner or id(n) in keep]
156
+ del g.node[:]
157
+ g.node.extend(body)
158
+ g.initializer.extend(numpy_helper.from_array(np.asarray(v), x) for x, v in stored.items())
159
+ return dedupe(m)
160
+
161
+
162
+ def dedupe(m):
163
+ """Identical initializers stored once; those no node reads dropped."""
164
+ from onnx import numpy_helper
165
+
166
+ g = m.graph
167
+ seen, alias = {}, {}
168
+ for i in list(g.initializer):
169
+ a = numpy_helper.to_array(i)
170
+ key = hashlib.sha256(str((a.dtype, a.shape)).encode() + a.tobytes()).hexdigest()
171
+ if key in seen:
172
+ alias[i.name] = seen[key]
173
+ g.initializer.remove(i)
174
+ else: seen[key] = i.name
175
+ for n in g.node:
176
+ for k, x in enumerate(n.input):
177
+ if x in alias: n.input[k] = alias[x]
178
+ read = {x for n in g.node for x in n.input} | {o.name for o in g.output}
179
+ for i in [i for i in g.initializer if i.name not in read]: g.initializer.remove(i)
180
+ return m
181
+
182
+
183
+ def shorten(m):
184
+ """Node and value names as base-36 counters; the inputs and outputs keep theirs."""
185
+ g = m.graph
186
+ keep = {v.name for v in list(g.input) + list(g.output)}
187
+ names = {}
188
+
189
+ def base36(k):
190
+ s = ""
191
+ while True:
192
+ s, k = "0123456789abcdefghijklmnopqrstuvwxyz"[k % 36] + s, k // 36
193
+ if not k: return s
194
+
195
+ def short(n):
196
+ if n == "" or n in keep: return n
197
+ if n not in names: names[n] = "_" + base36(len(names))
198
+ return names[n]
199
+
200
+ for i in g.initializer: i.name = short(i.name)
201
+ for k, n in enumerate(g.node):
202
+ for j, x in enumerate(n.input): n.input[j] = short(x)
203
+ for j, x in enumerate(n.output): n.output[j] = short(x)
204
+ n.name, n.doc_string = base36(k), "" # unique: onnxruntime-web's partitioner reports nodes by name
205
+ del g.value_info[:]
206
+ m.doc_string = ""
207
+ return m
208
+
209
+
210
+ # ------------------------------------------------------------------ calibration
211
+
212
+ def session(model, fold=False):
213
+ import onnxruntime as ort
214
+
215
+ o = ort.SessionOptions()
216
+ o.enable_cpu_mem_arena = False
217
+ o.enable_mem_pattern = False
218
+ o.log_severity_level = 3 # the overridable weights' warnings
219
+ if not fold: o.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
220
+ return ort.InferenceSession(model if isinstance(model, bytes) else str(model), o, providers=["CPUExecutionProvider"])
221
+
222
+
223
+ def feeds(g, mix):
224
+ """The graph's inputs from a mixture [2, samples] at 44.1 kHz, as separate.js cuts and transforms it: SCNet one
225
+ 11 s segment (no window, 1/sqrt(n)), TIGER one 12 s segment of the first channel (Hann), MRX 10 s of each
226
+ channel at three resolutions (Hann, 1/sqrt(n), magnitudes)."""
227
+ ins = {i.name: [d.dim_value or None for d in i.type.tensor_type.shape.dim] for i in g.input}
228
+
229
+ def spec(x, n, hop, win, scale):
230
+ pad = np.pad(x, ((0, 0), (n // 2, n // 2)), mode="reflect")
231
+ T = 1 + (pad.shape[1] - n) // hop
232
+ fr = np.stack([pad[:, t * hop:t * hop + n] for t in range(T)], 1) * win
233
+ return np.fft.rfft(fr, axis=-1).transpose(0, 2, 1) * scale # [C, F, T]
234
+
235
+ hann = lambda n: 0.5 - 0.5 * np.cos(2 * np.pi * np.arange(n) / n)
236
+ if list(ins) == ["mix_spec"]:
237
+ _, C2, F, T = ins["mix_spec"]
238
+ n = 2 * (F - 1)
239
+ tiger = n == 2048
240
+ x = mix[:C2 // 2, :(T - 1) * (n // 4)]
241
+ X = spec(x, n, n // 4, hann(n) if tiger else 1.0, 1.0 if tiger else n ** -0.5)[..., :T]
242
+ X = np.pad(X, ((0, 0), (0, 0), (0, T - X.shape[2])))
243
+ return {"mix_spec": np.stack([X.real, X.imag], 1).reshape(1, C2, F, T).astype(np.float32)}
244
+ x = mix[:, :441000]
245
+ return {k: np.abs(spec(x, int(k.split("_")[1]), 256, hann(int(k.split("_")[1])), int(k.split("_")[1]) ** -0.5)).astype(np.float32) for k in ins}
246
+
247
+
248
+ def outputs(s, feed):
249
+ """The outputs as the stems see them: a mask times the magnitudes it masks."""
250
+ ys = dict(zip([o.name for o in s.get_outputs()], s.run(None, feed)))
251
+ return {k: (y * feed["mag_" + k[5:]][:, None] if k.startswith("mask_") else y).astype(np.float64) for k, y in ys.items()}
252
+
253
+
254
+ def noise(y, y0):
255
+ """Error power over signal power, the worst output's."""
256
+ return max(((y[k] - y0[k]) ** 2).sum() / max((y0[k] ** 2).sum(), 1e-300) for k in y0)
257
+
258
+
259
+ def costs(m, feed, W):
260
+ """The cost at int8 of all the weights rounded together, and a function giving each one's alone (the others
261
+ float32): noise power over output power. The weights are fed as overridable initializers, one session for all."""
262
+ import onnx
263
+ from onnx import helper, numpy_helper
264
+
265
+ inits = {i.name: i for i in m.graph.initializer}
266
+ probe = onnx.ModelProto()
267
+ probe.CopyFrom(m)
268
+ probe.graph.input.extend(helper.make_tensor_value_info(n, inits[n].data_type, list(inits[n].dims)) for n in W)
269
+ s = session(probe.SerializeToString())
270
+ y0 = outputs(s, feed)
271
+ q = {n: rounded(node, numpy_helper.to_array(inits[n])) for n, node in W.items()}
272
+ return noise(outputs(s, {**feed, **q}), y0), lambda n: noise(outputs(s, {**feed, n: q[n]}), y0)
273
+
274
+
275
+ def int8(m, feed, budget):
276
+ """{weight: 'int8' | 'fp16'}: every weight int8 when their rounding together costs budget dB under the output or
277
+ less (no budget: always); else int8 for the weights cheapest per byte saved, each rounded alone, while their summed
278
+ cost stays within the budget (costs of separate weights add), float16 for the rest."""
279
+ W = weights(m.graph)
280
+ if budget is None: return {n: "int8" for n in W}
281
+ whole, alone = costs(m, feed, W)
282
+ print(f" every weight int8: SNR {-10 * np.log10(whole):.1f} dB against float32")
283
+ if whole <= 10 ** (-budget / 10): return {n: "int8" for n in W}
284
+ size = {i.name: int(np.prod(i.dims)) for i in m.graph.initializer}
285
+ cost = {}
286
+ for k, n in enumerate(W):
287
+ cost[n] = alone(n)
288
+ print(f"\r cost {k + 1}/{len(W)}", end="", file=sys.stderr)
289
+ print(file=sys.stderr)
290
+ kinds, total = {}, 0.0
291
+ for n in sorted(W, key=lambda n: cost[n] / size[n]):
292
+ if total + cost[n] <= 10 ** (-budget / 10): kinds[n], total = "int8", total + cost[n]
293
+ else: kinds[n] = "fp16"
294
+ for n in sorted((n for n in kinds if kinds[n] == "fp16"), key=lambda n: -cost[n]):
295
+ print(f" float16: {W[n].name or n}, {size[n]} values, alone at int8 {-10 * np.log10(max(cost[n], 1e-30)):.1f} dB")
296
+ print(f" int8: {sum(k == 'int8' for k in kinds.values())} of {len(kinds)} weights, "
297
+ f"{sum(size[n] for n in kinds if kinds[n] == 'int8') / sum(size[n] for n in W):.1%} of their values; predicted SNR {-10 * np.log10(max(total, 1e-30)):.1f} dB")
298
+ return kinds
299
+
300
+
301
+ def compact(path, out, kind, calibration=None, budget=40.0):
302
+ import onnx
303
+
304
+ m = onnx.load(str(path))
305
+ # the trained weights, chosen before folding (never a folded constant)
306
+ kinds = int8(m, feeds(m.graph, calibration), budget) if kind == "int8" else {n: kind for n in weights(m.graph)}
307
+ m = shorten(store(fold(m), kinds))
308
+ m.metadata_props.append(onnx.StringStringEntryProto(key="compact", value=kind + (f", budget {budget:g} dB" if kind == "int8" and budget else "")))
309
+ onnx.checker.check_model(m)
310
+ onnx.save(m, str(out))
311
+ return out
312
+
313
+
314
+ def verify(path, base, mix):
315
+ import onnx
316
+
317
+ a, b = session(base, True), session(path, True)
318
+ feed = feeds(onnx.load(str(base), load_external_data=False).graph, mix)
319
+ y0, y = outputs(a, feed), outputs(b, feed)
320
+ for k in y0:
321
+ d = y[k] - y0[k]
322
+ rel = np.abs(d).max() / np.abs(y0[k]).max()
323
+ print(f" {path.name} {k}: max|diff|/max|y| = {rel:.2e}, SNR {10 * np.log10((y0[k] ** 2).sum() / max((d ** 2).sum(), 1e-300)):.1f} dB against float32")
324
+
325
+
326
+ def main():
327
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
328
+ ap.add_argument("--model", required=True, help="scnet-large | scnet | mrx | tiger (any <dir>/<model>.onnx)")
329
+ ap.add_argument("--as", dest="kind", default="int8", choices=["fp32", "fp16", "int8"])
330
+ ap.add_argument("--budget", type=float, default=40, help="int8: the rounding's cost, dB under the output (40); 0: every weight int8")
331
+ ap.add_argument("--dir", default=None, help="default: $AUDIO_NEURAL_CACHE/<model> or ~/.cache/audiojs/neural/<model>")
332
+ ap.add_argument("--out", default=None, help="default: <dir>/<model>.<as>.onnx")
333
+ ap.add_argument("--calibrate", default=None, help="planar float32 stereo at 44.1 kHz (default <dir>/test.f32)")
334
+ ap.add_argument("--verify", action="store_true", help="the variant against the float32 graph on the calibration mix")
335
+ args = ap.parse_args()
336
+
337
+ cache = Path(os.environ.get("AUDIO_NEURAL_CACHE") or Path.home() / ".cache" / "audiojs" / "neural")
338
+ d = Path(args.dir) if args.dir else cache / args.model
339
+ path = d / f"{args.model}.onnx"
340
+ out = Path(args.out) if args.out else d / f"{args.model}.{args.kind}.onnx"
341
+ cal = Path(args.calibrate) if args.calibrate else d / "test.f32"
342
+ mix = np.fromfile(cal, np.float32).reshape(2, -1) if cal.exists() else None
343
+ budget = args.budget or None
344
+ if mix is None and ((args.kind == "int8" and budget) or args.verify): raise SystemExit(f"{cal}: no calibration mix (export with --verify writes test.f32)")
345
+ compact(path, out, args.kind, mix, budget)
346
+ print(f"wrote {out} ({out.stat().st_size / 1e6:.2f} MB, from {path.stat().st_size / 1e6:.2f})")
347
+ if args.verify: verify(out, path, mix)
348
+
349
+
350
+ if __name__ == "__main__":
351
+ sys.exit(main())
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // roformer.js, the package's own WebGPU engine for Mel-Band RoFormer, against onnxruntime on the same weights: Chromium on
3
+ // the GPU (playwright's 'chromium' channel: Metal here) runs the engine on one segment of scripts/reference.py's mix,
4
+ // onnxruntime-node's CPU the float16 ONNX file on the same spectrum; prints how far apart they are and the engine's time.
5
+ //
6
+ // node scripts/engine.mjs (needs playwright, onnxruntime-node, and in the neural cache, mel-roformer/:
7
+ // export-roformer.py --verify --engine, compact.py --model mel-roformer --as fp16)
8
+ import { createServer } from 'node:http'
9
+ import { readFileSync } from 'node:fs'
10
+ import path from 'node:path'
11
+ import os from 'node:os'
12
+ import { fileURLToPath } from 'node:url'
13
+ import { chromium } from 'playwright'
14
+ import * as ort from 'onnxruntime-node'
15
+ import { stft } from '../separate.js'
16
+
17
+ const HERE = path.dirname(fileURLToPath(import.meta.url)), CACHE = path.join(process.env.AUDIO_NEURAL_CACHE || path.join(os.homedir(), '.cache', 'audiojs', 'neural'), 'mel-roformer')
18
+ const F = 1025, T = 801, L = 352800, f32 = f => { let b = readFileSync(f); return new Float32Array(b.buffer, b.byteOffset, b.byteLength / 4) }
19
+
20
+ // one segment's spectrum, as separate.js's complexSegment makes it: [L re, L im, R re, R im][f][t]
21
+ const mix = f32(path.join(CACHE, 'test.f32')), N = mix.length / 2, spec = new Float32Array(4 * F * T)
22
+ for (let c = 0; c < 2; c++) {
23
+ let { re, im } = stft(Float64Array.from(mix.subarray(c * N, c * N + L)), { n: 2048, hop: 441 })
24
+ for (let f = 0; f < F; f++) for (let t = 0; t < T; t++) { spec[(2 * c * F + f) * T + t] = re[t][f]; spec[((2 * c + 1) * F + f) * T + t] = im[t][f] }
25
+ }
26
+ let cpu = await ort.InferenceSession.create(path.join(CACHE, 'mel-roformer.fp16.onnx'))
27
+ let want = (await cpu.run({ mix_spec: new ort.Tensor('float32', spec, [1, 4, F, T]) })).stems_spec.data
28
+
29
+ const files = { '/roformer.js': path.join(HERE, '..', 'roformer.js'), '/meta': path.join(CACHE, 'mel-roformer.engine.json'), '/bin': path.join(CACHE, 'mel-roformer.engine.bin') }
30
+ const server = createServer((q, r) => {
31
+ if (q.url === '/') return r.writeHead(200, { 'content-type': 'text/html' }).end('<!doctype html><title>engine</title>')
32
+ if (q.url === '/spec') return r.writeHead(200).end(Buffer.from(spec.buffer))
33
+ if (!files[q.url]) return r.writeHead(404).end()
34
+ r.writeHead(200, { 'content-type': q.url.endsWith('.js') ? 'text/javascript' : 'application/octet-stream' }).end(readFileSync(files[q.url]))
35
+ })
36
+ await new Promise(r => server.listen(0, '127.0.0.1', r))
37
+ const browser = await chromium.launch({ channel: 'chromium', args: ['--enable-unsafe-webgpu'] }), page = await browser.newPage()
38
+ await page.goto(`http://127.0.0.1:${server.address().port}/`)
39
+ const got = await page.evaluate(async () => {
40
+ const { engine } = await import('/roformer.js')
41
+ const meta = await (await fetch('/meta')).json(), bytes = new Uint8Array(await (await fetch('/bin')).arrayBuffer())
42
+ let t = performance.now()
43
+ const s = await engine(meta, bytes)
44
+ if (!s) return { none: true }
45
+ const ready = performance.now() - t, spec = new Float32Array(await (await fetch('/spec')).arrayBuffer()), times = []
46
+ let out
47
+ for (let k = 0; k < 4; k++) { t = performance.now(); out = (await s.run({ mix_spec: { data: spec } })).stems_spec.data; times.push(performance.now() - t) }
48
+ return { ready, times, out: [...out] }
49
+ })
50
+ await browser.close(); server.close()
51
+ if (got.none) console.log('no GPU with subgroup matrices here: the engine stands aside for onnxruntime')
52
+ else {
53
+ let e = 0, p = 0
54
+ for (let i = 0; i < want.length; i++) { e += (got.out[i] - want[i]) ** 2; p += want[i] ** 2 }
55
+ console.log(`engine ready in ${(got.ready / 1000).toFixed(1)} s; a segment ${got.times.map(t => t.toFixed(0)).join(', ')} ms; ` +
56
+ `${(10 * Math.log10(p / e)).toFixed(1)} dB from onnxruntime's CPU on the same weights`)
57
+ }
58
+ process.exit(0)