From 044240d8ebeae9c19a7be01db9d37a1cd1a57c8a Mon Sep 17 00:00:00 2001 From: BeatSeat <137616995+BeatSeat@users.noreply.github.com> Date: Tue, 23 Jun 2026 03:24:01 +0800 Subject: [PATCH] [Model] Add IndexTTS2 text-to-speech support (#3838) Signed-off-by: BeatSeat Signed-off-by: David Signed-off-by: wendavid552 Signed-off-by: WenDavid --- .buildkite/test-merge.yml | 36 + docs/models/supported_models.md | 1 + .../text_to_speech/README.md | 45 + .../text_to_speech/indextts2/end2end.py | 187 +++ .../online_serving/text_to_speech/README.md | 39 +- .../text_to_speech/indextts2/run_server.sh | 25 + .../text_to_speech/indextts2/speech_client.py | 126 ++ recipes/IndexTeam/IndexTTS-2.md | 139 +++ recipes/README.md | 1 + tests/assets/indextts2/ref_audio.wav | Bin 0 -> 478050 bytes .../sched/test_omni_scheduling_coordinator.py | 1 + tests/e2e/offline_inference/test_indextts2.py | 96 ++ tests/e2e/online_serving/test_indextts2.py | 82 ++ .../stage_input_processors/test_indextts2.py | 336 ++++++ vllm_omni/config/pipeline_registry.py | 4 + .../core/sched/omni_scheduling_coordinator.py | 3 + vllm_omni/deploy/indextts2.yaml | 57 + vllm_omni/deploy/indextts2_low_latency.yaml | 76 ++ vllm_omni/engine/arg_utils.py | 6 + .../entrypoints/openai/serving_speech.py | 7 + .../openai/tts_adapters/__init__.py | 1 + .../openai/tts_adapters/indextts2.py | 243 ++++ .../models/indextts2/__init__.py | 2 + .../models/indextts2/bigvgan_cuda_graph.py | 284 +++++ .../indextts2/configuration_indextts2.py | 156 +++ .../models/indextts2/dit_cuda_graph.py | 164 +++ .../models/indextts2/gpt/__init__.py | 2 + .../indextts2/gpt/conformer/__init__.py | 2 + .../indextts2/gpt/conformer/attention.py | 288 +++++ .../indextts2/gpt/conformer/embedding.py | 132 ++ .../indextts2/gpt/conformer/subsampling.py | 64 + .../models/indextts2/gpt/conformer_encoder.py | 422 +++++++ .../models/indextts2/gpt/embeddings.py | 20 + .../models/indextts2/gpt/perceiver.py | 149 +++ .../indextts2/indextts2_s2mel_decoder.py | 894 ++++++++++++++ .../models/indextts2/indextts2_talker.py | 1062 +++++++++++++++++ .../models/indextts2/pipeline.py | 49 + .../models/indextts2/preprocess_utils.py | 324 +++++ .../models/indextts2/prompt_utils.py | 47 + .../models/indextts2/s2mel/__init__.py | 2 + .../indextts2/s2mel/modules/__init__.py | 2 + .../models/indextts2/s2mel/modules/audio.py | 107 ++ .../models/indextts2/s2mel/modules/bigvgan.py | 359 ++++++ .../models/indextts2/s2mel/modules/commons.py | 73 ++ .../s2mel/modules/diffusion_transformer.py | 251 ++++ .../models/indextts2/s2mel/modules/encodec.py | 122 ++ .../indextts2/s2mel/modules/flow_matching.py | 201 ++++ .../s2mel/modules/gpt_fast/__init__.py | 2 + .../indextts2/s2mel/modules/gpt_fast/model.py | 303 +++++ .../s2mel/modules/length_regulator.py | 152 +++ .../models/indextts2/s2mel/modules/wavenet.py | 88 ++ .../models/indextts2/tokenizer.py | 56 + .../models/indextts2/utils/__init__.py | 2 + .../indextts2/utils/campplus/__init__.py | 2 + .../models/indextts2/utils/campplus/dtdnn.py | 117 ++ .../models/indextts2/utils/campplus/layers.py | 221 ++++ .../models/indextts2/utils/common.py | 88 ++ .../models/indextts2/utils/front.py | 208 ++++ .../indextts2/utils/maskgct/__init__.py | 2 + .../indextts2/utils/maskgct/quantize.py | 200 ++++ .../indextts2/utils/maskgct/repcodec_model.py | 120 ++ .../models/indextts2/utils/maskgct/vocos.py | 102 ++ vllm_omni/model_executor/models/registry.py | 11 + .../stage_input_processors/indextts2.py | 271 +++++ vllm_omni/worker/gpu_ar_model_runner.py | 1 + .../worker/gpu_generation_model_runner.py | 1 + 66 files changed, 8634 insertions(+), 2 deletions(-) create mode 100644 examples/offline_inference/text_to_speech/indextts2/end2end.py create mode 100644 examples/online_serving/text_to_speech/indextts2/run_server.sh create mode 100644 examples/online_serving/text_to_speech/indextts2/speech_client.py create mode 100644 recipes/IndexTeam/IndexTTS-2.md create mode 100644 tests/assets/indextts2/ref_audio.wav create mode 100644 tests/e2e/offline_inference/test_indextts2.py create mode 100644 tests/e2e/online_serving/test_indextts2.py create mode 100644 tests/model_executor/stage_input_processors/test_indextts2.py create mode 100644 vllm_omni/deploy/indextts2.yaml create mode 100644 vllm_omni/deploy/indextts2_low_latency.yaml create mode 100644 vllm_omni/entrypoints/openai/tts_adapters/indextts2.py create mode 100644 vllm_omni/model_executor/models/indextts2/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/bigvgan_cuda_graph.py create mode 100644 vllm_omni/model_executor/models/indextts2/configuration_indextts2.py create mode 100644 vllm_omni/model_executor/models/indextts2/dit_cuda_graph.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/conformer/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/conformer/attention.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/conformer/embedding.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/conformer/subsampling.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/conformer_encoder.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/embeddings.py create mode 100644 vllm_omni/model_executor/models/indextts2/gpt/perceiver.py create mode 100644 vllm_omni/model_executor/models/indextts2/indextts2_s2mel_decoder.py create mode 100644 vllm_omni/model_executor/models/indextts2/indextts2_talker.py create mode 100644 vllm_omni/model_executor/models/indextts2/pipeline.py create mode 100644 vllm_omni/model_executor/models/indextts2/preprocess_utils.py create mode 100644 vllm_omni/model_executor/models/indextts2/prompt_utils.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/audio.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/bigvgan.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/commons.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/diffusion_transformer.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/encodec.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/flow_matching.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/model.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/length_regulator.py create mode 100644 vllm_omni/model_executor/models/indextts2/s2mel/modules/wavenet.py create mode 100644 vllm_omni/model_executor/models/indextts2/tokenizer.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/campplus/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/campplus/dtdnn.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/campplus/layers.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/common.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/front.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/maskgct/__init__.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/maskgct/quantize.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/maskgct/repcodec_model.py create mode 100644 vllm_omni/model_executor/models/indextts2/utils/maskgct/vocos.py create mode 100644 vllm_omni/model_executor/stage_input_processors/indextts2.py diff --git a/.buildkite/test-merge.yml b/.buildkite/test-merge.yml index 84c93a2f3..f2d5e972b 100644 --- a/.buildkite/test-merge.yml +++ b/.buildkite/test-merge.yml @@ -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 diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md index 7c5db00d6..79c44d740 100644 --- a/docs/models/supported_models.md +++ b/docs/models/supported_models.md @@ -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` | ✅︎ | ✅︎ | | | diff --git a/examples/offline_inference/text_to_speech/README.md b/examples/offline_inference/text_to_speech/README.md index 3da75bc0f..ea1d36051 100644 --- a/examples/offline_inference/text_to_speech/README.md +++ b/examples/offline_inference/text_to_speech/README.md @@ -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. diff --git a/examples/offline_inference/text_to_speech/indextts2/end2end.py b/examples/offline_inference/text_to_speech/indextts2/end2end.py new file mode 100644 index 000000000..858487ae1 --- /dev/null +++ b/examples/offline_inference/text_to_speech/indextts2/end2end.py @@ -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()) diff --git a/examples/online_serving/text_to_speech/README.md b/examples/online_serving/text_to_speech/README.md index 1c180440f..1a3096fab 100644 --- a/examples/online_serving/text_to_speech/README.md +++ b/examples/online_serving/text_to_speech/README.md @@ -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 --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. diff --git a/examples/online_serving/text_to_speech/indextts2/run_server.sh b/examples/online_serving/text_to_speech/indextts2/run_server.sh new file mode 100644 index 000000000..4a17b3b04 --- /dev/null +++ b/examples/online_serving/text_to_speech/indextts2/run_server.sh @@ -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" diff --git a/examples/online_serving/text_to_speech/indextts2/speech_client.py b/examples/online_serving/text_to_speech/indextts2/speech_client.py new file mode 100644 index 000000000..b938f0bd3 --- /dev/null +++ b/examples/online_serving/text_to_speech/indextts2/speech_client.py @@ -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() diff --git a/recipes/IndexTeam/IndexTTS-2.md b/recipes/IndexTeam/IndexTTS-2.md new file mode 100644 index 000000000..54c1119dd --- /dev/null +++ b/recipes/IndexTeam/IndexTTS-2.md @@ -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," + }' \ + --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,", + "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. diff --git a/recipes/README.md b/recipes/README.md index 30f1577be..4da489c59 100644 --- a/recipes/README.md +++ b/recipes/README.md @@ -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 | diff --git a/tests/assets/indextts2/ref_audio.wav b/tests/assets/indextts2/ref_audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..d13a4c84749a421d50c2885d6024ab7e33eb2e35 GIT binary patch literal 478050 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%0DJ2TS~L%uM8&t+#KekH`Gl1HJ^IqfBBlpKs#!l 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

>IU5(af=o=zfM)Q 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�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%0DOG9qeY5ds^%5zI8 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‎_%uJ|72 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

tbRTVy?{OU^DhSH z%+&nmMY9!LIPe<4x4h=N4PQt2n_%DBjLahV1vn)%hlUv$)B2tYeB{O8gpFk5p+mT`Eik9=Kq< z+)5cOwW_=zr=FdtbDMLY!xQ%8BKatCrBrIOPR?v!Bk(wvA<%9NNq9!!4InsYE<8%W zyAM+B9{G9r+zx&7IhPHFF6;EQ7gUGI`#0pdIkGlxp7vLFF07Vz)i=oOqU(T}uGISm zwHhM-Cfsq2QfSg`X2Tk)jT-OZ1Ema8htl@?e(Lh(0Vk$koSlodKz*9A-c~`Oyl4 z8j{|K_Lp;6?9|Y6$k|z@eg?pUz2xe#CX7FkC=V}763lOeURA^(j+}m4_f2S(1p0dE z-_NH#((15G!TqTFHf8~;69UiNrIw9#0(fKYnxt})964I9?)Aysmw%AQXFle?@KkmG zqz%!2%;o%R)_VPEuY~(wU0L2==BdJd_!s;4U-?0zBQ8jxiRYx%o1aMQJ14a-8!{&o z{mw>#SDEg;@MnNuf(s0Mn?R>36EClmX|Jr8m2n^k?_+O(2Ev}}p2dbda#eqSvD9|eioD7*R7AfH}mHwsQ(fD1{)Un$X$lK3#>IJt=b`zeBdk7Ys7v7ES~wq z4*gh{|FBjLbv-D1F71$(m!DTWliDj_QZ8|W-Ww*$m%WaQZ%1zbwO>p5+g>c?5A2l3 z4=yJ5!MOqtK+e~|N9_$C(dBN;&V16fcv}INMdr4csW{favm2#z**(lJlk!#X058bz z&o`>HA9`8;yeSp@9}X+!A3C9cKW4~7{*V{HmAapRPj&SjLFR*AfM2Pg~QAd&R;R-(rhu*&cLz880lQ;9(iYIocmXbVQV64p3^eOju zRft(vYO>D!-Z=iQ&N8SqxlrwU%m=etzP71x-Q=FLUo&!mtr~FG%#04x46u0|cEZUIgcn;V%=cAk((4!&SfN)I*VmCu_n z3AM&x!OT+V3*H}@3{?-f>OVFJzK!I{a}LK^kKDeL5yJ%fN6gPA%a**0bgvGn>(kkI z;{JD0zq~8_Ua0qh-EC5gjJ+3iG3afS^FhxX)cS&%fq>I29&iim9$zW5V^_%7+ONp) zv2$cgiT22t$cp)0cQd%sEd%dT+bN;G_BrI(!Eevt9BZZlc*N51{RqzHH3Nw}G1Pw4 z6H!mH1D>_cL3sQZetnuj4P-K>eGlK~pMc$7(LN|8>X-=#J8I56bk?x%$?-&9-Fv5% zt7^yit6BK+T5w}**^0Xjd{^l=?Qo7tZaQ)X9nQ^SUps8F#pbY`N)@-L!E`-^#=~OW zuq*pkuoFW9?ZC$h=`{*o06cdWs;)6>;06P(qj{@R4hv44MOL51zHJJp&s845WG0i}fscQqZBacF-GWjwxn+nam!bw)c_aaqw80V-ABOf?Ca>zc34meuDW! zEb?t@D@KLqs&_}o^P>Jy=#|{drM_|L5xWkvuP|qmfMX>u&HPSI<@rt)KXOihU&yfu z+vLW!xq`X1=AY7&qR5-b9A5sIL=NsMqwc(<*T643F38{>KS^|#EdG%utAEzi0Dnlq zpE;Wl@KOYP66hqZOUU6{a%tHuNj`irh zep_k$BX(_EF}wMR%GMnoYLQ21w`DJG7tO`}k#Cb3@NKNgadsDGe*vmfV6Gu&x!|jq zjTdj29vOvghqwy1a8$4bhs|zlUelfo2(!S=Y~-r^cG;e94DfHA^N#vymdq->Uomq#Wvf&4$|?t1cKS3qpZs!xp3LkK>6?}b?X1ge z8O{=^gQ8~zv&k_#QLhkkeiB9p8um7*&)_}(f_(bjYx3SpuS@rbrwP_)34|t-HGiKP zW0m*Co}@*+JzSUl&~5vX6XeHy)IY3YcK`BU&j^mKyxw{O_PHL)6J`$Vrls-PuTLsJ zOQ4Go_(IDQZ@ewgoJ#qRfioRBECbdZmAa)4OO=@iqe0{NxS%-)P4G>eEH2O z)^zZ5JEip_GWrDb3tKFsrM7sc=8j$Gai@8bqw=O96^MV7? ze}lNd+uVKdmz#P%RFdI6gWf}Hc=YVj;l^P2x+Ox>P)5){XD&Y6)8N{(bx&J+=AozU zi0SQZ_K(8tYi%F0{I?ADA(QynZF6wi!}h!E1$Fis>?v%!zWcCc zA5^^RJk8r79)+G&v((8+!z>Q@q9u{h(A$tx!K``KCOzvPF{qJEp~(F9=)RJ6?xCVK zchIi}nhisq9gYwEN65$C0gn-Ch|J;_wwf7<@0vc_GY#?rOzvW>!Oz^od&7tMjyvRF zUft*Sy;4y#-?@Grj>)PS8@LYPi}?xli5~J`nN3YkJZ3X8QwM%L4zMli0~}&@m>);8 z=3*_+89V1Z-1n(jA=bSz>VUvQMk1p=(zUGKlkdu{p{_K2bDa!^kN;EU5;4>N723*Y ze9Ri9w+c8crrPYU9r`T;8*rNqEhp!n9w!~oZZEKwRqRsrz8&G|rpb z79Qzb3yFDEjC}*1|9b`6328R*G}kfd-)ocX+&qU~=IU2YA2VuR=%GPvd9}u&vLPuJ zc_KsQo$`|e{#fK5DBq_oW@#}6E{H$uA^-jkIsIi$1^Cf#X#dpzv$1Z4`E|`I&)2%H z-Wb`)Hmh65jt$FWQ6IoRJ}bQUNBFpB(T@V3OLN=CV5SQUSo4?Ytwrq(&Ij)kCm5Vu zXz5JOj+>PaymISa^(gqF#z&^W``?=BA7;0Guava)X4kY$nm4cyoo`^9`y=f3ECnp_ zbj&*pc%xp-6O>a?azeWbi~N|m-l_ZPCc$7Qw5 zngE~N(vqurgAV*~8N3|{aSv}~v=Py=UN zyhCpYAHC%0n-%hFlx8w93)Z%WF9vuq;FU_YO2;K-)x-WVQKkJ|I#%}IiVX7CotoFr zeV810=+~i}s>r##7ydW&8vsYVH~ek#>pULUt@hk`&R!Gm={){T(@Xe&FctloyQ}*T z%?a@btf=9id8)kM^KoH+)>GO1-JiND&>v`jzi9G4=$SW5neHz^BQ-&Qi_6Db9~aEM z(d%VVpRd$Sjb)N|n6&9VLtdNr5@tQibw6g$HLTr1iQM|3fa`&|>p>|Ct|n?Vfv1cV z-ubRXc=igkMe<7I23fNb-rK;%=*M~!b3J>IZK24l;`}V|L|iEK*#r%NSpFW*|FJ(4{%>DIPNl^EdcQOiixE>?rEBnHj2{m+Zsn?GBzg zHM;PCXs6mB@+mSFw-9mD3y|U`mx0u!IpHaN1KP;n^zf$wU{!u?> z_m9ba31(vZG+XqS9X(ZZ`1F!b)FX}k5q)vlXR$u)9g`uUKa3Y}>xkt_LgQ6Zf6i_h z`NTQq9m!Lza`>`WHW!!+)dYZRzX(O!>a3pT&=L4jE8VxkEe6Dc`j=n|joN z&!wDndd$6zStm8L)IXAIOP$kh?7f^7Kzji^tEa&8Q_wdE@IcizazCZ-EPX1$Rg#En zrPco~7@8$!H4xjUH+=Q{Ps1m$$6XA+=P;Rbbg5)tx>ug8d{W-NvRi$^;Zy3uL(=%K z<+A5{HntU(#o78N+gW5Y*~5?R(3v^)`9W(sbzkj_I}QfI0Z-5rf&G`EqU!du2>X zTeWFbyJ$>hons9xan=AMF{p3M#&y3q)bydoHi4a9G&|m0Wcn|kjI7P3cm34i{YYJK z6?oRr@0Ywb`fxx;&c4W<0u2UhOz!dYh#`ju@|OVm{=)|xGk~hjr1FIw_));?BOhmG z%y;q9Czf7O^bVya6SW<3DrS1YA@;4uOuN(9T?P2BRExbJrO)Q_gJS!2p$uz7hW;&QMjUeE_|C+qu!+6Orcf?+G^c1 z!TAS=$(1UP9gzbUK9?*JSD0%_y?{h*ct_xWqx=@~%9srWJ}qlTAHA1w&Y^Y7>Orpx zy|ChkD4q%*R>r}Tof zE3*u-nX`u2({DDnsEIB8ak@hz%6Uec@E}R)QCMeKx372vb%5k3kqOV5v#K5Gd2=yl zy7|B(YT>-l1b&0feP&~itEPYtxaOVG_C&)P_QKO13q6$`Gp>_8AC_cuckN|cH129E zeek&AN|I!09Sw z=1hJJJ=SpLlw-z@-sYH#I+vO-U}Q6?GxAm2w;9@;_Nq_lcJ5c%y05Gs-fezhZT^h; zwfvJRm-a)0B?soO7Rz(csWYz+vd>mN7_|#n z|1>|l6f*2QpVv@M0X>`GwIuYi=QWv%^$@;M@c11rnS}-l{jb?Kfoln`y}6R@$;HsS zEfw&6v^V!(pC=Ux&KCF-Ywi(zx^!05Ie3K3Ek6Vr?2!U*d4V5?6d2 zsDUedqomEgFx0*i?AY+z*2b=hvhkQRu#eBy<3r{#tAAXDW-mDC3!Mt^LsfoE6l|K4y?L%)|up_jR|lhs$TZgdwLErAyEfgrXznmzicDWKZ z`V4s7U(5d2uF1+QKgkpIev}KVFUh0Ok9F$0M)4}(Y|N9R*O9ly&U#*WE4kb=$)SZ8 z47@m33vzrk%kXMWEAS?k`K`j)2Po%=vm%^t+ z2RXZ}WA=C7eivha&H=h&r$x=l26eMp_V8tc^_-rswrz4QTWL}eiy4t#vkP9Vrg%}k z*2OLIl-cfoaa-t0X}y`VVDJOwPY z!4q{Xd$~$;8r#VAk@oRpMQzqT;GgWDVxYk=$UM<3kgIPNHqeczo+0ALw~ae$yTRTCL&L?|su;-K{cN_Iad9}cEc7O1f9Qo~*SQ|BPBj+PY5+gdJVy4@ z)I`&J1b&?k=E2lJJIM`QxV*jZ0fE1yW(gdDE}pp+oXL>G zpFAi#xI#Bod%b+itIXd3XK|~`Tu%5&%cFG`%ev*;1=yV+FAtt>?<#(l{DNP$HQy}7 z!q>>BHC~ll8Jl$`1bjvU`+g(ItuARzvlVj%^n6%{uM6~miq}^9=u_o3Fdu|HLB50U zoAp7pZ1JCy?ZF-?wo1Vmo9$>8`}XFh23gCla{QtO3A{(NF2{bYp6K+tyh^TtaJ1yd!EL;%rDN9bL6wMw$cP1|M*VKxlnIcekW?1 z;(*5iKVVLQhilSSc*B6#BIMF$`*6PM93>k1fmw^-Q#4(DBgPyYfcbyC@~NAER~r)f zB68yA2r>qAR!X0K`czzi?^4ynjWpXT2XqshDbW*ldBh6&`pMVj>Dcv(6i+qAZ1N;FBekw|c2y4O?Ve9ouZT*RHM+Zs8kZ z;TxfKU@|{ZSVEr&So@VB;7m0>y1J9P`#vR{H zbuGBA$RAPfDd>=;0rugJx_ZYa!*w63gc%$bttdFpE0?#c@EevxagnHvq zc{{3JYjLwK%Jr-Q8~`?XH9_F^Li3r`RC{EnSrb^q&Y2TwKgy_M59cjrhfmw1 zKJE0;W#34Q`l(kgc%d=WtTgnj9`wF0y@=@FNgqOTQGic-mq%1I7pIMekIsBUKOA6N z;N@>M3#K125ik5`fbZGJZ>#Ec3T=&zteD5A!qCt%&Xwp0qyTEKCZ3g7T|p2yuYBHa>StMnf!HO z_~TsoS=yH`$x>8tHUUE#&7oPAU@ z>uKE6T*KA33imXx3Gsgy;17H>z|WqEz6jKn{1UuHdj7mhkU=Ult_;&zPKEPV)f*h= z$@>)Yt6C#h5AkAOhaAvnLX+!5BnB092^W0_;A0u%x9oaC4Cg&qpJ7=>PNSI z#&CFb`6S<@gVJfqI?Xks2P5!k1ALbpI@OPt13%e)Ba5xwyNF#snj2-i_fj6hAk!867 zKCJmP_8p`ZR=8@1BmGvm@8DjSO*{I25-Z1jYHMe43 z!{?RD`2+k9A{yS5rEg^SgTvzAG9jnGMs#+6uROm>+uPsD_3s65GQ8yWd$;GPqId%QGc~`Py3wbm<}}y?4YQqqi5qwn!TYZo*2Pd$>*sib8r(QN z-d7&~u9Q3$+6%kkvD|jZ(ytBXie^f0W01}2P{SJI%TC@5J>9(E%))ms)`0J3+EyG# z4+4Ws5%a^4#RfA>bLgXeCh6g~%)a^L6&`}&Z)plHaG9=GMu5o zHv{>NRW%bCIEaJ#&%ynvo-teC75_bQi@_OGeGzdW*6+||dZBM}z<*LMFTI7R(R>!U zHCRv8&k|Z3FW0Cs&}V(wrIhAy;xm$;qFe{yurB$(h2JQyy5)FqO_|S>Z|`d5&v7nD zT?J>>o5W1eC^hxddD(iVznnbutXy84Dy>F74X>v9nw7~s3iPjcF}RqX>U#(dvjYy8 zw=TS$iBXZuiW!?A<3x~GEAU5DPab6B$i_CM5cTC5Ao6`-dl`UiM8Op%TiGZkYa*86#_%R1k>JZZha zKR|fR_e~2$zSmCp9X)JQE*7&#nikZa1vQfTOw-F1nql&hy*y^fNrcz;X=Yw)wk-0B zbl*Vl2al{CPJwrf0d8UF1C4s!0QXUkuYING8EUVyt~-adHk)b*%guSzLOZXXFwog) zwiA7s`+t!ib^fhr_^o?^VL3BWendWBGxdkQ1CPp$%MO}?Q@%kacUC*1ULLJsqt9ly zEt-65!09mL6rh$e$g_qIQwlUMqYe4fUv-?Rc-HZmJq$9pfL|3c_Jf>e6?h8WpqYYC zqsiRcz(DVed@`>=hJ%40wu8M)J!|1N>~M}xJqkD#ne-*3M=Gv=7d5rZ+7tIau?sim z{UpuS<;)a3>K%Bz=G7u^5uSZ-v6lAnKAlyvw)$ACUkY8`PdjDc(rq%Q%_3p%(&?4j zm@npsUgROv_W3o-i`gOcb7tMQyZ@_FZQSe1SMECbB|RTk3bcVIWiHAs=j(ej&>h2jvyW1|ZQdOu(Q zWsoVJUv=Tw;}oY$8S#s1=g{}6#)}+Vay_tTsy2+gKjLSohkNiWyqF(Wk4^Sr=wIaR71NN7vqq5H44wM{^?e$5eJOmTpfRY~#S5;Xx!Lp@ z_W8=TbQQU65}CFB&g-O4MX?{Rt>D&E&~3+2Ecx5w(^Yc)OXvay!iH z3w*|t^tu60%uKlOg4PGfc0+DJTlL?=ngf1echlwL2H@Np?&8*>3KvkDi(ZycDXgZZ(;y%u$>7kn8HW(Gdap-|(xxZhD9 z%!Q6`0yroIh_!pMMmv~YLnr7%-CNcHmhSR8L0^gWaUbxQlQRCNy*g_`&NuRG7W$q( z@w!940cscUg`Q`fj-i}YmF+@KOQV}LoN z3iOAvsPhQTxBeQLYMLhaJkWE{pF->GLUW{RH@#?Z{AK#WgV76$8^MFI){R$q$J(2T(e$64z39~;R zXLa;?phq=%waAu%-r}qa{Xyo@qwhJ;o0usz`>1yjj;X=9G9j(5nwj)$VxkiO41Jt~{sfs|4di{~6uorpd)%wdGx4`m`D91j6Qi_F*%Z`6{e{s> zYd#J0PWv|GEx~tai1x#%dDY)xApCsr{+bb^@;367%5xF6*V;@b{*+;mSLuwU@$7MyOu`KS<6m2oUB0 zwqWah+Nl!_59;qYZixWJzx@$O1MsYrx|oEgslNB}4yG z>a5_$sQDbg2)*FKWYS+Bb*>9t5A&>Ct^@RP;NSFSgGLHjCNVB%gqg(4f{`(wIkcde z7GGVtWW)^ULk>I`Sr>Vsad_B$@zzJgx74fdt*;jEmFOeQ~HzP}Jmq1gb^D|(3>NUq}3jLA$2)yE$4~{%%@K8s(z=7b^SsF8zqrM3N z-}(4^-e17Y)ZcpB5*Eg@%nhw&Zo$kb;!T(+@Y8~ZKG_T$VeOUU157@;U47K zwKMRn(M(wKm{GeNc#~nR4Kz1~l+ny&=(x>lPj8d#nWWwWx!{$(_LG^4`(oy;y4O-G?qbqy zec!-?>neJ~)8_dGufn6ZlQ}W*LhqxL%*5LIr(g7yn&^H_Pgw{A%I}N;uPz#m9zA+OSIb(gqF6iC%{<&1yIbfE~ zE4o1c&ly;uy=P?8XXgcLjeOJPeTnR|LcHrbb0)?;+(-Q+Y8Yx7RbRv$On8uJF9*Lr z2?3{zIi9Sk_rI2=o}$1Jbq;%R!DiKU*kLoVH}S(4!js3e864J=|jFn?7bg zz=O~MKgaN6Fe?F=p3BT9a%8!lgG&WY&sT~?l8-$~7HU=r zu3PXl`n$X*!40HuQzm*lFVh9q{yQUO*f(>tTLJT_D4ah$JpEd7Yl$Io~0bLP?7 zh}xmnZyk#a99w+nZ37OanT5Q8E+4-Ip49Wo?Yq>mv4b@{GsnuyE^D5ZGTo(l# zL;414ZO>k5Eb<6@?Z557bHp?lKf!D`yV_JKxKDjF-t3*#qSmtYn-{RaL@bV@iD{B) zfP2%|j(RS)=#|RKF{8)Xmuo&GM#fATXq(44z=E8&hYoV!E$k3G;5;}!w!7(`XTF*F zv5y|n=FZiV2Ko>~j>EK9j~VEt%=F5K&BnO*kX7s|f3bH=h8cJYxkQx&_ikZkoYe{F zHp2%@(TDXDcvL?ZJS@$7BHqp{75GB9IF`)C_Tt)g$lU}d3G0D_d!{S=+n+J0D-5!n z4Y482XTYmC;$hE-I!SQ+WcBtWWOgGTt~bX_6&^!e#|M^JBbY4- z_64pR9%gVNv)wrj+yVDtCTdHUxeCm`!a69#ccIM`_M70I$P+u$b_zs5<{>fTVa)9aN#1=oZw)xDYJ#CrP~Us_b_i#dtWDv&$_!4^;pMpo znl;n)(Y0p%XRn)YXYDlbbx=RMuyN-N_D2J~klF3)V#tGqpPfrx5$8Njv%aQWOJ)H> z^Wnlj6Z!Qe%$?Ue82G@MI__}w+QV!Vv!0FWrvP4n@;~4mpx(dDyi3gcho%~=e+K*& z?Nc#hH^7Gt@HpOQUTP=NQ~HSQnWc|A(PhT#AJm5E2^%Z!2W*3RNXmg=&&OOcV27HO zlJwjZtcNiU>!ngFs<6Jt9ybkM!3!K>$>_;<&|48&1-&-kMm7ygViHCf;rh<@vh-+8q4a^KddQWpL$2ABq>L~a7Q@{AoN1{)e(b(hkCz)YNVJc z#{5@2&xN0{I}`bnth+FGp+3Z=)**S&3)(MWO>_IN+2YdM6#jJ9%3tN5AEQyYB+uVg`wi`yM`@&Uc3tdt8?P+6BD+ z<^oL+_JXeXJXfIE+2*8&{ctyH8?#Dam|8RU%-%`;KIt``IC`n_%<`N~gI>4<`va`Q zwUzHk4>aPA>i1~M2-52}Uh&E#%Z)f1g3G95Z*^`lid%7dM zBOf=j=7*^Ul)gKGz*yiD2wZWLxsi7Wbab5!cy|VzB?I1}!@O$tlAH(iu2syS$27pD z=|SwkXAbkab31r_;4}JwmuBK#N3PyE<<4G$PcZx)z2H^pzJxiM=Ax9Il)wc&JK=`0)Y{deshM?kV7Os)rc8Hqqx3e^sx|d2K!Hh26=> z2AAjxLSJ*vKX7jdytH)|#0)3sU?lL%Vb zJkIsC{z45w{7ZXc%%yd1O>Dg;_6^kG=#f5l!KZbZ-vVxjK|gLTgEN7AP0TGOJK%i! zN+8#h_zdQ8?)$}eyO?RY@a@-}=u2H%%eQ;S3;j@-EzkU6@@a71$b9|W zXr^zW$HUBupWB+@3*ei3aEv+q%=3D^V_v|#D2Mue_=&m56n3#UyYSF)NAD=4*#hMM z0#i27f@r2IFiX?0$W!VOb-VIp1I?|eF&Z9^MP4<`K7a<+%$zwvu^#y0>U+Zf$n3lm zgLzYR)s}(pXHaK21q%P@#McclZ&nS!<8Cmq-F>-{1-*}<`Qz$*X- z-hkJ;gL)kPnM<6fffrH(iha?6x22buBhk!5YoxUpeHVK{dj?N@*|2D(Oe^=QV(HWt(ck3M2#?S+8uz$Mk3U@dF7{Cu97Fo$ z^0P-n6XyUsaggERQ0of~4Eck)Pr*Nc*&4`}8O@wI54`QYoOu90%|w0dEPBX*PwIdx;Y_T#9sR*{WU3Ev0z(JD z3+zQb2Ro6?3l0V_-0NQU)Zje$$d}`s``$gtStRjbV2nQQQ^l@S)@&SNLzvkk_i`%M z^-!{vm#3e9q(U~OfD<3k!wO7LDaOwx$ z)Jza+OMo4^;6v&?L(gLH7~Svl?p2KBqwh(s`-a67_a zk22tpo1IOcGvFE+@F7g0fur#p*m^THooU_$c?N63F@BjF&muz~LNbdH^#dzUzfXksOd6 z)eg8mWNx5ms;GSq>P{c>Db>>gxjDY(KP~bho5u&t64%oxAM`4|rddNZBM>#DORsf! zj|e#YGUZY|>6xXqOsoE+)P~N!Q{M!ErdNP%s?O;X)GV2I9+Q)8e$vdnYQwUq&X0cT z@ZW^?JyL$bp5%dVDDgz#<bpMiM{@bpxFUw`y+_X_qDoD=#W`!F|!@4~q4IJc;GzV!})za9IV zPMtIR_ka&6;jGCK+6v1o)Q3>AC_8RPkhRAelkqfd;_K*&DS(cuoDoY&zyw zBcD%w<-pf+=+V5}cgDfGtz0$Y$IJu6_27`V&vluvhih#IW=omiYw9saodWN^w|qsa zKcxAAQ!Yel#yRR<@H9(eJ&$nVZ{%_&PhVQ*RrcOEyu7&tVwv;;GH37zEgma4mw{i%$I4t4aPta>>j(p&w0gCY=3tIE9nPqURPPoDBPVY_R z6d@;}8hBKZ@Vsnek_x67z8?8ps72w~HW_oHF$S|$_2-7CsLm8-UFf425$+Gw#Q>Mq z*C+3q=WU#Kzca6Grs5yytsUx?aNT5LZq00157%AHbIEzqeG-_E2OJsJshP~}B1Xl! zn)o5tLe$yH2j_K3-2ks;%w}Yqx0%43UhyL8n;X6}N{}}qZR1A@^9oq2p{5Y%B2*`P z`o|gqFE8*^%8P4))h7*H5$LTd%Qpo|3v#Q}FFgbI5!Nrw&x98NJfW(n$LbI7w9(f} z#$E+Zgg_rJoWYMBvVk~?%l(WVoba!5SANLUO9JID7Tyr*gIm9irevg5M6Zks( zR&yBU#X>i2KJJmt9ETyM}PH6CVkd$ z-m%|zrQZtwM1%Y2;TnKBnveSuXK>UfuDvTL7>pQH)F0>@5?ea*wudBGEG18aZOoz(g4;7l0n1p5N@2~W9`g76Sba;HB#rK z>}%ku5AT>aTzJsx%&8M_PvYtPfBu2LfeY_x*1&??IGxdRhDM)Z_Wjg}(Ce#0e2Sn3 z2A@1bJkZ6Kxe$ZiB~++E1y55m?>Qgle{=6cezPz?Ge?2ky1%u^D5jhW&cM)9Xci9V zdDNE$R{qolFVVG&wyHLlInh^-^l|Ce&+HWJLoR%qz=P@R;#$^x3F<&O8|J)*+9J#r z97LBnTx+;*poUWp30_nC`(9bzSMDD9sL;JQ*tZq81Qz0S@BWHbVHnlIlu-T+}nx~#2`Z&`Ew3t6wc`pnVJpTxZX^M`yendo%iK`>UZP5jdfAKGdPOKNSW!vTh#@AOnsAPqA;@!eSoaJ zc1>qjp-e+dxuC$4AX=jywFYe@p&*-j4v&cO1uKIVdtpsz8b z_Z9c%m4~HN`3lN;W*_@Z+Bp|kvF2kD;~`E*uL+(5Xl`BHZ_M4!T=j00W` z@`R2%sO=rp9nhZ7cd(x5y0;QpIoP)y^mqFI@ZSX2XupHr!&&Y5TD=d46yC}lXRWKL z@no$x_vL2|@6Dx?pVL~Aeoo1c^frOB!8>|kv^l?_yNT}QQG5aVE@U{yD4&D84sz$I z#ll(!o>DgR81i4RX25GC(p)-TPjl_aMZtB0EVrsUn>;$MIJkQct4;(uX!Rk%c{5|) z$Yn4qM8Pj=3mzvpX-(C~gM20M zNfj4jP0hZ5#}gW7)RRrANpzV9L0vi4G#5Cau4BNWU0{BSm!Y1Ow%g*hC!-g_!p3F6 z|1SppQ+|O@i1uOhq^VHUQEdSIp3ozMkF!oQ*?8~sI^sRK>#c8f&*dDUJ2JkBCBNHq zziMFE4`2>RZ(zOdc)dY8$r{Ut8d-Z5=Ap5dKrRffalc|R!2EP?0aoDwufQQ@%GoJr zOnJMcYMvK*#BD{Yk9L^f4BXcN9sq1+nim+g=FCH@=52#~4tlqxL!-vr8P=BQ{XCq5 zVCLk+Vy;bJ7xaSUpJOjLw*wwHUiQDcZfw;vL*WnMDq-W$v+=d(x9| zU++OL_XTHGoRcySMEMfnthg;Ot0f#b~4nTk85a&xh8)%;tHh3-0Q>=1?IeQ>>0H0}xHEU3Z zK;=O5T;iNiyPnC{V~)`+WcJ~@*4Kb<2iAgY=$E5(M$9#ov(1gu@)(?3GjL50!+sB0 zIp*lm<_3PwW^VbPoR#%&J0;?CYMv>sX@heBU!cYY94BBv=kz>77mu~)am703PM)oO z1#9Ga$ZW)SP~Jc47TrfVLt%zk_wGMC(U0l1oMu0zUppU!5dUxwIt5^h%kGlZ4Gj(5FBZn^@;W9fNx^W-$E4~HIRxAEM zd<^_z)j^_;Q{QLg*K39#G3%br8aHo)9D*EY%?v)98H3!~dhpcp!b7!=)>ibaKt8d& z`c*}J96hq9yqsU>upbRttsL~u=MG>ddtLW;USGuhnW4$~PZH*#QA_hGKFrU+9tpVz zirF!5jG1K2BO|wpI0W_w59%9mYp1{mE!g3@&wY!S1&;}^J_k93@DADL~E}i&{7jr2OuRrJ)6tCoCCPAl)<4+G$ota_(@o-J*dT5Ck z+;Cp6=v`MiMXqnub4r|u`JCYRQETG$L&xIJaYpxgd_ONad*mJ?Lzy{~p2Y{c>3Tpt z1U|QN974hEnNs#eV3=)ik2Sy^oS-!teM~tUK&{{+`^yEtjF~CC_f&UyI_3gr3W{?9 z=XUqNI|#gP2X#3(^giW&W*jf0Iwf%2U2xJ|Xs9%gf^(Ln=kiGev{29`qsHN%Q9fpm za7`z#3ivM0e=yIdm-iwtYInGM62WyggcW_=pyo(%3c*ZbyHVXNIiUso?$iRGx9C>0l+a}dELnoFs zz%>l&S!kOg4R|pI>zadkkAwN51J4F%i%OXWPt-BM#;7yFxt!}okD1AR7`V3s&aQzs zAL{C?=Hij;rhh^ngWAe4O9A~ixNRf!`eCLTJ;jm5Vc;iifGs%0lewNDi`~n7p{nz* zIrApuWj=%hJRX{%jlN~B7v6BCe9+H3)OwIZh<%%yCtnG06M^4w-^s)pp|v@G$LomK z0J&jY4{A$f zJ@pr0@#r6RyPZF7;2<}|fqxrxhgm%p+7{*B=)z+>lh-Bh<>k+R>VWS9{OB_;jy?SW zeDLS@g7@T*L&-TDa0%uy@>=nthK8Tf5?+rE@}nH!VWw6@ZL8G^ovJO!>3;51^Lcn|BubuVyxhujy=tH_C2wtbV%5%6A|^JBmE zcv#;L0xyC3NM29qb-mL9MUf|^1Cmh~0xPN^unzoB=6%Y{6v{mEQO&hdr zSOWhY5f)K9I4m+aqIPU}9Zy)KC#+8OuqaPhSU}Tujbj7xhdI5#bS@nvtRBdh07@`Hx=r zfB%zvb!yq-mFxil&f^*V`UVb68jywy(FBEugarjAr6i;#_Dbntg4)M64vq?X+~#)b zcWjJjSf^c^MvWR;C$?$JM(twjwQSR^9sbp#QM+#Knzm}ts9VdX4O=yB5)<34MQd9p zEUZ?Y@MtIWuRQp_f5n)lHta9F79Qq={^L{DZ`3K)@}qZa-L`3yrmgD5Hf`OiTf=&> zjqLp^!wLP<2mIk_{1*1!W%S1<>96%k{I70p8#Qj!wh<3Wi`E_Q9}2$JpZxyMo)?4T z$Zz~7PtjlDH~RJX0G^t_zyJCB2>g8n{{MFb0=l(n*=cuaU90=YCG<&34~$Rh(F?2f zcfOrLfxQw%lkkqqR%#Z;|?S{09O&HQAAv#f)?hH>xzyAG_(&GZL)Tg9* zGE7h$>kkk9n?DW>3e<1(Vp;dPejDMgC1ma<3F7NiOHU}4IBUC{qTP#sAqb5e@|%Wph1H| z2GtBn9nd2*JUTi$G%O-CA|e=%2u>TEk{*{4oRU`l?u*=eorXzi2?Kid*E)%>8yBDY zY`O^w;v4<*pMQL@e*ORXMk#3_`XxgWQu~Ew#Ptuw&KCMFp477CfBm$6{r>csY3Ypy z;*Hb0b3onI-|LF@QCj9dQ z^?!CiUp>%?386`SlltMk({L2R|Ma^g3`mMgPaRM%J+Pojfs)YIVZr-)5W?Q8#=64?p;{S)qA5cgjzsbO4t z(gS<_@9OafUgYj0fA<|S@bo{tbn5}VuvW$Oec&4a@0l5464?O)}zsI1SNh$x;<$m|2^ilu%Mbc7}(+9;3NUGNZSHypR zAKrb<-#+PIzj#8=xRf4Ai8how5AUse$M4=Re*aV_lqaZfum6$fFDW7^JUlufI4QDr zcyO)oIuXIqNxX^^>Lk~Uj*p0|9f9`eKRbWFS!n(~f82BL-G2$GDQH~N|BZVP_jw#H zJaoG@c|dBvz_|YX`}RtRWoTG3Ww2A zVsK=5QbcgAF#IbjB0fGiE}?c}?c`dK(eX*O?tR7o)79mF#MSx#;u7)qCB~id4=dQe zSAYEB>A$~=?!U*MY+(N*F0uQc@ZLTZdhgb`_k-VmsXxCe@BZ|+^~k*%=a2OZD_N9o z4U-1+LaEW1OV(d2S8&SEB6uFSt?!+(zq%^??&-hzcK>+w#6MPz|IVvF@bo`>_0<2g zaJ+j6{^VuuzuUbd|G)u$pw4g-6FkYO1Nz0K+u!U8_kaArD?QL4c?R`LNlYD-#%}7b zp05X=4)8xIg?OwDNlQx0a66J1t_3p0LxNlsKG#lKg^>1I~_Yb)L zQonuN17GC95{vJUp^i1iGYJ5x;%e`v>8H2mkBW?w^|0E2GVT)ZR%6>22cDd)mc{o>#4eD1zaI^U-iYAD;_*`zk9m{$e1j2qAg$3br;e3JW* zxc?>ao_GJ@dUo$IY`6F-zj>nm@LxYMndMts_VmfMqJT@?d+z<8|Ngmuz6#^h=+XI) z4?@F0J@WtALC}}D_f_=x{VzKV?VgQK=-aFRe|8j_rnG)G{r`)jaQ71V?TvEpbLgr0 z=Y4hekq;n}dTZVNDez8=$wL1Lw*KL+yLS)#U;X>{m;3+K7r*ynf4|ASBk`C1{`+wK zrNeaZL4O~vdq?6g{r&gh`b&rD-h=)=T=$N|U;6v+!}XUA)4d1%SBEP%s5So>*CwT4 KMmq?@*#8Iizg9y4 literal 0 HcmV?d00001 diff --git a/tests/core/sched/test_omni_scheduling_coordinator.py b/tests/core/sched/test_omni_scheduling_coordinator.py index 1b36cd784..068cbd362 100644 --- a/tests/core/sched/test_omni_scheduling_coordinator.py +++ b/tests/core/sched/test_omni_scheduling_coordinator.py @@ -115,6 +115,7 @@ class TestFullPayloadCoordinatorSelection(unittest.TestCase): ("MiMoAudioModel", "code2wav"), ("Qwen3TTSCode2Wav", "code2wav"), ("CosyVoice3Model", "cosyvoice3_code2wav"), + ("IndexTTS2S2MelDecoder", "indextts2_s2mel_decoder"), ("DyninOmniForConditionalGeneration", "token2image"), ("DyninOmniForConditionalGeneration", "token2audio"), } diff --git a/tests/e2e/offline_inference/test_indextts2.py b/tests/e2e/offline_inference/test_indextts2.py new file mode 100644 index 000000000..6c2786efa --- /dev/null +++ b/tests/e2e/offline_inference/test_indextts2.py @@ -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) diff --git a/tests/e2e/online_serving/test_indextts2.py b/tests/e2e/online_serving/test_indextts2.py new file mode 100644 index 000000000..c0fceb1ce --- /dev/null +++ b/tests/e2e/online_serving/test_indextts2.py @@ -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) diff --git a/tests/model_executor/stage_input_processors/test_indextts2.py b/tests/model_executor/stage_input_processors/test_indextts2.py new file mode 100644 index 000000000..59f14b414 --- /dev/null +++ b/tests/model_executor/stage_input_processors/test_indextts2.py @@ -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 diff --git a/vllm_omni/config/pipeline_registry.py b/vllm_omni/config/pipeline_registry.py index f7109f395..63548ef0f 100644 --- a/vllm_omni/config/pipeline_registry.py +++ b/vllm_omni/config/pipeline_registry.py @@ -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", + ), } diff --git a/vllm_omni/core/sched/omni_scheduling_coordinator.py b/vllm_omni/core/sched/omni_scheduling_coordinator.py index 4056fd938..af6c73d6d 100644 --- a/vllm_omni/core/sched/omni_scheduling_coordinator.py +++ b/vllm_omni/core/sched/omni_scheduling_coordinator.py @@ -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. diff --git a/vllm_omni/deploy/indextts2.yaml b/vllm_omni/deploy/indextts2.yaml new file mode 100644 index 000000000..75eb60188 --- /dev/null +++ b/vllm_omni/deploy/indextts2.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 diff --git a/vllm_omni/deploy/indextts2_low_latency.yaml b/vllm_omni/deploy/indextts2_low_latency.yaml new file mode 100644 index 000000000..621116ab2 --- /dev/null +++ b/vllm_omni/deploy/indextts2_low_latency.yaml @@ -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 diff --git a/vllm_omni/engine/arg_utils.py b/vllm_omni/engine/arg_utils.py index 148412012..83bb5b1c1 100644 --- a/vllm_omni/engine/arg_utils.py +++ b/vllm_omni/engine/arg_utils.py @@ -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), diff --git a/vllm_omni/entrypoints/openai/serving_speech.py b/vllm_omni/entrypoints/openai/serving_speech.py index 0ae947efb..b5b98a359 100644 --- a/vllm_omni/entrypoints/openai/serving_speech.py +++ b/vllm_omni/entrypoints/openai/serving_speech.py @@ -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: diff --git a/vllm_omni/entrypoints/openai/tts_adapters/__init__.py b/vllm_omni/entrypoints/openai/tts_adapters/__init__.py index ca7b9612a..a2b9cce09 100644 --- a/vllm_omni/entrypoints/openai/tts_adapters/__init__.py +++ b/vllm_omni/entrypoints/openai/tts_adapters/__init__.py @@ -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, diff --git a/vllm_omni/entrypoints/openai/tts_adapters/indextts2.py b/vllm_omni/entrypoints/openai/tts_adapters/indextts2.py new file mode 100644 index 000000000..641e380da --- /dev/null +++ b/vllm_omni/entrypoints/openai/tts_adapters/indextts2.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/__init__.py b/vllm_omni/model_executor/models/indextts2/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/bigvgan_cuda_graph.py b/vllm_omni/model_executor/models/indextts2/bigvgan_cuda_graph.py new file mode 100644 index 000000000..c64a46404 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/bigvgan_cuda_graph.py @@ -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() diff --git a/vllm_omni/model_executor/models/indextts2/configuration_indextts2.py b/vllm_omni/model_executor/models/indextts2/configuration_indextts2.py new file mode 100644 index 000000000..504923b1a --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/configuration_indextts2.py @@ -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 + ) diff --git a/vllm_omni/model_executor/models/indextts2/dit_cuda_graph.py b/vllm_omni/model_executor/models/indextts2/dit_cuda_graph.py new file mode 100644 index 000000000..4d32b8c1b --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/dit_cuda_graph.py @@ -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, + ) diff --git a/vllm_omni/model_executor/models/indextts2/gpt/__init__.py b/vllm_omni/model_executor/models/indextts2/gpt/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/gpt/conformer/__init__.py b/vllm_omni/model_executor/models/indextts2/gpt/conformer/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/conformer/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/gpt/conformer/attention.py b/vllm_omni/model_executor/models/indextts2/gpt/conformer/attention.py new file mode 100644 index 000000000..7b34ab381 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/conformer/attention.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/gpt/conformer/embedding.py b/vllm_omni/model_executor/models/indextts2/gpt/conformer/embedding.py new file mode 100644 index 000000000..9c52df586 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/conformer/embedding.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/gpt/conformer/subsampling.py b/vllm_omni/model_executor/models/indextts2/gpt/conformer/subsampling.py new file mode 100644 index 000000000..f8e0b7aa0 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/conformer/subsampling.py @@ -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] diff --git a/vllm_omni/model_executor/models/indextts2/gpt/conformer_encoder.py b/vllm_omni/model_executor/models/indextts2/gpt/conformer_encoder.py new file mode 100644 index 000000000..67eebbab3 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/conformer_encoder.py @@ -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) + ] + ) diff --git a/vllm_omni/model_executor/models/indextts2/gpt/embeddings.py b/vllm_omni/model_executor/models/indextts2/gpt/embeddings.py new file mode 100644 index 000000000..bc9e8e466 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/embeddings.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/gpt/perceiver.py b/vllm_omni/model_executor/models/indextts2/gpt/perceiver.py new file mode 100644 index 000000000..98c7becfa --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/gpt/perceiver.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/indextts2_s2mel_decoder.py b/vllm_omni/model_executor/models/indextts2/indextts2_s2mel_decoder.py new file mode 100644 index 000000000..a374f1b45 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/indextts2_s2mel_decoder.py @@ -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, + ) diff --git a/vllm_omni/model_executor/models/indextts2/indextts2_talker.py b/vllm_omni/model_executor/models/indextts2/indextts2_talker.py new file mode 100644 index 000000000..2b1244ccc --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/indextts2_talker.py @@ -0,0 +1,1062 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +"""IndexTTS2 Stage 0: GPT-2 AR Talker with vLLM-native PagedAttention. + +Predicts mel codes autoregressively and collects hidden_states as latent +for Stage 1 (S2Mel + BigVGAN). +""" + +from __future__ import annotations + +import json +import math +import re +from collections.abc import Iterable +from typing import Any + +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from vllm.compilation.decorators import support_torch_compile +from vllm.config import VllmConfig +from vllm.distributed import get_pp_group +from vllm.logger import init_logger +from vllm.model_executor.layers.logits_processor import LogitsProcessor +from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead +from vllm.model_executor.model_loader.weight_utils import default_weight_loader +from vllm.model_executor.models.gpt2 import GPT2Block +from vllm.model_executor.models.utils import ( + is_pp_missing_parameter, + make_empty_intermediate_tensors_factory, + make_layers, + maybe_prefix, +) +from vllm.sequence import IntermediateTensors + +from vllm_omni.data_entry_keys import OmniPayload +from vllm_omni.model_executor.models.output_templates import OmniOutput +from vllm_omni.utils.speaker_cache import get_speaker_cache + +from .configuration_indextts2 import IndexTTS2Config +from .gpt.conformer_encoder import ConformerEncoder +from .gpt.embeddings import LearnedPositionEmbeddings +from .gpt.perceiver import PerceiverResampler +from .preprocess_utils import ( + compute_fbank, + load_campplus, + load_qwen_emotion, + load_reference_audio, + load_semantic_codec, + load_wav2vec2, + resolve_model_file, + wav2vec_extract, +) + +logger = init_logger(__name__) + + +def _find_most_similar_cosine(query: torch.Tensor, matrix: torch.Tensor) -> int: + sims = F.cosine_similarity(query.float(), matrix.float(), dim=1) + return int(torch.argmax(sims).item()) + + +@support_torch_compile( + dynamic_arg_dims={ + "input_ids": 0, + "positions": 0, + "intermediate_tensors": 0, + "inputs_embeds": 0, + } +) +class IndexTTS2TalkerForConditionalGeneration(nn.Module): + """vLLM-native GPT-2 AR talker for IndexTTS2. + + Stage 0 of the two-stage pipeline. Predicts mel codes (8194 vocab) + and accumulates hidden_states as latent for Stage 1 S2Mel decoder. + """ + + 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] + gpt_cfg = self.config.gpt + + self.model_dim = gpt_cfg["model_dim"] + self.num_layers = gpt_cfg["layers"] + self.num_heads = gpt_cfg["heads"] + self.max_mel_tokens = gpt_cfg["max_mel_tokens"] + self.max_text_tokens = gpt_cfg["max_text_tokens"] + self.number_mel_codes = gpt_cfg["number_mel_codes"] + self.start_mel_token = gpt_cfg["start_mel_token"] + self.stop_mel_token = gpt_cfg["stop_mel_token"] + self.number_text_tokens = gpt_cfg.get("number_text_tokens", 12000) + self.condition_num_latent = gpt_cfg.get("condition_num_latent", 32) + + # --- Flags for vLLM-Omni framework --- + self.have_multimodal_outputs = True + self.has_preprocess = True + self.has_postprocess = True + self.requires_raw_input_tokens = True + self.enable_update_additional_information = True + # S2Mel consumes the complete code sequence only at request end, but + # the connector accumulator needs to observe each appendable AR delta. + # Keep runner request-end gating disabled and emit per-step rows from + # make_omni_output; the full-payload builder reconstructs the sequence. + self.omni_payload_at_request_end = False + self.omni_request_end_token_ids = () + self._speaker_cache = get_speaker_cache() + self.gpu_resident_buffer_keys: set[tuple[str, str]] = { + ("codes", "mel"), + ("hidden_states", "latent"), + ("meta", "mel_start_offset"), + ("meta", "latent_acc"), + ("meta", "mel_code_count"), + } + + # --- GPT-2 transformer (vLLM-native with PagedAttention) --- + # Build a GPT2Config-like object for GPT2Block + from transformers import GPT2Config as HFGpt2Config + + max_seq_len = self.max_mel_tokens + self.max_text_tokens + self.condition_num_latent + 4 + gpt2_config = HFGpt2Config( + vocab_size=self.number_mel_codes, + n_positions=max_seq_len, + n_ctx=max_seq_len, + n_embd=self.model_dim, + n_layer=self.num_layers, + n_head=self.num_heads, + activation_function="gelu_new", + layer_norm_epsilon=1e-5, + ) + # Store for GPT2Block construction + cache_config = vllm_config.cache_config + quant_config = vllm_config.quant_config + self.start_layer, self.end_layer, self.h = make_layers( + gpt2_config.n_layer, + lambda prefix: GPT2Block(gpt2_config, cache_config, quant_config, prefix=prefix), + prefix=f"{prefix}.h", + ) + self.ln_f = nn.LayerNorm(self.model_dim, eps=gpt2_config.layer_norm_epsilon) + self.final_norm = nn.LayerNorm(self.model_dim, eps=gpt2_config.layer_norm_epsilon) + self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory( + ["hidden_states"], self.model_dim + ) + + # --- Embeddings --- + self.text_embedding = nn.Embedding(self.number_text_tokens + 1, self.model_dim) + self.mel_embedding = nn.Embedding(self.number_mel_codes, self.model_dim) + self.text_pos_embedding = LearnedPositionEmbeddings(self.max_text_tokens + 2, self.model_dim) + self.mel_pos_embedding = LearnedPositionEmbeddings(self.max_mel_tokens + 2 + 1, self.model_dim) + self.speed_emb = nn.Embedding(2, self.model_dim) + self.emo_layer = nn.Linear(self.model_dim, self.model_dim) + self.emovec_layer = nn.Linear(1024, self.model_dim) + + # --- Mel head (logits) --- + if get_pp_group().is_last_rank: + self.mel_head = ParallelLMHead( + self.number_mel_codes, + self.model_dim, + quant_config=quant_config, + prefix=maybe_prefix(prefix, "mel_head"), + ) + else: + self.mel_head = None # type: ignore[assignment] + self.logits_processor = LogitsProcessor(self.number_mel_codes) + + # --- Conditioning modules (speaker / emotion) --- + cond_cfg = gpt_cfg.get("condition_module", {}) + emo_cond_cfg = gpt_cfg.get("emo_condition_module", {}) + + self.conditioning_encoder = ConformerEncoder( + input_size=1024, + output_size=cond_cfg.get("output_size", 512), + linear_units=cond_cfg.get("linear_units", 2048), + attention_heads=cond_cfg.get("attention_heads", 8), + num_blocks=cond_cfg.get("num_blocks", 6), + input_layer=cond_cfg.get("input_layer", "conv2d2"), + ) + self.perceiver_encoder = PerceiverResampler( + self.model_dim, + dim_context=cond_cfg.get("output_size", 512), + ff_mult=cond_cfg.get("perceiver_mult", 2), + heads=cond_cfg.get("attention_heads", 8), + num_latents=self.condition_num_latent, + ) + + self.emo_conditioning_encoder = ConformerEncoder( + input_size=1024, + output_size=emo_cond_cfg.get("output_size", 512), + linear_units=emo_cond_cfg.get("linear_units", 1024), + attention_heads=emo_cond_cfg.get("attention_heads", 4), + num_blocks=emo_cond_cfg.get("num_blocks", 4), + input_layer=emo_cond_cfg.get("input_layer", "conv2d2"), + ) + self.emo_perceiver_encoder = PerceiverResampler( + 1024, + dim_context=emo_cond_cfg.get("output_size", 512), + ff_mult=emo_cond_cfg.get("perceiver_mult", 2), + heads=emo_cond_cfg.get("attention_heads", 4), + num_latents=1, + ) + + # Padding masks for perceiver cross-attention + self.cond_mask_pad = nn.ConstantPad1d((self.condition_num_latent, 0), True) + self.emo_cond_mask_pad = nn.ConstantPad1d((1, 0), True) + + # --- Lazy-loaded external models --- + self._w2v_stat: torch.Tensor | None = None + self._emo_matrix: torch.Tensor | None = None + self._spk_matrix: torch.Tensor | None = None + self._text_tokenizer: Any = None + + # Initialize embeddings per GPT-2 convention + for emb in [self.text_embedding, self.mel_embedding]: + emb.weight.data.normal_(mean=0.0, std=0.02) + self.speed_emb.weight.data.normal_(mean=0.0, std=0.0) + + # ------------------------------------------------------------------ + # vLLM required hooks + # ------------------------------------------------------------------ + + def embed_input_ids(self, input_ids: torch.Tensor, **_: Any) -> torch.Tensor: + return self.mel_embedding(input_ids) + + def _extract_mel_offsets(self, positions: torch.Tensor, kwargs: dict[str, Any]) -> torch.Tensor: + """Build per-token mel_start_offset from model_intermediate_buffer.""" + info_dicts = kwargs.get("model_intermediate_buffer") or kwargs.get("runtime_additional_information") or [] + if "runtime_additional_information" in kwargs and "model_intermediate_buffer" not in kwargs: + logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer") + if not info_dicts: + logger.warning_once("[OFFSET] no model_intermediate_buffer available; defaulting mel offsets to 0") + return torch.zeros_like(positions) + offsets = [] + for info in info_dicts: + if isinstance(info, dict): + meta = info.get("meta", {}) + val = meta.get("mel_start_offset", 0) + offsets.append(int(val) if not isinstance(val, int) else val) + else: + offsets.append(0) + return torch.tensor(offsets, dtype=positions.dtype, device=positions.device) + + def forward( + self, + input_ids: torch.Tensor, + positions: torch.Tensor, + intermediate_tensors: IntermediateTensors | None = None, + inputs_embeds: torch.Tensor | None = None, + **kwargs: Any, + ) -> torch.Tensor | IntermediateTensors: + """AR transformer forward. + + During prefill: inputs_embeds is set by preprocess(), input_ids ignored. + During decode: inputs_embeds is mel_embedding(input_ids) + mel_pos. + """ + if get_pp_group().is_first_rank: + if inputs_embeds is None: + logger.warning("[FORWARD] inputs_embeds=None — fallback decode path") + mel_start_offset = self._extract_mel_offsets(positions, kwargs) + mel_pos = positions - mel_start_offset + mel_pos = torch.clamp(mel_pos, min=0) + inputs_embeds = self.mel_embedding(input_ids) + self.mel_pos_embedding.emb(mel_pos) + hidden_states = inputs_embeds + else: + assert intermediate_tensors is not None + hidden_states = intermediate_tensors["hidden_states"] + + for layer in self.h[self.start_layer : self.end_layer]: + hidden_states = layer(hidden_states) + + if not get_pp_group().is_last_rank: + return IntermediateTensors({"hidden_states": hidden_states}) + + return self.final_norm(self.ln_f(hidden_states)) + + def compute_logits( + self, + hidden_states: torch.Tensor | OmniOutput, + sampling_metadata: Any = None, + ) -> torch.Tensor | None: + if isinstance(hidden_states, OmniOutput): + hidden_states = hidden_states.text_hidden_states + if hidden_states is None or self.mel_head is None: + return None + return self.logits_processor(self.mel_head, hidden_states) + + # ------------------------------------------------------------------ + # Omni multimodal output plumbing + # ------------------------------------------------------------------ + + def make_omni_output(self, model_outputs: torch.Tensor | OmniOutput, **kwargs: Any) -> OmniOutput: + """Collect mel_codes and hidden_states from intermediate buffer.""" + if isinstance(model_outputs, OmniOutput): + return model_outputs + + hidden = model_outputs + info_dicts = kwargs.get("model_intermediate_buffer") + if info_dicts is None: + info_dicts = kwargs.get("runtime_additional_information") or [] + if "runtime_additional_information" in kwargs and "model_intermediate_buffer" not in kwargs: + logger.warning_once("runtime_additional_information is deprecated, use model_intermediate_buffer") + + device = hidden.device + latent_dim = hidden.shape[-1] + _zero_codes = torch.zeros(0, 1, dtype=torch.long, device=device) + _zero_latent = torch.zeros(0, latent_dim, dtype=hidden.dtype, device=device) + + # Per-request lists: to_payload_element dispatches lists via + # element[idx], giving correct per-request mapping even during + # mixed prefill+decode steps. Zero-length placeholders for prefill + # requests are harmless — torch.cat ignores them in accumulation. + # + # Export semantics are appendable deltas for the existing full-payload + # accumulator: codes.mel is a 2-D row [1, 1], hidden_states.latent is + # the aligned one-row [1, D] slice from meta.latent_acc. + mel_codes_list: list[torch.Tensor] = [] + latent_list: list[torch.Tensor] = [] + meta_list: list[dict[str, Any]] = [] + + for info in info_dicts: + if not isinstance(info, dict): + mel_codes_list.append(_zero_codes) + latent_list.append(_zero_latent) + meta_list.append({}) + continue + codes = info.get("codes", {}) + mc = codes.get("mel") + info_meta = info.get("meta", {}) + # mel_len now comes from the int counter in meta, not from + # code_seq.shape[0] (codes.mel is a single-token delta). + mel_len = int(info_meta.get("mel_code_count", 0)) + if isinstance(mc, torch.Tensor) and mc.numel() > 0 and mel_len > 0: + code_seq = mc.reshape(-1) + mel_codes_list.append(code_seq[-1:].reshape(1, 1).contiguous()) + else: + mel_codes_list.append(_zero_codes) + + lat_acc = info_meta.get("latent_acc") + if isinstance(lat_acc, torch.Tensor) and lat_acc.numel() > 0 and mel_len > 0: + lat_seq = lat_acc.reshape(-1, lat_acc.shape[-1]) if lat_acc.ndim != 2 else lat_acc + idx = min(max(mel_len - 1, 0), int(lat_seq.shape[0]) - 1) + latent_list.append(lat_seq[idx : idx + 1].contiguous()) + else: + hs = info.get("hidden_states", {}) + lat = hs.get("latent") + if isinstance(lat, torch.Tensor) and lat.numel() > 0: + lat_seq = lat.reshape(-1, lat.shape[-1]) if lat.ndim != 2 else lat + latent_list.append(lat_seq[-1:].contiguous()) + else: + latent_list.append(_zero_latent) + + req_meta: dict[str, Any] = {} + if mel_len == 1: + s_ref = info_meta.get("S_ref") + ref_mel = info_meta.get("ref_mel") + style = info_meta.get("style") + if isinstance(s_ref, torch.Tensor): + req_meta["S_ref"] = s_ref + if isinstance(ref_mel, torch.Tensor): + req_meta["ref_mel"] = ref_mel + if isinstance(style, torch.Tensor): + req_meta["style"] = style + meta_list.append(req_meta) + + if not any(t.numel() > 0 for t in mel_codes_list): + return OmniOutput(text_hidden_states=hidden, multimodal_outputs={}) + + mm: OmniPayload = { + "codes": {"mel": mel_codes_list}, + "hidden_states": {"latent": latent_list}, + } + if any(meta_list): + mm["meta"] = meta_list + + return OmniOutput(text_hidden_states=hidden, multimodal_outputs=mm) + + # ------------------------------------------------------------------ + # preprocess / postprocess + # ------------------------------------------------------------------ + + def preprocess( + self, + input_ids: torch.Tensor, + input_embeds: torch.Tensor | None, + **info_dict: Any, + ) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]: + """Build prompt embeddings for prefill; compute mel embeddings for decode. + + Prefill layout:: + + [conds(32) + emo_vec(1) + duration(2)] [text_emb + text_pos] [start_mel + mel_pos(0)] + + Decode: ``mel_embedding(token) + mel_pos_embedding(step)``. + """ + span_len = int(input_ids.shape[0]) + meta = info_dict.get("meta", {}) + + # --- Decode path: span_len==1 and mel_start_offset already set --- + if span_len == 1 and isinstance(meta, dict) and meta.get("mel_start_offset") is not None: + return self._preprocess_decode(input_ids, info_dict, meta) + + # --- Prefill path --- + return self._preprocess_prefill(input_ids, input_embeds, info_dict, span_len) + + def _preprocess_decode( + self, + input_ids: torch.Tensor, + info_dict: dict[str, Any], + meta: dict[str, Any], + ) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]: + """Lightweight decode step: embed token + track code count.""" + # Track code count via int counter instead of accumulating a full tensor + # (the connector reconstructs the full sequence from per-step deltas). + prev_count = meta.get("mel_code_count", 0) + if not isinstance(prev_count, int): + prev_count = int(prev_count) + decode_step = prev_count + 1 + + # Only keep the current token; no O(T) history accumulation. + new_codes = input_ids.detach() + + mel_pos = input_ids.new_full((1,), decode_step) + embeds = self.mel_embedding(input_ids) + self.mel_pos_embedding.emb(mel_pos) + + return ( + input_ids, + embeds, + { + "codes": {"mel": new_codes}, + "meta": {"mel_code_count": decode_step}, + }, + ) + + def _preprocess_prefill( + self, + input_ids: torch.Tensor, + input_embeds: torch.Tensor | None, + info_dict: dict[str, Any], + span_len: int, + ) -> tuple[torch.Tensor, torch.Tensor, dict[str, Any]]: + """Full conditioning pipeline — runs once during prefill.""" + additional_information = info_dict.get("additional_information") + if isinstance(additional_information, dict): + merged: dict[str, Any] = {k: v for k, v in info_dict.items() if k != "additional_information"} + for k, v in additional_information.items(): + merged.setdefault(k, v) + info_dict = merged + + device = input_ids.device + + def _first(key, default=None): + v = info_dict.get(key) + return v[0] if isinstance(v, list) and v else default + + text = _first("text") + if not text: + raise ValueError("Missing additional_information.text for IndexTTS2 talker.") + + voice_path = _first("voice") + if voice_path is None: + raise ValueError("IndexTTS2 requires voice (reference audio path) in additional_information.") + + emo_audio_path = _first("emo_audio") + emo_text = _first("emo_text") + use_emo_text = bool(_first("use_emo_text", False)) + emo_vector = _first("emo_vector") + emo_alpha = float(_first("emo_alpha", 1.0)) + use_random = bool(_first("use_random", False)) + _raw_emo_voice = _first("emo_voice_name") + emo_voice_name = str(_raw_emo_voice).strip().lower() if _raw_emo_voice else None + + # --- Speaker cache lookup --- + _voice_name = _first("voice_name") + _voice_created_at = int(_first("voice_created_at", 0)) + _speaker_cache_key = None + _cached = None + if _voice_name: + _speaker_cache_key = self._speaker_cache.make_cache_key(_voice_name, "indextts2", _voice_created_at) + _cached = self._speaker_cache.get(_speaker_cache_key) + + # --- Load audio and extract features --- + # Keep the speaker-cache hit path truly lazy: cached speaker artifacts are + # sufficient for conditioning and same-speaker emotion reuse, so avoid + # loading/resampling reference audio and touching the Wav2Vec2 singleton + # unless a miss (or an uncached separate emotion audio) actually needs it. + wav_16k: torch.Tensor | None = None + w2v_model = None + w2v_proc = None + + if _cached is not None: + s_ref = _cached["S_ref"].to(device) + style = _cached["style"].to(device) + ref_mel = _cached["ref_mel"].to(device) + spk_cond_emb = _cached["spk_cond_emb"].to(device) + else: + wav_16k, wav_22k = load_reference_audio(voice_path, device, mode="speaker") + w2v_model, w2v_proc = load_wav2vec2(self.model_path, device) + self._ensure_w2v_stat_loaded(device) + spk_cond_emb = wav2vec_extract(wav_16k, w2v_model, w2v_proc, device, self._w2v_stat) + + semantic_codec = load_semantic_codec(self.model_path, self.config.semantic_codec, device) + with torch.no_grad(): + _, s_ref = semantic_codec.quantize(spk_cond_emb) # [B, T, 1024] quantized embeddings + + campplus = load_campplus(self.model_path, device) + fbank = compute_fbank(wav_16k, device) + with torch.no_grad(): + style = campplus(fbank) # [1, 192] + + ref_mel = self._compute_mel_22k(wav_22k, device) # [1, 80, T_ref] + + if _speaker_cache_key is not None: + self._speaker_cache.put( + _speaker_cache_key, + { + "S_ref": s_ref.detach().cpu().contiguous(), + "style": style.detach().cpu().contiguous(), + "ref_mel": ref_mel.detach().cpu().contiguous(), + "spk_cond_emb": spk_cond_emb.detach().cpu().contiguous(), + }, + ) + + model_dtype = next(self.conditioning_encoder.parameters()).dtype + spk_cond_emb = spk_cond_emb.to(device=device, dtype=model_dtype) + spk_lens = torch.tensor([spk_cond_emb.shape[1]], device=device, dtype=torch.long) + speech_cond, mask = self.conditioning_encoder(spk_cond_emb, spk_lens) + conds_mask = self.cond_mask_pad(mask.squeeze(1)) + conds = self.perceiver_encoder(speech_cond, conds_mask) # [1, 32, D] + + emo_vec = self._compute_emotion_vector( + wav_16k=wav_16k, + speaker_audio_path=voice_path, + emo_audio_path=emo_audio_path, + emo_voice_name=emo_voice_name, + main_text=text, + use_emo_text=use_emo_text, + emo_text=emo_text, + emo_vector=emo_vector, + emo_alpha=emo_alpha, + use_random=use_random, + style=style, + spk_cond_emb=spk_cond_emb, + w2v_model=w2v_model, + w2v_proc=w2v_proc, + device=device, + ) + + speed_zero = torch.zeros(1, device=device, dtype=torch.long) + speed_one = torch.ones(1, device=device, dtype=torch.long) + duration_emb = self.speed_emb(speed_zero) + duration_emb_half = self.speed_emb(speed_one) + + conds_with_emo = conds + emo_vec.unsqueeze(1) # [1, 32, D] + conds_prefix = torch.cat( + [conds_with_emo, duration_emb_half.unsqueeze(1), duration_emb.unsqueeze(1)], + dim=1, + ) # [1, 34, D] + + text_tokens = self._tokenize_text(text, device) # [1, L] + text_emb = self.text_embedding(text_tokens) + self.text_pos_embedding(text_tokens) + + start_mel = self.mel_embedding(torch.tensor([[self.start_mel_token]], device=device, dtype=torch.long)) + start_mel_pos = self.mel_pos_embedding.get_fixed_embedding(0, device) + start_mel = start_mel + start_mel_pos # [1, 1, D] + + inputs_embeds = torch.cat([conds_prefix, text_emb, start_mel], dim=1).squeeze(0) + mel_start_offset = int(conds_prefix.shape[1]) + int(text_emb.shape[1]) + prefill_mel_index = int(inputs_embeds.shape[0]) - 1 + + if int(inputs_embeds.shape[0]) < span_len: + pad_n = span_len - int(inputs_embeds.shape[0]) + pad_emb = torch.zeros(pad_n, self.model_dim, device=device, dtype=inputs_embeds.dtype) + inputs_embeds = torch.cat([inputs_embeds, pad_emb], dim=0) + elif int(inputs_embeds.shape[0]) > span_len: + inputs_embeds = inputs_embeds[:span_len] + + input_ids_out = torch.full_like(input_ids, self.start_mel_token) + + info_update: dict[str, Any] = { + "meta": { + "mel_start_offset": mel_start_offset, + "prefill_mel_index": prefill_mel_index, + "mel_code_count": 0, + "S_ref": s_ref.cpu().contiguous(), + "ref_mel": ref_mel.cpu().contiguous(), + "style": style.cpu().contiguous(), + "latent_acc": torch.zeros(0, self.model_dim, dtype=inputs_embeds.dtype, device=device), + }, + "codes": {"mel": torch.zeros(0, dtype=torch.long, device=device)}, + "hidden_states": {"latent": torch.zeros(0, self.model_dim, dtype=inputs_embeds.dtype, device=device)}, + } + + return input_ids_out, inputs_embeds, info_update + + def postprocess( + self, + hidden_states: torch.Tensor, + multimodal_outputs: Any = None, + **kwargs: Any, + ) -> dict[str, Any]: + """Store current decode-step hidden state for Stage 1. + + Only the current row is kept; the connector's full-payload accumulator + reconstructs the complete latent sequence from per-step deltas emitted + by make_omni_output. + """ + if hidden_states.shape[0] != 1: + meta = kwargs.get("meta", {}) + prefill_mel_index = int(meta.get("prefill_mel_index", hidden_states.shape[0] - 1)) + prefill_mel_index = min(max(prefill_mel_index, 0), hidden_states.shape[0] - 1) + return {"meta": {"latent_acc": hidden_states[prefill_mel_index : prefill_mel_index + 1].detach()}} + + return {"meta": {"latent_acc": hidden_states.detach()}} + + def _compute_mel_22k(self, wav_22k: torch.Tensor, device: torch.device) -> torch.Tensor: + """Compute 80-band mel spectrogram at 22.05kHz for Stage 1.""" + s2mel_cfg = self.config.s2mel.get("preprocess_params", {}) + sr = s2mel_cfg.get("sr", 22050) + spect = s2mel_cfg.get("spect_params", {}) + n_fft = spect.get("n_fft", 1024) + hop_length = spect.get("hop_length", 256) + win_length = spect.get("win_length", 1024) + n_mels = spect.get("n_mels", 80) + + # mel_spectrogram is pure torch with per-device caches — run on the + # wav's device (GPU after load_reference_audio) instead of CPU. + wav = wav_22k.float() + if wav.ndim == 1: + wav = wav.unsqueeze(0) + + from .s2mel.modules.audio import mel_spectrogram as mel_fn + + mel = mel_fn(wav, n_fft, n_mels, sr, hop_length, win_length, 0, None) + return mel.to(device=device) # [1, 80, T] + + # ------------------------------------------------------------------ + # Emotion conditioning + # ------------------------------------------------------------------ + + def _compute_emotion_vector( + self, + *, + wav_16k: torch.Tensor | None, + speaker_audio_path: Any, + emo_audio_path: str | None, + emo_voice_name: str | None, + main_text: str, + use_emo_text: bool, + emo_text: str | None, + emo_vector: Any, + emo_alpha: float, + use_random: bool, + style: torch.Tensor, + spk_cond_emb: torch.Tensor, + w2v_model: Any, + w2v_proc: Any, + device: torch.device, + ) -> torch.Tensor: + """Full emotion control logic aligned with official infer_v2.py. Returns [1, D].""" + # --- Mutual exclusion: text/vector guidance clears emotion reference audio --- + # Mirrors official infer_v2.py: use_emo_text is evaluated before explicit + # vectors, so text guidance wins if both are supplied by an internal caller. + if use_emo_text: + if emo_text is None: + emo_text = main_text + emo_vector = self._predict_emotion_from_text(emo_text, device) + if use_emo_text or emo_vector is not None: + emo_audio_path = None + + # --- Validate emo_vector (user-provided or text-predicted) --- + if emo_vector is not None: + if isinstance(emo_vector, torch.Tensor): + emo_vector = emo_vector.tolist() + elif isinstance(emo_vector, np.ndarray): + emo_vector = emo_vector.tolist() + if not isinstance(emo_vector, list) or len(emo_vector) != len(self._DESIRED_ORDER): + raise ValueError( + "IndexTTS2 emo_vector must contain 8 values in order: " + "happy, angry, sad, afraid, disgusted, melancholic, surprised, calm" + ) + if any(isinstance(v, bool) for v in emo_vector): + raise ValueError("IndexTTS2 emo_vector values must be numeric") + try: + emo_vector = [float(v) for v in emo_vector] + except (TypeError, ValueError) as exc: + raise ValueError("IndexTTS2 emo_vector values must be numeric") from exc + if not all(math.isfinite(v) for v in emo_vector): + raise ValueError("IndexTTS2 emo_vector values must be finite") + if not all(0.0 <= v <= 1.2 for v in emo_vector): + raise ValueError("IndexTTS2 emo_vector values must be between 0.0 and 1.2") + + # Alpha pre-scaling (official infer_v2.py:414-418): + # when emo_alpha != 1.0, scale emo_vector down before overlay. + emo_vector_scale = max(0.0, min(1.0, emo_alpha)) + if emo_vector_scale != 1.0: + emo_vector = [int(x * emo_vector_scale * 10000) / 10000 for x in emo_vector] + + # --- Path B: emo_vector (8-dim) → spk_matrix cosine lookup --- + emovec_mat = None + weight_sum = 0.0 + if emo_vector is not None: + weight_sum = sum(emo_vector) + vec = torch.tensor(emo_vector, device=device, dtype=torch.float32) + emovec_mat = self._compute_emo_from_distribution(vec, style, device, use_random=use_random) + + # --- Audio-based emotion with alpha blending --- + if emo_audio_path is None: + # No separate emotion audio: reuse speaker w2v-bert features instead of + # reloading + re-extracting the same audio (saves ~0.5s per prefill). + # Official code resamples orig→16k directly for emotion vs orig→22k→16k + # for speaker; the resulting w2v features are near-identical. + effective_alpha = 1.0 + emo_cond_emb = spk_cond_emb + else: + effective_alpha = emo_alpha + # emotion audio cache (enabled when emo_voice_name is provided) + _emo_cache_key = None + _emo_cached = None + if emo_voice_name: + _emo_cache_key = self._speaker_cache.make_cache_key(emo_voice_name, "indextts2_emo", 0) + _emo_cached = self._speaker_cache.get(_emo_cache_key) + + if _emo_cached is not None: + emo_cond_emb = _emo_cached["spk_cond_emb"].to(device) + logger.debug("[EMO] cache HIT for %s", emo_voice_name) + else: + emo_wav_16k, _ = load_reference_audio(emo_audio_path, device, mode="emotion") + if w2v_model is None or w2v_proc is None: + w2v_model, w2v_proc = load_wav2vec2(self.model_path, device) + self._ensure_w2v_stat_loaded(device) + emo_cond_emb = wav2vec_extract(emo_wav_16k, w2v_model, w2v_proc, device, self._w2v_stat) + if _emo_cache_key is not None: + self._speaker_cache.put( + _emo_cache_key, + { + "spk_cond_emb": emo_cond_emb.detach().cpu().contiguous(), + }, + ) + logger.debug("[EMO] cache MISS, cached for %s", emo_voice_name) + + emovec = self._merge_emovec(spk_cond_emb, emo_cond_emb, effective_alpha) + + # --- emo_vector overlay (official infer_v2.py:560-561) --- + # emovec_mat is raw feat2.pt weighted sum [1, model_dim], NOT projected. + # Mix: emovec_mat contributes the emotion prototype; (1-sum)*emovec + # preserves the audio-based emotion as a "base color". + if emovec_mat is not None: + emovec = emovec_mat.to(dtype=emovec.dtype) + (1.0 - weight_sum) * emovec + + return emovec + + _MELANCHOLIC_WORDS = frozenset( + { + "低落", + "melancholy", + "melancholic", + "depression", + "depressed", + "gloomy", + } + ) + + _DESIRED_ORDER = ["高兴", "愤怒", "悲伤", "恐惧", "反感", "低落", "惊讶", "自然"] + + def _predict_emotion_from_text(self, text: str, device: torch.device) -> list[float] | None: + """Use QwenEmotion CausalLM to predict 8-dim emotion vector from text (aligned with official).""" + model, tokenizer = load_qwen_emotion( + self.model_path, + device, + trust_remote_code=self.vllm_config.model_config.trust_remote_code, + ) + if model is None or tokenizer is None: + return None + + messages = [ + {"role": "system", "content": "文本情感分类"}, + {"role": "user", "content": text}, + ] + prompt_text = tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True, + enable_thinking=False, + ) + model_inputs = tokenizer([prompt_text], return_tensors="pt").to(model.device) + + with torch.no_grad(): + generated_ids = model.generate( + **model_inputs, + max_new_tokens=512, + pad_token_id=tokenizer.eos_token_id, + ) + output_ids = generated_ids[0][len(model_inputs.input_ids[0]) :].tolist() + + try: + index = len(output_ids) - output_ids[::-1].index(151668) + except ValueError: + index = 0 + content_str = tokenizer.decode(output_ids[index:], skip_special_tokens=True) + + try: + content = json.loads(content_str) + except json.JSONDecodeError: + content = { + m.group(1): float(m.group(2)) for m in re.finditer(r'([^\s":.,]+?)"?\s*:\s*([\d.]+)', content_str) + } + + text_lower = text.lower() + if any(w in text_lower for w in self._MELANCHOLIC_WORDS): + content["悲伤"], content["低落"] = content.get("低落", 0.0), content.get("悲伤", 0.0) + + max_score, min_score = 1.2, 0.0 + emo_vector = [] + for cn_key in self._DESIRED_ORDER: + val = content.get(cn_key, 0.0) + if isinstance(val, str): + try: + val = float(val) + except ValueError: + val = 0.0 + emo_vector.append(max(min_score, min(max_score, val))) + + if all(v <= 0.0 for v in emo_vector): + emo_vector[7] = 1.0 # default calm + + return emo_vector + + def _compute_emo_from_distribution( + self, + emo_dist: torch.Tensor, + style: torch.Tensor, + device: torch.device, + use_random: bool = False, + ) -> torch.Tensor: + """From 8-dim emotion distribution + CAMPPlus style, build emovec_mat via spk_matrix cosine lookup. + + Returns [1, model_dim] (feat2.pt rows are already model_dim). + """ + self._ensure_matrix_loaded("_emo_matrix", "emo_matrix", "Emotion matrix") + self._ensure_matrix_loaded("_spk_matrix", "spk_matrix", "Speaker matrix") + if self._emo_matrix is None or self._spk_matrix is None: + return torch.zeros(1, self.model_dim, device=device) + + weight_vector = emo_dist.to(device=device, dtype=torch.float32) + if weight_vector.ndim == 2: + weight_vector = weight_vector.squeeze(0) # [8] + + if use_random: + indices = [int(torch.randint(0, sub.shape[0], (1,), device=device).item()) for sub in self._spk_matrix] + else: + indices = [_find_most_similar_cosine(style, sub.to(device)) for sub in self._spk_matrix] + emo_rows = torch.cat( + [sub.to(device)[idx].unsqueeze(0) for idx, sub in zip(indices, self._emo_matrix)], dim=0 + ) # [8, model_dim] + + return (weight_vector.unsqueeze(1) * emo_rows).sum(0).unsqueeze(0) # [1, model_dim] + + def _compute_audio_emo_vec(self, cond_emb: torch.Tensor) -> torch.Tensor: + """Conformer → Perceiver → emovec_layer → emo_layer. Input: [1, T, 1024], output: [1, D].""" + emo_dtype = next(self.emo_conditioning_encoder.parameters()).dtype + cond_emb = cond_emb.to(device=cond_emb.device, dtype=emo_dtype) + cond_lens = torch.tensor([cond_emb.shape[1]], device=cond_emb.device, dtype=torch.long) + emo_cond, emo_mask = self.emo_conditioning_encoder(cond_emb, cond_lens) + emo_conds_mask = self.emo_cond_mask_pad(emo_mask.squeeze(1)) + emo_percept = self.emo_perceiver_encoder(emo_cond, emo_conds_mask) # [1, 1, 1024] + emo_vec_syn = self.emovec_layer(emo_percept.squeeze(1)) # [1, D] + return self.emo_layer(emo_vec_syn) # [1, D] + + def _merge_emovec( + self, + spk_cond_emb: torch.Tensor, + emo_cond_emb: torch.Tensor, + alpha: float, + ) -> torch.Tensor: + """Alpha-blend speaker and emotion audio vectors: base + alpha * (emo - base).""" + if emo_cond_emb is spk_cond_emb or alpha == 1.0: + return self._compute_audio_emo_vec(emo_cond_emb) + if alpha == 0.0: + return self._compute_audio_emo_vec(spk_cond_emb) + base_vec = self._compute_audio_emo_vec(spk_cond_emb) + emo_vec = self._compute_audio_emo_vec(emo_cond_emb) + return base_vec + alpha * (emo_vec - base_vec) # [1, D] + + # ------------------------------------------------------------------ + # Text tokenizer + # ------------------------------------------------------------------ + + def _tokenize_text(self, text: str, device: torch.device) -> torch.Tensor: + """Tokenize text using the BPE tokenizer, add start/stop tokens.""" + if self._text_tokenizer is None: + from .tokenizer import IndexTTS2Tokenizer + + bpe_path = resolve_model_file(self.model_path, "bpe.model") + if bpe_path is None: + raise FileNotFoundError(f"BPE model not found in {self.model_path}") + self._text_tokenizer = IndexTTS2Tokenizer(bpe_path, model_dir=self.model_path) + + token_ids = self._text_tokenizer.encode(text, add_special_tokens=False) + + # Wrap with start/stop text tokens + start_text = 0 + stop_text = 1 + token_ids = [start_text] + token_ids + [stop_text] + return torch.tensor([token_ids], device=device, dtype=torch.long) + + # ------------------------------------------------------------------ + # Lazy loading helpers + # ------------------------------------------------------------------ + + def _ensure_w2v_stat_loaded(self, device: torch.device) -> None: + if self._w2v_stat is not None: + return + stat_path = resolve_model_file(self.model_path, self.config.w2v_stat) + if stat_path is not None: + raw = torch.load(stat_path, map_location="cpu", weights_only=True) + if isinstance(raw, dict): + self._w2v_stat = torch.stack([raw["mean"], raw["var"]]) # [2, 1024] + else: + self._w2v_stat = raw + else: + logger.warning("Wav2Vec2-BERT stat file not found in %s", self.model_path) + + def _ensure_matrix_loaded(self, attr: str, cfg_key: str, label: str) -> None: + if getattr(self, attr) is not None: + return + path = resolve_model_file(self.model_path, getattr(self.config, cfg_key)) + if path is not None: + raw = torch.load(path, map_location="cpu", weights_only=True) + setattr(self, attr, torch.split(raw, list(self.config.emo_num))) + else: + logger.warning("%s not found in %s", label, self.model_path) + + # ------------------------------------------------------------------ + # Weight loading + # ------------------------------------------------------------------ + + def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]: + """Load weights from IndexTTS2 checkpoint (gpt.pth). + + Bypasses vllm's default .pt iterator because the model directory + contains raw-tensor files (feat1.pt, feat2.pt) that are not state + dicts and would crash ``pt_weights_iterator``. + + Weight mapping from gpt.pth checkpoint → vLLM model params: + gpt.h.{i}.* → h.{i}.* (strip ``gpt.`` prefix) + gpt.ln_f.* → ln_f.* + final_norm.* → final_norm.* + text_head.* → (skipped) + → identity + """ + _ = weights # don't iterate — raw .pt files crash the default iterator + + ckpt_path = resolve_model_file(self.model_path, self.config.gpt_checkpoint) + if ckpt_path is None: + raise FileNotFoundError(f"IndexTTS2 GPT checkpoint {self.config.gpt_checkpoint!r} not found") + state = torch.load(ckpt_path, map_location="cpu", weights_only=True) + + prefix_map = { + "gpt.h.": "h.", + "gpt.ln_f.": "ln_f.", + "final_norm.": "final_norm.", + "text_head.": "_skip_text_head.", + } + + params_dict = dict(self.named_parameters(remove_duplicate=False)) + loaded_params: set[str] = set() + + for name, loaded_weight in state.items(): + # Skip attention masks + if ".attn.bias" in name or ".attn.masked_bias" in name: + continue + # Skip wpe (null position embeddings) + if ".wpe." in name or ".wte." in name: + continue + + # Remap checkpoint name to model name + 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 + + # Skip unmapped / intentionally skipped weights + if mapped_name.startswith("_skip_"): + continue + + if is_pp_missing_parameter(mapped_name, self): + continue + + if mapped_name not in params_dict: + logger.debug("Skipping unrecognized weight: %s → %s", name, mapped_name) + continue + + param = params_dict[mapped_name] + + # GPT-2 Conv1D → Linear transpose + for conv1d_name in ["c_attn", "c_proj", "c_fc"]: + if conv1d_name not in mapped_name: + continue + if not mapped_name.endswith(".weight"): + continue + loaded_weight = loaded_weight.t() + + 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 IndexTTS2TalkerForConditionalGeneration", + len(loaded_params), + ) + required_prefixes = [ + "text_embedding.", + "mel_embedding.", + "text_pos_embedding.", + "mel_pos_embedding.", + "speed_emb.", + "emo_layer.", + "emovec_layer.", + "conditioning_encoder.", + "perceiver_encoder.", + "emo_conditioning_encoder.", + "emo_perceiver_encoder.", + "h.", + "ln_f.", + "final_norm.", + ] + if self.mel_head is not None: + required_prefixes.append("mel_head.") + 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 GPT checkpoint did not load required parameter groups: " + ", ".join(missing_prefixes) + ) + + # Ensure all sub-modules on correct device (some nn.Parameter created + # with torch.Tensor() end up on CPU even when model is on CUDA). + target_device = next((p.device for p in self.h.parameters()), torch.device("cpu")) + for mod in [ + self.conditioning_encoder, + self.emo_conditioning_encoder, + self.perceiver_encoder, + self.emo_perceiver_encoder, + ]: + mod.to(target_device) + + self._warmup_external_models(target_device) + return loaded_params + + def _warmup_external_models(self, device: torch.device) -> None: + """Preload external models to eliminate first-request latency.""" + try: + load_wav2vec2(self.model_path, device) + load_semantic_codec(self.model_path, self.config.semantic_codec, device) + load_campplus(self.model_path, device) + self._ensure_w2v_stat_loaded(device) + logger.info("External models preloaded: Wav2Vec2, RepCodec, CAMPPlus") + except Exception as e: + logger.warning("Failed to preload external models: %s", e) diff --git a/vllm_omni/model_executor/models/indextts2/pipeline.py b/vllm_omni/model_executor/models/indextts2/pipeline.py new file mode 100644 index 000000000..2ce02cee8 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/pipeline.py @@ -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}, + ), + ), +) diff --git a/vllm_omni/model_executor/models/indextts2/preprocess_utils.py b/vllm_omni/model_executor/models/indextts2/preprocess_utils.py new file mode 100644 index 000000000..452bce9bb --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/preprocess_utils.py @@ -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) + ) diff --git a/vllm_omni/model_executor/models/indextts2/prompt_utils.py b/vllm_omni/model_executor/models/indextts2/prompt_utils.py new file mode 100644 index 000000000..c33202785 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/prompt_utils.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/__init__.py b/vllm_omni/model_executor/models/indextts2/s2mel/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/__init__.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/audio.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/audio.py new file mode 100644 index 000000000..21bb45d73 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/audio.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/bigvgan.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/bigvgan.py new file mode 100644 index 000000000..5876bb147 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/bigvgan.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/commons.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/commons.py new file mode 100644 index 000000000..dd86f5847 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/commons.py @@ -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() diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/diffusion_transformer.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/diffusion_transformer.py new file mode 100644 index 000000000..f73fdc800 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/diffusion_transformer.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/encodec.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/encodec.py new file mode 100644 index 000000000..a41141dcb --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/encodec.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/flow_matching.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/flow_matching.py new file mode 100644 index 000000000..5192867af --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/flow_matching.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/__init__.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/model.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/model.py new file mode 100644 index 000000000..c410b6387 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/gpt_fast/model.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/length_regulator.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/length_regulator.py new file mode 100644 index 000000000..1f2757781 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/length_regulator.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/s2mel/modules/wavenet.py b/vllm_omni/model_executor/models/indextts2/s2mel/modules/wavenet.py new file mode 100644 index 000000000..4c5381766 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/s2mel/modules/wavenet.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/tokenizer.py b/vllm_omni/model_executor/models/indextts2/tokenizer.py new file mode 100644 index 000000000..be20d02be --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/tokenizer.py @@ -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, + ) diff --git a/vllm_omni/model_executor/models/indextts2/utils/__init__.py b/vllm_omni/model_executor/models/indextts2/utils/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/utils/campplus/__init__.py b/vllm_omni/model_executor/models/indextts2/utils/campplus/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/campplus/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/utils/campplus/dtdnn.py b/vllm_omni/model_executor/models/indextts2/utils/campplus/dtdnn.py new file mode 100644 index 000000000..71766d04a --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/campplus/dtdnn.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/utils/campplus/layers.py b/vllm_omni/model_executor/models/indextts2/utils/campplus/layers.py new file mode 100644 index 000000000..4919cf16e --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/campplus/layers.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/utils/common.py b/vllm_omni/model_executor/models/indextts2/utils/common.py new file mode 100644 index 000000000..c969dc159 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/common.py @@ -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"") + + words = line.split() + # restore english sentences + sent_placeholder_pattern = re.compile(r"()") + 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 diff --git a/vllm_omni/model_executor/models/indextts2/utils/front.py b/vllm_omni/model_executor/models/indextts2/utils/front.py new file mode 100644 index 000000000..3c3bd4cb7 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/front.py @@ -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 "" + + @property + def pad_token(self): + return None + + @property + def bos_token(self): + return "" + + @property + def eos_token(self): + return "" + + @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, + ) diff --git a/vllm_omni/model_executor/models/indextts2/utils/maskgct/__init__.py b/vllm_omni/model_executor/models/indextts2/utils/maskgct/__init__.py new file mode 100644 index 000000000..208f01a7c --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/maskgct/__init__.py @@ -0,0 +1,2 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project diff --git a/vllm_omni/model_executor/models/indextts2/utils/maskgct/quantize.py b/vllm_omni/model_executor/models/indextts2/utils/maskgct/quantize.py new file mode 100644 index 000000000..e4fcc6c79 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/maskgct/quantize.py @@ -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 diff --git a/vllm_omni/model_executor/models/indextts2/utils/maskgct/repcodec_model.py b/vllm_omni/model_executor/models/indextts2/utils/maskgct/repcodec_model.py new file mode 100644 index 000000000..fbccdb363 --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/maskgct/repcodec_model.py @@ -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) diff --git a/vllm_omni/model_executor/models/indextts2/utils/maskgct/vocos.py b/vllm_omni/model_executor/models/indextts2/utils/maskgct/vocos.py new file mode 100644 index 000000000..ce735a9ad --- /dev/null +++ b/vllm_omni/model_executor/models/indextts2/utils/maskgct/vocos.py @@ -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 diff --git a/vllm_omni/model_executor/models/registry.py b/vllm_omni/model_executor/models/registry.py index d057502c2..279de283d 100644 --- a/vllm_omni/model_executor/models/registry.py +++ b/vllm_omni/model_executor/models/registry.py @@ -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", diff --git a/vllm_omni/model_executor/stage_input_processors/indextts2.py b/vllm_omni/model_executor/stage_input_processors/indextts2.py new file mode 100644 index 000000000..9d38c8f2a --- /dev/null +++ b/vllm_omni/model_executor/stage_input_processors/indextts2.py @@ -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") diff --git a/vllm_omni/worker/gpu_ar_model_runner.py b/vllm_omni/worker/gpu_ar_model_runner.py index a23d6f4e2..23e6a4aac 100644 --- a/vllm_omni/worker/gpu_ar_model_runner.py +++ b/vllm_omni/worker/gpu_ar_model_runner.py @@ -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( diff --git a/vllm_omni/worker/gpu_generation_model_runner.py b/vllm_omni/worker/gpu_generation_model_runner.py index bc3d6ea2d..07f394842 100644 --- a/vllm_omni/worker/gpu_generation_model_runner.py +++ b/vllm_omni/worker/gpu_generation_model_runner.py @@ -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(