EvoTalk / model.py
akkiisfrommars's picture
Upload model.py with huggingface_hub
1acbb15 verified
Raw
History Blame Contribute Delete
20.5 kB
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
# Minimum frames per phoneme. Kept identical in prepare.py (target integerization)
# and here (inference rounding + length regulator) so training and inference agree.
MIN_DUR = 1
class EvoTalkConfig:
def __init__(self,
phoneme_vocab_size=42,
n_mels=100,
max_seq_len=1024,
max_mel_len=2048,
n_encoder_layers=6,
n_decoder_layers=6,
n_head=10,
n_embd=640,
n_query_groups=2,
bias=True,
dropout=0.1,
eps=1e-6,
use_rotary=True,
use_swiglu=True,
use_qk_norm=False,
use_gqa=True,
n_speakers=1,
speaker_emb_dim=256):
self.phoneme_vocab_size = phoneme_vocab_size
self.n_mels = n_mels
self.max_seq_len = max_seq_len
self.max_mel_len = max_mel_len
self.n_encoder_layers = n_encoder_layers
self.n_decoder_layers = n_decoder_layers
self.n_head = n_head
self.n_embd = n_embd
self.n_query_groups = n_query_groups if use_gqa else n_head
self.bias = bias
self.dropout = dropout
self.eps = eps
self.use_rotary = use_rotary
self.use_swiglu = use_swiglu
self.use_qk_norm = use_qk_norm
self.use_gqa = use_gqa
self.n_speakers = n_speakers
self.speaker_emb_dim = speaker_emb_dim
assert n_head % self.n_query_groups == 0, "n_head must be divisible by n_query_groups"
class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
rms = torch.sqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps)
return self.weight * (x / rms)
def precompute_freqs_cis(dim, end, theta=10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].float() / dim))
t = torch.arange(end, device=freqs.device)
freqs = torch.outer(t, freqs)
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
return freqs_cis
def apply_rotary_emb(xq, xk, freqs_cis):
xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
seq_len = xq_.size(2)
freqs_cis_seq = freqs_cis[:seq_len]
xq_out = torch.view_as_real(xq_ * freqs_cis_seq.unsqueeze(0)).flatten(3)
xk_out = torch.view_as_real(xk_ * freqs_cis_seq.unsqueeze(0)).flatten(3)
return xq_out.type_as(xq), xk_out.type_as(xk)
class GroupedQueryAttention(nn.Module):
def __init__(self, config, causal=False):
super().__init__()
assert config.n_embd % config.n_head == 0
self.head_dim = config.n_embd // config.n_head
self.n_head = config.n_head
self.n_embd = config.n_embd
self.n_query_groups = config.n_query_groups
self.causal = causal
self.kv_heads = config.n_head // config.n_query_groups if config.use_gqa else config.n_head
qkv_proj_size = (config.n_head + 2 * self.kv_heads) * self.head_dim
self.c_attn = nn.Linear(config.n_embd, qkv_proj_size, bias=config.bias)
self.c_proj = nn.Linear(config.n_embd, config.n_embd, bias=config.bias)
self.attn_dropout = nn.Dropout(config.dropout)
self.resid_dropout = nn.Dropout(config.dropout)
self.dropout = config.dropout
self.flash = hasattr(torch.nn.functional, "scaled_dot_product_attention")
self.qk_norm = getattr(config, "use_qk_norm", False)
if self.qk_norm:
self.q_norm = RMSNorm(self.head_dim, eps=config.eps)
self.k_norm = RMSNorm(self.head_dim, eps=config.eps)
def forward(self, x, freqs_cis=None, key_padding_mask=None):
B, T, C = x.size()
qkv = self.c_attn(x)
q_size = self.n_head * self.head_dim
k_size = self.kv_heads * self.head_dim
v_size = self.kv_heads * self.head_dim
q, k, v = qkv.split([q_size, k_size, v_size], dim=2)
q = q.view(B, T, self.n_head, self.head_dim).transpose(1, 2)
k = k.view(B, T, self.kv_heads, self.head_dim).transpose(1, 2)
v = v.view(B, T, self.kv_heads, self.head_dim).transpose(1, 2)
if self.kv_heads < self.n_head:
repeats = self.n_head // self.kv_heads
k = k.repeat_interleave(repeats, dim=1)
v = v.repeat_interleave(repeats, dim=1)
if freqs_cis is not None:
q, k = apply_rotary_emb(q, k, freqs_cis)
if self.qk_norm:
q = self.q_norm(q)
k = self.k_norm(k)
# key_padding_mask: (B, T) with True marking padded key positions.
# Broadcast as (B, 1, 1, T) so it masks those keys for every query head/position,
# which avoids materialising a full (B, 1, T, T) mask.
if self.flash:
attn_mask = None
if key_padding_mask is not None:
attn_mask = torch.zeros(B, 1, 1, T, dtype=q.dtype, device=x.device)
attn_mask = attn_mask.masked_fill(
key_padding_mask[:, None, None, :], float("-inf")
)
y = torch.nn.functional.scaled_dot_product_attention(
q, k, v,
attn_mask=attn_mask,
dropout_p=self.dropout if self.training else 0,
is_causal=self.causal,
)
else:
att = (q @ k.transpose(-2, -1)) * (1.0 / math.sqrt(k.size(-1)))
if key_padding_mask is not None:
att = att.masked_fill(key_padding_mask[:, None, None, :], float("-inf"))
att = F.softmax(att, dim=-1)
att = self.attn_dropout(att)
y = att @ v
y = y.transpose(1, 2).contiguous().view(B, T, C)
y = self.resid_dropout(self.c_proj(y))
return y
class Block(nn.Module):
def __init__(self, config, causal=False):
super().__init__()
self.ln_1 = RMSNorm(config.n_embd, eps=config.eps)
self.ln_2 = RMSNorm(config.n_embd, eps=config.eps)
self.attn = GroupedQueryAttention(config, causal=causal)
if config.use_swiglu:
self.mlp = nn.ModuleDict(dict(
gate=nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias),
up=nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias),
down=nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias),
act=nn.SiLU(),
dropout=nn.Dropout(config.dropout),
))
m = self.mlp
self.mlpf = lambda x: m.dropout(m.down(m.act(m.gate(x)) * m.up(x)))
else:
self.mlp = nn.ModuleDict(dict(
c_fc=nn.Linear(config.n_embd, 4 * config.n_embd, bias=config.bias),
c_proj=nn.Linear(4 * config.n_embd, config.n_embd, bias=config.bias),
act=nn.GELU(),
dropout=nn.Dropout(config.dropout),
))
m = self.mlp
self.mlpf = lambda x: m.dropout(m.c_proj(m.act(m.c_fc(x))))
def forward(self, x, freqs_cis=None, key_padding_mask=None):
x = x + self.attn(self.ln_1(x), freqs_cis, key_padding_mask)
x = x + self.mlpf(self.ln_2(x))
return x
class DurationPredictor(nn.Module):
def __init__(self, config):
super().__init__()
self.conv1 = nn.Conv1d(config.n_embd, config.n_embd, kernel_size=3, padding=1)
self.norm1 = RMSNorm(config.n_embd, eps=config.eps)
self.conv2 = nn.Conv1d(config.n_embd, config.n_embd, kernel_size=3, padding=1)
self.norm2 = RMSNorm(config.n_embd, eps=config.eps)
self.linear = nn.Linear(config.n_embd, 1)
self.dropout = nn.Dropout(config.dropout)
def forward(self, x):
x = x.transpose(1, 2)
x = self.dropout(F.relu(self.norm1(self.conv1(x).transpose(1, 2)).transpose(1, 2)))
x = self.dropout(F.relu(self.norm2(self.conv2(x).transpose(1, 2)).transpose(1, 2)))
x = x.transpose(1, 2)
return self.linear(x).squeeze(-1)
class VariancePredictor(nn.Module):
def __init__(self, config):
super().__init__()
self.conv1 = nn.Conv1d(config.n_embd, config.n_embd, kernel_size=3, padding=1)
self.norm1 = RMSNorm(config.n_embd, eps=config.eps)
self.conv2 = nn.Conv1d(config.n_embd, config.n_embd, kernel_size=3, padding=1)
self.norm2 = RMSNorm(config.n_embd, eps=config.eps)
self.linear = nn.Linear(config.n_embd, 1)
self.dropout = nn.Dropout(config.dropout)
def forward(self, x):
x = x.transpose(1, 2)
x = self.dropout(F.relu(self.norm1(self.conv1(x).transpose(1, 2)).transpose(1, 2)))
x = self.dropout(F.relu(self.norm2(self.conv2(x).transpose(1, 2)).transpose(1, 2)))
x = x.transpose(1, 2)
return self.linear(x).squeeze(-1)
class LengthRegulator(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x, durations, max_len=None):
outputs = []
for i in range(x.size(0)):
output = torch.repeat_interleave(x[i], durations[i].long(), dim=0)
outputs.append(output)
if max_len is None:
max_len = max(o.size(0) for o in outputs)
padded = torch.zeros(x.size(0), max_len, x.size(2), device=x.device, dtype=x.dtype)
for i, o in enumerate(outputs):
length = min(o.size(0), max_len)
padded[i, :length] = o[:length]
return padded
def inference(self, x, durations, min_duration=MIN_DUR):
durations = torch.clamp(durations.long(), min=min_duration)
outputs = []
for i in range(x.size(0)):
output = torch.repeat_interleave(x[i], durations[i], dim=0)
outputs.append(output)
max_len = max(o.size(0) for o in outputs)
padded = torch.zeros(x.size(0), max_len, x.size(2), device=x.device, dtype=x.dtype)
for i, o in enumerate(outputs):
padded[i, :o.size(0)] = o
return padded
class VarianceAdaptor(nn.Module):
def __init__(self, config):
super().__init__()
self.duration_predictor = DurationPredictor(config)
self.length_regulator = LengthRegulator()
self.pitch_predictor = VariancePredictor(config)
self.energy_predictor = VariancePredictor(config)
self.pitch_embedding = nn.Linear(1, config.n_embd)
self.energy_embedding = nn.Linear(1, config.n_embd)
def forward(self, x, durations=None, pitch_targets=None, energy_targets=None,
max_mel_len=None, duration_scale=1.0, embed_predicted=False):
# x: (B, T_phon, D) encoder output, one vector per phoneme.
#
# PHONEME-LEVEL variance (canonical FastSpeech2). Duration, pitch, and
# energy are all predicted from the unexpanded encoder output — one value
# per phoneme — and the pitch/energy embeddings are added BEFORE length
# regulation. This is deliberate: a per-phoneme average of energy/pitch
# carries far less information about the exact target mel than a per-frame
# value (per-frame energy is essentially mel magnitude), so the decoder
# can't lean on a leaked copy of its own target and then collapse when the
# predictor's inference outputs differ.
duration_preds = self.duration_predictor(x) # (B, T_phon) log-domain
pitch_preds = self.pitch_predictor(x) # (B, T_phon)
energy_preds = self.energy_predictor(x) # (B, T_phon)
# Choose the prosody actually fed to the decoder. Teacher forcing uses the
# ground-truth (phoneme-level) targets; otherwise the predictor's own
# (detached) outputs — detach keeps mel-loss gradients out of the
# predictors so they stay honestly supervised by their own loss.
tf_pitch = (pitch_targets is not None) and (not embed_predicted)
tf_energy = (energy_targets is not None) and (not embed_predicted)
pitch_used = pitch_targets if tf_pitch else pitch_preds.detach()
energy_used = energy_targets if tf_energy else energy_preds.detach()
# Targets are padded to the batch's max phoneme length; align to x.
pitch_used = pitch_used[:, :x.size(1)]
energy_used = energy_used[:, :x.size(1)]
x = x + self.pitch_embedding(pitch_used.unsqueeze(-1)) \
+ self.energy_embedding(energy_used.unsqueeze(-1))
# Expand the enriched phoneme representation to frame level.
if durations is not None:
dur_clamped = torch.clamp(durations.long(), min=MIN_DUR)
x = self.length_regulator(x, dur_clamped, max_len=max_mel_len)
else:
dur_rounded = torch.clamp(
torch.round((torch.exp(duration_preds) - 1) * duration_scale), min=MIN_DUR
).long()
x = self.length_regulator.inference(x, dur_rounded)
return x, duration_preds, pitch_preds, energy_preds
class EvoTalk(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.phoneme_emb = nn.Embedding(config.phoneme_vocab_size, config.n_embd)
self.speaker_emb = nn.Embedding(config.n_speakers, config.speaker_emb_dim)
self.speaker_proj = nn.Linear(config.speaker_emb_dim, config.n_embd)
self.encoder_drop = nn.Dropout(config.dropout)
self.decoder_drop = nn.Dropout(config.dropout)
if config.use_rotary:
head_dim = config.n_embd // config.n_head
enc_len = max(config.max_seq_len, config.max_mel_len)
self.freqs_cis = precompute_freqs_cis(head_dim, enc_len)
else:
self.freqs_cis = None
self.encoder_pos = nn.Embedding(config.max_seq_len, config.n_embd)
self.decoder_pos = nn.Embedding(config.max_mel_len, config.n_embd)
self.encoder = nn.ModuleList([Block(config, causal=False) for _ in range(config.n_encoder_layers)])
self.encoder_norm = RMSNorm(config.n_embd, eps=config.eps)
self.variance_adaptor = VarianceAdaptor(config)
self.decoder = nn.ModuleList([Block(config, causal=False) for _ in range(config.n_decoder_layers)])
self.decoder_norm = RMSNorm(config.n_embd, eps=config.eps)
self.mel_head = nn.Linear(config.n_embd, config.n_mels)
self.apply(self._init_weights)
for pn, p in self.named_parameters():
if pn.endswith("c_proj.weight") or pn.endswith("down.weight"):
torch.nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * (config.n_encoder_layers + config.n_decoder_layers)))
print(f"EvoTalk parameters: {self.get_num_params() / 1e6:.2f}M")
def get_num_params(self):
return sum(p.numel() for p in self.parameters())
def _init_weights(self, module):
if isinstance(module, nn.Linear):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
if module.bias is not None:
torch.nn.init.zeros_(module.bias)
elif isinstance(module, nn.Embedding):
torch.nn.init.normal_(module.weight, mean=0.0, std=0.02)
elif isinstance(module, RMSNorm):
torch.nn.init.ones_(module.weight)
def _make_mel_padding_mask(self, durations, T_mel, device):
# True marks padded frames (frames past the sum of the sample's durations).
dur_clamped = torch.clamp(durations.long(), min=MIN_DUR)
valid_len = dur_clamped.sum(dim=1).clamp(max=T_mel) # (B,)
positions = torch.arange(T_mel, device=device).unsqueeze(0) # (1, T)
return positions >= valid_len.unsqueeze(1) # (B, T) bool
def encode(self, phonemes, speaker_ids, src_mask=None):
device = phonemes.device
B, T = phonemes.size()
x = self.phoneme_emb(phonemes)
spk = self.speaker_proj(self.speaker_emb(speaker_ids))
x = x + spk.unsqueeze(1)
if self.config.use_rotary:
freqs_cis = self.freqs_cis.to(device)
else:
pos = torch.arange(0, T, dtype=torch.long, device=device).unsqueeze(0)
x = x + self.encoder_pos(pos)
freqs_cis = None
x = self.encoder_drop(x)
for block in self.encoder:
x = block(x, freqs_cis, key_padding_mask=src_mask)
x = self.encoder_norm(x)
return x
def forward(self, phonemes, speaker_ids, durations=None, pitch_targets=None, energy_targets=None,
mel_targets=None, src_mask=None, max_mel_len=None, embed_predicted=False):
device = phonemes.device
x = self.encode(phonemes, speaker_ids, src_mask)
x, duration_preds, pitch_preds, energy_preds = self.variance_adaptor(
x,
durations=durations,
pitch_targets=pitch_targets,
energy_targets=energy_targets,
max_mel_len=max_mel_len,
embed_predicted=embed_predicted,
)
B, T_mel, _ = x.size()
# Build a decoder padding mask so padded (silence) frames do not leak into
# real frames through the non-causal decoder self-attention.
mel_mask = None
if durations is not None:
mel_mask = self._make_mel_padding_mask(durations, T_mel, device)
if self.config.use_rotary:
freqs_cis = self.freqs_cis.to(device)
else:
pos = torch.arange(0, T_mel, dtype=torch.long, device=device).unsqueeze(0)
x = x + self.decoder_pos(pos)
freqs_cis = None
x = self.decoder_drop(x)
for block in self.decoder:
x = block(x, freqs_cis, key_padding_mask=mel_mask)
x = self.decoder_norm(x)
mel_out = self.mel_head(x)
# The training loss (masked) is computed in train.py. We return mel_mask so
# callers can reuse the exact valid-frame region if needed.
return mel_out, duration_preds, pitch_preds, energy_preds, mel_mask
@torch.no_grad()
def inference(self, phonemes, speaker_ids, src_mask=None, duration_scale=1.0):
x = self.encode(phonemes, speaker_ids, src_mask)
x, _, _, _ = self.variance_adaptor(x, duration_scale=duration_scale)
device = phonemes.device
B, T_mel, _ = x.size()
if self.config.use_rotary:
freqs_cis = self.freqs_cis.to(device)
else:
pos = torch.arange(0, T_mel, dtype=torch.long, device=device).unsqueeze(0)
x = x + self.decoder_pos(pos)
freqs_cis = None
# Inference is single-sequence, so there is no padding to mask.
for block in self.decoder:
x = block(x, freqs_cis, key_padding_mask=None)
x = self.decoder_norm(x)
mel_out = self.mel_head(x)
return mel_out
def configure_optimizers(self, weight_decay, learning_rate, betas, device_type):
decay = set()
no_decay = set()
whitelist_weight_modules = (nn.Linear, nn.Conv1d)
blacklist_weight_modules = (nn.LayerNorm, RMSNorm, nn.Embedding)
for mn, m in self.named_modules():
for pn, p in m.named_parameters():
fpn = f"{mn}.{pn}" if mn else pn
if pn.endswith("bias"):
no_decay.add(fpn)
elif pn.endswith("weight") and isinstance(m, whitelist_weight_modules):
decay.add(fpn)
elif pn.endswith("weight") and isinstance(m, blacklist_weight_modules):
no_decay.add(fpn)
param_dict = {pn: p for pn, p in self.named_parameters()}
inter_params = decay & no_decay
union_params = decay | no_decay
assert len(inter_params) == 0, f"Parameters in both decay and no_decay: {inter_params}"
assert len(param_dict.keys() - union_params) == 0, f"Parameters not assigned: {param_dict.keys() - union_params}"
optim_groups = [
{"params": [param_dict[pn] for pn in sorted(list(decay))], "weight_decay": weight_decay},
{"params": [param_dict[pn] for pn in sorted(list(no_decay))], "weight_decay": 0.0},
]
optimizer = torch.optim.AdamW(optim_groups, lr=learning_rate, betas=tuple(betas))
return optimizer