[Model] Add IndexTTS2 text-to-speech support (#3838)
Signed-off-by: BeatSeat <wendavid552@gmail.com> Signed-off-by: David <david@podcast.ai> Signed-off-by: wendavid552 <wendavid552@gmail.com> Signed-off-by: WenDavid <davidzhangcf@foxmail.com>
This commit is contained in:
1 parent
f4d922d8f6
commit
044240d8eb
66 files changed
+8634
-2
No files matched your search
@@ -363,6 +363,42 @@ steps:
|
||||
path: /mnt/hf-cache
|
||||
type: DirectoryOrCreate
|
||||
|
||||
- label: "TTS · IndexTTS2 Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/online_serving/test_indextts2.py
|
||||
- tests/e2e/offline_inference/test_indextts2.py
|
||||
- tests/model_executor/models/indextts2/
|
||||
- tests/model_executor/stage_input_processors/test_indextts2.py
|
||||
- tests/assets/indextts2/
|
||||
- vllm_omni/model_executor/models/indextts2/
|
||||
- vllm_omni/model_executor/stage_input_processors/indextts2.py
|
||||
- vllm_omni/deploy/indextts2.yaml
|
||||
- vllm_omni/deploy/indextts2_low_latency.yaml
|
||||
- vllm_omni/entrypoints/openai/serving_speech.py
|
||||
- vllm_omni/entrypoints/openai/tts_adapters/indextts2.py
|
||||
- vllm_omni/config/pipeline_registry.py
|
||||
- vllm_omni/model_executor/models/registry.py
|
||||
timeout_in_minutes: 20
|
||||
commands:
|
||||
- |
|
||||
timeout 20m bash -c "
|
||||
export VLLM_LOGGING_LEVEL=DEBUG
|
||||
pytest -s -v tests/e2e/online_serving/test_indextts2.py tests/e2e/offline_inference/test_indextts2.py -m 'advanced_model and cuda' --run-level 'advanced_model'
|
||||
"
|
||||
agents:
|
||||
queue: "gpu_4_queue"
|
||||
plugins:
|
||||
- docker#v5.2.0:
|
||||
image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT
|
||||
always-pull: true
|
||||
propagate-environment: true
|
||||
environment:
|
||||
- "HF_HOME=/fsx/hf_cache"
|
||||
- "HF_TOKEN"
|
||||
gpus: all
|
||||
volumes:
|
||||
- "/fsx/hf_cache:/fsx/hf_cache"
|
||||
|
||||
- label: "Omni · Qwen3-Omni Test"
|
||||
source_file_dependencies:
|
||||
- tests/e2e/offline_inference/test_qwen3_omni.py
|
||||
|
||||
@@ -74,6 +74,7 @@ th {
|
||||
| `MossTTSRealtime` | MOSS-TTS-Realtime | `OpenMOSS-Team/MOSS-TTS-Realtime` | ✅︎ | | | |
|
||||
| `HiggsAudioV2ForConditionalGeneration` | Higgs-Audio v2 | `bosonai/higgs-audio-v2-generation-3B-base` | ✅︎ | | | |
|
||||
| `HiggsMultimodalQwen3ForConditionalGeneration` | Higgs-Audio v3 (TTS) | `bosonai/higgs-audio-v3-tts-4b` | ✅︎ | | | |
|
||||
| `IndexTTS2TalkerForConditionalGeneration` | IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✅︎ | | | |
|
||||
| `NextStep11Pipeline` | NextStep-1.1 | `stepfun-ai/NextStep-1.1` | ✅︎ | ✅︎ | | ✅︎ |
|
||||
| `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | |
|
||||
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | |
|
||||
|
||||
@@ -23,6 +23,7 @@ list of supported architectures across all modalities, see
|
||||
| OmniVoice | `k2-fsa/OmniVoice` | 2 (gen + dec) | ✓ | — | voice design, language hint | 24 kHz |
|
||||
| Qwen3-TTS | `Qwen/Qwen3-TTS-12Hz-1.7B-{CustomVoice,VoiceDesign,Base}` | 2 (talker + code2wav) | ✓ (Base) | ✓ | 3 task variants | 24 kHz |
|
||||
| VoxCPM2 | `openbmb/VoxCPM2` | single (native AR) | ✓ | ✓ (online) | continuation | 48 kHz |
|
||||
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | 2 (AR talker + S2Mel DiT + BigVGAN) | ✓ (required) | — | emotion control (`--emo-audio`, `--emo-text`, `--emo-vector`) | 22.05 kHz |
|
||||
| Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | varies | ✓ | ✓ | voice presets | 24 kHz |
|
||||
|
||||
## Common Quick Start
|
||||
@@ -436,6 +437,50 @@ Streaming is exposed through the online OpenAI Speech API (`stream=true`). See [
|
||||
|
||||
---
|
||||
|
||||
## IndexTTS-2
|
||||
|
||||
2-stage TTS pipeline (GPT AR talker + S2Mel CFM DiT + BigVGAN vocoder) at 22.05 kHz. Every request requires reference audio for zero-shot voice cloning. Supports emotion conditioning via audio, text, or 8-dim vector.
|
||||
|
||||
### Quick start
|
||||
```bash
|
||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
||||
--model IndexTeam/IndexTTS-2 \
|
||||
--text "你好,这是一个语音合成测试。" \
|
||||
--ref-audio /path/to/reference.wav
|
||||
```
|
||||
|
||||
### Emotion control
|
||||
```bash
|
||||
# Emotion from reference audio
|
||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
||||
--model IndexTeam/IndexTTS-2 \
|
||||
--text "今天天气真好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-audio /path/to/happy.wav
|
||||
|
||||
# Emotion from 8-dim vector (happy angry sad afraid disgusted melancholy surprised calm)
|
||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
||||
--model IndexTeam/IndexTTS-2 \
|
||||
--text "今天天气真好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-vector 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
|
||||
|
||||
# Emotion from text description
|
||||
python examples/offline_inference/text_to_speech/indextts2/end2end.py \
|
||||
--model IndexTeam/IndexTTS-2 \
|
||||
--text "今天天气真好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-text "happy and excited"
|
||||
```
|
||||
|
||||
### Notes
|
||||
- `--ref-audio` is **required** — IndexTTS-2 does not support text-only synthesis.
|
||||
- Stage 0 (AR Talker): GPT-2 generates mel codes from text + reference audio.
|
||||
- Stage 1 (S2Mel + BigVGAN): CFM DiT converts mel codes to waveform at 22.05 kHz.
|
||||
- Deploy config: `vllm_omni/deploy/indextts2.yaml`. Stage 1 runs with `enforce_eager: true` (DiT has dynamic shapes).
|
||||
|
||||
---
|
||||
|
||||
## Voxtral TTS
|
||||
|
||||
Voxtral-4B-TTS (Mistral). Has its own argparse surface; uses voice presets and the `mistral_common` `SpeechRequest` protocol.
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Offline inference example for IndexTTS2 via vLLM-Omni.
|
||||
|
||||
Two-stage pipeline: GPT AR (Stage 0) → S2Mel + BigVGAN (Stage 1).
|
||||
Output is 22050 Hz mono WAV.
|
||||
|
||||
Usage:
|
||||
python end2end.py \
|
||||
--model /path/to/IndexTeam/IndexTTS-2 \
|
||||
--text "你好,这是一个语音合成测试。" \
|
||||
--ref-audio /path/to/ref.wav
|
||||
|
||||
# With emotion audio:
|
||||
python end2end.py \
|
||||
--model /path/to/IndexTeam/IndexTTS-2 \
|
||||
--text "今天天气真好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-audio /path/to/happy.wav
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import soundfile as sf
|
||||
import torch
|
||||
from vllm import SamplingParams
|
||||
from vllm.utils.argparse_utils import FlexibleArgumentParser
|
||||
|
||||
os.environ.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn")
|
||||
|
||||
from vllm_omni import Omni # noqa: E402
|
||||
from vllm_omni.model_executor.models.indextts2.prompt_utils import ( # noqa: E402
|
||||
build_indextts2_prefill_prompt_ids,
|
||||
)
|
||||
|
||||
|
||||
def build_request(
|
||||
model: str,
|
||||
text: str,
|
||||
ref_audio_path: str | None = None,
|
||||
emo_audio_path: str | None = None,
|
||||
emo_text: str | None = None,
|
||||
emo_vector: list[float] | None = None,
|
||||
emo_alpha: float | None = None,
|
||||
use_emo_text: bool = False,
|
||||
use_random: bool = False,
|
||||
) -> dict:
|
||||
additional: dict = {"text": [text]}
|
||||
if ref_audio_path:
|
||||
additional["voice"] = [str(ref_audio_path)]
|
||||
if emo_audio_path:
|
||||
additional["emo_audio"] = [str(emo_audio_path)]
|
||||
if emo_text:
|
||||
additional["emo_text"] = [emo_text]
|
||||
if emo_vector is not None:
|
||||
additional["emo_vector"] = [emo_vector]
|
||||
if emo_alpha is not None:
|
||||
additional["emo_alpha"] = [emo_alpha]
|
||||
if use_emo_text:
|
||||
additional["use_emo_text"] = [True]
|
||||
if use_random:
|
||||
additional["use_random"] = [True]
|
||||
return {
|
||||
"prompt_token_ids": build_indextts2_prefill_prompt_ids(model, text),
|
||||
"additional_information": additional,
|
||||
}
|
||||
|
||||
|
||||
def save_audio(waveform: torch.Tensor, path: str, sample_rate: int = 22050) -> None:
|
||||
audio_np = waveform.float().numpy()
|
||||
sf.write(path, audio_np, sample_rate)
|
||||
print(f" Saved {path} ({audio_np.shape}, {sample_rate} Hz)")
|
||||
|
||||
|
||||
def extract_audio(mm: dict) -> tuple[torch.Tensor | None, int]:
|
||||
audio = mm.get("audio")
|
||||
if audio is None:
|
||||
audio = mm.get("model_outputs")
|
||||
if isinstance(audio, list):
|
||||
chunks = [chunk.reshape(-1) for chunk in audio if isinstance(chunk, torch.Tensor) and chunk.numel() > 0]
|
||||
audio = torch.cat(chunks, dim=0) if chunks else None
|
||||
|
||||
sr_val = mm.get("sr")
|
||||
if isinstance(sr_val, list):
|
||||
sr_val = sr_val[-1] if sr_val else None
|
||||
if hasattr(sr_val, "item"):
|
||||
sample_rate = int(sr_val.item())
|
||||
else:
|
||||
sample_rate = int(sr_val) if sr_val is not None else 22050
|
||||
|
||||
return audio if isinstance(audio, torch.Tensor) else None, sample_rate
|
||||
|
||||
|
||||
def main(args) -> None:
|
||||
omni = Omni(
|
||||
model=args.model,
|
||||
deploy_config=args.deploy_config,
|
||||
stage_init_timeout=args.stage_init_timeout,
|
||||
)
|
||||
|
||||
# Stage 0: GPT AR (text → mel codes)
|
||||
gpt_sampling = SamplingParams(
|
||||
temperature=0.8,
|
||||
top_p=0.8,
|
||||
top_k=30,
|
||||
max_tokens=1500,
|
||||
repetition_penalty=10.0,
|
||||
stop_token_ids=[8193],
|
||||
seed=args.seed if args.seed is not None else 42,
|
||||
detokenize=False,
|
||||
)
|
||||
# Stage 1: S2Mel + BigVGAN (non-AR, params mostly ignored)
|
||||
s2mel_sampling = SamplingParams(
|
||||
temperature=0.0,
|
||||
max_tokens=65536,
|
||||
detokenize=True,
|
||||
)
|
||||
sampling_params = [gpt_sampling, s2mel_sampling]
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"Synthesizing: {args.text!r}")
|
||||
if args.ref_audio:
|
||||
print(f" ref_audio: {args.ref_audio}")
|
||||
inputs = build_request(
|
||||
model=args.model,
|
||||
text=args.text,
|
||||
ref_audio_path=args.ref_audio,
|
||||
emo_audio_path=args.emo_audio,
|
||||
emo_text=args.emo_text,
|
||||
emo_vector=args.emo_vector,
|
||||
emo_alpha=args.emo_alpha,
|
||||
use_emo_text=args.use_emo_text,
|
||||
use_random=args.use_random,
|
||||
)
|
||||
|
||||
for i, omni_out in enumerate(omni.generate(inputs, sampling_params_list=sampling_params)):
|
||||
mm = omni_out.multimodal_output
|
||||
if not mm:
|
||||
print(f" [req {i}] No multimodal output.")
|
||||
continue
|
||||
audio, sr = extract_audio(mm)
|
||||
if audio is None:
|
||||
print(f" [req {i}] No waveform in multimodal_output.")
|
||||
continue
|
||||
out_path = str(output_dir / f"output_{i}.wav")
|
||||
save_audio(audio.cpu(), out_path, sr)
|
||||
|
||||
print("Done.")
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = FlexibleArgumentParser(description="IndexTTS2 offline inference")
|
||||
parser.add_argument("--model", required=True, help="HF model path for IndexTTS2.")
|
||||
parser.add_argument("--text", default="你好,这是IndexTTS2语音合成测试。")
|
||||
parser.add_argument("--ref-audio", required=True, help="Reference audio for voice cloning.")
|
||||
parser.add_argument("--emo-audio", default=None, help="Emotion reference audio.")
|
||||
parser.add_argument("--emo-text", default=None, help="Emotion description text.")
|
||||
parser.add_argument(
|
||||
"--emo-vector",
|
||||
type=float,
|
||||
nargs=8,
|
||||
default=None,
|
||||
help="8-dim emotion vector: happy angry sad afraid disgusted melancholic surprised calm.",
|
||||
)
|
||||
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1].")
|
||||
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text.")
|
||||
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes.")
|
||||
parser.add_argument("--seed", type=int, default=None)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default=os.path.join(
|
||||
os.environ.get("XDG_CACHE_HOME", os.path.join(os.path.expanduser("~"), ".cache")),
|
||||
"indextts2_output",
|
||||
),
|
||||
)
|
||||
parser.add_argument("--deploy-config", default=None)
|
||||
parser.add_argument("--stage-init-timeout", type=int, default=600)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(parse_args())
|
||||
@@ -16,6 +16,7 @@ For the full list of supported architectures across all modalities, see
|
||||
|---|---|---|---|---|---|
|
||||
| Fish Speech S2 Pro | `fishaudio/s2-pro` | ✓ (`ref_audio`+`ref_text`) | ✓ (PCM stream) | — | ✓ |
|
||||
| GLM-TTS | `zai-org/GLM-TTS` | ✓ (`ref_audio`+`ref_text`, required) | ✓ (PCM stream) | — | ✓ |
|
||||
| IndexTTS-2 | `IndexTeam/IndexTTS-2` | ✓ (`ref_audio` or uploaded `voice`) | compat only, non-chunk | uploaded audio voice only; no presets | — |
|
||||
| Ming-omni-tts | `inclusionAI/Ming-omni-tts-0.5B` | ✓ (`ref_audio` / `speaker_embedding`) | ✓ (PCM stream) | IP labels + structured `instructions` | — |
|
||||
| Ming-flash-omni-TTS | `Jonathan1909/Ming-flash-omni-2.0` | — (caption-controlled) | — | caption fields (`instructions`) | — |
|
||||
| MOSS-TTS-Nano | `OpenMOSS-Team/MOSS-TTS-Nano` | ✓ (`ref_audio` required) | ✓ (PCM stream) | — | ✓ |
|
||||
@@ -34,7 +35,7 @@ Launch the server (defaults shown — adjust `--port`, `--gpu-memory-utilization
|
||||
vllm serve <hf-repo-or-local-path> --omni --port 8091
|
||||
```
|
||||
|
||||
Send a TTS request via curl:
|
||||
Send a TTS request via curl. These generic snippets assume a model with a preset/default voice; voice-cloning-only models such as IndexTTS-2 require `ref_audio` or an uploaded audio `voice` (see model-specific sections below).
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
@@ -90,7 +91,7 @@ curl -X POST http://localhost:8091/v1/audio/speech \
|
||||
}' --no-buffer | play -t raw -r 24000 -e signed -b 16 -c 1 -
|
||||
```
|
||||
|
||||
Adjust the player's sample rate to match the model (44.1 kHz for Fish Speech, 48 kHz for VoxCPM2, 24 kHz for the others).
|
||||
Adjust the player's sample rate to match the model (44.1 kHz for Fish Speech, 48 kHz for VoxCPM2, 22.05 kHz for IndexTTS-2, and 24 kHz for many others).
|
||||
|
||||
For full request-shape documentation (all parameters, response formats, error codes), see the [Speech API reference](../../../docs/serving/speech_api.md).
|
||||
|
||||
@@ -135,6 +136,40 @@ bash examples/online_serving/text_to_speech/glm_tts/run_gradio_demo.sh
|
||||
|
||||
---
|
||||
|
||||
## IndexTTS-2
|
||||
|
||||
2-stage TTS (GPT AR + S2Mel CFM DiT + BigVGAN) at 22.05 kHz. Requests use `ref_audio` for voice cloning, or an uploaded audio `voice` from `/v1/audio/voices`. Supports emotion conditioning via `emo_audio`, `emo_text`, or `emo_vector` passed in `extra_params`.
|
||||
|
||||
### Launch
|
||||
```bash
|
||||
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092
|
||||
# or, to pass the bundled deploy config explicitly:
|
||||
bash examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||
```
|
||||
|
||||
### Sending requests
|
||||
```bash
|
||||
# Voice cloning (ref_audio required)
|
||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
||||
--text "你好,世界!" \
|
||||
--ref-audio /path/to/reference.wav
|
||||
|
||||
# With emotion audio
|
||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
||||
--text "今天心情很好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-audio /path/to/happy.wav
|
||||
```
|
||||
|
||||
### Notes
|
||||
- Output: 22.05 kHz mono WAV.
|
||||
- Provide `ref_audio` on the documented raw request path, or pass `voice` only when it names an uploaded audio voice; IndexTTS-2 does not provide a built-in text-only preset voice.
|
||||
- Emotion params (`emo_audio`, `emo_text`, `emo_vector`, `emo_alpha`, `use_emo_text`, `use_random`) are passed via the `extra_params` field. Official precedence is `use_emo_text` > `emo_vector` > `emo_audio` > same emotion as the speaker reference.
|
||||
- `stream=true` is accepted as an OpenAI-compatible response path, but IndexTTS-2 is not async-chunk streaming; audio is produced after S2Mel receives the full mel-code sequence.
|
||||
- Deploy config: `vllm_omni/deploy/indextts2.yaml` (auto-loaded).
|
||||
|
||||
---
|
||||
|
||||
## Fish Speech S2 Pro
|
||||
|
||||
4B dual-AR TTS at 44.1 kHz. Server uses the DAC codec.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
# Launch vLLM-Omni server for IndexTTS2
|
||||
#
|
||||
# Usage from repository root:
|
||||
# examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||
# CUDA_VISIBLE_DEVICES=0 PORT=8092 MODEL=/path/to/IndexTeam/IndexTTS-2 examples/online_serving/text_to_speech/indextts2/run_server.sh
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd -- "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
|
||||
MODEL="${MODEL:-IndexTeam/IndexTTS-2}"
|
||||
PORT="${PORT:-8092}"
|
||||
DEPLOY_CONFIG="${DEPLOY_CONFIG:-$ROOT_DIR/vllm_omni/deploy/indextts2.yaml}"
|
||||
|
||||
echo "Starting IndexTTS2 server with model: $MODEL"
|
||||
|
||||
FLASHINFER_DISABLE_VERSION_CHECK=1 \
|
||||
vllm serve "$MODEL" \
|
||||
--host 0.0.0.0 \
|
||||
--port "$PORT" \
|
||||
--omni \
|
||||
--trust-remote-code \
|
||||
--deploy-config "$DEPLOY_CONFIG"
|
||||
@@ -0,0 +1,126 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""OpenAI-compatible client for IndexTTS2 TTS via /v1/audio/speech endpoint.
|
||||
|
||||
Examples:
|
||||
# With reference audio for voice cloning
|
||||
python speech_client.py --text "你好,世界!" \
|
||||
--ref-audio /path/to/reference.wav
|
||||
|
||||
# With emotion audio
|
||||
python speech_client.py --text "今天心情很好!" \
|
||||
--ref-audio /path/to/ref.wav \
|
||||
--emo-audio /path/to/happy.wav
|
||||
|
||||
Server setup:
|
||||
vllm serve IndexTeam/IndexTTS-2 --omni --host 0.0.0.0 --port 8092
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import os
|
||||
|
||||
import httpx
|
||||
|
||||
DEFAULT_API_BASE = "http://localhost:8092"
|
||||
DEFAULT_API_KEY = "sk-empty"
|
||||
|
||||
|
||||
def encode_audio_to_base64(audio_path: str) -> str:
|
||||
if not os.path.exists(audio_path):
|
||||
raise FileNotFoundError(f"Audio file not found: {audio_path}")
|
||||
ext = audio_path.lower().rsplit(".", 1)[-1]
|
||||
mime = {"wav": "audio/wav", "mp3": "audio/mpeg", "flac": "audio/flac"}.get(ext, "audio/wav")
|
||||
with open(audio_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode("utf-8")
|
||||
return f"data:{mime};base64,{b64}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="IndexTTS2 OpenAI speech client")
|
||||
parser.add_argument("--text", type=str, required=True)
|
||||
parser.add_argument("--ref-audio", type=str, default=None, help="Reference audio for voice cloning")
|
||||
parser.add_argument("--emo-audio", type=str, default=None, help="Emotion reference audio")
|
||||
parser.add_argument("--emo-text", type=str, default=None, help="Emotion description text")
|
||||
parser.add_argument(
|
||||
"--emo-vector",
|
||||
type=float,
|
||||
nargs=8,
|
||||
default=None,
|
||||
help="8-dim emotion vector: happy angry sad afraid disgusted melancholic surprised calm",
|
||||
)
|
||||
parser.add_argument("--emo-alpha", type=float, default=None, help="Emotion weight in [0, 1]")
|
||||
parser.add_argument("--use-emo-text", action="store_true", help="Infer emotion vector from emo-text or text")
|
||||
parser.add_argument("--use-random", action="store_true", help="Use random emotion prototypes")
|
||||
parser.add_argument("--model", type=str, default="IndexTeam/IndexTTS-2")
|
||||
parser.add_argument("--voice", type=str, default=None, help="Uploaded voice name to use instead of --ref-audio")
|
||||
parser.add_argument("--output", type=str, default="output.wav")
|
||||
parser.add_argument("--api-base", type=str, default=DEFAULT_API_BASE)
|
||||
parser.add_argument("--api-key", type=str, default=DEFAULT_API_KEY)
|
||||
parser.add_argument("--response-format", type=str, default="wav")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.ref_audio and not args.voice:
|
||||
parser.error("IndexTTS2 requires --ref-audio or --voice for voice cloning")
|
||||
|
||||
payload: dict = {
|
||||
"model": args.model,
|
||||
"input": args.text,
|
||||
"response_format": args.response_format,
|
||||
}
|
||||
if args.voice:
|
||||
payload["voice"] = args.voice
|
||||
|
||||
if args.ref_audio:
|
||||
ref = args.ref_audio
|
||||
if ref.startswith(("http://", "https://", "data:")):
|
||||
payload["ref_audio"] = ref
|
||||
else:
|
||||
payload["ref_audio"] = encode_audio_to_base64(ref)
|
||||
|
||||
extra_params = {}
|
||||
if args.emo_audio:
|
||||
emo = args.emo_audio
|
||||
if emo.startswith(("http://", "https://", "data:")):
|
||||
extra_params["emo_audio"] = emo
|
||||
else:
|
||||
extra_params["emo_audio"] = encode_audio_to_base64(emo)
|
||||
if args.emo_text:
|
||||
extra_params["emo_text"] = args.emo_text
|
||||
if args.emo_vector is not None:
|
||||
extra_params["emo_vector"] = args.emo_vector
|
||||
if args.emo_alpha is not None:
|
||||
extra_params["emo_alpha"] = args.emo_alpha
|
||||
if args.use_emo_text:
|
||||
extra_params["use_emo_text"] = True
|
||||
if args.use_random:
|
||||
extra_params["use_random"] = True
|
||||
if extra_params:
|
||||
payload["extra_params"] = extra_params
|
||||
|
||||
url = f"{args.api_base}/v1/audio/speech"
|
||||
print(f"POST {url}")
|
||||
print(f" text: {args.text}")
|
||||
if args.ref_audio:
|
||||
print(f" ref_audio: {args.ref_audio[:80]}...")
|
||||
|
||||
with httpx.Client(timeout=300) as client:
|
||||
resp = client.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers={"Authorization": f"Bearer {args.api_key}"},
|
||||
)
|
||||
|
||||
if resp.status_code != 200:
|
||||
print(f"Error {resp.status_code}: {resp.text[:500]}")
|
||||
return
|
||||
|
||||
with open(args.output, "wb") as f:
|
||||
f.write(resp.content)
|
||||
print(f"Saved: {args.output} ({len(resp.content):,} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,139 @@
|
||||
# IndexTTS-2 for voice-cloned TTS on 1x GPU
|
||||
|
||||
## Summary
|
||||
|
||||
- Vendor: IndexTeam
|
||||
- Model: `IndexTeam/IndexTTS-2`
|
||||
- Task: Text-to-speech with voice cloning and optional emotion control
|
||||
- Mode: Online serving with the OpenAI-compatible `/v1/audio/speech` API
|
||||
- Maintainer: Community
|
||||
|
||||
## When to use this recipe
|
||||
|
||||
Use this recipe to serve IndexTTS-2 as a two-stage TTS system for
|
||||
voice-cloned speech synthesis. Each request provides synthesis text plus either
|
||||
a reference audio clip or an uploaded audio voice; the model returns 22.05 kHz
|
||||
mono speech. IndexTTS-2 also supports optional emotion conditioning through
|
||||
model-specific `extra_params`.
|
||||
|
||||
IndexTTS-2 is **not an async-chunk streaming model** in the bundled deployment
|
||||
configuration: the S2Mel flow-matching stage consumes the full mel-code sequence
|
||||
from the AR talker before it produces audio. The OpenAI-compatible endpoint can
|
||||
still be called with `stream=true` as an HTTP compatibility path, but that should
|
||||
not be interpreted as incremental model streaming.
|
||||
|
||||
## References
|
||||
|
||||
- Upstream model:
|
||||
[IndexTeam/IndexTTS-2 on Hugging Face](https://huggingface.co/IndexTeam/IndexTTS-2)
|
||||
- Online serving example:
|
||||
[`examples/online_serving/text_to_speech/README.md#indextts-2`](../../examples/online_serving/text_to_speech/README.md#indextts-2)
|
||||
- Offline inference example:
|
||||
[`examples/offline_inference/text_to_speech/README.md#indextts-2`](../../examples/offline_inference/text_to_speech/README.md#indextts-2)
|
||||
- OpenAI-compatible client:
|
||||
[`examples/online_serving/text_to_speech/indextts2/speech_client.py`](../../examples/online_serving/text_to_speech/indextts2/speech_client.py)
|
||||
- Deploy config:
|
||||
[`vllm_omni/deploy/indextts2.yaml`](../../vllm_omni/deploy/indextts2.yaml)
|
||||
|
||||
## Hardware Support
|
||||
|
||||
### GPU
|
||||
|
||||
### 1x L4 24GB or larger CUDA GPU
|
||||
|
||||
#### Environment
|
||||
|
||||
- OS: Linux
|
||||
- Python: 3.10+
|
||||
- Driver / runtime: NVIDIA CUDA environment
|
||||
- vLLM version: Match the repository requirements for your checkout
|
||||
- vLLM-Omni version or commit: Use the commit you are deploying from
|
||||
|
||||
#### Command
|
||||
|
||||
Start the server from the repository root:
|
||||
|
||||
```bash
|
||||
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092
|
||||
```
|
||||
|
||||
The bundled deployment config is selected automatically for the `indextts2`
|
||||
model type. To make the config choice explicit, pass it directly:
|
||||
|
||||
```bash
|
||||
vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code --port 8092 \
|
||||
--deploy-config vllm_omni/deploy/indextts2.yaml
|
||||
```
|
||||
|
||||
#### Verification
|
||||
|
||||
Run the bundled client. Local audio paths passed to this client are converted to
|
||||
base64 data URLs before they are sent to the HTTP API:
|
||||
|
||||
```bash
|
||||
python examples/online_serving/text_to_speech/indextts2/speech_client.py \
|
||||
--api-base http://localhost:8092 \
|
||||
--text "你好,这是 IndexTTS-2 语音合成测试。" \
|
||||
--ref-audio /path/to/reference.wav \
|
||||
--output indextts2.wav
|
||||
```
|
||||
|
||||
For a raw HTTP request, pass `ref_audio` as an HTTP(S) URL, a base64 data URL,
|
||||
or a `file://` URI allowed by the server's `--allowed-local-media-path`. Do not
|
||||
pass an arbitrary local filesystem path directly in JSON unless your client
|
||||
converts it first:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8092/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "IndexTeam/IndexTTS-2",
|
||||
"input": "你好,这是 IndexTTS-2 语音合成测试。",
|
||||
"response_format": "wav",
|
||||
"ref_audio": "data:audio/wav;base64,<BASE64_ENCODED_AUDIO>"
|
||||
}' \
|
||||
--output indextts2.wav
|
||||
```
|
||||
|
||||
Optional emotion conditioning is passed through `extra_params`. Use one emotion
|
||||
source mode at a time:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8092/v1/audio/speech \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "IndexTeam/IndexTTS-2",
|
||||
"input": "今天心情很好!",
|
||||
"response_format": "wav",
|
||||
"ref_audio": "data:audio/wav;base64,<BASE64_ENCODED_REFERENCE_AUDIO>",
|
||||
"extra_params": {
|
||||
"emo_vector": [1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||
"emo_alpha": 0.8
|
||||
}
|
||||
}' \
|
||||
--output indextts2_happy.wav
|
||||
```
|
||||
|
||||
#### Notes
|
||||
|
||||
- Hardware scope: the default config is a single-GPU CUDA deployment. Stage 0
|
||||
and Stage 1 share GPU 0 by default.
|
||||
- Audio output: 22.05 kHz mono WAV.
|
||||
- Voice cloning: `ref_audio` is required for the documented raw request path.
|
||||
Alternatively, pass `voice` only when it names an uploaded audio voice from
|
||||
`/v1/audio/voices`; IndexTTS-2 does not provide a built-in text-only preset
|
||||
voice in this serving recipe.
|
||||
- Reference audio format: use an HTTP(S) URL, a base64 data URL, or a `file://`
|
||||
URI allowed by the server's `--allowed-local-media-path` for raw API calls.
|
||||
The bundled Python client accepts local paths and converts them to base64 data
|
||||
URLs.
|
||||
- Emotion control: `extra_params.emo_audio`, `extra_params.emo_vector`, and
|
||||
`extra_params.use_emo_text` are alternative conditioning modes. Official
|
||||
precedence is `use_emo_text` > `emo_vector` > `emo_audio` > same emotion as
|
||||
the speaker reference. The `emo_vector` order is `[happy, angry, sad, afraid,
|
||||
disgusted, melancholic, surprised, calm]`.
|
||||
- Streaming limitation: `async_chunk` is disabled in
|
||||
`vllm_omni/deploy/indextts2.yaml`; S2Mel requires the full mel-code sequence
|
||||
before audio generation.
|
||||
- First request can be slower because reference encoders and auxiliary models
|
||||
are lazily loaded and cached.
|
||||
@@ -43,6 +43,7 @@ recipes/
|
||||
| [`Helios/Helios.md`](./Helios/Helios.md) | Text-to-video, image-to-video, and video-to-video generation | 1x NVIDIA H20 |
|
||||
| [`inclusionAI/Ming-flash-omni-2.0.md`](./inclusionAI/Ming-flash-omni-2.0.md) | Online serving for multimodal chat + standalone TTS | 4x H100 / 1x H100 80GB |
|
||||
| [`inclusionAI/Ming-omni-tts.md`](./inclusionAI/Ming-omni-tts.md) | Offline + online dense Ming TTS/audio generation | 1x H100 80GB / 1x AMD MI300X (ROCm 7.2) |
|
||||
| [`IndexTeam/IndexTTS-2.md`](./IndexTeam/IndexTTS-2.md) | Online serving for voice-cloned TTS with optional emotion control | 1x L4 24GB or larger CUDA GPU |
|
||||
| [`LTX/LTX-2.md`](./LTX/LTX-2.md) | Text-to-video and image-to-video serving | 1x H200 141GB |
|
||||
| [`LTX/LTX-2.3.md`](./LTX/LTX-2.3.md) | Text-to-video with audio generation (22B) | 1x GPU (96GB VRAM) |
|
||||
| [`mistralai/Voxtral-TTS.md`](./mistralai/Voxtral-TTS.md) | Online serving for TTS | 1x RTX 4090 24GB |
|
||||
|
||||
Binary file not shown.
@@ -115,6 +115,7 @@ class TestFullPayloadCoordinatorSelection(unittest.TestCase):
|
||||
("MiMoAudioModel", "code2wav"),
|
||||
("Qwen3TTSCode2Wav", "code2wav"),
|
||||
("CosyVoice3Model", "cosyvoice3_code2wav"),
|
||||
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
||||
("DyninOmniForConditionalGeneration", "token2image"),
|
||||
("DyninOmniForConditionalGeneration", "token2audio"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""E2E offline inference tests for IndexTTS2 two-stage pipeline.
|
||||
|
||||
Stage 0 (GPT AR) → Stage 1 (S2Mel + BigVGAN). Output is 22050 Hz mono WAV.
|
||||
Covers: basic TTS.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.runtime import OmniRunner
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
from vllm_omni import Omni
|
||||
from vllm_omni.model_executor.models.indextts2.prompt_utils import (
|
||||
build_indextts2_prefill_prompt_ids,
|
||||
)
|
||||
|
||||
MODEL_NAME = "IndexTeam/IndexTTS-2"
|
||||
STAGE_CONFIG = get_deploy_config_path("indextts2.yaml")
|
||||
|
||||
_OMNI_RUNNER_PARAM = (
|
||||
MODEL_NAME,
|
||||
STAGE_CONFIG,
|
||||
)
|
||||
|
||||
ASSETS_DIR = Path(__file__).resolve().parents[2] / "assets" / "indextts2"
|
||||
REF_AUDIO = str(ASSETS_DIR / "ref_audio.wav")
|
||||
|
||||
pytestmark = [
|
||||
pytest.mark.advanced_model,
|
||||
pytest.mark.tts,
|
||||
pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True),
|
||||
]
|
||||
|
||||
SAMPLE_RATE = 22050
|
||||
|
||||
|
||||
def _audio_from_mm(mm: dict) -> torch.Tensor | None:
|
||||
audio = mm.get("audio")
|
||||
if audio is None:
|
||||
audio = mm.get("model_outputs")
|
||||
if isinstance(audio, list):
|
||||
chunks = [chunk.reshape(-1) for chunk in audio if isinstance(chunk, torch.Tensor) and chunk.numel() > 0]
|
||||
return torch.cat(chunks, dim=0) if chunks else None
|
||||
return audio if isinstance(audio, torch.Tensor) else None
|
||||
|
||||
|
||||
def _sample_rate_from_mm(mm: dict) -> int:
|
||||
sr = mm.get("sr")
|
||||
if isinstance(sr, list):
|
||||
sr = sr[-1] if sr else None
|
||||
if hasattr(sr, "item"):
|
||||
return int(sr.item())
|
||||
return int(sr) if sr is not None else SAMPLE_RATE
|
||||
|
||||
|
||||
def _build_request(text: str, ref_audio: str, **extra) -> dict:
|
||||
info = {
|
||||
"text": [text],
|
||||
"voice": [ref_audio],
|
||||
}
|
||||
info.update(extra)
|
||||
return {
|
||||
"prompt_token_ids": build_indextts2_prefill_prompt_ids(MODEL_NAME, text),
|
||||
"additional_information": info,
|
||||
}
|
||||
|
||||
|
||||
def _collect_audio(omni: Omni, request: dict) -> tuple[torch.Tensor, int]:
|
||||
for omni_out in omni.generate(request):
|
||||
mm = omni_out.multimodal_output
|
||||
assert mm is not None, "Expected multimodal_output"
|
||||
audio = _audio_from_mm(mm)
|
||||
assert audio is not None, "Expected audio output"
|
||||
return audio.cpu(), _sample_rate_from_mm(mm)
|
||||
raise AssertionError("No outputs received")
|
||||
|
||||
|
||||
def _assert_valid_audio(audio: torch.Tensor, sr: int) -> None:
|
||||
assert sr == SAMPLE_RATE
|
||||
assert audio.numel() > 0
|
||||
assert torch.any(audio != 0).item()
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"})
|
||||
def test_basic_english(omni_runner: OmniRunner) -> None:
|
||||
"""English TTS: text + ref_audio, no emotion conditioning."""
|
||||
req = _build_request("Hello, this is a voice synthesis test.", REF_AUDIO)
|
||||
audio, sr = _collect_audio(omni_runner.omni, req)
|
||||
_assert_valid_audio(audio, sr)
|
||||
@@ -0,0 +1,82 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""E2E online serving tests for IndexTTS2 via /v1/audio/speech endpoint.
|
||||
|
||||
Two-stage pipeline: GPT AR → S2Mel + BigVGAN. Output is 22050 Hz mono WAV.
|
||||
Covers: basic TTS and the speech endpoint's stream=True compatibility path.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.helpers.mark import hardware_test
|
||||
from tests.helpers.media import load_test_audio_data_url
|
||||
from tests.helpers.runtime import OmniServerParams
|
||||
from tests.helpers.stage_config import get_deploy_config_path
|
||||
|
||||
pytestmark = [pytest.mark.advanced_model, pytest.mark.tts]
|
||||
|
||||
MODEL = "IndexTeam/IndexTTS-2"
|
||||
|
||||
REF_AUDIO_URL = load_test_audio_data_url("indextts2/ref_audio.wav")
|
||||
|
||||
tts_server_params = [
|
||||
pytest.param(
|
||||
OmniServerParams(
|
||||
model=MODEL,
|
||||
stage_config_path=get_deploy_config_path("indextts2.yaml"),
|
||||
server_args=["--disable-log-stats"],
|
||||
),
|
||||
id="indextts2",
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_basic_english(omni_server, openai_client) -> None:
|
||||
"""
|
||||
Basic English TTS: text + ref_audio, no emotion.
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text + reference audio
|
||||
Output Modal: audio (22050 Hz, WAV)
|
||||
Input Setting: stream=False
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": "Hello, this is a voice cloning demo.",
|
||||
"stream": False,
|
||||
"response_format": "wav",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"min_audio_bytes": 1024,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": "L4"}, num_cards=1)
|
||||
@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True)
|
||||
def test_streaming(omni_server, openai_client) -> None:
|
||||
"""
|
||||
stream=True compatibility: IndexTTS2 itself is non-async-chunking, but the
|
||||
OpenAI speech endpoint should still accept the streaming request path and
|
||||
return a valid PCM response.
|
||||
|
||||
Deploy Setting: default yaml
|
||||
Input Modal: text + reference audio
|
||||
Output Modal: audio (22050 Hz, PCM)
|
||||
Input Setting: stream=True
|
||||
"""
|
||||
request_config = {
|
||||
"model": omni_server.model,
|
||||
"input": "Hello, this is a voice cloning demo.",
|
||||
"stream": True,
|
||||
"response_format": "pcm",
|
||||
"ref_audio": REF_AUDIO_URL,
|
||||
"min_audio_bytes": 1024,
|
||||
}
|
||||
openai_client.send_audio_speech_request(request_config)
|
||||
@@ -0,0 +1,336 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.stage_input_processors.indextts2 import (
|
||||
_strip_stop_token,
|
||||
talker2s2mel,
|
||||
talker2s2mel_full_payload,
|
||||
talker2s2mel_token_only,
|
||||
)
|
||||
|
||||
STOP_MEL_TOKEN = 8193
|
||||
LATENT_DIM = 16
|
||||
|
||||
|
||||
def _make_talker_output(
|
||||
*,
|
||||
mel_codes: torch.Tensor,
|
||||
latent: torch.Tensor,
|
||||
s_ref: torch.Tensor | None = None,
|
||||
ref_mel: torch.Tensor | None = None,
|
||||
style: torch.Tensor | None = None,
|
||||
finished: bool = True,
|
||||
):
|
||||
meta = {}
|
||||
if s_ref is not None:
|
||||
meta["S_ref"] = s_ref
|
||||
if ref_mel is not None:
|
||||
meta["ref_mel"] = ref_mel
|
||||
if style is not None:
|
||||
meta["style"] = style
|
||||
|
||||
mm = {
|
||||
"codes": {"mel": mel_codes},
|
||||
"hidden_states": {"latent": latent},
|
||||
"meta": meta,
|
||||
}
|
||||
return SimpleNamespace(
|
||||
finished=finished,
|
||||
outputs=[SimpleNamespace(multimodal_output=mm)],
|
||||
)
|
||||
|
||||
|
||||
# ── _strip_stop_token ──
|
||||
|
||||
|
||||
def test_strip_stop_token_basic():
|
||||
codes = torch.tensor([10, 20, 30, STOP_MEL_TOKEN, 99])
|
||||
latent = torch.randn(5, LATENT_DIM)
|
||||
|
||||
c, lat, lens = _strip_stop_token(codes, latent)
|
||||
|
||||
assert c.shape == (1, 3)
|
||||
assert lat.shape == (1, 3, LATENT_DIM)
|
||||
assert lens.tolist() == [3]
|
||||
assert (c[0] == torch.tensor([10, 20, 30])).all()
|
||||
|
||||
|
||||
def test_strip_stop_token_no_stop():
|
||||
codes = torch.tensor([10, 20, 30])
|
||||
latent = torch.randn(3, LATENT_DIM)
|
||||
|
||||
c, lat, lens = _strip_stop_token(codes, latent)
|
||||
|
||||
assert c.shape == (1, 3)
|
||||
assert lens.tolist() == [3]
|
||||
|
||||
|
||||
def test_strip_stop_token_at_start():
|
||||
codes = torch.tensor([STOP_MEL_TOKEN, 10, 20])
|
||||
latent = torch.randn(3, LATENT_DIM)
|
||||
|
||||
c, lat, lens = _strip_stop_token(codes, latent)
|
||||
|
||||
assert c.shape == (1, 0)
|
||||
assert lat.shape == (1, 0, LATENT_DIM)
|
||||
assert lens.tolist() == [0]
|
||||
|
||||
|
||||
def test_strip_stop_token_batch_with_padding():
|
||||
codes = torch.tensor(
|
||||
[
|
||||
[10, 20, STOP_MEL_TOKEN, 0],
|
||||
[30, 40, 50, STOP_MEL_TOKEN],
|
||||
]
|
||||
)
|
||||
latent = torch.randn(2, 4, LATENT_DIM)
|
||||
|
||||
c, lat, lens = _strip_stop_token(codes, latent)
|
||||
|
||||
assert c.shape == (2, 3)
|
||||
assert lat.shape == (2, 3, LATENT_DIM)
|
||||
assert lens.tolist() == [2, 3]
|
||||
assert c[0, 2].item() == 0 # padded with a valid semantic code
|
||||
|
||||
|
||||
def test_strip_stop_token_all_stop():
|
||||
codes = torch.tensor([STOP_MEL_TOKEN])
|
||||
latent = torch.randn(1, LATENT_DIM)
|
||||
|
||||
c, lat, lens = _strip_stop_token(codes, latent)
|
||||
|
||||
assert lens.tolist() == [0]
|
||||
assert c.shape[1] == 0
|
||||
assert lat.shape[1] == 0
|
||||
|
||||
|
||||
# ── talker2s2mel ──
|
||||
|
||||
|
||||
def test_talker2s2mel_basic():
|
||||
mel_codes = torch.tensor([10, 20, 30, STOP_MEL_TOKEN])
|
||||
latent = torch.randn(4, LATENT_DIM)
|
||||
s_ref = torch.randn(1, 128)
|
||||
ref_mel = torch.randn(1, 80, 200)
|
||||
style = torch.randn(1, 256)
|
||||
|
||||
outputs = talker2s2mel(
|
||||
source_outputs=[
|
||||
_make_talker_output(
|
||||
mel_codes=mel_codes,
|
||||
latent=latent,
|
||||
s_ref=s_ref,
|
||||
ref_mel=ref_mel,
|
||||
style=style,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert len(outputs) == 1
|
||||
info = outputs[0]["additional_information"]
|
||||
assert info["mel_codes"].shape == (1, 3)
|
||||
assert info["latent"].shape == (1, 3, LATENT_DIM)
|
||||
assert info["code_lens"].tolist() == [3]
|
||||
assert "S_ref" in info
|
||||
assert "ref_mel" in info
|
||||
assert "style" in info
|
||||
assert outputs[0]["prompt_token_ids"] == [0]
|
||||
|
||||
|
||||
def test_talker2s2mel_skips_unfinished():
|
||||
mel_codes = torch.tensor([10, 20])
|
||||
latent = torch.randn(2, LATENT_DIM)
|
||||
|
||||
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent, finished=False)])
|
||||
|
||||
assert len(outputs) == 0
|
||||
|
||||
|
||||
def test_talker2s2mel_skips_empty_mel():
|
||||
outputs = talker2s2mel(
|
||||
source_outputs=[
|
||||
_make_talker_output(
|
||||
mel_codes=torch.tensor([]),
|
||||
latent=torch.randn(0, LATENT_DIM),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
assert len(outputs) == 0
|
||||
|
||||
|
||||
def test_talker2s2mel_missing_optional_meta():
|
||||
mel_codes = torch.tensor([10, 20])
|
||||
latent = torch.randn(2, LATENT_DIM)
|
||||
|
||||
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent)])
|
||||
|
||||
assert len(outputs) == 1
|
||||
info = outputs[0]["additional_information"]
|
||||
assert "S_ref" not in info
|
||||
assert "ref_mel" not in info
|
||||
assert "style" not in info
|
||||
|
||||
|
||||
def test_talker2s2mel_batch():
|
||||
out0 = _make_talker_output(
|
||||
mel_codes=torch.tensor([1, 2, STOP_MEL_TOKEN]),
|
||||
latent=torch.randn(3, LATENT_DIM),
|
||||
s_ref=torch.randn(1, 64),
|
||||
)
|
||||
out1 = _make_talker_output(
|
||||
mel_codes=torch.tensor([5, 6, 7, 8]),
|
||||
latent=torch.randn(4, LATENT_DIM),
|
||||
ref_mel=torch.randn(1, 80, 100),
|
||||
)
|
||||
|
||||
outputs = talker2s2mel(source_outputs=[out0, out1])
|
||||
|
||||
assert len(outputs) == 2
|
||||
assert outputs[0]["additional_information"]["code_lens"].tolist() == [2]
|
||||
assert outputs[1]["additional_information"]["code_lens"].tolist() == [4]
|
||||
|
||||
|
||||
def test_talker2s2mel_tensors_on_cpu():
|
||||
device = torch.device("cpu")
|
||||
mel_codes = torch.tensor([10, 20], device=device)
|
||||
latent = torch.randn(2, LATENT_DIM, device=device)
|
||||
s_ref = torch.randn(1, 64, device=device)
|
||||
|
||||
outputs = talker2s2mel(source_outputs=[_make_talker_output(mel_codes=mel_codes, latent=latent, s_ref=s_ref)])
|
||||
|
||||
info = outputs[0]["additional_information"]
|
||||
assert info["latent"].device.type == "cpu"
|
||||
assert info["mel_codes"].device.type == "cpu"
|
||||
assert info["S_ref"].device.type == "cpu"
|
||||
|
||||
|
||||
def test_talker2s2mel_no_multimodal_dict():
|
||||
output = SimpleNamespace(
|
||||
finished=True,
|
||||
outputs=[SimpleNamespace(multimodal_output="not_a_dict")],
|
||||
)
|
||||
|
||||
results = talker2s2mel(source_outputs=[output])
|
||||
|
||||
assert len(results) == 0
|
||||
|
||||
|
||||
# ── full-payload connector path ──
|
||||
|
||||
|
||||
def _conditioning_tensors():
|
||||
return {
|
||||
"S_ref": torch.randn(1, 3, LATENT_DIM),
|
||||
"ref_mel": torch.randn(1, 80, 5),
|
||||
"style": torch.randn(1, 4),
|
||||
}
|
||||
|
||||
|
||||
def test_talker2s2mel_token_only_placeholder_finished_only():
|
||||
finished = _make_talker_output(mel_codes=torch.tensor([1]), latent=torch.randn(1, LATENT_DIM), finished=True)
|
||||
unfinished = _make_talker_output(mel_codes=torch.tensor([2]), latent=torch.randn(1, LATENT_DIM), finished=False)
|
||||
|
||||
outputs = talker2s2mel_token_only([unfinished, finished])
|
||||
|
||||
assert len(outputs) == 1
|
||||
assert outputs[0]["prompt_token_ids"] == [0]
|
||||
assert outputs[0]["additional_information"] is None
|
||||
assert outputs[0]["multi_modal_data"] is None
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_flat_keys_builds_s2mel_contract():
|
||||
cond = _conditioning_tensors()
|
||||
latent = torch.randn(3, LATENT_DIM)
|
||||
payload = {
|
||||
"codes.mel": torch.tensor([[10], [20], [30]]),
|
||||
"hidden_states.latent": latent,
|
||||
"meta.S_ref": cond["S_ref"],
|
||||
"meta.ref_mel": cond["ref_mel"],
|
||||
"meta.style": cond["style"],
|
||||
}
|
||||
|
||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-flat"))
|
||||
|
||||
assert result is not None
|
||||
assert result["mel_codes"].shape == (1, 3)
|
||||
assert result["mel_codes"].tolist() == [[10, 20, 30]]
|
||||
assert result["latent"].shape == (1, 3, LATENT_DIM)
|
||||
assert result["code_lens"].tolist() == [3]
|
||||
assert result["S_ref"].device.type == "cpu"
|
||||
assert result["ref_mel"].device.type == "cpu"
|
||||
assert result["style"].device.type == "cpu"
|
||||
assert result["S_ref"].data_ptr() != cond["S_ref"].data_ptr()
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_nested_fallback_input():
|
||||
cond = _conditioning_tensors()
|
||||
payload = {
|
||||
"codes": {"mel": torch.tensor([[4], [5]])},
|
||||
"hidden_states": {"latent": torch.randn(2, LATENT_DIM)},
|
||||
"meta": cond,
|
||||
}
|
||||
|
||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-nested"))
|
||||
|
||||
assert result is not None
|
||||
assert result["mel_codes"].tolist() == [[4, 5]]
|
||||
assert result["latent"].shape == (1, 2, LATENT_DIM)
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_normalizes_one_by_t_mel_row():
|
||||
cond = _conditioning_tensors()
|
||||
payload = {
|
||||
"codes.mel": torch.tensor([[7, 8, 9]]),
|
||||
"hidden_states.latent": torch.randn(3, LATENT_DIM),
|
||||
"meta": cond,
|
||||
}
|
||||
|
||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-row"))
|
||||
|
||||
assert result is not None
|
||||
assert result["mel_codes"].tolist() == [[7, 8, 9]]
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_trims_stop_token_and_matching_latent():
|
||||
cond = _conditioning_tensors()
|
||||
latent = torch.arange(4 * LATENT_DIM, dtype=torch.float32).reshape(4, LATENT_DIM)
|
||||
payload = {
|
||||
"codes.mel": torch.tensor([[1], [2], [STOP_MEL_TOKEN], [99]]),
|
||||
"hidden_states.latent": latent,
|
||||
"meta": cond,
|
||||
}
|
||||
|
||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-stop"))
|
||||
|
||||
assert result is not None
|
||||
assert result["mel_codes"].tolist() == [[1, 2]]
|
||||
assert result["latent"].shape == (1, 2, LATENT_DIM)
|
||||
assert result["latent"][0, 0].tolist() == latent[0].tolist()
|
||||
assert result["latent"][0, 1].tolist() == latent[1].tolist()
|
||||
assert result["code_lens"].tolist() == [2]
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_crops_mel_latent_to_common_length():
|
||||
cond = _conditioning_tensors()
|
||||
payload = {
|
||||
"codes.mel": torch.tensor([[1], [2], [3], [4]]),
|
||||
"hidden_states.latent": torch.randn(2, LATENT_DIM),
|
||||
"meta": cond,
|
||||
}
|
||||
|
||||
result = talker2s2mel_full_payload(None, payload, SimpleNamespace(request_id="r-crop"))
|
||||
|
||||
assert result is not None
|
||||
assert result["mel_codes"].tolist() == [[1, 2]]
|
||||
assert result["latent"].shape == (1, 2, LATENT_DIM)
|
||||
assert result["code_lens"].tolist() == [2]
|
||||
|
||||
|
||||
def test_talker2s2mel_full_payload_missing_required_fields_returns_none():
|
||||
assert talker2s2mel_full_payload(None, {"hidden_states.latent": torch.randn(1, LATENT_DIM)}, None) is None
|
||||
assert talker2s2mel_full_payload(None, {"codes.mel": torch.tensor([[1]])}, None) is None
|
||||
@@ -169,4 +169,8 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = {
|
||||
"vllm_omni.model_executor.models.dynin_omni.pipeline",
|
||||
"DYNIN_OMNI_PIPELINE",
|
||||
),
|
||||
"indextts2": (
|
||||
"vllm_omni.model_executor.models.indextts2.pipeline",
|
||||
"INDEXTTS2_PIPELINE",
|
||||
),
|
||||
}
|
||||
@@ -51,6 +51,9 @@ _FULL_PAYLOAD_INPUT_STAGES: frozenset[tuple[str, str]] = frozenset(
|
||||
("Qwen3TTSCode2Wav", "code2wav"),
|
||||
# cosyvoice3: cosyvoice3_talker (Stage 0) -> cosyvoice3_code2wav (Stage 1).
|
||||
("CosyVoice3Model", "cosyvoice3_code2wav"),
|
||||
# indextts2: indextts2_talker (Stage 0) -> indextts2_s2mel_decoder
|
||||
# (Stage 1). Stage 1 consumes the complete mel/latent payload.
|
||||
("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"),
|
||||
# dynin: token2text (Stage 0) -> token2image (Stage 1) ->
|
||||
# token2audio (Stage 2). Producer wires via
|
||||
# custom_process_next_stage_input_func: *_full_payload in deploy yaml.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
async_chunk: false
|
||||
trust_remote_code: true
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: false
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
connector_get_max_wait: 500
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 4
|
||||
gpu_memory_utilization: 0.4
|
||||
enable_prefix_caching: false
|
||||
skip_tokenizer_init: true
|
||||
tokenizer: gpt2
|
||||
async_scheduling: true
|
||||
max_num_batched_tokens: 512
|
||||
max_model_len: 2560
|
||||
devices: "0"
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.8
|
||||
top_p: 0.8
|
||||
top_k: 30
|
||||
max_tokens: 1500
|
||||
repetition_penalty: 10.0
|
||||
stop_token_ids: [8193]
|
||||
detokenize: false
|
||||
- stage_id: 1
|
||||
max_num_seqs: 4
|
||||
gpu_memory_utilization: 0.4
|
||||
enable_prefix_caching: false
|
||||
skip_tokenizer_init: true
|
||||
tokenizer: gpt2
|
||||
enforce_eager: true
|
||||
async_scheduling: true
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 32768
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
hf_overrides:
|
||||
diffusion_steps: 12
|
||||
s2mel_dit_bf16: true
|
||||
s2mel_vocoder_bf16: true
|
||||
s2mel_vocoder_cuda_graph: true
|
||||
s2mel_dit_cuda_graph: true
|
||||
s2mel_dit_cuda_graph_max_graphs: 4
|
||||
s2mel_vocoder_capture_sizes: [128, 256, 384, 512]
|
||||
s2mel_vocoder_compile_shapes: [256, 384, 512]
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
max_tokens: 32767
|
||||
@@ -0,0 +1,76 @@
|
||||
# IndexTTS2 low-latency deploy profile.
|
||||
#
|
||||
# Stage 0 (GPT2 AR talker) uses vLLM FULL_DECODE_ONLY CUDA graph + FlashInfer
|
||||
# attention backend + async_scheduling=false, mirroring the Higgs low-latency
|
||||
# profile. This reduces decode launch overhead and per-step scheduling
|
||||
# fragmentation for c1-c4 workloads.
|
||||
#
|
||||
# Usage:
|
||||
# vllm serve IndexTeam/IndexTTS-2 --omni --trust-remote-code \
|
||||
# --deploy-config vllm_omni/deploy/indextts2_low_latency.yaml
|
||||
#
|
||||
# For 2-GPU testing, copy this file and set stage 1 devices: "1".
|
||||
|
||||
async_chunk: false
|
||||
trust_remote_code: true
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 65536
|
||||
codec_streaming: false
|
||||
connector_get_sleep_s: 0.01
|
||||
connector_get_max_wait_first_chunk: 5000
|
||||
connector_get_max_wait: 500
|
||||
stages:
|
||||
- stage_id: 0
|
||||
max_num_seqs: 16
|
||||
gpu_memory_utilization: 0.6
|
||||
enable_prefix_caching: false
|
||||
skip_tokenizer_init: true
|
||||
tokenizer: gpt2
|
||||
enforce_eager: false
|
||||
attention_backend: FLASHINFER
|
||||
async_scheduling: false
|
||||
max_num_batched_tokens: 4096
|
||||
max_model_len: 2560
|
||||
devices: "0"
|
||||
compilation_config:
|
||||
cudagraph_mode: FULL_DECODE_ONLY
|
||||
cudagraph_capture_sizes: [1, 2, 4, 8, 16]
|
||||
cudagraph_num_of_warmups: 1
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
default_sampling_params:
|
||||
temperature: 0.8
|
||||
top_p: 0.8
|
||||
top_k: 30
|
||||
max_tokens: 1500
|
||||
repetition_penalty: 10.0
|
||||
stop_token_ids: [8193]
|
||||
detokenize: false
|
||||
- stage_id: 1
|
||||
max_num_seqs: 4
|
||||
gpu_memory_utilization: 0.4
|
||||
enable_prefix_caching: false
|
||||
skip_tokenizer_init: true
|
||||
tokenizer: gpt2
|
||||
enforce_eager: true
|
||||
async_scheduling: true
|
||||
max_num_batched_tokens: 8192
|
||||
max_model_len: 32768
|
||||
devices: "0"
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
hf_overrides:
|
||||
diffusion_steps: 12
|
||||
s2mel_dit_bf16: true
|
||||
s2mel_vocoder_bf16: true
|
||||
s2mel_vocoder_cuda_graph: true
|
||||
s2mel_dit_cuda_graph: true
|
||||
s2mel_dit_cuda_graph_max_graphs: 4
|
||||
s2mel_vocoder_capture_sizes: [128, 256, 384, 512]
|
||||
s2mel_vocoder_compile_shapes: [256, 384, 512]
|
||||
default_sampling_params:
|
||||
temperature: 0.0
|
||||
max_tokens: 32767
|
||||
@@ -20,6 +20,8 @@ logger = init_logger(__name__)
|
||||
_ARCH_TO_MODEL_TYPE: dict[str, str] = {
|
||||
"CosyVoice3Model": "cosyvoice3",
|
||||
"GLMTTSForConditionalGeneration": "glm_tts",
|
||||
"IndexTTS2S2MelDecoder": "indextts2",
|
||||
"IndexTTS2TalkerForConditionalGeneration": "indextts2",
|
||||
"OmniVoiceModel": "omnivoice",
|
||||
"VoxCPM2TalkerForConditionalGeneration": "voxcpm2",
|
||||
}
|
||||
@@ -35,6 +37,9 @@ def _register_omni_hf_configs() -> None:
|
||||
try:
|
||||
from transformers import AutoConfig
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.configuration_indextts2 import (
|
||||
IndexTTS2Config,
|
||||
)
|
||||
from vllm_omni.model_executor.models.ming_tts.config_ming_tts import MingDenseConfig
|
||||
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import (
|
||||
Qwen3TTSConfig,
|
||||
@@ -57,6 +62,7 @@ def _register_omni_hf_configs() -> None:
|
||||
|
||||
for model_type, config_cls in [
|
||||
("dense", MingDenseConfig),
|
||||
("indextts2", IndexTTS2Config),
|
||||
("qwen3_tts", Qwen3TTSConfig),
|
||||
("cosyvoice3", CosyVoice3Config),
|
||||
("glm_tts", GLMTTSConfig),
|
||||
|
||||
@@ -85,6 +85,7 @@ _HIGGS_AUDIO_V2_TTS_MODEL_STAGES = {"higgs_audio_v2"}
|
||||
_HIGGS_V3_TTS_MODEL_STAGES = {"higgs_audio_v3"}
|
||||
_GLM_TTS_MODEL_STAGES = {"glm_tts"}
|
||||
_STEP_AUDIO2_TTS_MODEL_STAGES = {"step_audio2_thinker"}
|
||||
_INDEXTTS2_TTS_MODEL_STAGES = {"indextts2_talker"}
|
||||
_TTS_MODEL_STAGES: set[str] = (
|
||||
_VOXTRAL_TTS_MODEL_STAGES
|
||||
| _QWEN3_TTS_MODEL_STAGES
|
||||
@@ -100,6 +101,7 @@ _TTS_MODEL_STAGES: set[str] = (
|
||||
| _MOSS_TTS_FULL_MODEL_STAGES
|
||||
| _GLM_TTS_MODEL_STAGES
|
||||
| _STEP_AUDIO2_TTS_MODEL_STAGES
|
||||
| _INDEXTTS2_TTS_MODEL_STAGES
|
||||
)
|
||||
_SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = {
|
||||
"fish_tts",
|
||||
@@ -109,6 +111,7 @@ _SAMPLING_MAX_TOKENS_TTS_MODEL_TYPES = {
|
||||
"voxcpm2",
|
||||
"higgs_audio_v2",
|
||||
"higgs_audio_v3",
|
||||
"indextts2",
|
||||
}
|
||||
_TTS_LANGUAGES = frozenset(
|
||||
{
|
||||
@@ -693,6 +696,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
return "glm_tts"
|
||||
if model_stage in _STEP_AUDIO2_TTS_MODEL_STAGES:
|
||||
return "step_audio2"
|
||||
if model_stage in _INDEXTTS2_TTS_MODEL_STAGES:
|
||||
return "indextts2"
|
||||
return None
|
||||
|
||||
def _get_custom_voice_dir(self) -> str | None:
|
||||
@@ -3429,6 +3434,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
|
||||
model_type = "higgs_audio_v2"
|
||||
elif self._tts_model_type == "glm_tts":
|
||||
model_type = "glm_tts"
|
||||
elif self._tts_model_type == "indextts2":
|
||||
model_type = "indextts2"
|
||||
elif self._is_tts:
|
||||
model_type = tts_params.get("task_type", ["unknown"])[0]
|
||||
else:
|
||||
|
||||
@@ -57,6 +57,7 @@ from vllm_omni.entrypoints.openai.tts_adapters import ( # noqa: E402,F401
|
||||
glm_tts,
|
||||
higgs_audio_v2,
|
||||
higgs_audio_v3,
|
||||
indextts2,
|
||||
ming_tts,
|
||||
moss_tts,
|
||||
omnivoice,
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
"""IndexTTS2 serving adapter."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from vllm.inputs import tokens_input
|
||||
from vllm.utils import random_uuid
|
||||
|
||||
from vllm_omni.entrypoints.openai.tts_adapters import register_tts_adapter
|
||||
from vllm_omni.entrypoints.openai.tts_adapters.base import ARTTSAdapter, PreparedRequest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm_omni.entrypoints.openai.protocol.audio import OpenAICreateSpeechRequest
|
||||
|
||||
_TTS_MAX_NEW_TOKENS_MIN = 1
|
||||
_TTS_MAX_NEW_TOKENS_MAX = 4096
|
||||
_INDEXTTS2_EMOTION_KEYS = ("emo_audio", "emo_vector", "emo_alpha", "emo_text", "use_emo_text", "use_random")
|
||||
|
||||
|
||||
def _update_conditioning_hash(h: hashlib._Hash, value: Any) -> None:
|
||||
"""Hash request conditioning values without serializing huge repr strings."""
|
||||
if value is None:
|
||||
h.update(b"none")
|
||||
return
|
||||
if isinstance(value, torch.Tensor):
|
||||
arr = value.detach().cpu().numpy()
|
||||
h.update(b"tensor")
|
||||
h.update(repr((arr.shape, str(arr.dtype))).encode("utf-8"))
|
||||
h.update(np.ascontiguousarray(arr).tobytes())
|
||||
return
|
||||
if isinstance(value, np.ndarray):
|
||||
arr = np.ascontiguousarray(value)
|
||||
h.update(b"ndarray")
|
||||
h.update(repr((arr.shape, str(arr.dtype))).encode("utf-8"))
|
||||
h.update(arr.tobytes())
|
||||
return
|
||||
if isinstance(value, (list, tuple)) and len(value) == 2 and isinstance(value[1], (int, np.integer)):
|
||||
samples = value[0]
|
||||
if isinstance(samples, (list, tuple, np.ndarray, torch.Tensor)):
|
||||
if isinstance(samples, torch.Tensor):
|
||||
samples_arr = samples.detach().cpu().numpy()
|
||||
else:
|
||||
samples_arr = np.asarray(samples, dtype=np.float32)
|
||||
samples_arr = np.ascontiguousarray(samples_arr, dtype=np.float32)
|
||||
h.update(b"audio")
|
||||
h.update(int(value[1]).to_bytes(8, byteorder="little", signed=True))
|
||||
h.update(int(samples_arr.size).to_bytes(8, byteorder="little", signed=False))
|
||||
h.update(samples_arr.tobytes())
|
||||
return
|
||||
if isinstance(value, Mapping):
|
||||
h.update(b"mapping")
|
||||
for key in sorted(value):
|
||||
h.update(b"\x01")
|
||||
h.update(repr(key).encode("utf-8"))
|
||||
h.update(b"\x02")
|
||||
_update_conditioning_hash(h, value[key])
|
||||
return
|
||||
if isinstance(value, (list, tuple)):
|
||||
h.update(f"sequence:{len(value)}".encode())
|
||||
for item in value:
|
||||
h.update(b"\x03")
|
||||
_update_conditioning_hash(h, item)
|
||||
return
|
||||
h.update(repr(value).encode("utf-8"))
|
||||
|
||||
|
||||
def _first_conditioning_value(value: Any) -> Any:
|
||||
if isinstance(value, list) and len(value) == 1:
|
||||
return value[0]
|
||||
return value
|
||||
|
||||
|
||||
def indextts2_conditioning_cache_salt(
|
||||
request: OpenAICreateSpeechRequest,
|
||||
tts_params: dict[str, Any],
|
||||
*,
|
||||
request_id: str | None = None,
|
||||
) -> str:
|
||||
"""Stable prefix-cache salt for IndexTTS2 placeholder prompts.
|
||||
|
||||
IndexTTS2 builds prefill embeddings from ``additional_information`` (speaker
|
||||
audio plus emotion controls), while the scheduler only sees dummy token ids.
|
||||
Include all supported control inputs so prefix-cache hits are only possible
|
||||
when text, speaker, and emotion conditioning are identical.
|
||||
"""
|
||||
h = hashlib.sha256()
|
||||
for key in (
|
||||
"text",
|
||||
"voice",
|
||||
"voice_name",
|
||||
"voice_created_at",
|
||||
"emo_audio",
|
||||
"emo_vector",
|
||||
"emo_alpha",
|
||||
"emo_text",
|
||||
"use_emo_text",
|
||||
"use_random",
|
||||
):
|
||||
h.update(b"\x00")
|
||||
h.update(key.encode("utf-8"))
|
||||
h.update(b"\x00")
|
||||
_update_conditioning_hash(h, tts_params.get(key))
|
||||
|
||||
for key, value in (
|
||||
("input", request.input),
|
||||
("ref_audio", request.ref_audio),
|
||||
):
|
||||
h.update(b"\x00")
|
||||
h.update(key.encode("utf-8"))
|
||||
h.update(b"\x00")
|
||||
_update_conditioning_hash(h, value)
|
||||
|
||||
use_random = bool(_first_conditioning_value(tts_params.get("use_random")))
|
||||
if use_random:
|
||||
h.update(b"\x00indextts2-use-random-request\x00")
|
||||
h.update((request_id or random_uuid()).encode("utf-8"))
|
||||
return h.hexdigest()[:32]
|
||||
|
||||
|
||||
@register_tts_adapter
|
||||
class IndexTTS2Adapter(ARTTSAdapter):
|
||||
stage_keys = frozenset({"indextts2_talker"})
|
||||
name = "indextts2"
|
||||
|
||||
def validate(self, request: OpenAICreateSpeechRequest) -> str | None:
|
||||
server = self.ctx.server
|
||||
if not request.input or not request.input.strip():
|
||||
return "Input text cannot be empty"
|
||||
|
||||
voice_lower = request.voice.lower() if isinstance(request.voice, str) else None
|
||||
has_uploaded_voice = bool(
|
||||
voice_lower
|
||||
and voice_lower in server.uploaded_speakers
|
||||
and server._get_uploaded_audio_data(voice_lower) is not None
|
||||
)
|
||||
if request.ref_audio is None and not has_uploaded_voice:
|
||||
return "IndexTTS2 requires 'ref_audio' or an uploaded audio voice for voice cloning"
|
||||
if request.ref_audio is not None:
|
||||
fmt_err = server._validate_ref_audio_format(request.ref_audio)
|
||||
if fmt_err:
|
||||
return fmt_err
|
||||
if request.max_new_tokens is not None:
|
||||
if request.max_new_tokens < _TTS_MAX_NEW_TOKENS_MIN:
|
||||
return f"max_new_tokens must be at least {_TTS_MAX_NEW_TOKENS_MIN}"
|
||||
if request.max_new_tokens > _TTS_MAX_NEW_TOKENS_MAX:
|
||||
return f"max_new_tokens cannot exceed {_TTS_MAX_NEW_TOKENS_MAX}"
|
||||
if request.extra_params is not None:
|
||||
if not isinstance(request.extra_params, dict):
|
||||
return "extra_params must be a JSON object/dict."
|
||||
extra_err = self._validate_extra_params(request.extra_params)
|
||||
if extra_err:
|
||||
return extra_err
|
||||
return None
|
||||
|
||||
def _validate_extra_params(self, extras: Mapping[str, Any]) -> str | None:
|
||||
server = self.ctx.server
|
||||
if "emo_audio" in extras:
|
||||
value = extras["emo_audio"]
|
||||
if not isinstance(value, str):
|
||||
return "extra_params.emo_audio must be a URL (http/https), base64 data URL (data:...), or file URI (file://...)"
|
||||
fmt_err = server._validate_ref_audio_format(value)
|
||||
if fmt_err:
|
||||
return fmt_err.replace("ref_audio", "extra_params.emo_audio", 1)
|
||||
|
||||
if "emo_vector" in extras:
|
||||
value = extras["emo_vector"]
|
||||
if not isinstance(value, list) or len(value) != 8:
|
||||
return "extra_params.emo_vector must be a list of 8 numbers"
|
||||
for item in value:
|
||||
if isinstance(item, bool) or not isinstance(item, (int, float)) or not math.isfinite(float(item)):
|
||||
return "extra_params.emo_vector must be a list of 8 finite numbers"
|
||||
if float(item) < 0.0 or float(item) > 1.2:
|
||||
return "extra_params.emo_vector values must be in [0, 1.2]"
|
||||
|
||||
if "emo_alpha" in extras:
|
||||
value = extras["emo_alpha"]
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(float(value)):
|
||||
return "extra_params.emo_alpha must be a finite number"
|
||||
|
||||
if "emo_text" in extras and extras["emo_text"] is not None and not isinstance(extras["emo_text"], str):
|
||||
return "extra_params.emo_text must be a string"
|
||||
|
||||
for key in ("use_emo_text", "use_random"):
|
||||
if key in extras and not isinstance(extras[key], bool):
|
||||
return f"extra_params.{key} must be a boolean"
|
||||
return None
|
||||
|
||||
async def build(
|
||||
self,
|
||||
request: OpenAICreateSpeechRequest,
|
||||
sampling_params_list: list,
|
||||
has_inline_ref_audio: bool,
|
||||
) -> PreparedRequest:
|
||||
del sampling_params_list, has_inline_ref_audio
|
||||
|
||||
server = self.ctx.server
|
||||
tts_params = await self._build_params(request)
|
||||
from vllm_omni.model_executor.models.indextts2.prompt_utils import (
|
||||
estimate_indextts2_prefill_prompt_len,
|
||||
)
|
||||
|
||||
ph_len = estimate_indextts2_prefill_prompt_len(
|
||||
server.engine_client.model_config.model,
|
||||
request.input,
|
||||
)
|
||||
prompt = tokens_input(prompt_token_ids=[1] * ph_len)
|
||||
prompt["additional_information"] = tts_params
|
||||
prompt["cache_salt"] = indextts2_conditioning_cache_salt(request, tts_params)
|
||||
return PreparedRequest(prompt=prompt, tts_params=tts_params, model_type="indextts2")
|
||||
|
||||
async def _build_params(self, request: OpenAICreateSpeechRequest) -> dict[str, Any]:
|
||||
server = self.ctx.server
|
||||
params: dict[str, Any] = {"text": [request.input]}
|
||||
voice_lower = request.voice.lower() if isinstance(request.voice, str) else None
|
||||
ref_audio_source = request.ref_audio
|
||||
using_uploaded_voice = False
|
||||
if ref_audio_source is None and voice_lower and voice_lower in server.uploaded_speakers:
|
||||
ref_audio_source = server._get_uploaded_audio_data(voice_lower)
|
||||
using_uploaded_voice = ref_audio_source is not None
|
||||
if ref_audio_source is not None and isinstance(ref_audio_source, str):
|
||||
wav_list, sr = await server._resolve_ref_audio(ref_audio_source)
|
||||
params["voice"] = [[wav_list, sr]]
|
||||
if using_uploaded_voice and voice_lower:
|
||||
params["voice_name"] = [voice_lower]
|
||||
params["voice_created_at"] = [server._voice_created_at(voice_lower)]
|
||||
|
||||
extras = request.extra_params if isinstance(request.extra_params, dict) else {}
|
||||
for key in _INDEXTTS2_EMOTION_KEYS:
|
||||
if key not in extras:
|
||||
continue
|
||||
if key == "emo_audio":
|
||||
wav_list, sr = await server._resolve_ref_audio(extras[key])
|
||||
params[key] = [[wav_list, sr]]
|
||||
else:
|
||||
params[key] = [extras[key]]
|
||||
return params
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,284 @@
|
||||
"""CUDA graph wrapper for BigVGAN vocoding in IndexTTS2 Stage 1.
|
||||
|
||||
BigVGAN is heavily CPU launch-bound (Snake/AliasFree small-op storm:
|
||||
~3500 copy_ + ~2000 mul + ~340 cudnn_convolution launches per request,
|
||||
GPU busy <15%). Capturing the forward for fixed mel-frame buckets and
|
||||
replaying collapses thousands of kernel launches into one graph launch.
|
||||
|
||||
Dynamic mel inputs are right zero-padded to the nearest captured bucket
|
||||
and the waveform is trimmed back to ``actual_frames * upsample_factor``
|
||||
after replay. Padding leakage into the kept region is bounded by the
|
||||
conv receptive field at the tail (~20-38 mel frames, ~0.2-0.4s audio)
|
||||
and is validated via WER/SIM A/B.
|
||||
|
||||
The compile layer (``compile_shapes``) additionally applies
|
||||
``torch.compile`` to the BigVGAN forward before CUDA-graph capture,
|
||||
letting Inductor fuse the Snake/alias-free small-op chain. The
|
||||
three-tier dispatch at runtime is: compiled-graph > plain-graph > eager.
|
||||
|
||||
Modeled on ``CUDAGraphDecoderWrapper``
|
||||
(vllm_omni/model_executor/models/qwen3_tts/cuda_graph_decoder_wrapper.py)
|
||||
and ``CUDAGraphGLMTTSDiTWrapper``
|
||||
(vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Mel-frame buckets at 22050 Hz / hop 256 (~86 fps).
|
||||
# Keep buckets modest so that CUDA-graph private pools do not exhaust
|
||||
# GPU memory when stage0 and stage1 share the same device (~22 GiB).
|
||||
# Inputs longer than the largest bucket fall back to eager BigVGAN.
|
||||
DEFAULT_CAPTURE_SIZES = [128, 256, 384, 512]
|
||||
|
||||
|
||||
class CUDAGraphBigVGANWrapper:
|
||||
"""Capture BigVGAN vocoding for fixed mel-frame buckets.
|
||||
|
||||
Supports an optional torch.compile layer on top of plain CUDA graphs.
|
||||
When ``compile_shapes`` is provided, those buckets are compiled with
|
||||
``torch.compile(mode="default", fullgraph=False, dynamic=False)`` and
|
||||
then captured into CUDA graphs — giving both Inductor operator fusion
|
||||
and zero-launch-overhead replay.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: torch.nn.Module,
|
||||
*,
|
||||
enabled: bool = True,
|
||||
capture_sizes: list[int] | None = None,
|
||||
compile_shapes: list[int] | None = None,
|
||||
) -> None:
|
||||
self.model = model
|
||||
self.enabled = bool(enabled)
|
||||
self.capture_sizes = sorted(set(capture_sizes or DEFAULT_CAPTURE_SIZES))
|
||||
self.compile_shapes = sorted(set(compile_shapes or []))
|
||||
self.num_mels = int(model.h.num_mels)
|
||||
self.upsample_factor = int(math.prod(model.h.upsample_rates))
|
||||
|
||||
# Plain CUDA graph tier
|
||||
self.graphs: dict[int, torch.cuda.CUDAGraph] = {}
|
||||
self.static_mel: dict[int, torch.Tensor] = {}
|
||||
self.static_wav: dict[int, torch.Tensor] = {}
|
||||
|
||||
# Compiled CUDA graph tier
|
||||
self._compiled_decoder: Callable[[torch.Tensor], torch.Tensor] | None = None
|
||||
self._compiled_graphs: dict[int, torch.cuda.CUDAGraph] = {}
|
||||
self._compiled_static_mel: dict[int, torch.Tensor] = {}
|
||||
self._compiled_static_wav: dict[int, torch.Tensor] = {}
|
||||
self._compiled_shapes: set[int] = set()
|
||||
|
||||
self._warmed_up = False
|
||||
self.last_call_info: dict[str, int | str | None] = {}
|
||||
|
||||
def warmup(self, device: torch.device, dtype: torch.dtype) -> None:
|
||||
if not self.enabled or self._warmed_up or device.type != "cuda" or torch.cuda.is_current_stream_capturing():
|
||||
return
|
||||
self.model.eval()
|
||||
|
||||
# Precompute Snake/SnakeBeta caches so torch.compile sees stable
|
||||
# buffers instead of lazy init inside the traced graph.
|
||||
self._precompute_snake_caches()
|
||||
|
||||
logger.info("Starting BigVGAN CUDA graph warmup for mel-frame buckets: %s", self.capture_sizes)
|
||||
for size in self.capture_sizes:
|
||||
try:
|
||||
self._capture(size, device, dtype)
|
||||
logger.info("Captured BigVGAN CUDA graph for mel frames=%d", size)
|
||||
except Exception:
|
||||
logger.warning("Failed to capture BigVGAN CUDA graph for size=%d", size, exc_info=True)
|
||||
|
||||
if self.compile_shapes:
|
||||
self._warmup_compile_shapes(device, dtype)
|
||||
|
||||
self._warmed_up = True
|
||||
|
||||
def _precompute_snake_caches(self) -> None:
|
||||
from vllm_omni.model_executor.models.common.snake_activation import Snake, SnakeBeta
|
||||
|
||||
for module in self.model.modules():
|
||||
if isinstance(module, (Snake, SnakeBeta)):
|
||||
module.precompute_exp_cache()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Plain CUDA graph capture
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _capture(self, size: int, device: torch.device, dtype: torch.dtype) -> None:
|
||||
mel = torch.zeros(1, self.num_mels, size, device=device, dtype=dtype)
|
||||
self.static_mel[size] = mel
|
||||
# Eager warm run so cuDNN autotuning happens outside capture.
|
||||
with torch.no_grad():
|
||||
_ = self.model(mel)
|
||||
torch.accelerator.synchronize(device)
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.no_grad(), torch.cuda.graph(graph, pool=current_platform.get_global_graph_pool()):
|
||||
self.static_wav[size] = self.model(mel)
|
||||
self.graphs[size] = graph
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Compiled CUDA graph capture (torch.compile + capture)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _warmup_compile_shapes(self, device: torch.device, dtype: torch.dtype) -> None:
|
||||
logger.info(
|
||||
"Starting torch.compile + CUDA Graph warmup for BigVGAN shapes: %s",
|
||||
self.compile_shapes,
|
||||
)
|
||||
try:
|
||||
self._compiled_decoder = torch.compile(
|
||||
self.model.forward,
|
||||
mode="default",
|
||||
fullgraph=False,
|
||||
dynamic=False,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning("Failed to create torch.compile BigVGAN wrapper", exc_info=True)
|
||||
self._compiled_decoder = None
|
||||
return
|
||||
|
||||
for size in self.compile_shapes:
|
||||
try:
|
||||
self._capture_compiled(size, device, dtype)
|
||||
self._compiled_shapes.add(size)
|
||||
logger.info(
|
||||
" torch.compile + CUDA Graph ready for mel frames=%d",
|
||||
size,
|
||||
)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
" Failed to capture torch.compile CUDA Graph for size=%d; "
|
||||
"this shape will use plain CUDA graph/eager fallback",
|
||||
size,
|
||||
exc_info=True,
|
||||
)
|
||||
logger.info(
|
||||
"torch.compile + CUDA Graph warmup complete: %d/%d shapes ready",
|
||||
len(self._compiled_shapes),
|
||||
len(self.compile_shapes),
|
||||
)
|
||||
|
||||
def _capture_compiled(self, size: int, device: torch.device, dtype: torch.dtype) -> None:
|
||||
if self._compiled_decoder is None:
|
||||
raise RuntimeError("Compiled decoder is not initialized")
|
||||
|
||||
mel = torch.zeros(1, self.num_mels, size, device=device, dtype=dtype)
|
||||
# Warmup iterations let Inductor finish tracing/autotuning before capture.
|
||||
with torch.inference_mode():
|
||||
for _ in range(5):
|
||||
_ = self._compiled_decoder(mel)
|
||||
torch.accelerator.synchronize(device)
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.inference_mode():
|
||||
with torch.cuda.graph(graph, pool=current_platform.get_global_graph_pool()):
|
||||
static_wav = self._compiled_decoder(mel)
|
||||
|
||||
self._compiled_static_mel[size] = mel
|
||||
self._compiled_static_wav[size] = static_wav
|
||||
self._compiled_graphs[size] = graph
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_padded_size(self, actual_frames: int) -> int | None:
|
||||
for size in self.capture_sizes:
|
||||
if actual_frames <= size:
|
||||
return size
|
||||
return None
|
||||
|
||||
def __call__(self, mel: torch.Tensor) -> torch.Tensor:
|
||||
"""Vocode ``mel`` [1, num_mels, T]; falls back to eager when ineligible."""
|
||||
actual_frames = int(mel.shape[-1]) if mel.ndim >= 3 else 0
|
||||
|
||||
# Eligibility guard
|
||||
if (
|
||||
not self.enabled
|
||||
or not self._warmed_up
|
||||
or mel.shape[0] != 1
|
||||
or mel.device.type != "cuda"
|
||||
or torch.cuda.is_current_stream_capturing()
|
||||
):
|
||||
self.last_call_info = {
|
||||
"mode": "eager",
|
||||
"reason": "ineligible",
|
||||
"batch": int(mel.shape[0]) if mel.ndim > 0 else 0,
|
||||
"frames": actual_frames,
|
||||
"bucket": None,
|
||||
"padding": None,
|
||||
}
|
||||
return self.model(mel)
|
||||
|
||||
# Tier 1: compiled CUDA graph (exact shape match)
|
||||
if actual_frames in self._compiled_shapes:
|
||||
return self._replay_compiled(actual_frames, actual_frames, mel, "exact_hit")
|
||||
|
||||
# Tier 1b: compiled CUDA graph (padded shape)
|
||||
padded = self._get_padded_size(actual_frames)
|
||||
if padded is not None and padded in self._compiled_shapes:
|
||||
return self._replay_compiled(actual_frames, padded, mel, "padded_hit")
|
||||
|
||||
# Tier 2: plain CUDA graph
|
||||
size = padded
|
||||
if size is None or size not in self.graphs:
|
||||
self.last_call_info = {
|
||||
"mode": "eager",
|
||||
"reason": "no_bucket",
|
||||
"batch": int(mel.shape[0]),
|
||||
"frames": actual_frames,
|
||||
"bucket": size,
|
||||
"padding": None if size is None else int(size - actual_frames),
|
||||
}
|
||||
return self.model(mel)
|
||||
|
||||
buf = self.static_mel[size]
|
||||
if actual_frames == size:
|
||||
buf.copy_(mel.to(buf.dtype))
|
||||
else:
|
||||
buf.zero_()
|
||||
buf[..., :actual_frames].copy_(mel.to(buf.dtype))
|
||||
self.graphs[size].replay()
|
||||
self.last_call_info = {
|
||||
"mode": "graph",
|
||||
"reason": "hit",
|
||||
"batch": int(mel.shape[0]),
|
||||
"frames": actual_frames,
|
||||
"bucket": int(size),
|
||||
"padding": int(size - actual_frames),
|
||||
}
|
||||
return self.static_wav[size][..., : actual_frames * self.upsample_factor].clone()
|
||||
|
||||
def _replay_compiled(
|
||||
self,
|
||||
actual_frames: int,
|
||||
bucket: int,
|
||||
mel: torch.Tensor,
|
||||
reason: str,
|
||||
) -> torch.Tensor:
|
||||
buf = self._compiled_static_mel[bucket]
|
||||
if actual_frames == bucket:
|
||||
buf.copy_(mel.to(buf.dtype))
|
||||
else:
|
||||
buf.zero_()
|
||||
buf[..., :actual_frames].copy_(mel.to(buf.dtype))
|
||||
self._compiled_graphs[bucket].replay()
|
||||
self.last_call_info = {
|
||||
"mode": "compile_graph",
|
||||
"reason": reason,
|
||||
"batch": int(mel.shape[0]),
|
||||
"frames": actual_frames,
|
||||
"bucket": int(bucket),
|
||||
"padding": int(bucket - actual_frames),
|
||||
}
|
||||
return self._compiled_static_wav[bucket][..., : actual_frames * self.upsample_factor].clone()
|
||||
@@ -0,0 +1,156 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import copy
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
|
||||
class IndexTTS2Config(PretrainedConfig):
|
||||
model_type = "indextts2"
|
||||
|
||||
# Defaults for the official IndexTTS2 checkpoint. Values from HF
|
||||
# config.json (passed via ``kwargs``) take precedence over these.
|
||||
_DEFAULTS = {
|
||||
"num_attention_heads": 20,
|
||||
"hidden_size": 1280,
|
||||
"dataset": {
|
||||
"bpe_model": "bpe.model",
|
||||
"sample_rate": 24000,
|
||||
"squeeze": False,
|
||||
"mel": {
|
||||
"sample_rate": 24000,
|
||||
"n_fft": 1024,
|
||||
"hop_length": 256,
|
||||
"win_length": 1024,
|
||||
"n_mels": 100,
|
||||
"mel_fmin": 0,
|
||||
"normalize": False,
|
||||
},
|
||||
},
|
||||
"gpt": {
|
||||
"model_dim": 1280,
|
||||
"max_mel_tokens": 1815,
|
||||
"max_text_tokens": 600,
|
||||
"heads": 20,
|
||||
"use_mel_codes_as_input": True,
|
||||
"mel_length_compression": 1024,
|
||||
"layers": 24,
|
||||
"number_text_tokens": 12000,
|
||||
"number_mel_codes": 8194,
|
||||
"start_mel_token": 8192,
|
||||
"stop_mel_token": 8193,
|
||||
"start_text_token": 0,
|
||||
"stop_text_token": 1,
|
||||
"train_solo_embeddings": False,
|
||||
"condition_num_latent": 32,
|
||||
"condition_type": "conformer_perceiver",
|
||||
"condition_module": {
|
||||
"output_size": 512,
|
||||
"linear_units": 2048,
|
||||
"attention_heads": 8,
|
||||
"num_blocks": 6,
|
||||
"input_layer": "conv2d2",
|
||||
"perceiver_mult": 2,
|
||||
},
|
||||
"emo_condition_module": {
|
||||
"output_size": 512,
|
||||
"linear_units": 1024,
|
||||
"attention_heads": 4,
|
||||
"num_blocks": 4,
|
||||
"input_layer": "conv2d2",
|
||||
"perceiver_mult": 2,
|
||||
},
|
||||
},
|
||||
"semantic_codec": {
|
||||
"codebook_size": 8192,
|
||||
"hidden_size": 1024,
|
||||
"codebook_dim": 8,
|
||||
"vocos_dim": 384,
|
||||
"vocos_intermediate_dim": 2048,
|
||||
"vocos_num_layers": 12,
|
||||
},
|
||||
"s2mel": {
|
||||
"preprocess_params": {
|
||||
"sr": 22050,
|
||||
"spect_params": {
|
||||
"n_fft": 1024,
|
||||
"win_length": 1024,
|
||||
"hop_length": 256,
|
||||
"n_mels": 80,
|
||||
"fmin": 0,
|
||||
"fmax": "None",
|
||||
},
|
||||
},
|
||||
"dit_type": "DiT",
|
||||
"reg_loss_type": "l1",
|
||||
"style_encoder": {"dim": 192},
|
||||
"length_regulator": {
|
||||
"channels": 512,
|
||||
"is_discrete": False,
|
||||
"in_channels": 1024,
|
||||
"content_codebook_size": 2048,
|
||||
"sampling_ratios": [1, 1, 1, 1],
|
||||
"vector_quantize": False,
|
||||
"n_codebooks": 1,
|
||||
"quantizer_dropout": 0.0,
|
||||
"f0_condition": False,
|
||||
"n_f0_bins": 512,
|
||||
},
|
||||
"DiT": {
|
||||
"hidden_dim": 512,
|
||||
"num_heads": 8,
|
||||
"depth": 13,
|
||||
"class_dropout_prob": 0.1,
|
||||
"block_size": 8192,
|
||||
"in_channels": 80,
|
||||
"style_condition": True,
|
||||
"final_layer_type": "wavenet",
|
||||
"target": "mel",
|
||||
"content_dim": 512,
|
||||
"content_codebook_size": 1024,
|
||||
"content_type": "discrete",
|
||||
"f0_condition": False,
|
||||
"n_f0_bins": 512,
|
||||
"content_codebooks": 1,
|
||||
"is_causal": False,
|
||||
"long_skip_connection": True,
|
||||
"zero_prompt_speech_token": False,
|
||||
"time_as_token": False,
|
||||
"style_as_token": False,
|
||||
"uvit_skip_connection": True,
|
||||
"add_resblock_in_transformer": False,
|
||||
},
|
||||
"wavenet": {
|
||||
"hidden_dim": 512,
|
||||
"num_layers": 8,
|
||||
"kernel_size": 5,
|
||||
"dilation_rate": 1,
|
||||
"p_dropout": 0.2,
|
||||
"style_condition": True,
|
||||
},
|
||||
},
|
||||
"gpt_checkpoint": "gpt.pth",
|
||||
"w2v_stat": "wav2vec2bert_stats.pt",
|
||||
"s2mel_checkpoint": "s2mel.pth",
|
||||
"emo_matrix": "feat2.pt",
|
||||
"spk_matrix": "feat1.pt",
|
||||
"emo_num": [3, 17, 2, 8, 4, 5, 10, 24],
|
||||
"qwen_emo_path": "qwen0.6bemo4-merge/",
|
||||
"vocoder": {"type": "bigvgan", "name": "nvidia/bigvgan_v2_22khz_80band_256x"},
|
||||
"version": 2.0,
|
||||
# Default-off performance tracing for IndexTTS2 overhead analysis.
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
defaults = copy.deepcopy(self._DEFAULTS)
|
||||
for attr, default in defaults.items():
|
||||
if not hasattr(self, attr):
|
||||
setattr(self, attr, default)
|
||||
# Keep gpt.model_dim in sync with top-level hidden_size.
|
||||
if isinstance(self.gpt, dict) and self.gpt.get("model_dim") != self.hidden_size:
|
||||
self.gpt["model_dim"] = self.hidden_size
|
||||
self.output_sample_rate = int(
|
||||
self.s2mel["preprocess_params"].get("sr", 22050) if isinstance(self.s2mel, dict) else 22050
|
||||
)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Per-shape CUDA graph capture for the S2Mel DiT transformer core.
|
||||
|
||||
Profiling showed the DiT estimator forward is CPU launch-bound (the GPU
|
||||
is idle ~85% of CFM wall time). Within one request the CFM runs 12 Euler
|
||||
steps with *identical* tensor shapes (T_in never changes), so the graph
|
||||
captured on the first step is replayed by every subsequent step. Across
|
||||
requests T_in varies, so graphs are cached per shape with a small LRU;
|
||||
a cache miss costs roughly one extra eager step (warm run + capture).
|
||||
|
||||
Capture safety relies on the IndexTTS2 S2Mel decoder only enabling this
|
||||
runner for full-mask shapes:
|
||||
|
||||
- ``x_lens == T`` for every sample (the decoder feeds full-length sequences), so
|
||||
the attention padding mask is all-True and dense flash attention is
|
||||
mathematically identical. The decoder sets ``_assume_full_mask`` on
|
||||
the gpt_fast ``Attention`` modules so the backend skips its
|
||||
``torch.any(~mask)`` D2H sync, which would abort stream capture.
|
||||
|
||||
Modeled on ``CUDAGraphGLMTTSDiTWrapper``
|
||||
(vllm_omni/model_executor/models/glm_tts/glm_tts_dit_wrapper.py).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.platforms import current_platform
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _GraphEntry:
|
||||
graph: torch.cuda.CUDAGraph
|
||||
static_x: torch.Tensor
|
||||
static_c: torch.Tensor
|
||||
static_pos: torch.Tensor
|
||||
static_mask: torch.Tensor | None
|
||||
static_out: torch.Tensor
|
||||
|
||||
|
||||
class CUDAGraphDiTRunner:
|
||||
"""Replay the gpt_fast Transformer forward via per-shape CUDA graphs."""
|
||||
|
||||
def __init__(self, transformer: torch.nn.Module, *, max_graphs: int = 4, enabled: bool = True) -> None:
|
||||
self.transformer = transformer
|
||||
self.max_graphs = int(max_graphs)
|
||||
self.enabled = bool(enabled)
|
||||
self._cache: OrderedDict[tuple, _GraphEntry] = OrderedDict()
|
||||
self.last_call_info: dict[str, object] = {}
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
x_in: torch.Tensor,
|
||||
c: torch.Tensor,
|
||||
input_pos: torch.Tensor,
|
||||
mask: torch.Tensor | None,
|
||||
) -> torch.Tensor:
|
||||
if not self.enabled or x_in.device.type != "cuda" or torch.cuda.is_current_stream_capturing():
|
||||
self.last_call_info = {
|
||||
"mode": "eager",
|
||||
"reason": "ineligible",
|
||||
"shape": tuple(x_in.shape),
|
||||
"cache_size": len(self._cache),
|
||||
}
|
||||
return self.transformer(x_in, c, input_pos, mask)
|
||||
|
||||
mask_key = None if mask is None else (tuple(mask.shape), mask.dtype)
|
||||
device_key = (x_in.device.type, x_in.device.index)
|
||||
key = (
|
||||
device_key,
|
||||
tuple(x_in.shape),
|
||||
x_in.dtype,
|
||||
tuple(c.shape),
|
||||
c.dtype,
|
||||
tuple(input_pos.shape),
|
||||
input_pos.dtype,
|
||||
mask_key,
|
||||
)
|
||||
entry = self._cache.get(key)
|
||||
if entry is None:
|
||||
entry = self._capture(x_in, c, input_pos, mask)
|
||||
if entry is None:
|
||||
self.last_call_info = {
|
||||
"mode": "eager",
|
||||
"reason": "capture_failed",
|
||||
"shape": tuple(x_in.shape),
|
||||
"cache_size": len(self._cache),
|
||||
}
|
||||
return self.transformer(x_in, c, input_pos, mask)
|
||||
self._cache[key] = entry
|
||||
while len(self._cache) > self.max_graphs:
|
||||
_, evicted = self._cache.popitem(last=False)
|
||||
del evicted
|
||||
# Kernels are only recorded (not executed) during capture, so
|
||||
# replay once to produce the first real output.
|
||||
entry.graph.replay()
|
||||
self.last_call_info = {
|
||||
"mode": "graph",
|
||||
"reason": "capture",
|
||||
"shape": tuple(x_in.shape),
|
||||
"cache_size": len(self._cache),
|
||||
}
|
||||
return entry.static_out.clone()
|
||||
|
||||
self._cache.move_to_end(key)
|
||||
entry.static_x.copy_(x_in)
|
||||
entry.static_c.copy_(c)
|
||||
entry.static_pos.copy_(input_pos)
|
||||
if entry.static_mask is not None and mask is not None:
|
||||
entry.static_mask.copy_(mask)
|
||||
entry.graph.replay()
|
||||
self.last_call_info = {
|
||||
"mode": "graph",
|
||||
"reason": "hit",
|
||||
"shape": tuple(x_in.shape),
|
||||
"cache_size": len(self._cache),
|
||||
}
|
||||
return entry.static_out.clone()
|
||||
|
||||
def _capture(
|
||||
self,
|
||||
x_in: torch.Tensor,
|
||||
c: torch.Tensor,
|
||||
input_pos: torch.Tensor,
|
||||
mask: torch.Tensor | None,
|
||||
) -> _GraphEntry | None:
|
||||
static_x = x_in.clone()
|
||||
static_c = c.clone()
|
||||
static_pos = input_pos.clone()
|
||||
# The mask content is constant (all-True; see module docstring) and
|
||||
# ignored by Attention via `_assume_full_mask`, but keep a static
|
||||
# clone so the captured graph never dereferences a freed tensor.
|
||||
static_mask = mask.contiguous().clone() if mask is not None else None
|
||||
try:
|
||||
# Eager warm run: cuDNN/flash-attn autotuning and lazy inits
|
||||
# must happen outside stream capture.
|
||||
with torch.no_grad():
|
||||
_ = self.transformer(static_x, static_c, static_pos, static_mask)
|
||||
torch.accelerator.synchronize(x_in.device)
|
||||
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.no_grad(), torch.cuda.graph(graph, pool=current_platform.get_global_graph_pool()):
|
||||
static_out = self.transformer(static_x, static_c, static_pos, static_mask)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Disabling S2Mel DiT CUDA graphs after capture failure for shape=%s",
|
||||
tuple(x_in.shape),
|
||||
exc_info=True,
|
||||
)
|
||||
self.enabled = False
|
||||
return None
|
||||
logger.debug("Captured S2Mel DiT CUDA graph for shape=%s", tuple(x_in.shape))
|
||||
return _GraphEntry(
|
||||
graph=graph,
|
||||
static_x=static_x,
|
||||
static_c=static_c,
|
||||
static_pos=static_pos,
|
||||
static_mask=static_mask,
|
||||
static_out=static_out,
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,288 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2019 Shigeki Karita
|
||||
# 2020 Mobvoi Inc (Binbin Zhang)
|
||||
# 2022 Xingchen Song (sxc19@mails.tsinghua.edu.cn)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Multi-Head Attention layer definition."""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class MultiHeadedAttention(nn.Module):
|
||||
"""Multi-Head Attention layer.
|
||||
|
||||
Args:
|
||||
n_head (int): The number of heads.
|
||||
n_feat (int): The number of features.
|
||||
dropout_rate (float): Dropout rate.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, n_head: int, n_feat: int, dropout_rate: float):
|
||||
"""Construct an MultiHeadedAttention object."""
|
||||
super().__init__()
|
||||
assert n_feat % n_head == 0
|
||||
# We assume d_v always equals d_k
|
||||
self.d_k = n_feat // n_head
|
||||
self.h = n_head
|
||||
self.linear_q = nn.Linear(n_feat, n_feat)
|
||||
self.linear_k = nn.Linear(n_feat, n_feat)
|
||||
self.linear_v = nn.Linear(n_feat, n_feat)
|
||||
self.linear_out = nn.Linear(n_feat, n_feat)
|
||||
self.dropout = nn.Dropout(p=dropout_rate)
|
||||
|
||||
def forward_qkv(
|
||||
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Transform query, key and value.
|
||||
|
||||
Args:
|
||||
query (torch.Tensor): Query tensor (#batch, time1, size).
|
||||
key (torch.Tensor): Key tensor (#batch, time2, size).
|
||||
value (torch.Tensor): Value tensor (#batch, time2, size).
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Transformed query tensor, size
|
||||
(#batch, n_head, time1, d_k).
|
||||
torch.Tensor: Transformed key tensor, size
|
||||
(#batch, n_head, time2, d_k).
|
||||
torch.Tensor: Transformed value tensor, size
|
||||
(#batch, n_head, time2, d_k).
|
||||
|
||||
"""
|
||||
n_batch = query.size(0)
|
||||
q = self.linear_q(query).view(n_batch, -1, self.h, self.d_k)
|
||||
k = self.linear_k(key).view(n_batch, -1, self.h, self.d_k)
|
||||
v = self.linear_v(value).view(n_batch, -1, self.h, self.d_k)
|
||||
q = q.transpose(1, 2) # (batch, head, time1, d_k)
|
||||
k = k.transpose(1, 2) # (batch, head, time2, d_k)
|
||||
v = v.transpose(1, 2) # (batch, head, time2, d_k)
|
||||
|
||||
return q, k, v
|
||||
|
||||
def forward_attention(
|
||||
self, value: torch.Tensor, scores: torch.Tensor, mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool)
|
||||
) -> torch.Tensor:
|
||||
"""Compute attention context vector.
|
||||
|
||||
Args:
|
||||
value (torch.Tensor): Transformed value, size
|
||||
(#batch, n_head, time2, d_k).
|
||||
scores (torch.Tensor): Attention score, size
|
||||
(#batch, n_head, time1, time2).
|
||||
mask (torch.Tensor): Mask, size (#batch, 1, time2) or
|
||||
(#batch, time1, time2), (0, 0, 0) means fake mask.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Transformed value (#batch, time1, d_model)
|
||||
weighted by the attention score (#batch, time1, time2).
|
||||
|
||||
"""
|
||||
n_batch = value.size(0)
|
||||
# NOTE(xcsong): When will `if mask.size(2) > 0` be True?
|
||||
# 1. onnx(16/4) [WHY? Because we feed real cache & real mask for the
|
||||
# 1st chunk to ease the onnx export.]
|
||||
# 2. pytorch training
|
||||
if mask.size(2) > 0: # time2 > 0
|
||||
mask = mask.unsqueeze(1).eq(0) # (batch, 1, *, time2)
|
||||
# For last chunk, time2 might be larger than scores.size(-1)
|
||||
mask = mask[:, :, :, : scores.size(-1)] # (batch, 1, *, time2)
|
||||
scores = scores.masked_fill(mask, -float("inf"))
|
||||
attn = torch.softmax(scores, dim=-1).masked_fill(mask, 0.0) # (batch, head, time1, time2)
|
||||
# NOTE(xcsong): When will `if mask.size(2) > 0` be False?
|
||||
# 1. onnx(16/-1, -1/-1, 16/0)
|
||||
# 2. jit (16/-1, -1/-1, 16/0, 16/4)
|
||||
else:
|
||||
attn = torch.softmax(scores, dim=-1) # (batch, head, time1, time2)
|
||||
|
||||
p_attn = self.dropout(attn)
|
||||
x = torch.matmul(p_attn, value) # (batch, head, time1, d_k)
|
||||
x = x.transpose(1, 2).contiguous().view(n_batch, -1, self.h * self.d_k) # (batch, time1, d_model)
|
||||
|
||||
return self.linear_out(x) # (batch, time1, d_model)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
|
||||
pos_emb: torch.Tensor = torch.empty(0),
|
||||
cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute scaled dot product attention.
|
||||
|
||||
Args:
|
||||
query (torch.Tensor): Query tensor (#batch, time1, size).
|
||||
key (torch.Tensor): Key tensor (#batch, time2, size).
|
||||
value (torch.Tensor): Value tensor (#batch, time2, size).
|
||||
mask (torch.Tensor): Mask tensor (#batch, 1, time2) or
|
||||
(#batch, time1, time2).
|
||||
1.When applying cross attention between decoder and encoder,
|
||||
the batch padding mask for input is in (#batch, 1, T) shape.
|
||||
2.When applying self attention of encoder,
|
||||
the mask is in (#batch, T, T) shape.
|
||||
3.When applying self attention of decoder,
|
||||
the mask is in (#batch, L, L) shape.
|
||||
4.If the different position in decoder see different block
|
||||
of the encoder, such as Mocha, the passed in mask could be
|
||||
in (#batch, L, T) shape. But there is no such case in current
|
||||
Wenet.
|
||||
cache (torch.Tensor): Cache tensor (1, head, cache_t, d_k * 2),
|
||||
where `cache_t == chunk_size * num_decoding_left_chunks`
|
||||
and `head * d_k == size`
|
||||
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time1, d_model).
|
||||
torch.Tensor: Cache tensor (1, head, cache_t + time1, d_k * 2)
|
||||
where `cache_t == chunk_size * num_decoding_left_chunks`
|
||||
and `head * d_k == size`
|
||||
|
||||
"""
|
||||
q, k, v = self.forward_qkv(query, key, value)
|
||||
|
||||
if cache.size(0) > 0:
|
||||
key_cache, value_cache = torch.split(cache, cache.size(-1) // 2, dim=-1)
|
||||
k = torch.cat([key_cache, k], dim=2)
|
||||
v = torch.cat([value_cache, v], dim=2)
|
||||
new_cache = torch.cat((k, v), dim=-1)
|
||||
|
||||
# Build boolean attention mask for SDPA.
|
||||
# Input mask convention: True = attend, False = ignore.
|
||||
# SDPA boolean mask convention: True = attend, False = masked to -inf.
|
||||
if mask.size(2) > 0:
|
||||
attn_mask = mask.unsqueeze(1) # (batch, 1, *, time2)
|
||||
attn_mask = attn_mask[:, :, :, : k.size(2)]
|
||||
else:
|
||||
attn_mask = None
|
||||
|
||||
# q/k/v: (batch, head, time, d_k)
|
||||
x = torch.nn.functional.scaled_dot_product_attention(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
attn_mask=attn_mask,
|
||||
dropout_p=self.dropout.p if self.training else 0.0,
|
||||
scale=1.0 / math.sqrt(self.d_k),
|
||||
)
|
||||
n_batch = query.size(0)
|
||||
x = x.transpose(1, 2).contiguous().view(n_batch, -1, self.h * self.d_k)
|
||||
|
||||
return self.linear_out(x), new_cache
|
||||
|
||||
|
||||
class RelPositionMultiHeadedAttention(MultiHeadedAttention):
|
||||
"""Multi-Head Attention layer with relative position encoding.
|
||||
Paper: https://arxiv.org/abs/1901.02860
|
||||
Args:
|
||||
n_head (int): The number of heads.
|
||||
n_feat (int): The number of features.
|
||||
dropout_rate (float): Dropout rate.
|
||||
"""
|
||||
|
||||
def __init__(self, n_head, n_feat, dropout_rate):
|
||||
"""Construct an RelPositionMultiHeadedAttention object."""
|
||||
super().__init__(n_head, n_feat, dropout_rate)
|
||||
# linear transformation for positional encoding
|
||||
self.linear_pos = nn.Linear(n_feat, n_feat, bias=False)
|
||||
# these two learnable bias are used in matrix c and matrix d
|
||||
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
|
||||
self.pos_bias_u = nn.Parameter(torch.Tensor(self.h, self.d_k))
|
||||
self.pos_bias_v = nn.Parameter(torch.Tensor(self.h, self.d_k))
|
||||
torch.nn.init.xavier_uniform_(self.pos_bias_u)
|
||||
torch.nn.init.xavier_uniform_(self.pos_bias_v)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
mask: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
|
||||
pos_emb: torch.Tensor = torch.empty(0),
|
||||
cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute 'Scaled Dot Product Attention' with rel. positional encoding.
|
||||
Args:
|
||||
query (torch.Tensor): Query tensor (#batch, time1, size).
|
||||
key (torch.Tensor): Key tensor (#batch, time2, size).
|
||||
value (torch.Tensor): Value tensor (#batch, time2, size).
|
||||
mask (torch.Tensor): Mask tensor (#batch, 1, time2) or
|
||||
(#batch, time1, time2), (0, 0, 0) means fake mask.
|
||||
pos_emb (torch.Tensor): Positional embedding tensor
|
||||
(#batch, time2, size).
|
||||
cache (torch.Tensor): Cache tensor (1, head, cache_t, d_k * 2),
|
||||
where `cache_t == chunk_size * num_decoding_left_chunks`
|
||||
and `head * d_k == size`
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time1, d_model).
|
||||
torch.Tensor: Cache tensor (1, head, cache_t + time1, d_k * 2)
|
||||
where `cache_t == chunk_size * num_decoding_left_chunks`
|
||||
and `head * d_k == size`
|
||||
"""
|
||||
q, k, v = self.forward_qkv(query, key, value)
|
||||
q = q.transpose(1, 2) # (batch, time1, head, d_k)
|
||||
|
||||
# NOTE(xcsong):
|
||||
# when export onnx model, for 1st chunk, we feed
|
||||
# cache(1, head, 0, d_k * 2) (16/-1, -1/-1, 16/0 mode)
|
||||
# or cache(1, head, real_cache_t, d_k * 2) (16/4 mode).
|
||||
# In all modes, `if cache.size(0) > 0` will always be `True`
|
||||
# and we will always do splitting and
|
||||
# concatnation(this will simplify onnx export). Note that
|
||||
# it's OK to concat & split zero-shaped tensors(see code below).
|
||||
# when export jit model, for 1st chunk, we always feed
|
||||
# cache(0, 0, 0, 0) since jit supports dynamic if-branch.
|
||||
# >>> a = torch.ones((1, 2, 0, 4))
|
||||
# >>> b = torch.ones((1, 2, 3, 4))
|
||||
# >>> c = torch.cat((a, b), dim=2)
|
||||
# >>> torch.equal(b, c) # True
|
||||
# >>> d = torch.split(a, 2, dim=-1)
|
||||
# >>> torch.equal(d[0], d[1]) # True
|
||||
if cache.size(0) > 0:
|
||||
key_cache, value_cache = torch.split(cache, cache.size(-1) // 2, dim=-1)
|
||||
k = torch.cat([key_cache, k], dim=2)
|
||||
v = torch.cat([value_cache, v], dim=2)
|
||||
# NOTE(xcsong): We do cache slicing in encoder.forward_chunk, since it's
|
||||
# non-trivial to calculate `next_cache_start` here.
|
||||
new_cache = torch.cat((k, v), dim=-1)
|
||||
|
||||
n_batch_pos = pos_emb.size(0)
|
||||
p = self.linear_pos(pos_emb).view(n_batch_pos, -1, self.h, self.d_k)
|
||||
p = p.transpose(1, 2) # (batch, head, time1, d_k)
|
||||
|
||||
# (batch, head, time1, d_k)
|
||||
q_with_bias_u = (q + self.pos_bias_u).transpose(1, 2)
|
||||
# (batch, head, time1, d_k)
|
||||
q_with_bias_v = (q + self.pos_bias_v).transpose(1, 2)
|
||||
|
||||
# compute attention score
|
||||
# first compute matrix a and matrix c
|
||||
# as described in https://arxiv.org/abs/1901.02860 Section 3.3
|
||||
# (batch, head, time1, time2)
|
||||
matrix_ac = torch.matmul(q_with_bias_u, k.transpose(-2, -1))
|
||||
|
||||
# compute matrix b and matrix d
|
||||
# (batch, head, time1, time2)
|
||||
matrix_bd = torch.matmul(q_with_bias_v, p.transpose(-2, -1))
|
||||
|
||||
scores = (matrix_ac + matrix_bd) / math.sqrt(self.d_k) # (batch, head, time1, time2)
|
||||
|
||||
return self.forward_attention(v, scores, mask), new_cache
|
||||
@@ -0,0 +1,132 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2020 Mobvoi Inc. (authors: Binbin Zhang, Di Wu)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# Modified from ESPnet(https://github.com/espnet/espnet)
|
||||
|
||||
"""Positional Encoding Module."""
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
|
||||
class PositionalEncoding(torch.nn.Module):
|
||||
"""Positional encoding.
|
||||
|
||||
:param int d_model: embedding dim
|
||||
:param float dropout_rate: dropout rate
|
||||
:param int max_len: maximum input length
|
||||
|
||||
PE(pos, 2i) = sin(pos/(10000^(2i/dmodel)))
|
||||
PE(pos, 2i+1) = cos(pos/(10000^(2i/dmodel)))
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 5000, reverse: bool = False):
|
||||
"""Construct an PositionalEncoding object."""
|
||||
super().__init__()
|
||||
self.d_model = d_model
|
||||
self.xscale = math.sqrt(self.d_model)
|
||||
self.dropout = torch.nn.Dropout(p=dropout_rate)
|
||||
self.max_len = max_len
|
||||
|
||||
pe = torch.zeros(self.max_len, self.d_model)
|
||||
position = torch.arange(0, self.max_len).unsqueeze(1)
|
||||
div_term = torch.exp(torch.arange(0, self.d_model, 2) * -(math.log(10000.0) / self.d_model))
|
||||
pe[:, 0::2] = torch.sin(position * div_term)
|
||||
pe[:, 1::2] = torch.cos(position * div_term)
|
||||
pe = pe.unsqueeze(0)
|
||||
self.register_buffer("pe", pe)
|
||||
|
||||
def forward(self, x: torch.Tensor, offset: int | torch.Tensor = 0) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Add positional encoding.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): Input. Its shape is (batch, time, ...)
|
||||
offset (int, torch.tensor): position offset
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Encoded tensor. Its shape is (batch, time, ...)
|
||||
torch.Tensor: for compatibility to RelPositionalEncoding
|
||||
"""
|
||||
|
||||
self.pe = self.pe.to(x.device)
|
||||
pos_emb = self.position_encoding(offset, x.size(1), False)
|
||||
x = x * self.xscale + pos_emb
|
||||
return self.dropout(x), self.dropout(pos_emb)
|
||||
|
||||
def position_encoding(self, offset: int | torch.Tensor, size: int, apply_dropout: bool = True) -> torch.Tensor:
|
||||
"""For getting encoding in a streaming fashion
|
||||
|
||||
Attention!!!!!
|
||||
we apply dropout only once at the whole utterance level in a none
|
||||
streaming way, but will call this function several times with
|
||||
increasing input size in a streaming scenario, so the dropout will
|
||||
be applied several times.
|
||||
|
||||
Args:
|
||||
offset (int or torch.tensor): start offset
|
||||
size (int): required size of position encoding
|
||||
|
||||
Returns:
|
||||
torch.Tensor: Corresponding encoding
|
||||
"""
|
||||
# How to subscript a Union type:
|
||||
# https://github.com/pytorch/pytorch/issues/69434
|
||||
if isinstance(offset, int):
|
||||
assert offset + size < self.max_len
|
||||
pos_emb = self.pe[:, offset : offset + size]
|
||||
elif isinstance(offset, torch.Tensor) and offset.dim() == 0: # scalar
|
||||
assert offset + size < self.max_len
|
||||
pos_emb = self.pe[:, offset : offset + size]
|
||||
else: # for batched streaming decoding on GPU
|
||||
assert torch.max(offset) + size < self.max_len
|
||||
index = offset.unsqueeze(1) + torch.arange(0, size).to(offset.device) # B X T
|
||||
flag = index > 0
|
||||
# remove negative offset
|
||||
index = index * flag
|
||||
pos_emb = F.embedding(index, self.pe[0]) # B X T X d_model
|
||||
|
||||
if apply_dropout:
|
||||
pos_emb = self.dropout(pos_emb)
|
||||
return pos_emb
|
||||
|
||||
|
||||
class RelPositionalEncoding(PositionalEncoding):
|
||||
"""Relative positional encoding module.
|
||||
See : Appendix B in https://arxiv.org/abs/1901.02860
|
||||
Args:
|
||||
d_model (int): Embedding dimension.
|
||||
dropout_rate (float): Dropout rate.
|
||||
max_len (int): Maximum input length.
|
||||
"""
|
||||
|
||||
def __init__(self, d_model: int, dropout_rate: float, max_len: int = 5000):
|
||||
"""Initialize class."""
|
||||
super().__init__(d_model, dropout_rate, max_len, reverse=True)
|
||||
|
||||
def forward(self, x: torch.Tensor, offset: int | torch.Tensor = 0) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute positional encoding.
|
||||
Args:
|
||||
x (torch.Tensor): Input tensor (batch, time, `*`).
|
||||
Returns:
|
||||
torch.Tensor: Encoded tensor (batch, time, `*`).
|
||||
torch.Tensor: Positional embedding tensor (1, time, `*`).
|
||||
"""
|
||||
self.pe = self.pe.to(x.device)
|
||||
x = x * self.xscale
|
||||
pos_emb = self.position_encoding(offset, x.size(1), False)
|
||||
return self.dropout(x), self.dropout(pos_emb)
|
||||
@@ -0,0 +1,64 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2021 Mobvoi Inc (Binbin Zhang, Di Wu)
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# Modified from ESPnet(https://github.com/espnet/espnet)
|
||||
|
||||
|
||||
"""Subsampling layer definition."""
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
class BaseSubsampling(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.right_context = 0
|
||||
self.subsampling_rate = 1
|
||||
|
||||
def position_encoding(self, offset: int | torch.Tensor, size: int) -> torch.Tensor:
|
||||
return self.pos_enc.position_encoding(offset, size)
|
||||
|
||||
|
||||
class Conv2dSubsampling2(BaseSubsampling):
|
||||
"""Convolutional 2D subsampling (to 1/2 length).
|
||||
|
||||
Args:
|
||||
idim (int): Input dimension.
|
||||
odim (int): Output dimension.
|
||||
dropout_rate (float): Dropout rate.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, idim: int, odim: int, dropout_rate: float, pos_enc_class: torch.nn.Module):
|
||||
super().__init__()
|
||||
self.conv = torch.nn.Sequential(
|
||||
torch.nn.Conv2d(1, odim, 3, 2),
|
||||
torch.nn.ReLU(),
|
||||
)
|
||||
self.out = torch.nn.Sequential(torch.nn.Linear(odim * ((idim - 1) // 2), odim))
|
||||
self.pos_enc = pos_enc_class
|
||||
self.subsampling_rate = 2
|
||||
self.right_context = 2
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, x_mask: torch.Tensor, offset: int | torch.Tensor = 0
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
x = x.unsqueeze(1) # (b, c=1, t, f)
|
||||
x = self.conv(x)
|
||||
b, c, t, f = x.size()
|
||||
x = self.out(x.transpose(1, 2).contiguous().view(b, t, c * f))
|
||||
x, pos_emb = self.pos_enc(x, offset)
|
||||
return x, pos_emb, x_mask[:, :, 2::2]
|
||||
@@ -0,0 +1,422 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.gpt.conformer.attention import (
|
||||
MultiHeadedAttention,
|
||||
RelPositionMultiHeadedAttention,
|
||||
)
|
||||
from vllm_omni.model_executor.models.indextts2.gpt.conformer.embedding import (
|
||||
PositionalEncoding,
|
||||
RelPositionalEncoding,
|
||||
)
|
||||
from vllm_omni.model_executor.models.indextts2.gpt.conformer.subsampling import (
|
||||
Conv2dSubsampling2,
|
||||
)
|
||||
from vllm_omni.model_executor.models.indextts2.utils.common import make_pad_mask
|
||||
|
||||
|
||||
class PositionwiseFeedForward(torch.nn.Module):
|
||||
"""Positionwise feed forward layer.
|
||||
|
||||
FeedForward are applied on each position of the sequence.
|
||||
The output dim is same with the input dim.
|
||||
|
||||
Args:
|
||||
idim (int): Input dimension.
|
||||
hidden_units (int): The number of hidden units.
|
||||
dropout_rate (float): Dropout rate.
|
||||
activation (torch.nn.Module): Activation function
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, idim: int, hidden_units: int, dropout_rate: float, activation: torch.nn.Module = torch.nn.ReLU()
|
||||
):
|
||||
"""Construct a PositionwiseFeedForward object."""
|
||||
super().__init__()
|
||||
self.w_1 = torch.nn.Linear(idim, hidden_units)
|
||||
self.activation = activation
|
||||
self.dropout = torch.nn.Dropout(dropout_rate)
|
||||
self.w_2 = torch.nn.Linear(hidden_units, idim)
|
||||
|
||||
def forward(self, xs: torch.Tensor) -> torch.Tensor:
|
||||
"""Forward function.
|
||||
|
||||
Args:
|
||||
xs: input tensor (B, L, D)
|
||||
Returns:
|
||||
output tensor, (B, L, D)
|
||||
"""
|
||||
return self.w_2(self.dropout(self.activation(self.w_1(xs))))
|
||||
|
||||
|
||||
class ConvolutionModule(nn.Module):
|
||||
"""ConvolutionModule in Conformer model."""
|
||||
|
||||
def __init__(self, channels: int, kernel_size: int = 15, activation: nn.Module = nn.ReLU(), bias: bool = True):
|
||||
"""Construct an ConvolutionModule object.
|
||||
Args:
|
||||
channels (int): The number of channels of conv layers.
|
||||
kernel_size (int): Kernel size of conv layers.
|
||||
causal (int): Whether use causal convolution or not
|
||||
"""
|
||||
super().__init__()
|
||||
|
||||
self.pointwise_conv1 = nn.Conv1d(
|
||||
channels,
|
||||
2 * channels,
|
||||
kernel_size=1,
|
||||
stride=1,
|
||||
padding=0,
|
||||
bias=bias,
|
||||
)
|
||||
# self.lorder is used to distinguish if it's a causal convolution,
|
||||
# if self.lorder > 0: it's a causal convolution, the input will be
|
||||
# padded with self.lorder frames on the left in forward.
|
||||
# else: it's a symmetrical convolution
|
||||
# kernel_size should be an odd number for none causal convolution
|
||||
assert (kernel_size - 1) % 2 == 0
|
||||
padding = (kernel_size - 1) // 2
|
||||
self.lorder = 0
|
||||
|
||||
self.depthwise_conv = nn.Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=padding,
|
||||
groups=channels,
|
||||
bias=bias,
|
||||
)
|
||||
|
||||
self.use_layer_norm = True
|
||||
self.norm = nn.LayerNorm(channels)
|
||||
|
||||
self.pointwise_conv2 = nn.Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size=1,
|
||||
stride=1,
|
||||
padding=0,
|
||||
bias=bias,
|
||||
)
|
||||
self.activation = activation
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
|
||||
cache: torch.Tensor = torch.zeros((0, 0, 0)),
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Compute convolution module.
|
||||
Args:
|
||||
x (torch.Tensor): Input tensor (#batch, time, channels).
|
||||
mask_pad (torch.Tensor): used for batch padding (#batch, 1, time),
|
||||
(0, 0, 0) means fake mask.
|
||||
cache (torch.Tensor): left context cache, it is only
|
||||
used in causal convolution (#batch, channels, cache_t),
|
||||
(0, 0, 0) meas fake cache.
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, channels).
|
||||
"""
|
||||
# exchange the temporal dimension and the feature dimension
|
||||
x = x.transpose(1, 2) # (#batch, channels, time)
|
||||
|
||||
# mask batch padding
|
||||
if mask_pad.size(2) > 0: # time > 0
|
||||
x.masked_fill_(~mask_pad, 0.0)
|
||||
|
||||
if self.lorder > 0:
|
||||
if cache.size(2) == 0: # cache_t == 0
|
||||
x = nn.functional.pad(x, (self.lorder, 0), "constant", 0.0)
|
||||
else:
|
||||
assert cache.size(0) == x.size(0) # equal batch
|
||||
assert cache.size(1) == x.size(1) # equal channel
|
||||
x = torch.cat((cache, x), dim=2)
|
||||
assert x.size(2) > self.lorder
|
||||
new_cache = x[:, :, -self.lorder :]
|
||||
else:
|
||||
# It's better we just return None if no cache is required,
|
||||
# However, for JIT export, here we just fake one tensor instead of
|
||||
# None.
|
||||
new_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
|
||||
|
||||
# GLU mechanism
|
||||
x = self.pointwise_conv1(x) # (batch, 2*channel, dim)
|
||||
x = nn.functional.glu(x, dim=1) # (batch, channel, dim)
|
||||
|
||||
# 1D Depthwise Conv
|
||||
x = self.depthwise_conv(x)
|
||||
if self.use_layer_norm:
|
||||
x = x.transpose(1, 2)
|
||||
x = self.activation(self.norm(x))
|
||||
if self.use_layer_norm:
|
||||
x = x.transpose(1, 2)
|
||||
x = self.pointwise_conv2(x)
|
||||
# mask batch padding
|
||||
if mask_pad.size(2) > 0: # time > 0
|
||||
x.masked_fill_(~mask_pad, 0.0)
|
||||
|
||||
return x.transpose(1, 2), new_cache
|
||||
|
||||
|
||||
class ConformerEncoderLayer(nn.Module):
|
||||
"""Encoder layer module.
|
||||
Args:
|
||||
size (int): Input dimension.
|
||||
self_attn (torch.nn.Module): Self-attention module instance.
|
||||
`MultiHeadedAttention` or `RelPositionMultiHeadedAttention`
|
||||
instance can be used as the argument.
|
||||
feed_forward (torch.nn.Module): Feed-forward module instance.
|
||||
`PositionwiseFeedForward` instance can be used as the argument.
|
||||
feed_forward_macaron (torch.nn.Module): Additional feed-forward module
|
||||
instance.
|
||||
`PositionwiseFeedForward` instance can be used as the argument.
|
||||
conv_module (torch.nn.Module): Convolution module instance.
|
||||
`ConvlutionModule` instance can be used as the argument.
|
||||
dropout_rate (float): Dropout rate.
|
||||
normalize_before (bool):
|
||||
True: use layer_norm before each sub-block.
|
||||
False: use layer_norm after each sub-block.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
size: int,
|
||||
self_attn: torch.nn.Module,
|
||||
feed_forward: nn.Module | None = None,
|
||||
feed_forward_macaron: nn.Module | None = None,
|
||||
conv_module: nn.Module | None = None,
|
||||
dropout_rate: float = 0.1,
|
||||
normalize_before: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
self.self_attn = self_attn
|
||||
self.feed_forward = feed_forward
|
||||
self.feed_forward_macaron = feed_forward_macaron
|
||||
self.conv_module = conv_module
|
||||
self.norm_ff = nn.LayerNorm(size, eps=1e-5)
|
||||
self.norm_mha = nn.LayerNorm(size, eps=1e-5)
|
||||
if feed_forward_macaron is not None:
|
||||
self.norm_ff_macaron = nn.LayerNorm(size, eps=1e-5)
|
||||
self.ff_scale = 0.5
|
||||
else:
|
||||
self.ff_scale = 1.0
|
||||
if self.conv_module is not None:
|
||||
self.norm_conv = nn.LayerNorm(size, eps=1e-5)
|
||||
self.norm_final = nn.LayerNorm(size, eps=1e-5)
|
||||
self.dropout = nn.Dropout(dropout_rate)
|
||||
self.size = size
|
||||
self.normalize_before = normalize_before
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
pos_emb: torch.Tensor,
|
||||
mask_pad: torch.Tensor = torch.ones((0, 0, 0), dtype=torch.bool),
|
||||
att_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
|
||||
cnn_cache: torch.Tensor = torch.zeros((0, 0, 0, 0)),
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Compute encoded features.
|
||||
|
||||
Args:
|
||||
x (torch.Tensor): (#batch, time, size)
|
||||
mask (torch.Tensor): Mask tensor for the input (#batch, time,time),
|
||||
(0, 0, 0) means fake mask.
|
||||
pos_emb (torch.Tensor): positional encoding, must not be None
|
||||
for ConformerEncoderLayer.
|
||||
mask_pad (torch.Tensor): batch padding mask used for conv module.
|
||||
(#batch, 1,time), (0, 0, 0) means fake mask.
|
||||
att_cache (torch.Tensor): Cache tensor of the KEY & VALUE
|
||||
(#batch=1, head, cache_t1, d_k * 2), head * d_k == size.
|
||||
cnn_cache (torch.Tensor): Convolution cache in conformer layer
|
||||
(#batch=1, size, cache_t2)
|
||||
Returns:
|
||||
torch.Tensor: Output tensor (#batch, time, size).
|
||||
torch.Tensor: Mask tensor (#batch, time, time).
|
||||
torch.Tensor: att_cache tensor,
|
||||
(#batch=1, head, cache_t1 + time, d_k * 2).
|
||||
torch.Tensor: cnn_cache tensor (#batch, size, cache_t2).
|
||||
"""
|
||||
|
||||
if self.feed_forward_macaron is not None:
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm_ff_macaron(x)
|
||||
x = residual + self.ff_scale * self.dropout(self.feed_forward_macaron(x))
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm_mha(x)
|
||||
x_att, new_att_cache = self.self_attn(x, x, x, mask, pos_emb, att_cache)
|
||||
x = residual + self.dropout(x_att)
|
||||
|
||||
new_cnn_cache = torch.zeros((0, 0, 0), dtype=x.dtype, device=x.device)
|
||||
if self.conv_module is not None:
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm_conv(x)
|
||||
x, new_cnn_cache = self.conv_module(x, mask_pad, cnn_cache)
|
||||
x = residual + self.dropout(x)
|
||||
|
||||
residual = x
|
||||
if self.normalize_before:
|
||||
x = self.norm_ff(x)
|
||||
x = residual + self.ff_scale * self.dropout(self.feed_forward(x))
|
||||
|
||||
if self.conv_module is not None:
|
||||
x = self.norm_final(x)
|
||||
|
||||
return x, mask, new_att_cache, new_cnn_cache
|
||||
|
||||
|
||||
class BaseEncoder(torch.nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "abs_pos",
|
||||
normalize_before: bool = True,
|
||||
concat_after: bool = False,
|
||||
):
|
||||
super().__init__()
|
||||
self._output_size = output_size
|
||||
|
||||
pos_enc_map = {"abs_pos": PositionalEncoding, "rel_pos": RelPositionalEncoding}
|
||||
if pos_enc_layer_type not in pos_enc_map:
|
||||
raise ValueError("unknown pos_enc_layer: " + pos_enc_layer_type)
|
||||
pos_enc_class = pos_enc_map[pos_enc_layer_type]
|
||||
|
||||
subsampling_map = {"conv2d2": Conv2dSubsampling2}
|
||||
if input_layer not in subsampling_map:
|
||||
raise ValueError("unknown input_layer: " + input_layer)
|
||||
subsampling_class = subsampling_map[input_layer]
|
||||
|
||||
self.embed = subsampling_class(
|
||||
input_size,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
pos_enc_class(output_size, dropout_rate),
|
||||
)
|
||||
|
||||
self.normalize_before = normalize_before
|
||||
self.after_norm = torch.nn.LayerNorm(output_size, eps=1e-5)
|
||||
|
||||
def output_size(self) -> int:
|
||||
return self._output_size
|
||||
|
||||
def forward(
|
||||
self,
|
||||
xs: torch.Tensor,
|
||||
xs_lens: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Embed positions in tensor.
|
||||
|
||||
Args:
|
||||
xs: padded input tensor (B, T, D)
|
||||
xs_lens: input length (B)
|
||||
|
||||
Returns:
|
||||
encoder output tensor xs, and subsampled masks
|
||||
xs: padded output tensor (B, T' ~= T/subsample_rate, D)
|
||||
masks: torch.Tensor batch padding mask after subsample
|
||||
(B, 1, T' ~= T/subsample_rate)
|
||||
"""
|
||||
T = xs.size(1)
|
||||
masks = ~make_pad_mask(xs_lens, T).unsqueeze(1) # (B, 1, T)
|
||||
xs, pos_emb, masks = self.embed(xs, masks)
|
||||
chunk_masks = masks
|
||||
mask_pad = masks # (B, 1, T/subsample_rate)
|
||||
for layer in self.encoders:
|
||||
xs, chunk_masks, _, _ = layer(xs, chunk_masks, pos_emb, mask_pad)
|
||||
if self.normalize_before:
|
||||
xs = self.after_norm(xs)
|
||||
# Here we assume the mask is not changed in encoder layers, so just
|
||||
# return the masks before encoder layers, and the masks will be used
|
||||
# for cross attention with decoder later
|
||||
return xs, masks
|
||||
|
||||
|
||||
class ConformerEncoder(BaseEncoder):
|
||||
"""Conformer encoder module."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_size: int,
|
||||
output_size: int = 256,
|
||||
attention_heads: int = 4,
|
||||
linear_units: int = 2048,
|
||||
num_blocks: int = 6,
|
||||
dropout_rate: float = 0.0,
|
||||
input_layer: str = "conv2d",
|
||||
pos_enc_layer_type: str = "rel_pos",
|
||||
normalize_before: bool = True,
|
||||
concat_after: bool = False,
|
||||
macaron_style: bool = False,
|
||||
use_cnn_module: bool = True,
|
||||
cnn_module_kernel: int = 15,
|
||||
):
|
||||
super().__init__(
|
||||
input_size,
|
||||
output_size,
|
||||
attention_heads,
|
||||
linear_units,
|
||||
num_blocks,
|
||||
dropout_rate,
|
||||
input_layer,
|
||||
pos_enc_layer_type,
|
||||
normalize_before,
|
||||
concat_after,
|
||||
)
|
||||
|
||||
activation = torch.nn.SiLU()
|
||||
|
||||
# self-attention module definition
|
||||
if pos_enc_layer_type != "rel_pos":
|
||||
encoder_selfattn_layer = MultiHeadedAttention
|
||||
else:
|
||||
encoder_selfattn_layer = RelPositionMultiHeadedAttention
|
||||
encoder_selfattn_layer_args = (
|
||||
attention_heads,
|
||||
output_size,
|
||||
dropout_rate,
|
||||
)
|
||||
|
||||
# feed-forward module definition
|
||||
positionwise_layer = PositionwiseFeedForward
|
||||
positionwise_layer_args = (
|
||||
output_size,
|
||||
linear_units,
|
||||
dropout_rate,
|
||||
activation,
|
||||
)
|
||||
# convolution module definition
|
||||
convolution_layer = ConvolutionModule
|
||||
convolution_layer_args = (
|
||||
output_size,
|
||||
cnn_module_kernel,
|
||||
activation,
|
||||
)
|
||||
|
||||
self.encoders = torch.nn.ModuleList(
|
||||
[
|
||||
ConformerEncoderLayer(
|
||||
output_size,
|
||||
encoder_selfattn_layer(*encoder_selfattn_layer_args),
|
||||
positionwise_layer(*positionwise_layer_args),
|
||||
positionwise_layer(*positionwise_layer_args) if macaron_style else None,
|
||||
convolution_layer(*convolution_layer_args) if use_cnn_module else None,
|
||||
dropout_rate,
|
||||
normalize_before,
|
||||
)
|
||||
for _ in range(num_blocks)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,20 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Learned position embeddings for IndexTTS2 GPT model."""
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
|
||||
class LearnedPositionEmbeddings(nn.Module):
|
||||
def __init__(self, seq_len: int, model_dim: int, init: float = 0.02):
|
||||
super().__init__()
|
||||
self.emb = nn.Embedding(seq_len, model_dim)
|
||||
self.emb.weight.data.normal_(mean=0.0, std=init)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
sl = x.shape[1]
|
||||
return self.emb(torch.arange(0, sl, device=x.device))
|
||||
|
||||
def get_fixed_embedding(self, ind: int, dev: torch.device) -> torch.Tensor:
|
||||
return self.emb(torch.tensor([ind], device=dev)).unsqueeze(0)
|
||||
@@ -0,0 +1,149 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Adapted from lucidrains/naturalspeech2-pytorch (PerceiverResampler)
|
||||
# https://github.com/lucidrains/naturalspeech2-pytorch/blob/659bec7f/
|
||||
# naturalspeech2_pytorch/naturalspeech2_pytorch.py#L532
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from einops import rearrange, repeat
|
||||
from torch import nn
|
||||
|
||||
|
||||
def exists(val):
|
||||
return val is not None
|
||||
|
||||
|
||||
def default(val, d):
|
||||
if exists(val):
|
||||
return val
|
||||
return d() if callable(d) else d
|
||||
|
||||
|
||||
class Attend(nn.Module):
|
||||
def __init__(self, dropout=0.0):
|
||||
super().__init__()
|
||||
self.dropout = dropout
|
||||
|
||||
def forward(self, q, k, v, mask=None):
|
||||
attn_mask = None
|
||||
if exists(mask):
|
||||
attn_mask = rearrange(mask, "b j -> b 1 1 j")
|
||||
|
||||
return F.scaled_dot_product_attention(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
attn_mask=attn_mask,
|
||||
dropout_p=self.dropout if self.training else 0.0,
|
||||
)
|
||||
|
||||
|
||||
class RMSNorm(nn.Module):
|
||||
def __init__(self, dim, scale=True):
|
||||
super().__init__()
|
||||
self.scale = dim**0.5
|
||||
self.gamma = nn.Parameter(torch.ones(dim)) if scale else None
|
||||
|
||||
def forward(self, x):
|
||||
gamma = default(self.gamma, 1)
|
||||
return F.normalize(x, dim=-1) * self.scale * gamma
|
||||
|
||||
|
||||
class GEGLU(nn.Module):
|
||||
def forward(self, x):
|
||||
x, gate = x.chunk(2, dim=-1)
|
||||
return F.gelu(gate) * x
|
||||
|
||||
|
||||
def FeedForward(dim, mult=4):
|
||||
dim_inner = int(dim * mult * 2 / 3)
|
||||
return nn.Sequential(nn.Linear(dim, dim_inner * 2), GEGLU(), nn.Linear(dim_inner, dim))
|
||||
|
||||
|
||||
class PerceiverResampler(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
depth=2,
|
||||
dim_context=None,
|
||||
num_latents=32,
|
||||
dim_head=64,
|
||||
heads=8,
|
||||
ff_mult=4,
|
||||
):
|
||||
super().__init__()
|
||||
dim_context = default(dim_context, dim)
|
||||
|
||||
self.proj_context = nn.Linear(dim_context, dim) if dim_context != dim else nn.Identity()
|
||||
|
||||
self.latents = nn.Parameter(torch.randn(num_latents, dim))
|
||||
nn.init.normal_(self.latents, std=0.02)
|
||||
|
||||
self.layers = nn.ModuleList([])
|
||||
for _ in range(depth):
|
||||
self.layers.append(
|
||||
nn.ModuleList(
|
||||
[
|
||||
Attention(dim=dim, dim_head=dim_head, heads=heads, cross_attn_include_queries=True),
|
||||
FeedForward(dim=dim, mult=ff_mult),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
self.norm = RMSNorm(dim)
|
||||
|
||||
def forward(self, x, mask=None):
|
||||
batch = x.shape[0]
|
||||
|
||||
x = self.proj_context(x)
|
||||
|
||||
latents = repeat(self.latents, "n d -> b n d", b=batch)
|
||||
|
||||
for attn, ff in self.layers:
|
||||
latents = attn(latents, x, mask=mask) + latents
|
||||
latents = ff(latents) + latents
|
||||
|
||||
return self.norm(latents)
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim,
|
||||
*,
|
||||
dim_context=None,
|
||||
dim_head=64,
|
||||
heads=8,
|
||||
dropout=0.0,
|
||||
cross_attn_include_queries=False,
|
||||
):
|
||||
super().__init__()
|
||||
self.scale = dim_head**-0.5
|
||||
self.heads = heads
|
||||
self.cross_attn_include_queries = cross_attn_include_queries
|
||||
|
||||
dim_inner = dim_head * heads
|
||||
dim_context = default(dim_context, dim)
|
||||
|
||||
self.attend = Attend(dropout=dropout)
|
||||
self.to_q = nn.Linear(dim, dim_inner, bias=False)
|
||||
self.to_kv = nn.Linear(dim_context, dim_inner * 2, bias=False)
|
||||
self.to_out = nn.Linear(dim_inner, dim, bias=False)
|
||||
|
||||
def forward(self, x, context=None, mask=None):
|
||||
h, has_context = self.heads, exists(context)
|
||||
|
||||
context = default(context, x)
|
||||
|
||||
if has_context and self.cross_attn_include_queries:
|
||||
context = torch.cat((x, context), dim=-2)
|
||||
|
||||
q, k, v = (self.to_q(x), *self.to_kv(context).chunk(2, dim=-1))
|
||||
q, k, v = map(lambda t: rearrange(t, "b n (h d) -> b h n d", h=h), (q, k, v))
|
||||
|
||||
out = self.attend(q, k, v, mask=mask)
|
||||
|
||||
out = rearrange(out, "b h n d -> b n (h d)")
|
||||
return self.to_out(out)
|
||||
@@ -0,0 +1,894 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""IndexTTS2 Stage 1: S2Mel decoder + BigVGAN vocoder.
|
||||
|
||||
Receives mel_codes + latent from Stage 0 (GPT AR talker), runs flow matching
|
||||
to synthesize mel spectrogram, then BigVGAN to produce waveform audio.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
|
||||
from vllm.sequence import IntermediateTensors
|
||||
|
||||
from vllm_omni.diffusion.model_loader.hub_prefetch import _repo_prefetch_lock
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
from .configuration_indextts2 import IndexTTS2Config
|
||||
from .preprocess_utils import load_semantic_codec, resolve_model_file
|
||||
from .s2mel.modules.commons import AttrDict, MyModel
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lazy loaders for external models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_bigvgan_models: dict[tuple[str, str], nn.Module] = {}
|
||||
|
||||
|
||||
def _patch_bigvgan_compat(cls):
|
||||
"""Monkey-patch BigVGAN._from_pretrained for huggingface_hub>=1.0 compat."""
|
||||
if getattr(cls._from_pretrained, "_hf1_patched", False):
|
||||
return
|
||||
orig = cls._from_pretrained.__func__
|
||||
|
||||
@classmethod
|
||||
def _compat(klass, *, proxies=None, resume_download=False, **kw):
|
||||
return orig(klass, proxies=proxies, resume_download=resume_download, **kw)
|
||||
|
||||
_compat._hf1_patched = True
|
||||
cls._from_pretrained = _compat
|
||||
|
||||
|
||||
def _strip_weight_norm(root: nn.Module) -> list[str]:
|
||||
"""Bake legacy weight_norm (weight_g/weight_v) into plain weights.
|
||||
|
||||
The DiT final-layer WaveNet (and any other S2Mel submodule) keeps
|
||||
torch.nn.utils.weight_norm forward pre-hooks after loading, so every
|
||||
CFM estimator step recomputes the normalization (~217 calls/request
|
||||
in profiling). Inference never updates these weights, so we fold the
|
||||
parametrization once at load time. Hooks live on the innermost conv
|
||||
(e.g. SConv1d.conv.conv), hence the generic module walk instead of
|
||||
the per-class remove_weight_norm() helpers.
|
||||
|
||||
Returns the qualified names (relative to ``root``) of the plain
|
||||
weight parameters created by the fold, so callers can mark them as
|
||||
checkpoint-initialized for vLLM's loader validation.
|
||||
"""
|
||||
from torch.nn.utils import remove_weight_norm
|
||||
|
||||
folded: list[str] = []
|
||||
for mod_name, module in root.named_modules():
|
||||
for hook in list(module._forward_pre_hooks.values()):
|
||||
if hook.__class__.__name__ == "WeightNorm":
|
||||
remove_weight_norm(module, name=hook.name)
|
||||
folded.append(f"{mod_name}.{hook.name}" if mod_name else hook.name)
|
||||
return folded
|
||||
|
||||
|
||||
def _load_bigvgan(vocoder_name: str, device: torch.device, dtype: torch.dtype = torch.float32):
|
||||
cache_key = (vocoder_name, str(device), str(dtype))
|
||||
if cache_key in _bigvgan_models:
|
||||
return _bigvgan_models[cache_key]
|
||||
|
||||
lock = _repo_prefetch_lock(vocoder_name) if not os.path.isdir(vocoder_name) else None
|
||||
if lock is not None:
|
||||
lock.__enter__()
|
||||
try:
|
||||
try:
|
||||
from .s2mel.modules import bigvgan as bigvgan_mod
|
||||
|
||||
_patch_bigvgan_compat(bigvgan_mod.BigVGAN)
|
||||
bigvgan_model = bigvgan_mod.BigVGAN.from_pretrained(vocoder_name)
|
||||
except (ImportError, ModuleNotFoundError):
|
||||
import bigvgan
|
||||
|
||||
_patch_bigvgan_compat(bigvgan.BigVGAN)
|
||||
bigvgan_model = bigvgan.BigVGAN.from_pretrained(vocoder_name)
|
||||
finally:
|
||||
if lock is not None:
|
||||
lock.__exit__(None, None, None)
|
||||
# One-time weight cast (vs per-call autocast, which re-casts every conv's
|
||||
# fp32 weights on each forward and is slower than fp32 on BigVGAN).
|
||||
bigvgan_model = bigvgan_model.to(device=device, dtype=dtype).eval()
|
||||
bigvgan_model.remove_weight_norm()
|
||||
for p in bigvgan_model.parameters():
|
||||
p.requires_grad_(False)
|
||||
_bigvgan_models[cache_key] = bigvgan_model
|
||||
return bigvgan_model
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1 Decoder
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class IndexTTS2S2MelDecoder(nn.Module):
|
||||
"""S2Mel + BigVGAN decoder for IndexTTS2 Stage 1.
|
||||
|
||||
Receives from Stage 0:
|
||||
- mel_codes: [T] mel code sequence
|
||||
- latent: [T, 1280] hidden states from GPT AR
|
||||
- S_ref: speaker semantic embeddings (from Wav2Vec2 → RepCodec)
|
||||
- ref_mel: [80, T_ref] reference mel spectrogram
|
||||
- style: [192] CAMPPlus style vector
|
||||
"""
|
||||
|
||||
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
self.vllm_config = vllm_config
|
||||
self.model_path = vllm_config.model_config.model
|
||||
self.config: IndexTTS2Config = vllm_config.model_config.hf_config # type: ignore[assignment]
|
||||
|
||||
# --- Flags for vLLM-Omni framework ---
|
||||
self.have_multimodal_outputs = True
|
||||
self.has_preprocess = False
|
||||
self.has_postprocess = False
|
||||
self.enable_update_additional_information = True
|
||||
self.requires_raw_input_tokens = True
|
||||
|
||||
# --- Build S2Mel model (CFM + LengthRegulator + gpt_layer) ---
|
||||
s2mel_cfg = self.config.s2mel
|
||||
s2mel_args = AttrDict(s2mel_cfg)
|
||||
# Ensure nested dicts are also AttrDicts
|
||||
for key in ["DiT", "wavenet", "length_regulator", "style_encoder", "preprocess_params"]:
|
||||
if key in s2mel_args and isinstance(s2mel_args[key], dict):
|
||||
s2mel_args[key] = AttrDict(s2mel_args[key])
|
||||
|
||||
self.s2mel = MyModel(s2mel_args, use_emovec=False, use_gpt_latent=True)
|
||||
|
||||
# Diffusion config — configurable via deploy YAML hf_overrides
|
||||
self.diffusion_steps: int = getattr(self.config, "diffusion_steps", 25)
|
||||
self.inference_cfg_rate: float = getattr(self.config, "inference_cfg_rate", 0.7)
|
||||
self.mel_code_to_frame_ratio: float = 1.72
|
||||
|
||||
# Run the DiT estimator under bf16 autocast (flash attention + faster
|
||||
# GEMMs); the CFM Euler solver stays float32. Disable via deploy YAML
|
||||
# hf_overrides `s2mel_dit_bf16: false` if quality regresses.
|
||||
self.s2mel_dit_bf16: bool = getattr(self.config, "s2mel_dit_bf16", True)
|
||||
# Run BigVGAN vocoding under bf16 autocast (conv-heavy, ~2x faster).
|
||||
# Disable via deploy YAML hf_overrides `s2mel_vocoder_bf16: false`.
|
||||
self.s2mel_vocoder_bf16: bool = getattr(self.config, "s2mel_vocoder_bf16", True)
|
||||
# Capture BigVGAN into bucketed CUDA graphs (collapses the Snake/conv
|
||||
# small-op launch storm). Enable via `s2mel_vocoder_cuda_graph: true`.
|
||||
self.s2mel_vocoder_cuda_graph: bool = getattr(self.config, "s2mel_vocoder_cuda_graph", False)
|
||||
self._vocoder_graph: Any = None
|
||||
# Capture the DiT transformer core into per-shape CUDA graphs.
|
||||
# Enable via `s2mel_dit_cuda_graph: true`.
|
||||
self.s2mel_dit_cuda_graph: bool = getattr(self.config, "s2mel_dit_cuda_graph", False)
|
||||
self.s2mel_dit_cuda_graph_max_graphs: int = int(getattr(self.config, "s2mel_dit_cuda_graph_max_graphs", 8))
|
||||
self.s2mel_vocoder_capture_sizes: list[int] | None = getattr(self.config, "s2mel_vocoder_capture_sizes", None)
|
||||
self.s2mel_vocoder_compile_shapes: list[int] | None = getattr(self.config, "s2mel_vocoder_compile_shapes", None)
|
||||
logger.info(
|
||||
"[S2Mel] dit_bf16=%s, vocoder_bf16=%s, "
|
||||
"vocoder_cuda_graph=%s, dit_cuda_graph=%s, dit_graph_lru=%d, vocoder_buckets=%s, "
|
||||
"vocoder_compile_shapes=%s",
|
||||
self.s2mel_dit_bf16,
|
||||
self.s2mel_vocoder_bf16,
|
||||
self.s2mel_vocoder_cuda_graph,
|
||||
self.s2mel_dit_cuda_graph,
|
||||
self.s2mel_dit_cuda_graph_max_graphs,
|
||||
self.s2mel_vocoder_capture_sizes,
|
||||
self.s2mel_vocoder_compile_shapes,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# vLLM hooks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor:
|
||||
if input_ids.numel() == 0:
|
||||
return torch.empty((0, 1), device=input_ids.device, dtype=torch.float32)
|
||||
return torch.zeros((input_ids.shape[0], 1), device=input_ids.device, dtype=torch.float32)
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
self,
|
||||
input_ids: torch.Tensor,
|
||||
positions: torch.Tensor | None = None,
|
||||
intermediate_tensors: IntermediateTensors | None = None,
|
||||
inputs_embeds: torch.Tensor | None = None,
|
||||
runtime_additional_information: list[dict[str, Any]] | None = None,
|
||||
**kwargs: Any,
|
||||
) -> torch.Tensor | OmniOutput:
|
||||
"""Run S2Mel flow matching + BigVGAN vocoding."""
|
||||
model_intermediate_buffer = kwargs.get("model_intermediate_buffer")
|
||||
if model_intermediate_buffer is None:
|
||||
model_intermediate_buffer = runtime_additional_information
|
||||
if runtime_additional_information is not None and "model_intermediate_buffer" not in kwargs:
|
||||
logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer")
|
||||
|
||||
request_infos: list[dict[str, Any]] = []
|
||||
if model_intermediate_buffer:
|
||||
request_infos = [info for info in model_intermediate_buffer if isinstance(info, dict)]
|
||||
additional_information: dict[str, Any] = request_infos[0] if request_infos else {}
|
||||
|
||||
device = input_ids.device
|
||||
model_dtype = self.s2mel.models["gpt_layer"][0].weight.dtype
|
||||
|
||||
logger.debug(
|
||||
"[S2Mel forward] model_intermediate_buffer len=%d, keys=%s",
|
||||
len(model_intermediate_buffer) if model_intermediate_buffer else 0,
|
||||
list(additional_information.keys()) if additional_information else [],
|
||||
)
|
||||
for k, v in additional_information.items():
|
||||
if isinstance(v, torch.Tensor):
|
||||
logger.debug("[S2Mel forward] %s: shape=%s, dtype=%s", k, v.shape, v.dtype)
|
||||
|
||||
# Extract Stage 0 outputs and pad multiple ready Stage-1 requests into
|
||||
# one S2Mel batch. The runner can split list-valued multimodal outputs
|
||||
# back to per-request payloads, so never silently drop batched items.
|
||||
if len(request_infos) > 1:
|
||||
mel_codes, latent, code_lens, code_lens_list, s_ref, ref_mel, style = self._build_batched_inputs(
|
||||
request_infos,
|
||||
device=device,
|
||||
model_dtype=model_dtype,
|
||||
)
|
||||
code_lens_raw = code_lens
|
||||
else:
|
||||
# Extract Stage 0 outputs and cast to model dtype
|
||||
latent = self._get_tensor(additional_information, "latent", device, model_dtype)
|
||||
mel_codes = self._get_tensor(additional_information, "mel_codes", device)
|
||||
code_lens_raw = additional_information.get("code_lens")
|
||||
s_ref = self._get_tensor(additional_information, "S_ref", device)
|
||||
ref_mel = self._get_tensor(additional_information, "ref_mel", device, model_dtype)
|
||||
style = self._get_tensor(additional_information, "style", device, model_dtype)
|
||||
|
||||
if mel_codes is None or latent is None:
|
||||
logger.warning("S2Mel decoder received empty mel_codes or latent")
|
||||
return OmniOutput(
|
||||
text_hidden_states=torch.zeros(1, device=device),
|
||||
multimodal_outputs={"audio": torch.zeros(1, device=device), "sr": 22050},
|
||||
)
|
||||
|
||||
logger.debug(
|
||||
"[S2Mel] extracted tensors — mel_codes=%s latent=%s code_lens=%s s_ref=%s ref_mel=%s style=%s",
|
||||
mel_codes.shape if mel_codes is not None else None,
|
||||
latent.shape if latent is not None else None,
|
||||
code_lens_raw,
|
||||
s_ref.shape if isinstance(s_ref, torch.Tensor) else None,
|
||||
ref_mel.shape if isinstance(ref_mel, torch.Tensor) else None,
|
||||
style.shape if isinstance(style, torch.Tensor) else None,
|
||||
)
|
||||
|
||||
# Ensure batch dimension
|
||||
if mel_codes.ndim == 1:
|
||||
mel_codes = mel_codes.unsqueeze(0)
|
||||
if latent.ndim == 2:
|
||||
latent = latent.unsqueeze(0)
|
||||
|
||||
# Compute code_lens
|
||||
if code_lens_raw is not None:
|
||||
if isinstance(code_lens_raw, torch.Tensor):
|
||||
if "code_lens_list" not in locals():
|
||||
code_lens_list = [int(x) for x in code_lens_raw.detach().cpu().reshape(-1).tolist()]
|
||||
code_lens = code_lens_raw.to(device=device, dtype=torch.long)
|
||||
else:
|
||||
code_lens_list = [int(x) for x in code_lens_raw]
|
||||
code_lens = torch.tensor(code_lens_list, device=device, dtype=torch.long)
|
||||
else:
|
||||
# Strip stop token (8193) and compute actual length
|
||||
stop_token = self.config.gpt.get("stop_mel_token", 8193)
|
||||
code_lens_list = []
|
||||
for i in range(mel_codes.shape[0]):
|
||||
stop_mask = (mel_codes[i] == stop_token).nonzero(as_tuple=False)
|
||||
if stop_mask.numel() > 0:
|
||||
code_lens_list.append(stop_mask[0].item())
|
||||
else:
|
||||
code_lens_list.append(mel_codes.shape[1])
|
||||
code_lens = torch.tensor(code_lens_list, device=device, dtype=torch.long)
|
||||
|
||||
# Trim to actual length
|
||||
max_len = max(code_lens_list)
|
||||
mel_codes = mel_codes[:, :max_len]
|
||||
latent = latent[:, :max_len, :]
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"[S2Mel] after trim — mel_codes=%s latent=%s code_lens=%s max_len=%d",
|
||||
mel_codes.shape,
|
||||
latent.shape,
|
||||
code_lens.tolist(),
|
||||
max_len,
|
||||
)
|
||||
|
||||
# --- S2Mel pipeline ---
|
||||
# 1. Project GPT latent: 1280 → 1024
|
||||
latent = latent.to(device=device, dtype=model_dtype)
|
||||
latent = self.s2mel.forward_gpt(latent).to(dtype=model_dtype) # [B, T, 1024]
|
||||
logger.debug("[S2Mel] step1 forward_gpt → latent=%s dtype=%s", latent.shape, latent.dtype)
|
||||
|
||||
# 2. Embed mel codes via semantic codec vq2emb
|
||||
semantic_codec = load_semantic_codec(self.model_path, self.config.semantic_codec, device)
|
||||
codebook_size = self.config.semantic_codec.get("codebook_size", 8192)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"mel_codes debug: shape=%s, min=%d, max=%d",
|
||||
mel_codes.shape,
|
||||
mel_codes.min().item(),
|
||||
mel_codes.max().item(),
|
||||
)
|
||||
if getattr(self.config, "s2mel_validate_code_range", False) and torch.any(
|
||||
(mel_codes < 0) | (mel_codes >= codebook_size)
|
||||
):
|
||||
raise ValueError(f"IndexTTS2 generated mel code outside semantic codebook range [0, {codebook_size - 1}]")
|
||||
with torch.no_grad():
|
||||
S_infer = semantic_codec.quantizer.vq2emb(mel_codes.unsqueeze(1)) # [B, T, 1024]
|
||||
S_infer = S_infer.transpose(1, 2).to(dtype=model_dtype) # [B, T, 1024]
|
||||
S_infer = S_infer + latent
|
||||
logger.debug("[S2Mel] step2 vq2emb → S_infer=%s dtype=%s", S_infer.shape, S_infer.dtype)
|
||||
|
||||
# 3. Length regulate: codes → mel frames
|
||||
target_lengths_list = [int(length * self.mel_code_to_frame_ratio) for length in code_lens_list]
|
||||
target_lengths = torch.tensor(target_lengths_list, device=device, dtype=torch.long)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("[S2Mel] step3 length_regulator target_lengths=%s", target_lengths_list)
|
||||
cond = self.s2mel.models["length_regulator"](S_infer, ylens=target_lengths, n_quantizers=3, f0=None)[
|
||||
0
|
||||
] # [B, T_mel, 512]
|
||||
logger.debug("[S2Mel] step3 length_regulator → cond=%s", cond.shape)
|
||||
|
||||
# 4. Reference prompt conditioning
|
||||
if s_ref is not None and ref_mel is not None:
|
||||
if ref_mel.ndim == 2:
|
||||
ref_mel = ref_mel.unsqueeze(0)
|
||||
|
||||
if s_ref.ndim == 3:
|
||||
# Official infer_v2.py uses the quantized embedding returned by
|
||||
# semantic_codec.quantize() directly as S_ref.
|
||||
s_ref_emb = s_ref.to(device=device, dtype=model_dtype)
|
||||
else:
|
||||
# Backward-compatible path for older payloads that carried
|
||||
# codebook indices instead of quantized embeddings.
|
||||
s_ref_codes = s_ref.long()
|
||||
if s_ref_codes.ndim == 1:
|
||||
s_ref_codes = s_ref_codes.unsqueeze(0) # [1, T]
|
||||
codebook_size = self.config.semantic_codec.get("codebook_size", 8192)
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"S_ref debug: shape=%s, min=%d, max=%d, codebook_size=%d",
|
||||
s_ref_codes.shape,
|
||||
s_ref_codes.min().item(),
|
||||
s_ref_codes.max().item(),
|
||||
codebook_size,
|
||||
)
|
||||
s_ref_codes = s_ref_codes.clamp(0, codebook_size - 1)
|
||||
with torch.no_grad():
|
||||
s_ref_emb = semantic_codec.quantizer.vq2emb(s_ref_codes.unsqueeze(1)) # [B, 1024, T]
|
||||
s_ref_emb = s_ref_emb.transpose(1, 2).to(dtype=model_dtype) # [B, T, 1024]
|
||||
|
||||
ref_target_lengths = self._infer_ref_lengths(ref_mel)
|
||||
prompt_condition = self.s2mel.models["length_regulator"](
|
||||
s_ref_emb, ylens=ref_target_lengths, n_quantizers=3, f0=None
|
||||
)[0]
|
||||
cat_condition = torch.cat([prompt_condition, cond], dim=1)
|
||||
logger.debug(
|
||||
"[S2Mel] step4 ref conditioning — s_ref_emb=%s prompt_condition=%s cat_condition=%s",
|
||||
s_ref_emb.shape,
|
||||
prompt_condition.shape,
|
||||
cat_condition.shape,
|
||||
)
|
||||
else:
|
||||
prompt_condition = None
|
||||
cat_condition = cond
|
||||
ref_mel = torch.zeros(cond.shape[0], 80, 0, device=device, dtype=model_dtype)
|
||||
|
||||
if style is None:
|
||||
style = torch.zeros(cat_condition.shape[0], 192, device=device, dtype=model_dtype)
|
||||
if style.ndim == 1:
|
||||
style = style.unsqueeze(0)
|
||||
|
||||
logger.debug(
|
||||
"[S2Mel] step5 CFM input — cat_condition=%s ref_mel=%s style=%s steps=%d cfg_rate=%.2f",
|
||||
cat_condition.shape,
|
||||
ref_mel.shape,
|
||||
style.shape,
|
||||
self.diffusion_steps,
|
||||
self.inference_cfg_rate,
|
||||
)
|
||||
|
||||
# 5. Flow matching inference (Euler ODE steps, CFG rate configurable)
|
||||
# CFM ODE solver runs in float32 — fp16 Euler steps diverge to noise.
|
||||
# The DiT estimator itself may run under bf16 autocast (see below).
|
||||
cfm = self.s2mel.models["cfm"]
|
||||
cfm.estimator_autocast_dtype = torch.bfloat16 if self.s2mel_dit_bf16 else None
|
||||
cond = cond.float()
|
||||
if prompt_condition is not None:
|
||||
prompt_condition = prompt_condition.float()
|
||||
cat_condition = cat_condition.float()
|
||||
ref_mel = ref_mel.float()
|
||||
style = style.float()
|
||||
|
||||
if prompt_condition is None:
|
||||
ref_lengths = torch.zeros(cat_condition.shape[0], device=device, dtype=torch.long)
|
||||
else:
|
||||
ref_lengths = self._infer_ref_lengths(ref_mel)
|
||||
ref_lengths_list = [int(length) for length in ref_lengths.tolist()]
|
||||
if len(ref_lengths_list) == 1 and cat_condition.shape[0] > 1:
|
||||
ref_lengths_list = ref_lengths_list * int(cat_condition.shape[0])
|
||||
ref_lengths = ref_lengths.expand(int(cat_condition.shape[0]))
|
||||
max_ref_len = int(ref_mel.size(-1))
|
||||
ref_len = ref_lengths_list[0] if ref_lengths_list else max_ref_len
|
||||
x_lens = target_lengths + ref_lengths
|
||||
|
||||
estimator = cfm.estimator
|
||||
required_cache_batch = max(2, int(cat_condition.shape[0]) * (2 if self.inference_cfg_rate > 0 else 1))
|
||||
if (
|
||||
estimator.transformer.freqs_cis is None
|
||||
or getattr(estimator.transformer, "max_batch_size", -1) < required_cache_batch
|
||||
):
|
||||
estimator.setup_caches(max_batch_size=required_cache_batch, max_seq_length=16384)
|
||||
logger.info("[S2Mel] DiT caches initialized (freqs_cis + causal_mask)")
|
||||
|
||||
cat_len = int(cat_condition.size(1))
|
||||
x_lens_list = [target_len + ref_len_i for target_len, ref_len_i in zip(target_lengths_list, ref_lengths_list)]
|
||||
same_ref_len = len(set(ref_lengths_list)) <= 1
|
||||
same_target_len = len(set(target_lengths_list)) <= 1
|
||||
dit_graph_full_mask = same_ref_len and same_target_len and all(length == cat_len for length in x_lens_list)
|
||||
if dit_graph_full_mask:
|
||||
self._enable_dit_cuda_graph(estimator)
|
||||
with torch.no_grad():
|
||||
mel = cfm.inference(
|
||||
cat_condition,
|
||||
x_lens,
|
||||
ref_mel,
|
||||
style,
|
||||
None, # f0
|
||||
self.diffusion_steps,
|
||||
inference_cfg_rate=self.inference_cfg_rate,
|
||||
)
|
||||
# Strip reference portion
|
||||
mel = mel[:, :, ref_len:]
|
||||
else:
|
||||
# Batched Stage-1 requests are padded to a shared length. The DiT
|
||||
# CUDA graph fast path assumes a full mask, so rebuild each request
|
||||
# with its exact reference/target lengths before enabling the graph.
|
||||
mels_list: list[torch.Tensor] = []
|
||||
for i in range(cat_condition.shape[0]):
|
||||
self._enable_dit_cuda_graph(estimator)
|
||||
ref_len_i = ref_lengths_list[i]
|
||||
target_len_i = target_lengths_list[i]
|
||||
if prompt_condition is not None:
|
||||
prompt_i = prompt_condition[i : i + 1, :ref_len_i]
|
||||
target_i = cond[i : i + 1, :target_len_i]
|
||||
cond_i = torch.cat([prompt_i, target_i], dim=1)
|
||||
else:
|
||||
cond_i = cat_condition[i : i + 1, :target_len_i]
|
||||
x_lens_i = torch.tensor([ref_len_i + target_len_i], device=device, dtype=torch.long)
|
||||
ref_mel_i = ref_mel[i : i + 1, :, :ref_len_i] if ref_mel.ndim == 3 else ref_mel[:, :ref_len_i]
|
||||
style_i = style[i : i + 1] if style.ndim == 2 else style
|
||||
with torch.no_grad():
|
||||
mel_i = cfm.inference(
|
||||
cond_i,
|
||||
x_lens_i,
|
||||
ref_mel_i,
|
||||
style_i,
|
||||
None,
|
||||
self.diffusion_steps,
|
||||
inference_cfg_rate=self.inference_cfg_rate,
|
||||
)
|
||||
mels_list.append(mel_i[0, :, ref_len_i:])
|
||||
mel = mels_list
|
||||
dit_graph_info = getattr(getattr(estimator, "_cuda_graph_runner", None), "last_call_info", None)
|
||||
mel_shape = [tuple(m.shape) for m in mel] if isinstance(mel, list) else tuple(mel.shape)
|
||||
logger.debug("[S2Mel] step5 CFM done → mel=%s (stripped ref %d frames)", mel_shape, ref_len)
|
||||
|
||||
# 6. BigVGAN vocoding
|
||||
vocoder_cfg = self.config.vocoder
|
||||
vocoder_name = vocoder_cfg.get("name", "nvidia/bigvgan_v2_22khz_80band_256x")
|
||||
voc_dtype = torch.bfloat16 if (self.s2mel_vocoder_bf16 and device.type == "cuda") else torch.float32
|
||||
bigvgan = _load_bigvgan(vocoder_name, device, voc_dtype)
|
||||
vocode = self._get_vocoder_graph(bigvgan, device, voc_dtype)
|
||||
upsample_factor = int(math.prod(bigvgan.h.upsample_rates))
|
||||
with torch.no_grad():
|
||||
wavs: list[torch.Tensor] = []
|
||||
vocoder_infos: list[dict[str, Any]] = []
|
||||
if isinstance(mel, list):
|
||||
# Per-request CFM output: variable-length mels. Batch them
|
||||
# for BigVGAN by padding to the max length.
|
||||
batch_size = len(mel)
|
||||
if batch_size == 1:
|
||||
mel_i = mel[0:1].to(voc_dtype) if mel[0].ndim == 3 else mel[0].unsqueeze(0).to(voc_dtype)
|
||||
wav_i = vocode(mel_i).float().squeeze(0).squeeze(0)
|
||||
wavs.append(torch.clamp(wav_i, -1.0, 1.0))
|
||||
last_call_info = getattr(vocode, "last_call_info", None)
|
||||
if isinstance(last_call_info, dict):
|
||||
vocoder_infos.append(dict(last_call_info))
|
||||
else:
|
||||
for i in range(batch_size):
|
||||
mel_i = mel[i].unsqueeze(0).to(voc_dtype) if mel[i].ndim == 2 else mel[i : i + 1].to(voc_dtype)
|
||||
wav_i = vocode(mel_i).float().squeeze(0).squeeze(0)
|
||||
wavs.append(torch.clamp(wav_i, -1.0, 1.0))
|
||||
last_call_info = getattr(vocode, "last_call_info", None)
|
||||
if isinstance(last_call_info, dict):
|
||||
vocoder_infos.append(dict(last_call_info))
|
||||
else:
|
||||
batch_size = mel.shape[0]
|
||||
if batch_size == 1:
|
||||
mel_i = mel[:, :, : target_lengths_list[0]].to(voc_dtype)
|
||||
wav_i = vocode(mel_i).float().squeeze(0).squeeze(0)
|
||||
wavs.append(torch.clamp(wav_i, -1.0, 1.0))
|
||||
last_call_info = getattr(vocode, "last_call_info", None)
|
||||
if isinstance(last_call_info, dict):
|
||||
vocoder_infos.append(dict(last_call_info))
|
||||
else:
|
||||
for i in range(batch_size):
|
||||
mel_i = mel[i : i + 1, :, : target_lengths_list[i]].to(voc_dtype)
|
||||
wav_i = vocode(mel_i).float().squeeze(0).squeeze(0)
|
||||
wavs.append(torch.clamp(wav_i, -1.0, 1.0))
|
||||
last_call_info = getattr(vocode, "last_call_info", None)
|
||||
if isinstance(last_call_info, dict):
|
||||
vocoder_infos.append(dict(last_call_info))
|
||||
wav: torch.Tensor | list[torch.Tensor] = wavs[0] if len(wavs) == 1 else wavs
|
||||
|
||||
# Keep the public vLLM-Omni audio contract as normalized float. This is
|
||||
# numerically equivalent to official infer_v2.py before its int16 save
|
||||
# step, while avoiding double scaling in OpenAI/soundfile encoders.
|
||||
if isinstance(wav, torch.Tensor) and wav.ndim == 0:
|
||||
wav = wav.unsqueeze(0)
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"[S2Mel] step6 BigVGAN done → wav=%s min=%.1f max=%.1f sr=22050",
|
||||
wav.shape if isinstance(wav, torch.Tensor) else [w.shape for w in wav],
|
||||
wav.min().item() if isinstance(wav, torch.Tensor) else min(w.min().item() for w in wav),
|
||||
wav.max().item() if isinstance(wav, torch.Tensor) else max(w.max().item() for w in wav),
|
||||
)
|
||||
|
||||
audio_cpu = [w.cpu() for w in wav] if isinstance(wav, list) else wav.cpu()
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={
|
||||
"audio": audio_cpu,
|
||||
"sr": torch.tensor(22050, dtype=torch.int32),
|
||||
},
|
||||
)
|
||||
|
||||
def compute_logits(self, hidden_states: Any, sampling_metadata: Any = None) -> None:
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_batched_inputs(
|
||||
self,
|
||||
infos: list[dict[str, Any]],
|
||||
*,
|
||||
device: torch.device,
|
||||
model_dtype: torch.dtype,
|
||||
) -> tuple[
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
torch.Tensor,
|
||||
list[int],
|
||||
torch.Tensor | None,
|
||||
torch.Tensor | None,
|
||||
torch.Tensor | None,
|
||||
]:
|
||||
stop_token = self.config.gpt.get("stop_mel_token", 8193)
|
||||
mel_items: list[torch.Tensor] = []
|
||||
latent_items: list[torch.Tensor] = []
|
||||
code_lens: list[int] = []
|
||||
s_ref_items: list[torch.Tensor | None] = []
|
||||
ref_mel_items: list[torch.Tensor | None] = []
|
||||
style_items: list[torch.Tensor | None] = []
|
||||
|
||||
for idx, info in enumerate(infos):
|
||||
mel = self._get_tensor(info, "mel_codes", device)
|
||||
latent = self._get_tensor(info, "latent", device, model_dtype)
|
||||
if mel is None or latent is None:
|
||||
raise ValueError(f"S2Mel batched input {idx} is missing mel_codes or latent")
|
||||
|
||||
mel = mel.to(device=device, dtype=torch.long).reshape(-1)
|
||||
if latent.ndim == 3 and latent.shape[0] == 1:
|
||||
latent = latent[0]
|
||||
elif latent.ndim != 2:
|
||||
latent = latent.reshape(-1, latent.shape[-1])
|
||||
|
||||
raw_len = info.get("code_lens")
|
||||
if isinstance(raw_len, torch.Tensor) and raw_len.numel() > 0:
|
||||
code_len = int(raw_len.reshape(-1)[0].item())
|
||||
elif raw_len is not None:
|
||||
code_len = int(raw_len[0] if isinstance(raw_len, list) else raw_len)
|
||||
else:
|
||||
stop_mask = (mel == stop_token).nonzero(as_tuple=False)
|
||||
code_len = int(stop_mask[0].item()) if stop_mask.numel() > 0 else int(mel.shape[0])
|
||||
code_len = max(0, min(code_len, int(mel.shape[0]), int(latent.shape[0])))
|
||||
mel_items.append(mel[:code_len])
|
||||
latent_items.append(latent[:code_len])
|
||||
code_lens.append(code_len)
|
||||
|
||||
s_ref = self._get_tensor(info, "S_ref", device)
|
||||
if isinstance(s_ref, torch.Tensor) and s_ref.ndim == 3 and s_ref.shape[0] == 1:
|
||||
s_ref = s_ref[0]
|
||||
s_ref_items.append(s_ref)
|
||||
|
||||
ref_mel = self._get_tensor(info, "ref_mel", device, model_dtype)
|
||||
if isinstance(ref_mel, torch.Tensor) and ref_mel.ndim == 3 and ref_mel.shape[0] == 1:
|
||||
ref_mel = ref_mel[0]
|
||||
ref_mel_items.append(ref_mel)
|
||||
|
||||
style = self._get_tensor(info, "style", device, model_dtype)
|
||||
if isinstance(style, torch.Tensor) and style.ndim == 2 and style.shape[0] == 1:
|
||||
style = style[0]
|
||||
style_items.append(style)
|
||||
|
||||
max_code_len = max(code_lens) if code_lens else 0
|
||||
latent_dim = latent_items[0].shape[-1]
|
||||
# Padding reaches vq2emb before length masking, so it must be a valid
|
||||
# semantic-code index. Actual sequence lengths are still carried by
|
||||
# code_lens/target_lengths; stop_token (8193) is outside the codebook.
|
||||
mel_batch = torch.zeros((len(mel_items), max_code_len), dtype=torch.long, device=device)
|
||||
latent_batch = torch.zeros(len(latent_items), max_code_len, latent_dim, device=device, dtype=model_dtype)
|
||||
for i, (mel, latent) in enumerate(zip(mel_items, latent_items)):
|
||||
mel_batch[i, : mel.shape[0]] = mel
|
||||
latent_batch[i, : latent.shape[0]] = latent.to(dtype=model_dtype)
|
||||
|
||||
s_ref_batch = self._stack_optional_sequence(s_ref_items, device=device, dtype=model_dtype)
|
||||
ref_mel_batch = self._stack_optional_ref_mel(ref_mel_items, device=device, dtype=model_dtype)
|
||||
style_batch = self._stack_optional_style(style_items, device=device, dtype=model_dtype)
|
||||
return (
|
||||
mel_batch,
|
||||
latent_batch,
|
||||
torch.tensor(code_lens, device=device, dtype=torch.long),
|
||||
code_lens,
|
||||
s_ref_batch,
|
||||
ref_mel_batch,
|
||||
style_batch,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _stack_optional_sequence(
|
||||
items: list[torch.Tensor | None],
|
||||
*,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
) -> torch.Tensor | None:
|
||||
if not items or any(item is None for item in items):
|
||||
return None
|
||||
tensors = [item.to(device=device) for item in items if item is not None]
|
||||
max_len = max(int(t.shape[0]) for t in tensors)
|
||||
if tensors[0].ndim == 1:
|
||||
out = torch.zeros(len(tensors), max_len, device=device, dtype=tensors[0].dtype)
|
||||
for i, t in enumerate(tensors):
|
||||
out[i, : t.shape[0]] = t
|
||||
return out
|
||||
out = torch.zeros(len(tensors), max_len, tensors[0].shape[-1], device=device, dtype=dtype)
|
||||
for i, t in enumerate(tensors):
|
||||
out[i, : t.shape[0], :] = t.to(dtype=dtype)
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _stack_optional_ref_mel(
|
||||
items: list[torch.Tensor | None],
|
||||
*,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
) -> torch.Tensor | None:
|
||||
if not items or any(item is None for item in items):
|
||||
return None
|
||||
tensors = [item.to(device=device, dtype=dtype) for item in items if item is not None]
|
||||
max_len = max(int(t.shape[-1]) for t in tensors)
|
||||
out = torch.zeros(len(tensors), tensors[0].shape[-2], max_len, device=device, dtype=dtype)
|
||||
for i, t in enumerate(tensors):
|
||||
out[i, :, : t.shape[-1]] = t
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _stack_optional_style(
|
||||
items: list[torch.Tensor | None],
|
||||
*,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype,
|
||||
) -> torch.Tensor | None:
|
||||
if not items or any(item is None for item in items):
|
||||
return None
|
||||
return torch.stack(
|
||||
[item.to(device=device, dtype=dtype).reshape(-1) for item in items if item is not None],
|
||||
dim=0,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _infer_ref_lengths(ref_mel: torch.Tensor) -> torch.Tensor:
|
||||
if ref_mel.ndim == 2:
|
||||
return torch.tensor([ref_mel.size(-1)], device=ref_mel.device, dtype=torch.long)
|
||||
nonzero = ref_mel.abs().sum(dim=1) > 0
|
||||
lengths = nonzero.long().sum(dim=1)
|
||||
return torch.clamp(lengths, min=1).to(device=ref_mel.device, dtype=torch.long)
|
||||
|
||||
@staticmethod
|
||||
def _get_tensor(
|
||||
info: dict,
|
||||
key: str,
|
||||
device: torch.device,
|
||||
dtype: torch.dtype | None = None,
|
||||
) -> torch.Tensor | None:
|
||||
"""Extract tensor from additional_information, handling nested dicts."""
|
||||
val = info.get(key)
|
||||
if val is None:
|
||||
for sub_key in ["codes", "hidden_states", "meta"]:
|
||||
sub = info.get(sub_key)
|
||||
if isinstance(sub, dict) and key in sub:
|
||||
val = sub[key]
|
||||
break
|
||||
if val is None:
|
||||
return None
|
||||
if isinstance(val, torch.Tensor):
|
||||
return val.to(device=device, dtype=dtype) if dtype else val.to(device=device)
|
||||
if isinstance(val, list):
|
||||
if val and isinstance(val[0], torch.Tensor):
|
||||
return val[0].to(device=device, dtype=dtype) if dtype else val[0].to(device=device)
|
||||
return None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Weight loading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights from s2mel.pth checkpoint.
|
||||
|
||||
Bypasses vllm's default .pt iterator (same reason as talker: raw
|
||||
tensor .pt files in model dir). Loads s2mel.pth directly and
|
||||
flattens the nested ``state["net"]`` structure:
|
||||
|
||||
- ``net["cfm"][key]`` → ``s2mel.models.cfm.{key}``
|
||||
- ``net["length_regulator"][key]`` → ``s2mel.models.length_regulator.{key}``
|
||||
- ``net["gpt_layer"][key]`` → ``s2mel.models.gpt_layer.{key}``
|
||||
"""
|
||||
_ = weights
|
||||
|
||||
ckpt_path = resolve_model_file(self.model_path, self.config.s2mel_checkpoint)
|
||||
if ckpt_path is None:
|
||||
raise FileNotFoundError(f"IndexTTS2 S2Mel checkpoint {self.config.s2mel_checkpoint!r} not found")
|
||||
state = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
net = state["net"] if "net" in state else state
|
||||
|
||||
params_dict = dict(self.named_parameters(remove_duplicate=False))
|
||||
loaded_params: set[str] = set()
|
||||
|
||||
prefix_map = {
|
||||
"cfm.": "s2mel.models.cfm.",
|
||||
"length_regulator.": "s2mel.models.length_regulator.",
|
||||
"gpt_layer.": "s2mel.models.gpt_layer.",
|
||||
}
|
||||
|
||||
# Flatten nested ModuleDict state: {module: {param: tensor}} → flat iter
|
||||
flat_items: Iterable[tuple[str, torch.Tensor]]
|
||||
if net and isinstance(next(iter(net.values())), dict):
|
||||
flat_items = (
|
||||
(f"{mod}.{param}", tensor) for mod, mod_state in net.items() for param, tensor in mod_state.items()
|
||||
)
|
||||
else:
|
||||
flat_items = net.items()
|
||||
|
||||
for name, loaded_weight in flat_items:
|
||||
mapped_name = name
|
||||
for old_prefix, new_prefix in prefix_map.items():
|
||||
if name.startswith(old_prefix):
|
||||
mapped_name = new_prefix + name[len(old_prefix) :]
|
||||
break
|
||||
|
||||
if mapped_name not in params_dict:
|
||||
logger.debug("Skipping unrecognized S2Mel weight: %s → %s", name, mapped_name)
|
||||
continue
|
||||
|
||||
param = params_dict[mapped_name]
|
||||
weight_loader = getattr(param, "weight_loader", default_weight_loader)
|
||||
weight_loader(param, loaded_weight)
|
||||
loaded_params.add(mapped_name)
|
||||
|
||||
logger.info("Loaded %d weights for IndexTTS2S2MelDecoder", len(loaded_params))
|
||||
required_prefixes = [
|
||||
"s2mel.models.cfm.",
|
||||
"s2mel.models.length_regulator.",
|
||||
"s2mel.models.gpt_layer.",
|
||||
]
|
||||
missing_prefixes = [
|
||||
prefix for prefix in required_prefixes if not any(name.startswith(prefix) for name in loaded_params)
|
||||
]
|
||||
if missing_prefixes:
|
||||
raise RuntimeError(
|
||||
"IndexTTS2 S2Mel checkpoint did not load required parameter groups: " + ", ".join(missing_prefixes)
|
||||
)
|
||||
|
||||
stripped = _strip_weight_norm(self.s2mel)
|
||||
if stripped:
|
||||
# The fold replaces weight_g/weight_v with a plain `weight`
|
||||
# parameter; register it as loaded so vLLM's initialization
|
||||
# check does not flag it as missing from the checkpoint.
|
||||
for name in stripped:
|
||||
loaded_params.add(f"s2mel.{name}")
|
||||
logger.info("Folded %d weight_norm parametrizations in S2Mel", len(stripped))
|
||||
|
||||
# Keep the CFM ODE solver in float32 once after loading instead of
|
||||
# recursively re-casting the module on every request.
|
||||
cfm = self.s2mel.models["cfm"]
|
||||
cfm.float()
|
||||
cfm.estimator_autocast_dtype = torch.bfloat16 if self.s2mel_dit_bf16 else None
|
||||
|
||||
self._warmup_external_models()
|
||||
return loaded_params
|
||||
|
||||
def _warmup_external_models(self) -> None:
|
||||
"""Preload vocoder + semantic codec to eliminate first-request latency."""
|
||||
device = next((p.device for p in self.s2mel.parameters()), torch.device("cpu"))
|
||||
try:
|
||||
vocoder_name = self.config.vocoder.get("name", "nvidia/bigvgan_v2_22khz_80band_256x")
|
||||
voc_dtype = torch.bfloat16 if (self.s2mel_vocoder_bf16 and device.type == "cuda") else torch.float32
|
||||
bigvgan = _load_bigvgan(vocoder_name, device, voc_dtype)
|
||||
load_semantic_codec(self.model_path, self.config.semantic_codec, device)
|
||||
# Warm up the CUDA graph + torch.compile wrapper at load time so the
|
||||
# ~95s compile cost is paid during startup rather than on the first
|
||||
# request. Mirrors the Qwen3-TTS pattern (enable_cudagraph in
|
||||
# load_weights). _get_vocoder_graph constructs the wrapper and calls
|
||||
# warmup() on first access.
|
||||
if self.s2mel_vocoder_cuda_graph and device.type == "cuda":
|
||||
self._get_vocoder_graph(bigvgan, device, voc_dtype)
|
||||
logger.info("BigVGAN + semantic codec preloaded")
|
||||
except Exception as e:
|
||||
logger.warning("Failed to preload Stage 1 models: %s", e)
|
||||
|
||||
def _get_vocoder_graph(self, bigvgan: nn.Module, device: torch.device, dtype: torch.dtype):
|
||||
"""Return the CUDA-graph vocoder wrapper, or the eager model when disabled.
|
||||
|
||||
Warmup is lazy (first request) following the GLM-TTS DiT wrapper
|
||||
pattern; on warmup failure the wrapper is disabled and all
|
||||
subsequent calls fall back to the eager model.
|
||||
"""
|
||||
if not self.s2mel_vocoder_cuda_graph or device.type != "cuda":
|
||||
return bigvgan
|
||||
if self._vocoder_graph is None:
|
||||
from vllm_omni.model_executor.models.indextts2.bigvgan_cuda_graph import (
|
||||
CUDAGraphBigVGANWrapper,
|
||||
)
|
||||
|
||||
self._vocoder_graph = CUDAGraphBigVGANWrapper(
|
||||
bigvgan,
|
||||
capture_sizes=self.s2mel_vocoder_capture_sizes,
|
||||
compile_shapes=self.s2mel_vocoder_compile_shapes,
|
||||
)
|
||||
try:
|
||||
self._vocoder_graph.warmup(device, dtype)
|
||||
except Exception:
|
||||
logger.warning("Disabling BigVGAN CUDA graphs after warmup failure", exc_info=True)
|
||||
self._vocoder_graph.enabled = False
|
||||
return self._vocoder_graph
|
||||
|
||||
def _enable_dit_cuda_graph(self, estimator: nn.Module) -> None:
|
||||
"""Attach a per-shape CUDA graph runner to the DiT transformer core.
|
||||
|
||||
Sets `_assume_full_mask` on the attention modules: this decoder
|
||||
always passes x_lens == T, so the padding mask is all-True and
|
||||
dense attention is identical — the flag removes the backend's
|
||||
per-step ``torch.any(~mask)`` D2H sync, which is also a
|
||||
stream-capture blocker.
|
||||
"""
|
||||
if not self.s2mel_dit_cuda_graph:
|
||||
return
|
||||
if getattr(estimator, "_cuda_graph_runner", None) is not None:
|
||||
return
|
||||
from vllm_omni.model_executor.models.indextts2.dit_cuda_graph import CUDAGraphDiTRunner
|
||||
|
||||
for layer in estimator.transformer.layers:
|
||||
layer.attention._assume_full_mask = True
|
||||
estimator._cuda_graph_runner = CUDAGraphDiTRunner(
|
||||
estimator.transformer,
|
||||
max_graphs=self.s2mel_dit_cuda_graph_max_graphs,
|
||||
)
|
||||
logger.info(
|
||||
"S2Mel DiT CUDA graph runner enabled (per-shape capture, LRU=%d)",
|
||||
self.s2mel_dit_cuda_graph_max_graphs,
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,49 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""IndexTTS2 pipeline: GPT AR talker (text → mel codes) → S2Mel + BigVGAN (mel → audio).
|
||||
|
||||
Two-stage non-streaming pipeline. S2Mel flow matching (25 Euler steps) requires
|
||||
the full mel code sequence, so async_chunk is not applicable.
|
||||
"""
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
PipelineConfig,
|
||||
StageExecutionType,
|
||||
StagePipelineConfig,
|
||||
)
|
||||
|
||||
_PROC = "vllm_omni.model_executor.stage_input_processors.indextts2"
|
||||
|
||||
INDEXTTS2_PIPELINE = PipelineConfig(
|
||||
model_type="indextts2",
|
||||
model_arch="IndexTTS2TalkerForConditionalGeneration",
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
model_stage="indextts2_talker",
|
||||
execution_type=StageExecutionType.LLM_AR,
|
||||
input_sources=(),
|
||||
owns_tokenizer=True,
|
||||
engine_output_type="latent",
|
||||
extras={"skip_tokenizer_init": True, "tokenizer": "gpt2"},
|
||||
custom_process_next_stage_input_func=f"{_PROC}.talker2s2mel_full_payload",
|
||||
sampling_constraints={
|
||||
"detokenize": False,
|
||||
"stop_token_ids": [8193],
|
||||
},
|
||||
),
|
||||
StagePipelineConfig(
|
||||
stage_id=1,
|
||||
model_stage="indextts2_s2mel_decoder",
|
||||
execution_type=StageExecutionType.LLM_GENERATION,
|
||||
input_sources=(0,),
|
||||
final_output=True,
|
||||
final_output_type="audio",
|
||||
engine_output_type="audio",
|
||||
model_arch="IndexTTS2S2MelDecoder",
|
||||
sync_process_input_func=f"{_PROC}.talker2s2mel_token_only",
|
||||
extras={"skip_tokenizer_init": True, "tokenizer": "gpt2"},
|
||||
sampling_constraints={"detokenize": True},
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,324 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""External model loading, audio I/O, and emotion conditioning for IndexTTS2."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from transformers.utils.hub import cached_file
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.diffusion.model_loader.hub_prefetch import _repo_prefetch_lock
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lazy external model caches — keyed by (model_path, device) for multi-device
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_wav2vec2_cache: dict[tuple[str, str], tuple] = {}
|
||||
_semantic_codec_cache: dict[tuple[str, str], Any] = {}
|
||||
_campplus_cache: dict[tuple[str, str], Any] = {}
|
||||
_qwen_emotion_cache: dict[tuple[str, str], tuple] = {}
|
||||
|
||||
|
||||
def _freeze(model):
|
||||
model.eval()
|
||||
for p in model.parameters():
|
||||
p.requires_grad_(False)
|
||||
return model
|
||||
|
||||
|
||||
def resolve_model_file(model_path: str, filename: str) -> str | None:
|
||||
"""Resolve an IndexTTS2 asset from a local model dir or HF repo id."""
|
||||
local_path = os.path.join(model_path, filename)
|
||||
if os.path.isfile(local_path):
|
||||
return local_path
|
||||
try:
|
||||
return cached_file(model_path, filename)
|
||||
except Exception as e:
|
||||
logger.warning("Could not resolve IndexTTS2 asset %s from %s: %s", filename, model_path, e)
|
||||
return None
|
||||
|
||||
|
||||
def load_wav2vec2(model_path: str, device: torch.device):
|
||||
cache_key = (model_path, str(device))
|
||||
if cache_key in _wav2vec2_cache:
|
||||
return _wav2vec2_cache[cache_key]
|
||||
from transformers import AutoFeatureExtractor, Wav2Vec2BertModel
|
||||
|
||||
model, processor = None, None
|
||||
# Try local subfolder first (no lock needed), then HF hub with lock.
|
||||
try:
|
||||
src_args = dict(pretrained_model_name_or_path=model_path, subfolder="wav2vec2bert")
|
||||
processor = AutoFeatureExtractor.from_pretrained(**src_args)
|
||||
model = Wav2Vec2BertModel.from_pretrained(**src_args)
|
||||
except Exception:
|
||||
try:
|
||||
with _repo_prefetch_lock("facebook/w2v-bert-2.0"):
|
||||
src_args = dict(pretrained_model_name_or_path="facebook/w2v-bert-2.0")
|
||||
processor = AutoFeatureExtractor.from_pretrained(**src_args)
|
||||
model = Wav2Vec2BertModel.from_pretrained(**src_args)
|
||||
except Exception:
|
||||
pass
|
||||
if model is None or processor is None:
|
||||
raise RuntimeError(f"Could not load Wav2Vec2-BERT from {model_path}/wav2vec2bert or facebook/w2v-bert-2.0")
|
||||
_freeze(model.to(device=device, dtype=torch.float32))
|
||||
_wav2vec2_cache[cache_key] = (model, processor)
|
||||
return model, processor
|
||||
|
||||
|
||||
def load_semantic_codec(model_path: str, config: dict, device: torch.device):
|
||||
cache_key = (model_path, str(device))
|
||||
if cache_key in _semantic_codec_cache:
|
||||
return _semantic_codec_cache[cache_key]
|
||||
from .utils.maskgct.repcodec_model import RepCodec
|
||||
|
||||
codec = RepCodec(
|
||||
codebook_size=config.get("codebook_size", 8192),
|
||||
hidden_size=config.get("hidden_size", 1024),
|
||||
codebook_dim=config.get("codebook_dim", 8),
|
||||
)
|
||||
ckpt_path = resolve_model_file(model_path, "semantic_codec.pth")
|
||||
if ckpt_path is not None:
|
||||
state = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
codec.load_state_dict(state, strict=False)
|
||||
else:
|
||||
import safetensors.torch
|
||||
from huggingface_hub import hf_hub_download
|
||||
|
||||
ckpt_path = hf_hub_download("amphion/MaskGCT", filename="semantic_codec/model.safetensors")
|
||||
safetensors.torch.load_model(codec, ckpt_path)
|
||||
_freeze(codec.to(device=device, dtype=torch.float32))
|
||||
_semantic_codec_cache[cache_key] = codec
|
||||
return codec
|
||||
|
||||
|
||||
def load_campplus(model_path: str, device: torch.device):
|
||||
cache_key = (model_path, str(device))
|
||||
if cache_key in _campplus_cache:
|
||||
return _campplus_cache[cache_key]
|
||||
from .utils.campplus.dtdnn import CAMPPlus
|
||||
|
||||
campplus = CAMPPlus(feat_dim=80, embedding_size=192)
|
||||
ckpt_path = resolve_model_file(model_path, "campplus.pth")
|
||||
if ckpt_path is None:
|
||||
from huggingface_hub import hf_hub_download
|
||||
|
||||
ckpt_path = hf_hub_download("funasr/campplus", filename="campplus_cn_common.bin")
|
||||
state = torch.load(ckpt_path, map_location="cpu", weights_only=True)
|
||||
campplus.load_state_dict(state, strict=False)
|
||||
_freeze(campplus.to(device=device, dtype=torch.float32))
|
||||
_campplus_cache[cache_key] = campplus
|
||||
return campplus
|
||||
|
||||
|
||||
def load_qwen_emotion(model_path: str, device: torch.device, *, trust_remote_code: bool = True):
|
||||
cache_key = (model_path, str(device))
|
||||
if cache_key in _qwen_emotion_cache:
|
||||
return _qwen_emotion_cache[cache_key]
|
||||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||||
|
||||
subfolder = "qwen0.6bemo4-merge"
|
||||
try:
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_path,
|
||||
subfolder=subfolder,
|
||||
trust_remote_code=trust_remote_code,
|
||||
)
|
||||
model = AutoModelForCausalLM.from_pretrained(
|
||||
model_path,
|
||||
subfolder=subfolder,
|
||||
torch_dtype="float16",
|
||||
device_map={"": str(device)},
|
||||
trust_remote_code=trust_remote_code,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
"Could not load QwenEmotion from %s/%s: %s. use_emo_text will fall back to default emotion.",
|
||||
model_path,
|
||||
subfolder,
|
||||
e,
|
||||
)
|
||||
return None, None
|
||||
_freeze(model)
|
||||
_qwen_emotion_cache[cache_key] = (model, tokenizer)
|
||||
return model, tokenizer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Audio utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def compute_fbank(wav_16k: torch.Tensor, device: torch.device) -> torch.Tensor:
|
||||
"""Compute 80-dim fbank features for CAMPPlus. Input: [T] at 16kHz."""
|
||||
try:
|
||||
import torchaudio.compliance.kaldi as kaldi
|
||||
|
||||
# kaldi.fbank is pure torch — keep on the wav's device (GPU when
|
||||
# available) instead of forcing a CPU round-trip (~100ms for 15s).
|
||||
wav = wav_16k.unsqueeze(0).float()
|
||||
fbank = kaldi.fbank(
|
||||
wav,
|
||||
num_mel_bins=80,
|
||||
dither=0,
|
||||
sample_frequency=16000,
|
||||
)
|
||||
fbank = fbank - fbank.mean(dim=0, keepdim=True)
|
||||
return fbank.unsqueeze(0).to(device=device) # [1, T, 80]
|
||||
except (ImportError, RuntimeError, OSError):
|
||||
logger.warning("torchaudio not available, returning zero fbank")
|
||||
return torch.zeros(1, 100, 80, device=device)
|
||||
|
||||
|
||||
def wav2vec_extract(
|
||||
wav_16k: torch.Tensor,
|
||||
model: Any,
|
||||
processor: Any,
|
||||
device: torch.device,
|
||||
w2v_stat: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
"""Extract Wav2Vec2-BERT features. Returns [1, T, 1024]."""
|
||||
wav_np = wav_16k.cpu().numpy()
|
||||
inputs = processor(wav_np, sampling_rate=16000, return_tensors="pt")
|
||||
input_features = inputs["input_features"].to(device=device, dtype=torch.float32)
|
||||
attention_mask = inputs.get("attention_mask")
|
||||
if attention_mask is not None:
|
||||
attention_mask = attention_mask.to(device=device)
|
||||
with torch.no_grad():
|
||||
# bf16 autocast roughly halves the w2v-bert forward (~600M params,
|
||||
# fp32 weights). Features are cast back to fp32 before stat
|
||||
# normalization; downstream consumers are fp32.
|
||||
if input_features.is_cuda:
|
||||
with torch.autocast("cuda", dtype=torch.bfloat16):
|
||||
outputs = model(
|
||||
input_features=input_features,
|
||||
attention_mask=attention_mask,
|
||||
output_hidden_states=True,
|
||||
)
|
||||
else:
|
||||
outputs = model(
|
||||
input_features=input_features,
|
||||
attention_mask=attention_mask,
|
||||
output_hidden_states=True,
|
||||
)
|
||||
feat = outputs.hidden_states[17].float()
|
||||
if w2v_stat is not None:
|
||||
mean = w2v_stat[0:1, :].to(device=device)
|
||||
std = torch.sqrt(w2v_stat[1:2, :].to(device=device))
|
||||
feat = (feat - mean) / std
|
||||
return feat
|
||||
|
||||
|
||||
def load_reference_audio(
|
||||
audio_path: str | tuple | list,
|
||||
device: torch.device,
|
||||
max_audio_length_seconds: float | None = 15,
|
||||
mode: str = "speaker",
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Load reference audio and resample to 16kHz and 22.05kHz.
|
||||
|
||||
Accepts either a file path (str) or a pre-loaded (wav_list, sr) tuple
|
||||
from the serving layer.
|
||||
|
||||
``mode`` mirrors official IndexTTS2 v2:
|
||||
- speaker: librosa default path first normalizes to 22.05kHz, truncates,
|
||||
then derives the 16kHz wav2vec/CAMPPlus input from that 22.05kHz signal.
|
||||
- emotion: librosa loads directly at 16kHz, then truncates.
|
||||
"""
|
||||
if mode not in {"speaker", "emotion"}:
|
||||
raise ValueError("IndexTTS2 audio mode must be 'speaker' or 'emotion'")
|
||||
|
||||
wav, sr = _load_audio_1d(audio_path)
|
||||
# Resample on GPU when available: CPU sinc resampling for awkward ratios
|
||||
# (e.g. 16000->22050, gcd=50) costs hundreds of ms; on GPU it is <5ms.
|
||||
# Downstream consumers move tensors to CPU explicitly where needed.
|
||||
if device.type == "cuda":
|
||||
wav = wav.to(device)
|
||||
if mode == "speaker":
|
||||
wav_22k = _resample(wav, sr, 22050)
|
||||
wav_22k = _truncate_audio(wav_22k, 22050, max_audio_length_seconds)
|
||||
wav_16k = _resample(wav_22k, 22050, 16000)
|
||||
return wav_16k, wav_22k
|
||||
|
||||
wav_16k = _resample(wav, sr, 16000)
|
||||
wav_16k = _truncate_audio(wav_16k, 16000, max_audio_length_seconds)
|
||||
wav_22k = _resample(wav_16k, 16000, 22050)
|
||||
return wav_16k, wav_22k
|
||||
|
||||
|
||||
def _load_audio_1d(audio_path: str | tuple | list) -> tuple[torch.Tensor, int]:
|
||||
if isinstance(audio_path, (list, tuple)) and len(audio_path) == 2:
|
||||
wav_data, sr = audio_path
|
||||
if isinstance(wav_data, np.ndarray):
|
||||
wav = torch.from_numpy(wav_data).float()
|
||||
elif isinstance(wav_data, list):
|
||||
# np.asarray + from_numpy is ~10x faster than torch.tensor(list)
|
||||
# for the long float lists the serving layer passes through msgspec.
|
||||
wav = torch.from_numpy(np.asarray(wav_data, dtype=np.float32))
|
||||
elif isinstance(wav_data, torch.Tensor):
|
||||
wav = wav_data.float()
|
||||
else:
|
||||
raise TypeError(f"Unsupported audio data type: {type(wav_data)}")
|
||||
return _mono_1d(wav), int(sr)
|
||||
|
||||
try:
|
||||
import torchaudio
|
||||
|
||||
wav, sr = torchaudio.load(audio_path)
|
||||
return _mono_1d(wav, channels_first=True), int(sr)
|
||||
except (ImportError, RuntimeError, OSError):
|
||||
pass
|
||||
|
||||
import soundfile as sf
|
||||
|
||||
audio_np, sr = sf.read(audio_path, dtype="float32", always_2d=False)
|
||||
return _mono_1d(torch.from_numpy(audio_np).float(), channels_first=False), int(sr)
|
||||
|
||||
|
||||
def _mono_1d(wav: torch.Tensor, channels_first: bool | None = None) -> torch.Tensor:
|
||||
if wav.ndim == 1:
|
||||
return wav.contiguous()
|
||||
if channels_first is True:
|
||||
return wav.mean(dim=0).contiguous()
|
||||
if channels_first is False:
|
||||
return wav.mean(dim=-1).contiguous()
|
||||
if wav.shape[0] <= 8 and wav.shape[-1] > wav.shape[0]:
|
||||
return wav.mean(dim=0).contiguous()
|
||||
return wav.mean(dim=-1).contiguous()
|
||||
|
||||
|
||||
def _truncate_audio(
|
||||
wav: torch.Tensor,
|
||||
sample_rate: int,
|
||||
max_audio_length_seconds: float | None,
|
||||
) -> torch.Tensor:
|
||||
if max_audio_length_seconds is None:
|
||||
return wav
|
||||
max_audio_samples = int(max_audio_length_seconds * sample_rate)
|
||||
if max_audio_samples > 0 and wav.shape[-1] > max_audio_samples:
|
||||
return wav[..., :max_audio_samples]
|
||||
return wav
|
||||
|
||||
|
||||
def _resample(wav: torch.Tensor, orig_sr: int, target_sr: int) -> torch.Tensor:
|
||||
if orig_sr == target_sr:
|
||||
return wav
|
||||
try:
|
||||
import torchaudio
|
||||
|
||||
return torchaudio.functional.resample(wav, orig_sr, target_sr)
|
||||
except (ImportError, RuntimeError, OSError):
|
||||
return (
|
||||
F.interpolate(
|
||||
wav.unsqueeze(0).unsqueeze(0), scale_factor=target_sr / orig_sr, mode="linear", align_corners=False
|
||||
)
|
||||
.squeeze(0)
|
||||
.squeeze(0)
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Prompt helpers for IndexTTS2 talker prefill."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.preprocess_utils import resolve_model_file
|
||||
from vllm_omni.model_executor.models.indextts2.tokenizer import IndexTTS2Tokenizer
|
||||
|
||||
_CONDITIONING_PREFIX_TOKENS = 34
|
||||
_TEXT_WRAPPER_TOKENS = 2
|
||||
_START_MEL_TOKENS = 1
|
||||
|
||||
|
||||
def _resolve_bpe_model_path(model_id_or_path: str) -> str:
|
||||
path = resolve_model_file(model_id_or_path, "bpe.model")
|
||||
if path is None:
|
||||
raise FileNotFoundError(f"Could not resolve bpe.model for {model_id_or_path!r}")
|
||||
return path
|
||||
|
||||
|
||||
@lru_cache(maxsize=16)
|
||||
def _get_text_tokenizer(model_id_or_path: str) -> IndexTTS2Tokenizer:
|
||||
return IndexTTS2Tokenizer(_resolve_bpe_model_path(model_id_or_path), model_dir=model_id_or_path)
|
||||
|
||||
|
||||
def estimate_indextts2_prefill_prompt_len(model_id_or_path: str, text: str) -> int:
|
||||
"""Return the placeholder prompt length expected by the IndexTTS2 talker.
|
||||
|
||||
The model-side prefill replaces placeholder embeddings with:
|
||||
34 conditioning tokens, text tokens wrapped with start/stop markers, and
|
||||
one start-mel token.
|
||||
"""
|
||||
tokenizer = _get_text_tokenizer(model_id_or_path)
|
||||
text_token_count = len(tokenizer.encode(text, add_special_tokens=False)) + _TEXT_WRAPPER_TOKENS
|
||||
return _CONDITIONING_PREFIX_TOKENS + text_token_count + _START_MEL_TOKENS
|
||||
|
||||
|
||||
def build_indextts2_prefill_prompt_ids(
|
||||
model_id_or_path: str,
|
||||
text: str,
|
||||
*,
|
||||
placeholder_token_id: int = 1,
|
||||
) -> list[int]:
|
||||
return [placeholder_token_id] * estimate_indextts2_prefill_prompt_len(model_id_or_path, text)
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,107 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
def dynamic_range_compression_torch(x, compression=1, clip_val=1e-5):
|
||||
return torch.log(torch.clamp(x, min=clip_val) * compression)
|
||||
|
||||
|
||||
def spectral_normalize_torch(magnitudes):
|
||||
return dynamic_range_compression_torch(magnitudes)
|
||||
|
||||
|
||||
def _hz_to_mel(freqs: np.ndarray | float) -> np.ndarray | float:
|
||||
"""librosa's default Slaney mel scale (htk=False)."""
|
||||
scalar = not hasattr(freqs, "__len__")
|
||||
freqs = np.atleast_1d(np.asarray(freqs, dtype=np.float64))
|
||||
f_min = 0.0
|
||||
f_sp = 200.0 / 3
|
||||
mels = (freqs - f_min) / f_sp
|
||||
|
||||
min_log_hz = 1000.0
|
||||
min_log_mel = (min_log_hz - f_min) / f_sp
|
||||
logstep = np.log(6.4) / 27.0
|
||||
log_t = freqs >= min_log_hz
|
||||
mels[log_t] = min_log_mel + np.log(freqs[log_t] / min_log_hz) / logstep
|
||||
return float(mels[0]) if scalar else mels
|
||||
|
||||
|
||||
def _mel_to_hz(mels: np.ndarray | float) -> np.ndarray | float:
|
||||
"""Inverse of librosa's default Slaney mel scale (htk=False)."""
|
||||
scalar = not hasattr(mels, "__len__")
|
||||
mels = np.atleast_1d(np.asarray(mels, dtype=np.float64))
|
||||
f_min = 0.0
|
||||
f_sp = 200.0 / 3
|
||||
freqs = f_min + f_sp * mels
|
||||
|
||||
min_log_hz = 1000.0
|
||||
min_log_mel = (min_log_hz - f_min) / f_sp
|
||||
logstep = np.log(6.4) / 27.0
|
||||
log_t = mels >= min_log_mel
|
||||
freqs[log_t] = min_log_hz * np.exp(logstep * (mels[log_t] - min_log_mel))
|
||||
return float(freqs[0]) if scalar else freqs
|
||||
|
||||
|
||||
def _mel_filterbank(sr: int, n_fft: int, n_mels: int, fmin: float, fmax: float | None) -> np.ndarray:
|
||||
"""Create a librosa.filters.mel-compatible Slaney filterbank."""
|
||||
if fmax is None:
|
||||
fmax = float(sr) / 2.0
|
||||
mels = np.linspace(_hz_to_mel(fmin), _hz_to_mel(fmax), n_mels + 2)
|
||||
freqs = _mel_to_hz(mels)
|
||||
fft_freqs = np.fft.rfftfreq(n_fft, d=1.0 / sr)
|
||||
|
||||
fdiff = np.diff(freqs)
|
||||
ramps = np.subtract.outer(freqs, fft_freqs)
|
||||
weights = np.zeros((n_mels, len(fft_freqs)), dtype=np.float32)
|
||||
for i in range(n_mels):
|
||||
lower = -ramps[i] / fdiff[i]
|
||||
upper = ramps[i + 2] / fdiff[i + 1]
|
||||
weights[i] = np.maximum(0, np.minimum(lower, upper))
|
||||
|
||||
enorm = 2.0 / (freqs[2 : n_mels + 2] - freqs[:n_mels])
|
||||
weights *= enorm[:, np.newaxis]
|
||||
return weights
|
||||
|
||||
|
||||
mel_basis = {}
|
||||
hann_window = {}
|
||||
|
||||
|
||||
def mel_spectrogram(y, n_fft, num_mels, sampling_rate, hop_size, win_size, fmin, fmax, center=False):
|
||||
global mel_basis, hann_window # pylint: disable=global-statement
|
||||
if f"{sampling_rate}_{fmax}_{y.device}" not in mel_basis:
|
||||
mel = _mel_filterbank(sr=sampling_rate, n_fft=n_fft, n_mels=num_mels, fmin=fmin, fmax=fmax)
|
||||
mel_basis[str(sampling_rate) + "_" + str(fmax) + "_" + str(y.device)] = (
|
||||
torch.from_numpy(mel).float().to(y.device)
|
||||
)
|
||||
hann_window[str(sampling_rate) + "_" + str(y.device)] = torch.hann_window(win_size).to(y.device)
|
||||
|
||||
y = torch.nn.functional.pad(
|
||||
y.unsqueeze(1), (int((n_fft - hop_size) / 2), int((n_fft - hop_size) / 2)), mode="reflect"
|
||||
)
|
||||
y = y.squeeze(1)
|
||||
|
||||
spec = torch.view_as_real(
|
||||
torch.stft(
|
||||
y,
|
||||
n_fft,
|
||||
hop_length=hop_size,
|
||||
win_length=win_size,
|
||||
window=hann_window[str(sampling_rate) + "_" + str(y.device)],
|
||||
center=center,
|
||||
pad_mode="reflect",
|
||||
normalized=False,
|
||||
onesided=True,
|
||||
return_complex=True,
|
||||
)
|
||||
)
|
||||
|
||||
spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9))
|
||||
|
||||
spec = torch.matmul(mel_basis[str(sampling_rate) + "_" + str(fmax) + "_" + str(y.device)], spec)
|
||||
spec = spectral_normalize_torch(spec)
|
||||
|
||||
return spec
|
||||
@@ -0,0 +1,359 @@
|
||||
# Copyright (c) 2024 NVIDIA CORPORATION.
|
||||
# Licensed under the MIT license.
|
||||
|
||||
# Adapted from https://github.com/jik876/hifi-gan under the MIT license.
|
||||
# LICENSE is in incl_licenses directory.
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from huggingface_hub import PyTorchModelHubMixin, hf_hub_download
|
||||
from torch.nn import Conv1d, ConvTranspose1d
|
||||
from torch.nn.utils import remove_weight_norm, weight_norm
|
||||
|
||||
from vllm_omni.model_executor.models.common.alias_free_activation import AliasFreeActivation1d
|
||||
from vllm_omni.model_executor.models.common.snake_activation import Snake, SnakeBeta
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.commons import AttrDict
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers (inlined from env.py / utils.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def load_hparams_from_json(path) -> AttrDict:
|
||||
with open(path) as f:
|
||||
data = f.read()
|
||||
return AttrDict(json.loads(data))
|
||||
|
||||
|
||||
def init_weights(m, mean=0.0, std=0.01):
|
||||
classname = m.__class__.__name__
|
||||
if classname.find("Conv") != -1:
|
||||
m.weight.data.normal_(mean, std)
|
||||
|
||||
|
||||
def get_padding(kernel_size, dilation=1):
|
||||
return int((kernel_size * dilation - dilation) / 2)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# AMP Residual Blocks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class AMPBlock1(torch.nn.Module):
|
||||
"""AMPBlock with Snake/SnakeBeta activations and extra convs2 (dilation=1)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
h: AttrDict,
|
||||
channels: int,
|
||||
kernel_size: int = 3,
|
||||
dilation: tuple = (1, 3, 5),
|
||||
activation: str = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.h = h
|
||||
|
||||
self.convs1 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=d,
|
||||
padding=get_padding(kernel_size, d),
|
||||
)
|
||||
)
|
||||
for d in dilation
|
||||
]
|
||||
)
|
||||
self.convs1.apply(init_weights)
|
||||
|
||||
self.convs2 = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=1,
|
||||
padding=get_padding(kernel_size, 1),
|
||||
)
|
||||
)
|
||||
for _ in range(len(dilation))
|
||||
]
|
||||
)
|
||||
self.convs2.apply(init_weights)
|
||||
|
||||
self.num_layers = len(self.convs1) + len(self.convs2)
|
||||
|
||||
if activation == "snake":
|
||||
self.activations = nn.ModuleList(
|
||||
[
|
||||
AliasFreeActivation1d(activation=Snake(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
]
|
||||
)
|
||||
elif activation == "snakebeta":
|
||||
self.activations = nn.ModuleList(
|
||||
[
|
||||
AliasFreeActivation1d(activation=SnakeBeta(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
]
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'."
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
acts1, acts2 = self.activations[::2], self.activations[1::2]
|
||||
for c1, c2, a1, a2 in zip(self.convs1, self.convs2, acts1, acts2):
|
||||
xt = a1(x)
|
||||
xt = c1(xt)
|
||||
xt = a2(xt)
|
||||
xt = c2(xt)
|
||||
x = xt + x
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for layer in self.convs1:
|
||||
remove_weight_norm(layer)
|
||||
for layer in self.convs2:
|
||||
remove_weight_norm(layer)
|
||||
|
||||
|
||||
class AMPBlock2(torch.nn.Module):
|
||||
"""AMPBlock with Snake/SnakeBeta activations, no extra convs2."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
h: AttrDict,
|
||||
channels: int,
|
||||
kernel_size: int = 3,
|
||||
dilation: tuple = (1, 3, 5),
|
||||
activation: str = None,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.h = h
|
||||
|
||||
self.convs = nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=d,
|
||||
padding=get_padding(kernel_size, d),
|
||||
)
|
||||
)
|
||||
for d in dilation
|
||||
]
|
||||
)
|
||||
self.convs.apply(init_weights)
|
||||
|
||||
self.num_layers = len(self.convs)
|
||||
|
||||
if activation == "snake":
|
||||
self.activations = nn.ModuleList(
|
||||
[
|
||||
AliasFreeActivation1d(activation=Snake(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
]
|
||||
)
|
||||
elif activation == "snakebeta":
|
||||
self.activations = nn.ModuleList(
|
||||
[
|
||||
AliasFreeActivation1d(activation=SnakeBeta(channels, alpha_logscale=h.snake_logscale))
|
||||
for _ in range(self.num_layers)
|
||||
]
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'."
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
for c, a in zip(self.convs, self.activations):
|
||||
xt = a(x)
|
||||
xt = c(xt)
|
||||
x = xt + x
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
for layer in self.convs:
|
||||
remove_weight_norm(layer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# BigVGAN Generator
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class BigVGAN(
|
||||
torch.nn.Module,
|
||||
PyTorchModelHubMixin,
|
||||
library_name="bigvgan",
|
||||
repo_url="https://github.com/NVIDIA/BigVGAN",
|
||||
docs_url="https://github.com/NVIDIA/BigVGAN/blob/main/README.md",
|
||||
pipeline_tag="audio-to-audio",
|
||||
license="mit",
|
||||
tags=["neural-vocoder", "audio-generation", "arxiv:2206.04658"],
|
||||
):
|
||||
"""BigVGAN neural vocoder with anti-aliased periodic activation."""
|
||||
|
||||
def __init__(self, h: AttrDict, use_cuda_kernel: bool = False):
|
||||
super().__init__()
|
||||
self.h = h
|
||||
|
||||
self.num_kernels = len(h.resblock_kernel_sizes)
|
||||
self.num_upsamples = len(h.upsample_rates)
|
||||
|
||||
# Pre-conv
|
||||
self.conv_pre = weight_norm(Conv1d(h.num_mels, h.upsample_initial_channel, 7, 1, padding=3))
|
||||
|
||||
# Define which AMPBlock to use. BigVGAN uses AMPBlock1 as default
|
||||
if h.resblock == "1":
|
||||
resblock_class = AMPBlock1
|
||||
elif h.resblock == "2":
|
||||
resblock_class = AMPBlock2
|
||||
else:
|
||||
raise ValueError(f"Incorrect resblock class specified in hyperparameters. Got {h.resblock}")
|
||||
|
||||
# Transposed conv-based upsamplers. does not apply anti-aliasing
|
||||
self.ups = nn.ModuleList()
|
||||
for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)):
|
||||
self.ups.append(
|
||||
nn.ModuleList(
|
||||
[
|
||||
weight_norm(
|
||||
ConvTranspose1d(
|
||||
h.upsample_initial_channel // (2**i),
|
||||
h.upsample_initial_channel // (2 ** (i + 1)),
|
||||
k,
|
||||
u,
|
||||
padding=(k - u) // 2,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
# Residual blocks using anti-aliased multi-periodicity composition modules (AMP)
|
||||
self.resblocks = nn.ModuleList()
|
||||
for i in range(len(self.ups)):
|
||||
ch = h.upsample_initial_channel // (2 ** (i + 1))
|
||||
for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)):
|
||||
self.resblocks.append(resblock_class(h, ch, k, d, activation=h.activation))
|
||||
|
||||
# Post-conv
|
||||
activation_post = (
|
||||
Snake(ch, alpha_logscale=h.snake_logscale)
|
||||
if h.activation == "snake"
|
||||
else (SnakeBeta(ch, alpha_logscale=h.snake_logscale) if h.activation == "snakebeta" else None)
|
||||
)
|
||||
if activation_post is None:
|
||||
raise NotImplementedError(
|
||||
"activation incorrectly specified. check the config file and look for 'activation'."
|
||||
)
|
||||
|
||||
self.activation_post = AliasFreeActivation1d(activation=activation_post)
|
||||
|
||||
# Whether to use bias for the final conv_post
|
||||
self.use_bias_at_final = h.get("use_bias_at_final", True)
|
||||
self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, padding=3, bias=self.use_bias_at_final))
|
||||
|
||||
# Weight initialization
|
||||
for i in range(len(self.ups)):
|
||||
self.ups[i].apply(init_weights)
|
||||
self.conv_post.apply(init_weights)
|
||||
|
||||
# Final tanh activation
|
||||
self.use_tanh_at_final = h.get("use_tanh_at_final", True)
|
||||
|
||||
def forward(self, x):
|
||||
# Pre-conv
|
||||
x = self.conv_pre(x)
|
||||
|
||||
for i in range(self.num_upsamples):
|
||||
# Upsampling
|
||||
for i_up in range(len(self.ups[i])):
|
||||
x = self.ups[i][i_up](x)
|
||||
# AMP blocks
|
||||
xs = None
|
||||
for j in range(self.num_kernels):
|
||||
if xs is None:
|
||||
xs = self.resblocks[i * self.num_kernels + j](x)
|
||||
else:
|
||||
xs += self.resblocks[i * self.num_kernels + j](x)
|
||||
x = xs / self.num_kernels
|
||||
|
||||
# Post-conv
|
||||
x = self.activation_post(x)
|
||||
x = self.conv_post(x)
|
||||
# Final tanh activation
|
||||
if self.use_tanh_at_final:
|
||||
x = torch.tanh(x)
|
||||
else:
|
||||
x = torch.clamp(x, min=-1.0, max=1.0)
|
||||
|
||||
return x
|
||||
|
||||
def remove_weight_norm(self):
|
||||
try:
|
||||
for up_layer in self.ups:
|
||||
for sub in up_layer:
|
||||
remove_weight_norm(sub)
|
||||
for block in self.resblocks:
|
||||
block.remove_weight_norm()
|
||||
remove_weight_norm(self.conv_pre)
|
||||
remove_weight_norm(self.conv_post)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _from_pretrained(cls, *, model_id: str, map_location: str = "cpu", **kwargs):
|
||||
def _resolve(filename):
|
||||
if os.path.isdir(model_id):
|
||||
return os.path.join(model_id, filename)
|
||||
return hf_hub_download(
|
||||
repo_id=model_id,
|
||||
filename=filename,
|
||||
**{
|
||||
k: kwargs[k]
|
||||
for k in (
|
||||
"revision",
|
||||
"cache_dir",
|
||||
"force_download",
|
||||
"proxies",
|
||||
"resume_download",
|
||||
"token",
|
||||
"local_files_only",
|
||||
)
|
||||
if k in kwargs
|
||||
},
|
||||
)
|
||||
|
||||
h = load_hparams_from_json(_resolve("config.json"))
|
||||
model = cls(h)
|
||||
|
||||
checkpoint_dict = torch.load(_resolve("bigvgan_generator.pt"), map_location=map_location)
|
||||
try:
|
||||
model.load_state_dict(checkpoint_dict["generator"], strict=False)
|
||||
except RuntimeError:
|
||||
model.remove_weight_norm()
|
||||
model.load_state_dict(checkpoint_dict["generator"], strict=False)
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,73 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class AttrDict(dict):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.__dict__ = self
|
||||
|
||||
|
||||
def fused_add_tanh_sigmoid_multiply(input_a, input_b, n_channels):
|
||||
n_channels_int = n_channels if isinstance(n_channels, int) else n_channels[0]
|
||||
in_act = input_a + input_b
|
||||
t_act_part, s_act_part = torch.split(in_act, n_channels_int, dim=1)
|
||||
t_act = torch.tanh(t_act_part)
|
||||
s_act = torch.sigmoid(s_act_part)
|
||||
acts = t_act * s_act
|
||||
return acts
|
||||
|
||||
|
||||
def sequence_mask(length, max_length=None):
|
||||
if max_length is None:
|
||||
max_length = length.max()
|
||||
x = torch.arange(max_length, dtype=length.dtype, device=length.device)
|
||||
return x.unsqueeze(0) < length.unsqueeze(1)
|
||||
|
||||
|
||||
class MyModel(nn.Module):
|
||||
def __init__(self, args, use_emovec=False, use_gpt_latent=False):
|
||||
super().__init__()
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.flow_matching import CFM
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.length_regulator import (
|
||||
InterpolateRegulator,
|
||||
)
|
||||
|
||||
length_regulator = InterpolateRegulator(
|
||||
channels=args.length_regulator.channels,
|
||||
sampling_ratios=args.length_regulator.sampling_ratios,
|
||||
is_discrete=args.length_regulator.is_discrete,
|
||||
in_channels=getattr(args.length_regulator, "in_channels", None),
|
||||
vector_quantize=getattr(args.length_regulator, "vector_quantize", False),
|
||||
codebook_size=args.length_regulator.content_codebook_size,
|
||||
n_codebooks=getattr(args.length_regulator, "n_codebooks", 1),
|
||||
quantizer_dropout=getattr(args.length_regulator, "quantizer_dropout", 0.0),
|
||||
f0_condition=getattr(args.length_regulator, "f0_condition", False),
|
||||
n_f0_bins=getattr(args.length_regulator, "n_f0_bins", 512),
|
||||
)
|
||||
|
||||
if use_gpt_latent:
|
||||
self.models = nn.ModuleDict(
|
||||
{
|
||||
"cfm": CFM(args),
|
||||
"length_regulator": length_regulator,
|
||||
"gpt_layer": torch.nn.Sequential(
|
||||
torch.nn.Linear(1280, 256),
|
||||
torch.nn.Linear(256, 128),
|
||||
torch.nn.Linear(128, 1024),
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
self.models = nn.ModuleDict({"cfm": CFM(args), "length_regulator": length_regulator})
|
||||
|
||||
def forward_gpt(self, x):
|
||||
x = self.models["gpt_layer"](x)
|
||||
return x
|
||||
|
||||
def enable_torch_compile(self):
|
||||
if "cfm" in self.models:
|
||||
self.models["cfm"].enable_torch_compile()
|
||||
@@ -0,0 +1,251 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import math
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.commons import sequence_mask
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.gpt_fast.model import ModelArgs, Transformer
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.wavenet import WN
|
||||
|
||||
|
||||
def modulate(x, shift, scale):
|
||||
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
|
||||
|
||||
|
||||
#################################################################################
|
||||
# Embedding Layers for Timesteps and Class Labels #
|
||||
#################################################################################
|
||||
|
||||
|
||||
class TimestepEmbedder(nn.Module):
|
||||
"""
|
||||
Embeds scalar timesteps into vector representations.
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size, frequency_embedding_size=256):
|
||||
super().__init__()
|
||||
self.mlp = nn.Sequential(
|
||||
nn.Linear(frequency_embedding_size, hidden_size, bias=True),
|
||||
nn.SiLU(),
|
||||
nn.Linear(hidden_size, hidden_size, bias=True),
|
||||
)
|
||||
self.frequency_embedding_size = frequency_embedding_size
|
||||
self.max_period = 10000
|
||||
self.scale = 1000
|
||||
|
||||
half = frequency_embedding_size // 2
|
||||
freqs = torch.exp(-math.log(self.max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half)
|
||||
self.register_buffer("freqs", freqs)
|
||||
|
||||
def timestep_embedding(self, t):
|
||||
"""
|
||||
Create sinusoidal timestep embeddings.
|
||||
:param t: a 1-D Tensor of N indices, one per batch element.
|
||||
These may be fractional.
|
||||
:param dim: the dimension of the output.
|
||||
:param max_period: controls the minimum frequency of the embeddings.
|
||||
:return: an (N, D) Tensor of positional embeddings.
|
||||
"""
|
||||
# https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
|
||||
|
||||
args = self.scale * t[:, None].float() * self.freqs[None]
|
||||
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
|
||||
if self.frequency_embedding_size % 2:
|
||||
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
|
||||
return embedding
|
||||
|
||||
def forward(self, t):
|
||||
t_freq = self.timestep_embedding(t)
|
||||
t_emb = self.mlp(t_freq.to(self.mlp[0].weight.dtype))
|
||||
return t_emb
|
||||
|
||||
|
||||
class FinalLayer(nn.Module):
|
||||
"""
|
||||
The final layer of DiT.
|
||||
"""
|
||||
|
||||
def __init__(self, hidden_size, patch_size, out_channels):
|
||||
super().__init__()
|
||||
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
||||
self.linear = weight_norm(nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True))
|
||||
self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True))
|
||||
|
||||
def forward(self, x, c):
|
||||
shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
|
||||
x = modulate(self.norm_final(x), shift, scale)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class DiT(torch.nn.Module):
|
||||
def __init__(self, args):
|
||||
super().__init__()
|
||||
self.time_as_token = getattr(args.DiT, "time_as_token", False)
|
||||
self.style_as_token = getattr(args.DiT, "style_as_token", False)
|
||||
self.uvit_skip_connection = getattr(args.DiT, "uvit_skip_connection", False)
|
||||
model_args = ModelArgs(
|
||||
block_size=16384, # args.DiT.block_size,
|
||||
n_layer=args.DiT.depth,
|
||||
n_head=args.DiT.num_heads,
|
||||
dim=args.DiT.hidden_dim,
|
||||
head_dim=args.DiT.hidden_dim // args.DiT.num_heads,
|
||||
vocab_size=1024,
|
||||
uvit_skip_connection=self.uvit_skip_connection,
|
||||
time_as_token=self.time_as_token,
|
||||
)
|
||||
self.transformer = Transformer(model_args)
|
||||
self.in_channels = args.DiT.in_channels
|
||||
self.out_channels = args.DiT.in_channels
|
||||
self.num_heads = args.DiT.num_heads
|
||||
|
||||
self.x_embedder = weight_norm(nn.Linear(args.DiT.in_channels, args.DiT.hidden_dim, bias=True))
|
||||
|
||||
self.content_type = args.DiT.content_type # 'discrete' or 'continuous'
|
||||
self.content_codebook_size = args.DiT.content_codebook_size # for discrete content
|
||||
self.content_dim = args.DiT.content_dim # for continuous content
|
||||
self.cond_projection = nn.Linear(args.DiT.content_dim, args.DiT.hidden_dim, bias=True) # continuous content
|
||||
|
||||
self.is_causal = args.DiT.is_causal
|
||||
|
||||
self.t_embedder = TimestepEmbedder(args.DiT.hidden_dim)
|
||||
|
||||
input_pos = torch.arange(16384)
|
||||
self.register_buffer("input_pos", input_pos)
|
||||
|
||||
self.final_layer_type = args.DiT.final_layer_type # mlp or wavenet
|
||||
if self.final_layer_type == "wavenet":
|
||||
self.t_embedder2 = TimestepEmbedder(args.wavenet.hidden_dim)
|
||||
self.conv1 = nn.Linear(args.DiT.hidden_dim, args.wavenet.hidden_dim)
|
||||
self.conv2 = nn.Conv1d(args.wavenet.hidden_dim, args.DiT.in_channels, 1)
|
||||
self.wavenet = WN(
|
||||
hidden_channels=args.wavenet.hidden_dim,
|
||||
kernel_size=args.wavenet.kernel_size,
|
||||
dilation_rate=args.wavenet.dilation_rate,
|
||||
n_layers=args.wavenet.num_layers,
|
||||
gin_channels=args.wavenet.hidden_dim,
|
||||
p_dropout=args.wavenet.p_dropout,
|
||||
causal=False,
|
||||
)
|
||||
self.final_layer = FinalLayer(args.wavenet.hidden_dim, 1, args.wavenet.hidden_dim)
|
||||
self.res_projection = nn.Linear(
|
||||
args.DiT.hidden_dim, args.wavenet.hidden_dim
|
||||
) # residual connection from transformer output to final output
|
||||
self.wavenet_style_condition = args.wavenet.style_condition
|
||||
assert args.DiT.style_condition == args.wavenet.style_condition
|
||||
else:
|
||||
self.final_mlp = nn.Sequential(
|
||||
nn.Linear(args.DiT.hidden_dim, args.DiT.hidden_dim),
|
||||
nn.SiLU(),
|
||||
nn.Linear(args.DiT.hidden_dim, args.DiT.in_channels),
|
||||
)
|
||||
self.transformer_style_condition = args.DiT.style_condition
|
||||
|
||||
self.class_dropout_prob = args.DiT.class_dropout_prob
|
||||
self.content_mask_embedder = nn.Embedding(1, args.DiT.hidden_dim)
|
||||
|
||||
self.long_skip_connection = args.DiT.long_skip_connection
|
||||
self.skip_linear = nn.Linear(args.DiT.hidden_dim + args.DiT.in_channels, args.DiT.hidden_dim)
|
||||
|
||||
self.cond_x_merge_linear = nn.Linear(
|
||||
args.DiT.hidden_dim
|
||||
+ args.DiT.in_channels * 2
|
||||
+ args.style_encoder.dim * self.transformer_style_condition * (not self.style_as_token),
|
||||
args.DiT.hidden_dim,
|
||||
)
|
||||
if self.style_as_token:
|
||||
self.style_in = nn.Linear(args.style_encoder.dim, args.DiT.hidden_dim)
|
||||
|
||||
def setup_caches(self, max_batch_size, max_seq_length):
|
||||
self.transformer.setup_caches(max_batch_size, max_seq_length, use_kv_cache=False)
|
||||
|
||||
def forward(self, x, prompt_x, x_lens, t, style, cond, mask_content=False, pre_mask=None):
|
||||
"""
|
||||
x (torch.Tensor): random noise
|
||||
prompt_x (torch.Tensor): reference mel + zero mel
|
||||
shape: (batch_size, 80, 795+1068)
|
||||
x_lens (torch.Tensor): mel frames output
|
||||
shape: (batch_size, mel_timesteps)
|
||||
t (torch.Tensor): radshape:
|
||||
shape: (batch_size)
|
||||
style (torch.Tensor): reference global style
|
||||
shape: (batch_size, 192)
|
||||
cond (torch.Tensor): semantic info of reference audio and altered audio
|
||||
shape: (batch_size, mel_timesteps(795+1069), 512)
|
||||
pre_mask (torch.Tensor | None): precomputed (x_mask, x_mask_expanded) to
|
||||
avoid recomputation across ODE steps. Tuple of (x_mask, x_mask_expanded).
|
||||
"""
|
||||
class_dropout = False
|
||||
if self.training and torch.rand(1) < self.class_dropout_prob:
|
||||
class_dropout = True
|
||||
if not self.training and mask_content:
|
||||
class_dropout = True
|
||||
cond_in_module = self.cond_projection
|
||||
|
||||
B, _, T = x.size()
|
||||
|
||||
t1 = self.t_embedder(t) # (N, D) # t1 [2, 512]
|
||||
cond = cond_in_module(cond) # cond [2,1863,512]->[2,1863,512]
|
||||
|
||||
x = x.transpose(1, 2) # [2,1863,80]
|
||||
prompt_x = prompt_x.transpose(1, 2) # [2,1863,80]
|
||||
|
||||
x_in = torch.cat([x, prompt_x, cond], dim=-1) # 80+80+512=672 [2, 1863, 672]
|
||||
|
||||
if self.transformer_style_condition and not self.style_as_token: # True and True
|
||||
x_in = torch.cat([x_in, style[:, None, :].expand(-1, T, -1)], dim=-1) # [2, 1863, 864]
|
||||
|
||||
if class_dropout: # False
|
||||
x_in[..., self.in_channels :] = x_in[..., self.in_channels :] * 0 # 80维后全置为0
|
||||
|
||||
x_in = self.cond_x_merge_linear(x_in) # (N, T, D) [2, 1863, 512]
|
||||
|
||||
if self.style_as_token: # False
|
||||
style = self.style_in(style)
|
||||
style = torch.zeros_like(style) if class_dropout else style
|
||||
x_in = torch.cat([style.unsqueeze(1), x_in], dim=1)
|
||||
|
||||
if self.time_as_token: # False
|
||||
x_in = torch.cat([t1.unsqueeze(1), x_in], dim=1)
|
||||
|
||||
if pre_mask is not None:
|
||||
x_mask, x_mask_expanded = pre_mask
|
||||
else:
|
||||
x_mask = (
|
||||
sequence_mask(x_lens + self.style_as_token + self.time_as_token, max_length=x_in.size(1))
|
||||
.to(x.device)
|
||||
.unsqueeze(1)
|
||||
)
|
||||
input_pos = self.input_pos[: x_in.size(1)]
|
||||
x_mask_expanded = x_mask[:, None, :].expand(-1, -1, x_in.size(1), -1) if not self.is_causal else None
|
||||
|
||||
input_pos = self.input_pos[: x_in.size(1)] # (T,) range(0,1863)
|
||||
# Route through per-shape CUDA graph runner if enabled.
|
||||
graph_runner = getattr(self, "_cuda_graph_runner", None)
|
||||
if graph_runner is not None:
|
||||
x_res = graph_runner(x_in, t1.unsqueeze(1), input_pos, x_mask_expanded)
|
||||
else:
|
||||
x_res = self.transformer(x_in, t1.unsqueeze(1), input_pos, x_mask_expanded) # [2, 1863, 512]
|
||||
x_res = x_res[:, 1:] if self.time_as_token else x_res
|
||||
x_res = x_res[:, 1:] if self.style_as_token else x_res
|
||||
|
||||
if self.long_skip_connection: # True
|
||||
x_res = self.skip_linear(torch.cat([x_res, x], dim=-1))
|
||||
if self.final_layer_type == "wavenet":
|
||||
x = self.conv1(x_res)
|
||||
x = x.transpose(1, 2)
|
||||
t2 = self.t_embedder2(t)
|
||||
x = self.wavenet(x, x_mask, g=t2.unsqueeze(2)).transpose(1, 2) + self.res_projection(
|
||||
x_res
|
||||
) # long residual connection
|
||||
x = self.final_layer(x, t1).transpose(1, 2)
|
||||
x = self.conv2(x)
|
||||
else:
|
||||
x = self.final_mlp(x_res)
|
||||
x = x.transpose(1, 2)
|
||||
# x [2,80,1863]
|
||||
return x
|
||||
@@ -0,0 +1,122 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
"""Convolutional layers wrappers and utilities."""
|
||||
|
||||
import math
|
||||
import typing as tp
|
||||
import warnings
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
from torch.nn import functional as F
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
|
||||
def apply_parametrization_norm(module: nn.Module, norm: str = "none") -> nn.Module:
|
||||
if norm == "weight_norm":
|
||||
return weight_norm(module)
|
||||
return module
|
||||
|
||||
|
||||
def get_norm_module(module: nn.Module, causal: bool = False, norm: str = "none", **norm_kwargs) -> nn.Module:
|
||||
return nn.Identity()
|
||||
|
||||
|
||||
def get_extra_padding_for_conv1d(x: torch.Tensor, kernel_size: int, stride: int, padding_total: int = 0) -> int:
|
||||
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 = "zero", value: float = 0.0):
|
||||
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 NormConv1d(nn.Module):
|
||||
def __init__(self, *args, causal: bool = False, norm: str = "none", norm_kwargs: dict[str, tp.Any] = {}, **kwargs):
|
||||
super().__init__()
|
||||
self.conv = apply_parametrization_norm(nn.Conv1d(*args, **kwargs), norm)
|
||||
self.norm = get_norm_module(self.conv, causal, norm, **norm_kwargs)
|
||||
self.norm_type = norm
|
||||
|
||||
def forward(self, x):
|
||||
x = self.conv(x)
|
||||
x = self.norm(x)
|
||||
return x
|
||||
|
||||
|
||||
class SConv1d(nn.Module):
|
||||
"""Conv1d with builtin handling of asymmetric or causal padding and normalization."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: int,
|
||||
kernel_size: int,
|
||||
stride: int = 1,
|
||||
dilation: int = 1,
|
||||
groups: int = 1,
|
||||
bias: bool = True,
|
||||
causal: bool = False,
|
||||
norm: str = "none",
|
||||
norm_kwargs: dict[str, tp.Any] = {},
|
||||
pad_mode: str = "reflect",
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
if stride > 1 and dilation > 1:
|
||||
warnings.warn(
|
||||
"SConv1d has been initialized with stride > 1 and dilation > 1"
|
||||
f" (kernel_size={kernel_size} stride={stride}, dilation={dilation})."
|
||||
)
|
||||
self.conv = NormConv1d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride,
|
||||
dilation=dilation,
|
||||
groups=groups,
|
||||
bias=bias,
|
||||
causal=causal,
|
||||
norm=norm,
|
||||
norm_kwargs=norm_kwargs,
|
||||
)
|
||||
self.causal = causal
|
||||
self.pad_mode = pad_mode
|
||||
|
||||
def forward(self, x):
|
||||
B, C, T = x.shape
|
||||
kernel_size = self.conv.conv.kernel_size[0]
|
||||
stride = self.conv.conv.stride[0]
|
||||
dilation = self.conv.conv.dilation[0]
|
||||
kernel_size = (kernel_size - 1) * dilation + 1
|
||||
padding_total = kernel_size - stride
|
||||
extra_padding = get_extra_padding_for_conv1d(x, kernel_size, stride, padding_total)
|
||||
if self.causal:
|
||||
x = pad1d(x, (padding_total, extra_padding), mode=self.pad_mode)
|
||||
else:
|
||||
padding_right = padding_total // 2
|
||||
padding_left = padding_total - padding_right
|
||||
x = pad1d(x, (padding_left, padding_right + extra_padding), mode=self.pad_mode)
|
||||
return self.conv(x)
|
||||
@@ -0,0 +1,201 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from abc import ABC
|
||||
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.diffusion_transformer import DiT
|
||||
|
||||
|
||||
class BASECFM(torch.nn.Module, ABC):
|
||||
def __init__(
|
||||
self,
|
||||
args,
|
||||
):
|
||||
super().__init__()
|
||||
self.sigma_min = 1e-6
|
||||
|
||||
self.estimator = None
|
||||
|
||||
self.in_channels = args.DiT.in_channels
|
||||
|
||||
if hasattr(args.DiT, "zero_prompt_speech_token"):
|
||||
self.zero_prompt_speech_token = args.DiT.zero_prompt_speech_token
|
||||
else:
|
||||
self.zero_prompt_speech_token = False
|
||||
|
||||
@torch.inference_mode()
|
||||
def inference(self, mu, x_lens, prompt, style, f0, n_timesteps, temperature=1.0, inference_cfg_rate=0.5):
|
||||
"""Forward diffusion
|
||||
|
||||
Args:
|
||||
mu (torch.Tensor): semantic info of reference audio and altered audio
|
||||
shape: (batch_size, mel_timesteps(795+1069), 512)
|
||||
x_lens (torch.Tensor): mel frames output
|
||||
shape: (batch_size, mel_timesteps)
|
||||
prompt (torch.Tensor): reference mel
|
||||
shape: (batch_size, 80, 795)
|
||||
style (torch.Tensor): reference global style
|
||||
shape: (batch_size, 192)
|
||||
f0 (None): unused, reserved for f0 conditioning
|
||||
n_timesteps (int): number of diffusion steps
|
||||
temperature (float, optional): temperature for scaling noise. Defaults to 1.0.
|
||||
|
||||
Returns:
|
||||
sample (torch.Tensor): generated mel-spectrogram
|
||||
shape: (batch_size, 80, mel_timesteps)
|
||||
"""
|
||||
B, T = mu.size(0), mu.size(1)
|
||||
z = torch.randn([B, self.in_channels, T], device=mu.device, dtype=mu.dtype) * temperature
|
||||
t_span = torch.linspace(0, 1, n_timesteps + 1, device=mu.device, dtype=mu.dtype)
|
||||
return self.solve_euler(z, x_lens, prompt, mu, style, f0, t_span, inference_cfg_rate)
|
||||
|
||||
def solve_euler(self, x, x_lens, prompt, mu, style, f0, t_span, inference_cfg_rate=0.5):
|
||||
"""
|
||||
Fixed euler solver for ODEs.
|
||||
Args:
|
||||
x (torch.Tensor): random noise
|
||||
t_span (torch.Tensor): n_timesteps interpolated
|
||||
shape: (n_timesteps + 1,)
|
||||
mu (torch.Tensor): semantic info of reference audio and altered audio
|
||||
shape: (batch_size, mel_timesteps(795+1069), 512)
|
||||
x_lens (torch.Tensor): mel frames output
|
||||
shape: (batch_size, mel_timesteps)
|
||||
prompt (torch.Tensor): reference mel
|
||||
shape: (batch_size, 80, 795)
|
||||
style (torch.Tensor): reference global style
|
||||
shape: (batch_size, 192)
|
||||
"""
|
||||
t = t_span[0]
|
||||
|
||||
# Optional bf16 autocast for the DiT estimator (set by the decoder via
|
||||
# `estimator_autocast_dtype`). The Euler solver state stays float32 —
|
||||
# only the estimator forward runs in reduced precision, which restores
|
||||
# flash-attention eligibility and halves GEMM cost.
|
||||
autocast_dtype = getattr(self, "estimator_autocast_dtype", None)
|
||||
|
||||
def _run_estimator(*args, **kw):
|
||||
if autocast_dtype is None:
|
||||
return self.estimator(*args, **kw)
|
||||
with torch.autocast(x.device.type, dtype=autocast_dtype):
|
||||
out = self.estimator(*args, **kw)
|
||||
return out.float()
|
||||
|
||||
prompt_len = prompt.size(-1)
|
||||
prompt_x = torch.zeros_like(x)
|
||||
prompt_x[..., :prompt_len] = prompt[..., :prompt_len]
|
||||
x[..., :prompt_len] = 0
|
||||
if self.zero_prompt_speech_token:
|
||||
mu[..., :prompt_len] = 0
|
||||
|
||||
# Precompute mask once — T and x_lens are constant across all ODE steps.
|
||||
# x_mask [B, 1, T] broadcasts to [2B, 1, T] when CFG doubles batch.
|
||||
pre_mask = self._precompute_mask(x, x_lens)
|
||||
|
||||
if inference_cfg_rate > 0:
|
||||
batch = x.shape[0]
|
||||
cfg_prompt_x = prompt_x.new_zeros((2 * batch, *prompt_x.shape[1:]))
|
||||
cfg_prompt_x[:batch].copy_(prompt_x)
|
||||
cfg_style = style.new_zeros((2 * batch, *style.shape[1:]))
|
||||
cfg_style[:batch].copy_(style)
|
||||
cfg_mu = mu.new_zeros((2 * batch, *mu.shape[1:]))
|
||||
cfg_mu[:batch].copy_(mu)
|
||||
cfg_x = x.new_empty((2 * batch, *x.shape[1:]))
|
||||
cfg_t = x.new_empty((2 * batch,))
|
||||
cfg_pre_mask = self._repeat_pre_mask_for_cfg(pre_mask)
|
||||
|
||||
for step in range(1, len(t_span)):
|
||||
dt = t_span[step] - t_span[step - 1]
|
||||
if inference_cfg_rate > 0:
|
||||
cfg_x[:batch].copy_(x)
|
||||
cfg_x[batch:].copy_(x)
|
||||
cfg_t.fill_(t)
|
||||
|
||||
stacked_dphi_dt = _run_estimator(
|
||||
cfg_x,
|
||||
cfg_prompt_x,
|
||||
x_lens,
|
||||
cfg_t,
|
||||
cfg_style,
|
||||
cfg_mu,
|
||||
pre_mask=cfg_pre_mask,
|
||||
)
|
||||
|
||||
dphi_dt, cfg_dphi_dt = stacked_dphi_dt.chunk(2, dim=0)
|
||||
dphi_dt = (1.0 + inference_cfg_rate) * dphi_dt - inference_cfg_rate * cfg_dphi_dt
|
||||
else:
|
||||
dphi_dt = _run_estimator(x, prompt_x, x_lens, t.unsqueeze(0), style, mu, pre_mask=pre_mask)
|
||||
|
||||
x = x + dt * dphi_dt
|
||||
t = t + dt
|
||||
x[:, :, :prompt_len] = 0
|
||||
|
||||
return x
|
||||
|
||||
@staticmethod
|
||||
def _repeat_pre_mask_for_cfg(pre_mask):
|
||||
"""Duplicate precomputed masks when CFG doubles the batch.
|
||||
|
||||
The original IndexTTS2 inference path used B=1, where a two-element
|
||||
timestep tensor happened to match CFG. Stage-1 batching makes the
|
||||
estimator input batch 2B, so all batch-indexed masks must be repeated
|
||||
consistently with ``torch.cat([cond, uncond], dim=0)``.
|
||||
"""
|
||||
if not isinstance(pre_mask, tuple):
|
||||
return pre_mask
|
||||
repeated = []
|
||||
for mask in pre_mask:
|
||||
if isinstance(mask, torch.Tensor):
|
||||
repeated.append(torch.cat([mask, mask], dim=0))
|
||||
else:
|
||||
repeated.append(mask)
|
||||
return tuple(repeated)
|
||||
|
||||
def _precompute_mask(self, x: torch.Tensor, x_lens: torch.Tensor):
|
||||
"""Precompute padding mask for DiT — reused across all ODE steps."""
|
||||
from .commons import sequence_mask
|
||||
|
||||
estimator = self._eager_estimator if hasattr(self, "_eager_estimator") else self.estimator
|
||||
T = x.size(2)
|
||||
style_offset = getattr(estimator, "style_as_token", False)
|
||||
time_offset = getattr(estimator, "time_as_token", False)
|
||||
T_in = T + int(style_offset) + int(time_offset)
|
||||
is_causal = getattr(estimator, "is_causal", False)
|
||||
|
||||
x_mask = sequence_mask(x_lens + int(style_offset) + int(time_offset), max_length=T_in).to(x.device).unsqueeze(1)
|
||||
if is_causal:
|
||||
return (x_mask, None)
|
||||
x_mask_expanded = x_mask[:, None, :].expand(-1, -1, T_in, -1)
|
||||
return (x_mask, x_mask_expanded)
|
||||
|
||||
|
||||
class CFM(BASECFM):
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
if args.dit_type == "DiT":
|
||||
self.estimator = DiT(args)
|
||||
object.__setattr__(self, "_eager_estimator", self.estimator)
|
||||
else:
|
||||
raise NotImplementedError(f"Unknown diffusion type {args.dit_type}")
|
||||
self._compiled = False
|
||||
|
||||
def enable_torch_compile(self):
|
||||
"""Enable torch.compile optimization for the estimator model.
|
||||
|
||||
This method applies torch.compile to the estimator (DiT model) for significant
|
||||
performance improvements during inference. It also configures distributed
|
||||
training optimizations if applicable.
|
||||
"""
|
||||
if torch.distributed.is_initialized():
|
||||
torch._inductor.config.reorder_for_compute_comm_overlap = True
|
||||
self.estimator = torch.compile(
|
||||
self._eager_estimator,
|
||||
fullgraph=True,
|
||||
dynamic=True,
|
||||
)
|
||||
self._compiled = True
|
||||
|
||||
def disable_torch_compile(self):
|
||||
self.estimator = self._eager_estimator
|
||||
self._compiled = False
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,303 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
# All rights reserved.
|
||||
|
||||
# This source code is licensed under the license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch import Tensor
|
||||
from torch.nn import functional as F
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.layer import Attention as DiffusionAttention
|
||||
from vllm_omni.diffusion.layers.norm import RMSNorm
|
||||
|
||||
|
||||
def find_multiple(n: int, k: int) -> int:
|
||||
if n % k == 0:
|
||||
return n
|
||||
return n + k - (n % k)
|
||||
|
||||
|
||||
class AdaptiveLayerNorm(nn.Module):
|
||||
r"""Adaptive Layer Normalization"""
|
||||
|
||||
def __init__(self, d_model, norm) -> None:
|
||||
super().__init__()
|
||||
self.project_layer = nn.Linear(d_model, 2 * d_model)
|
||||
self.norm = norm
|
||||
self.d_model = d_model
|
||||
self.eps = getattr(self.norm, "eps", getattr(self.norm, "variance_epsilon", 1e-5))
|
||||
|
||||
def forward(self, input: Tensor, embedding: Tensor = None) -> Tensor:
|
||||
if embedding is None:
|
||||
return self.norm(input)
|
||||
weight, bias = torch.split(
|
||||
self.project_layer(embedding),
|
||||
split_size_or_sections=self.d_model,
|
||||
dim=-1,
|
||||
)
|
||||
return weight * self.norm(input) + bias
|
||||
|
||||
|
||||
@dataclass
|
||||
class ModelArgs:
|
||||
block_size: int = 2048
|
||||
vocab_size: int = 32000
|
||||
n_layer: int = 32
|
||||
n_head: int = 32
|
||||
dim: int = 4096
|
||||
intermediate_size: int = None
|
||||
n_local_heads: int = -1
|
||||
head_dim: int = 64
|
||||
rope_base: float = 10000
|
||||
norm_eps: float = 1e-5
|
||||
has_cross_attention: bool = False
|
||||
context_dim: int = 0
|
||||
uvit_skip_connection: bool = False
|
||||
time_as_token: bool = False
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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 = AdaptiveLayerNorm(config.dim, RMSNorm(config.dim, eps=config.norm_eps))
|
||||
|
||||
self.freqs_cis: Tensor | None = None
|
||||
self.mask_cache: Tensor | None = None
|
||||
self.max_batch_size = -1
|
||||
self.max_seq_length = -1
|
||||
|
||||
def setup_caches(self, max_batch_size, max_seq_length, use_kv_cache=False):
|
||||
if self.max_seq_length >= max_seq_length and self.max_batch_size >= max_batch_size:
|
||||
return
|
||||
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.project_layer.weight.dtype
|
||||
device = self.norm.project_layer.weight.device
|
||||
|
||||
self.freqs_cis = precompute_freqs_cis(
|
||||
self.config.block_size, self.config.head_dim, self.config.rope_base, dtype
|
||||
).to(device)
|
||||
self.causal_mask = torch.tril(torch.ones(self.max_seq_length, self.max_seq_length, dtype=torch.bool)).to(device)
|
||||
self.use_kv_cache = use_kv_cache
|
||||
self.uvit_skip_connection = self.config.uvit_skip_connection
|
||||
if self.uvit_skip_connection:
|
||||
self.layers_emit_skip = [i for i in range(self.config.n_layer) if i < self.config.n_layer // 2]
|
||||
self.layers_receive_skip = [i for i in range(self.config.n_layer) if i > self.config.n_layer // 2]
|
||||
else:
|
||||
self.layers_emit_skip = []
|
||||
self.layers_receive_skip = []
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
c: Tensor,
|
||||
input_pos: Tensor | None = None,
|
||||
mask: Tensor | None = None,
|
||||
context: Tensor | None = None,
|
||||
context_input_pos: Tensor | None = None,
|
||||
cross_attention_mask: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
assert self.freqs_cis is not None, "Caches must be initialized first"
|
||||
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]
|
||||
else:
|
||||
mask = self.causal_mask[None, None, input_pos]
|
||||
mask = mask[..., input_pos]
|
||||
freqs_cis = self.freqs_cis[input_pos]
|
||||
if context is not None:
|
||||
context_freqs_cis = self.freqs_cis[context_input_pos]
|
||||
else:
|
||||
context_freqs_cis = None
|
||||
skip_in_x_list = []
|
||||
for i, layer in enumerate(self.layers):
|
||||
if self.uvit_skip_connection and i in self.layers_receive_skip:
|
||||
skip_in_x = skip_in_x_list.pop(-1)
|
||||
else:
|
||||
skip_in_x = None
|
||||
x = layer(x, c, input_pos, freqs_cis, mask, context, context_freqs_cis, cross_attention_mask, skip_in_x)
|
||||
if self.uvit_skip_connection and i in self.layers_emit_skip:
|
||||
skip_in_x_list.append(x)
|
||||
x = self.norm(x, c)
|
||||
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 = AdaptiveLayerNorm(config.dim, RMSNorm(config.dim, eps=config.norm_eps))
|
||||
self.attention_norm = AdaptiveLayerNorm(config.dim, RMSNorm(config.dim, eps=config.norm_eps))
|
||||
|
||||
if config.uvit_skip_connection:
|
||||
self.skip_in_linear = nn.Linear(config.dim * 2, config.dim)
|
||||
self.uvit_skip_connection = True
|
||||
else:
|
||||
self.uvit_skip_connection = False
|
||||
|
||||
self.time_as_token = config.time_as_token
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
c: Tensor,
|
||||
input_pos: Tensor,
|
||||
freqs_cis: Tensor,
|
||||
mask: Tensor,
|
||||
context: Tensor | None = None,
|
||||
context_freqs_cis: Tensor | None = None,
|
||||
cross_attention_mask: Tensor | None = None,
|
||||
skip_in_x: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
c = None if self.time_as_token else c
|
||||
if self.uvit_skip_connection and skip_in_x is not None:
|
||||
x = self.skip_in_linear(torch.cat([x, skip_in_x], dim=-1))
|
||||
h = x + self.attention(self.attention_norm(x, c), freqs_cis, mask, input_pos)
|
||||
out = h + self.feed_forward(self.ffn_norm(h, c))
|
||||
return out
|
||||
|
||||
|
||||
class Attention(nn.Module):
|
||||
def __init__(self, config: ModelArgs, is_cross_attention: bool = False):
|
||||
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
|
||||
if is_cross_attention:
|
||||
self.wq = nn.Linear(config.dim, config.n_head * config.head_dim, bias=False)
|
||||
self.wkv = nn.Linear(config.context_dim, 2 * config.n_local_heads * config.head_dim, bias=False)
|
||||
else:
|
||||
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
|
||||
|
||||
# vLLM-Omni diffusion attention backend.
|
||||
# Auto-selects FlashAttention / SageAttention / SDPA based on
|
||||
# DIFFUSION_ATTENTION_BACKEND env var or platform default.
|
||||
self.diff_attn = DiffusionAttention(
|
||||
num_heads=config.n_head,
|
||||
head_size=config.head_dim,
|
||||
softmax_scale=1.0 / math.sqrt(config.head_dim),
|
||||
causal=config.time_as_token,
|
||||
num_kv_heads=config.n_local_heads,
|
||||
role="cross" if is_cross_attention else "self",
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
x: Tensor,
|
||||
freqs_cis: Tensor,
|
||||
mask: Tensor,
|
||||
input_pos: Tensor | None = None,
|
||||
context: Tensor | None = None,
|
||||
context_freqs_cis: Tensor | None = None,
|
||||
) -> Tensor:
|
||||
bsz, seqlen, _ = x.shape
|
||||
|
||||
kv_size = self.n_local_heads * self.head_dim
|
||||
if context is None:
|
||||
q, k, v = self.wqkv(x).split([kv_size, kv_size, kv_size], dim=-1)
|
||||
context_seqlen = seqlen
|
||||
else:
|
||||
q = self.wq(x)
|
||||
k, v = self.wkv(context).split([kv_size, kv_size], dim=-1)
|
||||
context_seqlen = context.shape[1]
|
||||
|
||||
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)
|
||||
|
||||
q = apply_rotary_emb(q, freqs_cis)
|
||||
k = apply_rotary_emb(k, context_freqs_cis if context_freqs_cis is not None else freqs_cis)
|
||||
|
||||
# vLLM-Omni diffusion attention backend.
|
||||
# Expects (B, S, H, D) layout — skip the transpose.
|
||||
# GQA handled internally via num_kv_heads init param.
|
||||
# KV cache is not used by the S2Mel DiT, so skip it here.
|
||||
#
|
||||
# Mask format: diffusion_transformer.py passes a 4D padding mask
|
||||
# (B, 1, T, T). The FlashAttention backend requires 2D (B, T).
|
||||
# Squeeze to 2D for universal compatibility.
|
||||
#
|
||||
# Fast path: when the caller guarantees no padding (x_lens == T,
|
||||
# the S2Mel CFM always runs full-length sequences), the mask is
|
||||
# all-True and dense attention is mathematically identical. Skip
|
||||
# building AttentionMetadata entirely — this avoids the backend's
|
||||
# `torch.any(~mask)` D2H sync per ODE step and keeps the forward
|
||||
# CUDA-graph capturable.
|
||||
if getattr(self, "_assume_full_mask", False):
|
||||
attn_metadata = None
|
||||
else:
|
||||
attn_mask = mask
|
||||
if attn_mask is not None and attn_mask.dim() == 4:
|
||||
# (B, 1, T, T) → (B, T) — take the first query row;
|
||||
# all query positions share the same padding pattern.
|
||||
attn_mask = attn_mask[:, 0, 0, :]
|
||||
attn_metadata = AttentionMetadata(attn_mask=attn_mask) if attn_mask is not None else None
|
||||
y = self.diff_attn(q, k, v, attn_metadata=attn_metadata)
|
||||
y = y.flatten(2, 3)
|
||||
|
||||
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)
|
||||
|
||||
def forward(self, x: Tensor) -> Tensor:
|
||||
return self.w2(F.silu(self.w1(x)) * self.w3(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)
|
||||
@@ -0,0 +1,152 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.commons import sequence_mask
|
||||
|
||||
# f0_bin = 256
|
||||
f0_max = 1100.0
|
||||
f0_min = 50.0
|
||||
f0_mel_min = 1127 * np.log(1 + f0_min / 700)
|
||||
f0_mel_max = 1127 * np.log(1 + f0_max / 700)
|
||||
|
||||
|
||||
def f0_to_coarse(f0, f0_bin):
|
||||
f0_mel = 1127 * (1 + f0 / 700).log()
|
||||
a = (f0_bin - 2) / (f0_mel_max - f0_mel_min)
|
||||
b = f0_mel_min * a - 1.0
|
||||
f0_mel = torch.where(f0_mel > 0, f0_mel * a - b, f0_mel)
|
||||
f0_coarse = torch.round(f0_mel).long()
|
||||
f0_coarse = f0_coarse * (f0_coarse > 0)
|
||||
f0_coarse = f0_coarse + ((f0_coarse < 1) * 1)
|
||||
f0_coarse = f0_coarse * (f0_coarse < f0_bin)
|
||||
f0_coarse = f0_coarse + ((f0_coarse >= f0_bin) * (f0_bin - 1))
|
||||
return f0_coarse
|
||||
|
||||
|
||||
class InterpolateRegulator(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels: int,
|
||||
sampling_ratios: tuple,
|
||||
is_discrete: bool = False,
|
||||
in_channels: int = None, # only applies to continuous input
|
||||
vector_quantize: bool = False, # whether to use vector quantization, only applies to continuous input
|
||||
codebook_size: int = 1024, # for discrete only
|
||||
out_channels: int = None,
|
||||
groups: int = 1,
|
||||
n_codebooks: int = 1, # number of codebooks
|
||||
quantizer_dropout: float = 0.0, # dropout for quantizer
|
||||
f0_condition: bool = False,
|
||||
n_f0_bins: int = 512,
|
||||
):
|
||||
super().__init__()
|
||||
self.sampling_ratios = sampling_ratios
|
||||
out_channels = out_channels or channels
|
||||
model = nn.ModuleList([])
|
||||
if len(sampling_ratios) > 0:
|
||||
self.interpolate = True
|
||||
for _ in sampling_ratios:
|
||||
module = nn.Conv1d(channels, channels, 3, 1, 1)
|
||||
norm = nn.GroupNorm(groups, channels)
|
||||
act = nn.Mish()
|
||||
model.extend([module, norm, act])
|
||||
else:
|
||||
self.interpolate = False
|
||||
model.append(nn.Conv1d(channels, out_channels, 1, 1))
|
||||
self.model = nn.Sequential(*model)
|
||||
self.embedding = nn.Embedding(codebook_size, channels)
|
||||
self.is_discrete = is_discrete
|
||||
|
||||
self.mask_token = nn.Parameter(torch.zeros(1, channels))
|
||||
|
||||
self.n_codebooks = n_codebooks
|
||||
if n_codebooks > 1:
|
||||
self.extra_codebooks = nn.ModuleList(
|
||||
[nn.Embedding(codebook_size, channels) for _ in range(n_codebooks - 1)]
|
||||
)
|
||||
self.extra_codebook_mask_tokens = nn.ParameterList(
|
||||
[nn.Parameter(torch.zeros(1, channels)) for _ in range(n_codebooks - 1)]
|
||||
)
|
||||
self.quantizer_dropout = quantizer_dropout
|
||||
|
||||
if f0_condition:
|
||||
self.f0_embedding = nn.Embedding(n_f0_bins, channels)
|
||||
self.f0_condition = f0_condition
|
||||
self.n_f0_bins = n_f0_bins
|
||||
self.f0_bins = torch.arange(2, 1024, 1024 // n_f0_bins)
|
||||
self.f0_mask = nn.Parameter(torch.zeros(1, channels))
|
||||
else:
|
||||
self.f0_condition = False
|
||||
|
||||
if not is_discrete:
|
||||
self.content_in_proj = nn.Linear(in_channels, channels)
|
||||
if vector_quantize:
|
||||
from dac.nn.quantize import VectorQuantize
|
||||
|
||||
self.vq = VectorQuantize(channels, codebook_size, 8)
|
||||
|
||||
def forward(self, x, ylens=None, n_quantizers=None, f0=None):
|
||||
# apply token drop
|
||||
if self.training:
|
||||
n_quantizers = torch.ones((x.shape[0],)) * self.n_codebooks
|
||||
dropout = torch.randint(1, self.n_codebooks + 1, (x.shape[0],))
|
||||
n_dropout = int(x.shape[0] * self.quantizer_dropout)
|
||||
n_quantizers[:n_dropout] = dropout[:n_dropout]
|
||||
n_quantizers = n_quantizers.to(x.device)
|
||||
# decide whether to drop for each sample in batch
|
||||
else:
|
||||
n_quantizers = torch.ones((x.shape[0],), device=x.device) * (
|
||||
self.n_codebooks if n_quantizers is None else n_quantizers
|
||||
)
|
||||
if self.is_discrete:
|
||||
if self.n_codebooks > 1:
|
||||
assert len(x.size()) == 3
|
||||
x_emb = self.embedding(x[:, 0])
|
||||
for i, emb in enumerate(self.extra_codebooks):
|
||||
x_emb = x_emb + (n_quantizers > i + 1)[..., None, None] * emb(x[:, i + 1])
|
||||
# add mask token if not using this codebook
|
||||
# x_emb = x_emb + (n_quantizers <= i+1)[..., None, None] * self.extra_codebook_mask_tokens[i]
|
||||
x = x_emb
|
||||
elif self.n_codebooks == 1:
|
||||
if len(x.size()) == 2:
|
||||
x = self.embedding(x)
|
||||
else:
|
||||
x = self.embedding(x[:, 0])
|
||||
else:
|
||||
x = self.content_in_proj(x)
|
||||
# x in (B, T, D)
|
||||
mask = sequence_mask(ylens).unsqueeze(-1)
|
||||
if self.interpolate:
|
||||
x = F.interpolate(x.transpose(1, 2).contiguous(), size=ylens.max(), mode="nearest")
|
||||
else:
|
||||
x = x.transpose(1, 2).contiguous()
|
||||
mask = mask[:, : x.size(2), :]
|
||||
ylens = ylens.clamp(max=x.size(2)).long()
|
||||
if self.f0_condition:
|
||||
if f0 is None:
|
||||
x = x + self.f0_mask.unsqueeze(-1)
|
||||
else:
|
||||
# quantized_f0 = torch.bucketize(f0, self.f0_bins.to(f0.device)) # (N, T)
|
||||
quantized_f0 = f0_to_coarse(f0, self.n_f0_bins)
|
||||
quantized_f0 = quantized_f0.clamp(0, self.n_f0_bins - 1).long()
|
||||
f0_emb = self.f0_embedding(quantized_f0)
|
||||
f0_emb = F.interpolate(f0_emb.transpose(1, 2).contiguous(), size=ylens.max(), mode="nearest")
|
||||
x = x + f0_emb
|
||||
out = self.model(x).transpose(1, 2).contiguous()
|
||||
if hasattr(self, "vq"):
|
||||
(
|
||||
out_q,
|
||||
commitment_loss,
|
||||
codebook_loss,
|
||||
codes,
|
||||
out,
|
||||
) = self.vq(out.transpose(1, 2))
|
||||
out_q = out_q.transpose(1, 2)
|
||||
return out_q * mask, ylens, codes, commitment_loss, codebook_loss
|
||||
olens = ylens
|
||||
return out * mask, olens, None, None, None
|
||||
@@ -0,0 +1,88 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.s2mel.modules.encodec import SConv1d
|
||||
|
||||
from . import commons
|
||||
|
||||
|
||||
class WN(torch.nn.Module):
|
||||
def __init__(
|
||||
self, hidden_channels, kernel_size, dilation_rate, n_layers, gin_channels=0, p_dropout=0, causal=False
|
||||
):
|
||||
super().__init__()
|
||||
conv1d_type = SConv1d
|
||||
assert kernel_size % 2 == 1
|
||||
self.hidden_channels = hidden_channels
|
||||
self.kernel_size = (kernel_size,)
|
||||
self.dilation_rate = dilation_rate
|
||||
self.n_layers = n_layers
|
||||
self.gin_channels = gin_channels
|
||||
self.p_dropout = p_dropout
|
||||
|
||||
self.in_layers = torch.nn.ModuleList()
|
||||
self.res_skip_layers = torch.nn.ModuleList()
|
||||
self.drop = nn.Dropout(p_dropout)
|
||||
|
||||
if gin_channels != 0:
|
||||
self.cond_layer = conv1d_type(gin_channels, 2 * hidden_channels * n_layers, 1, norm="weight_norm")
|
||||
|
||||
for i in range(n_layers):
|
||||
dilation = dilation_rate**i
|
||||
padding = int((kernel_size * dilation - dilation) / 2)
|
||||
in_layer = conv1d_type(
|
||||
hidden_channels,
|
||||
2 * hidden_channels,
|
||||
kernel_size,
|
||||
dilation=dilation,
|
||||
padding=padding,
|
||||
norm="weight_norm",
|
||||
causal=causal,
|
||||
)
|
||||
self.in_layers.append(in_layer)
|
||||
|
||||
# last one is not necessary
|
||||
if i < n_layers - 1:
|
||||
res_skip_channels = 2 * hidden_channels
|
||||
else:
|
||||
res_skip_channels = hidden_channels
|
||||
|
||||
res_skip_layer = conv1d_type(hidden_channels, res_skip_channels, 1, norm="weight_norm", causal=causal)
|
||||
self.res_skip_layers.append(res_skip_layer)
|
||||
|
||||
def forward(self, x, x_mask, g=None, **kwargs):
|
||||
output = torch.zeros_like(x)
|
||||
|
||||
if g is not None:
|
||||
g = self.cond_layer(g)
|
||||
|
||||
for i in range(self.n_layers):
|
||||
x_in = self.in_layers[i](x)
|
||||
if g is not None:
|
||||
cond_offset = i * 2 * self.hidden_channels
|
||||
g_l = g[:, cond_offset : cond_offset + 2 * self.hidden_channels, :]
|
||||
else:
|
||||
g_l = torch.zeros_like(x_in)
|
||||
|
||||
acts = commons.fused_add_tanh_sigmoid_multiply(x_in, g_l, self.hidden_channels)
|
||||
acts = self.drop(acts)
|
||||
|
||||
res_skip_acts = self.res_skip_layers[i](acts)
|
||||
if i < self.n_layers - 1:
|
||||
res_acts = res_skip_acts[:, : self.hidden_channels, :]
|
||||
x = (x + res_acts) * x_mask
|
||||
output = output + res_skip_acts[:, self.hidden_channels :, :]
|
||||
else:
|
||||
output = output + res_skip_acts
|
||||
return output * x_mask
|
||||
|
||||
def remove_weight_norm(self):
|
||||
if self.gin_channels != 0:
|
||||
torch.nn.utils.remove_weight_norm(self.cond_layer)
|
||||
for conv_layer in self.in_layers:
|
||||
torch.nn.utils.remove_weight_norm(conv_layer)
|
||||
for conv_layer in self.res_skip_layers:
|
||||
torch.nn.utils.remove_weight_norm(conv_layer)
|
||||
@@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
|
||||
from transformers import PreTrainedTokenizer
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.utils.front import TextTokenizer
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class IndexTTS2Tokenizer(PreTrainedTokenizer):
|
||||
@classmethod
|
||||
def from_pretrained(cls, pretrained_model_name_or_path, *args, **kwargs):
|
||||
vocab_file = kwargs.pop("vocab_file", None)
|
||||
if vocab_file is None:
|
||||
vocab_file = os.path.join(pretrained_model_name_or_path, "bpe.model")
|
||||
logger.info(f"IndexTTS2Tokenizer.from_pretrained: {pretrained_model_name_or_path}")
|
||||
return cls(vocab_file, **kwargs)
|
||||
|
||||
def __init__(self, vocab_file: str, **kwargs):
|
||||
logger.info(f"IndexTTS2Tokenizer.__init__: vocab_file={vocab_file}")
|
||||
self.vocab_file = vocab_file
|
||||
self._tok = TextTokenizer(vocab_file)
|
||||
logger.info("IndexTTS2Tokenizer initialized successfully")
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@property
|
||||
def vocab_size(self):
|
||||
return self._tok.vocab_size
|
||||
|
||||
def get_vocab(self):
|
||||
return self._tok.get_vocab()
|
||||
|
||||
def _tokenize(self, text):
|
||||
return self._tok.tokenize(text)
|
||||
|
||||
def _convert_token_to_id(self, token):
|
||||
return self._tok.convert_tokens_to_ids(token)[0]
|
||||
|
||||
def convert_tokens_to_string(self, tokens):
|
||||
return self._tok.decode(self._tok.convert_tokens_to_ids(tokens))
|
||||
|
||||
def split_segments(
|
||||
self,
|
||||
tokenized: list[str],
|
||||
max_text_tokens_per_segment: int = 120,
|
||||
quick_streaming_tokens: int = 0,
|
||||
) -> list[list[str]]:
|
||||
return self._tok.split_segments(
|
||||
tokenized,
|
||||
max_text_tokens_per_segment=max_text_tokens_per_segment,
|
||||
quick_streaming_tokens=quick_streaming_tokens,
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,117 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import torch.nn.functional as F
|
||||
from torch import nn
|
||||
|
||||
from .layers import (
|
||||
BasicResBlock,
|
||||
CAMDenseTDNNBlock,
|
||||
DenseLayer,
|
||||
StatsPool,
|
||||
TDNNLayer,
|
||||
TransitLayer,
|
||||
get_nonlinear,
|
||||
)
|
||||
|
||||
|
||||
class FCM(nn.Module):
|
||||
def __init__(self, block=BasicResBlock, num_blocks=[2, 2], m_channels=32, feat_dim=80):
|
||||
super().__init__()
|
||||
self.in_planes = m_channels
|
||||
self.conv1 = nn.Conv2d(1, m_channels, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(m_channels)
|
||||
|
||||
self.layer1 = self._make_layer(block, m_channels, num_blocks[0], stride=2)
|
||||
self.layer2 = self._make_layer(block, m_channels, num_blocks[1], stride=2)
|
||||
|
||||
self.conv2 = nn.Conv2d(m_channels, m_channels, kernel_size=3, stride=(2, 1), padding=1, bias=False)
|
||||
self.bn2 = nn.BatchNorm2d(m_channels)
|
||||
self.out_channels = m_channels * (feat_dim // 8)
|
||||
|
||||
def _make_layer(self, block, planes, num_blocks, stride):
|
||||
strides = [stride] + [1] * (num_blocks - 1)
|
||||
layers = []
|
||||
for stride in strides:
|
||||
layers.append(block(self.in_planes, planes, stride))
|
||||
self.in_planes = planes * block.expansion
|
||||
return nn.Sequential(*layers)
|
||||
|
||||
def forward(self, x):
|
||||
x = x.unsqueeze(1)
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out = self.layer1(out)
|
||||
out = self.layer2(out)
|
||||
out = F.relu(self.bn2(self.conv2(out)))
|
||||
|
||||
shape = out.shape
|
||||
out = out.reshape(shape[0], shape[1] * shape[2], shape[3])
|
||||
return out
|
||||
|
||||
|
||||
class CAMPPlus(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
feat_dim=80,
|
||||
embedding_size=512,
|
||||
growth_rate=32,
|
||||
bn_size=4,
|
||||
init_channels=128,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=True,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.head = FCM(feat_dim=feat_dim)
|
||||
channels = self.head.out_channels
|
||||
|
||||
self.xvector = nn.Sequential(
|
||||
OrderedDict(
|
||||
[
|
||||
(
|
||||
"tdnn",
|
||||
TDNNLayer(channels, init_channels, 5, stride=2, dilation=1, padding=-1, config_str=config_str),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
channels = init_channels
|
||||
for i, (num_layers, kernel_size, dilation) in enumerate(zip((12, 24, 16), (3, 3, 3), (1, 2, 2))):
|
||||
block = CAMDenseTDNNBlock(
|
||||
num_layers=num_layers,
|
||||
in_channels=channels,
|
||||
out_channels=growth_rate,
|
||||
bn_channels=bn_size * growth_rate,
|
||||
kernel_size=kernel_size,
|
||||
dilation=dilation,
|
||||
config_str=config_str,
|
||||
memory_efficient=memory_efficient,
|
||||
)
|
||||
self.xvector.add_module(f"block{i + 1}", block)
|
||||
channels = channels + num_layers * growth_rate
|
||||
self.xvector.add_module(
|
||||
f"transit{i + 1}", TransitLayer(channels, channels // 2, bias=False, config_str=config_str)
|
||||
)
|
||||
channels //= 2
|
||||
|
||||
self.xvector.add_module("out_nonlinear", get_nonlinear(config_str, channels))
|
||||
|
||||
self.xvector.add_module("stats", StatsPool())
|
||||
self.xvector.add_module("dense", DenseLayer(channels * 2, embedding_size, config_str="batchnorm_"))
|
||||
|
||||
for m in self.modules():
|
||||
if isinstance(m, (nn.Conv1d, nn.Linear)):
|
||||
nn.init.kaiming_normal_(m.weight.data)
|
||||
if m.bias is not None:
|
||||
nn.init.zeros_(m.bias)
|
||||
|
||||
def forward(self, x):
|
||||
x = x.permute(0, 2, 1) # (B,T,F) => (B,F,T)
|
||||
x = self.head(x)
|
||||
x = self.xvector(x)
|
||||
return x
|
||||
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright 3D-Speaker (https://github.com/alibaba-damo-academy/3D-Speaker). All Rights Reserved.
|
||||
# Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import torch.utils.checkpoint as cp
|
||||
from torch import nn
|
||||
|
||||
|
||||
def get_nonlinear(config_str, channels):
|
||||
nonlinear = nn.Sequential()
|
||||
for name in config_str.split("-"):
|
||||
if name == "relu":
|
||||
nonlinear.add_module("relu", nn.ReLU(inplace=True))
|
||||
elif name == "prelu":
|
||||
nonlinear.add_module("prelu", nn.PReLU(channels))
|
||||
elif name == "batchnorm":
|
||||
nonlinear.add_module("batchnorm", nn.BatchNorm1d(channels))
|
||||
elif name == "batchnorm_":
|
||||
nonlinear.add_module("batchnorm", nn.BatchNorm1d(channels, affine=False))
|
||||
else:
|
||||
raise ValueError(f"Unexpected module ({name}).")
|
||||
return nonlinear
|
||||
|
||||
|
||||
def statistics_pooling(x, dim=-1, keepdim=False, unbiased=True, eps=1e-2):
|
||||
mean = x.mean(dim=dim)
|
||||
std = x.std(dim=dim, unbiased=unbiased)
|
||||
stats = torch.cat([mean, std], dim=-1)
|
||||
if keepdim:
|
||||
stats = stats.unsqueeze(dim=dim)
|
||||
return stats
|
||||
|
||||
|
||||
class StatsPool(nn.Module):
|
||||
def forward(self, x):
|
||||
return statistics_pooling(x)
|
||||
|
||||
|
||||
class TDNNLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
padding=0,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
):
|
||||
super().__init__()
|
||||
if padding < 0:
|
||||
assert kernel_size % 2 == 1, f"Expect equal paddings, but got even kernel size ({kernel_size})"
|
||||
padding = (kernel_size - 1) // 2 * dilation
|
||||
self.linear = nn.Conv1d(
|
||||
in_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias
|
||||
)
|
||||
self.nonlinear = get_nonlinear(config_str, out_channels)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.linear(x)
|
||||
x = self.nonlinear(x)
|
||||
return x
|
||||
|
||||
|
||||
class CAMLayer(nn.Module):
|
||||
def __init__(self, bn_channels, out_channels, kernel_size, stride, padding, dilation, bias, reduction=2):
|
||||
super().__init__()
|
||||
self.linear_local = nn.Conv1d(
|
||||
bn_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias
|
||||
)
|
||||
self.linear1 = nn.Conv1d(bn_channels, bn_channels // reduction, 1)
|
||||
self.relu = nn.ReLU(inplace=True)
|
||||
self.linear2 = nn.Conv1d(bn_channels // reduction, out_channels, 1)
|
||||
self.sigmoid = nn.Sigmoid()
|
||||
|
||||
def forward(self, x):
|
||||
y = self.linear_local(x)
|
||||
context = x.mean(-1, keepdim=True) + self.seg_pooling(x)
|
||||
context = self.relu(self.linear1(context))
|
||||
m = self.sigmoid(self.linear2(context))
|
||||
return y * m
|
||||
|
||||
def seg_pooling(self, x, seg_len=100, stype="avg"):
|
||||
if stype == "avg":
|
||||
seg = F.avg_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
||||
elif stype == "max":
|
||||
seg = F.max_pool1d(x, kernel_size=seg_len, stride=seg_len, ceil_mode=True)
|
||||
else:
|
||||
raise ValueError("Wrong segment pooling type.")
|
||||
shape = seg.shape
|
||||
seg = seg.unsqueeze(-1).expand(*shape, seg_len).reshape(*shape[:-1], -1)
|
||||
seg = seg[..., : x.shape[-1]]
|
||||
return seg
|
||||
|
||||
|
||||
class CAMDenseTDNNLayer(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels,
|
||||
out_channels,
|
||||
bn_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=False,
|
||||
):
|
||||
super().__init__()
|
||||
assert kernel_size % 2 == 1, f"Expect equal paddings, but got even kernel size ({kernel_size})"
|
||||
padding = (kernel_size - 1) // 2 * dilation
|
||||
self.memory_efficient = memory_efficient
|
||||
self.nonlinear1 = get_nonlinear(config_str, in_channels)
|
||||
self.linear1 = nn.Conv1d(in_channels, bn_channels, 1, bias=False)
|
||||
self.nonlinear2 = get_nonlinear(config_str, bn_channels)
|
||||
self.cam_layer = CAMLayer(
|
||||
bn_channels, out_channels, kernel_size, stride=stride, padding=padding, dilation=dilation, bias=bias
|
||||
)
|
||||
|
||||
def bn_function(self, x):
|
||||
return self.linear1(self.nonlinear1(x))
|
||||
|
||||
def forward(self, x):
|
||||
if self.training and self.memory_efficient:
|
||||
x = cp.checkpoint(self.bn_function, x)
|
||||
else:
|
||||
x = self.bn_function(x)
|
||||
x = self.cam_layer(self.nonlinear2(x))
|
||||
return x
|
||||
|
||||
|
||||
class CAMDenseTDNNBlock(nn.ModuleList):
|
||||
def __init__(
|
||||
self,
|
||||
num_layers,
|
||||
in_channels,
|
||||
out_channels,
|
||||
bn_channels,
|
||||
kernel_size,
|
||||
stride=1,
|
||||
dilation=1,
|
||||
bias=False,
|
||||
config_str="batchnorm-relu",
|
||||
memory_efficient=False,
|
||||
):
|
||||
super().__init__()
|
||||
for i in range(num_layers):
|
||||
layer = CAMDenseTDNNLayer(
|
||||
in_channels=in_channels + i * out_channels,
|
||||
out_channels=out_channels,
|
||||
bn_channels=bn_channels,
|
||||
kernel_size=kernel_size,
|
||||
stride=stride,
|
||||
dilation=dilation,
|
||||
bias=bias,
|
||||
config_str=config_str,
|
||||
memory_efficient=memory_efficient,
|
||||
)
|
||||
self.add_module(f"tdnnd{i + 1}", layer)
|
||||
|
||||
def forward(self, x):
|
||||
for layer in self:
|
||||
x = torch.cat([x, layer(x)], dim=1)
|
||||
return x
|
||||
|
||||
|
||||
class TransitLayer(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, bias=True, config_str="batchnorm-relu"):
|
||||
super().__init__()
|
||||
self.nonlinear = get_nonlinear(config_str, in_channels)
|
||||
self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
||||
|
||||
def forward(self, x):
|
||||
x = self.nonlinear(x)
|
||||
x = self.linear(x)
|
||||
return x
|
||||
|
||||
|
||||
class DenseLayer(nn.Module):
|
||||
def __init__(self, in_channels, out_channels, bias=False, config_str="batchnorm-relu"):
|
||||
super().__init__()
|
||||
self.linear = nn.Conv1d(in_channels, out_channels, 1, bias=bias)
|
||||
self.nonlinear = get_nonlinear(config_str, out_channels)
|
||||
|
||||
def forward(self, x):
|
||||
if len(x.shape) == 2:
|
||||
x = self.linear(x.unsqueeze(dim=-1)).squeeze(dim=-1)
|
||||
else:
|
||||
x = self.linear(x)
|
||||
x = self.nonlinear(x)
|
||||
return x
|
||||
|
||||
|
||||
class BasicResBlock(nn.Module):
|
||||
expansion = 1
|
||||
|
||||
def __init__(self, in_planes, planes, stride=1):
|
||||
super().__init__()
|
||||
self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=(stride, 1), padding=1, bias=False)
|
||||
self.bn1 = nn.BatchNorm2d(planes)
|
||||
self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)
|
||||
self.bn2 = nn.BatchNorm2d(planes)
|
||||
|
||||
self.shortcut = nn.Sequential()
|
||||
if stride != 1 or in_planes != self.expansion * planes:
|
||||
self.shortcut = nn.Sequential(
|
||||
nn.Conv2d(in_planes, self.expansion * planes, kernel_size=1, stride=(stride, 1), bias=False),
|
||||
nn.BatchNorm2d(self.expansion * planes),
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
out = F.relu(self.bn1(self.conv1(x)))
|
||||
out = self.bn2(self.conv2(out))
|
||||
out += self.shortcut(x)
|
||||
out = F.relu(out)
|
||||
return out
|
||||
@@ -0,0 +1,88 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import re
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
def tokenize_by_CJK_char(line: str, do_upper_case=True) -> str:
|
||||
"""
|
||||
Tokenize a line of text with CJK char.
|
||||
|
||||
Note: All return characters will be upper case.
|
||||
|
||||
Example:
|
||||
input = "你好世界是 hello world 的中文"
|
||||
output = "你 好 世 界 是 HELLO WORLD 的 中 文"
|
||||
|
||||
Args:
|
||||
line:
|
||||
The input text.
|
||||
|
||||
Return:
|
||||
A new string tokenize by CJK char.
|
||||
"""
|
||||
# CJK ranges from nltk/tokenize/util.py (alvations/nltk@79eed6d)
|
||||
CJK_RANGE_PATTERN = (
|
||||
r"([\u1100-\u11ff\u2e80-\ua4cf\ua840-\uD7AF\uF900-\uFAFF\uFE30-\uFE4F\uFF65-\uFFDC\U00020000-\U0002FFFF])"
|
||||
)
|
||||
chars = re.split(CJK_RANGE_PATTERN, line.strip())
|
||||
return " ".join([w.strip().upper() if do_upper_case else w.strip() for w in chars if w.strip()])
|
||||
|
||||
|
||||
def de_tokenized_by_CJK_char(line: str, do_lower_case=False) -> str:
|
||||
"""
|
||||
Example:
|
||||
input = "你 好 世 界 是 HELLO WORLD 的 中 文"
|
||||
output = "你好世界是 hello world 的中文"
|
||||
|
||||
do_lower_case:
|
||||
input = "SEE YOU!"
|
||||
output = "see you!"
|
||||
"""
|
||||
# replace english words in the line with placeholders
|
||||
english_word_pattern = re.compile(r"([A-Z]+(?:[\s'-][A-Z-]+)*)", re.IGNORECASE)
|
||||
english_sents = english_word_pattern.findall(line)
|
||||
for i, sent in enumerate(english_sents):
|
||||
line = line.replace(sent, f"<sent_{i}>")
|
||||
|
||||
words = line.split()
|
||||
# restore english sentences
|
||||
sent_placeholder_pattern = re.compile(r"(<sent_(\d+)>)")
|
||||
for i in range(len(words)):
|
||||
all_matches = sent_placeholder_pattern.findall(words[i])
|
||||
if all_matches:
|
||||
# restore the english word
|
||||
for h, j in all_matches:
|
||||
placeholder_index = int(j)
|
||||
words[i] = words[i].replace(h, english_sents[placeholder_index])
|
||||
if do_lower_case:
|
||||
words[i] = words[i].lower()
|
||||
return "".join(words)
|
||||
|
||||
|
||||
def make_pad_mask(lengths: torch.Tensor, max_len: int = 0) -> torch.Tensor:
|
||||
"""Make mask tensor containing indices of padded part.
|
||||
|
||||
See description of make_non_pad_mask.
|
||||
|
||||
Args:
|
||||
lengths (torch.Tensor): Batch of lengths (B,).
|
||||
Returns:
|
||||
torch.Tensor: Mask tensor containing indices of padded part.
|
||||
|
||||
Examples:
|
||||
>>> lengths = [5, 3, 2]
|
||||
>>> make_pad_mask(lengths)
|
||||
masks = [[0, 0, 0, 0 ,0],
|
||||
[0, 0, 0, 1, 1],
|
||||
[0, 0, 1, 1, 1]]
|
||||
"""
|
||||
batch_size = lengths.size(0)
|
||||
max_len = max_len if max_len > 0 else lengths.max().item()
|
||||
seq_range = torch.arange(0, max_len, dtype=torch.int64, device=lengths.device)
|
||||
seq_range_expand = seq_range.unsqueeze(0).expand(batch_size, max_len)
|
||||
seq_length_expand = lengths.unsqueeze(-1)
|
||||
mask = seq_range_expand >= seq_length_expand
|
||||
return mask
|
||||
@@ -0,0 +1,208 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import os
|
||||
import warnings
|
||||
from typing import overload
|
||||
|
||||
from sentencepiece import SentencePieceProcessor
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.utils.common import (
|
||||
de_tokenized_by_CJK_char,
|
||||
tokenize_by_CJK_char,
|
||||
)
|
||||
|
||||
|
||||
class TextTokenizer:
|
||||
def __init__(self, vocab_file: str):
|
||||
self.vocab_file = vocab_file
|
||||
|
||||
if self.vocab_file is None:
|
||||
raise ValueError("vocab_file is None")
|
||||
if not os.path.exists(self.vocab_file):
|
||||
raise ValueError(f"vocab_file {self.vocab_file} does not exist")
|
||||
self.sp_model = SentencePieceProcessor(model_file=self.vocab_file)
|
||||
self.pre_tokenizers = [tokenize_by_CJK_char]
|
||||
|
||||
@property
|
||||
def vocab_size(self):
|
||||
return self.sp_model.GetPieceSize()
|
||||
|
||||
@property
|
||||
def unk_token(self):
|
||||
return "<unk>"
|
||||
|
||||
@property
|
||||
def pad_token(self):
|
||||
return None
|
||||
|
||||
@property
|
||||
def bos_token(self):
|
||||
return "<s>"
|
||||
|
||||
@property
|
||||
def eos_token(self):
|
||||
return "</s>"
|
||||
|
||||
@property
|
||||
def pad_token_id(self):
|
||||
return -1
|
||||
|
||||
@property
|
||||
def bos_token_id(self):
|
||||
return 0
|
||||
|
||||
@property
|
||||
def eos_token_id(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def unk_token_id(self):
|
||||
return self.sp_model.unk_id()
|
||||
|
||||
@property
|
||||
def special_tokens_map(self):
|
||||
return {
|
||||
"unk_token": self.unk_token,
|
||||
"pad_token": self.pad_token,
|
||||
"bos_token": self.bos_token,
|
||||
"eos_token": self.eos_token,
|
||||
}
|
||||
|
||||
def get_vocab(self):
|
||||
vocab = {self.convert_ids_to_tokens(i): i for i in range(self.vocab_size)}
|
||||
return vocab
|
||||
|
||||
@overload
|
||||
def convert_ids_to_tokens(self, ids: int) -> str: ...
|
||||
|
||||
@overload
|
||||
def convert_ids_to_tokens(self, ids: list[int]) -> list[str]: ...
|
||||
|
||||
def convert_ids_to_tokens(self, ids: list[int] | int):
|
||||
return self.sp_model.IdToPiece(ids)
|
||||
|
||||
def convert_tokens_to_ids(self, tokens: list[str] | str) -> list[int]:
|
||||
if isinstance(tokens, str):
|
||||
tokens = [tokens]
|
||||
return [self.sp_model.PieceToId(token) for token in tokens]
|
||||
|
||||
def tokenize(self, text: str) -> list[str]:
|
||||
return self.encode(text, out_type=str)
|
||||
|
||||
def encode(self, text: str, **kwargs):
|
||||
if len(text) == 0:
|
||||
return []
|
||||
if len(text.strip()) == 1:
|
||||
return self.sp_model.Encode(text, out_type=kwargs.pop("out_type", int), **kwargs)
|
||||
for pre_tokenizer in self.pre_tokenizers:
|
||||
text = pre_tokenizer(text)
|
||||
return self.sp_model.Encode(text, out_type=kwargs.pop("out_type", int), **kwargs)
|
||||
|
||||
def decode(self, ids: list[int] | int, do_lower_case=False, **kwargs):
|
||||
if isinstance(ids, int):
|
||||
ids = [ids]
|
||||
decoded = self.sp_model.Decode(ids, out_type=kwargs.pop("out_type", str), **kwargs)
|
||||
return de_tokenized_by_CJK_char(decoded, do_lower_case=do_lower_case)
|
||||
|
||||
@staticmethod
|
||||
def split_segments_by_token(
|
||||
tokenized_str: list[str],
|
||||
split_tokens: list[str],
|
||||
max_text_tokens_per_segment: int,
|
||||
quick_streaming_tokens: int = 0,
|
||||
) -> list[list[str]]:
|
||||
if len(tokenized_str) == 0:
|
||||
return []
|
||||
segments: list[list[str]] = []
|
||||
current_segment = []
|
||||
current_segment_tokens_len = 0
|
||||
i = 0
|
||||
while i < len(tokenized_str):
|
||||
token = tokenized_str[i]
|
||||
current_segment.append(token)
|
||||
current_segment_tokens_len += 1
|
||||
if not ("," in split_tokens or "▁," in split_tokens) and (
|
||||
"," in current_segment or "▁," in current_segment
|
||||
):
|
||||
sub_segments = TextTokenizer.split_segments_by_token(
|
||||
current_segment,
|
||||
[",", "▁,"],
|
||||
max_text_tokens_per_segment=max_text_tokens_per_segment,
|
||||
quick_streaming_tokens=quick_streaming_tokens,
|
||||
)
|
||||
elif "-" not in split_tokens and "-" in current_segment:
|
||||
sub_segments = TextTokenizer.split_segments_by_token(
|
||||
current_segment,
|
||||
["-"],
|
||||
max_text_tokens_per_segment=max_text_tokens_per_segment,
|
||||
quick_streaming_tokens=quick_streaming_tokens,
|
||||
)
|
||||
elif current_segment_tokens_len <= max_text_tokens_per_segment:
|
||||
if token in split_tokens and current_segment_tokens_len > 2:
|
||||
if i < len(tokenized_str) - 1:
|
||||
if tokenized_str[i + 1] in ["'", "▁'"]:
|
||||
current_segment.append(tokenized_str[i + 1])
|
||||
i += 1
|
||||
segments.append(current_segment)
|
||||
current_segment = []
|
||||
current_segment_tokens_len = 0
|
||||
i += 1
|
||||
continue
|
||||
else:
|
||||
sub_segments = []
|
||||
for j in range(0, len(current_segment), max_text_tokens_per_segment):
|
||||
if j + max_text_tokens_per_segment < len(current_segment):
|
||||
sub_segments.append(current_segment[j : j + max_text_tokens_per_segment])
|
||||
else:
|
||||
sub_segments.append(current_segment[j:])
|
||||
warnings.warn(
|
||||
f"The tokens length of segment exceeds limit: {max_text_tokens_per_segment}, "
|
||||
f"Tokens in segment: {current_segment}."
|
||||
"Maybe unexpected behavior",
|
||||
RuntimeWarning,
|
||||
)
|
||||
segments.extend(sub_segments)
|
||||
current_segment = []
|
||||
current_segment_tokens_len = 0
|
||||
i += 1
|
||||
if current_segment_tokens_len > 0:
|
||||
assert current_segment_tokens_len <= max_text_tokens_per_segment
|
||||
segments.append(current_segment)
|
||||
merged_segments = []
|
||||
total_token = 0
|
||||
for segment in segments:
|
||||
total_token += len(segment)
|
||||
if len(segment) == 0:
|
||||
continue
|
||||
if len(merged_segments) == 0:
|
||||
merged_segments.append(segment)
|
||||
elif (
|
||||
len(merged_segments[-1]) + len(segment) <= max_text_tokens_per_segment
|
||||
and total_token > quick_streaming_tokens
|
||||
):
|
||||
merged_segments[-1] = merged_segments[-1] + segment
|
||||
elif len(merged_segments[-1]) + len(segment) <= max_text_tokens_per_segment / 2:
|
||||
merged_segments[-1] = merged_segments[-1] + segment
|
||||
else:
|
||||
merged_segments.append(segment)
|
||||
return merged_segments
|
||||
|
||||
punctuation_marks_tokens = [
|
||||
".",
|
||||
"!",
|
||||
"?",
|
||||
"▁.",
|
||||
"▁?",
|
||||
"▁...",
|
||||
]
|
||||
|
||||
def split_segments(
|
||||
self, tokenized: list[str], max_text_tokens_per_segment=120, quick_streaming_tokens=0
|
||||
) -> list[list[str]]:
|
||||
return TextTokenizer.split_segments_by_token(
|
||||
tokenized,
|
||||
self.punctuation_marks_tokens,
|
||||
max_text_tokens_per_segment=max_text_tokens_per_segment,
|
||||
quick_streaming_tokens=quick_streaming_tokens,
|
||||
)
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
@@ -0,0 +1,200 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2024 Amphion.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
#
|
||||
# Merged from factorized_vector_quantize.py and residual_vq.py.
|
||||
# Only FactorizedVectorQuantize ("fvq") quantizer type is retained
|
||||
# as it is the only one used by RepCodec in IndexTTS2.
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from einops import rearrange
|
||||
from torch.nn.utils import weight_norm
|
||||
|
||||
|
||||
def WNConv1d(*args, **kwargs):
|
||||
return weight_norm(nn.Conv1d(*args, **kwargs))
|
||||
|
||||
|
||||
class FactorizedVectorQuantize(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
input_dim,
|
||||
codebook_size,
|
||||
codebook_dim,
|
||||
commitment=0.005,
|
||||
codebook_loss_weight=1.0,
|
||||
use_l2_normalize=True,
|
||||
):
|
||||
super().__init__()
|
||||
self.input_dim = input_dim
|
||||
self.codebook_size = codebook_size
|
||||
self.codebook_dim = codebook_dim
|
||||
self.commitment = commitment
|
||||
self.codebook_loss_weight = codebook_loss_weight
|
||||
self.use_l2_normalize = use_l2_normalize
|
||||
|
||||
if self.input_dim != self.codebook_dim:
|
||||
self.in_project = WNConv1d(self.input_dim, self.codebook_dim, kernel_size=1)
|
||||
self.out_project = WNConv1d(self.codebook_dim, self.input_dim, kernel_size=1)
|
||||
else:
|
||||
self.in_project = nn.Identity()
|
||||
self.out_project = nn.Identity()
|
||||
|
||||
self.codebook = nn.Embedding(self.codebook_size, self.codebook_dim)
|
||||
|
||||
def forward(self, z):
|
||||
z_e = self.in_project(z)
|
||||
z_q, indices = self.decode_latents(z_e)
|
||||
|
||||
if self.training:
|
||||
commit_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2]) * self.commitment
|
||||
codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2]) * self.codebook_loss_weight
|
||||
else:
|
||||
commit_loss = torch.zeros(z.shape[0], device=z.device)
|
||||
codebook_loss = torch.zeros(z.shape[0], device=z.device)
|
||||
|
||||
z_q = z_e + (z_q - z_e).detach()
|
||||
z_q = self.out_project(z_q)
|
||||
|
||||
return z_q, commit_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):
|
||||
encodings = rearrange(latents, "b d t -> (b t) d")
|
||||
codebook = self.codebook.weight
|
||||
|
||||
if self.use_l2_normalize:
|
||||
encodings = F.normalize(encodings)
|
||||
codebook = F.normalize(codebook)
|
||||
|
||||
dist = (
|
||||
encodings.pow(2).sum(1, keepdim=True)
|
||||
- 2 * encodings @ codebook.t()
|
||||
+ codebook.pow(2).sum(1, keepdim=True).t()
|
||||
)
|
||||
indices = rearrange((-dist).max(1)[1], "(b t) -> b t", b=latents.size(0))
|
||||
z_q = self.decode_code(indices)
|
||||
return z_q, indices
|
||||
|
||||
def vq2emb(self, vq, out_proj=True):
|
||||
emb = self.decode_code(vq)
|
||||
if out_proj:
|
||||
emb = self.out_project(emb)
|
||||
return emb
|
||||
|
||||
|
||||
class ResidualVQ(nn.Module):
|
||||
"""Residual Vector Quantization (SoundStream).
|
||||
|
||||
Only supports FactorizedVectorQuantize ("fvq") quantizer type.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_dim: int = 256,
|
||||
num_quantizers: int = 8,
|
||||
codebook_size: int = 1024,
|
||||
codebook_dim: int = 256,
|
||||
quantizer_type: str = "fvq",
|
||||
quantizer_dropout: float = 0.5,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__()
|
||||
self.input_dim = input_dim
|
||||
self.num_quantizers = num_quantizers
|
||||
self.codebook_size = codebook_size
|
||||
self.codebook_dim = codebook_dim
|
||||
self.quantizer_dropout = quantizer_dropout
|
||||
|
||||
self.quantizers = nn.ModuleList(
|
||||
[
|
||||
FactorizedVectorQuantize(
|
||||
input_dim=input_dim,
|
||||
codebook_size=codebook_size,
|
||||
codebook_dim=codebook_dim,
|
||||
**kwargs,
|
||||
)
|
||||
for _ in range(num_quantizers)
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, z, n_quantizers: int = None):
|
||||
quantized_out = 0.0
|
||||
residual = z
|
||||
|
||||
all_commit_losses = []
|
||||
all_codebook_losses = []
|
||||
all_indices = []
|
||||
all_quantized = []
|
||||
|
||||
if n_quantizers is None:
|
||||
n_quantizers = self.num_quantizers
|
||||
|
||||
if self.training:
|
||||
n_q = torch.ones((z.shape[0],)) * self.num_quantizers + 1
|
||||
dropout = torch.randint(1, self.num_quantizers + 1, (z.shape[0],))
|
||||
n_dropout = int(z.shape[0] * self.quantizer_dropout)
|
||||
n_q[:n_dropout] = dropout[:n_dropout]
|
||||
n_quantizers = n_q.to(z.device)
|
||||
|
||||
for i, quantizer in enumerate(self.quantizers):
|
||||
if not self.training and i >= n_quantizers:
|
||||
break
|
||||
|
||||
z_q_i, commit_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(residual)
|
||||
|
||||
mask = torch.full((z.shape[0],), fill_value=i, device=z.device) < n_quantizers
|
||||
quantized_out = quantized_out + z_q_i * mask[:, None, None]
|
||||
residual = residual - z_q_i
|
||||
|
||||
commit_loss_i = (commit_loss_i * mask).mean()
|
||||
codebook_loss_i = (codebook_loss_i * mask).mean()
|
||||
|
||||
all_commit_losses.append(commit_loss_i)
|
||||
all_codebook_losses.append(codebook_loss_i)
|
||||
all_indices.append(indices_i)
|
||||
all_quantized.append(z_q_i)
|
||||
|
||||
(
|
||||
all_commit_losses,
|
||||
all_codebook_losses,
|
||||
all_indices,
|
||||
all_quantized,
|
||||
) = map(
|
||||
torch.stack,
|
||||
(
|
||||
all_commit_losses,
|
||||
all_codebook_losses,
|
||||
all_indices,
|
||||
all_quantized,
|
||||
),
|
||||
)
|
||||
|
||||
return (
|
||||
quantized_out,
|
||||
all_indices,
|
||||
all_commit_losses,
|
||||
all_codebook_losses,
|
||||
all_quantized,
|
||||
)
|
||||
|
||||
def vq2emb(self, vq, n_quantizers=None):
|
||||
quantized_out = 0.0
|
||||
if n_quantizers is None:
|
||||
n_quantizers = self.num_quantizers
|
||||
for idx, quantizer in enumerate(self.quantizers):
|
||||
if idx >= n_quantizers:
|
||||
break
|
||||
quantized_out += quantizer.vq2emb(vq[idx])
|
||||
return quantized_out
|
||||
@@ -0,0 +1,120 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2024 Amphion.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
import torch.nn as nn
|
||||
from torch.nn import functional as F
|
||||
|
||||
from vllm_omni.model_executor.models.indextts2.utils.maskgct.quantize import ResidualVQ
|
||||
from vllm_omni.model_executor.models.indextts2.utils.maskgct.vocos import VocosBackbone
|
||||
|
||||
|
||||
def init_weights(m):
|
||||
if isinstance(m, nn.Conv1d):
|
||||
nn.init.trunc_normal_(m.weight, std=0.02)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
if isinstance(m, nn.Linear):
|
||||
nn.init.trunc_normal_(m.weight, std=0.02)
|
||||
nn.init.constant_(m.bias, 0)
|
||||
|
||||
|
||||
class RepCodec(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
codebook_size=8192,
|
||||
hidden_size=1024,
|
||||
codebook_dim=8,
|
||||
vocos_dim=384,
|
||||
vocos_intermediate_dim=2048,
|
||||
vocos_num_layers=12,
|
||||
num_quantizers=1,
|
||||
downsample_scale=1,
|
||||
cfg=None,
|
||||
):
|
||||
super().__init__()
|
||||
if cfg is not None:
|
||||
_g = lambda attr, default: getattr(cfg, attr, default) # noqa: E731
|
||||
codebook_size = _g("codebook_size", codebook_size)
|
||||
codebook_dim = _g("codebook_dim", codebook_dim)
|
||||
hidden_size = _g("hidden_size", hidden_size)
|
||||
vocos_dim = _g("vocos_dim", vocos_dim)
|
||||
vocos_intermediate_dim = _g("vocos_intermediate_dim", vocos_intermediate_dim)
|
||||
vocos_num_layers = _g("vocos_num_layers", vocos_num_layers)
|
||||
num_quantizers = _g("num_quantizers", num_quantizers)
|
||||
downsample_scale = _g("downsample_scale", downsample_scale)
|
||||
|
||||
self.codebook_size = codebook_size
|
||||
self.codebook_dim = codebook_dim
|
||||
self.hidden_size = hidden_size
|
||||
self.vocos_dim = vocos_dim
|
||||
self.vocos_intermediate_dim = vocos_intermediate_dim
|
||||
self.vocos_num_layers = vocos_num_layers
|
||||
self.num_quantizers = num_quantizers
|
||||
self.downsample_scale = downsample_scale
|
||||
|
||||
if self.downsample_scale is not None and self.downsample_scale > 1:
|
||||
self.down = nn.Conv1d(self.hidden_size, self.hidden_size, kernel_size=3, stride=2, padding=1)
|
||||
self.up = nn.Conv1d(self.hidden_size, self.hidden_size, kernel_size=3, stride=1, padding=1)
|
||||
|
||||
self.encoder = nn.Sequential(
|
||||
VocosBackbone(
|
||||
input_channels=self.hidden_size,
|
||||
dim=self.vocos_dim,
|
||||
intermediate_dim=self.vocos_intermediate_dim,
|
||||
num_layers=self.vocos_num_layers,
|
||||
adanorm_num_embeddings=None,
|
||||
),
|
||||
nn.Linear(self.vocos_dim, self.hidden_size),
|
||||
)
|
||||
self.decoder = nn.Sequential(
|
||||
VocosBackbone(
|
||||
input_channels=self.hidden_size,
|
||||
dim=self.vocos_dim,
|
||||
intermediate_dim=self.vocos_intermediate_dim,
|
||||
num_layers=self.vocos_num_layers,
|
||||
adanorm_num_embeddings=None,
|
||||
),
|
||||
nn.Linear(self.vocos_dim, self.hidden_size),
|
||||
)
|
||||
|
||||
self.quantizer = ResidualVQ(
|
||||
input_dim=hidden_size,
|
||||
num_quantizers=num_quantizers,
|
||||
codebook_size=codebook_size,
|
||||
codebook_dim=codebook_dim,
|
||||
quantizer_type="fvq",
|
||||
quantizer_dropout=0.0,
|
||||
commitment=0.15,
|
||||
codebook_loss_weight=1.0,
|
||||
use_l2_normalize=True,
|
||||
)
|
||||
|
||||
self.reset_parameters()
|
||||
|
||||
def quantize(self, x):
|
||||
if self.downsample_scale is not None and self.downsample_scale > 1:
|
||||
x = x.transpose(1, 2)
|
||||
x = self.down(x)
|
||||
x = F.gelu(x)
|
||||
x = x.transpose(1, 2)
|
||||
|
||||
x = self.encoder(x.transpose(1, 2)).transpose(1, 2)
|
||||
|
||||
(
|
||||
quantized_out,
|
||||
all_indices,
|
||||
all_commit_losses,
|
||||
all_codebook_losses,
|
||||
_,
|
||||
) = self.quantizer(x)
|
||||
|
||||
if all_indices.shape[0] == 1:
|
||||
return all_indices.squeeze(0), quantized_out.transpose(1, 2)
|
||||
return all_indices, quantized_out.transpose(1, 2)
|
||||
|
||||
def reset_parameters(self):
|
||||
self.apply(init_weights)
|
||||
@@ -0,0 +1,102 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
# Copyright (c) 2024 Amphion.
|
||||
#
|
||||
# This source code is licensed under the MIT license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
|
||||
import torch
|
||||
from torch import nn
|
||||
|
||||
|
||||
class ConvNeXtBlock(nn.Module):
|
||||
"""ConvNeXt Block adapted from https://github.com/facebookresearch/ConvNeXt to 1D audio signal."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
intermediate_dim: int,
|
||||
layer_scale_init_value: float,
|
||||
adanorm_num_embeddings: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.dwconv = nn.Conv1d(dim, dim, kernel_size=7, padding=3, groups=dim)
|
||||
self.adanorm = adanorm_num_embeddings is not None
|
||||
if adanorm_num_embeddings:
|
||||
raise ValueError("AdaLayerNorm is not supported in inference-only mode")
|
||||
else:
|
||||
self.norm = nn.LayerNorm(dim, eps=1e-6)
|
||||
self.pwconv1 = nn.Linear(dim, intermediate_dim)
|
||||
self.act = nn.GELU()
|
||||
self.pwconv2 = nn.Linear(intermediate_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: torch.Tensor, cond_embedding_id: torch.Tensor | None = None) -> torch.Tensor:
|
||||
residual = x
|
||||
x = self.dwconv(x)
|
||||
x = x.transpose(1, 2)
|
||||
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.transpose(1, 2)
|
||||
x = residual + x
|
||||
return x
|
||||
|
||||
|
||||
class VocosBackbone(nn.Module):
|
||||
"""Vocos backbone module built with ConvNeXt blocks."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
input_channels: int,
|
||||
dim: int,
|
||||
intermediate_dim: int,
|
||||
num_layers: int,
|
||||
layer_scale_init_value: float | None = None,
|
||||
adanorm_num_embeddings: int | None = None,
|
||||
):
|
||||
super().__init__()
|
||||
self.input_channels = input_channels
|
||||
self.embed = nn.Conv1d(input_channels, dim, kernel_size=7, padding=3)
|
||||
self.adanorm = adanorm_num_embeddings is not None
|
||||
if adanorm_num_embeddings:
|
||||
raise ValueError("AdaLayerNorm is not supported in inference-only mode")
|
||||
else:
|
||||
self.norm = nn.LayerNorm(dim, eps=1e-6)
|
||||
layer_scale_init_value = layer_scale_init_value or 1 / num_layers
|
||||
self.convnext = nn.ModuleList(
|
||||
[
|
||||
ConvNeXtBlock(
|
||||
dim=dim,
|
||||
intermediate_dim=intermediate_dim,
|
||||
layer_scale_init_value=layer_scale_init_value,
|
||||
adanorm_num_embeddings=adanorm_num_embeddings,
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
]
|
||||
)
|
||||
self.final_layer_norm = nn.LayerNorm(dim, eps=1e-6)
|
||||
self.apply(self._init_weights)
|
||||
|
||||
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, x: torch.Tensor, **kwargs) -> torch.Tensor:
|
||||
x = self.embed(x)
|
||||
x = self.norm(x.transpose(1, 2))
|
||||
x = x.transpose(1, 2)
|
||||
for conv_block in self.convnext:
|
||||
x = conv_block(x)
|
||||
x = self.final_layer_norm(x.transpose(1, 2))
|
||||
return x
|
||||
@@ -305,6 +305,17 @@ _OMNI_MODELS = {
|
||||
"dynin_omni",
|
||||
"DyninOmniForConditionalGeneration",
|
||||
),
|
||||
## IndexTTS2
|
||||
"IndexTTS2TalkerForConditionalGeneration": (
|
||||
"indextts2",
|
||||
"indextts2_talker",
|
||||
"IndexTTS2TalkerForConditionalGeneration",
|
||||
),
|
||||
"IndexTTS2S2MelDecoder": (
|
||||
"indextts2",
|
||||
"indextts2_s2mel_decoder",
|
||||
"IndexTTS2S2MelDecoder",
|
||||
),
|
||||
## Ming-flash-omni-2.0
|
||||
"MingFlashOmniForConditionalGeneration": (
|
||||
"ming_flash_omni",
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Stage input processor for IndexTTS2: Talker (GPT AR) → S2Mel decoder."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
STOP_MEL_TOKEN = 8193
|
||||
|
||||
|
||||
def _cpu_view(tensor: torch.Tensor) -> torch.Tensor:
|
||||
"""Return a contiguous CPU tensor suitable for connector serialization.
|
||||
|
||||
Always returns an owning copy so downstream consumers cannot mutate the
|
||||
source tensors that may still be referenced by the producer stage.
|
||||
"""
|
||||
out = tensor.detach()
|
||||
if out.device.type != "cpu":
|
||||
return out.cpu().contiguous()
|
||||
return out.clone().contiguous()
|
||||
|
||||
|
||||
def _strip_stop_token(
|
||||
codes: torch.Tensor,
|
||||
latent: torch.Tensor,
|
||||
stop_mel_token: int = STOP_MEL_TOKEN,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Strip at the first stop token, matching official IndexTTS2 v2.
|
||||
|
||||
Returns: (codes [B, T'], latent [B, T', D], code_lens [B]).
|
||||
"""
|
||||
if codes.ndim == 1:
|
||||
codes = codes.unsqueeze(0)
|
||||
if latent.ndim == 2:
|
||||
latent = latent.unsqueeze(0)
|
||||
|
||||
if codes.device.type != "cpu":
|
||||
codes = codes.detach().cpu()
|
||||
if latent.device.type != "cpu":
|
||||
latent = latent.detach().cpu()
|
||||
|
||||
device = codes.device
|
||||
code_lens = []
|
||||
codes_out = []
|
||||
latent_out = []
|
||||
|
||||
for i in range(codes.shape[0]):
|
||||
code = codes[i]
|
||||
lat = latent[i]
|
||||
|
||||
stop_mask = (code == stop_mel_token).nonzero(as_tuple=False)
|
||||
if stop_mask.numel() > 0:
|
||||
valid_len = int(stop_mask[0].item())
|
||||
else:
|
||||
valid_len = int(code.shape[0])
|
||||
code_lens.append(valid_len)
|
||||
codes_out.append(code[:valid_len])
|
||||
latent_out.append(lat[:valid_len])
|
||||
|
||||
max_len = max(code_lens) if code_lens else 0
|
||||
if max_len == 0:
|
||||
return (
|
||||
torch.zeros(codes.shape[0], 0, dtype=torch.long, device=device),
|
||||
torch.zeros(codes.shape[0], 0, latent.shape[-1], device=device, dtype=latent.dtype),
|
||||
torch.zeros(codes.shape[0], dtype=torch.long, device=device),
|
||||
)
|
||||
|
||||
padded_codes = torch.zeros((len(codes_out), max_len), dtype=torch.long, device=device)
|
||||
padded_latent = torch.zeros(
|
||||
len(latent_out),
|
||||
max_len,
|
||||
latent_out[0].shape[-1],
|
||||
device=device,
|
||||
dtype=latent_out[0].dtype,
|
||||
)
|
||||
for i, (c, lat) in enumerate(zip(codes_out, latent_out)):
|
||||
padded_codes[i, : c.shape[0]] = c
|
||||
padded_latent[i, : lat.shape[0]] = lat
|
||||
|
||||
return padded_codes, padded_latent, torch.tensor(code_lens, dtype=torch.long, device=device)
|
||||
|
||||
|
||||
def _normalize_mel_sequence(mel_codes: torch.Tensor) -> torch.Tensor:
|
||||
mel_codes = mel_codes.to(torch.long)
|
||||
if mel_codes.ndim <= 1:
|
||||
return mel_codes.reshape(-1).contiguous() if mel_codes.ndim == 0 else mel_codes.contiguous()
|
||||
if mel_codes.ndim == 2 and (mel_codes.shape[0] == 1 or mel_codes.shape[1] == 1):
|
||||
return mel_codes.reshape(-1).contiguous()
|
||||
return mel_codes.reshape(-1).contiguous()
|
||||
|
||||
|
||||
def _normalize_latent_sequence(latent: torch.Tensor) -> torch.Tensor:
|
||||
if latent.ndim <= 1:
|
||||
return latent.reshape(1, -1).contiguous()
|
||||
if latent.ndim == 2:
|
||||
return latent.contiguous()
|
||||
if latent.ndim == 3 and latent.shape[0] == 1:
|
||||
return latent[0].contiguous()
|
||||
return latent.reshape(-1, latent.shape[-1]).contiguous()
|
||||
|
||||
|
||||
def _build_s2mel_additional_information(
|
||||
mel_codes: torch.Tensor,
|
||||
latent: torch.Tensor,
|
||||
meta: dict[str, Any],
|
||||
*,
|
||||
context: str,
|
||||
) -> dict[str, Any]:
|
||||
"""Build the Stage-1 S2Mel tensor contract shared by legacy and connector paths."""
|
||||
mel_codes_clean, latent_clean, code_lens = _strip_stop_token(mel_codes, latent)
|
||||
|
||||
additional_information = {
|
||||
"latent": _cpu_view(latent_clean),
|
||||
"mel_codes": _cpu_view(mel_codes_clean),
|
||||
"code_lens": _cpu_view(code_lens),
|
||||
}
|
||||
|
||||
for key in ("S_ref", "ref_mel", "style"):
|
||||
val = meta.get(key)
|
||||
if isinstance(val, torch.Tensor):
|
||||
additional_information[key] = _cpu_view(val)
|
||||
else:
|
||||
logger.warning("[%s] %s MISSING — Stage 1 will use fallback", context, key)
|
||||
|
||||
return additional_information
|
||||
|
||||
|
||||
def _get_payload_value(pooling_output: dict[str, Any], dotted_key: str, nested_parent: str, nested_key: str) -> Any:
|
||||
value = pooling_output.get(dotted_key)
|
||||
if value is not None:
|
||||
return value
|
||||
nested = pooling_output.get(nested_parent)
|
||||
if isinstance(nested, dict):
|
||||
return nested.get(nested_key)
|
||||
return None
|
||||
|
||||
|
||||
def _request_id(request: Any) -> str:
|
||||
return str(getattr(request, "external_req_id", None) or getattr(request, "request_id", "?"))
|
||||
|
||||
|
||||
def talker2s2mel(
|
||||
source_outputs: list[Any],
|
||||
prompt: Any = None,
|
||||
_requires_multimodal_data: bool = False,
|
||||
) -> list[Any]:
|
||||
"""Legacy orchestrator path: collect all Stage 0 output, format Stage 1 input."""
|
||||
_old_nt = torch.get_num_threads()
|
||||
torch.set_num_threads(1)
|
||||
try:
|
||||
return _talker2s2mel_impl(source_outputs)
|
||||
finally:
|
||||
torch.set_num_threads(_old_nt)
|
||||
|
||||
|
||||
def _talker2s2mel_impl(source_outputs: list[Any]) -> list[Any]:
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
s2mel_inputs: list[OmniTokensPrompt] = []
|
||||
|
||||
for i, talker_output in enumerate(source_outputs):
|
||||
if not talker_output.finished:
|
||||
continue
|
||||
|
||||
output = talker_output.outputs[0]
|
||||
mm = output.multimodal_output
|
||||
if not isinstance(mm, dict):
|
||||
logger.warning("Talker output %d has no multimodal_output dict", i)
|
||||
continue
|
||||
|
||||
codes_dict = mm.get("codes", {})
|
||||
mel_codes = codes_dict.get("mel")
|
||||
if not isinstance(mel_codes, torch.Tensor) or mel_codes.numel() == 0:
|
||||
logger.warning("Talker output %d has empty mel_codes", i)
|
||||
continue
|
||||
mel_codes = mel_codes.to(torch.long)
|
||||
|
||||
hs_dict = mm.get("hidden_states", {})
|
||||
latent = hs_dict.get("latent")
|
||||
if not isinstance(latent, torch.Tensor) or latent.numel() == 0:
|
||||
logger.warning("Talker output %d has empty latent", i)
|
||||
continue
|
||||
|
||||
meta = mm.get("meta", {})
|
||||
if not isinstance(meta, dict):
|
||||
meta = {}
|
||||
|
||||
additional_information = _build_s2mel_additional_information(mel_codes, latent, meta, context="talker2s2mel")
|
||||
s2mel_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0],
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
additional_information=additional_information,
|
||||
)
|
||||
)
|
||||
|
||||
return s2mel_inputs
|
||||
|
||||
|
||||
def talker2s2mel_token_only(
|
||||
source_outputs: list[Any],
|
||||
prompt: Any = None,
|
||||
_requires_multimodal_data: bool = False,
|
||||
) -> list[Any]:
|
||||
"""Sync-side placeholder for Stage 1; tensors arrive via full-payload connector."""
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
|
||||
del prompt
|
||||
s2mel_inputs: list[OmniTokensPrompt] = []
|
||||
for talker_output in source_outputs:
|
||||
if not talker_output.finished:
|
||||
continue
|
||||
s2mel_inputs.append(
|
||||
OmniTokensPrompt(
|
||||
prompt_token_ids=[0],
|
||||
additional_information=None,
|
||||
multi_modal_data=None,
|
||||
mm_processor_kwargs=None,
|
||||
)
|
||||
)
|
||||
return s2mel_inputs
|
||||
|
||||
|
||||
def talker2s2mel_full_payload(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any],
|
||||
request: Any,
|
||||
**_: Any,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Build the complete S2Mel input from accumulated per-step talker deltas."""
|
||||
del transfer_manager
|
||||
rid = _request_id(request)
|
||||
if not isinstance(pooling_output, dict):
|
||||
logger.warning("talker2s2mel_full_payload: pooling_output not a dict for req=%s", rid)
|
||||
return None
|
||||
|
||||
mel_codes = _get_payload_value(pooling_output, "codes.mel", "codes", "mel")
|
||||
if not isinstance(mel_codes, torch.Tensor) or mel_codes.numel() == 0:
|
||||
logger.warning("talker2s2mel_full_payload: missing codes.mel for req=%s", rid)
|
||||
return None
|
||||
|
||||
latent = _get_payload_value(pooling_output, "hidden_states.latent", "hidden_states", "latent")
|
||||
if not isinstance(latent, torch.Tensor) or latent.numel() == 0:
|
||||
logger.warning("talker2s2mel_full_payload: missing hidden_states.latent for req=%s", rid)
|
||||
return None
|
||||
|
||||
mel_seq = _normalize_mel_sequence(mel_codes)
|
||||
latent_seq = _normalize_latent_sequence(latent)
|
||||
if mel_seq.numel() == 0 or latent_seq.numel() == 0:
|
||||
logger.warning("talker2s2mel_full_payload: empty normalized mel/latent for req=%s", rid)
|
||||
return None
|
||||
|
||||
common_len = min(int(mel_seq.shape[0]), int(latent_seq.shape[0]))
|
||||
if common_len <= 0:
|
||||
logger.warning("talker2s2mel_full_payload: no common mel/latent length for req=%s", rid)
|
||||
return None
|
||||
|
||||
mel_seq = mel_seq[:common_len]
|
||||
latent_seq = latent_seq[:common_len]
|
||||
|
||||
meta = {
|
||||
"S_ref": _get_payload_value(pooling_output, "meta.S_ref", "meta", "S_ref"),
|
||||
"ref_mel": _get_payload_value(pooling_output, "meta.ref_mel", "meta", "ref_mel"),
|
||||
"style": _get_payload_value(pooling_output, "meta.style", "meta", "style"),
|
||||
}
|
||||
return _build_s2mel_additional_information(mel_seq, latent_seq, meta, context="full_payload")
|
||||
@@ -303,6 +303,7 @@ class GPUARModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin):
|
||||
"Qwen3TTSCode2Wav",
|
||||
"CosyVoice3Model",
|
||||
"DyninOmniForConditionalGeneration",
|
||||
"IndexTTS2TalkerForConditionalGeneration",
|
||||
}
|
||||
if getattr(self.model_config, "model_arch", None) in _OMNI_CONNECTOR_INIT_ARCHS:
|
||||
self.init_omni_connectors(
|
||||
|
||||
@@ -64,6 +64,7 @@ class GPUGenerationModelRunner(OmniGPUModelRunner, OmniConnectorModelRunnerMixin
|
||||
"Qwen3TTSCode2Wav",
|
||||
"CosyVoice3Model",
|
||||
"DyninOmniForConditionalGeneration",
|
||||
"IndexTTS2S2MelDecoder",
|
||||
}
|
||||
if getattr(self.model_config, "model_arch", None) in _OMNI_CONNECTOR_INIT_ARCHS:
|
||||
self.init_omni_connectors(
|
||||
|
||||
Reference in new issue
Block a user