SFlowTTS / Modules /diffusion /style_prior.py
FashionFlora's picture
Upload full repo excluding dump_40, dump_100, precomputed_tokens, precomputed_data
fb0011a verified
Raw History Blame Contribute Delete
16.3 kB
# file: Modules/diffusion/style_prior.py
# A robust, expressive diffusion prior for style vectors
# Keeps EDM/Karras compatibility so your sampler/schedule work unmodified.
from typing import Optional, Tuple, List
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange, reduce
# ------------------------------
# small utils
# ------------------------------
def exists(x):
return x is not None
def default(x, d):
return x if exists(x) else d() if callable(d) else d
def rand_bool(shape, proba, device):
return torch.rand(shape, device=device) < proba
# ------------------------------
# Positional time embedding
# ------------------------------
class LearnedTimeEmbed(nn.Module):
def __init__(self, dim: int):
super().__init__()
assert dim % 2 == 0
half = dim // 2
self.w = nn.Parameter(torch.randn(half))
def forward(self, t: torch.Tensor) -> torch.Tensor:
t = t.view(-1, 1)
f = t * self.w[None, :] * 2.0 * math.pi
fourier = torch.cat([f.sin(), f.cos()], dim=-1)
# include raw t to help very small t
return torch.cat([t, fourier], dim=-1)
class TimeMLP(nn.Module):
def __init__(self, in_dim: int, out_dim: int):
super().__init__()
self.net = nn.Sequential(
nn.Linear(in_dim, out_dim),
nn.SiLU(),
nn.Linear(out_dim, out_dim),
)
def forward(self, x):
return self.net(x)
# ------------------------------
# AdaLN-Zero (DiT-style)
# ------------------------------
class AdaLNZero(nn.Module):
def __init__(self, hidden_dim: int, cond_dim: int):
super().__init__()
self.ln = nn.LayerNorm(hidden_dim, elementwise_affine=False)
self.mlp = nn.Sequential(
nn.SiLU(),
nn.Linear(cond_dim, hidden_dim * 2),
)
# zero init last layer to stabilize early training
nn.init.zeros_(self.mlp[-1].weight)
nn.init.zeros_(self.mlp[-1].bias)
def forward(self, x: torch.Tensor, c: torch.Tensor):
# x: [B, T, C], c: [B, cond_dim]
scale, shift = self.mlp(c).chunk(2, dim=-1)
x = self.ln(x)
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
# ------------------------------
# Multi-Head Self/Cross Attention
# ------------------------------
class MHA(nn.Module):
def __init__(self, dim: int, heads: int, head_dim: int,
dropout: float = 0.0):
super().__init__()
self.heads = heads
inner = heads * head_dim
self.to_q = nn.Linear(dim, inner, bias=False)
self.to_k = nn.Linear(dim, inner, bias=False)
self.to_v = nn.Linear(dim, inner, bias=False)
self.to_out = nn.Sequential(
nn.Linear(inner, dim),
nn.Dropout(dropout),
)
def forward(self, x, context=None):
# x: [B, T, C]; context: [B, S, C] or None -> self-attn
if context is None:
context = x
q = self.to_q(x)
k = self.to_k(context)
v = self.to_v(context)
h = self.heads
q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=h),
(q, k, v))
scale = q.shape[-1] ** -0.5
sim = torch.einsum("b h n d, b h m d -> b h n m", q, k) * scale
attn = sim.softmax(dim=-1)
out = torch.einsum("b h n m, b h m d -> b h n d", attn, v)
out = rearrange(out, "b h n d -> b n (h d)")
return self.to_out(out)
# ------------------------------
# Style-DiT block: AdaLN-Zero + MHA + FFN
# ------------------------------
class FFN(nn.Module):
def __init__(self, dim: int, mult: int = 4, dropout: float = 0.0):
super().__init__()
inner = dim * mult
self.net = nn.Sequential(
nn.Linear(dim, inner),
nn.GELU(),
nn.Dropout(dropout),
nn.Linear(inner, dim),
)
def forward(self, x):
return self.net(x)
class StyleDiTBlock(nn.Module):
def __init__(
self,
dim: int,
heads: int,
head_dim: int,
cond_dim: int,
dropout: float = 0.0,
):
super().__init__()
self.adaln1 = AdaLNZero(dim, cond_dim)
self.self_attn = MHA(dim, heads, head_dim, dropout)
self.adaln2 = AdaLNZero(dim, cond_dim)
self.cross_attn = MHA(dim, heads, head_dim, dropout)
self.adaln3 = AdaLNZero(dim, cond_dim)
self.ff = FFN(dim, mult=4, dropout=dropout)
def forward(self, x, cond_vec, context):
# x: [B, T, C]; context: [B, S, C]
x = x + self.self_attn(self.adaln1(x, cond_vec))
x = x + self.cross_attn(self.adaln2(x, cond_vec), context=context)
x = x + self.ff(self.adaln3(x, cond_vec))
return x
# ------------------------------
# StyleDiT backbone for style vectors
# ------------------------------
class StyleDiT(nn.Module):
"""
A compact DiT-like model to predict denoised style vectors.
We treat the style vector as a length-T sequence (T small; e.g., 1 or few
learnable "style tokens") to preserve DiT’s inductive bias. Typically T=4
tokens works well; output is mean pooled to style_dim.
"""
def __init__(
self,
style_dim: int, # final output dim (e.g., 256 for gs+dur)
model_dim: int = 512, # hidden width
num_tokens: int = 4, # style tokens
depth: int = 8,
heads: int = 8,
head_dim: int = 64,
time_embed_dim: int = 128, # time conditioning
txt_dim: int = 768, # BERT hidden size
ref_dim: int = 256, # reference style feature dim (gs||dur)
dropout: float = 0.0,
):
super().__init__()
self.num_tokens = num_tokens
# style token input proj
self.in_proj = nn.Linear(style_dim, model_dim)
# learned token embeddings (to let the model expand a single vector)
self.token_embed = nn.Parameter(
torch.randn(1, num_tokens, model_dim) * 0.02
)
# time embedding
self.time_pe = LearnedTimeEmbed(time_embed_dim)
self.time_mlp = TimeMLP(time_embed_dim + 1, model_dim)
# text conditioning proj
self.txt_proj = nn.Linear(txt_dim, model_dim)
# ref conditioning proj (for multi-ref, we will average before proj)
self.ref_proj = nn.Linear(ref_dim, model_dim)
# fusion: build a single cond vector (mean over sequence)
cond_dim = model_dim * 3 # time + pooled text + pooled ref
self.blocks = nn.ModuleList([
StyleDiTBlock(
dim=model_dim,
heads=heads,
head_dim=head_dim,
cond_dim=cond_dim,
dropout=dropout,
)
for _ in range(depth)
])
self.out_ln = nn.LayerNorm(model_dim)
self.out_proj = nn.Linear(model_dim, style_dim)
# small zero-init to stabilize early steps
nn.init.zeros_(self.out_proj.weight)
nn.init.zeros_(self.out_proj.bias)
def _cond_vector(
self,
t: torch.Tensor, # [B]
txt: torch.Tensor, # [B, L, txt_dim]
ref: torch.Tensor, # [B, R, ref_dim] (R refs, concat gs||dur or any)
) -> Tuple[torch.Tensor, torch.Tensor]:
# time
t_feat = self.time_pe(t) # [B, time_embed_dim+1]
t_feat = self.time_mlp(t_feat) # [B, model_dim]
# text: mean pool
txt_ctx = self.txt_proj(txt) # [B, L, model_dim]
txt_pool = txt_ctx.mean(dim=1) # [B, model_dim]
# ref: mean pool across refs
ref_ctx = self.ref_proj(ref) # [B, R, model_dim]
ref_pool = ref_ctx.mean(dim=1) # [B, model_dim]
cond = torch.cat([t_feat, txt_pool, ref_pool], dim=-1) # [B, cond_dim]
# use text as "context" for cross-attn (you can also concat ref_ctx)
context = txt_ctx
return cond, context
def forward(
self,
x_noisy: torch.Tensor, # [B, 1, style_dim] or [B, T, style_dim]
t: torch.Tensor, # [B] time in [0, 1] (we derive from sigma)
txt: torch.Tensor, # [B, L, txt_dim] (BERT)
ref: torch.Tensor, # [B, R, ref_dim] (gs||dur or variants)
) -> torch.Tensor:
b = x_noisy.size(0)
# flatten style sequence to tokens
if x_noisy.dim() == 3:
tokens_in = x_noisy # [B, T, style_dim]
else:
tokens_in = x_noisy.view(b, 1, -1) # [B, 1, style_dim]
# project tokens into model space
h = self.in_proj(tokens_in) # [B, T, model_dim]
# broadcast learned token embeddings and concat
tok = self.token_embed.expand(b, self.num_tokens, -1) # [B, T2, C]
h = torch.cat([h, tok], dim=1) # [B, T+num_tokens, C]
# build cond vector + context
cond_vec, context = self._cond_vector(t, txt, ref) # [B, Cc], [B, L, C]
# transformer blocks with AdaLN-Zero and self/cross-attn
for blk in self.blocks:
h = blk(h, cond_vec, context)
h = self.out_ln(h)
h = h.mean(dim=1) # pool tokens
out = self.out_proj(h) # [B, style_dim] — predicts x_pred in EDM
return out
# ------------------------------
# Karras EDM with Min-SNR weight + EMA + CFG
# ------------------------------
class KDiffusionWithEMA(nn.Module):
"""
EDM-style Karras diffusion wrapper with:
- Min-SNR loss weighting for stability
- Classifier-free guidance (cfg) for text conditioning
- EMA of network parameters
API compatible with your current KDiffusion usage:
- denoise_fn(x_noisy, sigma or sigmas, ...) returns x_denoised
- forward(x0, noise=None, ...) -> scalar loss
"""
alias = "k"
def __init__(
self,
net: nn.Module, # StyleDiT
sigma_data: float = 0.5,
sigma_min: float = 1e-4,
sigma_max: float = 3.0,
min_snr_gamma: float = 5.0, # Min-SNR weighting
ema_decay: float = 0.9999,
):
super().__init__()
self.net = net
self.sigma_data = sigma_data
self.sigma_min = sigma_min
self.sigma_max = sigma_max
self.min_snr_gamma = min_snr_gamma
# EMA
self.ema_decay = ema_decay
self.net_ema = copy_model(net)
@torch.no_grad()
def ema_update(self):
for p, p_ema in zip(self.net.parameters(), self.net_ema.parameters()):
p_ema.data.mul_(self.ema_decay).add_(p.data, alpha=1 - self.ema_decay)
@staticmethod
def _sigmas_to_t(sigmas: torch.Tensor) -> torch.Tensor:
# Use VK mapping (t in [0,1]): t = atan(sigma)/(pi/2)
return torch.atan(sigmas) / (math.pi / 2.0)
def _get_scale_weights(self, sigmas: torch.Tensor):
# same as your KDiffusion
sd = self.sigma_data
c_noise = torch.log(sigmas) * 0.25
s = sigmas.view(-1, 1, 1)
c_skip = (sd ** 2) / (s ** 2 + sd ** 2)
c_out = s * sd * (sd ** 2 + s ** 2) ** -0.5
c_in = (s ** 2 + sd ** 2) ** -0.5
return c_skip, c_out, c_in, c_noise
def denoise_fn(
self,
x_noisy: torch.Tensor, # [B, 1, D] style vector noisy
sigmas: Optional[torch.Tensor] = None,
sigma: Optional[float] = None,
*,
embedding: torch.Tensor, # [B, L, txt_dim]
features: Optional[torch.Tensor] = None, # [B, R, ref_dim]
embedding_mask_proba: float = 0.0,
embedding_scale: float = 1.0,
use_ema: bool = True,
) -> torch.Tensor:
b, device = x_noisy.size(0), x_noisy.device
sigmas = default(
sigmas, lambda: torch.full((b,), float(sigma), device=device)
)
# classifier-free guidance — mask a subset of text embeddings
txt = embedding
if embedding_mask_proba > 0:
mask = rand_bool((b, 1, 1), embedding_mask_proba, device)
txt_fixed = torch.zeros_like(txt) # unconditional token
txt_in = torch.where(mask, txt_fixed, txt)
else:
txt_in = txt
# features (reference) — allow None; if none, use zeros
if features is None:
features = torch.zeros(b, 1, self.net.ref_proj.in_features,
device=device)
# Convert sigmas to t in [0, 1]
t = self._sigmas_to_t(sigmas) # [B]
# Choose net or net_ema for denoising
net = self.net_ema if use_ema else self.net
# Predict x_pred (EDM parameterization)
x_pred = net(x_noisy, t, txt_in, features)
x_pred = x_pred.view(b, 1, -1)
# Classifier-free guidance scale:
if embedding_scale != 1.0 and embedding_mask_proba > 0.0:
# do one more unconditional pass (use all-masked text)
txt_uncond = torch.zeros_like(txt)
x_pred_un = net(x_noisy.transpose(1, 2), t, txt_uncond, features)
x_pred_un = x_pred_un.view(b, 1, -1)
x_pred = x_pred_un + (x_pred - x_pred_un) * embedding_scale
# EDM output transform
# Karras EDM requires we combine with skip/out weights
c_skip, c_out, c_in, c_noise = self._get_scale_weights(sigmas)
x_in = c_in * x_noisy
# "x_pred" here is the predicted clean target in EDM space
x_denoised = c_skip * x_noisy + c_out * x_pred
return x_denoised
def _loss_weight(self, sigmas: torch.Tensor) -> torch.Tensor:
# Min-SNR gamma weighting as in "Min-SNR Diffusion Training"
# w = gamma / (snr + gamma)
sd = self.sigma_data
snr = (sd ** 2) / (sigmas ** 2)
w = self.min_snr_gamma / (snr + self.min_snr_gamma)
return w
def forward(
self,
x0: torch.Tensor, # [B, 1, D] clean style target
noise: Optional[torch.Tensor] = None,
*,
embedding: torch.Tensor, # [B, L, txt_dim]
features: Optional[torch.Tensor] = None, # [B, R, ref_dim]
sigma_dist_mean: float = -1.2,
sigma_dist_std: float = 1.2,
embedding_mask_proba: float = 0.1,
) -> torch.Tensor:
b, device = x0.size(0), x0.device
# sample sigmas log-normal (EDM)
sigmas = torch.exp(
torch.randn(b, device=device) * sigma_dist_std + sigma_dist_mean
)
s = sigmas.view(b, 1, 1)
noise = default(noise, lambda: torch.randn_like(x0))
x_noisy = x0 + s * noise
# Convert to EDM param
c_skip, c_out, c_in, c_noise = self._get_scale_weights(sigmas)
x_in = c_in * x_noisy
# predict x_pred with network
txt = embedding
if embedding_mask_proba > 0.0:
mask = rand_bool((b, 1, 1), embedding_mask_proba, device)
txt = torch.where(mask, torch.zeros_like(txt), txt)
if features is None:
features = torch.zeros(
b, 1, self.net.ref_proj.in_features, device=device
)
t = self._sigmas_to_t(sigmas)
x_pred = self.net(x_in, t, txt, features)
x_pred = x_pred.view(b, 1, -1)
# Weighted MSE against clean x0 in EDM space
x_denoised = c_skip * x_noisy + c_out * x_pred
loss = F.mse_loss(x_denoised, x0, reduction="none")
loss = reduce(loss, "b ... -> b", "mean")
# apply Min-SNR weight
w = self._loss_weight(sigmas)
loss = (loss * w).mean()
# EMA update outside if you want; or here per step
self.ema_update()
return loss
def copy_model(model: nn.Module) -> nn.Module:
import copy
m = copy.deepcopy(model)
for p in m.parameters():
p.requires_grad_(False)
return m