@audio/neural-separate 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +5 -3
- package/README.md +94 -10
- package/index.d.ts +11 -2
- package/package.json +6 -2
- package/roformer.js +396 -0
- package/scripts/compact.py +351 -0
- package/scripts/engine.mjs +58 -0
- package/scripts/export-roformer.py +481 -0
- package/scripts/export-scnet.py +6 -6
- package/scripts/reference.py +5 -2
- package/separate.js +144 -31
|
@@ -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/export-scnet.py
CHANGED
|
@@ -25,7 +25,7 @@ The STFT is SCNet's own: n_fft 4096, hop 1024, no window (torch.stft's default,
|
|
|
25
25
|
ones), normalized (1/sqrt(4096)), centered with reflect padding.
|
|
26
26
|
|
|
27
27
|
Writes <out-dir>/<model>.onnx (scnet-large 169 MB, scnet 43 MB) and, with --fp16,
|
|
28
|
-
<model>.fp16.onnx (half that), weights embedded. Default out-dir:
|
|
28
|
+
<model>.fp16.onnx (half that: float16 weights, float32 compute, scripts/compact.py's), weights embedded. Default out-dir:
|
|
29
29
|
$AUDIO_NEURAL_CACHE/<model>, else ~/.cache/audiojs/neural/<model>. --verify compares the stems rebuilt from the ONNX
|
|
30
30
|
output with SCNet.forward on the same segment, and writes the reference
|
|
31
31
|
separation test.js compares the JS pipeline against (scripts/reference.py),
|
|
@@ -187,11 +187,11 @@ def export(model, path, opset):
|
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
def fp16(path, out):
|
|
190
|
-
"""float16 weights, float32
|
|
191
|
-
|
|
192
|
-
|
|
190
|
+
"""float16 weights, float32 compute (scripts/compact.py --as fp16)."""
|
|
191
|
+
sys.path.insert(0, str(Path(__file__).parent))
|
|
192
|
+
import compact
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
compact.compact(path, out, "fp16")
|
|
195
195
|
print(f"wrote {out} ({out.stat().st_size / 1e6:.1f} MB)")
|
|
196
196
|
|
|
197
197
|
|
|
@@ -245,7 +245,7 @@ def main():
|
|
|
245
245
|
ap.add_argument("--model", default="scnet-large", choices=list(MODELS))
|
|
246
246
|
ap.add_argument("--out-dir", default=None, help="default: $AUDIO_NEURAL_CACHE/<model> or ~/.cache/audiojs/neural/<model>")
|
|
247
247
|
ap.add_argument("--opset", type=int, default=17)
|
|
248
|
-
ap.add_argument("--fp16", action="store_true", help="also write <model>.fp16.onnx (float16 weights, float32
|
|
248
|
+
ap.add_argument("--fp16", action="store_true", help="also write <model>.fp16.onnx (float16 weights, float32 compute)")
|
|
249
249
|
ap.add_argument("--verify", action="store_true", help="compare with SCNet.forward; write the reference separation")
|
|
250
250
|
args = ap.parse_args()
|
|
251
251
|
|
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")
|