Files
vllm-omni/vllm_omni/diffusion/models/auk/auk_vae.py
T

506 lines
21 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
#
# Adapted from the AuK reference release (MIT), which in turn adapts NVIDIA
# BigVGAN (MIT), jik876/hifi-gan (MIT), junjun3518/alias-free-torch
# (Apache-2.0), EdwardDixon/snake (MIT) and adefossez/julius (MIT).
"""BigVGAN-flow VAE for AuK: 24 kHz mono waveform <-> 50 Hz, 64-channel latents.
Inference only. The reference module also carries a residual coupling flow, a KL
term and a discriminator; all three are training-only, so the released
``flow.*`` tensors are skipped (see :meth:`AuKVAE.load_weights`).
Parameter and buffer names follow the reference release, so the published
``vae.safetensors`` loads without a key map. That is also why the convolutions
are built with the legacy ``weight_norm`` parametrization (``weight_g`` /
``weight_v``): :meth:`remove_weight_norm` folds them into plain weights once
the checkpoint is in.
"""
from __future__ import annotations
import inspect
import math
import warnings
from pathlib import Path
import torch
import torch.nn.functional as F
from safetensors.torch import load_file
from torch import nn
from torch.nn.utils import remove_weight_norm as _fold_weight_norm
from torch.nn.utils import weight_norm as _apply_weight_norm
from vllm.logger import init_logger
logger = init_logger(__name__)
__all__ = ["AuKVAE"]
# Checkpoint prefix owned by the training-only coupling flow.
TRAINING_ONLY_PREFIX = "flow."
def _weight_norm(module: nn.Module) -> nn.Module:
"""Legacy ``weight_g``/``weight_v`` split, which is how the release stores these tensors."""
with warnings.catch_warnings():
warnings.filterwarnings("ignore", message=".*weight_norm.*deprecated.*", category=FutureWarning)
return _apply_weight_norm(module)
def _same_padding(kernel_size: int, dilation: int = 1) -> int:
return int((kernel_size * dilation - dilation) / 2)
def _kaiser_sinc_filter(cutoff: float, half_width: float, kernel_size: int) -> torch.Tensor:
"""Kaiser-windowed sinc low-pass, returned as a ``[1, 1, kernel_size]`` conv kernel."""
half_size = kernel_size // 2
attenuation = 2.285 * (half_size - 1) * math.pi * (4 * half_width) + 7.95
if attenuation > 50.0:
beta = 0.1102 * (attenuation - 8.7)
elif attenuation >= 21.0:
beta = 0.5842 * (attenuation - 21.0) ** 0.4 + 0.07886 * (attenuation - 21.0)
else:
beta = 0.0
window = torch.kaiser_window(kernel_size, beta=beta, periodic=False)
if kernel_size % 2 == 0:
time = torch.arange(-half_size, half_size) + 0.5
else:
time = torch.arange(kernel_size) - half_size
if cutoff == 0:
return torch.zeros_like(time).view(1, 1, kernel_size)
taps = 2 * cutoff * window * torch.sinc(2 * cutoff * time)
# Normalize to unit sum so a constant input passes through unscaled.
return (taps / taps.sum()).view(1, 1, kernel_size)
class Conv(nn.Conv1d):
"""1-D convolution over ``[B, C, T]``; ``causal`` swaps centred padding for a left pad."""
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int = 1,
stride: int = 1,
dilation: int = 1,
bias: bool = True,
causal: bool = False,
) -> None:
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=0 if causal else _same_padding(kernel_size, dilation),
dilation=dilation,
bias=bias,
)
self.causal = causal
self.left_padding = dilation * (kernel_size - 1) if causal else 0
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.causal:
x = F.pad(x, (self.left_padding, 0))
return super().forward(x)
class ConvTranspose(nn.ConvTranspose1d):
"""Transposed 1-D convolution; ``causal`` drops the trailing ``stride`` samples instead of padding."""
def __init__(
self, in_channels: int, out_channels: int, kernel_size: int, stride: int, causal: bool = False
) -> None:
if causal and kernel_size != 2 * stride:
raise ValueError(f"causal ConvTranspose needs kernel_size == 2 * stride, got {kernel_size} and {stride}")
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=0 if causal else (kernel_size - stride) // 2,
)
self.causal = causal
self.trim = stride
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = super().forward(x)
return x[:, :, : -self.trim] if self.causal else x
class NormConv(nn.Module):
"""Weight-normalized conv held under ``layer``; the attribute name is load-bearing for the checkpoint."""
def __init__(self, in_channels: int, out_channels: int, kernel_size: int = 1, stride: int = 1) -> None:
super().__init__()
self.layer = _weight_norm(
nn.Conv1d(in_channels, out_channels, kernel_size, stride=stride, padding=(kernel_size - 1) // 2)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.layer(x)
class ResStack(nn.Module):
"""Stack of dilated residual conv pairs. The leaky slope is torch's 0.01 default, as upstream."""
def __init__(self, channels: int, kernel_size: int = 3, base: int = 3, nums: int = 4) -> None:
super().__init__()
self.layers = nn.ModuleList(
[
nn.Sequential(
nn.LeakyReLU(),
_weight_norm(nn.Conv1d(channels, channels, kernel_size, dilation=base**i, padding=base**i)),
nn.LeakyReLU(),
_weight_norm(nn.Conv1d(channels, channels, kernel_size, dilation=1, padding=1)),
)
for i in range(nums)
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for layer in self.layers:
x = x + layer(x)
return x
class Encoder(nn.Module):
"""Strided conv encoder over ``[B, 1, T]``, emitting ``[B, 2 * latent_dim, T / hop]`` mean/log-std pairs."""
def __init__(
self,
latent_dim: int,
channels: tuple[int, ...],
down_sample_factors: tuple[int, ...],
in_channels: int = 1,
proj_kernel_size: int = 3,
stack_kernel_size: int = 3,
stack_dilation_base: int = 2,
stacks: int = 6,
) -> None:
super().__init__()
layers: list[nn.Module] = [
NormConv(in_channels, channels[0], kernel_size=proj_kernel_size),
nn.LeakyReLU(0.2, True),
]
for in_c, out_c, factor in zip(channels[:-1], channels[1:], down_sample_factors, strict=True):
layers += [
NormConv(in_c, out_c, kernel_size=factor * 2, stride=factor),
ResStack(out_c, stack_kernel_size, stack_dilation_base, stacks),
nn.LeakyReLU(0.2, True),
]
layers.append(NormConv(channels[-1], latent_dim * 2, kernel_size=proj_kernel_size))
self.generator = nn.Sequential(*layers)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.generator(x)
class SnakeBeta(nn.Module):
"""``x + sin^2(alpha * x) / beta`` with per-channel learned alpha and beta (log-scale in AuK)."""
def __init__(self, channels: int, alpha_logscale: bool = False) -> None:
super().__init__()
self.alpha_logscale = alpha_logscale
init = torch.zeros(channels) if alpha_logscale else torch.ones(channels)
self.alpha = nn.Parameter(init.clone())
self.beta = nn.Parameter(init.clone())
self.eps = 1e-9
def forward(self, x: torch.Tensor) -> torch.Tensor:
alpha = self.alpha.unsqueeze(0).unsqueeze(-1)
beta = self.beta.unsqueeze(0).unsqueeze(-1)
if self.alpha_logscale:
alpha = torch.exp(alpha)
beta = torch.exp(beta)
return x + (1.0 / (beta + self.eps)) * torch.sin(x * alpha).pow(2)
class LowPass(nn.Module):
"""Fixed FIR low-pass with replicate padding, applied per channel."""
def __init__(
self,
cutoff: float = 0.5,
half_width: float = 0.6,
stride: int = 1,
kernel_size: int = 12,
causal: bool = False,
) -> None:
super().__init__()
if not 0.0 <= cutoff <= 0.5:
raise ValueError(f"cutoff must lie in [0, 0.5], got {cutoff}")
if causal:
self.pad_left, self.pad_right = kernel_size - 1, 0
else:
even = kernel_size % 2 == 0
self.pad_left = kernel_size // 2 - int(even)
self.pad_right = kernel_size // 2
self.stride = stride
self.register_buffer("filter", _kaiser_sinc_filter(cutoff, half_width, kernel_size))
def forward(self, x: torch.Tensor) -> torch.Tensor:
channels = x.size(1)
x = F.pad(x, (self.pad_left, self.pad_right), mode="replicate")
return F.conv1d(x, self.filter.expand(channels, -1, -1), stride=self.stride, groups=channels)
class Upsample(nn.Module):
"""Band-limited interpolation by ``ratio``. Always non-causal, matching how AuK builds it."""
def __init__(self, ratio: int = 2, kernel_size: int = 12) -> None:
super().__init__()
self.ratio = ratio
self.pad = kernel_size // ratio - 1
self.pad_left = self.pad * ratio + (kernel_size - ratio) // 2
self.pad_right = self.pad * ratio + (kernel_size - ratio + 1) // 2
self.register_buffer("filter", _kaiser_sinc_filter(0.5 / ratio, 0.6 / ratio, kernel_size))
def forward(self, x: torch.Tensor) -> torch.Tensor:
channels = x.size(1)
x = F.pad(x, (self.pad, self.pad), mode="replicate")
x = self.ratio * F.conv_transpose1d(x, self.filter.expand(channels, -1, -1), stride=self.ratio, groups=channels)
return x[..., self.pad_left : -self.pad_right]
class Downsample(nn.Module):
"""Decimation by ``ratio`` behind a low-pass; ``lowpass`` holds the filter buffer."""
def __init__(self, ratio: int = 2, kernel_size: int = 12, causal: bool = False) -> None:
super().__init__()
self.lowpass = LowPass(
cutoff=0.5 / ratio, half_width=0.6 / ratio, stride=ratio, kernel_size=kernel_size, causal=causal
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.lowpass(x)
class AliasFreeActivation(nn.Module):
"""Oversample, apply the pointwise activation, decimate, so snake harmonics do not fold back."""
def __init__(
self,
activation: nn.Module,
up_ratio: int = 2,
down_ratio: int = 2,
up_kernel_size: int = 12,
down_kernel_size: int = 12,
causal: bool = False,
) -> None:
super().__init__()
self.act = activation
self.upsample = Upsample(up_ratio, up_kernel_size)
self.downsample = Downsample(down_ratio, down_kernel_size, causal=causal)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.downsample(self.act(self.upsample(x)))
class AmpBlock(nn.Module):
"""Anti-aliased multi-periodicity residual block: alternating alias-free snake and dilated convs."""
def __init__(
self,
channels: int,
kernel_size: int,
dilations: tuple[int, ...],
snake_logscale: bool = True,
causal: bool = True,
act_causal: bool = False,
) -> None:
super().__init__()
self.convs1 = nn.ModuleList(
[_weight_norm(Conv(channels, channels, kernel_size, 1, dilation=d, causal=causal)) for d in dilations]
)
self.convs2 = nn.ModuleList(
[_weight_norm(Conv(channels, channels, kernel_size, 1, dilation=1, causal=causal)) for _ in dilations]
)
self.activations = nn.ModuleList(
[
AliasFreeActivation(SnakeBeta(channels, alpha_logscale=snake_logscale), causal=act_causal)
for _ in range(len(self.convs1) + len(self.convs2))
]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
acts1, acts2 = self.activations[::2], self.activations[1::2]
for conv1, conv2, act1, act2 in zip(self.convs1, self.convs2, acts1, acts2, strict=True):
x = conv2(act2(conv1(act1(x)))) + x
return x
class AuKVAE(nn.Module):
"""AuK's audio codec: :meth:`encode` waveform to normalized latents, :meth:`decode` back.
Build from the released ``model.vae.model_init_kwargs`` with :meth:`from_config`, then
:meth:`load_weights`. Latents are normalized by the checkpoint's global statistics, which is
the space the rectified-flow transformer works in.
"""
def __init__(
self,
*,
upsample_rates: tuple[int, ...] = (5, 4, 3, 2, 2, 2),
upsample_kernel_sizes: tuple[int, ...] = (10, 8, 6, 4, 4, 4),
upsample_initial_channel: int = 1536,
resblock_kernel_sizes: tuple[int, ...] = (3, 7, 11),
resblock_dilation_sizes: tuple[tuple[int, ...], ...] = ((1, 3, 5), (1, 3, 5), (1, 3, 5)),
downsample_rates: tuple[int, ...] = (2, 2, 2, 3, 4, 5),
downsample_channels: tuple[int, ...] = (12, 24, 48, 96, 192, 384, 768),
snake_logscale: bool = True,
latent_dim: int = 64,
use_vae: bool = True,
causal: bool = True,
act_causal: bool = True,
sample_rate: int = 24000,
) -> None:
super().__init__()
if not use_vae:
raise ValueError("AuKVAE requires use_vae=True: the encoder head emits mean/log-std pairs")
self.latent_dim = latent_dim
self.sample_rate = sample_rate
self.hop_size = math.prod(downsample_rates)
self.num_kernels = len(resblock_kernel_sizes)
self.num_upsamples = len(upsample_rates)
self._norm_folded = False
self.register_buffer("global_mean", torch.zeros(latent_dim, dtype=torch.float32))
self.register_buffer("global_log_std", torch.ones(latent_dim, dtype=torch.float32))
self.audio_encoder = Encoder(
latent_dim=latent_dim,
channels=tuple(downsample_channels),
down_sample_factors=tuple(downsample_rates),
)
self.conv_pre = _weight_norm(Conv(latent_dim, upsample_initial_channel, 7, 1, causal=False))
self.ups = nn.ModuleList()
for i, (rate, kernel) in enumerate(zip(upsample_rates, upsample_kernel_sizes, strict=True)):
# One-element inner list: the checkpoint keys are ``ups.<i>.0.*``.
self.ups.append(
nn.ModuleList(
[
_weight_norm(
ConvTranspose(
upsample_initial_channel // (2**i),
upsample_initial_channel // (2 ** (i + 1)),
kernel,
rate,
causal=causal,
)
)
]
)
)
self.resblocks = nn.ModuleList()
channels = upsample_initial_channel
for i in range(self.num_upsamples):
channels = upsample_initial_channel // (2 ** (i + 1))
for kernel, dilations in zip(resblock_kernel_sizes, resblock_dilation_sizes, strict=True):
self.resblocks.append(
AmpBlock(
channels,
kernel,
tuple(dilations),
snake_logscale=snake_logscale,
causal=causal,
act_causal=act_causal,
)
)
self.activation_post = AliasFreeActivation(
SnakeBeta(channels, alpha_logscale=snake_logscale), causal=act_causal
)
self.conv_post = _weight_norm(Conv(channels, 1, 7, 1, causal=causal, bias=False))
@classmethod
def from_config(cls, cfg: dict) -> AuKVAE:
"""Build from the released ``model_init_kwargs`` mapping, ignoring training-only entries."""
accepted = set(inspect.signature(cls.__init__).parameters) - {"self"}
return cls(**{key: value for key, value in cfg.items() if key in accepted})
def load_weights(
self, path: str | Path, *, device: str | torch.device | None = None, fold_weight_norm: bool = True
) -> tuple[list[str], list[str]]:
"""Load a released ``vae.safetensors``, then fold weight norm away. Returns (missing, unexpected).
``device`` defaults to where the module already lives, which is the placement that matters:
the fold reduces ``weight_v`` to a norm, and a CPU reduction lands up to one ULP away from the
accelerator's. The encoder amplifies its activations roughly fiftyfold, so that one ULP grows
into a ~4e-4 relative drift by the latent head. Move the module first, then load.
"""
target = self.global_mean.device if device is None else torch.device(device)
missing, unexpected = self.load_state_dict(load_file(str(path), device=str(target)), strict=False)
missing, unexpected = list(missing), list(unexpected)
stray = [key for key in missing + unexpected if not key.startswith(TRAINING_ONLY_PREFIX)]
if stray:
raise ValueError(f"AuK VAE weights at {path} do not match this module: {sorted(stray)[:8]}")
logger.info(
"Loaded AuK VAE from %s (skipped %d training-only %s* tensors)", path, len(unexpected), TRAINING_ONLY_PREFIX
)
if fold_weight_norm:
self.remove_weight_norm()
return missing, unexpected
def remove_weight_norm(self) -> None:
"""Fold ``weight_g``/``weight_v`` into plain weights, on whichever device the module is on.
Idempotent, and meant to run once the checkpoint is in. See :meth:`load_weights` for why the
device this runs on is a numeric decision rather than a convenience.
"""
if self._norm_folded:
return
folded = 0
for module in self.modules():
if hasattr(module, "weight_g"):
_fold_weight_norm(module)
folded += 1
self._norm_folded = True
logger.debug("Folded weight norm on %d AuK VAE convolutions", folded)
def encode(
self, wav: torch.Tensor, *, sample: bool = False, generator: torch.Generator | None = None
) -> torch.Tensor:
"""Encode a mono ``[1, T]`` waveform at :attr:`sample_rate` into ``[1, T / hop_size, latent_dim]``.
``sample=False`` takes the posterior mean, which is what a reproducible pipeline wants;
``sample=True`` draws ``mean + randn * exp(log_std)`` from ``generator``. Latents come back
normalized by the checkpoint's global statistics.
"""
if wav.dim() != 2 or wav.size(0) != 1:
raise ValueError(f"encode expects a mono [1, T] waveform, got {tuple(wav.shape)}")
# Statistics stay in fp32 even under an ambient autocast, as in the reference.
with torch.autocast(device_type=wav.device.type, enabled=False):
mean, log_std = self.audio_encoder(wav.unsqueeze(1)).chunk(2, dim=1)
if sample:
latents = mean + self._noise_like(mean, generator) * torch.exp(log_std)
else:
latents = mean
latents = latents.transpose(1, 2).float()
return (latents - self.global_mean.float()) / torch.sqrt(self.global_log_std.float())
def decode(self, latents: torch.Tensor) -> torch.Tensor:
"""Decode normalized ``[B, Np, latent_dim]`` latents into a ``[B, Np * hop_size]`` waveform in [-1, 1]."""
if latents.dim() != 3 or latents.size(-1) != self.latent_dim:
raise ValueError(f"decode expects [B, Np, {self.latent_dim}] latents, got {tuple(latents.shape)}")
x = latents.float() * torch.sqrt(self.global_log_std.float()) + self.global_mean.float()
x = self.conv_pre(x.permute(0, 2, 1))
for i in range(self.num_upsamples):
for up in self.ups[i]:
x = up(x)
first = i * self.num_kernels
residual = self.resblocks[first](x)
for j in range(1, self.num_kernels):
residual = residual + self.resblocks[first + j](x)
x = residual / self.num_kernels
x = self.conv_post(self.activation_post(x))
return torch.clamp(x, min=-1.0, max=1.0).squeeze(1)
@staticmethod
def _noise_like(reference: torch.Tensor, generator: torch.Generator | None) -> torch.Tensor:
"""Draw standard normal noise shaped like ``reference``; ``generator`` decides which RNG stream."""
device = reference.device if generator is None else generator.device
noise = torch.randn(reference.shape, generator=generator, dtype=reference.dtype, device=device)
return noise.to(reference.device)