@audio/neural-separate 0.4.0 → 0.6.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 +19 -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 +519 -0
- package/scripts/reference.py +5 -2
- package/separate.js +72 -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
|
|
|
@@ -62,13 +63,22 @@ together (0.9 GB of it the browser idle): wasm `scnet` 3.0 GB, `scnet-large` 4.6
|
|
|
62
63
|
on wasm 9 and 0.3 minutes. Float16 compute does not pay: `tiger`'s on WebGPU is 13 % faster and its music 4.7 dB from
|
|
63
64
|
the float32 graph's.
|
|
64
65
|
|
|
66
|
+
A voice's room (de-reverb):
|
|
67
|
+
|
|
68
|
+
| `model` | Architecture | Files | Weights | Targets | Real-time factor: Node CPU · WebGPU |
|
|
69
|
+
|---|---|---|---|---|---|
|
|
70
|
+
| `'dereverb-room'` | anvuew's room de-reverb: a mono BS-RoFormer (axial transformers over time and 63 contiguous bands of the complex spectrogram, dim 128, depth 16, 29.4 M parameters), trained on voices in rooms simulated with pyroomacoustics | 60 MB, float16 weights (export 122 MB) | GPL-3.0 | `dry`: the voice with its room off, early reflections and tail; the residual is the room | 2.3 · 0.65 (Node's WebGPU) |
|
|
71
|
+
|
|
72
|
+
Each channel heard apart, 8.7 s segments every 4.35 s as `demix()` runs it (`num_overlap: 2`), nothing normalized. Measured through [`audio`](https://github.com/audiojs/audio)'s `dereverb()` (`bench/rx/dereverb.mjs`, test): VoiceBank test utterances in 19 MIT IR Survey rooms (Traer & McDermott, PNAS 2016; T60 0.38–1.85 s), each response split at direct + 50 ms, PESQ against the dry take 1.39 → 2.65, STOI 0.77 → 0.94, DNSMOS OVRL 2.19 → 2.98, the late tail 15.6 dB down; iZotope RX 12 De-reverb tuned 1.45, its Dialogue Isolate with the reverb off 1.79. It takes the early reflections too: the output stands 5.2 dB under the direct sound and its first 50 ms, where the voice is 10 dB over the tail. Music is room to it (MUSDB18 previews, GuitarSet, sung tones and four pieces lose 6 dB in their worst octave as they are, and 9 dB of themselves in a room); `audio` passes music by a speech/music guard. The real-time factors are of 20 s of stereo on a busy 14-core M4 Max (load averages over 100), so upper bounds; the float16 file stands 74 dB from the PyTorch forward through this package's pipeline, the float32 export 123 dB.
|
|
73
|
+
|
|
65
74
|
`targets` picks a subset: `targets: ['vocals']` runs only the vocals graph of `umxhq` (against the residual, see Algorithm: 2.4× faster, vocals SDR 6.50 instead of 6.75) and of `htdemucs_ft`, and skips the other sources' iSTFT for `htdemucs`. Presets resample to their 44.1 kHz and back.
|
|
66
75
|
|
|
67
76
|
## Weights
|
|
68
77
|
|
|
69
78
|
Nothing ships in this package. `scnet-large`, `scnet`, `mrx` and `tiger` read a compact file each, `<model>.int8.onnx`
|
|
70
|
-
(Size, below),
|
|
71
|
-
|
|
79
|
+
(Size, below), and `mel-roformer` its float16 weights, `mel-roformer.fp16.onnx` (or `roformer.js`'s `mel-roformer.engine.bin`), hosted on Hugging Face
|
|
80
|
+
(`audiojs/scnet-large`, `audiojs/scnet`, `audiojs/mrx`, `audiojs/tiger-dnr`, `audiojs/mel-roformer`, each with its model
|
|
81
|
+
card and licence): without `weights`, `https://huggingface.co/<repo>/resolve/<revision>/<file>` at the
|
|
72
82
|
revision `REVISIONS` pins, fetched once and checked against its SHA-256 (the browser keeps it in the Cache API, Node in
|
|
73
83
|
the neural cache, below). Every model can also be exported from its original checkpoint:
|
|
74
84
|
|
|
@@ -88,6 +98,10 @@ python3 node_modules/@audio/neural-separate/scripts/export-mrx.py --verify
|
|
|
88
98
|
pip install safetensors
|
|
89
99
|
python3 node_modules/@audio/neural-separate/scripts/export-tiger.py --verify
|
|
90
100
|
|
|
101
|
+
pip install einops beartype rotary-embedding-torch librosa
|
|
102
|
+
python3 node_modules/@audio/neural-separate/scripts/export-roformer.py --verify --fp16 --engine
|
|
103
|
+
node node_modules/@audio/neural-separate/scripts/engine.mjs # roformer.js in Chromium against onnxruntime (playwright)
|
|
104
|
+
|
|
91
105
|
python3 node_modules/@audio/neural-separate/scripts/compact.py --model tiger --verify # tiger.int8.onnx from tiger.onnx
|
|
92
106
|
```
|
|
93
107
|
|
|
@@ -97,6 +111,8 @@ They land in `$AUDIO_NEURAL_CACHE/<model>/`, default `~/.cache/audiojs/neural/<m
|
|
|
97
111
|
- **`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
112
|
- **`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
113
|
- **`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.
|
|
114
|
+
- **`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.
|
|
115
|
+
- **`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
116
|
- **`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
117
|
|
|
102
118
|
## Size
|
|
@@ -279,6 +295,7 @@ Open-source stem separation has these lineages; this package runs SCNet, Open-Un
|
|
|
279
295
|
| **SCNet** (`scnet`, `scnet-large`; starrytong) | MIT | MIT: "I confirm that the released SCNet and SCNet-large pretrained weights are distributed under the MIT License, consistent with the source code. You are welcome to redistribute the original checkpoints and format-converted versions, including ONNX exports, as part of your MIT-licensed tool, with appropriate attribution." (the author in [starrytong/SCNet#35](https://github.com/starrytong/SCNet/issues/35), 2026) | trained on MUSDB18-HQ; hosted with its configs by [ZFTurbo/Music-Source-Separation-Training](https://github.com/ZFTurbo/Music-Source-Separation-Training) (MIT), whose own MUSDB18-HQ checkpoints (SCNet XL, BS-RoFormer) carry its author's MIT statement only for some files ([#254](https://github.com/ZFTurbo/Music-Source-Separation-Training/issues/254)) |
|
|
280
296
|
| **MRX** (MERL; `mrx`) | MIT | MIT: the repository's [`.reuse/dep5`](https://github.com/merlresearch/cocktail-fork-separation/blob/main/.reuse/dep5) names the four checkpoints, "Copyright: 2023 Mitsubishi Electric Research Laboratories (MERL) / License: MIT" | trained on Divide and Remaster v2, whose music and effects come from FMA and FSD50K clips each under its own licence, some non-commercial (the DnR v3 paper's account) |
|
|
281
297
|
| **TIGER** (Tsinghua; `tiger`) | MIT (repository LICENSE; its README badge says Apache 2.0) | Apache 2.0: the [model card](https://huggingface.co/JusperLee/TIGER-DnR) states `license: apache-2.0` | trained on Divide and Remaster v1 (652 test mixtures in its paper), sourced as v2 is |
|
|
298
|
+
| **anvuew's de-reverb** (`dereverb-room`) | the architecture: BS-RoFormer (Phil Wang's [BS-RoFormer](https://github.com/lucidrains/BS-RoFormer), MIT, in ZFTurbo's Music-Source-Separation-Training) | **GPL-3.0** ([model card](https://huggingface.co/anvuew/dereverb_room)): the converted copy is hosted under the same licence, fetched at run time, never bundled | trained on convolutional reverbs from pyroomacoustics, the dry voices not named |
|
|
282
299
|
| **Bandit v2** (DnR v3) | Apache 2.0 | **CC BY-SA 4.0** ([Zenodo](https://zenodo.org/records/12701995)): a converted copy is an adaptation under the same terms | not shipped; BandIt v1's DnR checkpoints are CC BY-NC 4.0 ([Zenodo](https://zenodo.org/records/10160698)) |
|
|
283
300
|
| **Spleeter** (Deezer) | MIT | **undocumented** | the README licenses only "the code of Spleeter"; the pretrained weights' license is an open, unresolved question ([deezer/spleeter#898](https://github.com/deezer/spleeter/issues/898)) — do not assume MIT |
|
|
284
301
|
|
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.6.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,519 @@
|
|
|
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) and Band-Split RoFormer (Lu, Wang, Kong, Hung, "Music Source Separation with Band-Split RoPE Transformer", ICASSP
|
|
4
|
+
2024) to the ONNX contract @audio/neural-separate expects for modelType 'complex': the network between its STFT and
|
|
5
|
+
iSTFT, which JS computes, written for the GPU.
|
|
6
|
+
|
|
7
|
+
pip install torch onnx onnxruntime pyyaml einops beartype rotary-embedding-torch librosa
|
|
8
|
+
python3 export-roformer.py --model mel-roformer --verify
|
|
9
|
+
python3 export-roformer.py --model dereverb-room --verify --fp16
|
|
10
|
+
|
|
11
|
+
The models, each run by ZFTurbo/Music-Source-Separation-Training's (MIT) models/bs_roformer code at a pinned commit:
|
|
12
|
+
mel-roformer Kimberley Jensen's vocals Mel-Band RoFormer (huggingface.co/KimberleyJSN/melbandroformer, MIT), with
|
|
13
|
+
the config that repository runs it by (configs/KimberleyJensen/config_vocals_mel_band_roformer_kj.yaml)
|
|
14
|
+
dereverb-room anvuew's room de-reverb, a mono BS-RoFormer (huggingface.co/anvuew/dereverb_room, GPL-3.0): a voice
|
|
15
|
+
in a room in, the voice dry out; trained on voices in rooms pyroomacoustics simulated (its card); its
|
|
16
|
+
own config, dim 128, depth 16, 63 bands, 29.4 M parameters
|
|
17
|
+
Each checkpoint and config at a pinned revision, the checkpoint checked by its SHA-256. What follows is told of
|
|
18
|
+
mel-roformer; dereverb-room is one channel, 8.7 s segments (384000 samples, hop 512, 751 frames), its bands contiguous
|
|
19
|
+
(each frequency in one band, so its masks are taken as they come) and one RMSNorm after its transformers, not one each.
|
|
20
|
+
|
|
21
|
+
One 8 s segment (352800 samples at 44.1 kHz, 801 frames) per run:
|
|
22
|
+
input mix_spec [1, 4, 1025, 801] STFT, complex as channels (L re, L im, R re, R im)
|
|
23
|
+
output stems_spec [1, 4, 1025, 801] the vocals, per channel, re and im
|
|
24
|
+
The STFT is the model's own: n_fft 2048, hop 441, Hann window of 2048, unnormalized, centered with reflect padding.
|
|
25
|
+
|
|
26
|
+
The graph computes what MelBandRoformer.forward computes between its STFT and iSTFT, rearranged so a GPU runs it in few
|
|
27
|
+
and large operations; none of it changes the function (--verify: against the PyTorch forward):
|
|
28
|
+
- the 60 bands' input projections and mask MLPs run as batched matrix products, bands of near widths grouped and
|
|
29
|
+
zero-padded to the widest of their group (10 groups, none wider than 1.3 times its narrowest), not 60
|
|
30
|
+
small products each;
|
|
31
|
+
- each RMSNorm's scale (√d·γ) is folded into the weights of the linear layer it feeds; the attention's query scale
|
|
32
|
+
into the query weights; its gates' projection joins the q, k, v projection;
|
|
33
|
+
- the rotary embedding's interleaved pairs become halves (the same permutation of q's and k's head features leaves
|
|
34
|
+
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;
|
|
35
|
+
- the masks of overlapping mel bands are averaged by two gathers and a weight, not a scatter, the DC bin's weight 0
|
|
36
|
+
(zero_dc);
|
|
37
|
+
- no tensor passes 100 MB: each transformer runs on slices of its sequences, the mask MLP by group of bands
|
|
38
|
+
(onnxruntime's WebGPU Softmax went wrong past WebGPU's default 128 MiB storage binding).
|
|
39
|
+
|
|
40
|
+
Writes <out-dir>/mel-roformer.onnx (928 MB), and with --fp16 mel-roformer.fp16.onnx (float16 weights, float32 compute,
|
|
41
|
+
half the download). Products computed in float16 measured slower on WebGPU (3.2 against 2.6 s a segment): not offered.
|
|
42
|
+
Default out-dir: $AUDIO_NEURAL_CACHE/<model>, else ~/.cache/audiojs/neural/<model>. --verify compares the stems rebuilt
|
|
43
|
+
from onnxruntime's output with MelBandRoformer.forward on the same segment and writes the reference separation test.js
|
|
44
|
+
compares the JS pipeline against (scripts/reference.py, chunked as separate.js chunks: step 0.5, Kim's num_overlap 2).
|
|
45
|
+
Last run: torch 2.14, onnxruntime 1.31.
|
|
46
|
+
"""
|
|
47
|
+
import argparse
|
|
48
|
+
import gc
|
|
49
|
+
import math
|
|
50
|
+
import os
|
|
51
|
+
import sys
|
|
52
|
+
import urllib.request
|
|
53
|
+
from pathlib import Path
|
|
54
|
+
|
|
55
|
+
REPO = "https://raw.githubusercontent.com/ZFTurbo/Music-Source-Separation-Training/84b1eac0887756b4f1a9d7a1ff49105939749ed2/"
|
|
56
|
+
MODELS = { # checkpoint URL, its SHA-256, config (a path in REPO or a URL), class, segment (samples), the stem's name
|
|
57
|
+
"mel-roformer": ("https://huggingface.co/KimberleyJSN/melbandroformer/resolve/ac9b0614ab3cd7f77219e18ba494dfd93956c348/MelBandRoformer.ckpt",
|
|
58
|
+
"87201f4d31afb5bc79993230fc49446918425574db48c01c405e44f365c7559e",
|
|
59
|
+
"configs/KimberleyJensen/config_vocals_mel_band_roformer_kj.yaml", "MelBandRoformer", 352800, "vocals"),
|
|
60
|
+
"dereverb-room": ("https://huggingface.co/anvuew/dereverb_room/resolve/0b85f5b80b7f779b2dfe80f33a1b35b38af9376d/dereverb_room_anvuew_sdr_13.7432.ckpt",
|
|
61
|
+
"2edec521f09e26341c1923dc82c8c52dbc86478b42b9999f679535743c970cb3",
|
|
62
|
+
"https://huggingface.co/anvuew/dereverb_room/resolve/0b85f5b80b7f779b2dfe80f33a1b35b38af9376d/dereverb_room_anvuew.yaml",
|
|
63
|
+
"BSRoformer", 384000, "dry"),
|
|
64
|
+
}
|
|
65
|
+
SEGMENT = 352800 # the model's, set by main()
|
|
66
|
+
STEP = 0.5
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fetch(url, path):
|
|
70
|
+
if not path.exists():
|
|
71
|
+
print(f"fetching {url}")
|
|
72
|
+
urllib.request.urlretrieve(url, str(path) + ".part")
|
|
73
|
+
os.replace(str(path) + ".part", path)
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def load(src, name="mel-roformer"):
|
|
78
|
+
"""The model with its weights: the model code, config and checkpoint fetched into `src` once."""
|
|
79
|
+
import hashlib
|
|
80
|
+
import torch
|
|
81
|
+
import yaml
|
|
82
|
+
|
|
83
|
+
pkg = src / "roformer_code" / "models" / "bs_roformer"
|
|
84
|
+
pkg.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
(pkg.parent / "__init__.py").touch()
|
|
86
|
+
(pkg / "__init__.py").touch()
|
|
87
|
+
for f in ("mel_band_roformer.py", "bs_roformer.py", "attend.py"):
|
|
88
|
+
fetch(REPO + "models/bs_roformer/" + f, pkg / f)
|
|
89
|
+
sys.path.insert(0, str(src / "roformer_code"))
|
|
90
|
+
sys.dont_write_bytecode = True
|
|
91
|
+
from models.bs_roformer.mel_band_roformer import MelBandRoformer
|
|
92
|
+
from models.bs_roformer.bs_roformer import BSRoformer
|
|
93
|
+
|
|
94
|
+
class Loader(yaml.SafeLoader): pass # the config's resolutions are a !!python/tuple
|
|
95
|
+
Loader.add_constructor("tag:yaml.org,2002:python/tuple", lambda l, n: tuple(l.construct_sequence(n)))
|
|
96
|
+
url, sha, config, cls, _, _ = MODELS[name]
|
|
97
|
+
cfg = yaml.load(fetch(config if "://" in config else REPO + config, src / Path(config).name).read_text(), Loader=Loader)
|
|
98
|
+
ckpt = fetch(url, src / Path(url).name)
|
|
99
|
+
h = hashlib.sha256()
|
|
100
|
+
with open(ckpt, "rb") as f:
|
|
101
|
+
for b in iter(lambda: f.read(1 << 24), b""):
|
|
102
|
+
h.update(b)
|
|
103
|
+
if h.hexdigest() != sha:
|
|
104
|
+
raise SystemExit(f"{ckpt}: SHA-256 {h.hexdigest()}, expected {sha}")
|
|
105
|
+
model = {"MelBandRoformer": MelBandRoformer, "BSRoformer": BSRoformer}[cls](**cfg["model"])
|
|
106
|
+
state = torch.load(ckpt, map_location="cpu", weights_only=True)
|
|
107
|
+
model.load_state_dict(state.get("state_dict", state) if isinstance(state, dict) else state)
|
|
108
|
+
return model.eval(), cfg
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def stft(model, x):
|
|
112
|
+
"""The model's STFT: (B, C, L) -> (B, 2C, F, T), complex as channels."""
|
|
113
|
+
import torch
|
|
114
|
+
|
|
115
|
+
B, C, L = x.shape
|
|
116
|
+
z = torch.view_as_real(torch.stft(x.reshape(-1, L), **model.stft_kwargs, window=model.stft_window_fn(), return_complex=True))
|
|
117
|
+
return z.permute(0, 3, 1, 2).reshape(B, 2 * C, z.shape[1], z.shape[2])
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def istft(model, y, length):
|
|
121
|
+
"""(B, 2·S·C, F, T) -> (B, S, C, L)"""
|
|
122
|
+
import torch
|
|
123
|
+
|
|
124
|
+
B, _, Fr, T = y.shape
|
|
125
|
+
y = torch.view_as_complex(y.reshape(-1, 2, Fr, T).permute(0, 2, 3, 1).contiguous())
|
|
126
|
+
x = torch.istft(y, **model.stft_kwargs, window=model.stft_window_fn(), length=length)
|
|
127
|
+
return x.reshape(B, -1, model.audio_channels, length)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def bands(model):
|
|
131
|
+
"""(frequencies, (f·S + s) of each band's pairs, bands in order, frequencies per band): Mel-Band RoFormer's
|
|
132
|
+
overlapping mel bands, BS-RoFormer's contiguous ones"""
|
|
133
|
+
S = model.audio_channels
|
|
134
|
+
if hasattr(model, "freq_indices"):
|
|
135
|
+
return model.num_bands_per_freq.numel(), model.freq_indices.tolist(), model.num_freqs_per_band.tolist()
|
|
136
|
+
nf = [d // (2 * S) for d in model.band_split.dim_inputs]
|
|
137
|
+
return sum(nf), list(range(sum(nf) * S)), nf
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def groups(widths, slack=1.3):
|
|
141
|
+
"""Contiguous runs of bands whose widest is at most `slack` times their narrowest: [(first, last + 1, widest)]"""
|
|
142
|
+
out, b0 = [], 0
|
|
143
|
+
for b in range(1, len(widths) + 1):
|
|
144
|
+
if b == len(widths) or max(widths[b0:b + 1]) > slack * min(widths[b0:b + 1]):
|
|
145
|
+
out.append((b0, b, max(widths[b0:b])))
|
|
146
|
+
b0 = b
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Core:
|
|
151
|
+
"""The model's forward between its STFT and its iSTFT, for the GPU (the module docstring says how)."""
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def build(model, T):
|
|
155
|
+
import torch
|
|
156
|
+
import torch.nn as nn
|
|
157
|
+
import torch.nn.functional as F
|
|
158
|
+
|
|
159
|
+
f64 = torch.float64
|
|
160
|
+
S = model.audio_channels
|
|
161
|
+
Fq, fi, nf = bands(model)
|
|
162
|
+
widths = [2 * n * S for n in nf] # a band's input: its (f, s) pairs, re and im
|
|
163
|
+
G = groups(widths)
|
|
164
|
+
lin = torch.matmul
|
|
165
|
+
# every tensor under WebGPU's default storage binding (128 MiB, maxStorageBufferBindingSize): onnxruntime
|
|
166
|
+
# 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
|
|
167
|
+
# at 123 MB), so a transformer runs on as many sequences at a time as keep its largest, the attention's
|
|
168
|
+
# scores (heads × n × n) or its feed-forward's hidden layer (n × 4·dim), under 100 MB
|
|
169
|
+
BUDGET = 100e6
|
|
170
|
+
|
|
171
|
+
def sliced(f, x):
|
|
172
|
+
per = max(f.biggest(x.shape[1]), 1)
|
|
173
|
+
size = max(1, int(BUDGET // per))
|
|
174
|
+
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)
|
|
175
|
+
|
|
176
|
+
rot = lambda n, emb: emb(emb.get_seq_pos(n, device="cpu", dtype=torch.float32)) # (n, dh) interleaved
|
|
177
|
+
|
|
178
|
+
def rms(m):
|
|
179
|
+
return (m.gamma.detach().double() * m.scale)
|
|
180
|
+
|
|
181
|
+
class Mod(nn.Module):
|
|
182
|
+
def __init__(s):
|
|
183
|
+
super().__init__()
|
|
184
|
+
# the STFT as columns: E[t, (f·S + s)·2 + c], and a zero column past them (index Fq·S·2)
|
|
185
|
+
pos = [] # each band's input columns, in its order
|
|
186
|
+
o = 0
|
|
187
|
+
for n in nf:
|
|
188
|
+
pos.append([fi[k] * 2 + c for k in range(o, o + n * S) for c in range(2)])
|
|
189
|
+
o += n * S
|
|
190
|
+
zero = Fq * S * 2
|
|
191
|
+
bs = model.band_split.to_features
|
|
192
|
+
s.split_idx, s.split_w, s.split_b = [], nn.ParameterList(), nn.ParameterList()
|
|
193
|
+
for b0, b1, W in G:
|
|
194
|
+
idx = torch.full((b1 - b0, W), zero, dtype=torch.long)
|
|
195
|
+
w = torch.zeros(b1 - b0, W, bs[0][1].out_features, dtype=f64)
|
|
196
|
+
bias = torch.zeros(b1 - b0, 1, w.shape[2], dtype=f64)
|
|
197
|
+
for j, b in enumerate(range(b0, b1)):
|
|
198
|
+
norm, lin = bs[b][0], bs[b][1]
|
|
199
|
+
idx[j, :widths[b]] = torch.tensor(pos[b])
|
|
200
|
+
w[j, :widths[b]] = (lin.weight.detach().double() * rms(norm)[None]).T
|
|
201
|
+
bias[j, 0] = lin.bias.detach().double()
|
|
202
|
+
s.register_buffer(f"si{len(s.split_idx)}", idx)
|
|
203
|
+
s.split_idx.append(f"si{len(s.split_idx)}")
|
|
204
|
+
s.split_w.append(nn.Parameter(w.float(), requires_grad=False))
|
|
205
|
+
s.split_b.append(nn.Parameter(bias.float(), requires_grad=False))
|
|
206
|
+
# transformers: time then frequency, per depth
|
|
207
|
+
s.blocks = nn.ModuleList()
|
|
208
|
+
for time, freq in model.layers:
|
|
209
|
+
s.blocks.append(nn.ModuleList([Block(time, T), Block(freq, len(nf))]))
|
|
210
|
+
# BS-RoFormer's one norm after the transformers (Mel-Band RoFormer's end each transformer)
|
|
211
|
+
fn = getattr(model, "final_norm", None)
|
|
212
|
+
s.final = nn.Parameter(rms(fn).float(), requires_grad=False) if fn is not None else None
|
|
213
|
+
# mask estimator (one stem): its MLP's linear layers before the last batched over bands (their shapes
|
|
214
|
+
# alike), the last by group, then GLU
|
|
215
|
+
me = model.mask_estimators[0].to_freqs
|
|
216
|
+
lin = [[m for m in me[b][0] if isinstance(m, nn.Linear)] for b in range(len(nf))]
|
|
217
|
+
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)])
|
|
218
|
+
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)])
|
|
219
|
+
s.m2_w, s.m2_b = nn.ParameterList(), nn.ParameterList()
|
|
220
|
+
col, cols = 0, {}
|
|
221
|
+
for b0, b1, W in G:
|
|
222
|
+
H = lin[0][-1].in_features
|
|
223
|
+
w = torch.zeros(b1 - b0, H, 2 * W)
|
|
224
|
+
bias = torch.zeros(b1 - b0, 1, 2 * W)
|
|
225
|
+
for j, b in enumerate(range(b0, b1)):
|
|
226
|
+
l2 = lin[b][-1]
|
|
227
|
+
n = widths[b]
|
|
228
|
+
w[j, :, :n], w[j, :, W:W + n] = l2.weight.detach()[:n].T, l2.weight.detach()[n:].T
|
|
229
|
+
bias[j, 0, :n], bias[j, 0, W:W + n] = l2.bias.detach()[:n], l2.bias.detach()[n:]
|
|
230
|
+
for e in range(n):
|
|
231
|
+
cols.setdefault(pos[b][e], []).append(col + j * W + e)
|
|
232
|
+
s.m2_w.append(nn.Parameter(w, requires_grad=False))
|
|
233
|
+
s.m2_b.append(nn.Parameter(bias, requires_grad=False))
|
|
234
|
+
col += (b1 - b0) * W
|
|
235
|
+
# each output column's sources among the masks (at most two bands overlap), the zero column for none,
|
|
236
|
+
# and its weight: 1 over the bands that cover it, 0 at DC
|
|
237
|
+
i1 = torch.full((zero,), col, dtype=torch.long)
|
|
238
|
+
i2 = torch.full((zero,), col, dtype=torch.long)
|
|
239
|
+
wt = torch.zeros(zero)
|
|
240
|
+
for o, src in cols.items():
|
|
241
|
+
assert len(src) <= 2, "three bands over one frequency"
|
|
242
|
+
i1[o] = src[0]
|
|
243
|
+
i2[o] = src[-1] if len(src) == 2 else col
|
|
244
|
+
wt[o] = 0 if o // (S * 2) == 0 and model.zero_dc else 1 / len(src)
|
|
245
|
+
s.register_buffer("i1", i1)
|
|
246
|
+
s.register_buffer("i2", i2)
|
|
247
|
+
s.register_buffer("wt", wt)
|
|
248
|
+
s.Fq, s.S = Fq, S
|
|
249
|
+
|
|
250
|
+
def forward(s, mix_spec):
|
|
251
|
+
Fq, S = s.Fq, s.S
|
|
252
|
+
T_ = mix_spec.shape[3]
|
|
253
|
+
# (1, 2S, F, T) -> E (T, F·S·2): [t, (f·S + s)·2 + c]
|
|
254
|
+
E = mix_spec.reshape(S, 2, Fq, T_).permute(3, 2, 0, 1).reshape(T_, Fq * S * 2)
|
|
255
|
+
Ez = torch.cat([E, torch.zeros(T_, 1, dtype=E.dtype)], 1)
|
|
256
|
+
xs = []
|
|
257
|
+
for k, name in enumerate(s.split_idx):
|
|
258
|
+
idx = getattr(s, name) # (nb, W)
|
|
259
|
+
x = Ez[:, idx].permute(1, 0, 2) # (nb, T, W)
|
|
260
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
261
|
+
xs.append(lin(x, s.split_w[k]) + s.split_b[k])
|
|
262
|
+
x = torch.cat(xs, 0) # (B, T, D)
|
|
263
|
+
for time, freq in s.blocks:
|
|
264
|
+
x = sliced(time, x) # sequences over time, one per band
|
|
265
|
+
x = sliced(freq, x.transpose(0, 1)).transpose(0, 1) # over bands, one per frame
|
|
266
|
+
if s.final is not None:
|
|
267
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12) * s.final
|
|
268
|
+
ms = []
|
|
269
|
+
for k, (lo, hi, W) in enumerate(G):
|
|
270
|
+
h = x[lo:hi]
|
|
271
|
+
for w, b in zip(s.mw, s.mb):
|
|
272
|
+
h = torch.tanh(lin(h, w[lo:hi]) + b[lo:hi]) # (nb, T, H)
|
|
273
|
+
y = lin(h, s.m2_w[k]) + s.m2_b[k] # (nb, T, 2W)
|
|
274
|
+
m = y[..., :W] * torch.sigmoid(y[..., W:]) # GLU
|
|
275
|
+
ms.append(m.permute(1, 0, 2).reshape(T_, (hi - lo) * W))
|
|
276
|
+
M = torch.cat(ms + [torch.zeros(T_, 1)], 1) # (T, columns + 1)
|
|
277
|
+
mask = (M[:, s.i1] + M[:, s.i2]) * s.wt # (T, F·S·2)
|
|
278
|
+
e = E.reshape(T_, Fq, S, 2)
|
|
279
|
+
mk = mask.reshape(T_, Fq, S, 2)
|
|
280
|
+
er, ei, mr, mi = e[..., 0], e[..., 1], mk[..., 0], mk[..., 1]
|
|
281
|
+
out = torch.stack([er * mr - ei * mi, er * mi + ei * mr], -1) # (T, F, S, 2)
|
|
282
|
+
return out.permute(2, 3, 1, 0).reshape(1, 2 * S, Fq, T_)
|
|
283
|
+
|
|
284
|
+
class Block(nn.Module):
|
|
285
|
+
"""One Transformer of the model (its layers and output norm) over sequences of n: x (batch, n, D)."""
|
|
286
|
+
|
|
287
|
+
def __init__(s, tr, n):
|
|
288
|
+
super().__init__()
|
|
289
|
+
s.layers = nn.ModuleList()
|
|
290
|
+
for attn, ff in tr.layers:
|
|
291
|
+
s.layers.append(nn.ModuleList([Attn(attn, n), FF(ff)]))
|
|
292
|
+
s.out = None if isinstance(tr.norm, nn.Identity) else nn.Parameter(rms(tr.norm).float(), requires_grad=False)
|
|
293
|
+
|
|
294
|
+
def biggest(s, n):
|
|
295
|
+
"""bytes of the largest tensor one sequence of n makes"""
|
|
296
|
+
a, ff = s.layers[0]
|
|
297
|
+
return 4 * max(a.h * n * n, n * ff.w1.shape[1], n * a.qkvg.shape[1])
|
|
298
|
+
|
|
299
|
+
def forward(s, x):
|
|
300
|
+
for attn, ff in s.layers:
|
|
301
|
+
x = attn(x) + x
|
|
302
|
+
x = ff(x) + x
|
|
303
|
+
return x if s.out is None else x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12) * s.out
|
|
304
|
+
|
|
305
|
+
class Attn(nn.Module):
|
|
306
|
+
def __init__(s, a, n):
|
|
307
|
+
super().__init__()
|
|
308
|
+
h, dh = a.heads, a.to_qkv.out_features // (3 * a.heads)
|
|
309
|
+
g = rms(a.norm)[None]
|
|
310
|
+
wq, wk, wv = (a.to_qkv.weight.detach().double() * g).reshape(3, h, dh, -1)
|
|
311
|
+
# interleaved pairs -> halves, the same permutation of q's and k's features
|
|
312
|
+
perm = torch.cat([torch.arange(0, dh, 2), torch.arange(1, dh, 2)])
|
|
313
|
+
wq, wk = wq[:, perm] * a.scale, wk[:, perm]
|
|
314
|
+
wg = a.to_gates.weight.detach().double() * g
|
|
315
|
+
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)
|
|
316
|
+
s.gb = nn.Parameter(a.to_gates.bias.detach().float(), requires_grad=False)
|
|
317
|
+
s.wo = nn.Parameter(a.to_out[0].weight.detach().T.float(), requires_grad=False)
|
|
318
|
+
freqs = rot(n, a.rotary_embed).double()[:, perm] # (n, dh)
|
|
319
|
+
s.register_buffer("cos", freqs.cos().float())
|
|
320
|
+
s.register_buffer("sin", freqs.sin().float())
|
|
321
|
+
R = torch.zeros(dh, dh) # x @ R: (-x[half:], x[:half])
|
|
322
|
+
R[torch.arange(dh // 2) + dh // 2, torch.arange(dh // 2)] = -1
|
|
323
|
+
R[torch.arange(dh // 2), torch.arange(dh // 2) + dh // 2] = 1
|
|
324
|
+
s.register_buffer("R", R)
|
|
325
|
+
s.h, s.dh = h, dh
|
|
326
|
+
|
|
327
|
+
def forward(s, x):
|
|
328
|
+
Bt, n, _ = x.shape
|
|
329
|
+
h, dh = s.h, s.dh
|
|
330
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
331
|
+
y = lin(x, s.qkvg)
|
|
332
|
+
q, k, v = (y[..., i * h * dh:(i + 1) * h * dh].reshape(Bt, n, h, dh).transpose(1, 2) for i in range(3))
|
|
333
|
+
gates = torch.sigmoid(y[..., 3 * h * dh:] + s.gb) # (Bt, n, h)
|
|
334
|
+
q = q * s.cos + torch.matmul(q, s.R) * s.sin
|
|
335
|
+
k = k * s.cos + torch.matmul(k, s.R) * s.sin
|
|
336
|
+
o = torch.matmul(torch.softmax(torch.matmul(q, k.transpose(-1, -2)), -1), v) # (Bt, h, n, dh)
|
|
337
|
+
o = o * gates.transpose(1, 2)[..., None]
|
|
338
|
+
return lin(o.transpose(1, 2).reshape(Bt, n, h * dh), s.wo)
|
|
339
|
+
|
|
340
|
+
class FF(nn.Module):
|
|
341
|
+
def __init__(s, ff):
|
|
342
|
+
super().__init__()
|
|
343
|
+
norm, l1, _, _, l2, _ = ff.net
|
|
344
|
+
s.w1 = nn.Parameter((l1.weight.detach().double() * rms(norm)[None]).T.float(), requires_grad=False)
|
|
345
|
+
s.b1 = nn.Parameter(l1.bias.detach().float(), requires_grad=False)
|
|
346
|
+
s.w2 = nn.Parameter(l2.weight.detach().T.float(), requires_grad=False)
|
|
347
|
+
s.b2 = nn.Parameter(l2.bias.detach().float(), requires_grad=False)
|
|
348
|
+
|
|
349
|
+
def forward(s, x):
|
|
350
|
+
x = x / x.norm(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
351
|
+
return lin(F.gelu(lin(x, s.w1) + s.b1), s.w2) + s.b2
|
|
352
|
+
|
|
353
|
+
return Mod().eval()
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def frames(model):
|
|
357
|
+
import torch
|
|
358
|
+
|
|
359
|
+
return stft(model, torch.zeros(1, model.audio_channels, SEGMENT)).shape[3]
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def export(model, path, opset):
|
|
363
|
+
import torch
|
|
364
|
+
|
|
365
|
+
T = frames(model)
|
|
366
|
+
core = Core.build(model, T)
|
|
367
|
+
spec = stft(model, torch.randn(1, model.audio_channels, SEGMENT))
|
|
368
|
+
with torch.no_grad():
|
|
369
|
+
torch.onnx.export(core, (spec,), str(path), input_names=["mix_spec"], output_names=["stems_spec"],
|
|
370
|
+
opset_version=opset, dynamo=False)
|
|
371
|
+
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 bands(model)[2]])}")
|
|
372
|
+
return core
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def engine(model, out_dir, name):
|
|
376
|
+
"""The GPU form's tensors for the package's own WebGPU engine (roformer.js): <name>.engine.bin, each tensor in turn
|
|
377
|
+
(weights float16, tables float32, indices int32), and <name>.engine.json naming them, their type, shape and offset,
|
|
378
|
+
with the band groups and the sizes the engine reads."""
|
|
379
|
+
import json
|
|
380
|
+
import numpy as np
|
|
381
|
+
import torch
|
|
382
|
+
|
|
383
|
+
T = frames(model)
|
|
384
|
+
core = Core.build(model, T)
|
|
385
|
+
tensors, blob, off = {}, bytearray(), 0
|
|
386
|
+
for n, t in list(core.named_parameters()) + list(core.named_buffers()):
|
|
387
|
+
a = t.detach().numpy()
|
|
388
|
+
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"
|
|
389
|
+
b = a.astype(kind).tobytes()
|
|
390
|
+
pad = -len(blob) % 16
|
|
391
|
+
blob += bytes(pad)
|
|
392
|
+
tensors[n] = {"type": kind, "shape": list(a.shape), "offset": len(blob)}
|
|
393
|
+
blob += b
|
|
394
|
+
nf = bands(model)[2]
|
|
395
|
+
meta = {
|
|
396
|
+
"model": name, "frames": T, "freqs": core.Fq, "channels": core.S, "dim": model.layers[0][0].layers[0][0].to_qkv.in_features,
|
|
397
|
+
"heads": model.layers[0][0].layers[0][0].heads, "depth": len(model.layers), "bands": len(nf),
|
|
398
|
+
"groups": [list(g) for g in groups([2 * n * core.S for n in nf])], "widths": [2 * n * core.S for n in nf],
|
|
399
|
+
"tensors": tensors,
|
|
400
|
+
}
|
|
401
|
+
(out_dir / f"{name}.engine.bin").write_bytes(bytes(blob))
|
|
402
|
+
(out_dir / f"{name}.engine.json").write_text(json.dumps(meta))
|
|
403
|
+
print(f"wrote {out_dir / (name + '.engine.bin')} ({len(blob) / 1e6:.1f} MB) and its .json, {len(tensors)} tensors")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def fp16(path, out):
|
|
407
|
+
"""float16 weights, float32 compute (scripts/compact.py --as fp16)."""
|
|
408
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
409
|
+
import compact
|
|
410
|
+
|
|
411
|
+
compact.compact(path, out, "fp16")
|
|
412
|
+
print(f"wrote {out} ({out.stat().st_size / 1e6:.1f} MB)")
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def session(path):
|
|
416
|
+
import onnxruntime as ort
|
|
417
|
+
|
|
418
|
+
o = ort.SessionOptions()
|
|
419
|
+
o.enable_cpu_mem_arena = False
|
|
420
|
+
o.enable_mem_pattern = False
|
|
421
|
+
return ort.InferenceSession(str(path), o, providers=["CPUExecutionProvider"])
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def mixes(C):
|
|
425
|
+
import torch
|
|
426
|
+
|
|
427
|
+
t = torch.arange(SEGMENT) / 44100
|
|
428
|
+
tone = sum(torch.sin(2 * math.pi * f * t) / k for k, f in enumerate((110, 220, 440, 880, 1760), 1))
|
|
429
|
+
return (("noise", 0.1 * torch.randn(1, C, SEGMENT)), ("tones", 0.2 * torch.stack([tone, 0.8 * tone][:C])[None]))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def verify(model, run, label, tol):
|
|
433
|
+
"""The stem rebuilt from run(spec) against the model's forward: noise and tones."""
|
|
434
|
+
import numpy as np
|
|
435
|
+
import torch
|
|
436
|
+
|
|
437
|
+
for name, mix in mixes(model.audio_channels):
|
|
438
|
+
with torch.no_grad():
|
|
439
|
+
want = model(mix).numpy()
|
|
440
|
+
got = istft(model, torch.as_tensor(run(stft(model, mix))), SEGMENT)[:, 0].numpy()
|
|
441
|
+
want = want[..., :got.shape[-1]]
|
|
442
|
+
rel = np.abs(got - want).max() / max(np.abs(want).max(), 1e-12)
|
|
443
|
+
print(f" verify {label} {name}: max|diff|/max|y| = {rel:.2e} " + ("OK" if rel < tol else "FAIL"))
|
|
444
|
+
if rel >= tol:
|
|
445
|
+
raise SystemExit(f"{label}: verification failed ({rel:.2e} >= {tol})")
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def reference(model, out_dir, stem):
|
|
449
|
+
"""The reference separation: the model's forward on segments as scripts/reference.py's chunked() cuts them
|
|
450
|
+
(step 0.5, no standardization: Kim's config normalizes nothing, nor anvuew's), over a 20 s mix with a noise floor;
|
|
451
|
+
a mono model's channel by channel, as separate.js runs a mono preset."""
|
|
452
|
+
import torch
|
|
453
|
+
|
|
454
|
+
import numpy as np
|
|
455
|
+
|
|
456
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
457
|
+
import reference as ref
|
|
458
|
+
|
|
459
|
+
def run(seg):
|
|
460
|
+
with torch.no_grad():
|
|
461
|
+
y = model(torch.from_numpy(seg)[None]) # (1, C, L), BS-RoFormer's (1, 1 stem, C, L)
|
|
462
|
+
return (y[0] if y.ndim == 4 else y[0][None]).numpy() # (1 stem, C, L)
|
|
463
|
+
|
|
464
|
+
# under the mix a noise floor 60 dB down: each band normalizes its input, and a silent band's is its rounding
|
|
465
|
+
if model.audio_channels == 1:
|
|
466
|
+
each = lambda mix: np.concatenate([ref.chunked(mix[c:c + 1], run, SEGMENT, STEP)[0] for c in range(len(mix))])
|
|
467
|
+
else:
|
|
468
|
+
each = lambda mix: ref.chunked(mix, run, SEGMENT, STEP)[0]
|
|
469
|
+
ref.write(out_dir, lambda mix: {stem: each(mix)}, 20.0, 1e-3)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def main():
|
|
473
|
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
474
|
+
ap.add_argument("--model", default="mel-roformer", choices=list(MODELS))
|
|
475
|
+
ap.add_argument("--out-dir", default=None, help="default: $AUDIO_NEURAL_CACHE/<model> or ~/.cache/audiojs/neural/<model>")
|
|
476
|
+
ap.add_argument("--opset", type=int, default=17)
|
|
477
|
+
ap.add_argument("--fp16", action="store_true", help="also write <model>.fp16.onnx (float16 weights, float32 compute)")
|
|
478
|
+
ap.add_argument("--verify", action="store_true", help="compare with the model's forward; write the reference separation")
|
|
479
|
+
ap.add_argument("--engine", action="store_true", help="also write <model>.engine.bin/.json: the tensors for roformer.js, the package's WebGPU engine")
|
|
480
|
+
args = ap.parse_args()
|
|
481
|
+
if args.engine and MODELS[args.model][3] != "MelBandRoformer":
|
|
482
|
+
raise SystemExit("--engine: roformer.js runs Mel-Band RoFormer alone")
|
|
483
|
+
global SEGMENT
|
|
484
|
+
SEGMENT = MODELS[args.model][4]
|
|
485
|
+
|
|
486
|
+
cache = Path(os.environ.get("AUDIO_NEURAL_CACHE") or Path.home() / ".cache" / "audiojs" / "neural")
|
|
487
|
+
out_dir = Path(args.out_dir) if args.out_dir else cache / args.model
|
|
488
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
489
|
+
src = out_dir / "src"
|
|
490
|
+
src.mkdir(exist_ok=True)
|
|
491
|
+
try:
|
|
492
|
+
model, _ = load(src, args.model)
|
|
493
|
+
except ImportError as e:
|
|
494
|
+
raise SystemExit(f"required: pip install torch onnx onnxruntime pyyaml einops beartype rotary-embedding-torch librosa ({e})")
|
|
495
|
+
path = out_dir / f"{args.model}.onnx"
|
|
496
|
+
core = export(model, path, args.opset)
|
|
497
|
+
if args.verify:
|
|
498
|
+
import torch
|
|
499
|
+
|
|
500
|
+
with torch.no_grad():
|
|
501
|
+
verify(model, lambda spec: core(spec), "the GPU form in torch", 1e-4)
|
|
502
|
+
# onnxruntime against torch: on the tones 1.7e-4, where torch's own float32 stands 4.6e-3 from its float64
|
|
503
|
+
s = session(path)
|
|
504
|
+
verify(model, lambda spec: s.run(None, {"mix_spec": spec.numpy()})[0], path.name, 1e-3)
|
|
505
|
+
del s
|
|
506
|
+
gc.collect()
|
|
507
|
+
reference(model, out_dir, MODELS[args.model][5])
|
|
508
|
+
if args.fp16:
|
|
509
|
+
fp16(path, out_dir / f"{args.model}.fp16.onnx")
|
|
510
|
+
if args.verify:
|
|
511
|
+
s = session(out_dir / f"{args.model}.fp16.onnx")
|
|
512
|
+
verify(model, lambda spec: s.run(None, {"mix_spec": spec.numpy()})[0], f"{args.model}.fp16.onnx", 2e-2)
|
|
513
|
+
if args.engine:
|
|
514
|
+
engine(model, out_dir, args.model)
|
|
515
|
+
print(f"done: {args.model} in {out_dir}")
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
if __name__ == "__main__":
|
|
519
|
+
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,24 @@ 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
|
+
|
|
383
|
+
// anvuew's room de-reverb (a mono BS-RoFormer, GPL-3.0 weights) as scripts/export-roformer.py exports it: its STFT
|
|
384
|
+
// (Hann of 2048, hop 512, unnormalized), 8.7 s segments of 751 frames every 4.35 s, as Music-Source-Separation-Training's
|
|
385
|
+
// demix() runs it (num_overlap 2); each channel apart, nothing normalized. Its one stem is the voice dry; the residual
|
|
386
|
+
// is the room
|
|
387
|
+
const ROOM = {
|
|
388
|
+
modelType: 'complex', sampleRate: 44100, targets: ['dry'], sources: ['dry'], n: 2048, hop: 512, window: 'hann',
|
|
389
|
+
normalized: false, segment: 384000, frames: 751, step: 0.5, standardize: false, mono: true, gpu: true, script: 'export-roformer.py',
|
|
390
|
+
}
|
|
391
|
+
|
|
374
392
|
// The compact files (scripts/compact.py: int8 weights, a few float16, float32 compute), each within 0.05 dB of its
|
|
375
393
|
// export's median SDR or SNR per stem (README, Size); their Hugging Face repositories, and SHA-256 checked when fetched
|
|
376
394
|
const FILES = {
|
|
@@ -378,6 +396,10 @@ const FILES = {
|
|
|
378
396
|
scnet: { file: 'scnet.int8.onnx', repo: 'audiojs/scnet', sha256: '98228931494151762a1c4ab1ec7899a894b1f81fd4a509921a9d2f9bacc50845' },
|
|
379
397
|
mrx: { file: 'mrx.int8.onnx', repo: 'audiojs/mrx', sha256: 'd876c92d224f5d8cd2207d528b23369da895a6de71fb24132416fcb16f9f8424' },
|
|
380
398
|
tiger: { file: 'tiger.int8.onnx', repo: 'audiojs/tiger-dnr', sha256: '2189859ac7e89cf4d9d284c6f393e737aaa2b3233ed7ee424ad622012bce7bca' },
|
|
399
|
+
// float16 weights, float32 compute: half the download; int8 (259 MB) ran slower on WebGPU and 10 dB further from float32
|
|
400
|
+
'mel-roformer': { file: 'mel-roformer.fp16.onnx', repo: 'audiojs/mel-roformer', sha256: 'adcbbb826f12453fbb86652b503242d565a9c81cdef1bca2fe0e54752eba91ef' },
|
|
401
|
+
// float16 weights, float32 compute: 60 MB, its output 49 dB from the export's
|
|
402
|
+
'dereverb-room': { file: 'dereverb-room.fp16.onnx', repo: 'audiojs/dereverb-room', sha256: '99797e086a71d1cb57d50f3b5be93dcb7c89c49d1537c60487a488a24aaee9ee' },
|
|
381
403
|
}
|
|
382
404
|
|
|
383
405
|
// The hosted revision of each compact file's repository (a commit), its URL https://huggingface.co/<repo>/resolve/<revision>/
|
|
@@ -388,6 +410,8 @@ export const REVISIONS = {
|
|
|
388
410
|
scnet: 'e71bcdb42bce1d62b7fae25ba90fcbcf90f73f46',
|
|
389
411
|
mrx: 'dedc823a0bea2ed085d19e8b7f32589d35169ee8',
|
|
390
412
|
tiger: '1c959ab7ff8794dc5663b104ba301687ab95d2ab',
|
|
413
|
+
'mel-roformer': 'c1aba02c00a0bee852d6754fe6ede8164f90a4b3',
|
|
414
|
+
'dereverb-room': 'b4c2d14da9f2199099656195b06fd8253fd16e1d',
|
|
391
415
|
}
|
|
392
416
|
|
|
393
417
|
const hosted = name => models[name].repo && REVISIONS[name] ? `https://huggingface.co/${models[name].repo}/resolve/${REVISIONS[name]}/${models[name].file}` : null
|
|
@@ -406,6 +430,11 @@ export const models = {
|
|
|
406
430
|
mrx: { ...MRX, ...FILES.mrx },
|
|
407
431
|
// TIGER (JusperLee/TIGER, Apache-2.0 weights): the same three stems, three band-split models, about 50 times MRX's time
|
|
408
432
|
tiger: { ...TIGER, ...FILES.tiger },
|
|
433
|
+
// Mel-Band RoFormer (Kimberley Jensen's vocals model, MIT weights) as scripts/export-roformer.py exports it for the GPU
|
|
434
|
+
'mel-roformer': { ...MEL, ...FILES['mel-roformer'] },
|
|
435
|
+
// anvuew's room de-reverb (BS-RoFormer, GPL-3.0 weights) as scripts/export-roformer.py exports it: a voice in a room
|
|
436
|
+
// in, the voice dry out
|
|
437
|
+
'dereverb-room': { ...ROOM, ...FILES['dereverb-room'] },
|
|
409
438
|
}
|
|
410
439
|
|
|
411
440
|
async function cacheDir() {
|
|
@@ -452,6 +481,38 @@ async function local(graphs, name, opts) {
|
|
|
452
481
|
`, or pass opts.weights: a URL or directory holding ${name}/<file>.onnx`)
|
|
453
482
|
}
|
|
454
483
|
|
|
484
|
+
// A preset's own WebGPU engine (roformer.js) as a session, its tensors from `weights` or the hosted revision, or null
|
|
485
|
+
// where the GPU has no subgroup matrices (onnxruntime then): in the browser. Kept after a separation for KEEP ms, so the
|
|
486
|
+
// 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
|
|
487
|
+
const KEEP = 60_000
|
|
488
|
+
let kept = null
|
|
489
|
+
function engineSession(opts, p) {
|
|
490
|
+
let name = opts.model, key = `${name}:${opts.weights ?? ''}:${p.engine.sha256}`
|
|
491
|
+
if (kept?.key === key) { clearTimeout(kept.timer); return kept.session }
|
|
492
|
+
kept?.session.then(s => s?.drop())
|
|
493
|
+
let mine = { key, timer: null }
|
|
494
|
+
// free() after a separation lets go KEEP ms later, unless the next one has taken it
|
|
495
|
+
mine.session = makeEngine(opts, p, name).then(s => s && {
|
|
496
|
+
...s, drop: s.free,
|
|
497
|
+
free() { clearTimeout(mine.timer); mine.timer = setTimeout(() => { if (kept === mine) kept = null; s.free() }, KEEP) },
|
|
498
|
+
})
|
|
499
|
+
return (kept = mine).session
|
|
500
|
+
}
|
|
501
|
+
async function makeEngine(opts, p, name) {
|
|
502
|
+
let { engine, gpu } = await import('./roformer.js'), device = await gpu()
|
|
503
|
+
if (!device) return null
|
|
504
|
+
try {
|
|
505
|
+
let base = opts.weights != null ? `${await weightsBase(opts, name)}${name}/` : `https://huggingface.co/${p.repo}/resolve/${REVISIONS[name]}/`
|
|
506
|
+
let [meta, bytes] = await Promise.all([fetchJson(base + p.engine.meta), checked(base + p.engine.bin, p.engine.sha256)])
|
|
507
|
+
return await engine(meta, bytes, device)
|
|
508
|
+
} catch (e) {
|
|
509
|
+
// its files not there (weights without them), or the GPU refusing its buffers: onnxruntime, as without it
|
|
510
|
+
device.destroy()
|
|
511
|
+
console.warn(`neural-separate: ${name}'s own engine stands aside (${e.message})`)
|
|
512
|
+
return null
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
455
516
|
// The hosted compact file into the cache, checked against its SHA-256, written whole or not at all → its file URL
|
|
456
517
|
async function fetched(name, dest) {
|
|
457
518
|
let [fs, path, { pathToFileURL }] = await Promise.all([import('node:fs'), import('node:path'), import('node:url')])
|
|
@@ -503,12 +564,18 @@ async function resolveModel(opts) {
|
|
|
503
564
|
// No pooled arena: one htdemucs segment peaks at 2.7 GB RSS in onnxruntime-node instead of 3.2 GB.
|
|
504
565
|
const sessionOptions = { enableCpuMemArena: false, enableMemPattern: false }
|
|
505
566
|
|
|
506
|
-
// A graph's session; a fetched file with a SHA-256 checked against it first
|
|
507
|
-
|
|
567
|
+
// A graph's session; a fetched file with a SHA-256 checked against it first. A preset written for the GPU (`gpu`) runs
|
|
568
|
+
// on WebGPU where there is one (onnxruntime-web's in a browser, onnxruntime-node's in Node) unless `device` says
|
|
569
|
+
// otherwise: SCNet's LSTMs and the others' many small operators run no faster there
|
|
570
|
+
function loader(opts, preset) {
|
|
571
|
+
let device = opts.device ?? (preset?.gpu ? backends().then(b => b.includes('webgpu') ? 'webgpu' : undefined) : undefined)
|
|
572
|
+
let own = !opts.device && !opts.session && preset?.engine && !isNode ? engineSession(opts, preset) : null
|
|
508
573
|
return async g => {
|
|
509
574
|
if (opts.session) return opts.session(g.spec, opts)
|
|
575
|
+
let s = own && await own
|
|
576
|
+
if (s) return s
|
|
510
577
|
let spec = g.sha256 && !g.spec.startsWith('file:') ? await checked(g.spec, g.sha256) : g.spec
|
|
511
|
-
return neuralLoad(spec, { backend:
|
|
578
|
+
return neuralLoad(spec, { backend: await device, sessionOptions })
|
|
512
579
|
}
|
|
513
580
|
}
|
|
514
581
|
|
|
@@ -959,7 +1026,7 @@ export default async function separate(audio, opts = {}) {
|
|
|
959
1026
|
if (!channelData[0].length) return { stems: Object.fromEntries(targets.map(t => [t, empty()])), sampleRate: rate, residual: empty() }
|
|
960
1027
|
let targetRate = opts.targetRate ?? modelRate ?? rate
|
|
961
1028
|
let proc = targetRate !== rate ? channelData.map(c => resampleSinc(c, { from: rate, to: targetRate })) : channelData
|
|
962
|
-
let load = loader(opts)
|
|
1029
|
+
let load = loader(opts, preset)
|
|
963
1030
|
// the input at the loudness the model trained at, its stems scaled back (a silent input as it is)
|
|
964
1031
|
let level = preset?.loudness == null ? -Infinity : lufs(proc, targetRate), gain = Number.isFinite(level) ? 10 ** ((preset.loudness - level) / 20) : 1
|
|
965
1032
|
if (gain !== 1) proc = proc.map(c => c.map(v => v * gain))
|