Spaces:
Running
Running
File size: 20,854 Bytes
be60cf4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 | """Encoder-decoder Transformer: log-mel windows -> chart token sequences."""
import math
import torch
import torch.nn as nn
from .vocab import N_MELS, VOCAB, WINDOW
def sinusoidal(length, dim):
pos = torch.arange(length, dtype=torch.float32)[:, None]
i = torch.arange(dim // 2, dtype=torch.float32)[None]
angle = pos / torch.pow(10000.0, 2 * i / dim)
emb = torch.zeros(length, dim)
emb[:, 0::2] = torch.sin(angle)
emb[:, 1::2] = torch.cos(angle)
return emb
class BeatHiRes(nn.Module):
"""Frame-resolution beat/downbeat head: encoder states (T/4) are
upsampled 4x so supervision and peaks live on the 11.6 ms frame grid
instead of 46 ms encoder bins (4x finer timing per peak)."""
def __init__(self, d):
super().__init__()
self.up = nn.ConvTranspose1d(d, d // 2, 4, stride=4)
self.out = nn.Conv1d(d // 2, 2, 3, padding=1)
def forward(self, mem): # (B, L, d) -> (B, 4L, 2)
h = nn.functional.gelu(self.up(mem.transpose(1, 2)))
return self.out(h).transpose(1, 2)
def _parse_pattern(spec, n):
"""Layer-sharing schedule. spec is either an int (number of UNIQUE
physical layers, cycled to length n) or a comma string like "E0,E1,E0,E1"
(labels; each distinct label is one physical layer). Returns a list of n
physical-layer indices, e.g. [0,1,0,1] for 2-unique/4-physical-depth."""
if spec is None:
return list(range(n)) # no sharing: every layer unique
if isinstance(spec, int):
return [k % spec for k in range(n)]
labels = [s.strip() for s in str(spec).split(",") if s.strip()]
assert len(labels) == n, f"pattern {spec} has {len(labels)} labels, need {n}"
order = {}
out = []
for lb in labels:
if lb not in order:
order[lb] = len(order)
out.append(order[lb])
return out
class Adapter(nn.Module):
"""Rank-r residual adapter applied after a (possibly reused) transformer
block: x + gate * up(gelu(down(x))). gate init 0 so the whole stack starts
identical to hard weight sharing; the adapter only differentiates a reused
physical layer as training moves the gate off zero."""
def __init__(self, d, rank):
super().__init__()
self.down = nn.Linear(d, rank, bias=False)
self.up = nn.Linear(rank, d, bias=False)
nn.init.normal_(self.down.weight, std=0.02)
nn.init.zeros_(self.up.weight)
self.gate = nn.Parameter(torch.zeros(1))
def forward(self, x):
return x + self.gate * self.up(nn.functional.gelu(self.down(x)))
class SharedEncoder(nn.Module):
"""Encoder whose physical layers are reused according to a sharing pattern.
Each POSITION in the depth keeps its own (a) rank-r adapter and (b) final
LayerNorm — so ΔW starts at 0 (hard sharing) but every position can drift.
With pattern=None this is exactly n unique layers + a norm, matching the
stock nn.TransformerEncoder param count (adapters/pos-norms are opt-in).
v1.7 depth-diversity options (all default-off, init == v1.6 behaviour):
depth_emb: per-depth learned d-vector ADDED to the block input at each
reuse (params = depth x d, zero-init so init == hard sharing).
adapter_rank_ffn > 0: SPLIT adapters — instead of one post-block adapter,
each depth owns a rank-`adapter_rank` adapter on the attention sublayer
output and a rank-`adapter_rank_ffn` adapter on the FFN sublayer output
(patterning lives in the FFN). Gate init 0 keeps init == hard sharing.
"""
def __init__(self, d, nhead, ffn, dropout, pattern, adapter_rank=0,
unique_layernorm=False, adapter_rank_ffn=0, depth_emb=False):
super().__init__()
# `pattern` is the resolved list of physical-layer indices per depth
n_phys = max(pattern) + 1
self.layers = nn.ModuleList([
nn.TransformerEncoderLayer(d, nhead, ffn, dropout, activation="gelu",
batch_first=True, norm_first=True)
for _ in range(n_phys)])
self.plan = pattern
self.split = adapter_rank_ffn > 0
if self.split: # v1.7: per-sublayer adapters (attention rank-a, FFN rank-f)
self.attn_adapters = nn.ModuleList([
Adapter(d, adapter_rank) if adapter_rank else nn.Identity()
for _ in pattern])
self.ffn_adapters = nn.ModuleList([
Adapter(d, adapter_rank_ffn) for _ in pattern])
else: # v1.6: one residual adapter after the whole block
self.adapters = nn.ModuleList([
Adapter(d, adapter_rank) if adapter_rank else nn.Identity()
for _ in pattern])
self.depth_emb = (nn.Parameter(torch.zeros(len(pattern), d))
if depth_emb else None)
self.pos_norms = nn.ModuleList([
nn.LayerNorm(d) if unique_layernorm else nn.Identity()
for _ in pattern])
self.norm = nn.LayerNorm(d)
def forward(self, x, src_key_padding_mask=None):
for k, phys in enumerate(self.plan):
if self.depth_emb is not None:
x = x + self.depth_emb[k]
layer = self.layers[phys]
if self.split:
# norm_first decomposition of nn.TransformerEncoderLayer with
# per-depth adapters applied to each sublayer OUTPUT (Houlsby
# placement); at gate=0 this is bit-equal to the stock layer
x = x + self.attn_adapters[k](
layer._sa_block(layer.norm1(x), None, src_key_padding_mask))
x = x + self.ffn_adapters[k](layer._ff_block(layer.norm2(x)))
else:
x = layer(x, src_key_padding_mask=src_key_padding_mask)
x = self.adapters[k](x)
x = self.pos_norms[k](x)
return self.norm(x)
class SharedDecoder(nn.Module):
"""Decoder counterpart of SharedEncoder (see that docstring). Split mode
puts rank-`adapter_rank` adapters on BOTH attention sublayers (self and
cross) and rank-`adapter_rank_ffn` on the FFN sublayer."""
def __init__(self, d, nhead, ffn, dropout, pattern, adapter_rank=0,
unique_layernorm=False, adapter_rank_ffn=0, depth_emb=False):
super().__init__()
n_phys = max(pattern) + 1
self.layers = nn.ModuleList([
nn.TransformerDecoderLayer(d, nhead, ffn, dropout, activation="gelu",
batch_first=True, norm_first=True)
for _ in range(n_phys)])
self.plan = pattern
self.split = adapter_rank_ffn > 0
if self.split:
self.sa_adapters = nn.ModuleList([
Adapter(d, adapter_rank) if adapter_rank else nn.Identity()
for _ in pattern])
self.ca_adapters = nn.ModuleList([
Adapter(d, adapter_rank) if adapter_rank else nn.Identity()
for _ in pattern])
self.ffn_adapters = nn.ModuleList([
Adapter(d, adapter_rank_ffn) for _ in pattern])
else:
self.adapters = nn.ModuleList([
Adapter(d, adapter_rank) if adapter_rank else nn.Identity()
for _ in pattern])
self.depth_emb = (nn.Parameter(torch.zeros(len(pattern), d))
if depth_emb else None)
self.pos_norms = nn.ModuleList([
nn.LayerNorm(d) if unique_layernorm else nn.Identity()
for _ in pattern])
self.norm = nn.LayerNorm(d)
def forward(self, tgt, memory, tgt_mask=None, tgt_key_padding_mask=None,
tgt_is_causal=False):
x = tgt
for k, phys in enumerate(self.plan):
if self.depth_emb is not None:
x = x + self.depth_emb[k]
layer = self.layers[phys]
if self.split:
x = x + self.sa_adapters[k](layer._sa_block(
layer.norm1(x), tgt_mask, tgt_key_padding_mask, tgt_is_causal))
x = x + self.ca_adapters[k](layer._mha_block(
layer.norm2(x), memory, None, None))
x = x + self.ffn_adapters[k](layer._ff_block(layer.norm3(x)))
else:
x = layer(x, memory, tgt_mask=tgt_mask,
tgt_key_padding_mask=tgt_key_padding_mask,
tgt_is_causal=tgt_is_causal)
x = self.adapters[k](x)
x = self.pos_norms[k](x)
return self.norm(x)
class ChartModel(nn.Module):
def __init__(
self,
d_model=512,
nhead=8,
enc_layers=6,
dec_layers=6,
ffn=2048,
dropout=0.1,
vocab_size=None,
max_tgt=None,
aux=False,
global_ctx=False,
func_time=False,
ptr=False,
beat=False,
in_ch=None,
emb_factor=None,
clean_phase=False,
enc_share=None,
dec_share=None,
adapter_rank=0,
unique_layernorm=False,
adapter_rank_ffn=0,
depth_emb=False,
unshare_last_dec=False,
):
super().__init__()
from .vocab import MAX_TGT
vocab_size = vocab_size or VOCAB.size
max_tgt = max_tgt or MAX_TGT
self.d_model = d_model
# clean_phase (v1.6): the encoder never sees the 2 grid-phase channels
# (in_ch stays N_MELS), and phase is injected into the DECODER memory
# only, via a small projection. This removes the condition leak that
# forced beat supervision to be masked on slot windows in v1.5.
self.clean_phase = clean_phase
self.in_ch = in_ch or N_MELS # legacy slot mode appends 2 grid-phase channels
front_ch = N_MELS if clean_phase else self.in_ch
# conv frontend: (B, front_ch, T) -> (B, T/4, d)
self.frontend = nn.Sequential(
nn.Conv1d(front_ch, d_model, 3, stride=2, padding=1),
nn.GELU(),
nn.Conv1d(d_model, d_model, 3, stride=2, padding=1),
nn.GELU(),
)
enc_len = WINDOW // 4
self.register_buffer("enc_pos", sinusoidal(enc_len, d_model), persistent=False)
self.register_buffer("dec_pos", sinusoidal(max_tgt, d_model), persistent=False)
# phase injection (clean_phase only): 2 grid-phase channels at frame
# rate -> encoder rate (T/4) -> additive conditioning on the memory the
# DECODER reads. The beat head reads the pre-injection (clean) memory.
# phase_proj input = [measure_phase, beat_phase, valid_flag]. valid=0 on
# time-mode windows (their raw phase is -1), giving the decoder a clean
# zero-phase conditioning there; slot windows pass valid=1 + real phase.
if clean_phase:
self.phase_proj = nn.Sequential(
nn.Conv1d(3, d_model, 3, stride=2, padding=1),
nn.GELU(),
nn.Conv1d(d_model, d_model, 3, stride=2, padding=1),
)
else:
self.phase_proj = None
# sharing schedule (v1.6-small/tiny): reuse physical layers per pattern.
# enc_share/dec_share: int (#unique layers) or "E0,E1,E0,E1" label string.
self.enc_share = enc_share
self.dec_share = dec_share
if (enc_share is None and dec_share is None and not adapter_rank
and not adapter_rank_ffn and not depth_emb and not unshare_last_dec):
enc_layer = nn.TransformerEncoderLayer(
d_model, nhead, ffn, dropout, activation="gelu",
batch_first=True, norm_first=True)
self.encoder = nn.TransformerEncoder(enc_layer, enc_layers,
nn.LayerNorm(d_model))
dec_layer = nn.TransformerDecoderLayer(
d_model, nhead, ffn, dropout, activation="gelu",
batch_first=True, norm_first=True)
self.decoder = nn.TransformerDecoder(dec_layer, dec_layers,
nn.LayerNorm(d_model))
else:
enc_plan = _parse_pattern(enc_share, enc_layers)
dec_plan = _parse_pattern(dec_share, dec_layers)
# v1.7: un-share the LAST decoder layer (pattern-shaping layer in
# front of the generation head gets its own physical parameters);
# no-op if that depth is already unique.
if unshare_last_dec and dec_plan.count(dec_plan[-1]) > 1:
dec_plan = dec_plan[:-1] + [max(dec_plan) + 1]
self.encoder = SharedEncoder(d_model, nhead, ffn, dropout, enc_plan,
adapter_rank, unique_layernorm,
adapter_rank_ffn, depth_emb)
self.decoder = SharedDecoder(d_model, nhead, ffn, dropout, dec_plan,
adapter_rank, unique_layernorm,
adapter_rank_ffn, depth_emb)
# ALBERT-style factorized embeddings (v1.5): vocab->E->d. Position
# tokens dominate the table (1728/1810 rows), so E=64 cuts the
# embedding block ~3.5x; logits stay tied via the projected matrix.
self.emb_factor = emb_factor
if emb_factor:
self.tok_emb = nn.Embedding(vocab_size, emb_factor, padding_idx=VOCAB.pad)
nn.init.normal_(self.tok_emb.weight, std=0.02)
self.emb_proj = nn.Linear(emb_factor, d_model, bias=False)
nn.init.normal_(self.emb_proj.weight, std=1.0 / (emb_factor ** 0.5) * 0.16)
self.out = None
else:
self.tok_emb = nn.Embedding(vocab_size, d_model, padding_idx=VOCAB.pad)
nn.init.normal_(self.tok_emb.weight, std=0.02) # keep tied logits well-scaled
self.emb_proj = None
self.out = nn.Linear(d_model, vocab_size, bias=False)
self.out.weight = self.tok_emb.weight # weight tying
self.dropout = nn.Dropout(dropout)
# auxiliary per-position onset-heatmap head on the encoder (v2)
self.aux = nn.Linear(d_model, 1) if aux else None
# pointer alignment head: each generated note must point to its audio
# frame (differentiable provenance; explainability that trains alignment)
self.ptr = nn.Linear(d_model, d_model) if ptr else None
# beat/downbeat head: explicit metrical percept on the encoder.
# beat="hires" builds the frame-resolution head (11.6 ms bins)
self.beat = (BeatHiRes(d_model) if beat == "hires"
else (nn.Linear(d_model, 2) if beat else None))
# song-level context: coarse whole-song summary + window position (v4).
# Lets the model see beyond the window, so intentional gaps (a breath
# before a strong section) are informed decisions, not failures.
# v2 fix (after the v4 negative result): summary chunks get DEDICATED
# positional embeddings instead of reusing window positions.
if global_ctx:
self.gsum_proj = nn.Linear(N_MELS, d_model)
self.seg_emb = nn.Parameter(torch.zeros(2, d_model))
self.pos_emb = nn.Embedding(16, d_model) # window position bucket
self.gpos = nn.Parameter(torch.randn(128, d_model) * 0.02)
else:
self.gsum_proj = None
# functional time embeddings: the 1728 TIME tokens share a sinusoidal
# basis + small projection instead of free embeddings (fewer params,
# neighbouring times get similar representations)
if func_time:
self.register_buffer("time_basis", sinusoidal(WINDOW, 64), persistent=False)
self.time_proj = nn.Linear(64, d_model)
# match the 0.02-std scale of tok_emb rows (basis row norm ~ sqrt(32))
nn.init.normal_(self.time_proj.weight, std=0.004)
nn.init.zeros_(self.time_proj.bias)
else:
self.time_proj = None
def encode(self, mel, gsum=None, pos_bucket=None):
# mel: (B, C, T). clean_phase: C may be N_MELS (audio only) or N_MELS+2
# (audio + 2 grid-phase channels); only the audio channels reach the
# encoder, so the returned memory is CLEAN (phase-free) and safe for the
# beat head. gsum: (B, G, n_mels) whole-song summary chunks;
# pos_bucket: (B,) window-position bucket in [0, 16).
audio = mel[:, :N_MELS] if self.clean_phase else mel
h = self.frontend(audio).transpose(1, 2) # (B, T/4, d)
h = h + self.enc_pos[: h.shape[1]]
if self.gsum_proj is not None and gsum is not None:
g = self.gsum_proj(gsum) + self.seg_emb[1] + self.gpos[: gsum.shape[1]]
h = h + self.seg_emb[0]
p = self.pos_emb(pos_bucket).unsqueeze(1) # (B, 1, d)
h = torch.cat([p, g, h], dim=1)
return self.encoder(self.dropout(h))
def phase_mem(self, memory, mel):
"""Phase-conditioned copy of `memory` for the DECODER to cross-attend.
Adds a projection of the grid-phase channels to the audio-frame slice
of memory; the beat head keeps reading the clean `memory`. No-op unless
clean_phase is on and phase channels are present. Time-mode windows
(raw phase = -1) get valid=0 and zeroed phase -> ~zero conditioning."""
nch = N_MELS
if not self.clean_phase or mel.shape[1] < nch + 2:
return memory
ph = mel[:, nch:nch + 2] # (B, 2, T) in [0,1); -1 marks no-grid frames
valid = (ph[:, :1] >= 0).float() # 1 where a grid exists, else 0
ph_in = torch.cat([ph.clamp(min=0.0), valid], dim=1) # (B, 3, T)
p = self.phase_proj(ph_in).transpose(1, 2) # (B, T/4, d)
L = p.shape[1]
out = memory.clone()
out[:, -L:] = out[:, -L:] + p # audio frames are the last L memory slots
return out
def emb_matrix(self):
"""Full embedding matrix; factorized and/or functional rows resolved."""
E = self.tok_emb.weight
if self.emb_proj is not None:
E = self.emb_proj(E)
if self.time_proj is not None:
t = self.time_proj(self.time_basis) # (WINDOW, d)
E = torch.cat([E[: VOCAB.time0], t, E[VOCAB.time0 + WINDOW :]], dim=0)
return E
def decode(self, tgt_in, memory):
# tgt_in: (B, L) token ids
L = tgt_in.shape[1]
E = self.emb_matrix()
h = nn.functional.embedding(tgt_in, E) * math.sqrt(self.d_model) + self.dec_pos[:L]
mask = nn.Transformer.generate_square_subsequent_mask(L, device=tgt_in.device)
pad_mask = tgt_in == VOCAB.pad
h = self.decoder(
self.dropout(h),
memory,
tgt_mask=mask,
tgt_key_padding_mask=pad_mask,
tgt_is_causal=True,
)
return h @ E.T # tied output projection (functional rows included)
def enable_ptr(self):
self.ptr = nn.Linear(self.d_model, self.d_model)
def enable_beat(self, hires=False):
self.beat = BeatHiRes(self.d_model) if hires else nn.Linear(self.d_model, 2)
def forward(self, mel, tgt, return_aux=False, gsum=None, pos_bucket=None,
return_extras=False, in_tgt=None):
# in_tgt: optional corrupted decoder input (e.g. MASKed types for the
# skeleton->color infill curriculum); gold targets stay = tgt
memory = self.encode(mel, gsum=gsum, pos_bucket=pos_bucket) # CLEAN
dec_mem = self.phase_mem(memory, mel) # phase-conditioned for the decoder
dec_in = (in_tgt if in_tgt is not None else tgt)[:, :-1]
L = dec_in.shape[1]
E = self.emb_matrix()
import math as _m
h = nn.functional.embedding(dec_in, E) * _m.sqrt(self.d_model) + self.dec_pos[:L]
dm = nn.Transformer.generate_square_subsequent_mask(L, device=tgt.device)
h = self.decoder(self.dropout(h), dec_mem, tgt_mask=dm,
tgt_key_padding_mask=dec_in == VOCAB.pad, tgt_is_causal=True)
logits = h @ E.T
if not (return_aux or return_extras):
return logits
outs = [logits]
# aux/ptr/beat heads read the CLEAN memory (no phase leak)
aux_mem = memory[:, -(WINDOW // 4):]
outs.append(self.aux(aux_mem).squeeze(-1) if self.aux is not None else None)
if return_extras:
ptr_logits = None
if self.ptr is not None:
q = self.ptr(h) # (B, L, d)
ptr_logits = q @ aux_mem.transpose(1, 2) / _m.sqrt(self.d_model)
beat_logits = self.beat(aux_mem) if self.beat is not None else None
outs += [ptr_logits, beat_logits]
return tuple(outs)
def count_params(m):
return sum(p.numel() for p in m.parameters() if p.requires_grad)
|