[Refactor] fish_speech: vendor DAC codec modules, drop external fish-speech dependency (#4871)
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
This commit is contained in:
1 parent
043f03d576
commit
e1c59edec6
15 files changed
+1517
-38
No files matched your search
@@ -126,11 +126,7 @@ python examples/offline_inference/text_to_speech/cosyvoice3/end2end.py \
|
||||
## Fish Speech S2 Pro
|
||||
|
||||
4B dual-AR text-to-speech model from FishAudio with the DAC codec at 44.1 kHz.
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
pip install fish-speech
|
||||
```
|
||||
No extra packages are required; the DAC codec is vendored in vLLM-Omni.
|
||||
|
||||
### Quick start
|
||||
```bash
|
||||
|
||||
@@ -155,13 +155,8 @@ The client supports `--api-base`, `--model`, `--text`, `--ref-audio`, `--ref-tex
|
||||
|
||||
## Fish Speech S2 Pro
|
||||
|
||||
4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
pip install fish-speech
|
||||
```
|
||||
4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec, which is vendored in
|
||||
vLLM-Omni (no extra packages required).
|
||||
|
||||
### Launch
|
||||
|
||||
|
||||
@@ -130,11 +130,7 @@ Text → [Stage 0: AR] → Speech Tokens → [Stage 1: DiT + HiFT] → Audio (24
|
||||
## Fish Speech S2 Pro
|
||||
|
||||
4B dual-AR text-to-speech model from FishAudio with the DAC codec at 44.1 kHz.
|
||||
|
||||
### Prerequisites
|
||||
```bash
|
||||
pip install fish-speech
|
||||
```
|
||||
No extra packages are required; the DAC codec is vendored in vLLM-Omni.
|
||||
|
||||
### Quick start
|
||||
```bash
|
||||
|
||||
@@ -255,13 +255,8 @@ python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
||||
|
||||
## Fish Speech S2 Pro
|
||||
|
||||
4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
pip install fish-speech
|
||||
```
|
||||
4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec, which is vendored in
|
||||
vLLM-Omni (no extra packages required).
|
||||
|
||||
### Kvcache attention fast path
|
||||
|
||||
|
||||
@@ -378,3 +378,7 @@ ba = "ba"
|
||||
SER = "SER"
|
||||
# NumPy ndarray.flags.writeable (canonical flag name, not "writable").
|
||||
writeable = "writeable"
|
||||
# "convnet" abbreviates convolutional network. It appears in the vendored Fish
|
||||
# Speech DAC codec identifiers (convnet_type, transconvnet_type) and is not a
|
||||
# typo of "convent".
|
||||
convnet = "convnet"
|
||||
@@ -36,19 +36,13 @@ Other hardware and configurations are welcome as community validation lands.
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- Required package: `fish-speech` (for DAC codec)
|
||||
- CUDA 12.8
|
||||
- vLLM version: 0.19.0
|
||||
- vLLM-Omni version or commit: c93359bb354a6aa5c14d062430cb85b2c4db251e
|
||||
|
||||
```bash
|
||||
# Install PortAudio dependency if missing (required by pyaudio which is a dependency of fish-speech)
|
||||
# For Ubuntu/Debian:
|
||||
if ! dpkg -l | grep -q libportaudio2; then
|
||||
sudo apt-get update && sudo apt-get install -y libportaudio2 portaudio19-dev
|
||||
fi
|
||||
pip install fish-speech
|
||||
```
|
||||
No extra packages are required: the DAC codec architecture is vendored in
|
||||
vLLM-Omni (`vllm_omni/model_executor/models/fish_speech/dac_modules/`), so the
|
||||
model runs out of the box, including in the official Docker image.
|
||||
|
||||
#### Kvcache attention fast path
|
||||
|
||||
@@ -129,7 +123,6 @@ curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- Required package: `fish-speech` (for DAC codec)
|
||||
- CUDA 12.8
|
||||
- vLLM version: 0.19.0+
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""CPU tests for the vendored DAC codec modules (dac_modules).
|
||||
|
||||
Guards the checkpoint contract: the module tree built by build_dac_codec()
|
||||
must keep the exact parameter/buffer names and shapes that codec.pth was
|
||||
saved with (verified against the upstream fish-speech 0.1.0 package), and
|
||||
the encode/decode surfaces used by dac_encoder.py / fish_speech_dac_decoder.py
|
||||
must keep their shapes.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
# Spot checks covering every submodule family and both weight-norm styles.
|
||||
# Derived from the upstream fish-speech 0.1.0 state dict (541 entries total);
|
||||
# legacy weight_norm (weight_g/weight_v) comes from the descript quantizer
|
||||
# projections, parametrizations.weight.original0/1 from the causal convs.
|
||||
EXPECTED_KEY_SAMPLES = [
|
||||
# encoder: first conv + deepest encoder-block transformer
|
||||
"encoder.block.0.conv.parametrizations.weight.original0",
|
||||
"encoder.block.4.block.5.layers.3.attention.wqkv.weight",
|
||||
"encoder.block.4.block.5.freqs_cis",
|
||||
"encoder.block.1.block.0.block.0.alpha",
|
||||
# quantizer: semantic + residual VQ (legacy weight_norm keys)
|
||||
"quantizer.semantic_quantizer.quantizers.0.codebook.weight",
|
||||
"quantizer.quantizer.quantizers.8.in_proj.weight_g",
|
||||
"quantizer.quantizer.quantizers.8.in_proj.weight_v",
|
||||
# quantizer pre/post transformers + down/upsample convnext
|
||||
"quantizer.pre_module.layers.7.feed_forward.w1.weight",
|
||||
"quantizer.post_module.norm.weight",
|
||||
"quantizer.downsample.1.1.dwconv.conv.weight",
|
||||
"quantizer.upsample.0.0.conv.weight",
|
||||
# decoder: transposed conv + final conv
|
||||
"decoder.model.1.block.1.conv.parametrizations.weight.original1",
|
||||
"decoder.model.6.conv.parametrizations.weight.original0",
|
||||
]
|
||||
|
||||
NUM_STATE_DICT_ENTRIES = 541
|
||||
DAC_NUM_CODEBOOKS = 10 # 1 semantic + 9 residual
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def codec():
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_utils import build_dac_codec
|
||||
|
||||
model = build_dac_codec()
|
||||
model.eval()
|
||||
return model
|
||||
|
||||
|
||||
def test_state_dict_contract(codec):
|
||||
sd = codec.state_dict()
|
||||
assert len(sd) == NUM_STATE_DICT_ENTRIES
|
||||
for key in EXPECTED_KEY_SAMPLES:
|
||||
assert key in sd, f"checkpoint-contract key missing: {key}"
|
||||
|
||||
|
||||
def test_no_fish_speech_import_required(codec):
|
||||
import sys
|
||||
|
||||
assert "fish_speech" not in sys.modules, "vendored codec must not import the fish-speech package"
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def test_encode_shapes(codec):
|
||||
frame_length = codec.frame_length # 2048 samples per code frame
|
||||
wav = torch.randn(1, 1, frame_length * 3 + 100)
|
||||
lengths = torch.tensor([wav.shape[-1]], dtype=torch.long)
|
||||
codes, code_lengths = codec.encode(wav, lengths)
|
||||
assert codes.shape[0] == 1
|
||||
assert codes.shape[1] == DAC_NUM_CODEBOOKS
|
||||
assert codes.shape[2] == 4 # ceil(3.x frames)
|
||||
assert code_lengths.tolist() == [4]
|
||||
assert codes.dtype in (torch.int64, torch.int32)
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def test_decode_shapes(codec):
|
||||
frames = 8
|
||||
codes = torch.randint(0, 1024, (1, DAC_NUM_CODEBOOKS, frames), dtype=torch.long)
|
||||
feature_lengths = torch.tensor([frames], dtype=torch.long)
|
||||
wav, audio_lengths = codec.decode(codes, feature_lengths)
|
||||
assert wav.shape[0] == 1 and wav.shape[1] == 1
|
||||
assert audio_lengths.tolist() == [frames * codec.frame_length]
|
||||
assert wav.shape[-1] >= audio_lengths[0] - codec.frame_length
|
||||
|
||||
|
||||
@torch.no_grad()
|
||||
def test_decoder_runner_mutations(codec):
|
||||
"""The stage-1 runner prunes encode-only parts and bakes weight norm;
|
||||
decode must still work afterwards (mirrors fish_speech_dac_decoder)."""
|
||||
from torch.nn.utils.parametrize import remove_parametrizations
|
||||
|
||||
codec.encoder = None
|
||||
codec.quantizer.pre_module = None
|
||||
codec.quantizer.downsample = None
|
||||
for module in codec.modules():
|
||||
parametrizations = getattr(module, "parametrizations", None)
|
||||
if not parametrizations:
|
||||
continue
|
||||
for name in list(parametrizations.keys()):
|
||||
remove_parametrizations(module, name, leave_parametrized=True)
|
||||
|
||||
codes = torch.randint(0, 1024, (2, DAC_NUM_CODEBOOKS, 5), dtype=torch.long)
|
||||
wav, audio_lengths = codec.decode(codes, torch.tensor([5, 3], dtype=torch.long))
|
||||
assert wav.shape[0] == 2
|
||||
assert audio_lengths.tolist() == [5 * codec.frame_length, 3 * codec.frame_length]
|
||||
@@ -0,0 +1,27 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Vendored DAC codec modules for Fish Speech S2 Pro.
|
||||
|
||||
Adopted from the ``fish-speech`` 0.1.0 PyPI release
|
||||
(https://pypi.org/project/fish-speech/0.1.0/, Apache-2.0) and
|
||||
descript-audio-codec (https://github.com/descriptinc/descript-audio-codec,
|
||||
MIT), trimmed to inference-only so the codec loads without external model
|
||||
packages. Module attribute paths and weight-norm styles are kept identical
|
||||
to upstream so ``codec.pth`` checkpoints load unchanged.
|
||||
"""
|
||||
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.codec import DAC
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.rvq import (
|
||||
DownsampleResidualVectorQuantize,
|
||||
)
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.transformer import (
|
||||
ModelArgs,
|
||||
WindowLimitedTransformer,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"DAC",
|
||||
"DownsampleResidualVectorQuantize",
|
||||
"ModelArgs",
|
||||
"WindowLimitedTransformer",
|
||||
]
|
||||
@@ -0,0 +1,335 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from the fish-speech 0.1.0 PyPI release (Apache-2.0)
|
||||
# https://pypi.org/project/fish-speech/0.1.0/
|
||||
# Copyright (c) Fish Audio
|
||||
"""DAC codec model (encoder / quantizer / decoder).
|
||||
|
||||
Differences from upstream ``fish_speech.models.dac.modded_dac.DAC``, none of
|
||||
which affect the parameter tree or the inference math:
|
||||
|
||||
- Base class is plain ``nn.Module`` instead of ``(audiotools BaseModel,
|
||||
descript CodecMixin)`` -- both bases only add training/CLI conveniences.
|
||||
- ``self.delay = self.get_delay()`` (a CodecMixin helper) is dropped; nothing
|
||||
in the inference paths reads it.
|
||||
- The training-only ``forward``/``preprocess`` methods are dropped; only
|
||||
``encode`` and ``decode`` are used at inference time.
|
||||
"""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.conv import (
|
||||
CausalWNConv1d,
|
||||
CausalWNConvTranspose1d,
|
||||
)
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.layers import (
|
||||
Snake1d,
|
||||
WNConv1d,
|
||||
WNConvTranspose1d,
|
||||
)
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.transformer import (
|
||||
ModelArgs,
|
||||
WindowLimitedTransformer,
|
||||
)
|
||||
|
||||
|
||||
def init_weights(m):
|
||||
if isinstance(m, nn.Conv1d):
|
||||
nn.init.trunc_normal_(m.weight, std=0.02)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
|
||||
class ResidualUnit(nn.Module):
|
||||
def __init__(self, dim: int = 16, dilation: int = 1, causal: bool = False):
|
||||
super().__init__()
|
||||
conv_class = CausalWNConv1d if causal else WNConv1d
|
||||
pad = ((7 - 1) * dilation) // 2
|
||||
self.block = nn.Sequential(
|
||||
Snake1d(dim),
|
||||
conv_class(dim, dim, kernel_size=7, dilation=dilation, padding=pad),
|
||||
Snake1d(dim),
|
||||
conv_class(dim, dim, kernel_size=1),
|
||||
)
|
||||
self.causal = causal
|
||||
|
||||
def forward(self, x):
|
||||
y = self.block(x)
|
||||
pad = x.shape[-1] - y.shape[-1]
|
||||
if pad > 0:
|
||||
if self.causal:
|
||||
x = x[..., :-pad]
|
||||
else:
|
||||
x = x[..., pad // 2 : -pad // 2]
|
||||
return x + y
|
||||
|
||||
|
||||
class EncoderBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int = 16,
|
||||
stride: int = 1,
|
||||
causal: bool = False,
|
||||
n_t_layer: int = 0,
|
||||
transformer_general_config=None,
|
||||
):
|
||||
super().__init__()
|
||||
conv_class = CausalWNConv1d if causal else WNConv1d
|
||||
transformer_module = (
|
||||
nn.Identity()
|
||||
if n_t_layer == 0
|
||||
else (
|
||||
WindowLimitedTransformer(
|
||||
causal=causal,
|
||||
input_dim=dim,
|
||||
window_size=512,
|
||||
config=transformer_general_config(
|
||||
n_layer=n_t_layer,
|
||||
n_head=dim // 64,
|
||||
dim=dim,
|
||||
intermediate_size=dim * 3,
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
self.block = nn.Sequential(
|
||||
ResidualUnit(dim // 2, dilation=1, causal=causal),
|
||||
ResidualUnit(dim // 2, dilation=3, causal=causal),
|
||||
ResidualUnit(dim // 2, dilation=9, causal=causal),
|
||||
Snake1d(dim // 2),
|
||||
conv_class(
|
||||
dim // 2,
|
||||
dim,
|
||||
kernel_size=2 * stride,
|
||||
stride=stride,
|
||||
padding=math.ceil(stride / 2),
|
||||
),
|
||||
transformer_module,
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.block(x)
|
||||
|
||||
|
||||
class Encoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
d_model: int = 64,
|
||||
strides: list = [2, 4, 8, 8],
|
||||
d_latent: int = 64,
|
||||
n_transformer_layers: list = [0, 0, 4, 4],
|
||||
transformer_general_config: ModelArgs = None,
|
||||
causal: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
conv_class = CausalWNConv1d if causal else WNConv1d
|
||||
# Create first convolution
|
||||
self.block = [conv_class(1, d_model, kernel_size=7, padding=3)]
|
||||
|
||||
# Create EncoderBlocks that double channels as they downsample by `stride`
|
||||
for stride, n_t_layer in zip(strides, n_transformer_layers):
|
||||
d_model *= 2
|
||||
self.block += [
|
||||
EncoderBlock(
|
||||
d_model,
|
||||
stride=stride,
|
||||
causal=causal,
|
||||
n_t_layer=n_t_layer,
|
||||
transformer_general_config=transformer_general_config,
|
||||
)
|
||||
]
|
||||
|
||||
# Create last convolution
|
||||
self.block += [
|
||||
Snake1d(d_model),
|
||||
conv_class(d_model, d_latent, kernel_size=3, padding=1),
|
||||
]
|
||||
|
||||
# Wrap black into nn.Sequential
|
||||
self.block = nn.Sequential(*self.block)
|
||||
self.enc_dim = d_model
|
||||
|
||||
def forward(self, x):
|
||||
return self.block(x)
|
||||
|
||||
|
||||
class DecoderBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int = 16,
|
||||
output_dim: int = 8,
|
||||
stride: int = 1,
|
||||
causal: bool = False,
|
||||
n_t_layer: int = 0,
|
||||
transformer_general_config=None,
|
||||
):
|
||||
super().__init__()
|
||||
conv_trans_class = CausalWNConvTranspose1d if causal else WNConvTranspose1d
|
||||
# NOTE: upstream constructs a WindowLimitedTransformer from
|
||||
# ``n_t_layer`` here but never registers it (the line placing it in
|
||||
# ``self.block`` is commented out upstream), so decoder blocks carry no
|
||||
# transformer parameters. It is not built here at all -- same module
|
||||
# tree, without allocating throwaway weights.
|
||||
del n_t_layer, transformer_general_config
|
||||
self.block = nn.Sequential(
|
||||
Snake1d(input_dim),
|
||||
conv_trans_class(
|
||||
input_dim,
|
||||
output_dim,
|
||||
kernel_size=2 * stride,
|
||||
stride=stride,
|
||||
padding=math.ceil(stride / 2),
|
||||
),
|
||||
ResidualUnit(output_dim, dilation=1, causal=causal),
|
||||
ResidualUnit(output_dim, dilation=3, causal=causal),
|
||||
ResidualUnit(output_dim, dilation=9, causal=causal),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
return self.block(x)
|
||||
|
||||
|
||||
class Decoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_channel,
|
||||
channels,
|
||||
rates,
|
||||
d_out: int = 1,
|
||||
causal: bool = False,
|
||||
n_transformer_layers: list = [0, 0, 0, 0],
|
||||
transformer_general_config=None,
|
||||
):
|
||||
super().__init__()
|
||||
conv_class = CausalWNConv1d if causal else WNConv1d
|
||||
# Add first conv layer
|
||||
layers = [conv_class(input_channel, channels, kernel_size=7, padding=3)]
|
||||
|
||||
# Add upsampling + MRF blocks
|
||||
for i, (stride, n_t_layer) in enumerate(zip(rates, n_transformer_layers)):
|
||||
input_dim = channels // 2**i
|
||||
output_dim = channels // 2 ** (i + 1)
|
||||
layers += [
|
||||
DecoderBlock(
|
||||
input_dim,
|
||||
output_dim,
|
||||
stride,
|
||||
causal=causal,
|
||||
n_t_layer=n_t_layer,
|
||||
transformer_general_config=transformer_general_config,
|
||||
)
|
||||
]
|
||||
|
||||
# Add final conv layer
|
||||
layers += [
|
||||
Snake1d(output_dim),
|
||||
conv_class(output_dim, d_out, kernel_size=7, padding=3),
|
||||
nn.Tanh(),
|
||||
]
|
||||
|
||||
self.model = nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
return self.model(x)
|
||||
|
||||
|
||||
class DAC(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
encoder_dim: int = 64,
|
||||
encoder_rates: list[int] = [2, 4, 8, 8],
|
||||
latent_dim: int = None,
|
||||
decoder_dim: int = 1536,
|
||||
decoder_rates: list[int] = [8, 8, 4, 2],
|
||||
quantizer: torch.nn.Module = None,
|
||||
sample_rate: int = 44100,
|
||||
causal: bool = True,
|
||||
encoder_transformer_layers: list[int] = [0, 0, 0, 0],
|
||||
decoder_transformer_layers: list[int] = [0, 0, 0, 0],
|
||||
transformer_general_config=None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.encoder_dim = encoder_dim
|
||||
self.encoder_rates = encoder_rates
|
||||
self.decoder_dim = decoder_dim
|
||||
self.decoder_rates = decoder_rates
|
||||
self.sample_rate = sample_rate
|
||||
|
||||
if latent_dim is None:
|
||||
latent_dim = encoder_dim * (2 ** len(encoder_rates))
|
||||
|
||||
self.latent_dim = latent_dim
|
||||
|
||||
self.hop_length = int(math.prod(encoder_rates))
|
||||
self.encoder = Encoder(
|
||||
encoder_dim,
|
||||
encoder_rates,
|
||||
latent_dim,
|
||||
causal=causal,
|
||||
n_transformer_layers=encoder_transformer_layers,
|
||||
transformer_general_config=transformer_general_config,
|
||||
)
|
||||
|
||||
self.quantizer = quantizer
|
||||
|
||||
self.decoder = Decoder(
|
||||
latent_dim,
|
||||
decoder_dim,
|
||||
decoder_rates,
|
||||
causal=causal,
|
||||
n_transformer_layers=decoder_transformer_layers,
|
||||
transformer_general_config=transformer_general_config,
|
||||
)
|
||||
self.sample_rate = sample_rate
|
||||
self.apply(init_weights)
|
||||
|
||||
self.frame_length = self.hop_length * 4
|
||||
|
||||
def encode(
|
||||
self,
|
||||
audio_data: torch.Tensor,
|
||||
audio_lengths: torch.Tensor = None,
|
||||
n_quantizers: int = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Encode given audio data and return quantized latent codes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
audio_data : Tensor[B x 1 x T]
|
||||
Audio data to encode
|
||||
audio_lengths : Tensor[B], optional
|
||||
Valid sample counts per batch entry
|
||||
n_quantizers : int, optional
|
||||
Number of quantizers to use, by default None (all quantizers)
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple of (codes Tensor[B x N x F], code lengths Tensor[B]).
|
||||
"""
|
||||
# pad to multiple of self.frame_length
|
||||
if audio_data.ndim == 2:
|
||||
audio_data = audio_data.unsqueeze(1)
|
||||
length = audio_data.shape[-1]
|
||||
right_pad = math.ceil(length / self.frame_length) * self.frame_length - length
|
||||
audio_data = nn.functional.pad(audio_data, (0, right_pad))
|
||||
if audio_lengths is None:
|
||||
audio_lengths = torch.LongTensor([length + right_pad]).to(audio_data.device)
|
||||
|
||||
z = self.encoder(audio_data)
|
||||
vq_results = self.quantizer(z, n_quantizers, **kwargs)
|
||||
indices = vq_results.codes
|
||||
indices_lens = torch.ceil(audio_lengths / self.frame_length).long()
|
||||
return indices, indices_lens
|
||||
|
||||
def decode(self, indices: torch.Tensor, feature_lengths):
|
||||
"""Decode codebook indices [B x N x F] to waveform [B x 1 x T]."""
|
||||
if indices.ndim == 2:
|
||||
indices = indices[None]
|
||||
|
||||
z = self.quantizer.decode(indices)
|
||||
audio_lengths = feature_lengths * self.frame_length
|
||||
return self.decoder(z), audio_lengths
|
||||
@@ -0,0 +1,194 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from the fish-speech 0.1.0 PyPI release (Apache-2.0)
|
||||
# https://pypi.org/project/fish-speech/0.1.0/
|
||||
# Copyright (c) Fish Audio
|
||||
"""Causal convolution primitives shared by the DAC encoder/decoder and the
|
||||
downsample RVQ. Upstream duplicates these between ``modded_dac.py`` and
|
||||
``rvq.py``; they are consolidated here (the copies are identical)."""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.utils.parametrizations import weight_norm
|
||||
from torch.nn.utils.parametrize import remove_parametrizations
|
||||
|
||||
|
||||
def unpad1d(x: torch.Tensor, paddings: tuple[int, int]):
|
||||
"""Remove padding from x, handling properly zero padding. Only for 1d!"""
|
||||
padding_left, padding_right = paddings
|
||||
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
||||
assert (padding_left + padding_right) <= x.shape[-1]
|
||||
end = x.shape[-1] - padding_right
|
||||
return x[..., padding_left:end]
|
||||
|
||||
|
||||
def get_extra_padding_for_conv1d(x: torch.Tensor, kernel_size: int, stride: int, padding_total: int = 0) -> int:
|
||||
"""See `pad_for_conv1d`."""
|
||||
length = x.shape[-1]
|
||||
n_frames = (length - kernel_size + padding_total) / stride + 1
|
||||
ideal_length = (math.ceil(n_frames) - 1) * stride + (kernel_size - padding_total)
|
||||
return ideal_length - length
|
||||
|
||||
|
||||
def pad1d(
|
||||
x: torch.Tensor,
|
||||
paddings: tuple[int, int],
|
||||
mode: str = "zeros",
|
||||
value: float = 0.0,
|
||||
):
|
||||
"""Tiny wrapper around F.pad, just to allow for reflect padding on small input.
|
||||
If this is the case, we insert extra 0 padding to the right
|
||||
before the reflection happen.
|
||||
"""
|
||||
length = x.shape[-1]
|
||||
padding_left, padding_right = paddings
|
||||
assert padding_left >= 0 and padding_right >= 0, (padding_left, padding_right)
|
||||
if mode == "reflect":
|
||||
max_pad = max(padding_left, padding_right)
|
||||
extra_pad = 0
|
||||
if length <= max_pad:
|
||||
extra_pad = max_pad - length + 1
|
||||
x = F.pad(x, (0, extra_pad))
|
||||
padded = F.pad(x, paddings, mode, value)
|
||||
end = padded.shape[-1] - extra_pad
|
||||
return padded[..., :end]
|
||||
else:
|
||||
return F.pad(x, paddings, mode, value)
|
||||
|
||||
|
||||
class CausalConvNet(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
dilation=1,
|
||||
stride=1,
|
||||
groups=1,
|
||||
padding=None,
|
||||
):
|
||||
super().__init__()
|
||||
self.conv = nn.Conv1d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
dilation=dilation,
|
||||
groups=groups,
|
||||
)
|
||||
self.stride = stride
|
||||
self.kernel_size = (kernel_size - 1) * dilation + 1
|
||||
self.dilation = dilation
|
||||
self.padding = self.kernel_size - self.stride
|
||||
|
||||
def forward(self, x):
|
||||
pad = self.padding
|
||||
extra_padding = get_extra_padding_for_conv1d(x, self.kernel_size, self.stride, pad)
|
||||
x = pad1d(x, (pad, extra_padding), mode="constant", value=0)
|
||||
return self.conv(x).contiguous()
|
||||
|
||||
def weight_norm(self, name="weight", dim=0):
|
||||
self.conv = weight_norm(self.conv, name=name, dim=dim)
|
||||
return self
|
||||
|
||||
def remove_weight_norm(self):
|
||||
self.conv = remove_parametrizations(self.conv)
|
||||
return self
|
||||
|
||||
|
||||
class CausalTransConvNet(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, kernel_size, dilation=1, stride=1, padding=None):
|
||||
super().__init__()
|
||||
self.conv = nn.ConvTranspose1d(in_channels, out_channels, kernel_size, stride=stride, dilation=dilation)
|
||||
self.stride = stride
|
||||
self.kernel_size = kernel_size
|
||||
|
||||
def forward(self, x):
|
||||
x = self.conv(x)
|
||||
pad = self.kernel_size - self.stride
|
||||
padding_right = math.ceil(pad)
|
||||
padding_left = pad - padding_right
|
||||
x = unpad1d(x, (padding_left, padding_right))
|
||||
return x.contiguous()
|
||||
|
||||
def weight_norm(self, name="weight", dim=0):
|
||||
self.conv = weight_norm(self.conv, name=name, dim=dim)
|
||||
return self
|
||||
|
||||
def remove_weight_norm(self):
|
||||
self.conv = remove_parametrizations(self.conv)
|
||||
return self
|
||||
|
||||
|
||||
def CausalWNConv1d(*args, **kwargs):
|
||||
return CausalConvNet(*args, **kwargs).weight_norm()
|
||||
|
||||
|
||||
def CausalWNConvTranspose1d(*args, **kwargs):
|
||||
return CausalTransConvNet(*args, **kwargs).weight_norm()
|
||||
|
||||
|
||||
class ConvNeXtBlock(nn.Module):
|
||||
r"""ConvNeXt Block. There are two equivalent implementations:
|
||||
(1) DwConv -> LayerNorm (channels_first) -> 1x1 Conv -> GELU -> 1x1 Conv; all in (N, C, H, W)
|
||||
(2) DwConv -> Permute to (N, H, W, C); LayerNorm (channels_last) -> Linear -> GELU -> Linear; Permute back
|
||||
We use (2) as we find it slightly faster in PyTorch
|
||||
|
||||
Args:
|
||||
dim (int): Number of input channels.
|
||||
layer_scale_init_value (float): Init value for Layer Scale. Default: 1e-6.
|
||||
mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4.0.
|
||||
kernel_size (int): Kernel size for depthwise conv. Default: 7.
|
||||
dilation (int): Dilation for depthwise conv. Default: 1.
|
||||
""" # noqa: E501
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
layer_scale_init_value: float = 1e-6,
|
||||
mlp_ratio: float = 4.0,
|
||||
kernel_size: int = 7,
|
||||
dilation: int = 1,
|
||||
):
|
||||
super().__init__()
|
||||
convnet_type = CausalConvNet
|
||||
self.dwconv = convnet_type(
|
||||
dim,
|
||||
dim,
|
||||
kernel_size=kernel_size,
|
||||
groups=dim,
|
||||
dilation=dilation,
|
||||
) # depthwise conv
|
||||
self.norm = nn.LayerNorm(dim, eps=1e-6)
|
||||
# pointwise/1x1 convs, implemented with linear layers
|
||||
self.pwconv1 = nn.Linear(dim, int(mlp_ratio * dim))
|
||||
self.act = nn.GELU()
|
||||
self.pwconv2 = nn.Linear(int(mlp_ratio * dim), dim)
|
||||
self.gamma = (
|
||||
nn.Parameter(layer_scale_init_value * torch.ones(dim), requires_grad=True)
|
||||
if layer_scale_init_value > 0
|
||||
else None
|
||||
)
|
||||
|
||||
def forward(self, x, apply_residual: bool = True):
|
||||
input = x
|
||||
|
||||
x = self.dwconv(x)
|
||||
x = x.permute(0, 2, 1) # (N, C, L) -> (N, L, C)
|
||||
x = self.norm(x)
|
||||
x = self.pwconv1(x)
|
||||
x = self.act(x)
|
||||
x = self.pwconv2(x)
|
||||
|
||||
if self.gamma is not None:
|
||||
x = self.gamma * x
|
||||
|
||||
x = x.permute(0, 2, 1) # (N, L, C) -> (N, C, L)
|
||||
|
||||
if apply_residual:
|
||||
x = input + x
|
||||
|
||||
return x
|
||||
@@ -0,0 +1,238 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from https://github.com/descriptinc/descript-audio-codec (MIT License)
|
||||
# Copyright (c) 2023-present, Descript
|
||||
"""Descript audio codec building blocks (Snake activation, weight-normed convs,
|
||||
vector quantizers).
|
||||
|
||||
Uses the legacy ``torch.nn.utils.weight_norm`` (``weight_g``/``weight_v`` keys)
|
||||
exactly like upstream so quantizer projection weights in ``codec.pth`` load
|
||||
unchanged. The ``einops.rearrange`` calls are replaced with equivalent
|
||||
reshape/permute ops; ``from_latents`` (training-only) is dropped.
|
||||
"""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
|
||||
def WNConv1d(*args, **kwargs):
|
||||
return weight_norm(nn.Conv1d(*args, **kwargs))
|
||||
|
||||
|
||||
def WNConvTranspose1d(*args, **kwargs):
|
||||
return weight_norm(nn.ConvTranspose1d(*args, **kwargs))
|
||||
|
||||
|
||||
# Scripting this brings model speed up 1.4x
|
||||
@torch.jit.script
|
||||
def snake(x, alpha):
|
||||
shape = x.shape
|
||||
x = x.reshape(shape[0], shape[1], -1)
|
||||
x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
|
||||
x = x.reshape(shape)
|
||||
return x
|
||||
|
||||
|
||||
class Snake1d(nn.Module):
|
||||
def __init__(self, channels):
|
||||
super().__init__()
|
||||
self.alpha = nn.Parameter(torch.ones(1, channels, 1))
|
||||
|
||||
def forward(self, x):
|
||||
return snake(x, self.alpha)
|
||||
|
||||
|
||||
class VectorQuantize(nn.Module):
|
||||
"""
|
||||
Implementation of VQ similar to Karpathy's repo:
|
||||
https://github.com/karpathy/deep-vector-quantization
|
||||
Additionally uses following tricks from Improved VQGAN
|
||||
(https://arxiv.org/pdf/2110.04627.pdf):
|
||||
1. Factorized codes: Perform nearest neighbor lookup in low-dimensional space
|
||||
for improved codebook usage
|
||||
2. l2-normalized codes: Converts euclidean distance to cosine similarity which
|
||||
improves training stability
|
||||
"""
|
||||
|
||||
def __init__(self, input_dim: int, codebook_size: int, codebook_dim: int):
|
||||
super().__init__()
|
||||
self.codebook_size = codebook_size
|
||||
self.codebook_dim = codebook_dim
|
||||
|
||||
self.in_proj = WNConv1d(input_dim, codebook_dim, kernel_size=1)
|
||||
self.out_proj = WNConv1d(codebook_dim, input_dim, kernel_size=1)
|
||||
self.codebook = nn.Embedding(codebook_size, codebook_dim)
|
||||
|
||||
def forward(self, z):
|
||||
"""Quantized the input tensor using a fixed codebook and returns
|
||||
the corresponding codebook vectors
|
||||
|
||||
Parameters
|
||||
----------
|
||||
z : Tensor[B x D x T]
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tensor[B x D x T]
|
||||
Quantized continuous representation of input
|
||||
Tensor[1]
|
||||
Commitment loss to train encoder to predict vectors closer to codebook
|
||||
entries
|
||||
Tensor[1]
|
||||
Codebook loss to update the codebook
|
||||
Tensor[B x T]
|
||||
Codebook indices (quantized discrete representation of input)
|
||||
Tensor[B x D x T]
|
||||
Projected latents (continuous representation of input before quantization)
|
||||
"""
|
||||
|
||||
# Factorized codes (ViT-VQGAN) Project input into low-dimensional space
|
||||
z_e = self.in_proj(z) # z_e : (B x D x T)
|
||||
z_q, indices = self.decode_latents(z_e)
|
||||
|
||||
commitment_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2])
|
||||
codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2])
|
||||
|
||||
z_q = z_e + (z_q - z_e).detach() # noop in forward pass, straight-through gradient estimator in backward pass
|
||||
|
||||
z_q = self.out_proj(z_q)
|
||||
|
||||
return z_q, commitment_loss, codebook_loss, indices, z_e
|
||||
|
||||
def embed_code(self, embed_id):
|
||||
return F.embedding(embed_id, self.codebook.weight)
|
||||
|
||||
def decode_code(self, embed_id):
|
||||
return self.embed_code(embed_id).transpose(1, 2)
|
||||
|
||||
def decode_latents(self, latents):
|
||||
batch, dim, time = latents.shape
|
||||
# rearrange "b d t -> (b t) d"
|
||||
encodings = latents.permute(0, 2, 1).reshape(batch * time, dim)
|
||||
codebook = self.codebook.weight # codebook: (N x D)
|
||||
|
||||
# L2 normalize encodings and codebook (ViT-VQGAN)
|
||||
encodings = F.normalize(encodings)
|
||||
codebook = F.normalize(codebook)
|
||||
|
||||
# Compute euclidean distance with codebook
|
||||
dist = (
|
||||
encodings.pow(2).sum(1, keepdim=True)
|
||||
- 2 * encodings @ codebook.t()
|
||||
+ codebook.pow(2).sum(1, keepdim=True).t()
|
||||
)
|
||||
# rearrange "(b t) -> b t"
|
||||
indices = (-dist).max(1)[1].reshape(batch, time)
|
||||
z_q = self.decode_code(indices)
|
||||
return z_q, indices
|
||||
|
||||
|
||||
class ResidualVectorQuantize(nn.Module):
|
||||
"""
|
||||
Introduced in SoundStream: An end2end neural audio codec
|
||||
https://arxiv.org/abs/2107.03312
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int = 512,
|
||||
n_codebooks: int = 9,
|
||||
codebook_size: int = 1024,
|
||||
codebook_dim: int | list = 8,
|
||||
quantizer_dropout: float = 0.0,
|
||||
):
|
||||
super().__init__()
|
||||
if isinstance(codebook_dim, int):
|
||||
codebook_dim = [codebook_dim for _ in range(n_codebooks)]
|
||||
|
||||
self.n_codebooks = n_codebooks
|
||||
self.codebook_dim = codebook_dim
|
||||
self.codebook_size = codebook_size
|
||||
|
||||
self.quantizers = nn.ModuleList(
|
||||
[VectorQuantize(input_dim, codebook_size, codebook_dim[i]) for i in range(n_codebooks)]
|
||||
)
|
||||
self.quantizer_dropout = quantizer_dropout
|
||||
|
||||
def forward(self, z, n_quantizers: int = None):
|
||||
"""Quantized the input tensor using a fixed set of `n` codebooks and returns
|
||||
the corresponding codebook vectors
|
||||
|
||||
Parameters
|
||||
----------
|
||||
z : Tensor[B x D x T]
|
||||
n_quantizers : int, optional
|
||||
No. of quantizers to use
|
||||
(n_quantizers < self.n_codebooks ex: for quantizer dropout)
|
||||
Note: if `self.quantizer_dropout` is True, this argument is ignored
|
||||
when in training mode, and a random number of quantizers is used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tuple of (z_q, codes, latents, commitment_loss, codebook_loss).
|
||||
"""
|
||||
z_q = 0
|
||||
residual = z
|
||||
commitment_loss = 0
|
||||
codebook_loss = 0
|
||||
|
||||
codebook_indices = []
|
||||
latents = []
|
||||
|
||||
if n_quantizers is None:
|
||||
n_quantizers = self.n_codebooks
|
||||
if self.training:
|
||||
n_quantizers = torch.ones((z.shape[0],)) * self.n_codebooks + 1
|
||||
dropout = torch.randint(1, self.n_codebooks + 1, (z.shape[0],))
|
||||
n_dropout = int(z.shape[0] * self.quantizer_dropout)
|
||||
n_quantizers[:n_dropout] = dropout[:n_dropout]
|
||||
n_quantizers = n_quantizers.to(z.device)
|
||||
|
||||
for i, quantizer in enumerate(self.quantizers):
|
||||
if self.training is False and i >= n_quantizers:
|
||||
break
|
||||
|
||||
z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(residual)
|
||||
|
||||
# Create mask to apply quantizer dropout
|
||||
mask = torch.full((z.shape[0],), fill_value=i, device=z.device) < n_quantizers
|
||||
z_q = z_q + z_q_i * mask[:, None, None]
|
||||
residual = residual - z_q_i
|
||||
|
||||
# Sum losses
|
||||
commitment_loss += (commitment_loss_i * mask).mean()
|
||||
codebook_loss += (codebook_loss_i * mask).mean()
|
||||
|
||||
codebook_indices.append(indices_i)
|
||||
latents.append(z_e_i)
|
||||
|
||||
codes = torch.stack(codebook_indices, dim=1)
|
||||
latents = torch.cat(latents, dim=1)
|
||||
|
||||
return z_q, codes, latents, commitment_loss, codebook_loss
|
||||
|
||||
def from_codes(self, codes: torch.Tensor):
|
||||
"""Given the quantized codes, reconstruct the continuous representation
|
||||
|
||||
Parameters
|
||||
----------
|
||||
codes : Tensor[B x N x T]
|
||||
Quantized discrete representation of input
|
||||
|
||||
Returns
|
||||
-------
|
||||
Tensor[B x D x T]
|
||||
Quantized continuous representation of input
|
||||
"""
|
||||
z_q = 0.0
|
||||
z_p = []
|
||||
n_codebooks = codes.shape[1]
|
||||
for i in range(n_codebooks):
|
||||
z_p_i = self.quantizers[i].decode_code(codes[:, i, :])
|
||||
z_p.append(z_p_i)
|
||||
|
||||
z_q_i = self.quantizers[i].out_proj(z_p_i)
|
||||
z_q = z_q + z_q_i
|
||||
return z_q, torch.cat(z_p, dim=1), codes
|
||||
@@ -0,0 +1,177 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from the fish-speech 0.1.0 PyPI release (Apache-2.0)
|
||||
# https://pypi.org/project/fish-speech/0.1.0/
|
||||
# Copyright (c) Fish Audio
|
||||
"""Downsampled residual vector quantizer (1 semantic + N residual codebooks)."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.conv import (
|
||||
CausalConvNet,
|
||||
CausalTransConvNet,
|
||||
ConvNeXtBlock,
|
||||
)
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules.layers import (
|
||||
ResidualVectorQuantize,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VQResult:
|
||||
z: torch.Tensor
|
||||
codes: torch.Tensor
|
||||
latents: torch.Tensor
|
||||
codebook_loss: torch.Tensor
|
||||
commitment_loss: torch.Tensor
|
||||
semantic_distill_z: torch.Tensor | None = None
|
||||
|
||||
|
||||
class DownsampleResidualVectorQuantize(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int = 1024,
|
||||
n_codebooks: int = 9,
|
||||
codebook_dim: int = 8,
|
||||
quantizer_dropout: float = 0.5,
|
||||
codebook_size: int = 1024,
|
||||
semantic_codebook_size: int = 4096,
|
||||
downsample_factor: tuple[int] = (2, 2),
|
||||
downsample_dims: tuple[int] | None = None,
|
||||
pre_module: nn.Module | None = None,
|
||||
post_module: nn.Module | None = None,
|
||||
semantic_predictor_module: nn.Module | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
if downsample_dims is None:
|
||||
downsample_dims = [input_dim for _ in range(len(downsample_factor))]
|
||||
|
||||
all_dims = (input_dim,) + tuple(downsample_dims)
|
||||
|
||||
self.semantic_quantizer = ResidualVectorQuantize(
|
||||
input_dim=input_dim,
|
||||
n_codebooks=1,
|
||||
codebook_size=semantic_codebook_size,
|
||||
codebook_dim=codebook_dim,
|
||||
quantizer_dropout=0.0,
|
||||
)
|
||||
|
||||
self.quantizer = ResidualVectorQuantize(
|
||||
input_dim=input_dim,
|
||||
n_codebooks=n_codebooks,
|
||||
codebook_size=codebook_size,
|
||||
codebook_dim=codebook_dim,
|
||||
quantizer_dropout=quantizer_dropout,
|
||||
)
|
||||
|
||||
self.downsample_factor = downsample_factor
|
||||
self.downsample_dims = downsample_dims
|
||||
|
||||
convnet_type = CausalConvNet
|
||||
transconvnet_type = CausalTransConvNet
|
||||
|
||||
self.downsample = nn.Sequential(
|
||||
*[
|
||||
nn.Sequential(
|
||||
convnet_type(
|
||||
all_dims[idx],
|
||||
all_dims[idx + 1],
|
||||
kernel_size=factor,
|
||||
stride=factor,
|
||||
),
|
||||
ConvNeXtBlock(dim=all_dims[idx + 1]),
|
||||
)
|
||||
for idx, factor in enumerate(downsample_factor)
|
||||
]
|
||||
)
|
||||
|
||||
self.upsample = nn.Sequential(
|
||||
*[
|
||||
nn.Sequential(
|
||||
transconvnet_type(
|
||||
all_dims[idx + 1],
|
||||
all_dims[idx],
|
||||
kernel_size=factor,
|
||||
stride=factor,
|
||||
),
|
||||
ConvNeXtBlock(dim=all_dims[idx]),
|
||||
)
|
||||
for idx, factor in reversed(list(enumerate(downsample_factor)))
|
||||
]
|
||||
)
|
||||
self.apply(self._init_weights)
|
||||
# leave for transformer, LSTM or Mamba or something else
|
||||
self.pre_module = pre_module if pre_module is not None else nn.Identity()
|
||||
self.post_module = post_module if post_module is not None else nn.Identity()
|
||||
self.semantic_predictor_module = (
|
||||
semantic_predictor_module if semantic_predictor_module is not None else nn.Identity()
|
||||
)
|
||||
|
||||
def _init_weights(self, m):
|
||||
if isinstance(m, (nn.Conv1d, nn.Linear)):
|
||||
nn.init.trunc_normal_(m.weight, std=0.02)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
def forward(self, z, n_quantizers: int = None, semantic_len: torch.Tensor = None, **kwargs):
|
||||
# z: (B, D, T)
|
||||
original_shape = z.shape
|
||||
if semantic_len is None:
|
||||
semantic_len = torch.LongTensor([z.shape[-1]])
|
||||
z = self.downsample(z)
|
||||
z = self.pre_module(z) # B, T, D
|
||||
(
|
||||
semantic_z,
|
||||
semantic_codes,
|
||||
semantic_latents,
|
||||
semantic_commitment_loss,
|
||||
semantic_codebook_loss,
|
||||
) = self.semantic_quantizer(z)
|
||||
residual_z = z - semantic_z
|
||||
residual_z, codes, latents, commitment_loss, codebook_loss = self.quantizer(
|
||||
residual_z, n_quantizers=n_quantizers
|
||||
)
|
||||
z = semantic_z + residual_z
|
||||
commitment_loss = commitment_loss + semantic_commitment_loss
|
||||
codebook_loss = codebook_loss + semantic_codebook_loss
|
||||
codes = torch.cat([semantic_codes, codes], dim=1)
|
||||
latents = torch.cat([semantic_latents, latents], dim=1)
|
||||
z = self.post_module(z)
|
||||
z = self.upsample(z)
|
||||
# z: (B, D, T)
|
||||
|
||||
# Pad or crop z to match original shape
|
||||
diff = original_shape[-1] - z.shape[-1]
|
||||
right = 0
|
||||
left = abs(diff) - right
|
||||
|
||||
if diff > 0:
|
||||
z = F.pad(z, (left, right))
|
||||
elif diff < 0:
|
||||
z = z[..., left:]
|
||||
|
||||
results = VQResult(
|
||||
z=z,
|
||||
codes=codes,
|
||||
latents=latents,
|
||||
commitment_loss=commitment_loss,
|
||||
codebook_loss=codebook_loss,
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
def decode(self, indices: torch.Tensor):
|
||||
new_indices = torch.zeros_like(indices)
|
||||
new_indices[:, 0] = torch.clamp(indices[:, 0], max=self.semantic_quantizer.codebook_size - 1)
|
||||
new_indices[:, 1:] = torch.clamp(indices[:, 1:], max=self.quantizer.codebook_size - 1)
|
||||
|
||||
z_q_semantic = self.semantic_quantizer.from_codes(new_indices[:, :1])[0]
|
||||
z_q_residual = self.quantizer.from_codes(new_indices[:, 1:])[0]
|
||||
z_q = z_q_semantic + z_q_residual
|
||||
z_q = self.post_module(z_q)
|
||||
z_q = self.upsample(z_q)
|
||||
return z_q
|
||||
@@ -0,0 +1,419 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# Adopted from the fish-speech 0.1.0 PyPI release (Apache-2.0)
|
||||
# https://pypi.org/project/fish-speech/0.1.0/
|
||||
# Copyright (c) Fish Audio
|
||||
"""Window-limited transformer used inside the DAC codec (encoder blocks,
|
||||
decoder blocks, and RVQ pre/post modules)."""
|
||||
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
from torch import Tensor, nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
|
||||
def find_multiple(n: int, k: int) -> int:
|
||||
if n % k == 0:
|
||||
return n
|
||||
return n + k - (n % k)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs:
|
||||
block_size: int = 2048
|
||||
n_layer: int = 8
|
||||
n_head: int = 8
|
||||
dim: int = 512
|
||||
intermediate_size: int = 1536
|
||||
n_local_heads: int = -1
|
||||
head_dim: int = 64
|
||||
rope_base: float = 10000
|
||||
norm_eps: float = 1e-5
|
||||
dropout_rate: float = 0.1
|
||||
attn_dropout_rate: float = 0.1
|
||||
channels_first: bool = True # to be compatible with conv1d input/output
|
||||
pos_embed_type: str = "rope" # can be "rope" or "conformer"
|
||||
max_relative_position: int = 128 # for conformer-style relative position embedding
|
||||
|
||||
def __post_init__(self):
|
||||
if self.n_local_heads == -1:
|
||||
self.n_local_heads = self.n_head
|
||||
if self.intermediate_size is None:
|
||||
hidden_dim = 4 * self.dim
|
||||
n_hidden = int(2 * hidden_dim / 3)
|
||||
self.intermediate_size = find_multiple(n_hidden, 256)
|
||||
assert self.pos_embed_type in [
|
||||
"rope",
|
||||
"conformer",
|
||||
], "pos_embed_type must be either 'rope' or 'conformer'"
|
||||
|
||||
|
||||
class KVCache(nn.Module):
|
||||
def __init__(self, max_batch_size, max_seq_length, n_heads, head_dim, dtype=torch.bfloat16):
|
||||
super().__init__()
|
||||
cache_shape = (max_batch_size, n_heads, max_seq_length, head_dim)
|
||||
self.register_buffer("k_cache", torch.zeros(cache_shape, dtype=dtype))
|
||||
self.register_buffer("v_cache", torch.zeros(cache_shape, dtype=dtype))
|
||||
|
||||
def update(self, input_pos, k_val, v_val):
|
||||
# input_pos: [S], k_val: [B, H, S, D]
|
||||
assert input_pos.shape[0] == k_val.shape[2]
|
||||
|
||||
k_out = self.k_cache
|
||||
v_out = self.v_cache
|
||||
k_out[:, :, input_pos] = k_val
|
||||
v_out[:, :, input_pos] = v_val
|
||||
|
||||
return (
|
||||
k_out[:, :, : input_pos.max() + 1, :],
|
||||
v_out[:, :, : input_pos.max() + 1, :],
|
||||
)
|
||||
|
||||
def clear_cache(self, prompt_len):
|
||||
self.k_cache[:, :, prompt_len:, :].fill_(0)
|
||||
self.v_cache[:, :, prompt_len:, :].fill_(0)
|
||||
|
||||
|
||||
class Transformer(nn.Module):
|
||||
def __init__(self, config: ModelArgs) -> None:
|
||||
super().__init__()
|
||||
self.config = config
|
||||
|
||||
self.layers = nn.ModuleList(TransformerBlock(config) for _ in range(config.n_layer))
|
||||
self.norm = RMSNorm(config.dim, eps=config.norm_eps)
|
||||
|
||||
# Only compute RoPE frequencies if using RoPE
|
||||
if config.pos_embed_type == "rope":
|
||||
freqs_cis = precompute_freqs_cis(self.config.block_size, self.config.head_dim, self.config.rope_base)
|
||||
self.register_buffer("freqs_cis", freqs_cis)
|
||||
else:
|
||||
self.register_buffer("freqs_cis", None)
|
||||
|
||||
causal_mask = torch.tril(torch.ones(self.config.block_size, self.config.block_size, dtype=torch.bool))
|
||||
self.register_buffer("causal_mask", causal_mask)
|
||||
|
||||
self.max_batch_size = -1
|
||||
self.max_seq_length = -1
|
||||
self.use_kv_cache = False
|
||||
|
||||
def setup_caches(self, max_batch_size, max_seq_length):
|
||||
"""
|
||||
This method will only be called during inference when using KV cache.
|
||||
"""
|
||||
head_dim = self.config.dim // self.config.n_head
|
||||
max_seq_length = find_multiple(max_seq_length, 8)
|
||||
self.max_seq_length = max_seq_length
|
||||
self.max_batch_size = max_batch_size
|
||||
dtype = self.norm.weight.dtype
|
||||
device = self.norm.weight.device
|
||||
|
||||
for b in self.layers:
|
||||
b.attention.kv_cache = KVCache(
|
||||
max_batch_size,
|
||||
max_seq_length,
|
||||
self.config.n_local_heads,
|
||||
head_dim,
|
||||
dtype,
|
||||
).to(device)
|
||||
|
||||
self.use_kv_cache = True
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
input_pos: Tensor | None = None,
|
||||
mask: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
if self.config.pos_embed_type == "rope":
|
||||
assert self.freqs_cis is not None, "RoPE frequencies must be initialized for RoPE positional embedding"
|
||||
freqs_cis = self.freqs_cis[input_pos]
|
||||
else:
|
||||
freqs_cis = None
|
||||
|
||||
if mask is None: # in case of non-causal model
|
||||
if not self.training and self.use_kv_cache:
|
||||
mask = self.causal_mask[None, None, input_pos]
|
||||
mask = mask[..., : input_pos.max() + 1]
|
||||
else:
|
||||
mask = self.causal_mask[None, None, input_pos]
|
||||
mask = mask[..., input_pos]
|
||||
|
||||
for i, layer in enumerate(self.layers):
|
||||
x = layer(x, input_pos, freqs_cis, mask)
|
||||
x = self.norm(x)
|
||||
return x
|
||||
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, config: ModelArgs) -> None:
|
||||
super().__init__()
|
||||
self.attention = Attention(config)
|
||||
self.feed_forward = FeedForward(config)
|
||||
self.ffn_norm = RMSNorm(config.dim, eps=config.norm_eps)
|
||||
self.attention_norm = RMSNorm(config.dim, eps=config.norm_eps)
|
||||
self.attention_layer_scale = LayerScale(config.dim, inplace=True)
|
||||
self.ffn_layer_scale = LayerScale(config.dim, inplace=True)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
input_pos: Tensor,
|
||||
freqs_cis: Tensor,
|
||||
mask: Tensor,
|
||||
) -> Tensor:
|
||||
h = x + self.attention_layer_scale(self.attention(self.attention_norm(x), freqs_cis, mask, input_pos))
|
||||
out = h + self.ffn_layer_scale(self.feed_forward(self.ffn_norm(h)))
|
||||
return out
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs):
|
||||
super().__init__()
|
||||
assert config.dim % config.n_head == 0
|
||||
|
||||
total_head_dim = (config.n_head + 2 * config.n_local_heads) * config.head_dim
|
||||
# key, query, value projections for all heads, but in a batch
|
||||
self.wqkv = nn.Linear(config.dim, total_head_dim, bias=False)
|
||||
self.wo = nn.Linear(config.head_dim * config.n_head, config.dim, bias=False)
|
||||
self.kv_cache = None
|
||||
|
||||
self.n_head = config.n_head
|
||||
self.head_dim = config.head_dim
|
||||
self.n_local_heads = config.n_local_heads
|
||||
self.dim = config.dim
|
||||
self.attn_dropout_rate = config.attn_dropout_rate
|
||||
self.pos_embed_type = config.pos_embed_type
|
||||
|
||||
# Add relative position embedding for conformer-style
|
||||
if self.pos_embed_type == "conformer":
|
||||
self.max_relative_position = config.max_relative_position
|
||||
num_pos_embeddings = 2 * config.max_relative_position + 1
|
||||
self.rel_pos_embeddings = nn.Parameter(torch.zeros(num_pos_embeddings, self.head_dim))
|
||||
nn.init.normal_(self.rel_pos_embeddings, mean=0.0, std=0.02)
|
||||
|
||||
def _compute_conformer_pos_scores(self, q: Tensor, seqlen: int) -> Tensor:
|
||||
# q: [B, H, S, D]
|
||||
# Returns: [B, H, S, S]
|
||||
positions = torch.arange(seqlen, device=q.device)
|
||||
relative_positions = positions.unsqueeze(1) - positions.unsqueeze(0) # [S, S]
|
||||
relative_positions = torch.clamp(
|
||||
relative_positions + self.max_relative_position,
|
||||
0,
|
||||
2 * self.max_relative_position,
|
||||
)
|
||||
rel_embeddings = self.rel_pos_embeddings[relative_positions] # [S, S, D]
|
||||
|
||||
# Compute attention scores with relative position embeddings
|
||||
q = q.transpose(1, 2) # [B, S, H, D]
|
||||
rel_logits = torch.matmul(q, rel_embeddings.transpose(-2, -1)) # [B, S, H, S]
|
||||
rel_logits = rel_logits.transpose(1, 2) # [B, H, S, S]
|
||||
return rel_logits
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
freqs_cis: Tensor,
|
||||
mask: Tensor,
|
||||
input_pos: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
bsz, seqlen, _ = x.shape
|
||||
|
||||
kv_size = self.n_local_heads * self.head_dim
|
||||
q, k, v = self.wqkv(x).split([kv_size, kv_size, kv_size], dim=-1)
|
||||
context_seqlen = seqlen
|
||||
|
||||
q = q.view(bsz, seqlen, self.n_head, self.head_dim)
|
||||
k = k.view(bsz, context_seqlen, self.n_local_heads, self.head_dim)
|
||||
v = v.view(bsz, context_seqlen, self.n_local_heads, self.head_dim)
|
||||
|
||||
if self.pos_embed_type == "rope":
|
||||
q = apply_rotary_emb(q, freqs_cis)
|
||||
k = apply_rotary_emb(k, freqs_cis)
|
||||
|
||||
q, k, v = map(lambda x: x.transpose(1, 2), (q, k, v))
|
||||
|
||||
if self.kv_cache is not None:
|
||||
k, v = self.kv_cache.update(input_pos, k, v)
|
||||
|
||||
k = k.repeat_interleave(self.n_head // self.n_local_heads, dim=1)
|
||||
v = v.repeat_interleave(self.n_head // self.n_local_heads, dim=1)
|
||||
|
||||
if self.pos_embed_type == "conformer":
|
||||
# Compute attention scores
|
||||
scale = 1.0 / math.sqrt(self.head_dim)
|
||||
scores = torch.matmul(q, k.transpose(-2, -1)) * scale
|
||||
|
||||
# Add relative position embeddings for conformer-style
|
||||
rel_scores = self._compute_conformer_pos_scores(q, seqlen)
|
||||
scores = scores + rel_scores
|
||||
|
||||
# Apply attention
|
||||
if mask is not None:
|
||||
scores = scores.masked_fill(~mask, float("-inf"))
|
||||
|
||||
attn = F.softmax(scores, dim=-1)
|
||||
if self.attn_dropout_rate > 0 and self.training:
|
||||
attn = F.dropout(attn, p=self.attn_dropout_rate)
|
||||
|
||||
y = torch.matmul(attn, v)
|
||||
else:
|
||||
y = F.scaled_dot_product_attention(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
dropout_p=self.attn_dropout_rate if self.training else 0.0,
|
||||
attn_mask=mask,
|
||||
)
|
||||
y = y.transpose(1, 2).contiguous().view(bsz, seqlen, self.head_dim * self.n_head)
|
||||
y = self.wo(y)
|
||||
return y
|
||||
|
||||
|
||||
class FeedForward(nn.Module):
|
||||
def __init__(self, config: ModelArgs) -> None:
|
||||
super().__init__()
|
||||
self.w1 = nn.Linear(config.dim, config.intermediate_size, bias=False)
|
||||
self.w3 = nn.Linear(config.dim, config.intermediate_size, bias=False)
|
||||
self.w2 = nn.Linear(config.intermediate_size, config.dim, bias=False)
|
||||
self.dropout = nn.Dropout(config.dropout_rate)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
return self.w2(self.dropout(F.silu(self.w1(x)) * self.w3(x)))
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, dim: int, eps: float = 1e-5):
|
||||
super().__init__()
|
||||
self.eps = eps
|
||||
self.weight = nn.Parameter(torch.ones(dim))
|
||||
|
||||
def _norm(self, x):
|
||||
return x * torch.rsqrt(torch.mean(x * x, dim=-1, keepdim=True) + self.eps)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
output = self._norm(x.float()).type_as(x)
|
||||
return output * self.weight
|
||||
|
||||
|
||||
class LayerScale(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
init_values: float | Tensor = 1e-2,
|
||||
inplace: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.inplace = inplace
|
||||
self.gamma = nn.Parameter(init_values * torch.ones(dim))
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
return x.mul_(self.gamma) if self.inplace else x * self.gamma
|
||||
|
||||
|
||||
class WindowLimitedTransformer(Transformer):
|
||||
"""
|
||||
Transformer with window limited attention, causal.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: ModelArgs,
|
||||
input_dim: int = 512,
|
||||
window_size: int | None = None,
|
||||
causal: bool = True,
|
||||
look_ahead_conv: nn.Module = None,
|
||||
):
|
||||
super().__init__(config)
|
||||
self.window_size = window_size
|
||||
self.causal = causal
|
||||
self.channels_first = config.channels_first
|
||||
self.look_ahead_conv = look_ahead_conv if look_ahead_conv is not None else nn.Identity()
|
||||
self.input_proj = nn.Linear(input_dim, config.dim) if input_dim != config.dim else nn.Identity()
|
||||
self.output_proj = nn.Linear(config.dim, input_dim) if input_dim != config.dim else nn.Identity()
|
||||
|
||||
def make_window_limited_mask(
|
||||
self,
|
||||
max_length: int,
|
||||
x_lens: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
"""
|
||||
Make mask to form window limited attention.
|
||||
"""
|
||||
if self.causal:
|
||||
mask = torch.tril(torch.ones(max_length, max_length))
|
||||
row_indices = torch.arange(max_length).view(-1, 1)
|
||||
window_size = self.window_size or max_length
|
||||
valid_range = (row_indices - window_size + 1).clamp(min=0)
|
||||
column_indices = torch.arange(max_length)
|
||||
mask = (column_indices >= valid_range) & mask.bool()
|
||||
else:
|
||||
raise NotImplementedError
|
||||
mask = mask.bool()[None, None]
|
||||
return mask
|
||||
|
||||
def make_mask(
|
||||
self,
|
||||
max_length: int,
|
||||
x_lens: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
"""
|
||||
Make ordinary mask if window size is not specified.
|
||||
"""
|
||||
if self.causal:
|
||||
mask = torch.tril(torch.ones(max_length, max_length))
|
||||
else:
|
||||
mask = torch.ones(max_length, max_length)
|
||||
mask = mask.bool()[None, None]
|
||||
for i, x_len in enumerate(x_lens):
|
||||
mask[:x_len, i] = 0
|
||||
mask = mask.bool()[None, None]
|
||||
return mask
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
x_lens: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
if self.channels_first:
|
||||
x = x.transpose(1, 2)
|
||||
x = self.input_proj(x) # (B, T, D)
|
||||
x = self.look_ahead_conv(x)
|
||||
input_pos = torch.arange(x.shape[1], device=x.device)
|
||||
# construct mask to form window limited attention
|
||||
max_length = x.shape[1]
|
||||
if self.window_size is not None:
|
||||
mask = self.make_window_limited_mask(max_length, x_lens)
|
||||
else:
|
||||
mask = self.make_mask(max_length, x_lens)
|
||||
mask = mask.to(x.device)
|
||||
x = super().forward(x, input_pos, mask)
|
||||
x = self.output_proj(x) # (B, T, D)
|
||||
if self.channels_first:
|
||||
x = x.transpose(1, 2)
|
||||
return x
|
||||
|
||||
|
||||
def precompute_freqs_cis(seq_len: int, n_elem: int, base: int = 10000, dtype: torch.dtype = torch.bfloat16) -> Tensor:
|
||||
freqs = 1.0 / (base ** (torch.arange(0, n_elem, 2)[: (n_elem // 2)].float() / n_elem))
|
||||
t = torch.arange(seq_len, device=freqs.device)
|
||||
freqs = torch.outer(t, freqs)
|
||||
freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
|
||||
cache = torch.stack([freqs_cis.real, freqs_cis.imag], dim=-1)
|
||||
return cache.to(dtype=dtype)
|
||||
|
||||
|
||||
def apply_rotary_emb(x: Tensor, freqs_cis: Tensor) -> Tensor:
|
||||
xshaped = x.float().reshape(*x.shape[:-1], -1, 2)
|
||||
freqs_cis = freqs_cis.view(1, xshaped.size(1), 1, xshaped.size(3), 2)
|
||||
x_out2 = torch.stack(
|
||||
[
|
||||
xshaped[..., 0] * freqs_cis[..., 0] - xshaped[..., 1] * freqs_cis[..., 1],
|
||||
xshaped[..., 1] * freqs_cis[..., 0] + xshaped[..., 0] * freqs_cis[..., 1],
|
||||
],
|
||||
-1,
|
||||
)
|
||||
|
||||
x_out2 = x_out2.flatten(3)
|
||||
return x_out2.type_as(x)
|
||||
@@ -19,12 +19,12 @@ def build_dac_codec() -> nn.Module:
|
||||
Returns the model on CPU in eval mode -- caller is responsible for
|
||||
loading weights and moving to the target device.
|
||||
"""
|
||||
from fish_speech.models.dac.modded_dac import (
|
||||
from vllm_omni.model_executor.models.fish_speech.dac_modules import (
|
||||
DAC,
|
||||
DownsampleResidualVectorQuantize,
|
||||
ModelArgs,
|
||||
WindowLimitedTransformer,
|
||||
)
|
||||
from fish_speech.models.dac.rvq import DownsampleResidualVectorQuantize
|
||||
|
||||
base_transformer_kwargs = dict(
|
||||
block_size=16384,
|
||||
|
||||
@@ -5,8 +5,8 @@ Loads the DAC codec from ``codec.pth`` and decodes codebook indices
|
||||
|
||||
Analogous to ``Qwen3TTSCode2Wav`` in qwen3_tts.
|
||||
|
||||
Requires the ``fish-speech`` package for the DAC model architecture.
|
||||
Install with: ``pip install fish-speech``
|
||||
The DAC model architecture is vendored in ``dac_modules`` -- no external
|
||||
``fish-speech`` package is required.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
Reference in new issue
Block a user