@audio/neural-separate 0.4.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.
- package/README.md +10 -2
- package/index.d.ts +2 -2
- package/package.json +5 -2
- package/roformer.js +396 -0
- package/scripts/engine.mjs +58 -0
- package/scripts/export-roformer.py +481 -0
- package/scripts/reference.py +5 -2
- package/separate.js +57 -5
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ In the browser `@audio/neural-runtime` runs `onnxruntime-web` instead (`device:
|
|
|
28
28
|
| `'htdemucs_ft'` | four fine-tuned HTDemucs, one per source | 4 × 174 MB | research only | 8.67 · 9.45 · 9.68 · 5.58 | vocals only: 1.1 · not measured · not measured |
|
|
29
29
|
| `'scnet-large'` | SCNet-large: band-split convolutions around dual-path LSTMs, on the complex spectrogram | 45 MB (export 169 MB) | MIT | 11.00 · 10.27 · 8.21 · 6.87 | 2.1 · 7.8 · 5.0 |
|
|
30
30
|
| `'scnet'` | SCNet: the same at half the width | 13 MB (export 43 MB) | MIT | 9.88 · 9.43 · 8.35 · 6.15 | 0.70 · 1.7 · 0.91 |
|
|
31
|
+
| `'mel-roformer'` | Mel-Band RoFormer (Kimberley Jensen's vocals model): axial transformers over time and mel bands, on the complex spectrogram; written for the GPU | 464 MB, float16 weights (export 928 MB) | MIT | vocals only: 12.08 | 1.4 · not measured · 0.56 (Node's WebGPU; the export 0.44) |
|
|
31
32
|
|
|
32
33
|
SDR as measured below (Verification), identical to the Python originals'. The previews are 7 s excerpts, so these numbers sit apart from full-track results in scale, not in order: the Demucs README reports overall SDR 9.0 for fine-tuned HT Demucs against 5.3 for Open-Unmix on the MUSDB18-HQ test set.
|
|
33
34
|
|
|
@@ -67,8 +68,9 @@ the float32 graph's.
|
|
|
67
68
|
## Weights
|
|
68
69
|
|
|
69
70
|
Nothing ships in this package. `scnet-large`, `scnet`, `mrx` and `tiger` read a compact file each, `<model>.int8.onnx`
|
|
70
|
-
(Size, below),
|
|
71
|
-
|
|
71
|
+
(Size, below), and `mel-roformer` its float16 weights, `mel-roformer.fp16.onnx` (or `roformer.js`'s `mel-roformer.engine.bin`), hosted on Hugging Face
|
|
72
|
+
(`audiojs/scnet-large`, `audiojs/scnet`, `audiojs/mrx`, `audiojs/tiger-dnr`, `audiojs/mel-roformer`, each with its model
|
|
73
|
+
card and licence): without `weights`, `https://huggingface.co/<repo>/resolve/<revision>/<file>` at the
|
|
72
74
|
revision `REVISIONS` pins, fetched once and checked against its SHA-256 (the browser keeps it in the Cache API, Node in
|
|
73
75
|
the neural cache, below). Every model can also be exported from its original checkpoint:
|
|
74
76
|
|
|
@@ -88,6 +90,10 @@ python3 node_modules/@audio/neural-separate/scripts/export-mrx.py --verify
|
|
|
88
90
|
pip install safetensors
|
|
89
91
|
python3 node_modules/@audio/neural-separate/scripts/export-tiger.py --verify
|
|
90
92
|
|
|
93
|
+
pip install einops beartype rotary-embedding-torch librosa
|
|
94
|
+
python3 node_modules/@audio/neural-separate/scripts/export-roformer.py --verify --fp16 --engine
|
|
95
|
+
node node_modules/@audio/neural-separate/scripts/engine.mjs # roformer.js in Chromium against onnxruntime (playwright)
|
|
96
|
+
|
|
91
97
|
python3 node_modules/@audio/neural-separate/scripts/compact.py --model tiger --verify # tiger.int8.onnx from tiger.onnx
|
|
92
98
|
```
|
|
93
99
|
|
|
@@ -97,6 +103,8 @@ They land in `$AUDIO_NEURAL_CACHE/<model>/`, default `~/.cache/audiojs/neural/<m
|
|
|
97
103
|
- **`export-scnet.py`** fetches SCNet's code from ZFTurbo/Music-Source-Separation-Training at a pinned commit and the author's MUSDB18-HQ checkpoint from that repository's releases, and exports the network between its STFT and iSTFT (the same split as demucs.onnx's): `mix_spec [1, 4, 2049, 476]` in, `stems_spec [1, 16, 2049, 476]` out, one 11 s segment. Two changes to the graph, neither to its function: the rFFT and irFFT over time in its separation network become products with cosine and sine matrices (torch 2.14 exports no `fft_rfft`), and each `GroupNorm(1, C)` reduces its mean and variance one axis at a time: exported whole, onnxruntime summed a 10-million-value group in float32 and the stems came out 52 to 55 dB from PyTorch's, against 110 dB now (PyTorch's own float32 against float64: 110 dB). `--fp16` also writes float16 weights.
|
|
98
104
|
- **`export-mrx.py`** fetches MRX's code and MERL's checkpoint (`default_mrx_pre_trained_weights.pth`, trained with the SNR loss so its stems keep the mixture's level; the three others are scale-invariant or trained on loudness- or EQ-adapted stems, and their stems come out at another level: on Divide and Remaster v3 their SNR is negative where their SI-SDR is up to 1 dB higher) from merlresearch/cocktail-fork-separation at a pinned commit, and exports the network between its STFTs and iSTFTs: each channel's three magnitude spectrograms `mag_1024 [B, 513, T]`, `mag_2048 [B, 1025, T]`, `mag_8192 [B, 4097, T]` in (scaled 1/√n, torch.stft's `normalized=True`), the masks `mask_<n> [B, 3, F, T]` out, music, speech and sfx; B and T free.
|
|
99
105
|
- **`export-tiger.py`** fetches TIGER's model code (JusperLee/TIGER, `look2hear/models/tiger_dnr.py` and the two layer modules it reads) at a pinned commit and its Divide and Remaster weights (huggingface.co/JusperLee/TIGER-DnR) at a pinned revision, and exports TIGERDNR's three models between their STFT and iSTFT as one graph, each its kept source (the first's dialogue, the second's effects, the third's music): `mix_spec [1, 2, 1025, 1034]` (one channel's STFT, n 2048, hop 512, Hann, unnormalized) in, `stems_spec [1, 6, 1025, 1034]` (dialogue, music, effects, re and im) out, one 12 s segment. Three changes to the graph, none to its function: `F.adaptive_avg_pool1d`, which the exporter takes only for output sizes dividing the input's, becomes a cumulative sum read at torch's windows; UConvBlock's global sum starts at its first term, not at a zeros tensor exported as a constant the size of the features; each `GroupNorm(1, C)` reduces axis by axis, as SCNet's. ONNX Runtime runs the graph in 12 s segments at about twice real time on a CPU: three models of 15,000 small operators each, every sample in three segments.
|
|
106
|
+
- **`export-roformer.py`** fetches Mel-Band RoFormer's code and Kim's config from ZFTurbo/Music-Source-Separation-Training at a pinned commit and Kim's checkpoint (huggingface.co/KimberleyJSN/melbandroformer, MIT) at a pinned revision, checked by its SHA-256, and exports the network between its STFT and iSTFT: `mix_spec [1, 4, 1025, 801]` in, `stems_spec [1, 4, 1025, 801]` (the vocals) out, one 8 s segment every 4 s. It is written for the GPU, its function unchanged (`--verify`: 1e-5 to 2e-4 of the peak from MelBandRoformer.forward, whose own float32 stands 5e-3 from its float64 on tones): the 60 bands' projections and mask MLPs as batched products, bands of near widths grouped and zero-padded; each RMSNorm's scale folded into the weights it feeds, the attention's scale into the queries, its gates' projection joined to q, k, v; the rotary embedding's interleaved pairs turned to halves (the same permutation of q's and k's features), so it is two products with cos and sin tables and one with a fixed matrix; the overlapping mel bands' masks averaged by two gathers, not a scatter. 826 operators where an export of BS-RoFormer as written has about 3,000, and SCNet's LSTMs none: on Node's WebGPU (Metal, M4 Max) 2.4 s a segment against 9 s on its CPU; SCNet-large's LSTMs take 19 of its 26 s a segment there. And no tensor passes 100 MB, each transformer run on slices of its sequences: onnxruntime 1.30's WebGPU Softmax over a tensor past WebGPU's default 128 MiB storage binding came out wrong one run in two (36 dB from the CPU's at 307 MB, every run right at 123 MB), and the stems with it. `--fp16`: float16 weights, float32 compute, half the download, 3.0 s a segment and 48 dB from the float32 file's stems.
|
|
107
|
+
- **`roformer.js`**: in a browser whose GPU offers subgroup matrices (`chromium-experimental-subgroup-matrix`: Chromium on Metal), `mel-roformer` runs on kernels of its own instead of onnxruntime, from `mel-roformer.engine.bin` and `.json` (`export-roformer.py --engine`: the same tensors, hosted beside the ONNX file; a browser downloads one or the other). Matrix products and attention on the GPU's matrix units, float32: band split, norms, the q, k, v projection with the rotation and gates, attention in two passes over the keys (the rows' maxima, then the output in registers), feed-forward with GELU, the mask MLP with tanh and GLU, the masks averaged and applied; 163 dispatches a segment, recorded once; kept on the GPU a minute after a separation, so the next starts at once (its weights and 2 GB of buffers made in about a second the first time). Against onnxruntime's CPU on the same weights 101.5 dB (`scripts/engine.mjs`); in Chromium on an M4 Max under the same load as onnxruntime-web's WebGPU there, 1.5 to 1.8 s a segment against 2.6 to 3.3; GPU time by kernel: the time attention a third, the matrix products half (about 3 TFLOPS each, against onnxruntime's 1.8 for its float32 products and 0.5 to 0.75 for its attention). Elsewhere (Node, Firefox, Safari, a GPU without them) onnxruntime runs the ONNX file as before.
|
|
100
108
|
- **`export-htdemucs.py`** follows [sevagh/demucs.onnx](https://github.com/sevagh/demucs.onnx): the STFT and iSTFT, which ONNX export cannot carry, move out of the graph. Rather than a vendored copy of `htdemucs.py`, it runs upstream's own `forward` with its four transform methods swapped on the instance. One graph per 7.8 s segment: `mix [1, 2, 343980]` and `mix_spec [1, 4, 2048, 336]` (complex as channels) in, `stems_spec [1, 4, 4, 2048, 336]` and `stems_wave [1, 4, 2, 343980]` out; a source is the iSTFT of the first plus the second.
|
|
101
109
|
|
|
102
110
|
## Size
|
package/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export type AudioInput = Float32Array[] | { channelData: Float32Array[]; sampleR
|
|
|
12
12
|
export type ModelSpec = string | Uint8Array
|
|
13
13
|
|
|
14
14
|
/** Presets whose files the export scripts write: <weights>/<name>/<target>.onnx or <weights>/<name>/<name>.onnx */
|
|
15
|
-
export type ModelName = 'umxhq' | 'htdemucs' | 'htdemucs_ft' | 'scnet-large' | 'scnet' | 'mrx' | 'tiger'
|
|
15
|
+
export type ModelName = 'umxhq' | 'htdemucs' | 'htdemucs_ft' | 'scnet-large' | 'scnet' | 'mrx' | 'tiger' | 'mel-roformer'
|
|
16
16
|
|
|
17
17
|
/** One graph whose output stacks several sources on its S axis */
|
|
18
18
|
export type MultiGraph = { url: ModelSpec; targets: string[] }
|
|
@@ -74,7 +74,7 @@ export interface ModelPreset {
|
|
|
74
74
|
export const models: Record<ModelName, ModelPreset>
|
|
75
75
|
|
|
76
76
|
/** The hosted revision (commit) of each compact preset's repository: https://huggingface.co/<repo>/resolve/<revision>/<file> is read without opts.weights in the browser, and in Node when the cache holds neither the file nor its export. Empty: not hosted. */
|
|
77
|
-
export const REVISIONS: Record<'scnet-large' | 'scnet' | 'mrx' | 'tiger', string>
|
|
77
|
+
export const REVISIONS: Record<'scnet-large' | 'scnet' | 'mrx' | 'tiger' | 'mel-roformer', string>
|
|
78
78
|
|
|
79
79
|
/** A neural-runtime-shaped session — enough of it to drive separate() with a test double. */
|
|
80
80
|
export interface Session {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@audio/neural-separate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Source separation (stems): a song's by SCNet (MIT weights), Hybrid Transformer Demucs, Open-Unmix with multichannel Wiener refinement; a soundtrack's dialogue, music and effects by MRX (MIT weights) and TIGER (Apache-2.0); model-agnostic ONNX",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,18 +15,21 @@
|
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"separate.js",
|
|
18
|
+
"roformer.js",
|
|
18
19
|
"index.d.ts",
|
|
19
20
|
"scripts/export-openunmix.py",
|
|
20
21
|
"scripts/export-htdemucs.py",
|
|
21
22
|
"scripts/export-scnet.py",
|
|
22
23
|
"scripts/export-mrx.py",
|
|
23
24
|
"scripts/export-tiger.py",
|
|
25
|
+
"scripts/export-roformer.py",
|
|
26
|
+
"scripts/engine.mjs",
|
|
24
27
|
"scripts/reference.py",
|
|
25
28
|
"scripts/compact.py",
|
|
26
29
|
"NOTICE"
|
|
27
30
|
],
|
|
28
31
|
"dependencies": {
|
|
29
|
-
"@audio/neural-runtime": "^0.1.
|
|
32
|
+
"@audio/neural-runtime": "^0.1.1",
|
|
30
33
|
"@audio/resample-sinc": "^1.1.2",
|
|
31
34
|
"fourier-transform": "^2.4.1"
|
|
32
35
|
},
|
package/roformer.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// Mel-Band RoFormer on WebGPU, kernels of its own: the GPU form scripts/export-roformer.py writes (--engine), one 8 s
|
|
2
|
+
// segment a run, what its ONNX graph computes (47.8 dB from onnxruntime's CPU on the float32 graph: the float16 weights'
|
|
3
|
+
// rounding, as the float16 ONNX file's). Matrix products and attention on the GPU's matrix units through
|
|
4
|
+
// chromium_experimental_subgroup_matrix (Apple's simdgroup matrices: Chromium on Metal), float32 throughout: under the same
|
|
5
|
+
// load, 1.6 s a segment where onnxruntime-web's WebGPU takes 2.6 to 3.3 (an M4 Max). Attention in two passes over the
|
|
6
|
+
// keys (the rows' maxima, then the output in registers), never past 128 MiB a dispatch needs no binding beyond it.
|
|
7
|
+
// let rf = await engine(meta, bytes) // null where the GPU lacks subgroup matrices
|
|
8
|
+
// let { stems_spec } = await rf.run({ mix_spec }) // as an onnxruntime session: [1, 4, F, T] in and out
|
|
9
|
+
const TILE = 32
|
|
10
|
+
|
|
11
|
+
/** A device with what the kernels need, or null */
|
|
12
|
+
export async function gpu() {
|
|
13
|
+
let adapter = await globalThis.navigator?.gpu?.requestAdapter({ powerPreference: 'high-performance' }).catch(() => null)
|
|
14
|
+
if (!adapter || adapter.info?.isFallbackAdapter || !['subgroups', 'chromium-experimental-subgroup-matrix'].every(f => adapter.features.has(f))) return null
|
|
15
|
+
let cfg = [...(adapter.info.subgroupMatrixConfigs ?? [])]
|
|
16
|
+
if (!cfg.some(c => c.componentType === 'f32' && c.M === 8 && c.N === 8 && c.K === 8)) return null
|
|
17
|
+
return adapter.requestDevice({
|
|
18
|
+
requiredFeatures: ['subgroups', 'chromium-experimental-subgroup-matrix'],
|
|
19
|
+
requiredLimits: { maxComputeWorkgroupStorageSize: 32768, maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize, maxBufferSize: adapter.limits.maxBufferSize },
|
|
20
|
+
}).catch(() => null)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The engine as an onnxruntime-shaped session ({ inputs, outputs, run(feeds), free() }), or null without such a GPU */
|
|
24
|
+
export async function engine(meta, bytes, device) {
|
|
25
|
+
device ??= await gpu()
|
|
26
|
+
if (!device) return null
|
|
27
|
+
let rf = await roformer(device, meta, bytes), { freqs: F, frames: T } = meta
|
|
28
|
+
return {
|
|
29
|
+
inputs: [{ name: 'mix_spec', dims: [1, 4, F, T], type: 'float32' }], outputs: [{ name: 'stems_spec', dims: [1, 4, F, T], type: 'float32' }],
|
|
30
|
+
backend: 'roformer',
|
|
31
|
+
async run(feeds) { return { stems_spec: { data: await rf.run(feeds.mix_spec.data), dims: [1, 4, F, T], type: 'float32' } } },
|
|
32
|
+
free() { device.destroy() },
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function roformer(device, meta, bytes) {
|
|
37
|
+
const { frames: T, freqs: F, channels: S, dim: D, heads: H, depth, bands: B, groups } = meta
|
|
38
|
+
const DH = 64, FF = 1536, MH = 1536, Tp = Math.ceil(T / TILE) * TILE, QKV = 3 * H * DH, QKVG = Math.ceil((QKV + H) / TILE) * TILE
|
|
39
|
+
const COLS = F * S * 2, LT = Math.ceil(T / 16) * 16, LF = Math.ceil(B / 16) * 16
|
|
40
|
+
const pad16 = w => Math.ceil(w / 16) * 16
|
|
41
|
+
const G = groups.map(([b0, b1, W]) => ({ b0, b1, n: b1 - b0, W, Wp: pad16(W) }))
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------- tensors
|
|
44
|
+
const view = n => {
|
|
45
|
+
const t = meta.tensors[n], len = t.shape.reduce((a, b) => a * b, 1)
|
|
46
|
+
return t.type === 'float16' ? new Float16Array(bytes.buffer, bytes.byteOffset + t.offset, len)
|
|
47
|
+
: t.type === 'int32' ? new Int32Array(bytes.buffer, bytes.byteOffset + t.offset, len)
|
|
48
|
+
: new Float32Array(bytes.buffer, bytes.byteOffset + t.offset, len)
|
|
49
|
+
}
|
|
50
|
+
const usage = GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
51
|
+
const buf = (n, type = 'f32') => device.createBuffer({ size: Math.max(16, n * 4), usage })
|
|
52
|
+
const upload = data => { const b = device.createBuffer({ size: Math.max(16, data.byteLength), usage, mappedAtCreation: true }); new data.constructor(b.getMappedRange()).set(data); b.unmap(); return b }
|
|
53
|
+
const f32 = a => a instanceof Float32Array ? a : Float32Array.from(a)
|
|
54
|
+
// a weight [batch, K, N] to [batch, Kp, Np], zeros past (rows, cols)
|
|
55
|
+
const padded = (a, batch, K, N, Kp, Np, place = (k, n) => k * Np + n) => {
|
|
56
|
+
const o = new Float32Array(batch * Kp * Np)
|
|
57
|
+
for (let b = 0; b < batch; b++) for (let k = 0; k < K; k++) for (let n = 0; n < N; n++) o[b * Kp * Np + place(k, n)] = a[(b * K + k) * N + n]
|
|
58
|
+
return o
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const W = { split: [], splitB: [], splitIdx: [], m2: [], m2B: [] }
|
|
62
|
+
for (const [g, gr] of G.entries()) {
|
|
63
|
+
W.split.push(upload(padded(view(`split_w.${g}`), gr.n, gr.W, D, gr.Wp, D)))
|
|
64
|
+
W.splitB.push(upload(f32(view(`split_b.${g}`))))
|
|
65
|
+
const idx = view(`si${g}`), pi = new Int32Array(gr.n * gr.Wp).fill(COLS)
|
|
66
|
+
for (let j = 0; j < gr.n; j++) for (let e = 0; e < gr.W; e++) pi[j * gr.Wp + e] = idx[j * gr.W + e]
|
|
67
|
+
W.splitIdx.push(upload(pi))
|
|
68
|
+
// the last mask layer: its value half to [0, W), its gate half to [Wp, Wp + W)
|
|
69
|
+
const w = view(`m2_w.${g}`), b = view(`m2_b.${g}`), Wp = gr.Wp, wo = new Float32Array(gr.n * MH * 2 * Wp), bo = new Float32Array(gr.n * 2 * Wp)
|
|
70
|
+
for (let j = 0; j < gr.n; j++) {
|
|
71
|
+
for (let k = 0; k < MH; k++) for (let e = 0; e < gr.W; e++) {
|
|
72
|
+
wo[(j * MH + k) * 2 * Wp + e] = w[(j * MH + k) * 2 * gr.W + e]
|
|
73
|
+
wo[(j * MH + k) * 2 * Wp + Wp + e] = w[(j * MH + k) * 2 * gr.W + gr.W + e]
|
|
74
|
+
}
|
|
75
|
+
for (let e = 0; e < gr.W; e++) { bo[j * 2 * Wp + e] = b[j * 2 * gr.W + e]; bo[j * 2 * Wp + Wp + e] = b[j * 2 * gr.W + gr.W + e] }
|
|
76
|
+
}
|
|
77
|
+
W.m2.push(upload(wo)); W.m2B.push(upload(bo))
|
|
78
|
+
}
|
|
79
|
+
W.mw = [0, 1].map(i => upload(f32(view(`mw.${i}`)))); W.mb = [0, 1].map(i => upload(f32(view(`mb.${i}`))))
|
|
80
|
+
W.blocks = []
|
|
81
|
+
for (let d = 0; d < depth; d++) for (let k = 0; k < 2; k++) {
|
|
82
|
+
const p = `blocks.${d}.${k}.`, a = p + 'layers.0.0.', f = p + 'layers.0.1.'
|
|
83
|
+
W.blocks.push({
|
|
84
|
+
out: upload(f32(view(p + 'out'))),
|
|
85
|
+
qkvg: upload(padded(view(a + 'qkvg'), 1, D, QKV + H, D, QKVG)), gb: upload(f32(view(a + 'gb'))), wo: upload(f32(view(a + 'wo'))),
|
|
86
|
+
w1: upload(f32(view(f + 'w1'))), b1: upload(f32(view(f + 'b1'))), w2: upload(f32(view(f + 'w2'))), b2: upload(f32(view(f + 'b2'))),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
const rope = k => { const c = view(`blocks.0.${k}.layers.0.0.cos`), s = view(`blocks.0.${k}.layers.0.0.sin`); return [upload(f32(c)), upload(f32(s))] }
|
|
90
|
+
const [cosT, sinT] = rope(0), [cosF, sinF] = rope(1)
|
|
91
|
+
// the masks' sources: per output column, two (base, stride: the group's Wp) into the GLU outputs, and its weight
|
|
92
|
+
const i1 = view('i1'), i2 = view('i2'), wt = view('wt'), gofs = [], cols = []
|
|
93
|
+
let mOff = 0, cOff = 0
|
|
94
|
+
for (const gr of G) { gofs.push(mOff); cols.push(cOff); mOff += gr.n * Tp * gr.Wp; cOff += gr.n * gr.W }
|
|
95
|
+
const src = c => {
|
|
96
|
+
if (c >= cOff) return [0, 0, 0]
|
|
97
|
+
let g = cols.findLastIndex(o => o <= c), gr = G[g], r = c - cols[g], j = Math.floor(r / gr.W), e = r % gr.W
|
|
98
|
+
return [gofs[g] + j * Tp * gr.Wp + e, gr.Wp, 1]
|
|
99
|
+
}
|
|
100
|
+
const map = new Uint32Array(COLS * 8) // per column: base1, stride1, on1, base2, stride2, on2, -, -
|
|
101
|
+
for (let o = 0; o < COLS; o++) map.set([...src(i1[o]), ...src(i2[o])], o * 8)
|
|
102
|
+
const maskMap = upload(map)
|
|
103
|
+
const maskWt = upload(Float32Array.from({ length: COLS }, (_, o) => wt[o]))
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------- activations
|
|
106
|
+
const E = buf(Tp * (COLS + 1)), X = buf(B * Tp * D), XN = buf(B * Tp * D), Y = buf(B * Tp * QKVG)
|
|
107
|
+
const att = Math.max(B * H * LT, T * H * LF) * DH
|
|
108
|
+
const Qb = buf(att), Kb = buf(att), Vb = buf(att), Ob = buf(att), Gt = buf(B * Tp * H), Z = buf(B * Tp * H * DH)
|
|
109
|
+
const FFH = buf(B * Tp * FF), H2 = buf(B * Tp * MH), Mg = buf(mOff), OUT = buf(4 * F * T)
|
|
110
|
+
const splitA = G.map(gr => buf(gr.n * Tp * gr.Wp)), glu = G.map(gr => buf(gr.n * Tp * 2 * gr.Wp)), scratch = buf(4)
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------- kernels
|
|
113
|
+
const pipes = new Map()
|
|
114
|
+
const pipe = (key, code) => pipes.get(key) ?? pipes.set(key, device.createComputePipeline({ layout: 'auto', compute: { module: device.createShaderModule({ code }), entryPoint: 'main' } })).get(key)
|
|
115
|
+
const uni = (...v) => { const b = device.createBuffer({ size: Math.ceil(v.length / 4) * 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); device.queue.writeBuffer(b, 0, new Uint32Array(v.map(x => typeof x === 'number' && !Number.isInteger(x) ? new Uint32Array(new Float32Array([x]).buffer)[0] : x))); return b }
|
|
116
|
+
const steps = [] // [pipeline, bindings, [x, y, z]]
|
|
117
|
+
const step = (p, bindings, grid) => steps.push([p, device.createBindGroup({ layout: p.getBindGroupLayout(0), entries: bindings.map((b, i) => ({ binding: i, resource: { buffer: b } })) }), grid])
|
|
118
|
+
|
|
119
|
+
const HEAD = `enable subgroups;\nenable chromium_experimental_subgroup_matrix;\n`
|
|
120
|
+
// C[b] (M×N, row stride ldc, at c0 + b·sc) = A[b] (M×K, at a0 + b·sa) · Bm[b] (K×N, at b0 + b·sb), then the epilogue:
|
|
121
|
+
// ep: '' as is; 'b' + bias (at bias0 + b·sbias); then 'g' gelu or 't' tanh; 'r' C += it (a residual)
|
|
122
|
+
const MM = ep => pipe('mm' + ep, HEAD + `
|
|
123
|
+
@group(0) @binding(0) var<storage, read> A: array<f32>;
|
|
124
|
+
@group(0) @binding(1) var<storage, read> Bm: array<f32>;
|
|
125
|
+
@group(0) @binding(2) var<storage, read_write> C: array<f32>;
|
|
126
|
+
@group(0) @binding(3) var<storage, read> bias: array<f32>;
|
|
127
|
+
struct P { M: u32, N: u32, K: u32, a0: u32, sa: u32, b0: u32, sb: u32, c0: u32, sc: u32, ldc: u32, bias0: u32, sbias: u32 }
|
|
128
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
129
|
+
var<workgroup> tile: array<f32, 1024>;
|
|
130
|
+
@compute @workgroup_size(32)
|
|
131
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(subgroup_invocation_id) lane: u32) {
|
|
132
|
+
_ = bias[0]; // bound whether read or not: the layout is the module's own
|
|
133
|
+
let r0 = wg.y * 32u; let n0 = wg.x * 32u; let b = wg.z;
|
|
134
|
+
let ab = p.a0 + b * p.sa + r0 * p.K; let bb = p.b0 + b * p.sb + n0;
|
|
135
|
+
${[0, 1, 2, 3].map(i => [0, 1, 2, 3].map(j => `var c${i}${j}: subgroup_matrix_result<f32, 8, 8>;`).join(' ')).join('\n ')}
|
|
136
|
+
for (var k = 0u; k < p.K; k += 8u) {
|
|
137
|
+
${[0, 1, 2, 3].map(i => `let a${i} = subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>>(&A, ab + ${i * 8}u * p.K + k, false, p.K);`).join('\n ')}
|
|
138
|
+
${[0, 1, 2, 3].map(j => `let b${j} = subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>>(&Bm, bb + k * p.N + ${j * 8}u, false, p.N);`).join('\n ')}
|
|
139
|
+
${[0, 1, 2, 3].map(i => [0, 1, 2, 3].map(j => `c${i}${j} = subgroupMatrixMultiplyAccumulate(a${i}, b${j}, c${i}${j});`).join(' ')).join('\n ')}
|
|
140
|
+
}
|
|
141
|
+
${[0, 1, 2, 3].map(i => [0, 1, 2, 3].map(j => `subgroupMatrixStore(&tile, ${i * 8 * 32 + j * 8}u, c${i}${j}, false, 32u);`).join(' ')).join('\n ')}
|
|
142
|
+
workgroupBarrier();
|
|
143
|
+
for (var e = lane; e < 1024u; e += 32u) {
|
|
144
|
+
let r = r0 + e / 32u; let n = n0 + e % 32u;
|
|
145
|
+
if (r < p.M && n < p.N) {
|
|
146
|
+
var v = tile[e];
|
|
147
|
+
${ep.includes('b') ? 'v += bias[p.bias0 + b * p.sbias + n];' : ''}
|
|
148
|
+
${ep.includes('g') ? 'v = 0.5 * v * (1.0 + erf_(v * 0.7071067811865476));' : ep.includes('t') ? 'v = tanh(v);' : ''}
|
|
149
|
+
let o = p.c0 + b * p.sc + r * p.ldc + n;
|
|
150
|
+
${ep.includes('r') ? 'C[o] += v;' : 'C[o] = v;'}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fn erf_(x: f32) -> f32 { // Abramowitz & Stegun 7.1.26 is 1.5e-7; GELU's own float32 erf is as close
|
|
155
|
+
let t = 1.0 / (1.0 + 0.3275911 * abs(x));
|
|
156
|
+
let y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * exp(-x * x);
|
|
157
|
+
return select(-y, y, x >= 0.0);
|
|
158
|
+
}`)
|
|
159
|
+
const mm = (ep, A, Bm, C, bias, { M, N, K, batch = 1, a0 = 0, sa = 0, b0 = 0, sb = 0, c0 = 0, sc = 0, ldc = N, bias0 = 0, sbias = 0 }) =>
|
|
160
|
+
step(MM(ep), [A, Bm, C, bias ?? maskWt, uni(M, N, K, a0, sa, b0, sb, c0, sc, ldc, bias0, sbias)], [N / 32, M / 32, batch])
|
|
161
|
+
|
|
162
|
+
// rows of n: y = x / max(|x|, 1e-12), or in place, scaled by g (a vector): x = x / max(|x|, 1e-12) · g
|
|
163
|
+
const NORM = scaled => pipe('norm' + scaled, HEAD + `
|
|
164
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
165
|
+
@group(0) @binding(1) var<storage, read_write> y: array<f32>;
|
|
166
|
+
@group(0) @binding(2) var<storage, read> g: array<f32>;
|
|
167
|
+
struct P { rows: u32, n: u32 }
|
|
168
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
169
|
+
@compute @workgroup_size(32)
|
|
170
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(subgroup_invocation_id) lane: u32) {
|
|
171
|
+
_ = g[0]; _ = y[0];
|
|
172
|
+
let r = wg.x + wg.y * 65535u;
|
|
173
|
+
if (r >= p.rows) { return; }
|
|
174
|
+
var s = 0.0;
|
|
175
|
+
for (var i = lane; i < p.n; i += 32u) { let v = x[r * p.n + i]; s += v * v; }
|
|
176
|
+
let k = 1.0 / max(sqrt(subgroupAdd(s)), 1e-12);
|
|
177
|
+
for (var i = lane; i < p.n; i += 32u) { ${scaled ? 'x[r * p.n + i] = x[r * p.n + i] * k * g[i];' : 'y[r * p.n + i] = x[r * p.n + i] * k;'} }
|
|
178
|
+
}`)
|
|
179
|
+
const grid1 = n => [Math.min(n, 65535), Math.ceil(n / 65535), 1]
|
|
180
|
+
const norm = (x, y, rows, n, g) => step(NORM(!!g), [x, g ? scratch : y, g ?? maskWt, uni(rows, n)], grid1(rows))
|
|
181
|
+
|
|
182
|
+
// a group's band inputs: band j's columns of E at frame t (none past T), normalized: A[j, t, e]
|
|
183
|
+
const SPLIT = pipe('split', HEAD + `
|
|
184
|
+
@group(0) @binding(0) var<storage, read> E: array<f32>;
|
|
185
|
+
@group(0) @binding(1) var<storage, read> idx: array<i32>;
|
|
186
|
+
@group(0) @binding(2) var<storage, read_write> A: array<f32>;
|
|
187
|
+
struct P { Wp: u32, Tp: u32, T: u32, cols: u32 }
|
|
188
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
189
|
+
@compute @workgroup_size(32)
|
|
190
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(subgroup_invocation_id) lane: u32) {
|
|
191
|
+
let t = wg.x; let j = wg.y;
|
|
192
|
+
var s = 0.0;
|
|
193
|
+
for (var e = lane; e < p.Wp; e += 32u) { let v = select(0.0, E[t * (p.cols + 1u) + u32(idx[j * p.Wp + e])], t < p.T); s += v * v; }
|
|
194
|
+
let k = 1.0 / max(sqrt(subgroupAdd(s)), 1e-12);
|
|
195
|
+
for (var e = lane; e < p.Wp; e += 32u) {
|
|
196
|
+
let v = select(0.0, E[t * (p.cols + 1u) + u32(idx[j * p.Wp + e])], t < p.T);
|
|
197
|
+
A[(j * p.Tp + t) * p.Wp + e] = v * k;
|
|
198
|
+
}
|
|
199
|
+
}`)
|
|
200
|
+
|
|
201
|
+
// q, k, v of each head to sequences, the rotation on q and k, the gates: token r = band·Tp + t; over time a sequence is
|
|
202
|
+
// a band, its positions frames (n T, padded to L); over frequency a frame, its positions bands (n B, padded to L)
|
|
203
|
+
const ROPE = pipe('rope', HEAD + `
|
|
204
|
+
@group(0) @binding(0) var<storage, read> Yq: array<f32>;
|
|
205
|
+
@group(0) @binding(1) var<storage, read_write> Q: array<f32>;
|
|
206
|
+
@group(0) @binding(2) var<storage, read_write> K: array<f32>;
|
|
207
|
+
@group(0) @binding(3) var<storage, read_write> V: array<f32>;
|
|
208
|
+
@group(0) @binding(4) var<storage, read_write> Gt: array<f32>;
|
|
209
|
+
@group(0) @binding(5) var<storage, read> cs: array<f32>;
|
|
210
|
+
@group(0) @binding(6) var<storage, read> sn: array<f32>;
|
|
211
|
+
@group(0) @binding(7) var<storage, read> gb: array<f32>;
|
|
212
|
+
struct P { time: u32, Tp: u32, T: u32, L: u32, ld: u32 }
|
|
213
|
+
@group(0) @binding(8) var<uniform> p: P;
|
|
214
|
+
@compute @workgroup_size(64)
|
|
215
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) li: vec3u) {
|
|
216
|
+
let t = wg.x; let band = wg.y; let h = wg.z; let d = li.x;
|
|
217
|
+
if (t >= p.T) { return; }
|
|
218
|
+
let r = band * p.Tp + t;
|
|
219
|
+
var seq = band; var pos = t;
|
|
220
|
+
if (p.time == 0u) { seq = t; pos = band; }
|
|
221
|
+
let o = ((seq * ${H}u + h) * p.L + pos) * 64u + d;
|
|
222
|
+
let y = r * p.ld + h * 64u;
|
|
223
|
+
let rot = select(Yq[y + d - 32u], -Yq[y + d + 32u], d < 32u);
|
|
224
|
+
let rk = select(Yq[y + ${H * DH}u + d - 32u], -Yq[y + ${H * DH}u + d + 32u], d < 32u);
|
|
225
|
+
Q[o] = Yq[y + d] * cs[pos * 64u + d] + rot * sn[pos * 64u + d];
|
|
226
|
+
K[o] = Yq[y + ${H * DH}u + d] * cs[pos * 64u + d] + rk * sn[pos * 64u + d];
|
|
227
|
+
V[o] = Yq[y + ${2 * H * DH}u + d];
|
|
228
|
+
if (d == 0u) { Gt[r * ${H}u + h] = 1.0 / (1.0 + exp(-(Yq[r * p.ld + ${QKV}u + h] + gb[h]))); }
|
|
229
|
+
}`)
|
|
230
|
+
// and back: Z[r, h·64 + d] = O · gate
|
|
231
|
+
const MERGE = pipe('merge', HEAD + `
|
|
232
|
+
@group(0) @binding(0) var<storage, read> O: array<f32>;
|
|
233
|
+
@group(0) @binding(1) var<storage, read> Gt: array<f32>;
|
|
234
|
+
@group(0) @binding(2) var<storage, read_write> Zb: array<f32>;
|
|
235
|
+
struct P { time: u32, Tp: u32, T: u32, L: u32 }
|
|
236
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
237
|
+
@compute @workgroup_size(64)
|
|
238
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(local_invocation_id) li: vec3u) {
|
|
239
|
+
let t = wg.x; let band = wg.y; let h = wg.z; let d = li.x;
|
|
240
|
+
let r = band * p.Tp + t;
|
|
241
|
+
if (t >= p.T) { Zb[r * ${H * DH}u + h * 64u + d] = 0.0; return; }
|
|
242
|
+
var seq = band; var pos = t;
|
|
243
|
+
if (p.time == 0u) { seq = t; pos = band; }
|
|
244
|
+
Zb[r * ${H * DH}u + h * 64u + d] = O[((seq * ${H}u + h) * p.L + pos) * 64u + d] * Gt[r * ${H}u + h];
|
|
245
|
+
}`)
|
|
246
|
+
// attention in two passes over the keys, 32 a block, 16 query rows a subgroup: the rows' maxima first (Q Kᵀ alone),
|
|
247
|
+
// then exp(S − max) and the output accumulated in registers, no rescaling; keys past n masked
|
|
248
|
+
const BR = 16, BC = 32, SG = 4, I2 = [0, 1], J4 = [0, 1, 2, 3], D8 = [...Array(8).keys()]
|
|
249
|
+
const scores = `${I2.map(i => J4.map(j => `var s${i}_${j}: subgroup_matrix_result<f32, 8, 8>;`).join(' ')).join('\n ')}
|
|
250
|
+
for (var k = 0u; k < 8u; k++) {
|
|
251
|
+
${I2.map(i => `let q${i} = subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>>(&Q, base + (r0 + ${i * 8}u) * 64u + k * 8u, false, 64u);`).join('\n ')}
|
|
252
|
+
${J4.map(j => `let k${j} = subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>>(&K, base + (c0 + ${j * 8}u) * 64u + k * 8u, true, 64u);`).join('\n ')}
|
|
253
|
+
${I2.map(i => J4.map(j => `s${i}_${j} = subgroupMatrixMultiplyAccumulate(q${i}, k${j}, s${i}_${j});`).join(' ')).join('\n ')}
|
|
254
|
+
}
|
|
255
|
+
${I2.map(i => J4.map(j => `subgroupMatrixStore(&S[sg], ${i * 8 * BC + j * 8}u, s${i}_${j}, false, ${BC}u);`).join(' ')).join('\n ')}
|
|
256
|
+
workgroupBarrier();`
|
|
257
|
+
const ATT = pipe('att', HEAD + `
|
|
258
|
+
@group(0) @binding(0) var<storage, read> Q: array<f32>;
|
|
259
|
+
@group(0) @binding(1) var<storage, read> K: array<f32>;
|
|
260
|
+
@group(0) @binding(2) var<storage, read> V: array<f32>;
|
|
261
|
+
@group(0) @binding(3) var<storage, read_write> O: array<f32>;
|
|
262
|
+
struct P { n: u32, np: u32 }
|
|
263
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
264
|
+
var<workgroup> S: array<array<f32, ${BR * BC}>, ${SG}>;
|
|
265
|
+
var<workgroup> Ow: array<array<f32, ${BR * 64}>, ${SG}>;
|
|
266
|
+
@compute @workgroup_size(${32 * SG})
|
|
267
|
+
fn main(@builtin(workgroup_id) wg: vec3u, @builtin(subgroup_id) sg: u32, @builtin(subgroup_invocation_id) lane: u32) {
|
|
268
|
+
let b = wg.y;
|
|
269
|
+
let r0 = (wg.x * ${SG}u + sg) * ${BR}u;
|
|
270
|
+
let base = b * p.np * 64u;
|
|
271
|
+
let row = lane / 2u; let half = (lane % 2u) * ${BC / 2}u; // two lanes a row, 16 columns each
|
|
272
|
+
var mx = -1e30;
|
|
273
|
+
for (var c0 = 0u; c0 < p.n; c0 += ${BC}u) {
|
|
274
|
+
${scores}
|
|
275
|
+
for (var j = 0u; j < ${BC / 2}u; j++) { if (c0 + half + j < p.n) { mx = max(mx, S[sg][row * ${BC}u + half + j]); } }
|
|
276
|
+
workgroupBarrier();
|
|
277
|
+
}
|
|
278
|
+
mx = max(mx, subgroupShuffleXor(mx, 1u));
|
|
279
|
+
var l = 0.0;
|
|
280
|
+
${I2.map(i => D8.map(t => `var o${i}_${t}: subgroup_matrix_result<f32, 8, 8>;`).join(' ')).join('\n ')}
|
|
281
|
+
for (var c0 = 0u; c0 < p.n; c0 += ${BC}u) {
|
|
282
|
+
${scores}
|
|
283
|
+
for (var j = 0u; j < ${BC / 2}u; j++) {
|
|
284
|
+
let at = row * ${BC}u + half + j;
|
|
285
|
+
let e = select(0.0, exp(S[sg][at] - mx), c0 + half + j < p.n);
|
|
286
|
+
S[sg][at] = e; l += e;
|
|
287
|
+
}
|
|
288
|
+
workgroupBarrier();
|
|
289
|
+
${I2.map(i => J4.map(j => `let p${i}_${j} = subgroupMatrixLoad<subgroup_matrix_left<f32, 8, 8>>(&S[sg], ${i * 8 * BC + j * 8}u, false, ${BC}u);`).join(' ')).join('\n ')}
|
|
290
|
+
${D8.map(t => J4.map(j => `let v${j}_${t} = subgroupMatrixLoad<subgroup_matrix_right<f32, 8, 8>>(&V, base + (c0 + ${j * 8}u) * 64u + ${t * 8}u, false, 64u);`).join(' ') + '\n ' + I2.map(i => J4.map(j => `o${i}_${t} = subgroupMatrixMultiplyAccumulate(p${i}_${j}, v${j}_${t}, o${i}_${t});`).join(' ')).join(' ')).join('\n ')}
|
|
291
|
+
workgroupBarrier();
|
|
292
|
+
}
|
|
293
|
+
l += subgroupShuffleXor(l, 1u);
|
|
294
|
+
${I2.map(i => D8.map(t => `subgroupMatrixStore(&Ow[sg], ${i * 8 * 64 + t * 8}u, o${i}_${t}, false, 64u);`).join(' ')).join('\n ')}
|
|
295
|
+
workgroupBarrier();
|
|
296
|
+
// lane pairs hold their row's sum; each lane writes half its row
|
|
297
|
+
for (var d = 0u; d < 32u; d++) {
|
|
298
|
+
let r = r0 + row;
|
|
299
|
+
if (r < p.n) { O[base + r * 64u + half * 2u + d] = Ow[sg][row * 64u + half * 2u + d] / l; }
|
|
300
|
+
}
|
|
301
|
+
}`)
|
|
302
|
+
// GLU: value half by the sigmoid of the gate half
|
|
303
|
+
const GLU = pipe('glu', HEAD + `
|
|
304
|
+
@group(0) @binding(0) var<storage, read> Yg: array<f32>;
|
|
305
|
+
@group(0) @binding(1) var<storage, read_write> Mo: array<f32>;
|
|
306
|
+
struct P { rows: u32, Wp: u32, at: u32 }
|
|
307
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
308
|
+
@compute @workgroup_size(64)
|
|
309
|
+
fn main(@builtin(global_invocation_id) gi: vec3u) {
|
|
310
|
+
let i = gi.x + gi.y * 65535u * 64u;
|
|
311
|
+
if (i >= p.rows * p.Wp) { return; }
|
|
312
|
+
let r = i / p.Wp; let e = i % p.Wp;
|
|
313
|
+
Mo[p.at + i] = Yg[r * 2u * p.Wp + e] / (1.0 + exp(-Yg[r * 2u * p.Wp + p.Wp + e]));
|
|
314
|
+
}`)
|
|
315
|
+
// the masks averaged over the bands that cover a column, times the spectrum: out planes [s·2 + c][f][t]
|
|
316
|
+
const MASK = pipe('mask', HEAD + `
|
|
317
|
+
@group(0) @binding(0) var<storage, read> Mo: array<f32>;
|
|
318
|
+
@group(0) @binding(1) var<storage, read> map: array<u32>;
|
|
319
|
+
@group(0) @binding(2) var<storage, read> wt: array<f32>;
|
|
320
|
+
@group(0) @binding(3) var<storage, read> E: array<f32>;
|
|
321
|
+
@group(0) @binding(4) var<storage, read_write> Out: array<f32>;
|
|
322
|
+
struct P { T: u32, F: u32, cols: u32 }
|
|
323
|
+
@group(0) @binding(5) var<uniform> p: P;
|
|
324
|
+
fn mask(t: u32, o: u32) -> f32 {
|
|
325
|
+
let a = map[o * 8u]; let sa = map[o * 8u + 1u]; let on1 = map[o * 8u + 2u];
|
|
326
|
+
let b = map[o * 8u + 3u]; let sb = map[o * 8u + 4u]; let on2 = map[o * 8u + 5u];
|
|
327
|
+
var v = 0.0;
|
|
328
|
+
if (on1 == 1u) { v += Mo[a + t * sa]; }
|
|
329
|
+
if (on2 == 1u) { v += Mo[b + t * sb]; }
|
|
330
|
+
return v * wt[o];
|
|
331
|
+
}
|
|
332
|
+
@compute @workgroup_size(64)
|
|
333
|
+
fn main(@builtin(global_invocation_id) gi: vec3u) {
|
|
334
|
+
let t = gi.x; let fs = gi.y; // fs = f·S + s
|
|
335
|
+
if (t >= p.T) { return; }
|
|
336
|
+
let f = fs / ${S}u; let s = fs % ${S}u;
|
|
337
|
+
let o = fs * 2u;
|
|
338
|
+
let er = E[t * (p.cols + 1u) + o]; let ei = E[t * (p.cols + 1u) + o + 1u];
|
|
339
|
+
let mr = mask(t, o); let mi = mask(t, o + 1u);
|
|
340
|
+
Out[((s * 2u) * p.F + f) * p.T + t] = er * mr - ei * mi;
|
|
341
|
+
Out[((s * 2u + 1u) * p.F + f) * p.T + t] = er * mi + ei * mr;
|
|
342
|
+
}`)
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------- the run, recorded once
|
|
345
|
+
for (const [g, gr] of G.entries()) {
|
|
346
|
+
step(SPLIT, [E, W.splitIdx[g], splitA[g], uni(gr.Wp, Tp, T, COLS)], [Tp, gr.n, 1])
|
|
347
|
+
mm('b', splitA[g], W.split[g], X, W.splitB[g], { M: Tp, N: D, K: gr.Wp, batch: gr.n, sa: Tp * gr.Wp, sb: gr.Wp * D, c0: gr.b0 * Tp * D, sc: Tp * D, sbias: D })
|
|
348
|
+
}
|
|
349
|
+
for (const [i, w] of W.blocks.entries()) {
|
|
350
|
+
const time = i % 2 === 0, L = time ? LT : LF, n = time ? T : B, seqs = time ? B : T
|
|
351
|
+
norm(X, XN, B * Tp, D)
|
|
352
|
+
mm('', XN, w.qkvg, Y, null, { M: B * Tp, N: QKVG, K: D })
|
|
353
|
+
step(ROPE, [Y, Qb, Kb, Vb, Gt, time ? cosT : cosF, time ? sinT : sinF, w.gb, uni(time ? 1 : 0, Tp, T, L, QKVG)], [Tp, B, H])
|
|
354
|
+
step(ATT, [Qb, Kb, Vb, Ob, uni(n, L)], [Math.ceil(L / (BR * SG)), seqs * H, 1])
|
|
355
|
+
step(MERGE, [Ob, Gt, Z, uni(time ? 1 : 0, Tp, T, L)], [Tp, B, H])
|
|
356
|
+
mm('r', Z, w.wo, X, null, { M: B * Tp, N: D, K: H * DH })
|
|
357
|
+
norm(X, XN, B * Tp, D)
|
|
358
|
+
mm('bg', XN, w.w1, FFH, w.b1, { M: B * Tp, N: FF, K: D })
|
|
359
|
+
mm('br', FFH, w.w2, X, w.b2, { M: B * Tp, N: D, K: FF })
|
|
360
|
+
norm(X, X, B * Tp, D, w.out)
|
|
361
|
+
}
|
|
362
|
+
mm('bt', X, W.mw[0], FFH, W.mb[0], { M: Tp, N: MH, K: D, batch: B, sa: Tp * D, sb: D * MH, sc: Tp * MH, sbias: MH })
|
|
363
|
+
mm('bt', FFH, W.mw[1], H2, W.mb[1], { M: Tp, N: MH, K: MH, batch: B, sa: Tp * MH, sb: MH * MH, sc: Tp * MH, sbias: MH })
|
|
364
|
+
for (const [g, gr] of G.entries()) {
|
|
365
|
+
mm('b', H2, W.m2[g], glu[g], W.m2B[g], { M: Tp, N: 2 * gr.Wp, K: MH, batch: gr.n, a0: gr.b0 * Tp * MH, sa: Tp * MH, sb: MH * 2 * gr.Wp, sc: Tp * 2 * gr.Wp, sbias: 2 * gr.Wp })
|
|
366
|
+
const n = gr.n * Tp * gr.Wp
|
|
367
|
+
step(GLU, [glu[g], Mg, uni(gr.n * Tp, gr.Wp, gofs[g])], [Math.min(Math.ceil(n / 64), 65535), Math.ceil(n / 64 / 65535), 1])
|
|
368
|
+
}
|
|
369
|
+
step(MASK, [Mg, maskMap, maskWt, E, OUT, uni(T, F, COLS)], [Math.ceil(T / 64), F * S, 1])
|
|
370
|
+
const zero = device.createCommandEncoder()
|
|
371
|
+
for (const b of [Qb, Kb, Vb]) zero.clearBuffer(b)
|
|
372
|
+
device.queue.submit([zero.finish()])
|
|
373
|
+
|
|
374
|
+
const stage = new Float32Array(Tp * (COLS + 1)), read = device.createBuffer({ size: 4 * F * T * 4, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ })
|
|
375
|
+
return {
|
|
376
|
+
steps: steps.length,
|
|
377
|
+
async run(spec) {
|
|
378
|
+
// planes [s·2 + c][f][t] → E[t][(f·S + s)·2 + c], a zero column past them, rows past T zero
|
|
379
|
+
stage.fill(0)
|
|
380
|
+
for (let s = 0; s < S; s++) for (let c = 0; c < 2; c++) for (let f = 0; f < F; f++) {
|
|
381
|
+
const plane = ((s * 2 + c) * F + f) * T, col = (f * S + s) * 2 + c
|
|
382
|
+
for (let t = 0; t < T; t++) stage[t * (COLS + 1) + col] = spec[plane + t]
|
|
383
|
+
}
|
|
384
|
+
device.queue.writeBuffer(E, 0, stage)
|
|
385
|
+
const e = device.createCommandEncoder(), pass = e.beginComputePass()
|
|
386
|
+
for (const [p, bg, grid] of steps) { pass.setPipeline(p); pass.setBindGroup(0, bg); pass.dispatchWorkgroups(...grid) }
|
|
387
|
+
pass.end()
|
|
388
|
+
e.copyBufferToBuffer(OUT, 0, read, 0, 4 * F * T * 4)
|
|
389
|
+
device.queue.submit([e.finish()])
|
|
390
|
+
await read.mapAsync(GPUMapMode.READ)
|
|
391
|
+
const out = new Float32Array(read.getMappedRange().slice(0))
|
|
392
|
+
read.unmap()
|
|
393
|
+
return out
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -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)
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Export Mel-Band RoFormer (Wang, Lu, Won, Chen, Song, "Mel-Band RoFormer for Music Source Separation", ISMIR LBD
|
|
3
|
+
2023) to the ONNX contract @audio/neural-separate expects for modelType 'complex': the network between its STFT and
|
|
4
|
+
iSTFT, which JS computes, written for the GPU.
|
|
5
|
+
|
|
6
|
+
pip install torch onnx onnxruntime pyyaml einops beartype rotary-embedding-torch librosa
|
|
7
|
+
python3 export-roformer.py --model mel-roformer --verify
|
|
8
|
+
|
|
9
|
+
The model is Kimberley Jensen's vocals Mel-Band RoFormer (huggingface.co/KimberleyJSN/melbandroformer, MIT), with the
|
|
10
|
+
config ZFTurbo/Music-Source-Separation-Training (MIT) runs it by (configs/KimberleyJensen/
|
|
11
|
+
config_vocals_mel_band_roformer_kj.yaml) and that repository's models/bs_roformer code, both at a pinned commit.
|
|
12
|
+
|
|
13
|
+
One 8 s segment (352800 samples at 44.1 kHz, 801 frames) per run:
|
|
14
|
+
input mix_spec [1, 4, 1025, 801] STFT, complex as channels (L re, L im, R re, R im)
|
|
15
|
+
output stems_spec [1, 4, 1025, 801] the vocals, per channel, re and im
|
|
16
|
+
The STFT is the model's own: n_fft 2048, hop 441, Hann window of 2048, unnormalized, centered with reflect padding.
|
|
17
|
+
|
|
18
|
+
The graph computes what MelBandRoformer.forward computes between its STFT and iSTFT, rearranged so a GPU runs it in few
|
|
19
|
+
and large operations; none of it changes the function (--verify: against the PyTorch forward):
|
|
20
|
+
- the 60 bands' input projections and mask MLPs run as batched matrix products, bands of near widths grouped and
|
|
21
|
+
zero-padded to the widest of their group (10 groups, none wider than 1.3 times its narrowest), not 60
|
|
22
|
+
small products each;
|
|
23
|
+
- each RMSNorm's scale (√d·γ) is folded into the weights of the linear layer it feeds; the attention's query scale
|
|
24
|
+
into the query weights; its gates' projection joins the q, k, v projection;
|
|
25
|
+
- the rotary embedding's interleaved pairs become halves (the same permutation of q's and k's head features leaves
|
|
26
|
+
q·k as it was), so the rotation is two products with tables of cos and sin and one with a fixed 64×64 matrix;
|
|
27
|
+
- the masks of overlapping mel bands are averaged by two gathers and a weight, not a scatter, the DC bin's weight 0
|
|
28
|
+
(zero_dc);
|
|
29
|
+
- no tensor passes 100 MB: each transformer runs on slices of its sequences, the mask MLP by group of bands
|
|
30
|
+
(onnxruntime's WebGPU Softmax went wrong past WebGPU's default 128 MiB storage binding).
|
|
31
|
+
|
|
32
|
+
Writes <out-dir>/mel-roformer.onnx (928 MB), and with --fp16 mel-roformer.fp16.onnx (float16 weights, float32 compute,
|
|
33
|
+
half the download). Products computed in float16 measured slower on WebGPU (3.2 against 2.6 s a segment): not offered.
|
|
34
|
+
Default out-dir: $AUDIO_NEURAL_CACHE/<model>, else ~/.cache/audiojs/neural/<model>. --verify compares the stems rebuilt
|
|
35
|
+
from onnxruntime's output with MelBandRoformer.forward on the same segment and writes the reference separation test.js
|
|
36
|
+
compares the JS pipeline against (scripts/reference.py, chunked as separate.js chunks: step 0.5, Kim's num_overlap 2).
|
|
37
|
+
Last run: torch 2.14, onnxruntime 1.31.
|
|
38
|
+
"""
|
|
39
|
+
import argparse
|
|
40
|
+
import gc
|
|
41
|
+
import math
|
|
42
|
+
import os
|
|
43
|
+
import sys
|
|
44
|
+
import urllib.request
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
|
|
47
|
+
REPO = "https://raw.githubusercontent.com/ZFTurbo/Music-Source-Separation-Training/84b1eac0887756b4f1a9d7a1ff49105939749ed2/"
|
|
48
|
+
MODELS = { # checkpoint URL, its SHA-256, config path in REPO
|
|
49
|
+
"mel-roformer": ("https://huggingface.co/KimberleyJSN/melbandroformer/resolve/ac9b0614ab3cd7f77219e18ba494dfd93956c348/MelBandRoformer.ckpt",
|
|
50
|
+
"87201f4d31afb5bc79993230fc49446918425574db48c01c405e44f365c7559e",
|
|
51
|
+
"configs/KimberleyJensen/config_vocals_mel_band_roformer_kj.yaml"),
|
|
52
|
+
}
|
|
53
|
+
SEGMENT = 352800
|
|
54
|
+
STEP = 0.5
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def fetch(url, path):
|
|
58
|
+
if not path.exists():
|
|
59
|
+
print(f"fetching {url}")
|
|
60
|
+
urllib.request.urlretrieve(url, str(path) + ".part")
|
|
61
|
+
os.replace(str(path) + ".part", path)
|
|
62
|
+
return path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load(src, name="mel-roformer"):
|
|
66
|
+
"""MelBandRoformer with its weights: the model code, config and checkpoint fetched into `src` once."""
|
|
67
|
+
import hashlib
|
|
68
|
+
import torch
|
|
69
|
+
import yaml
|
|
70
|
+
|
|
71
|
+
pkg = src / "roformer_code" / "models" / "bs_roformer"
|
|
72
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
(pkg.parent / "__init__.py").touch()
|
|
74
|
+
(pkg / "__init__.py").touch()
|
|
75
|
+
for f in ("mel_band_roformer.py", "attend.py"):
|
|
76
|
+
fetch(REPO + "models/bs_roformer/" + f, pkg / f)
|
|
77
|
+
sys.path.insert(0, str(src / "roformer_code"))
|
|
78
|
+
sys.dont_write_bytecode = True
|
|
79
|
+
from models.bs_roformer.mel_band_roformer import MelBandRoformer
|
|
80
|
+
|
|
81
|
+
class Loader(yaml.SafeLoader): pass # the config's resolutions are a !!python/tuple
|
|
82
|
+
Loader.add_constructor("tag:yaml.org,2002:python/tuple", lambda l, n: tuple(l.construct_sequence(n)))
|
|
83
|
+
url, sha, config = MODELS[name]
|
|
84
|
+
cfg = yaml.load(fetch(REPO + config, src / Path(config).name).read_text(), Loader=Loader)
|
|
85
|
+
ckpt = fetch(url, src / Path(url).name)
|
|
86
|
+
h = hashlib.sha256()
|
|
87
|
+
with open(ckpt, "rb") as f:
|
|
88
|
+
for b in iter(lambda: f.read(1 << 24), b""):
|
|
89
|
+
h.update(b)
|
|
90
|
+
if h.hexdigest() != sha:
|
|
91
|
+
raise SystemExit(f"{ckpt}: SHA-256 {h.hexdigest()}, expected {sha}")
|
|
92
|
+
model = MelBandRoformer(**cfg["model"])
|
|
93
|
+
state = torch.load(ckpt, map_location="cpu", weights_only=True)
|
|
94
|
+
model.load_state_dict(state.get("state_dict", state) if isinstance(state, dict) else state)
|
|
95
|
+
return model.eval(), cfg
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def stft(model, x):
|
|
99
|
+
"""The model's STFT: (B, C, L) -> (B, 2C, F, T), complex as channels."""
|
|
100
|
+
import torch
|
|
101
|
+
|
|
102
|
+
B, C, L = x.shape
|
|
103
|
+
z = torch.view_as_real(torch.stft(x.reshape(-1, L), **model.stft_kwargs, window=model.stft_window_fn(), return_complex=True))
|
|
104
|
+
return z.permute(0, 3, 1, 2).reshape(B, 2 * C, z.shape[1], z.shape[2])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def istft(model, y, length):
|
|
108
|
+
"""(B, 2·S·C, F, T) -> (B, S, C, L)"""
|
|
109
|
+
import torch
|
|
110
|
+
|
|
111
|
+
B, _, Fr, T = y.shape
|
|
112
|
+
y = torch.view_as_complex(y.reshape(-1, 2, Fr, T).permute(0, 2, 3, 1).contiguous())
|
|
113
|
+
x = torch.istft(y, **model.stft_kwargs, window=model.stft_window_fn(), length=length)
|
|
114
|
+
return x.reshape(B, -1, model.audio_channels, length)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def groups(widths, slack=1.3):
|
|
118
|
+
"""Contiguous runs of bands whose widest is at most `slack` times their narrowest: [(first, last + 1, widest)]"""
|
|
119
|
+
out, b0 = [], 0
|
|
120
|
+
for b in range(1, len(widths) + 1):
|
|
121
|
+
if b == len(widths) or max(widths[b0:b + 1]) > slack * min(widths[b0:b + 1]):
|
|
122
|
+
out.append((b0, b, max(widths[b0:b])))
|
|
123
|
+
b0 = b
|
|
124
|
+
return out
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Core:
|
|
128
|
+
"""MelBandRoformer.forward between its STFT and its iSTFT, for the GPU (the module docstring says how)."""
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def build(model, T):
|
|
132
|
+
import torch
|
|
133
|
+
import torch.nn as nn
|
|
134
|
+
import torch.nn.functional as F
|
|
135
|
+
|
|
136
|
+
f64 = torch.float64
|
|
137
|
+
Fq = model.num_bands_per_freq.numel() # 1025
|
|
138
|
+
S = model.audio_channels
|
|
139
|
+
fi = model.freq_indices.tolist() # (f·S + s) per band's pair, bands in order
|
|
140
|
+
nf = model.num_freqs_per_band.tolist()
|
|
141
|
+
widths = [2 * n * S for n in nf] # a band's input: its (f, s) pairs, re and im
|
|
142
|
+
G = groups(widths)
|
|
143
|
+
lin = torch.matmul
|
|
144
|
+
# every tensor under WebGPU's default storage binding (128 MiB, maxStorageBufferBindingSize): onnxruntime
|
|
145
|
+
# 1.30's WebGPU Softmax over a larger one came out wrong one run in two (36 dB from the CPU's at 307 MB, right
|
|
146
|
+
# at 123 MB), so a transformer runs on as many sequences at a time as keep its largest, the attention's
|
|
147
|
+
# scores (heads × n × n) or its feed-forward's hidden layer (n × 4·dim), under 100 MB
|
|
148
|
+
BUDGET = 100e6
|
|
149
|
+
|
|
150
|
+
def sliced(f, x):
|
|
151
|
+
per = max(f.biggest(x.shape[1]), 1)
|
|
152
|
+
size = max(1, int(BUDGET // per))
|
|
153
|
+
return torch.cat([f(x[i:i + size]) for i in range(0, x.shape[0], size)]) if size < x.shape[0] else f(x)
|
|
154
|
+
|
|
155
|
+
rot = lambda n, emb: emb(emb.get_seq_pos(n, device="cpu", dtype=torch.float32)) # (n, dh) interleaved
|
|
156
|
+
|
|
157
|
+
def rms(m):
|
|
158
|
+
return (m.gamma.detach().double() * m.scale)
|
|
159
|
+
|
|
160
|
+
class Mod(nn.Module):
|
|
161
|
+
def __init__(s):
|
|
162
|
+
super().__init__()
|
|
163
|
+
# the STFT as columns: E[t, (f·S + s)·2 + c], and a zero column past them (index Fq·S·2)
|
|
164
|
+
pos = [] # each band's input columns, in its order
|
|
165
|
+
o = 0
|
|
166
|
+
for n in nf:
|
|
167
|
+
pos.append([fi[k] * 2 + c for k in range(o, o + n * S) for c in range(2)])
|
|
168
|
+
o += n * S
|
|
169
|
+
zero = Fq * S * 2
|
|
170
|
+
bs = model.band_split.to_features
|
|
171
|
+
s.split_idx, s.split_w, s.split_b = [], nn.ParameterList(), nn.ParameterList()
|
|
172
|
+
for b0, b1, W in G:
|
|
173
|
+
idx = torch.full((b1 - b0, W), zero, dtype=torch.long)
|
|
174
|
+
w = torch.zeros(b1 - b0, W, bs[0][1].out_features, dtype=f64)
|
|
175
|
+
bias = torch.zeros(b1 - b0, 1, w.shape[2], dtype=f64)
|
|
176
|
+
for j, b in enumerate(range(b0, b1)):
|
|
177
|
+
norm, lin = bs[b][0], bs[b][1]
|
|
178
|
+
idx[j, :widths[b]] = torch.tensor(pos[b])
|
|
179
|
+
w[j, :widths[b]] = (lin.weight.detach().double() * rms(norm)[None]).T
|
|
180
|
+
bias[j, 0] = lin.bias.detach().double()
|
|
181
|
+
s.register_buffer(f"si{len(s.split_idx)}", idx)
|
|
182
|
+
s.split_idx.append(f"si{len(s.split_idx)}")
|
|
183
|
+
s.split_w.append(nn.Parameter(w.float(), requires_grad=False))
|
|
184
|
+
s.split_b.append(nn.Parameter(bias.float(), requires_grad=False))
|
|
185
|
+
# transformers: time then frequency, per depth
|
|
186
|
+
s.blocks = nn.ModuleList()
|
|
187
|
+
for time, freq in model.layers:
|
|
188
|
+
s.blocks.append(nn.ModuleList([Block(time, T), Block(freq, len(nf))]))
|
|
189
|
+
# mask estimator (one stem): its MLP's linear layers before the last batched over bands (their shapes
|
|
190
|
+
# alike), the last by group, then GLU
|
|
191
|
+
me = model.mask_estimators[0].to_freqs
|
|
192
|
+
lin = [[m for m in me[b][0] if isinstance(m, nn.Linear)] for b in range(len(nf))]
|
|
193
|
+
s.mw = nn.ParameterList([nn.Parameter(torch.stack([l[i].weight.detach().T for l in lin]), requires_grad=False) for i in range(len(lin[0]) - 1)])
|
|
194
|
+
s.mb = nn.ParameterList([nn.Parameter(torch.stack([l[i].bias.detach() for l in lin])[:, None], requires_grad=False) for i in range(len(lin[0]) - 1)])
|
|
195
|
+
s.m2_w, s.m2_b = nn.ParameterList(), nn.ParameterList()
|
|
196
|
+
col, cols = 0, {}
|
|
197
|
+
for b0, b1, W in G:
|
|
198
|
+
H = lin[0][-1].in_features
|
|
199
|
+
w = torch.zeros(b1 - b0, H, 2 * W)
|
|
200
|
+
bias = torch.zeros(b1 - b0, 1, 2 * W)
|
|
201
|
+
for j, b in enumerate(range(b0, b1)):
|
|
202
|
+
l2 = lin[b][-1]
|
|
203
|
+
n = widths[b]
|
|
204
|
+
w[j, :, :n], w[j, :, W:W + n] = l2.weight.detach()[:n].T, l2.weight.detach()[n:].T
|
|
205
|
+
bias[j, 0, :n], bias[j, 0, W:W + n] = l2.bias.detach()[:n], l2.bias.detach()[n:]
|
|
206
|
+
for e in range(n):
|
|
207
|
+
cols.setdefault(pos[b][e], []).append(col + j * W + e)
|
|
208
|
+
s.m2_w.append(nn.Parameter(w, requires_grad=False))
|
|
209
|
+
s.m2_b.append(nn.Parameter(bias, requires_grad=False))
|
|
210
|
+
col += (b1 - b0) * W
|
|
211
|
+
# each output column's sources among the masks (at most two bands overlap), the zero column for none,
|
|
212
|
+
# and its weight: 1 over the bands that cover it, 0 at DC
|
|
213
|
+
i1 = torch.full((zero,), col, dtype=torch.long)
|
|
214
|
+
i2 = torch.full((zero,), col, dtype=torch.long)
|
|
215
|
+
wt = torch.zeros(zero)
|
|
216
|
+
for o, src in cols.items():
|
|
217
|
+
assert len(src) <= 2, "three bands over one frequency"
|
|
218
|
+
i1[o] = src[0]
|
|
219
|
+
i2[o] = src[-1] if len(src) == 2 else col
|
|
220
|
+
wt[o] = 0 if o // (S * 2) == 0 and model.zero_dc else 1 / len(src)
|
|
221
|
+
s.register_buffer("i1", i1)
|
|
222
|
+
s.register_buffer("i2", i2)
|
|
223
|
+
s.register_buffer("wt", wt)
|
|
224
|
+
s.Fq, s.S = Fq, S
|
|
225
|
+
|
|
226
|
+
def forward(s, mix_spec):
|
|
227
|
+
Fq, S = s.Fq, s.S
|
|
228
|
+
T_ = mix_spec.shape[3]
|
|
229
|
+
# (1, 2S, F, T) -> E (T, F·S·2): [t, (f·S + s)·2 + c]
|
|
230
|
+
E = mix_spec.reshape(S, 2, Fq, T_).permute(3, 2, 0, 1).reshape(T_, Fq * S * 2)
|
|
231
|
+
Ez = torch.cat([E, torch.zeros(T_, 1, dtype=E.dtype)], 1)
|
|
232
|
+
xs = []
|
|
233
|
+
for k, name in enumerate(s.split_idx):
|
|
234
|
+
idx = getattr(s, name) # (nb, W)
|
|
235
|
+
x = Ez[:, idx].permute(1, 0, 2) # (nb, T, W)
|
|
236
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
237
|
+
xs.append(lin(x, s.split_w[k]) + s.split_b[k])
|
|
238
|
+
x = torch.cat(xs, 0) # (B, T, D)
|
|
239
|
+
for time, freq in s.blocks:
|
|
240
|
+
x = sliced(time, x) # sequences over time, one per band
|
|
241
|
+
x = sliced(freq, x.transpose(0, 1)).transpose(0, 1) # over bands, one per frame
|
|
242
|
+
ms = []
|
|
243
|
+
for k, (lo, hi, W) in enumerate(G):
|
|
244
|
+
h = x[lo:hi]
|
|
245
|
+
for w, b in zip(s.mw, s.mb):
|
|
246
|
+
h = torch.tanh(lin(h, w[lo:hi]) + b[lo:hi]) # (nb, T, H)
|
|
247
|
+
y = lin(h, s.m2_w[k]) + s.m2_b[k] # (nb, T, 2W)
|
|
248
|
+
m = y[..., :W] * torch.sigmoid(y[..., W:]) # GLU
|
|
249
|
+
ms.append(m.permute(1, 0, 2).reshape(T_, (hi - lo) * W))
|
|
250
|
+
M = torch.cat(ms + [torch.zeros(T_, 1)], 1) # (T, columns + 1)
|
|
251
|
+
mask = (M[:, s.i1] + M[:, s.i2]) * s.wt # (T, F·S·2)
|
|
252
|
+
e = E.reshape(T_, Fq, S, 2)
|
|
253
|
+
mk = mask.reshape(T_, Fq, S, 2)
|
|
254
|
+
er, ei, mr, mi = e[..., 0], e[..., 1], mk[..., 0], mk[..., 1]
|
|
255
|
+
out = torch.stack([er * mr - ei * mi, er * mi + ei * mr], -1) # (T, F, S, 2)
|
|
256
|
+
return out.permute(2, 3, 1, 0).reshape(1, 2 * S, Fq, T_)
|
|
257
|
+
|
|
258
|
+
class Block(nn.Module):
|
|
259
|
+
"""One Transformer of the model (its layers and output norm) over sequences of n: x (batch, n, D)."""
|
|
260
|
+
|
|
261
|
+
def __init__(s, tr, n):
|
|
262
|
+
super().__init__()
|
|
263
|
+
s.layers = nn.ModuleList()
|
|
264
|
+
for attn, ff in tr.layers:
|
|
265
|
+
s.layers.append(nn.ModuleList([Attn(attn, n), FF(ff)]))
|
|
266
|
+
s.out = nn.Parameter(rms(tr.norm).float(), requires_grad=False)
|
|
267
|
+
|
|
268
|
+
def biggest(s, n):
|
|
269
|
+
"""bytes of the largest tensor one sequence of n makes"""
|
|
270
|
+
a, ff = s.layers[0]
|
|
271
|
+
return 4 * max(a.h * n * n, n * ff.w1.shape[1], n * a.qkvg.shape[1])
|
|
272
|
+
|
|
273
|
+
def forward(s, x):
|
|
274
|
+
for attn, ff in s.layers:
|
|
275
|
+
x = attn(x) + x
|
|
276
|
+
x = ff(x) + x
|
|
277
|
+
return x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12) * s.out
|
|
278
|
+
|
|
279
|
+
class Attn(nn.Module):
|
|
280
|
+
def __init__(s, a, n):
|
|
281
|
+
super().__init__()
|
|
282
|
+
h, dh = a.heads, a.to_qkv.out_features // (3 * a.heads)
|
|
283
|
+
g = rms(a.norm)[None]
|
|
284
|
+
wq, wk, wv = (a.to_qkv.weight.detach().double() * g).reshape(3, h, dh, -1)
|
|
285
|
+
# interleaved pairs -> halves, the same permutation of q's and k's features
|
|
286
|
+
perm = torch.cat([torch.arange(0, dh, 2), torch.arange(1, dh, 2)])
|
|
287
|
+
wq, wk = wq[:, perm] * a.scale, wk[:, perm]
|
|
288
|
+
wg = a.to_gates.weight.detach().double() * g
|
|
289
|
+
s.qkvg = nn.Parameter(torch.cat([wq.reshape(h * dh, -1), wk.reshape(h * dh, -1), wv.reshape(h * dh, -1), wg]).T.float(), requires_grad=False)
|
|
290
|
+
s.gb = nn.Parameter(a.to_gates.bias.detach().float(), requires_grad=False)
|
|
291
|
+
s.wo = nn.Parameter(a.to_out[0].weight.detach().T.float(), requires_grad=False)
|
|
292
|
+
freqs = rot(n, a.rotary_embed).double()[:, perm] # (n, dh)
|
|
293
|
+
s.register_buffer("cos", freqs.cos().float())
|
|
294
|
+
s.register_buffer("sin", freqs.sin().float())
|
|
295
|
+
R = torch.zeros(dh, dh) # x @ R: (-x[half:], x[:half])
|
|
296
|
+
R[torch.arange(dh // 2) + dh // 2, torch.arange(dh // 2)] = -1
|
|
297
|
+
R[torch.arange(dh // 2), torch.arange(dh // 2) + dh // 2] = 1
|
|
298
|
+
s.register_buffer("R", R)
|
|
299
|
+
s.h, s.dh = h, dh
|
|
300
|
+
|
|
301
|
+
def forward(s, x):
|
|
302
|
+
Bt, n, _ = x.shape
|
|
303
|
+
h, dh = s.h, s.dh
|
|
304
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
305
|
+
y = lin(x, s.qkvg)
|
|
306
|
+
q, k, v = (y[..., i * h * dh:(i + 1) * h * dh].reshape(Bt, n, h, dh).transpose(1, 2) for i in range(3))
|
|
307
|
+
gates = torch.sigmoid(y[..., 3 * h * dh:] + s.gb) # (Bt, n, h)
|
|
308
|
+
q = q * s.cos + torch.matmul(q, s.R) * s.sin
|
|
309
|
+
k = k * s.cos + torch.matmul(k, s.R) * s.sin
|
|
310
|
+
o = torch.matmul(torch.softmax(torch.matmul(q, k.transpose(-1, -2)), -1), v) # (Bt, h, n, dh)
|
|
311
|
+
o = o * gates.transpose(1, 2)[..., None]
|
|
312
|
+
return lin(o.transpose(1, 2).reshape(Bt, n, h * dh), s.wo)
|
|
313
|
+
|
|
314
|
+
class FF(nn.Module):
|
|
315
|
+
def __init__(s, ff):
|
|
316
|
+
super().__init__()
|
|
317
|
+
norm, l1, _, _, l2, _ = ff.net
|
|
318
|
+
s.w1 = nn.Parameter((l1.weight.detach().double() * rms(norm)[None]).T.float(), requires_grad=False)
|
|
319
|
+
s.b1 = nn.Parameter(l1.bias.detach().float(), requires_grad=False)
|
|
320
|
+
s.w2 = nn.Parameter(l2.weight.detach().T.float(), requires_grad=False)
|
|
321
|
+
s.b2 = nn.Parameter(l2.bias.detach().float(), requires_grad=False)
|
|
322
|
+
|
|
323
|
+
def forward(s, x):
|
|
324
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
325
|
+
return lin(F.gelu(lin(x, s.w1) + s.b1), s.w2) + s.b2
|
|
326
|
+
|
|
327
|
+
return Mod().eval()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def frames(model):
|
|
331
|
+
import torch
|
|
332
|
+
|
|
333
|
+
return stft(model, torch.zeros(1, model.audio_channels, SEGMENT)).shape[3]
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def export(model, path, opset):
|
|
337
|
+
import torch
|
|
338
|
+
|
|
339
|
+
T = frames(model)
|
|
340
|
+
core = Core.build(model, T)
|
|
341
|
+
spec = stft(model, torch.randn(1, model.audio_channels, SEGMENT))
|
|
342
|
+
with torch.no_grad():
|
|
343
|
+
torch.onnx.export(core, (spec,), str(path), input_names=["mix_spec"], output_names=["stems_spec"],
|
|
344
|
+
opset_version=opset, dynamo=False)
|
|
345
|
+
print(f"wrote {path} ({path.stat().st_size / 1e6:.1f} MB), segment {SEGMENT} samples, spec {list(spec.shape)}, groups {groups([2 * n * model.audio_channels for n in model.num_freqs_per_band.tolist()])}")
|
|
346
|
+
return core
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def engine(model, out_dir, name):
|
|
350
|
+
"""The GPU form's tensors for the package's own WebGPU engine (roformer.js): <name>.engine.bin, each tensor in turn
|
|
351
|
+
(weights float16, tables float32, indices int32), and <name>.engine.json naming them, their type, shape and offset,
|
|
352
|
+
with the band groups and the sizes the engine reads."""
|
|
353
|
+
import json
|
|
354
|
+
import numpy as np
|
|
355
|
+
import torch
|
|
356
|
+
|
|
357
|
+
T = frames(model)
|
|
358
|
+
core = Core.build(model, T)
|
|
359
|
+
tensors, blob, off = {}, bytearray(), 0
|
|
360
|
+
for n, t in list(core.named_parameters()) + list(core.named_buffers()):
|
|
361
|
+
a = t.detach().numpy()
|
|
362
|
+
kind = "int32" if a.dtype.kind in "iu" else "float16" if n.rsplit(".", 1)[-1] in ("qkvg", "wo", "w1", "w2") or n.startswith(("split_w.", "mw.", "m2_w.")) else "float32"
|
|
363
|
+
b = a.astype(kind).tobytes()
|
|
364
|
+
pad = -len(blob) % 16
|
|
365
|
+
blob += bytes(pad)
|
|
366
|
+
tensors[n] = {"type": kind, "shape": list(a.shape), "offset": len(blob)}
|
|
367
|
+
blob += b
|
|
368
|
+
nf = model.num_freqs_per_band.tolist()
|
|
369
|
+
meta = {
|
|
370
|
+
"model": name, "frames": T, "freqs": core.Fq, "channels": core.S, "dim": model.layers[0][0].layers[0][0].to_qkv.in_features,
|
|
371
|
+
"heads": model.layers[0][0].layers[0][0].heads, "depth": len(model.layers), "bands": len(nf),
|
|
372
|
+
"groups": [list(g) for g in groups([2 * n * core.S for n in nf])], "widths": [2 * n * core.S for n in nf],
|
|
373
|
+
"tensors": tensors,
|
|
374
|
+
}
|
|
375
|
+
(out_dir / f"{name}.engine.bin").write_bytes(bytes(blob))
|
|
376
|
+
(out_dir / f"{name}.engine.json").write_text(json.dumps(meta))
|
|
377
|
+
print(f"wrote {out_dir / (name + '.engine.bin')} ({len(blob) / 1e6:.1f} MB) and its .json, {len(tensors)} tensors")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def fp16(path, out):
|
|
381
|
+
"""float16 weights, float32 compute (scripts/compact.py --as fp16)."""
|
|
382
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
383
|
+
import compact
|
|
384
|
+
|
|
385
|
+
compact.compact(path, out, "fp16")
|
|
386
|
+
print(f"wrote {out} ({out.stat().st_size / 1e6:.1f} MB)")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def session(path):
|
|
390
|
+
import onnxruntime as ort
|
|
391
|
+
|
|
392
|
+
o = ort.SessionOptions()
|
|
393
|
+
o.enable_cpu_mem_arena = False
|
|
394
|
+
o.enable_mem_pattern = False
|
|
395
|
+
return ort.InferenceSession(str(path), o, providers=["CPUExecutionProvider"])
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def mixes():
|
|
399
|
+
import torch
|
|
400
|
+
|
|
401
|
+
t = torch.arange(SEGMENT) / 44100
|
|
402
|
+
tone = sum(torch.sin(2 * math.pi * f * t) / k for k, f in enumerate((110, 220, 440, 880, 1760), 1))
|
|
403
|
+
return (("noise", 0.1 * torch.randn(1, 2, SEGMENT)), ("tones", 0.2 * torch.stack([tone, 0.8 * tone])[None]))
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def verify(model, run, label, tol):
|
|
407
|
+
"""The vocals rebuilt from run(spec) against MelBandRoformer.forward: noise and tones."""
|
|
408
|
+
import numpy as np
|
|
409
|
+
import torch
|
|
410
|
+
|
|
411
|
+
for name, mix in mixes():
|
|
412
|
+
with torch.no_grad():
|
|
413
|
+
want = model(mix).numpy()
|
|
414
|
+
got = istft(model, torch.as_tensor(run(stft(model, mix))), SEGMENT)[:, 0].numpy()
|
|
415
|
+
want = want[..., :got.shape[-1]]
|
|
416
|
+
rel = np.abs(got - want).max() / max(np.abs(want).max(), 1e-12)
|
|
417
|
+
print(f" verify {label} {name}: max|diff|/max|y| = {rel:.2e} " + ("OK" if rel < tol else "FAIL"))
|
|
418
|
+
if rel >= tol:
|
|
419
|
+
raise SystemExit(f"{label}: verification failed ({rel:.2e} >= {tol})")
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def reference(model, out_dir):
|
|
423
|
+
"""The reference separation: MelBandRoformer.forward on segments as scripts/reference.py's chunked() cuts them
|
|
424
|
+
(step 0.5, no standardization: Kim's config normalizes nothing), over a 20 s mix with a noise floor."""
|
|
425
|
+
import torch
|
|
426
|
+
|
|
427
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
428
|
+
import reference as ref
|
|
429
|
+
|
|
430
|
+
def run(seg):
|
|
431
|
+
with torch.no_grad():
|
|
432
|
+
return model(torch.from_numpy(seg)[None])[0][None].numpy() # (1 stem, C, L)
|
|
433
|
+
|
|
434
|
+
# under the mix a noise floor 60 dB down: each band normalizes its input, and a silent band's is its rounding
|
|
435
|
+
ref.write(out_dir, lambda mix: {"vocals": ref.chunked(mix, run, SEGMENT, STEP)[0]}, 20.0, 1e-3)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def main():
|
|
439
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
440
|
+
ap.add_argument("--model", default="mel-roformer", choices=list(MODELS))
|
|
441
|
+
ap.add_argument("--out-dir", default=None, help="default: $AUDIO_NEURAL_CACHE/<model> or ~/.cache/audiojs/neural/<model>")
|
|
442
|
+
ap.add_argument("--opset", type=int, default=17)
|
|
443
|
+
ap.add_argument("--fp16", action="store_true", help="also write <model>.fp16.onnx (float16 weights, float32 compute)")
|
|
444
|
+
ap.add_argument("--verify", action="store_true", help="compare with MelBandRoformer.forward; write the reference separation")
|
|
445
|
+
ap.add_argument("--engine", action="store_true", help="also write <model>.engine.bin/.json: the tensors for roformer.js, the package's WebGPU engine")
|
|
446
|
+
args = ap.parse_args()
|
|
447
|
+
|
|
448
|
+
cache = Path(os.environ.get("AUDIO_NEURAL_CACHE") or Path.home() / ".cache" / "audiojs" / "neural")
|
|
449
|
+
out_dir = Path(args.out_dir) if args.out_dir else cache / args.model
|
|
450
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
451
|
+
src = out_dir / "src"
|
|
452
|
+
src.mkdir(exist_ok=True)
|
|
453
|
+
try:
|
|
454
|
+
model, _ = load(src, args.model)
|
|
455
|
+
except ImportError as e:
|
|
456
|
+
raise SystemExit(f"required: pip install torch onnx onnxruntime pyyaml einops beartype rotary-embedding-torch librosa ({e})")
|
|
457
|
+
path = out_dir / f"{args.model}.onnx"
|
|
458
|
+
core = export(model, path, args.opset)
|
|
459
|
+
if args.verify:
|
|
460
|
+
import torch
|
|
461
|
+
|
|
462
|
+
with torch.no_grad():
|
|
463
|
+
verify(model, lambda spec: core(spec), "the GPU form in torch", 1e-4)
|
|
464
|
+
# onnxruntime against torch: on the tones 1.7e-4, where torch's own float32 stands 4.6e-3 from its float64
|
|
465
|
+
s = session(path)
|
|
466
|
+
verify(model, lambda spec: s.run(None, {"mix_spec": spec.numpy()})[0], path.name, 1e-3)
|
|
467
|
+
del s
|
|
468
|
+
gc.collect()
|
|
469
|
+
reference(model, out_dir)
|
|
470
|
+
if args.fp16:
|
|
471
|
+
fp16(path, out_dir / f"{args.model}.fp16.onnx")
|
|
472
|
+
if args.verify:
|
|
473
|
+
s = session(out_dir / f"{args.model}.fp16.onnx")
|
|
474
|
+
verify(model, lambda spec: s.run(None, {"mix_spec": spec.numpy()})[0], f"{args.model}.fp16.onnx", 2e-2)
|
|
475
|
+
if args.engine:
|
|
476
|
+
engine(model, out_dir, args.model)
|
|
477
|
+
print(f"done: {args.model} in {out_dir}")
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
if __name__ == "__main__":
|
|
481
|
+
sys.exit(main())
|
package/scripts/reference.py
CHANGED
|
@@ -64,9 +64,12 @@ def chunked(mix, run, L, step=0.25):
|
|
|
64
64
|
return out[..., border:border + N] if wide else out
|
|
65
65
|
|
|
66
66
|
|
|
67
|
-
def write(out_dir, separate, seconds=9.0):
|
|
68
|
-
"""separate(mix (2, N) float32) -> { target: (2, N) array }
|
|
67
|
+
def write(out_dir, separate, seconds=9.0, floor=0.0):
|
|
68
|
+
"""separate(mix (2, N) float32) -> { target: (2, N) array }. `floor`: white noise of that RMS under the mix, so no
|
|
69
|
+
band of it is silent (a model that normalizes each band's input turns a silent band's rounding into its direction)"""
|
|
69
70
|
mix = test_mix(seconds)
|
|
71
|
+
if floor:
|
|
72
|
+
mix = (mix + floor * np.random.RandomState(1).randn(*mix.shape)).astype(np.float32)
|
|
70
73
|
mix.tofile(out_dir / "test.f32")
|
|
71
74
|
for name, x in separate(mix).items():
|
|
72
75
|
np.asarray(x, dtype=np.float32).tofile(out_dir / f"test.{name}.f32")
|
package/separate.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { fft, ifft } from 'fourier-transform'
|
|
29
29
|
import resampleSinc from '@audio/resample-sinc'
|
|
30
|
-
import { load as neuralLoad, tensor, fetchModel } from '@audio/neural-runtime'
|
|
30
|
+
import { load as neuralLoad, tensor, fetchModel, fetchJson, backends } from '@audio/neural-runtime'
|
|
31
31
|
|
|
32
32
|
const PI2 = Math.PI * 2
|
|
33
33
|
const isNode = typeof process !== 'undefined' && !!process.versions?.node
|
|
@@ -371,6 +371,15 @@ const TIGER = {
|
|
|
371
371
|
standardize: false, mono: true, script: 'export-tiger.py',
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
// Mel-Band RoFormer's STFT (Hann of 2048, hop 441, unnormalized), 8 s segments of 801 frames every 4 s, as
|
|
375
|
+
// Music-Source-Separation-Training's demix() runs Kim's vocals model (num_overlap 2); nothing normalized
|
|
376
|
+
const MEL = {
|
|
377
|
+
modelType: 'complex', sampleRate: 44100, targets: ['vocals'], sources: ['vocals'], n: 2048, hop: 441, window: 'hann',
|
|
378
|
+
normalized: false, segment: 352800, frames: 801, step: 0.5, standardize: false, gpu: true, script: 'export-roformer.py',
|
|
379
|
+
// roformer.js's tensors, for a browser whose GPU has subgroup matrices: the engine runs in onnxruntime's place
|
|
380
|
+
engine: { meta: 'mel-roformer.engine.json', bin: 'mel-roformer.engine.bin', sha256: '140fedcd9d03a8f918b235688c69f4dfdd86b08f9d430277659c095688a026ff' },
|
|
381
|
+
}
|
|
382
|
+
|
|
374
383
|
// The compact files (scripts/compact.py: int8 weights, a few float16, float32 compute), each within 0.05 dB of its
|
|
375
384
|
// export's median SDR or SNR per stem (README, Size); their Hugging Face repositories, and SHA-256 checked when fetched
|
|
376
385
|
const FILES = {
|
|
@@ -378,6 +387,8 @@ const FILES = {
|
|
|
378
387
|
scnet: { file: 'scnet.int8.onnx', repo: 'audiojs/scnet', sha256: '98228931494151762a1c4ab1ec7899a894b1f81fd4a509921a9d2f9bacc50845' },
|
|
379
388
|
mrx: { file: 'mrx.int8.onnx', repo: 'audiojs/mrx', sha256: 'd876c92d224f5d8cd2207d528b23369da895a6de71fb24132416fcb16f9f8424' },
|
|
380
389
|
tiger: { file: 'tiger.int8.onnx', repo: 'audiojs/tiger-dnr', sha256: '2189859ac7e89cf4d9d284c6f393e737aaa2b3233ed7ee424ad622012bce7bca' },
|
|
390
|
+
// float16 weights, float32 compute: half the download; int8 (259 MB) ran slower on WebGPU and 10 dB further from float32
|
|
391
|
+
'mel-roformer': { file: 'mel-roformer.fp16.onnx', repo: 'audiojs/mel-roformer', sha256: 'adcbbb826f12453fbb86652b503242d565a9c81cdef1bca2fe0e54752eba91ef' },
|
|
381
392
|
}
|
|
382
393
|
|
|
383
394
|
// The hosted revision of each compact file's repository (a commit), its URL https://huggingface.co/<repo>/resolve/<revision>/
|
|
@@ -388,6 +399,7 @@ export const REVISIONS = {
|
|
|
388
399
|
scnet: 'e71bcdb42bce1d62b7fae25ba90fcbcf90f73f46',
|
|
389
400
|
mrx: 'dedc823a0bea2ed085d19e8b7f32589d35169ee8',
|
|
390
401
|
tiger: '1c959ab7ff8794dc5663b104ba301687ab95d2ab',
|
|
402
|
+
'mel-roformer': 'c1aba02c00a0bee852d6754fe6ede8164f90a4b3',
|
|
391
403
|
}
|
|
392
404
|
|
|
393
405
|
const hosted = name => models[name].repo && REVISIONS[name] ? `https://huggingface.co/${models[name].repo}/resolve/${REVISIONS[name]}/${models[name].file}` : null
|
|
@@ -406,6 +418,8 @@ export const models = {
|
|
|
406
418
|
mrx: { ...MRX, ...FILES.mrx },
|
|
407
419
|
// TIGER (JusperLee/TIGER, Apache-2.0 weights): the same three stems, three band-split models, about 50 times MRX's time
|
|
408
420
|
tiger: { ...TIGER, ...FILES.tiger },
|
|
421
|
+
// Mel-Band RoFormer (Kimberley Jensen's vocals model, MIT weights) as scripts/export-roformer.py exports it for the GPU
|
|
422
|
+
'mel-roformer': { ...MEL, ...FILES['mel-roformer'] },
|
|
409
423
|
}
|
|
410
424
|
|
|
411
425
|
async function cacheDir() {
|
|
@@ -452,6 +466,38 @@ async function local(graphs, name, opts) {
|
|
|
452
466
|
`, or pass opts.weights: a URL or directory holding ${name}/<file>.onnx`)
|
|
453
467
|
}
|
|
454
468
|
|
|
469
|
+
// A preset's own WebGPU engine (roformer.js) as a session, its tensors from `weights` or the hosted revision, or null
|
|
470
|
+
// where the GPU has no subgroup matrices (onnxruntime then): in the browser. Kept after a separation for KEEP ms, so the
|
|
471
|
+
// next one starts at once (its 466 MB of weights and 2 GB of buffers on the GPU, made in about a second), then let go
|
|
472
|
+
const KEEP = 60_000
|
|
473
|
+
let kept = null
|
|
474
|
+
function engineSession(opts, p) {
|
|
475
|
+
let name = opts.model, key = `${name}:${opts.weights ?? ''}:${p.engine.sha256}`
|
|
476
|
+
if (kept?.key === key) { clearTimeout(kept.timer); return kept.session }
|
|
477
|
+
kept?.session.then(s => s?.drop())
|
|
478
|
+
let mine = { key, timer: null }
|
|
479
|
+
// free() after a separation lets go KEEP ms later, unless the next one has taken it
|
|
480
|
+
mine.session = makeEngine(opts, p, name).then(s => s && {
|
|
481
|
+
...s, drop: s.free,
|
|
482
|
+
free() { clearTimeout(mine.timer); mine.timer = setTimeout(() => { if (kept === mine) kept = null; s.free() }, KEEP) },
|
|
483
|
+
})
|
|
484
|
+
return (kept = mine).session
|
|
485
|
+
}
|
|
486
|
+
async function makeEngine(opts, p, name) {
|
|
487
|
+
let { engine, gpu } = await import('./roformer.js'), device = await gpu()
|
|
488
|
+
if (!device) return null
|
|
489
|
+
try {
|
|
490
|
+
let base = opts.weights != null ? `${await weightsBase(opts, name)}${name}/` : `https://huggingface.co/${p.repo}/resolve/${REVISIONS[name]}/`
|
|
491
|
+
let [meta, bytes] = await Promise.all([fetchJson(base + p.engine.meta), checked(base + p.engine.bin, p.engine.sha256)])
|
|
492
|
+
return await engine(meta, bytes, device)
|
|
493
|
+
} catch (e) {
|
|
494
|
+
// its files not there (weights without them), or the GPU refusing its buffers: onnxruntime, as without it
|
|
495
|
+
device.destroy()
|
|
496
|
+
console.warn(`neural-separate: ${name}'s own engine stands aside (${e.message})`)
|
|
497
|
+
return null
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
455
501
|
// The hosted compact file into the cache, checked against its SHA-256, written whole or not at all → its file URL
|
|
456
502
|
async function fetched(name, dest) {
|
|
457
503
|
let [fs, path, { pathToFileURL }] = await Promise.all([import('node:fs'), import('node:path'), import('node:url')])
|
|
@@ -503,12 +549,18 @@ async function resolveModel(opts) {
|
|
|
503
549
|
// No pooled arena: one htdemucs segment peaks at 2.7 GB RSS in onnxruntime-node instead of 3.2 GB.
|
|
504
550
|
const sessionOptions = { enableCpuMemArena: false, enableMemPattern: false }
|
|
505
551
|
|
|
506
|
-
// A graph's session; a fetched file with a SHA-256 checked against it first
|
|
507
|
-
|
|
552
|
+
// A graph's session; a fetched file with a SHA-256 checked against it first. A preset written for the GPU (`gpu`) runs
|
|
553
|
+
// on WebGPU where there is one (onnxruntime-web's in a browser, onnxruntime-node's in Node) unless `device` says
|
|
554
|
+
// otherwise: SCNet's LSTMs and the others' many small operators run no faster there
|
|
555
|
+
function loader(opts, preset) {
|
|
556
|
+
let device = opts.device ?? (preset?.gpu ? backends().then(b => b.includes('webgpu') ? 'webgpu' : undefined) : undefined)
|
|
557
|
+
let own = !opts.device && !opts.session && preset?.engine && !isNode ? engineSession(opts, preset) : null
|
|
508
558
|
return async g => {
|
|
509
559
|
if (opts.session) return opts.session(g.spec, opts)
|
|
560
|
+
let s = own && await own
|
|
561
|
+
if (s) return s
|
|
510
562
|
let spec = g.sha256 && !g.spec.startsWith('file:') ? await checked(g.spec, g.sha256) : g.spec
|
|
511
|
-
return neuralLoad(spec, { backend:
|
|
563
|
+
return neuralLoad(spec, { backend: await device, sessionOptions })
|
|
512
564
|
}
|
|
513
565
|
}
|
|
514
566
|
|
|
@@ -959,7 +1011,7 @@ export default async function separate(audio, opts = {}) {
|
|
|
959
1011
|
if (!channelData[0].length) return { stems: Object.fromEntries(targets.map(t => [t, empty()])), sampleRate: rate, residual: empty() }
|
|
960
1012
|
let targetRate = opts.targetRate ?? modelRate ?? rate
|
|
961
1013
|
let proc = targetRate !== rate ? channelData.map(c => resampleSinc(c, { from: rate, to: targetRate })) : channelData
|
|
962
|
-
let load = loader(opts)
|
|
1014
|
+
let load = loader(opts, preset)
|
|
963
1015
|
// the input at the loudness the model trained at, its stems scaled back (a silent input as it is)
|
|
964
1016
|
let level = preset?.loudness == null ? -Infinity : lufs(proc, targetRate), gain = Number.isFinite(level) ? 10 ** ((preset.loudness - level) / 20) : 1
|
|
965
1017
|
if (gain !== 1) proc = proc.map(c => c.map(v => v * gain))
|