[Model] Support Qwen3-TTS model series (#895)

Signed-off-by: Gaohan123 <hgaoaf@connect.ust.hk>
This commit is contained in:
Gao Han authored and GitHub committed 2026-01-22 16:57:37 +08:00
1 parent 0df8e80574
commit a54c323952
35 files changed
+9642 -42

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ nav:
- Image-To-Video: user_guide/examples/offline_inference/image_to_video.md
- Qwen2.5-Omni: user_guide/examples/offline_inference/qwen2_5_omni.md
- Qwen3-Omni: user_guide/examples/offline_inference/qwen3_omni.md
- Qwen3-TTS Offline Inference: user_guide/examples/offline_inference/qwen3_tts.md
- Text-To-Image: user_guide/examples/offline_inference/text_to_image.md
- Text-To-Video: user_guide/examples/offline_inference/text_to_video.md
- Online Serving:
+16 -33
View File
@@ -36,7 +36,6 @@ Input data structures for multi-modal inputs.
Engine classes for offline and online inference.
- [vllm_omni.diffusion.diffusion_engine.BackgroundResources][]
- [vllm_omni.diffusion.diffusion_engine.DiffusionEngine][]
- [vllm_omni.engine.AdditionalInformationEntry][]
- [vllm_omni.engine.AdditionalInformationPayload][]
@@ -57,38 +56,11 @@ Core scheduling and caching components.
- [vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler][]
- [vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler][]
- [vllm_omni.core.sched.output.OmniNewRequestData][]
## Model Executor
Model execution components.
- [vllm_omni.model_executor.custom_process_mixin.CustomProcessMixin][]
- [vllm_omni.model_executor.models.output_templates.OmniOutput][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni.Qwen2_5OmniForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_talker.Qwen2_5OmniTalkerForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniConditionalGenerationMixin][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_thinker.Qwen2_5OmniThinkerForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavBigVGANModel][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavDiTModel][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavForConditionalGenerationVLLM][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_5_omni_token2wav.Qwen2_5OmniToken2WavModel][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2EmbeddingModel][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2ForCausalLM][]
- [vllm_omni.model_executor.models.qwen2_5_omni.qwen2_old.Qwen2Model][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_moe.Qwen3MoeForCausalLM][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni.Qwen3OmniMoeForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_code2wav.Qwen3OmniMoeCode2Wav][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_code_predictor_mtp.Qwen3OmniCodePredictorBaseModel][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_code_predictor_mtp.Qwen3OmniMoeTalkerCodePredictor][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_talker.Qwen3OmniMoeModel][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_talker.Qwen3OmniMoeTalkerForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_talker.Qwen3OmniMoeTalkerSharedExpertWrapper][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3MoeLLMForCausalLM][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3MoeLLMModel][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeConditionalGenerationMixin][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerForConditionalGeneration][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerMultiModalProcessor][]
- [vllm_omni.model_executor.models.qwen3_omni.qwen3_omni_moe_thinker.Qwen3OmniMoeThinkerProcessingInfo][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedGroupResidualVectorQuantization][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.DistributedResidualVectorQuantization][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.EuclideanCodebook][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.VectorQuantization][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.vq.core_vq.preprocess][]
## Configuration
@@ -98,6 +70,17 @@ Configuration classes.
- [vllm_omni.diffusion.cache.teacache.config.TeaCacheConfig][]
- [vllm_omni.distributed.omni_connectors.utils.config.ConnectorSpec][]
- [vllm_omni.distributed.omni_connectors.utils.config.OmniTransferConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSSpeakerEncoderConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerCodePredictorConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts.Qwen3TTSTalkerConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2Config][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2.Qwen3TTSTokenizerV2DecoderConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1Config][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderBigVGANConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1DecoderDiTConfig][]
- [vllm_omni.model_executor.models.qwen3_tts.tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1.Qwen3TTSTokenizerV1EncoderConfig][]
## Workers
+4 -4
View File
@@ -34,10 +34,10 @@ CATEGORIES = {
"name": "Core",
"description": "Core scheduling and caching components.",
},
"model_executor": {
"name": "Model Executor",
"description": "Model execution components.",
},
# "model_executor": {
# "name": "Model Executor",
# "description": "Model execution components.",
# },
"config": {
"name": "Configuration",
"description": "Configuration classes.",
+3
View File
@@ -34,6 +34,9 @@ th {
|`StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` |
|`Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` |
|`StableAudioPipeline` | Stable-Audio-Open | `stabilityai/stable-audio-open-1.0` |
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-CustomVoice | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice` |
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-VoiceDesign | `Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign` |
|`Qwen3TTSForConditionalGeneration` | Qwen3-TTS-12Hz-1.7B-Base | `Qwen/Qwen3-TTS-12Hz-0.6B-Base` |
## List of Supported Models for NPU
@@ -0,0 +1,94 @@
# Qwen3-TTS Offline Inference
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/offline_inference/qwen3_tts>.
This directory contains an offline demo for running Qwen3 TTS models with vLLM Omni. It builds task-specific inputs and generates WAV files locally.
## Model Overview
Qwen3 TTS provides multiple task variants for speech generation:
- **CustomVoice**: Generate speech with a known speaker identity (speaker ID) and optional instruction.
- **VoiceDesign**: Generate speech from text plus a descriptive instruction that designs a new voice.
- **Base**: Voice cloning using a reference audio + reference transcript, with optional mode selection.
## Setup
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
## Quick Start
Run a single sample for a task:
```
python end2end.py --query-type CustomVoice
```
Generated audio files are saved to `output_audio/` by default.
## Task Usage
### CustomVoice
Single sample:
```
python end2end.py --query-type CustomVoice
```
Batch sample (multiple prompts in one run):
```
python end2end.py --query-type CustomVoice --use-batch-sample
```
### VoiceDesign
Single sample:
```
python end2end.py --query-type VoiceDesign
```
Batch sample:
```
python end2end.py --query-type VoiceDesign --use-batch-sample
```
### Base (Voice Clone)
Single sample:
```
python end2end.py --query-type Base
```
Batch sample:
```
python end2end.py --query-type Base --use-batch-sample
```
Mode selection for Base:
- `--mode-tag icl` (default): standard mode
- `--mode-tag xvec_only`: enable `x_vector_only_mode` in the request
Examples:
```
python end2end.py --query-type Base --mode-tag icl
```
## Notes
- The script uses the model paths embedded in `end2end.py`. Update them if your local cache path differs.
- Use `--output-dir` (preferred) or `--output-wav` to change the output folder.
## Example materials
??? abstract "end2end.py"
``````py
--8<-- "examples/offline_inference/qwen3_tts/end2end.py"
``````
@@ -119,6 +119,7 @@ Use `extra_body` to pass generation parameters:
| `seed` | int | None | Random seed (reproducible) |
| `negative_prompt` | str | None | Negative prompt |
| `num_outputs_per_prompt` | int | 1 | Number of images to generate |
| `--cfg-parallel-size`. | int | 1 | Number of GPUs for CFG parallelism |
## Response Format
@@ -0,0 +1,84 @@
# Qwen3-TTS Offline Inference
This directory contains an offline demo for running Qwen3 TTS models with vLLM Omni. It builds task-specific inputs and generates WAV files locally.
## Model Overview
Qwen3 TTS provides multiple task variants for speech generation:
- **CustomVoice**: Generate speech with a known speaker identity (speaker ID) and optional instruction.
- **VoiceDesign**: Generate speech from text plus a descriptive instruction that designs a new voice.
- **Base**: Voice cloning using a reference audio + reference transcript, with optional mode selection.
## Setup
Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
## Quick Start
Run a single sample for a task:
```
python end2end.py --query-type CustomVoice
```
Generated audio files are saved to `output_audio/` by default.
## Task Usage
### CustomVoice
Single sample:
```
python end2end.py --query-type CustomVoice
```
Batch sample (multiple prompts in one run):
```
python end2end.py --query-type CustomVoice --use-batch-sample
```
### VoiceDesign
Single sample:
```
python end2end.py --query-type VoiceDesign
```
Batch sample:
```
python end2end.py --query-type VoiceDesign --use-batch-sample
```
### Base (Voice Clone)
Single sample:
```
python end2end.py --query-type Base
```
Batch sample:
```
python end2end.py --query-type Base --use-batch-sample
```
Mode selection for Base:
- `--mode-tag icl` (default): standard mode
- `--mode-tag xvec_only`: enable `x_vector_only_mode` in the request
Examples:
```
python end2end.py --query-type Base --mode-tag icl
```
## Notes
- The script uses the model paths embedded in `end2end.py`. Update them if your local cache path differs.
- Use `--output-dir` (preferred) or `--output-wav` to change the output folder.
@@ -0,0 +1,381 @@
"""Offline inference demo for Qwen3 TTS via vLLM Omni.
Provides single and batch sample inputs for CustomVoice, VoiceDesign, and Base
tasks, then runs Omni generation and saves output wav files.
"""
import os
from typing import NamedTuple
import soundfile as sf
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
from vllm import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni import Omni
class QueryResult(NamedTuple):
"""Container for a prepared Omni request."""
inputs: dict
model_name: str
def get_custom_voice_query(use_batch_sample: bool = False) -> QueryResult:
"""Build CustomVoice sample inputs.
Args:
use_batch_sample: When True, return a batch of prompts; otherwise a single prompt.
Returns:
QueryResult with Omni inputs and the CustomVoice model path.
"""
task_type = "CustomVoice"
if use_batch_sample:
texts = ["其实我真的有发现,我是一个特别善于观察别人情绪的人。", "She said she would be here by noon."]
instructs = ["", "Very happy."]
languages = ["Chinese", "English"]
speakers = ["Vivian", "Ryan"]
inputs = []
for text, instruct, language, speaker in zip(texts, instructs, languages, speakers):
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs.append(
{
"prompt": prompt,
"additional_information": {
"task_type": [task_type],
"text": [text],
"instruct": [instruct],
"language": [language],
"speaker": [speaker],
"max_new_tokens": [2048],
},
}
)
else:
text = "其实我真的有发现,我是一个特别善于观察别人情绪的人。"
language = "Chinese"
speaker = "Vivian"
instruct = "用特别愤怒的语气说"
prompts = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs = {
"prompt": prompts,
"additional_information": {
"task_type": [task_type],
"text": [text],
"language": [language],
"speaker": [speaker],
"instruct": [instruct],
"max_new_tokens": [2048],
},
}
return QueryResult(
inputs=inputs,
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
)
def get_voice_design_query(use_batch_sample: bool = False) -> QueryResult:
"""Build VoiceDesign sample inputs.
Args:
use_batch_sample: When True, return a batch of prompts; otherwise a single prompt.
Returns:
QueryResult with Omni inputs and the VoiceDesign model path.
"""
task_type = "VoiceDesign"
if use_batch_sample:
texts = [
"哥哥,你回来啦,人家等了你好久好久了,要抱抱!",
"It's in the top drawer... wait, it's empty? No way, that's impossible! I'm sure I put it there!",
]
instructs = [
"体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。",
"Speak in an incredulous tone, but with a hint of panic beginning to creep into your voice.",
]
languages = ["Chinese", "English"]
inputs = []
for text, instruct, language in zip(texts, instructs, languages):
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs.append(
{
"prompt": prompt,
"additional_information": {
"task_type": [task_type],
"text": [text],
"language": [language],
"instruct": [instruct],
"max_new_tokens": [2048],
"non_streaming_mode": [True],
},
}
)
else:
text = "哥哥,你回来啦,人家等了你好久好久了,要抱抱!"
instruct = "体现撒娇稚嫩的萝莉女声,音调偏高且起伏明显,营造出黏人、做作又刻意卖萌的听觉效果。"
language = "Chinese"
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs = {
"prompt": prompt,
"additional_information": {
"task_type": [task_type],
"text": [text],
"language": [language],
"instruct": [instruct],
"max_new_tokens": [2048],
"non_streaming_mode": [True],
},
}
return QueryResult(
inputs=inputs,
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
)
def get_base_query(use_batch_sample: bool = False, mode_tag: str = "icl") -> QueryResult:
"""Build Base (voice clone) sample inputs.
Args:
use_batch_sample: When True, return a batch of prompts (Case 2).
mode_tag: "icl" or "xvec_only" to control x_vector_only_mode behavior.
Returns:
QueryResult with Omni inputs and the Base model path.
"""
task_type = "Base"
ref_audio_path_1 = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-TTS-Repo/clone_2.wav"
ref_audio_single = ref_audio_path_1
ref_text_single = (
"Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you."
)
syn_text_single = "Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye."
syn_lang_single = "Auto"
x_vector_only_mode = mode_tag == "xvec_only"
if use_batch_sample:
syn_text_batch = [
"Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye.",
"其实我真的有发现,我是一个特别善于观察别人情绪的人。",
]
syn_lang_batch = ["Chinese", "English"]
inputs = []
for text, language in zip(syn_text_batch, syn_lang_batch):
prompt = f"<|im_start|>assistant\n{text}<|im_end|>\n<|im_start|>assistant\n"
inputs.append(
{
"prompt": prompt,
"additional_information": {
"task_type": [task_type],
"ref_audio": [ref_audio_single],
"ref_text": [ref_text_single],
"text": [text],
"language": [language],
"x_vector_only_mode": [x_vector_only_mode],
"max_new_tokens": [2048],
},
}
)
else:
prompt = f"<|im_start|>assistant\n{syn_text_single}<|im_end|>\n<|im_start|>assistant\n"
inputs = {
"prompt": prompt,
"additional_information": {
"task_type": [task_type],
"ref_audio": [ref_audio_single],
"ref_text": [ref_text_single],
"text": [syn_text_single],
"language": [syn_lang_single],
"x_vector_only_mode": [x_vector_only_mode],
"max_new_tokens": [2048],
},
}
return QueryResult(
inputs=inputs,
model_name="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
)
def main(args):
"""Run offline inference with Omni using prepared sample inputs.
Args:
args: Parsed CLI args from parse_args().
"""
query_func = query_map[args.query_type]
if args.query_type in {"CustomVoice", "VoiceDesign"}:
query_result = query_func(use_batch_sample=args.use_batch_sample)
elif args.query_type == "Base":
query_result = query_func(
use_batch_sample=args.use_batch_sample,
mode_tag=args.mode_tag,
)
else:
query_result = query_func()
model_name = query_result.model_name
omni = Omni(
model=model_name,
stage_configs_path=args.stage_configs_path,
log_stats=args.enable_stats,
stage_init_timeout=args.stage_init_timeout,
)
sampling_params = SamplingParams(
temperature=0.9,
top_p=1.0,
top_k=50,
max_tokens=2048,
seed=42,
detokenize=False,
repetition_penalty=1.05,
)
sampling_params_list = [
sampling_params,
]
output_dir = args.output_dir if getattr(args, "output_dir", None) else args.output_wav
os.makedirs(output_dir, exist_ok=True)
omni_generator = omni.generate(query_result.inputs, sampling_params_list)
for stage_outputs in omni_generator:
for output in stage_outputs.request_output:
request_id = output.request_id
audio_tensor = output.multimodal_output["audio"]
output_wav = os.path.join(output_dir, f"output_{request_id}.wav")
audio_samplerate = output.multimodal_output["sr"].item()
# Convert to numpy array and ensure correct format
audio_numpy = audio_tensor.float().detach().cpu().numpy()
# Ensure audio is 1D (flatten if needed)
if audio_numpy.ndim > 1:
audio_numpy = audio_numpy.flatten()
# Save audio file with explicit WAV format
sf.write(output_wav, audio_numpy, samplerate=audio_samplerate, format="WAV")
print(f"Request ID: {request_id}, Saved audio to {output_wav}")
def parse_args():
"""Parse CLI arguments for offline TTS inference.
Returns:
argparse.Namespace with CLI options.
"""
parser = FlexibleArgumentParser(description="Demo on using vLLM for offline inference with audio language models")
parser.add_argument(
"--query-type",
"-q",
type=str,
default="CustomVoice",
choices=query_map.keys(),
help="Query type.",
)
parser.add_argument(
"--enable-stats",
action="store_true",
default=False,
help="Enable writing detailed statistics (default: disabled)",
)
parser.add_argument(
"--stage-init-timeout",
type=int,
default=300,
help="Timeout for initializing a single stage in seconds (default: 300)",
)
parser.add_argument(
"--batch-timeout",
type=int,
default=5,
help="Timeout for batching in seconds (default: 5)",
)
parser.add_argument(
"--init-timeout",
type=int,
default=300,
help="Timeout for initializing stages in seconds (default: 300)",
)
parser.add_argument(
"--shm-threshold-bytes",
type=int,
default=65536,
help="Threshold for using shared memory in bytes (default: 65536)",
)
parser.add_argument(
"--output-wav",
default="output_audio",
help="[Deprecated] Output wav directory (use --output-dir).",
)
parser.add_argument(
"--num-prompts",
type=int,
default=1,
help="Number of prompts to generate.",
)
parser.add_argument(
"--txt-prompts",
type=str,
default=None,
help="Path to a .txt file with one prompt per line (preferred).",
)
parser.add_argument(
"--stage-configs-path",
type=str,
default=None,
help="Path to a stage configs file.",
)
parser.add_argument(
"--audio-path",
"-a",
type=str,
default=None,
help="Path to local audio file. If not provided, uses default audio asset.",
)
parser.add_argument(
"--sampling-rate",
type=int,
default=16000,
help="Sampling rate for audio loading (default: 16000).",
)
parser.add_argument(
"--log-dir",
type=str,
default="logs",
help="Log directory (default: logs).",
)
parser.add_argument(
"--py-generator",
action="store_true",
default=False,
help="Use py_generator mode. The returned type of Omni.generate() is a Python Generator object.",
)
parser.add_argument(
"--use-batch-sample",
action="store_true",
default=False,
help="Use batch input sample for CustomVoice/VoiceDesign/Base query.",
)
parser.add_argument(
"--mode-tag",
type=str,
default="icl",
choices=["icl", "xvec_only"],
help="Mode tag for Base query x_vector_only_mode (default: icl).",
)
return parser.parse_args()
query_map = {
"CustomVoice": get_custom_voice_query,
"VoiceDesign": get_voice_design_query,
"Base": get_base_query,
}
if __name__ == "__main__":
args = parse_args()
main(args)
@@ -0,0 +1,59 @@
from vllm_omni.inputs.preprocess import OmniInputPreprocessor
def _make_preprocessor(monkeypatch):
preprocessor = object.__new__(OmniInputPreprocessor)
monkeypatch.setattr(preprocessor, "_truncate_inputs", lambda tokens, tokenization_kwargs=None: tokens)
monkeypatch.setattr(
preprocessor,
"_process_multimodal",
lambda *args, **kwargs: {"prompt_token_ids": [1, 2, 3]},
)
monkeypatch.setattr(preprocessor, "_tokenize_prompt", lambda prompt_text, tokenization_kwargs=None: [9, 8, 7])
return preprocessor
def test_process_tokens_keeps_additional_information(monkeypatch):
preprocessor = _make_preprocessor(monkeypatch)
parsed = {
"prompt_token_ids": [1, 2, 3],
"prompt_embeds": "embeds",
"additional_information": {"task": ["tts"], "lang": ["auto"]},
}
inputs = OmniInputPreprocessor._process_tokens(preprocessor, parsed)
assert inputs["prompt_token_ids"] == [1, 2, 3]
assert inputs["prompt_embeds"] == "embeds"
assert inputs["additional_information"] == {"task": ["tts"], "lang": ["auto"]}
def test_process_text_keeps_additional_information(monkeypatch):
preprocessor = _make_preprocessor(monkeypatch)
parsed = {
"prompt": "hello",
"prompt_embeds": "embeds",
"additional_information": {"speaker": ["alice"]},
}
inputs = OmniInputPreprocessor._process_text(preprocessor, parsed)
assert inputs["prompt_token_ids"] == [9, 8, 7]
assert inputs["prompt_embeds"] == "embeds"
assert inputs["additional_information"] == {"speaker": ["alice"]}
def test_process_text_multimodal_skips_empty_payloads(monkeypatch):
preprocessor = _make_preprocessor(monkeypatch)
parsed = {
"prompt": "hello",
"multi_modal_data": {"image": "fake"},
"prompt_embeds": None,
"additional_information": None,
}
inputs = OmniInputPreprocessor._process_text(preprocessor, parsed)
assert inputs["prompt_token_ids"] == [1, 2, 3]
assert "prompt_embeds" not in inputs
assert "additional_information" not in inputs
@@ -0,0 +1,49 @@
from types import SimpleNamespace
import torch
from vllm_omni.core.sched.output import OmniNewRequestData
def test_omni_new_request_data_copies_payloads():
prompt_embeds = torch.randn(2, 3)
additional_information = {
"speaker": ["test"],
"codes": torch.tensor([1, 2], dtype=torch.int64),
}
request = SimpleNamespace(
request_id="req-1",
prompt_token_ids=[101, 102],
mm_features=None,
sampling_params=None,
pooling_params=None,
num_computed_tokens=0,
lora_request=None,
prompt_embeds=prompt_embeds,
additional_information=additional_information,
)
data = OmniNewRequestData.from_request(request, ([0, 1],), prefill_token_ids=[101, 102])
assert data.prompt_embeds is prompt_embeds
assert data.additional_information is additional_information
assert data.prefill_token_ids == [101, 102]
def test_omni_new_request_data_allows_missing_payloads():
request = SimpleNamespace(
request_id="req-2",
prompt_token_ids=[201, 202],
mm_features=None,
sampling_params=None,
pooling_params=None,
num_computed_tokens=0,
lora_request=None,
prompt_embeds=None,
additional_information=None,
)
data = OmniNewRequestData.from_request(request, ([0],), prefill_token_ids=None)
assert data.prompt_embeds is None
assert data.additional_information is None
@@ -0,0 +1,75 @@
import torch
from vllm_omni.worker.gpu_generation_model_runner import GPUGenerationModelRunner
class _DummyInputBatch:
def __init__(self):
self.req_ids = ["req-1"]
self.req_id_to_index = {"req-1": 0}
self.num_reqs = 1
self.vocab_size = 10
def _make_runner(multimodal_outputs):
runner = object.__new__(GPUGenerationModelRunner)
runner.execute_model_state = (
None,
None,
None,
None,
None,
None,
None,
None,
None,
multimodal_outputs,
)
runner.kv_connector_output = None
runner.input_batch = _DummyInputBatch()
runner.use_async_scheduling = False
runner.device = torch.device("cpu")
runner.supports_mm_inputs = False
return runner
def test_sample_tokens_tensor_output():
multimodal_outputs = torch.randn(1, 2, 3)
runner = _make_runner(multimodal_outputs)
output = GPUGenerationModelRunner.sample_tokens(runner)
assert len(output.pooler_output) == 1
assert output.pooler_output[0]["model_outputs"].shape == (2, 3)
def test_sample_tokens_list_output():
multimodal_outputs = [torch.randn(2, 1)]
runner = _make_runner(multimodal_outputs)
output = GPUGenerationModelRunner.sample_tokens(runner)
assert len(output.pooler_output) == 1
assert output.pooler_output[0]["model_outputs"].shape == (2, 1)
def test_sample_tokens_list_allows_none_output():
multimodal_outputs = [None]
runner = _make_runner(multimodal_outputs)
output = GPUGenerationModelRunner.sample_tokens(runner)
assert len(output.pooler_output) == 1
assert output.pooler_output[0]["model_outputs"] is None
def test_sample_tokens_dict_output():
multimodal_outputs = {"audio": torch.randn(1, 4), "unused": None}
runner = _make_runner(multimodal_outputs)
output = GPUGenerationModelRunner.sample_tokens(runner)
assert len(output.pooler_output) == 1
assert "audio" in output.pooler_output[0]
assert "unused" not in output.pooler_output[0]
assert output.pooler_output[0]["audio"].shape == (1, 4)
@@ -2,6 +2,7 @@ import time
from collections import defaultdict
from vllm.distributed.kv_events import KVEventBatch
from vllm.logger import init_logger
from vllm.v1.core.kv_cache_manager import KVCacheBlocks
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.core.sched.request_queue import create_request_queue
@@ -145,6 +146,35 @@ class OmniGenerationScheduler(VLLMScheduler):
# Update internal state (advance num_computed_tokens, free encoder inputs,
# etc.)
self._update_after_schedule(scheduler_output)
try:
# Rewrap base NewRequestData entries with OmniNewRequestData,
# enriching with request-level payloads
new_list = []
for nr in scheduler_output.scheduled_new_reqs:
req_id = getattr(nr, "req_id", None)
request = self.requests.get(req_id) if req_id else None
# Build omni entry preserving all base fields
omni_nr = OmniNewRequestData(
req_id=nr.req_id,
prompt_token_ids=nr.prompt_token_ids,
mm_features=nr.mm_features,
sampling_params=nr.sampling_params,
pooling_params=nr.pooling_params,
block_ids=nr.block_ids,
num_computed_tokens=nr.num_computed_tokens,
lora_request=nr.lora_request,
# Enrich with omni payloads from the live request object
prompt_embeds=(getattr(request, "prompt_embeds", None) if request else None),
additional_information=(getattr(request, "additional_information", None) if request else None),
)
new_list.append(omni_nr)
scheduler_output.scheduled_new_reqs = new_list # type: ignore[assignment]
except Exception:
# If anything goes wrong, leave the original output unchanged
init_logger(__name__).exception("Failed to wrap scheduled_new_reqs with OmniNewRequestData")
return scheduler_output
"""
+20
View File
@@ -11,11 +11,31 @@ from vllm_omni.config import OmniModelConfig
logger = init_logger(__name__)
def _register_omni_hf_configs() -> None:
try:
from transformers import AutoConfig
from vllm_omni.model_executor.models.qwen3_tts.configuration_qwen3_tts import (
Qwen3TTSConfig,
)
except Exception as exc: # pragma: no cover - best-effort optional registration
logger.warning("Skipping omni HF config registration due to import error: %s", exc)
return
try:
AutoConfig.register("qwen3_tts", Qwen3TTSConfig)
except ValueError:
# Already registered elsewhere; ignore.
return
def register_omni_models_to_vllm():
from vllm.model_executor.models import ModelRegistry
from vllm_omni.model_executor.models.registry import _OMNI_MODELS
_register_omni_hf_configs()
supported_archs = ModelRegistry.get_supported_archs()
for arch, (mod_folder, mod_relname, cls_name) in _OMNI_MODELS.items():
if arch not in supported_archs:
+4 -4
View File
@@ -232,8 +232,8 @@ class OmniInputProcessor(InputProcessor):
# (direct-transfer path)
prompt_embeds_payload: PromptEmbedsPayload | None = None
additional_information_payload: AdditionalInformationPayload | None = None
if "prompt_embeds" in decoder_inputs: # type: ignore[operator]
pe: torch.Tensor = decoder_inputs["prompt_embeds"] # type: ignore[index]
pe: torch.Tensor | None = decoder_inputs.get("prompt_embeds") # type: ignore[operator]
if pe is not None:
if pe.ndim != 2:
raise ValueError("prompt_embeds must be of shape (seq_len, hidden_size)")
# Move to CPU and ensure contiguous memory for stable serialization
@@ -246,8 +246,8 @@ class OmniInputProcessor(InputProcessor):
shape=[int(seq_len), int(hidden_size)],
dtype=dtype_str,
)
if "additional_information" in decoder_inputs: # type: ignore[operator]
raw_info: dict[str, Any] = decoder_inputs["additional_information"] # type: ignore[index] # noqa: E501
raw_info: dict[str, Any] | None = decoder_inputs.get("additional_information") # type: ignore[operator]
if raw_info is not None:
entries: dict[str, AdditionalInformationEntry] = {}
for key, value in raw_info.items():
if isinstance(value, torch.Tensor):
+40
View File
@@ -20,6 +20,46 @@ class OmniInputPreprocessor(InputPreprocessor):
Supports processing tokens, embeddings, text, and multimodal inputs.
"""
def _process_text(
self,
parsed_content: TextPrompt,
tokenization_kwargs: dict[str, Any] | None = None,
*,
mm_uuids: MultiModalUUIDDict | None = None,
) -> OmniTokenInputs | MultiModalInputs:
prompt_text = parsed_content["prompt"]
inputs: OmniTokenInputs | MultiModalInputs
if multi_modal_data := parsed_content.get("multi_modal_data"):
inputs = self._process_multimodal(
prompt_text,
multi_modal_data,
parsed_content.get("mm_processor_kwargs") or {},
tokenization_kwargs=tokenization_kwargs,
mm_uuids=mm_uuids,
)
prompt_embeds = parsed_content.get("prompt_embeds")
if prompt_embeds is not None:
inputs["prompt_embeds"] = prompt_embeds
additional_information = parsed_content.get("additional_information")
if additional_information is not None:
inputs["additional_information"] = additional_information
else:
prompt_token_ids = self._tokenize_prompt(
prompt_text,
tokenization_kwargs=tokenization_kwargs,
)
inputs = token_inputs_omni(
prompt_token_ids,
prompt_embeds=parsed_content.get("prompt_embeds"),
additional_information=parsed_content.get("additional_information"),
)
if cache_salt := parsed_content.get("cache_salt"):
inputs["cache_salt"] = cache_salt
return inputs
def _process_tokens(
self,
parsed_content: TokensPrompt,
Whitespace-only changes.
@@ -0,0 +1,516 @@
# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
#
# 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.
from transformers.configuration_utils import PretrainedConfig, layer_type_validation
from transformers.modeling_rope_utils import rope_config_validation
from transformers.utils import logging
logger = logging.get_logger(__name__)
class Qwen3TTSSpeakerEncoderConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of a [`Qwen3TTSSpeakerEncoder`].
It is used to instantiate a Qwen3TTS speaker encoder model according to the specified arguments, defining the model
architecture. The architecture is based on the ECAPA-TDNN model.
Args:
mel_dim (`int`, *optional*, defaults to 128):
The dimension of the input mel-spectrogram.
enc_dim (`int`, *optional*, defaults to 192):
The dimension of the final speaker embedding.
enc_channels (`list[int]`, *optional*, defaults to `[512, 512, 512, 512, 1536]`):
A list of output channels for each TDNN/SERes2Net layer in the encoder.
The first channel size is for the initial TDNN layer,
the intermediate ones for the `SqueezeExcitationRes2NetBlock` layers,
and the last one for the multi-layer feature aggregation.
enc_kernel_sizes (`list[int]`, *optional*, defaults to `[5, 3, 3, 3, 1]`):
A list of kernel sizes for each layer in the encoder, corresponding to `enc_channels`.
enc_dilations (`list[int]`, *optional*, defaults to `[1, 2, 3, 4, 1]`):
A list of dilations for each layer in the encoder, corresponding to `enc_channels`.
enc_attention_channels (`int`, *optional*, defaults to 128):
The number of attention channels in the `AttentiveStatisticsPooling` layer.
enc_res2net_scale (`int`, *optional*,defaults to 8):
The scale of the `Res2NetBlock` in the encoder.
enc_se_channels (`int`, *optional*, defaults to 128):
The number of channels in the squeeze part of the `SqueezeExcitationBlock`.
"""
def __init__(
self,
mel_dim=128,
enc_dim=1024,
enc_channels=[512, 512, 512, 512, 1536],
enc_kernel_sizes=[5, 3, 3, 3, 1],
enc_dilations=[1, 2, 3, 4, 1],
enc_attention_channels=128,
enc_res2net_scale=8,
enc_se_channels=128,
sample_rate=24000,
):
self.mel_dim = mel_dim
self.enc_dim = enc_dim
self.enc_channels = enc_channels
self.enc_kernel_sizes = enc_kernel_sizes
self.enc_dilations = enc_dilations
self.enc_attention_channels = enc_attention_channels
self.enc_res2net_scale = enc_res2net_scale
self.enc_se_channels = enc_se_channels
self.sample_rate = sample_rate
class Qwen3TTSTalkerCodePredictorConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of a [`Qwen3TTSTalkerCodePredictorModel`].
It is used to instantiate a Qwen3TTSTalkerCodePredictor model according to the specified arguments,
defining the model architecture.
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
vocab_size (`int`, *optional*, defaults to 151936):
Vocabulary size of the Qwen3TTSTalkerCodePredictor model.
Defines the number of different tokens that can be represented by the
`inputs_ids` passed when calling [`Qwen3TTSTalkerCodePredictorModel`]
hidden_size (`int`, *optional*, defaults to 4096):
Dimension of the hidden representations.
intermediate_size (`int`, *optional*, defaults to 22016):
Dimension of the MLP representations.
num_hidden_layers (`int`, *optional*, defaults to 32):
Number of hidden layers in the Transformer encoder.
num_attention_heads (`int`, *optional*, defaults to 32):
Number of attention heads for each attention layer in the Transformer encoder.
num_key_value_heads (`int`, *optional*, defaults to 32):
This is the number of key_value heads that should be used to implement Grouped Query Attention. If
`num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
`num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When
converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
by meanpooling all the original heads within that group. For more details, check out [this
paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`.
head_dim (`int`, *optional*, defaults to 128):
The attention head dimension.
hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
The non-linear activation function (function or string) in the decoder.
max_position_embeddings (`int`, *optional*, defaults to 32768):
The maximum sequence length that this model might ever be used with.
initializer_range (`float`, *optional*, defaults to 0.02):
The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
rms_norm_eps (`float`, *optional*, defaults to 1e-06):
The epsilon used by the rms normalization layers.
use_cache (`bool`, *optional*, defaults to `True`):
Whether or not the model should return the last key/values attentions (not used by all models). Only
relevant if `config.is_decoder=True`.
tie_word_embeddings (`bool`, *optional*, defaults to `False`):
Whether the model's input and output word embeddings should be tied.
rope_theta (`float`, *optional*, defaults to 10000.0):
The base period of the RoPE embeddings.
rope_scaling (`Dict`, *optional*):
Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type
and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value
accordingly.
Expected contents:
`rope_type` (`str`):
The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope',
'llama3'], with 'default' being the original RoPE implementation.
`factor` (`float`, *optional*):
Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In
most scaling types, a `factor` of x will enable the model to handle sequences of length x *
original maximum pre-trained length.
`original_max_position_embeddings` (`int`, *optional*):
Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during
pretraining.
`attention_factor` (`float`, *optional*):
Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention
computation. If unspecified, it defaults to value recommended by the implementation, using the
`factor` field to infer the suggested value.
`beta_fast` (`float`, *optional*):
Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear
ramp function. If unspecified, it defaults to 32.
`beta_slow` (`float`, *optional*):
Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear
ramp function. If unspecified, it defaults to 1.
`short_factor` (`list[float]`, *optional*):
Only used with 'longrope'. The scaling factor to be applied to short contexts (<
`original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
size divided by the number of attention heads divided by 2
`long_factor` (`list[float]`, *optional*):
Only used with 'longrope'. The scaling factor to be applied to long contexts (<
`original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
size divided by the number of attention heads divided by 2
`low_freq_factor` (`float`, *optional*):
Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE
`high_freq_factor` (`float`, *optional*):
Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE
attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
Whether to use a bias in the query, key, value and output projection layers during self-attention.
use_sliding_window (`bool`, *optional*, defaults to `False`):
Whether to use sliding window attention.
sliding_window (`int`, *optional*, defaults to 4096):
Sliding window attention (SWA) window size. If not specified, will default to `4096`.
max_window_layers (`int`, *optional*, defaults to 28):
The number of layers using full attention.
The first `max_window_layers` layers will use full attention, while any
additional layer afterwards will use SWA (Sliding Window Attention).
layer_types (`list`, *optional*):
Attention pattern for each layer.
attention_dropout (`float`, *optional*, defaults to 0.0):
The dropout ratio for the attention probabilities.
"""
model_type = "qwen3_tts_talker_code_predictor"
keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Qwen3TTSTalkerCodePredictor`
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
base_model_pp_plan = {
"embed_tokens": (["input_ids"], ["inputs_embeds"]),
"layers": (["hidden_states", "attention_mask"], ["hidden_states"]),
"norm": (["hidden_states"], ["hidden_states"]),
}
def __init__(
self,
vocab_size=2048,
hidden_size=1024,
intermediate_size=3072,
num_hidden_layers=5,
num_attention_heads=16,
num_key_value_heads=8,
head_dim=128,
hidden_act="silu",
max_position_embeddings=32768,
initializer_range=0.02,
rms_norm_eps=0.000001,
use_cache=True,
tie_word_embeddings=False,
rope_theta=10000,
rope_scaling=None,
attention_bias=False,
use_sliding_window=False,
sliding_window=4096,
max_window_layers=28,
layer_types=None,
attention_dropout=0,
num_code_groups=32,
**kwargs,
):
super().__init__(
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
self.vocab_size = vocab_size
self.max_position_embeddings = max_position_embeddings
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.use_sliding_window = use_sliding_window
self.sliding_window = sliding_window if self.use_sliding_window else None
self.max_window_layers = max_window_layers
# for backward compatibility
if num_key_value_heads is None:
num_key_value_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.head_dim = head_dim
self.hidden_act = hidden_act
self.initializer_range = initializer_range
self.rms_norm_eps = rms_norm_eps
self.use_cache = use_cache
self.rope_theta = rope_theta
self.rope_scaling = rope_scaling
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
# Validate the correctness of rotary position embeddings parameters
# BC: if there is a 'type' field, move it to 'rope_type'.
if self.rope_scaling is not None and "type" in self.rope_scaling:
self.rope_scaling["rope_type"] = self.rope_scaling["type"]
rope_config_validation(self)
self.layer_types = layer_types
if self.layer_types is None:
self.layer_types = [
"sliding_attention"
if self.sliding_window is not None and i >= self.max_window_layers
else "full_attention"
for i in range(self.num_hidden_layers)
]
layer_type_validation(self.layer_types)
self.num_code_groups = num_code_groups
class Qwen3TTSTalkerConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of a [`Qwen3TTSTalkerModel`]. It is used to instantiate a
Qwen3TTSTalker model according to the specified arguments, defining the model architecture.
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
vocab_size (`int`, *optional*, defaults to 151936):
Vocabulary size of the Qwen3TTSTalker model.
Defines the number of different tokens that can be represented by the
`inputs_ids` passed when calling [`Qwen3TTSTalkerModel`]
hidden_size (`int`, *optional*, defaults to 2048):
Dimension of the hidden representations.
intermediate_size (`int`, *optional*, defaults to 6144):
Dimension of the MLP representations.
num_hidden_layers (`int`, *optional*, defaults to 24):
Number of hidden layers in the Transformer encoder.
num_attention_heads (`int`, *optional*, defaults to 32):
Number of attention heads for each attention layer in the Transformer encoder.
num_key_value_heads (`int`, *optional*, defaults to 4):
This is the number of key_value heads that should be used to implement Grouped Query Attention. If
`num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
`num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When
converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
by meanpooling all the original heads within that group. For more details, check out [this
paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`.
hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
The non-linear activation function (function or string) in the decoder.
max_position_embeddings (`int`, *optional*, defaults to 32768):
The maximum sequence length that this model might ever be used with.
initializer_range (`float`, *optional*, defaults to 0.02):
The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
rms_norm_eps (`float`, *optional*, defaults to 1e-06):
The epsilon used by the rms normalization layers.
use_cache (`bool`, *optional*, defaults to `True`):
Whether or not the model should return the last key/values attentions (not used by all models). Only
relevant if `config.is_decoder=True`.
tie_word_embeddings (`bool`, *optional*, defaults to `False`):
Whether the model's input and output word embeddings should be tied.
rope_theta (`float`, *optional*, defaults to 10000.0):
The base period of the RoPE embeddings.
rope_scaling (`Dict`, *optional*):
Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type
and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value
accordingly.
Expected contents:
`rope_type` (`str`):
The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope',
'llama3'], with 'default' being the original RoPE implementation.
`factor` (`float`, *optional*):
Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In
most scaling types, a `factor` of x will enable the model to handle sequences of length x *
original maximum pre-trained length.
`original_max_position_embeddings` (`int`, *optional*):
Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during
pretraining.
`attention_factor` (`float`, *optional*):
Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention
computation. If unspecified, it defaults to value recommended by the implementation, using the
`factor` field to infer the suggested value.
`beta_fast` (`float`, *optional*):
Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear
ramp function. If unspecified, it defaults to 32.
`beta_slow` (`float`, *optional*):
Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear
ramp function. If unspecified, it defaults to 1.
`short_factor` (`list[float]`, *optional*):
Only used with 'longrope'. The scaling factor to be applied to short contexts (<
`original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
size divided by the number of attention heads divided by 2
`long_factor` (`list[float]`, *optional*):
Only used with 'longrope'. The scaling factor to be applied to long contexts (<
`original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
size divided by the number of attention heads divided by 2
`low_freq_factor` (`float`, *optional*):
Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE
`high_freq_factor` (`float`, *optional*):
Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE
attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
Whether to use a bias in the query, key, value and output projection layers during self-attention.
use_sliding_window (`bool`, *optional*, defaults to `False`):
Whether to use sliding window attention.
sliding_window (`int`, *optional*, defaults to 4096):
Sliding window attention (SWA) window size. If not specified, will default to `4096`.
attention_dropout (`float`, *optional*, defaults to 0.0):
The dropout ratio for the attention probabilities.
"""
model_type = "qwen3_tts_talker"
keys_to_ignore_at_inference = ["past_key_values"]
# Default tensor parallel plan for base model `Qwen3TTSTalker`
base_model_tp_plan = {
"layers.*.self_attn.q_proj": "colwise",
"layers.*.self_attn.k_proj": "colwise",
"layers.*.self_attn.v_proj": "colwise",
"layers.*.self_attn.o_proj": "rowwise",
"layers.*.mlp.gate_proj": "colwise",
"layers.*.mlp.up_proj": "colwise",
"layers.*.mlp.down_proj": "rowwise",
}
base_model_pp_plan = {
"embed_tokens": (["input_ids"], ["inputs_embeds"]),
"layers": (["hidden_states", "attention_mask"], ["hidden_states"]),
"norm": (["hidden_states"], ["hidden_states"]),
}
sub_configs = {"code_predictor_config": Qwen3TTSTalkerCodePredictorConfig}
def __init__(
self,
code_predictor_config=None,
vocab_size=3072,
hidden_size=1024,
intermediate_size=2048,
num_hidden_layers=20,
num_attention_heads=16,
num_key_value_heads=2,
hidden_act="silu",
max_position_embeddings=32768,
initializer_range=0.02,
rms_norm_eps=0.000001,
use_cache=True,
tie_word_embeddings=False,
rope_theta=10000,
rope_scaling=None,
attention_bias=False,
use_sliding_window=False,
sliding_window=4096,
attention_dropout=0,
num_code_groups=32,
text_hidden_size=2048,
codec_eos_token_id=4198,
codec_think_id=4202,
codec_nothink_id=4203,
codec_think_bos_id=4204,
codec_think_eos_id=4205,
codec_pad_id=4196,
codec_bos_id=4197,
spk_id=None,
spk_is_dialect=None,
codec_language_id=None,
**kwargs,
):
super().__init__(
tie_word_embeddings=tie_word_embeddings,
**kwargs,
)
self.vocab_size = vocab_size
self.max_position_embeddings = max_position_embeddings
self.hidden_size = hidden_size
self.intermediate_size = intermediate_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.use_sliding_window = use_sliding_window
self.sliding_window = sliding_window if use_sliding_window else None
self.num_key_value_heads = num_key_value_heads
self.hidden_act = hidden_act
self.initializer_range = initializer_range
self.rms_norm_eps = rms_norm_eps
self.use_cache = use_cache
self.rope_theta = rope_theta
self.rope_scaling = rope_scaling
self.attention_bias = attention_bias
self.attention_dropout = attention_dropout
# Validate the correctness of rotary position embeddings parameters
# BC: if there is a 'type' field, move it to 'rope_type'.
if self.rope_scaling is not None and "type" in self.rope_scaling:
self.rope_scaling["rope_type"] = self.rope_scaling["type"]
if code_predictor_config is None:
code_predictor_config = {}
self.code_predictor_config = Qwen3TTSTalkerCodePredictorConfig()
logger.info("code_predictor_config is None. Initializing code_predictor model with default values")
elif isinstance(code_predictor_config, Qwen3TTSTalkerCodePredictorConfig):
self.code_predictor_config = code_predictor_config
else:
self.code_predictor_config = Qwen3TTSTalkerCodePredictorConfig(**code_predictor_config)
self.num_code_groups = num_code_groups
self.text_hidden_size = text_hidden_size
self.codec_eos_token_id = codec_eos_token_id
self.codec_think_id = codec_think_id
self.codec_language_id = codec_language_id
self.codec_nothink_id = codec_nothink_id
self.codec_think_bos_id = codec_think_bos_id
self.codec_think_eos_id = codec_think_eos_id
self.codec_pad_id = codec_pad_id
self.codec_bos_id = codec_bos_id
self.spk_id = spk_id
self.spk_is_dialect = spk_is_dialect
class Qwen3TTSConfig(PretrainedConfig):
"""
This is the configuration class to store the configuration of a [`Qwen3TTSForConditionalGeneration`].
"""
model_type = "qwen3_tts"
sub_configs = {
"talker_config": Qwen3TTSTalkerConfig,
"speaker_encoder_config": Qwen3TTSSpeakerEncoderConfig,
}
def __init__(
self,
talker_config=None,
speaker_encoder_config=None,
tokenizer_type=None,
tts_model_size=None,
tts_model_type=None,
im_start_token_id=151644,
im_end_token_id=151645,
tts_pad_token_id=151671,
tts_bos_token_id=151672,
tts_eos_token_id=151673,
**kwargs,
):
super().__init__(**kwargs)
if talker_config is None:
talker_config = {}
logger.info("talker_config is None. Initializing talker model with default values")
if speaker_encoder_config is None:
speaker_encoder_config = {}
logger.info("speaker_encoder_config is None. Initializing talker model with default values")
self.talker_config = Qwen3TTSTalkerConfig(**talker_config)
self.speaker_encoder_config = Qwen3TTSSpeakerEncoderConfig(**speaker_encoder_config)
self.tokenizer_type = tokenizer_type
self.tts_model_size = tts_model_size
self.tts_model_type = tts_model_type
self.im_start_token_id = im_start_token_id
self.im_end_token_id = im_end_token_id
self.tts_pad_token_id = tts_pad_token_id
self.tts_bos_token_id = tts_bos_token_id
self.tts_eos_token_id = tts_eos_token_id
def get_text_config(self, **kwargs):
# vLLM expects text config to expose hidden_size/num_attention_heads.
# For Qwen3 TTS, the talker config is the text model config.
config = self.talker_config
if hasattr(config, "rope_parameters"):
delattr(config, "rope_parameters")
return config
__all__ = ["Qwen3TTSConfig", "Qwen3TTSTalkerConfig", "Qwen3TTSSpeakerEncoderConfig"]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,102 @@
# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
#
# 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.
from transformers.feature_extraction_utils import BatchFeature
from transformers.processing_utils import ProcessingKwargs, ProcessorMixin
class Qwen3TTSProcessorKwargs(ProcessingKwargs, total=False):
_defaults = {
"text_kwargs": {
"padding": False,
"padding_side": "left",
}
}
class Qwen3TTSProcessor(ProcessorMixin):
r"""
Constructs a Qwen3TTS processor.
Args:
tokenizer ([`Qwen2TokenizerFast`], *optional*):
The text tokenizer.
chat_template (`Optional[str]`, *optional*):
The Jinja template to use for formatting the conversation.
If not provided, the default chat template is used.
"""
attributes = ["tokenizer"]
tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")
def __init__(self, tokenizer=None, chat_template=None):
super().__init__(tokenizer, chat_template=chat_template)
def __call__(self, text=None, **kwargs) -> BatchFeature:
"""
Main method to prepare for the model one or several sequences(s) and audio(s).
This method forwards the `text` and `kwargs` arguments to
Qwen2TokenizerFast's [`~Qwen2TokenizerFast.__call__`] if `text` is not `None`
to encode the text.
Args:
text (`str`, `List[str]`, `List[List[str]]`):
The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
(pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
`is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
"""
if text is None:
raise ValueError("You need to specify either a `text` input to process.")
output_kwargs = self._merge_kwargs(
Qwen3TTSProcessorKwargs,
tokenizer_init_kwargs=self.tokenizer.init_kwargs,
**kwargs,
)
if not isinstance(text, list):
text = [text]
texts_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
return BatchFeature(
data={**texts_inputs},
tensor_type=kwargs.get("return_tensors"),
)
def batch_decode(self, *args, **kwargs):
"""
This method forwards all its arguments to Qwen2TokenizerFast's [`~PreTrainedTokenizer.batch_decode`]. Please
refer to the docstring of this method for more information.
"""
return self.tokenizer.batch_decode(*args, **kwargs)
def decode(self, *args, **kwargs):
"""
This method forwards all its arguments to Qwen2TokenizerFast's [`~PreTrainedTokenizer.decode`]. Please refer to
the docstring of this method for more information.
"""
return self.tokenizer.decode(*args, **kwargs)
def apply_chat_template(self, conversations, chat_template=None, **kwargs):
if isinstance(conversations[0], dict):
conversations = [conversations]
return super().apply_chat_template(conversations, chat_template, **kwargs)
@property
def model_input_names(self):
tokenizer_input_names = self.tokenizer.model_input_names
return list(dict.fromkeys(tokenizer_input_names))
__all__ = ["Qwen3TTSProcessor"]
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,417 @@
# Copyright 2026 The Alibaba Qwen team.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
import base64
import io
import urllib.request
from urllib.parse import urlparse
import librosa
import numpy as np
import soundfile as sf
import torch
from torch.nn.utils.rnn import pad_sequence
from transformers import AutoConfig, AutoFeatureExtractor, AutoModel
from .tokenizer_12hz.configuration_qwen3_tts_tokenizer_v2 import Qwen3TTSTokenizerV2Config
from .tokenizer_12hz.modeling_qwen3_tts_tokenizer_v2 import (
Qwen3TTSTokenizerV2EncoderOutput,
Qwen3TTSTokenizerV2Model,
)
from .tokenizer_25hz.configuration_qwen3_tts_tokenizer_v1 import Qwen3TTSTokenizerV1Config
from .tokenizer_25hz.modeling_qwen3_tts_tokenizer_v1 import (
Qwen3TTSTokenizerV1EncoderOutput,
Qwen3TTSTokenizerV1Model,
)
AudioInput = (
str # wav path, or base64 string
| np.ndarray # 1-D float array
| list[str]
| list[np.ndarray]
)
class Qwen3TTSTokenizer:
"""
A wrapper for Qwen3 TTS Tokenizer 25Hz/12Hz with HuggingFace-style loading.
- from_pretrained(): loads speech tokenizer model via AutoModel and feature_extractor via AutoFeatureExtractor.
- encode(): supports wav path(s), base64 audio string(s), numpy array(s).
- decode(): accepts either the raw model encode output, or a minimal dict/list-of-dicts.
Notes:
- For numpy array input, you must pass `sr` so the audio can be resampled to model sample rate.
- Returned audio is float32 numpy arrays and the output sample rate.
"""
def __init__(self):
self.model = None
self.feature_extractor = None
self.config = None
self.device = None
@classmethod
def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs) -> "Qwen3TTSTokenizer":
"""
Initialize tokenizer with HuggingFace `from_pretrained` style.
Args:
pretrained_model_name_or_path (str):
HuggingFace repo id or local directory.
**kwargs (Any):
Forwarded to `AutoModel.from_pretrained(...)` directly.
Typical examples: device_map="cuda:0", dtype=torch.bfloat16, attn_implementation="eager".
Returns:
Qwen3TTSTokenizer:
Initialized instance with `model`, `feature_extractor`, `config`.
"""
inst = cls()
AutoConfig.register("qwen3_tts_tokenizer_25hz", Qwen3TTSTokenizerV1Config)
AutoModel.register(Qwen3TTSTokenizerV1Config, Qwen3TTSTokenizerV1Model)
AutoConfig.register("qwen3_tts_tokenizer_12hz", Qwen3TTSTokenizerV2Config)
AutoModel.register(Qwen3TTSTokenizerV2Config, Qwen3TTSTokenizerV2Model)
inst.feature_extractor = AutoFeatureExtractor.from_pretrained(pretrained_model_name_or_path)
inst.model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs)
inst.config = inst.model.config
inst.device = getattr(inst.model, "device", None)
if inst.device is None:
# fallback: infer from first parameter device
try:
inst.device = next(inst.model.parameters()).device
except StopIteration:
inst.device = torch.device("cpu")
return inst
def _is_probably_base64(self, s: str) -> bool:
if s.startswith("data:audio"):
return True
# Heuristic: no filesystem path separators and long enough.
if ("/" not in s and "\\" not in s) and len(s) > 256:
return True
return False
def _is_url(self, s: str) -> bool:
try:
u = urlparse(s)
return u.scheme in ("http", "https") and bool(u.netloc)
except Exception:
return False
def _decode_base64_to_wav_bytes(self, b64: str) -> bytes:
# Accept both "data:audio/wav;base64,...." and raw base64
if "," in b64 and b64.strip().startswith("data:"):
b64 = b64.split(",", 1)[1]
return base64.b64decode(b64)
def load_audio(
self,
x: str,
target_sr: int,
) -> np.ndarray:
"""
Load audio from wav path or base64 string, then resample to target_sr.
Args:
x (str):
A wav file path, or a base64 audio string (raw or data URL).
target_sr (int):
Target sampling rate.
Returns:
np.ndarray:
1-D float32 waveform at target_sr.
"""
if self._is_url(x):
with urllib.request.urlopen(x) as resp:
audio_bytes = resp.read()
with io.BytesIO(audio_bytes) as f:
audio, sr = sf.read(f, dtype="float32", always_2d=False)
elif self._is_probably_base64(x):
wav_bytes = self._decode_base64_to_wav_bytes(x)
with io.BytesIO(wav_bytes) as f:
audio, sr = sf.read(f, dtype="float32", always_2d=False)
else:
audio, sr = librosa.load(x, sr=None, mono=True)
if audio.ndim > 1:
audio = np.mean(audio, axis=-1)
if sr != target_sr:
audio = librosa.resample(y=audio, orig_sr=sr, target_sr=target_sr)
return audio.astype(np.float32)
def _normalize_audio_inputs(
self,
audios: AudioInput,
sr: int | None,
) -> list[np.ndarray]:
"""
Normalize all supported input types into a list of 1-D numpy float32 waveforms
at `self.feature_extractor.sampling_rate`.
Args:
audios (AudioInput):
- str: wav path OR base64 audio string
- np.ndarray: raw waveform (sr must be provided)
- list[str] / list[np.ndarray]
sr (Optional[int]):
Sampling rate for raw numpy input. Required if input is np.ndarray or list[np.ndarray].
Returns:
List[np.ndarray]:
List of float32 waveforms resampled to model input SR.
"""
target_sr = int(self.feature_extractor.sampling_rate)
if isinstance(audios, (str, np.ndarray)):
audios = [audios]
if len(audios) == 0:
return []
if isinstance(audios[0], str):
# wav path list or base64 list
return [self.load_audio(x, target_sr=target_sr) for x in audios] # type: ignore[arg-type]
# numpy list
if sr is None:
raise ValueError("For numpy waveform input, you must provide `sr` (original sampling rate).")
out: list[np.ndarray] = []
for a in audios: # type: ignore[assignment]
if not isinstance(a, np.ndarray):
raise TypeError("Mixed input types are not supported. Use all paths/base64 or all numpy arrays.")
if a.ndim > 1:
a = np.mean(a, axis=-1)
if int(sr) != target_sr:
a = librosa.resample(y=a.astype(np.float32), orig_sr=int(sr), target_sr=target_sr)
out.append(a.astype(np.float32))
return out
def encode(
self,
audios: AudioInput,
sr: int | None = None,
return_dict: bool = True,
) -> (
Qwen3TTSTokenizerV1EncoderOutput
| Qwen3TTSTokenizerV2EncoderOutput
| tuple[list[torch.Tensor], list[torch.Tensor] | None, list[torch.Tensor] | None]
| tuple[list[torch.Tensor]]
):
"""
Batch-encode audio into discrete codes (and optional conditioning, depending on 25Hz/12Hz).
Args:
audios (AudioInput):
Supported forms:
- np.ndarray: waveform (requires sr)
- list[np.ndarray]: waveforms (requires sr)
- str: wav path OR base64 audio string
- list[str]: wav paths and/or base64 strings
sr (Optional[int], default=None):
Original sampling rate for numpy waveform input.
return_dict (bool, default=True):
Forwarded to model.encode(...). If True, returns ModelOutput.
Returns:
Qwen3TTSTokenizerV1EncoderOutput | Qwen3TTSTokenizerV2EncoderOutput | tuple:
Encoder output or tuple returned by model.encode. If return_dict=True,
returns a model-specific encoder output. For 25Hz models, this includes
audio_codes/xvectors/ref_mels; for 12Hz models, this includes audio_codes.
If return_dict=False, returns the raw tuple from model.encode.
"""
wavs = self._normalize_audio_inputs(audios, sr=sr)
inputs = self.feature_extractor(
raw_audio=wavs,
sampling_rate=int(self.feature_extractor.sampling_rate),
return_tensors="pt",
)
inputs = inputs.to(self.device).to(self.model.dtype)
with torch.inference_mode():
# model.encode expects (B, T) and (B, T)
enc = self.model.encode(
inputs["input_values"].squeeze(1),
inputs["padding_mask"].squeeze(1),
return_dict=return_dict,
)
return enc
def decode(
self,
encoded,
) -> tuple[list[np.ndarray], int]:
"""
Decode back to waveform.
Usage:
1) Pass the raw output of `encode(...)` directly (recommended).
- 25Hz: expects fields audio_codes, xvectors, ref_mels
- 12Hz: expects field audio_codes
2) Pass a dict or list[dict] (minimal form) for custom pipelines:
- 25Hz dict keys: {"audio_codes", "xvectors", "ref_mels"}
- 12Hz dict keys: {"audio_codes"}
Values can be torch tensors or numpy arrays.
Args:
encoded (Any):
- ModelOutput returned by `encode()`, OR
- dict, OR
- list[dict]
Returns:
Tuple[List[np.ndarray], int]:
- wavs: list of 1-D float32 numpy arrays
- sample_rate: int, model output sampling rate
"""
model_type = self.model.get_model_type()
def _to_tensor(x, dtype=None):
if isinstance(x, torch.Tensor):
return x
x = np.asarray(x)
t = torch.from_numpy(x)
if dtype is not None:
t = t.to(dtype)
return t
# Normalize `encoded` into the same shapes as the official demo uses.
if hasattr(encoded, "audio_codes"):
# ModelOutput from encode()
audio_codes_list = encoded.audio_codes
xvectors_list = getattr(encoded, "xvectors", None)
ref_mels_list = getattr(encoded, "ref_mels", None)
elif isinstance(encoded, dict):
audio_codes_list = encoded["audio_codes"]
xvectors_list = encoded.get("xvectors", None)
ref_mels_list = encoded.get("ref_mels", None)
elif isinstance(encoded, list):
# list of dicts
audio_codes_list = [e["audio_codes"] for e in encoded]
xvectors_list = [e["xvectors"] for e in encoded] if ("xvectors" in encoded[0]) else None
ref_mels_list = [e["ref_mels"] for e in encoded] if ("ref_mels" in encoded[0]) else None
else:
raise TypeError("`encoded` must be an encode output, a dict, or a list of dicts.")
# Ensure list form for per-sample tensors
if isinstance(audio_codes_list, torch.Tensor):
# Could be a single sample tensor or an already padded batch tensor.
t = audio_codes_list
if t.dim() == 1:
# 25Hz single sample: (C,) -> (1, C)
t = t.unsqueeze(0)
elif t.dim() == 2:
# 12Hz single sample: (C, Q) -> (1, C, Q)
t = t.unsqueeze(0)
audio_codes_padded = t.to(self.device)
else:
# List[Tensor/np]
audio_codes_list = [_to_tensor(c, dtype=torch.long) for c in audio_codes_list]
audio_codes_padded = pad_sequence(audio_codes_list, batch_first=True, padding_value=0).to(self.device)
with torch.inference_mode():
if model_type == "qwen3_tts_tokenizer_25hz":
if xvectors_list is None or ref_mels_list is None:
raise ValueError("25Hz decode requires `xvectors` and `ref_mels`.")
if isinstance(xvectors_list, torch.Tensor):
xvectors_batch = xvectors_list
if xvectors_batch.dim() == 1: # (D,) -> (1, D)
xvectors_batch = xvectors_batch.unsqueeze(0)
xvectors_batch = xvectors_batch.to(self.device).to(self.model.dtype)
else:
xvectors_list = [_to_tensor(x, dtype=torch.float32) for x in xvectors_list]
xvectors_batch = torch.stack(xvectors_list, dim=0).to(self.device).to(self.model.dtype)
if isinstance(ref_mels_list, torch.Tensor):
ref_mels_padded = ref_mels_list
if ref_mels_padded.dim() == 2: # (T, M) -> (1, T, M)
ref_mels_padded = ref_mels_padded.unsqueeze(0)
ref_mels_padded = ref_mels_padded.to(self.device).to(self.model.dtype)
else:
ref_mels_list = [_to_tensor(m, dtype=torch.float32) for m in ref_mels_list]
ref_mels_padded = (
pad_sequence(ref_mels_list, batch_first=True, padding_value=0)
.to(self.device)
.to(self.model.dtype)
)
dec = self.model.decode(audio_codes_padded, xvectors_batch, ref_mels_padded, return_dict=True)
wav_tensors = dec.audio_values
elif model_type == "qwen3_tts_tokenizer_12hz":
dec = self.model.decode(audio_codes_padded, return_dict=True)
wav_tensors = dec.audio_values
else:
raise ValueError(f"Unknown model type: {model_type}")
wavs = [w.to(torch.float32).detach().cpu().numpy() for w in wav_tensors]
return wavs, int(self.model.get_output_sample_rate())
def get_model_type(self) -> str:
"""
Get the underlying tokenizer model type.
Returns:
str: Model type string from `self.model.config.model_type`
(e.g. "qwen3_tts_tokenizer_25hz" / "qwen3_tts_tokenizer_12hz").
"""
return self.model.get_model_type()
def get_input_sample_rate(self) -> int:
"""
Get the expected input sample rate for encoding.
Returns:
int: Input sample rate (Hz).
"""
return int(self.model.get_input_sample_rate())
def get_output_sample_rate(self) -> int:
"""
Get the output sample rate for decoded waveforms.
Returns:
int: Output sample rate (Hz).
"""
return int(self.model.get_output_sample_rate())
def get_encode_downsample_rate(self) -> int:
"""
Get the encoder downsample rate (waveform samples per code step).
Returns:
int: Encode downsample rate.
"""
return int(self.model.get_encode_downsample_rate())
def get_decode_upsample_rate(self) -> int:
"""
Get the decoder upsample rate (waveform samples per code step).
Returns:
int: Decode upsample rate.
"""
return int(self.model.get_decode_upsample_rate())
@@ -0,0 +1 @@
# Qwen3 TTS 12Hz tokenizer package.
@@ -0,0 +1,171 @@
# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
#
# 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.
"""Qwen3TTSTokenizerV2 model configuration"""
from transformers import MimiConfig
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import logging
logger = logging.get_logger(__name__)
class Qwen3TTSTokenizerV2DecoderConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of a [`Qwen3TTSTokenizerV2DecoderConfig`].
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
codebook_size (`int`, *optional*, defaults to 2048):
Number of entries in each residual codebook used for acoustic token quantization.
hidden_size (`int`, *optional*, defaults to 1024):
Dimensionality of the hidden states and embeddings in the autoregressive transformer decoder.
max_position_embeddings (`int`, *optional*, defaults to 8000):
Maximum sequence length that the autoregressive decoder can handle. Determines positional embedding size.
rope_theta (`float`, *optional*, defaults to 10000.0):
The base period for rotary position embeddings (RoPE) applied to attention layers.
num_attention_heads (`int`, *optional*, defaults to 16):
Number of attention heads for each attention layer in the decoder.
num_key_value_heads (`int`, *optional*, defaults to 16):
Number of key and value attention heads used in grouped-query attention (if applicable).
attention_bias (`bool`, *optional*, defaults to `False`):
Whether to use bias in the attention projection layers.
sliding_window (`int`, *optional*, defaults to 72):
Window size for local attention mechanism, limiting attention context to improve efficiency.
intermediate_size (`int`, *optional*, defaults to 3072):
Dimensionality of the feed-forward (intermediate) layer in each transformer block.
hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
The non-linear activation function used in the feed-forward layers.
Supports `"silu"`, `"relu"`, `"gelu"`, etc.
layer_scale_initial_scale (`float`, *optional*, defaults to 0.01):
Initial value for LayerScale applied in transformer blocks, helping stabilize training.
rms_norm_eps (`float`, *optional*, defaults to 1e-5):
Epsilon value for RMS normalization layers to prevent division by zero.
num_hidden_layers (`int`, *optional*, defaults to 8):
Number of transformer blocks in the autoregressive decoder.
num_quantizers (`int`, *optional*, defaults to 16):
Number of residual vector quantizers used in the vocoder for fine-grained audio reconstruction.
upsample_rates (`Tuple[int]`, *optional*, defaults to `(8, 5, 4, 3)`):
Rate at which features are upsampled in the final waveform synthesis stage.
upsampling_ratios (`Tuple[int]`, *optional*, defaults to `(2, 2)`):
Ratios used in transposed convolutional layers to progressively upsample feature maps to waveform.
decoder_dim (`int`, *optional*, defaults to 1536):
Final dimensionality of the decoder's output before waveform generation.
attention_dropout (`float`, *optional*, defaults to 0.0):
Dropout probability applied to attention weights in the decoder.
"""
def __init__(
self,
codebook_size=2048,
hidden_size=1024,
latent_dim=1024,
max_position_embeddings=8000,
rope_theta=10000,
num_attention_heads=16,
num_key_value_heads=16,
attention_bias=False,
sliding_window=72,
intermediate_size=3072,
hidden_act="silu",
layer_scale_initial_scale=0.01,
rms_norm_eps=1e-5,
num_hidden_layers=8,
num_quantizers=16,
upsample_rates=(8, 5, 4, 3),
upsampling_ratios=(2, 2),
decoder_dim=1536,
attention_dropout=0.0,
**kwargs,
):
super().__init__(**kwargs)
self.codebook_size = codebook_size
self.hidden_size = hidden_size
self.latent_dim = latent_dim
self.max_position_embeddings = max_position_embeddings
self.rope_theta = rope_theta
self.num_attention_heads = num_attention_heads
self.num_key_value_heads = num_key_value_heads
self.attention_bias = attention_bias
self.sliding_window = sliding_window
self.intermediate_size = intermediate_size
self.hidden_act = hidden_act
self.layer_scale_initial_scale = layer_scale_initial_scale
self.rms_norm_eps = rms_norm_eps
self.num_hidden_layers = num_hidden_layers
self.num_quantizers = num_quantizers
self.upsample_rates = upsample_rates
self.upsampling_ratios = upsampling_ratios
self.decoder_dim = decoder_dim
self.attention_dropout = attention_dropout
@property
def layer_types(self):
"""
All layer in code2wav should be sliding attention
"""
return ["sliding_attention"] * self.num_hidden_layers
class Qwen3TTSTokenizerV2Config(PretrainedConfig):
"""
This is the configuration class to store the configuration of a [`Qwen3TTSTokenizerV2Config`].
It is used to instantiate a Qwen3TTSTokenizerV2Model model according to the specified
sub-models configurations, defining the model architecture.
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
encoder_config (`dict`, *optional*): Configuration of the underlying encoder sub-model.
decoder_config (`dict`, *optional*): Configuration of the underlying decoder sub-model.
"""
model_type = "qwen3_tts_tokenizer_12hz"
sub_configs = {
"encoder_config": MimiConfig,
"decoder_config": Qwen3TTSTokenizerV2DecoderConfig,
}
def __init__(
self,
encoder_config=None,
decoder_config=None,
encoder_valid_num_quantizers=16,
input_sample_rate=24000,
output_sample_rate=24000,
decode_upsample_rate=1920,
encode_downsample_rate=1920,
**kwargs,
):
super().__init__(**kwargs)
if encoder_config is None:
encoder_config = {}
logger.info("encoder_config is None. Initializing encoder with default values")
if decoder_config is None:
decoder_config = {}
logger.info("decoder_config is None. Initializing decoder with default values")
self.encoder_config = MimiConfig(**encoder_config)
self.decoder_config = Qwen3TTSTokenizerV2DecoderConfig(**decoder_config)
self.encoder_valid_num_quantizers = encoder_valid_num_quantizers
self.input_sample_rate = input_sample_rate
self.output_sample_rate = output_sample_rate
self.decode_upsample_rate = decode_upsample_rate
self.encode_downsample_rate = encode_downsample_rate
__all__ = ["Qwen3TTSTokenizerV2Config", "Qwen3TTSTokenizerV2DecoderConfig"]
@@ -0,0 +1 @@
# Qwen3 TTS 25Hz tokenizer package.
@@ -0,0 +1,332 @@
# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
#
# 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.
"""Qwen3TTSTokenizerV1 model configuration"""
from transformers.configuration_utils import PretrainedConfig
from transformers.utils import logging
logger = logging.get_logger(__name__)
class Qwen3TTSTokenizerV1DecoderDiTConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of the Qwen3TTSTokenizerV1DecoderToken2WavDiT.
It defines the architecture of the DiT model, which is used for generating mel-spectrograms from tokens.
Args:
hidden_size (`int`, *optional*, defaults to 1024):
The dimension of the model.
num_hidden_layers (`int`, *optional*, defaults to 22):
The number of transformer blocks in the DiT model.
num_attention_heads (`int`, *optional*, defaults to 16):
The number of attention heads in each transformer block.
ff_mult (`int`, *optional*, defaults to 2):
The multiplier for the feedforward layer in each transformer block.
emb_dim (`int`, *optional*, defaults to 512):
The dimension of the embedding layer.
head_dim (`int`, *optional*, defaults to 64):
The dimension of each attention head.
repeats (`int`, *optional*, defaults to 2):
The number of times the codec embeddings are repeated.
num_embeds (`int`, *optional*, defaults to 8193):
The number of unique embeddings in the codec.
mel_dim (`int`, *optional*, defaults to 80):
The dimension of the mel-spectrogram.
dropout (`float`, *optional*, defaults to 0.1):
The dropout rate for the transformer blocks.
enc_emb_dim (`int`, *optional*, defaults to 192):
The dimension of the pre-trained speaker embedding.
enc_dim (`int`, *optional*, defaults to 128):
The dimension of the encoder output.
enc_channels (`list[int]`, *optional*, defaults to `[256, 256, 256, 256, 768]`):
A list of output channels for each TDNN/SERes2Net layer in the encoder.
enc_kernel_sizes (`list[int]`, *optional*, defaults to `[5, 3, 3, 3, 1]`):
A list of kernel sizes for each layer in the encoder.
enc_dilations (`list[int]`, *optional*, defaults to `[1, 2, 3, 4, 1]`):
A list of dilations for each layer in the encoder.
enc_attention_channels (`int`, *optional*, defaults to 64):
The number of attention channels in the SqueezeExcitationBlock.
enc_res2net_scale (`int`, *optional*, defaults to 2):
The scale of the Res2Net block in the encoder.
enc_se_channels (`int`, *optional*, defaults to 64):
The number of output channels after squeeze in the SqueezeExcitationBlock.
"""
model_type = "qwen3_tts_tokenizer_v1_decoder_dit"
def __init__(
self,
hidden_size=1024,
num_hidden_layers=22,
num_attention_heads=16,
ff_mult=2,
emb_dim=512,
head_dim=64,
rope_theta=10000.0,
max_position_embeddings=32768,
block_size=24,
look_ahead_layers=[10],
look_backward_layers=[0, 20],
repeats=2,
num_embeds=8193,
mel_dim=80,
dropout=0.1,
enc_emb_dim=192,
enc_dim=128,
enc_channels=[256, 256, 256, 256, 768],
enc_kernel_sizes=[5, 3, 3, 3, 1],
enc_dilations=[1, 2, 3, 4, 1],
enc_attention_channels=64,
enc_res2net_scale=2,
enc_se_channels=64,
**kwargs,
):
self.hidden_size = hidden_size
self.num_hidden_layers = num_hidden_layers
self.num_attention_heads = num_attention_heads
self.ff_mult = ff_mult
self.emb_dim = emb_dim
self.head_dim = head_dim
self.rope_theta = rope_theta
self.max_position_embeddings = max_position_embeddings
self.block_size = block_size
self.look_ahead_layers = look_ahead_layers
self.look_backward_layers = look_backward_layers
self.repeats = repeats
self.num_embeds = num_embeds
self.mel_dim = mel_dim
self.dropout = dropout
self.enc_emb_dim = enc_emb_dim
self.enc_dim = enc_dim
self.enc_channels = enc_channels
self.enc_kernel_sizes = enc_kernel_sizes
self.enc_dilations = enc_dilations
self.enc_attention_channels = enc_attention_channels
self.enc_res2net_scale = enc_res2net_scale
self.enc_se_channels = enc_se_channels
super().__init__(**kwargs)
class Qwen3TTSTokenizerV1DecoderBigVGANConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of the Qwen3TTSTokenizerV1DecoderToken2WavBigVGAN module.
It defines the architecture of the BigVGAN model, which is used for converting mel-spectrograms to waveforms.
Args:
mel_dim (`int`, *optional*, defaults to 80):
The dimension of the mel-spectrogram.
upsample_initial_channel (`int`, *optional*, defaults to 1536):
The number of channels in the initial upsampling layer.
resblock_kernel_sizes (`list[int]`, *optional*, defaults to `[3, 7, 11]`):
A list of kernel sizes for each residual block.
resblock_dilation_sizes (`list[list[int]]`, *optional*, defaults to `[[1, 3, 5], [1, 3, 5], [1, 3, 5]]`):
A list of dilation sizes for each residual block.
upsample_rates (`list[int]`, *optional*, defaults to `[5, 3, 2, 2, 2, 2]`):
A list of upsampling rates for each upsampling layer.
upsample_kernel_sizes (`list[int]`, *optional*, defaults to `[11, 7, 4, 4, 4, 4]`):
A list of kernel sizes for each upsampling layer.
"""
model_type = "qwen3_tts_tokenizer_v1_decoder_bigvgan"
def __init__(
self,
mel_dim=80,
upsample_initial_channel=1536,
resblock_kernel_sizes=[3, 7, 11],
resblock_dilation_sizes=[[1, 3, 5], [1, 3, 5], [1, 3, 5]],
upsample_rates=[5, 3, 2, 2, 2, 2],
upsample_kernel_sizes=[11, 7, 4, 4, 4, 4],
**kwargs,
):
self.mel_dim = mel_dim
self.upsample_initial_channel = upsample_initial_channel
self.resblock_kernel_sizes = resblock_kernel_sizes
self.resblock_dilation_sizes = resblock_dilation_sizes
self.upsample_rates = upsample_rates
self.upsample_kernel_sizes = upsample_kernel_sizes
super().__init__(**kwargs)
class Qwen3TTSTokenizerV1DecoderConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of a [`Qwen3TTSTokenizerV1DecoderConfig`].
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
dit_config ([`DiT_Args`], *optional*):
Configuration class for the Diffusion Transformer (DiT) module responsible for generating mel-spectrograms.
bigvgan_config ([`BigVGAN_Args`], *optional*):
Configuration class for the BigVGAN module responsible for converting mel-spectrograms to waveforms.
"""
model_type = "qwen3_tts_tokenizer_v1_decoder"
sub_configs = {
"dit_config": Qwen3TTSTokenizerV1DecoderDiTConfig,
"bigvgan_config": Qwen3TTSTokenizerV1DecoderBigVGANConfig,
}
def __init__(self, dit_config=None, bigvgan_config=None, **kwargs):
if dit_config is None:
dit_config = {}
if bigvgan_config is None:
bigvgan_config = {}
self.dit_config = Qwen3TTSTokenizerV1DecoderDiTConfig(**dit_config)
self.bigvgan_config = Qwen3TTSTokenizerV1DecoderBigVGANConfig(**bigvgan_config)
super().__init__(**kwargs)
class Qwen3TTSTokenizerV1EncoderConfig(PretrainedConfig):
r"""
This is the configuration class to store the configuration of the Qwen3TTSTokenizerV1 Encoder.
The encoder typically takes mel-spectrogram features and produces high-level
audio representations, then (optionally) applies an Audio-VQ module (e.g., GRVQ)
to discretize continuous representations into codes.
Args:
n_mels (`int`, *optional*, defaults to 128):
Number of mel bins in the input mel-spectrogram.
n_ctx (`int`, *optional*, defaults to 1500):
Maximum input sequence length (in frames/tokens) for the encoder.
n_state (`int`, *optional*, defaults to 1280):
Hidden size (model dimension) of the encoder transformer.
n_head (`int`, *optional*, defaults to 20):
Number of attention heads in each transformer layer.
n_layer (`int`, *optional*, defaults to 32):
Number of transformer layers.
n_window (`int`, *optional*, defaults to 100):
Window size used by the model for local attention / chunking (implementation-dependent).
output_dim (`int`, *optional*, defaults to 3584):
Output feature dimension produced by the encoder head (before/after projection, implementation-dependent).
grad_checkpointing (`bool`, *optional*, defaults to `False`):
Whether to enable gradient checkpointing to reduce memory usage during training.
enable_mp (`bool`, *optional*, defaults to `False`):
Whether to enable model parallel features (implementation-dependent).
audio_sequence_parallel (`bool`, *optional*, defaults to `False`):
Whether to enable sequence parallelism for audio branch (implementation-dependent).
audio_vq_type (`str`, *optional*, defaults to `"GRVQ"`):
Type of audio vector-quantization module. Common choices: `"GRVQ"`, `"RVQ"`, etc.
audio_vq_layers (`int`, *optional*, defaults to 6):
Number of VQ layers / quantizers (e.g., number of residual quantizers for RVQ/GRVQ-like designs).
audio_vq_codebook_size (`int`, *optional*, defaults to 32768):
Size of each codebook (number of entries).
audio_vq_codebook_dim (`int`, *optional*, defaults to 1280):
Dimension of codebook vectors (often equals encoder hidden size).
audio_vq_pe (`bool`, *optional*, defaults to `True`):
Whether to use positional encoding (or position embeddings) inside the VQ module.
audio_vq_ds_rate (`int`, *optional*, defaults to 2):
Downsampling rate applied before VQ (e.g., temporal downsample factor).
"""
model_type = "qwen3_tts_tokenizer_v1_encoder"
def __init__(
self,
n_mels=128,
n_ctx=1500,
n_state=1280,
n_head=20,
n_layer=32,
n_window=100,
output_dim=3584,
grad_checkpointing=False,
enable_mp=False,
audio_sequence_parallel=False,
audio_vq_type="GRVQ",
audio_vq_layers=6,
audio_vq_codebook_size=32768,
audio_vq_codebook_dim=1280,
audio_vq_pe=True,
audio_vq_ds_rate=2,
**kwargs,
):
super().__init__(**kwargs)
self.n_mels = n_mels
self.n_ctx = n_ctx
self.n_state = n_state
self.n_head = n_head
self.n_layer = n_layer
self.n_window = n_window
self.output_dim = output_dim
self.grad_checkpointing = grad_checkpointing
self.enable_mp = enable_mp
self.audio_sequence_parallel = audio_sequence_parallel
self.audio_vq_type = audio_vq_type
self.audio_vq_layers = audio_vq_layers
self.audio_vq_codebook_size = audio_vq_codebook_size
self.audio_vq_codebook_dim = audio_vq_codebook_dim
self.audio_vq_pe = audio_vq_pe
self.audio_vq_ds_rate = audio_vq_ds_rate
class Qwen3TTSTokenizerV1Config(PretrainedConfig):
"""
This is the configuration class to store the configuration of a [`Qwen3TTSTokenizerV1Config`].
It is used to instantiate a Qwen3TTSTokenizerV1Model model according to the specified
sub-models configurations, defining the model architecture.
Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
documentation from [`PretrainedConfig`] for more information.
Args:
encoder_config (`dict`, *optional*): Configuration of the underlying encoder sub-model.
decoder_config (`dict`, *optional*): Configuration of the underlying decoder sub-model.
"""
model_type = "qwen3_tts_tokenizer_25hz"
sub_configs = {
"encoder_config": Qwen3TTSTokenizerV1EncoderConfig,
"decoder_config": Qwen3TTSTokenizerV1DecoderConfig,
}
def __init__(
self,
encoder_config=None,
decoder_config=None,
input_sample_rate=24000,
output_sample_rate=24000,
decode_upsample_rate=1920,
encode_downsample_rate=1920,
**kwargs,
):
super().__init__(**kwargs)
if encoder_config is None:
encoder_config = {}
logger.info("encoder_config is None. Initializing encoder with default values")
if decoder_config is None:
decoder_config = {}
logger.info("decoder_config is None. Initializing decoder with default values")
self.encoder_config = Qwen3TTSTokenizerV1EncoderConfig(**encoder_config)
self.decoder_config = Qwen3TTSTokenizerV1DecoderConfig(**decoder_config)
self.input_sample_rate = input_sample_rate
self.output_sample_rate = output_sample_rate
self.decode_upsample_rate = decode_upsample_rate
self.encode_downsample_rate = encode_downsample_rate
__all__ = [
"Qwen3TTSTokenizerV1Config",
"Qwen3TTSTokenizerV1EncoderConfig",
"Qwen3TTSTokenizerV1DecoderConfig",
"Qwen3TTSTokenizerV1DecoderBigVGANConfig",
"Qwen3TTSTokenizerV1DecoderDiTConfig",
]
@@ -0,0 +1 @@
# Qwen3 TTS 25Hz vector-quantization package.
@@ -0,0 +1,515 @@
# 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.
#
# This implementation is inspired from
# https://github.com/lucidrains/vector-quantize-pytorch
# which is released under MIT License. Hereafter, the original license:
# MIT License
#
# Copyright (c) 2020 Phil Wang
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""Core vector quantization implementation."""
import random
import typing as tp
from math import ceil
import numpy as np
import torch
import torch.nn.functional as F
from einops import rearrange, repeat
from torch import nn
def round_up_multiple(num, mult):
return ceil(num / mult) * mult
def default(val: tp.Any, d: tp.Any) -> tp.Any:
return val if val is not None else d
def ema_inplace(moving_avg, new, decay: float):
moving_avg.data.mul_(decay).add_(new, alpha=(1 - decay))
def laplace_smoothing(x, n_categories: int, epsilon: float = 1e-5):
return (x + epsilon) / (x.sum() + n_categories * epsilon)
def uniform_init(*shape: int):
t = torch.empty(shape)
nn.init.kaiming_uniform_(t)
return t
def sample_vectors(samples, num: int):
num_samples, device = samples.shape[0], samples.device
if num_samples >= num:
indices = torch.randperm(num_samples, device=device)[:num]
else:
indices = torch.randint(0, num_samples, (num,), device=device)
return samples[indices]
@torch.no_grad()
def kmeans(samples, num_clusters: int, num_iters: int = 10):
dim, dtype = samples.shape[-1], samples.dtype
means = sample_vectors(samples, num_clusters)
for _ in range(num_iters):
dists = -(
samples.pow(2).sum(1, keepdim=True)
- 2 * torch.matmul(samples, means.t())
+ means.t().pow(2).sum(0, keepdim=True)
)
buckets = dists.max(dim=-1).indices
del dists
bins = torch.bincount(buckets, minlength=num_clusters)
zero_mask = bins == 0
bins_min_clamped = bins.masked_fill(zero_mask, 1)
new_means = buckets.new_zeros(num_clusters, dim, dtype=dtype)
new_means.scatter_add_(0, repeat(buckets, "n -> n d", d=dim), samples)
new_means = new_means / bins_min_clamped[..., None]
means = torch.where(zero_mask[..., None], means, new_means)
return means, bins
def preprocess(x):
x = rearrange(x, "... d -> (...) d")
return x
def postprocess_emb(embed_ind, shape):
return embed_ind.view(*shape[:-1])
class EuclideanCodebook(nn.Module):
"""Codebook with Euclidean distance.
Args:
dim (int): Dimension.
codebook_size (int): Codebook size.
kmeans_init (bool): Whether to use k-means to initialize the codebooks.
If set to true, run the k-means algorithm on the first training batch and use
the learned centroids as initialization.
kmeans_iters (int): Number of iterations used for k-means algorithm at initialization.
decay (float): Decay for exponential moving average over the codebooks.
epsilon (float): Epsilon value for numerical stability.
threshold_ema_dead_code (int): Threshold for dead code expiration. Replace any codes
that have an exponential moving average cluster size less than the specified threshold with
randomly selected vector from the current batch.
"""
def __init__(
self,
dim: int,
codebook_size: int,
kmeans_init: int = False,
kmeans_iters: int = 10,
decay: float = 0.99,
epsilon: float = 1e-5,
threshold_ema_dead_code: float = 2.0,
):
super().__init__()
self.decay = decay
self.codebook_size = codebook_size
self.kmeans_iters = kmeans_iters
self.epsilon = epsilon
self.threshold_ema_dead_code = threshold_ema_dead_code
self.inited = None
self.cluster_size = None
self.embed = None
self.embed_avg = None
self.training = True
def init_embed_(self, data):
if self.inited:
return
embed, cluster_size = kmeans(data, self.codebook_size, self.kmeans_iters)
self.embed.data.copy_(embed)
self.embed_avg.data.copy_(embed.clone())
self.cluster_size.data.copy_(cluster_size)
self.inited.data.copy_(torch.Tensor([True]))
# Make sure all buffers across workers are in sync after initialization
# distrib.broadcast_tensors([self.embed, self.embed_avg, self.cluster_size, self.inited])
def replace_(self, samples, mask):
modified_codebook = torch.where(mask[..., None], sample_vectors(samples, self.codebook_size), self.embed)
self.embed.data.copy_(modified_codebook)
def expire_codes_(self, batch_samples):
if self.threshold_ema_dead_code == 0:
return
cluster_size = self.cluster_size / sum(self.cluster_size) * self.codebook_size
expired_codes = cluster_size < self.threshold_ema_dead_code
if not torch.any(expired_codes):
return
else:
print(f"VQ expire infos: num_expire={sum(expired_codes)}, cluster_size[:5]={cluster_size[:5]}")
batch_samples = rearrange(batch_samples, "... d -> (...) d")
self.replace_(batch_samples, mask=expired_codes)
# sync buffers outside for efficiency
# distrib.broadcast_tensors(self.buffers())
def quantize(self, x):
embed = self.embed.t()
dist = -(x.pow(2).sum(1, keepdim=True) - 2 * x @ embed + embed.pow(2).sum(0, keepdim=True))
embed_ind = dist.max(dim=-1).indices
return embed_ind
def dequantize(self, embed_ind):
quantize = F.embedding(embed_ind, self.embed)
return quantize
def encode(self, x, buffers):
self.inited, self.cluster_size, self.embed, self.embed_avg = buffers
shape = x.shape
# pre-process
x = preprocess(x)
# quantize
embed_ind = self.quantize(x)
# post-process
embed_ind = postprocess_emb(embed_ind, shape)
return embed_ind
def decode(self, embed_ind, buffers):
self.inited, self.cluster_size, self.embed, self.embed_avg = buffers
quantize = self.dequantize(embed_ind)
return quantize
def forward(self, x, buffers):
self.inited, self.cluster_size, self.embed, self.embed_avg = buffers
shape, dtype = x.shape, x.dtype
x = preprocess(x)
self.init_embed_(x)
if self.training:
# We do the expiry of code at that point as buffers are in sync
# and all the workers will take the same decision.
self.expire_codes_(x)
embed_ind = self.quantize(x)
embed_onehot = F.one_hot(embed_ind, self.codebook_size).type(dtype)
embed_ind = postprocess_emb(embed_ind, shape)
quantize = self.dequantize(embed_ind)
if self.training:
ema_inplace(self.cluster_size, embed_onehot.sum(0), self.decay)
embed_sum = x.t() @ embed_onehot
ema_inplace(self.embed_avg, embed_sum.t(), self.decay)
cluster_size = (
laplace_smoothing(self.cluster_size, self.codebook_size, self.epsilon) * self.cluster_size.sum()
)
embed_normalized = self.embed_avg / cluster_size.unsqueeze(1)
self.embed.data.copy_(embed_normalized)
# Note: after ema update, there is a very small difference between codebooks on GPUs.
# The impact can be very small, ignore it.
return quantize, embed_ind
class VectorQuantization(nn.Module):
"""Vector quantization implementation.
Currently, supports only euclidean distance.
Args:
dim (int): Dimension
codebook_size (int): Codebook size
codebook_dim (int): Codebook dimension. If not defined, uses the specified dimension in dim.
decay (float): Decay for exponential moving average over the codebooks.
epsilon (float): Epsilon value for numerical stability.
kmeans_init (bool): Whether to use kmeans to initialize the codebooks.
kmeans_iters (int): Number of iterations used for kmeans initialization.
threshold_ema_dead_code (int): Threshold for dead code expiration. Replace any codes
that have an exponential moving average cluster size less than the specified threshold with
randomly selected vector from the current batch.
commitment_weight (float): Weight for commitment loss.
"""
def __init__(
self,
dim: int,
codebook_size: int,
codebook_dim: int | None = None,
decay: float = 0.99,
epsilon: float = 1e-5,
kmeans_init: bool = True,
kmeans_iters: int = 50,
threshold_ema_dead_code: float = 2.0,
commitment_weight: float = 1.0,
):
super().__init__()
_codebook_dim: int = default(codebook_dim, dim)
requires_projection = _codebook_dim != dim
self.project_in = (nn.Linear(dim, _codebook_dim)) if requires_projection else (nn.Identity())
self.project_out = (nn.Linear(_codebook_dim, dim)) if requires_projection else (nn.Identity())
self.epsilon = epsilon
self.commitment_weight = commitment_weight
self._codebook = EuclideanCodebook(
dim=_codebook_dim,
codebook_size=codebook_size,
kmeans_init=kmeans_init,
kmeans_iters=kmeans_iters,
decay=decay,
epsilon=epsilon,
threshold_ema_dead_code=threshold_ema_dead_code,
)
self.codebook_size = codebook_size
self.training = True
@property
def codebook(self):
return self._codebook.embed
def encode(self, x, buffers):
# x = rearrange(x, "b d n -> b n d")
x = self.project_in(x)
embed_in = self._codebook.encode(x, buffers)
return embed_in
def decode(self, embed_ind, buffers):
quantize = self._codebook.decode(embed_ind, buffers)
quantize = self.project_out(quantize)
# quantize = rearrange(quantize, "b n d -> b d n")
return quantize
def forward(self, x, buffers):
device = x.device
# x = rearrange(x, "b d n -> b n d")
x = self.project_in(x)
quantize, embed_ind = self._codebook(x, buffers)
if self.training:
quantize = x + (quantize - x).detach()
loss = torch.tensor([0.0], device=device, requires_grad=self.training)
if self.training:
if self.commitment_weight > 0:
commit_loss = F.mse_loss(quantize.detach(), x)
loss = loss + commit_loss * self.commitment_weight
quantize = self.project_out(quantize)
# quantize = rearrange(quantize, "b n d -> b d n")
return quantize, embed_ind, loss
class DistributedResidualVectorQuantization(nn.Module):
"""Efficient distributed residual vector quantization implementation.
Follows Algorithm 1. in https://arxiv.org/pdf/2107.03312.pdf
"""
def __init__(self, *, num_quantizers, quantize_dropout: bool = False, rand_num_quant: list | None = None, **kwargs):
super().__init__()
"""
dim: int,
codebook_size: int,
codebook_dim: tp.Optional[int] = None,
"""
codebook_size, codebook_dim = (
kwargs["codebook_size"],
kwargs["codebook_dim"] if kwargs["codebook_dim"] else kwargs["dim"],
)
kmeans_init = kwargs["kmeans_init"]
if isinstance(kmeans_init, bool):
if not kwargs["kmeans_init"]:
# use uniform init
embed = uniform_init(num_quantizers, codebook_size, codebook_dim)
inited = True
else:
# to perform kmeans init on first batch
embed = torch.zeros(num_quantizers, codebook_size, codebook_dim)
inited = False
elif isinstance(kmeans_init, str):
# use prepared kmeans init
embed = np.load(kmeans_init)
embed = torch.from_numpy(embed)
if embed.dim() == 2:
embed = embed.unsqueeze(0)
inited = True
else:
raise TypeError("kmeans_init should be either a bool or string path to init weights.")
self.register_buffer("inited", torch.Tensor([[inited] for _ in range(num_quantizers)]))
self.register_buffer("cluster_size", torch.zeros(num_quantizers, codebook_size))
self.register_buffer("embed", embed)
self.register_buffer("embed_avg", embed.clone())
self.q0_ds_ratio = 1
if "q0_ds_ratio" in kwargs:
self.q0_ds_ratio = kwargs.pop("q0_ds_ratio")
self.layers = nn.ModuleList()
for i in range(num_quantizers):
vq_args = dict(**kwargs)
vq = VectorQuantization(**vq_args)
self.layers.append(vq)
self.quantize_dropout = quantize_dropout
self.rand_num_quant = rand_num_quant
def forward(self, x, n_q: int | None = None):
quantized_out = torch.zeros_like(x)
residual = x
bb, cc, tt = x.shape
device = x.device
all_losses = []
all_indices = []
all_sub_quants = []
n_q = n_q or len(self.layers)
should_quantize_dropout = self.training and self.quantize_dropout and self.rand_num_quant is not None
if should_quantize_dropout:
rand_quantize_dropout_index = random.choice(self.rand_num_quant)
null_indices_shape = (x.shape[0], x.shape[2])
null_indices = torch.full(null_indices_shape, -1.0, device=device, dtype=torch.long)
null_loss = torch.full((1,), 0.0, device=device, dtype=x.dtype)
null_sub_quant = torch.full(x.shape, -1, device=device, dtype=x.dtype)
for quantizer_index, layer in enumerate(self.layers[:n_q]):
# dropout except the first quantizer
if should_quantize_dropout and quantizer_index >= rand_quantize_dropout_index:
all_indices.append(null_indices)
all_losses.append(null_loss)
all_sub_quants.append(null_sub_quant)
continue
quant_in = residual
if self.q0_ds_ratio > 1 and quantizer_index == 0:
quant_in = F.interpolate(quant_in, size=[tt // 2])
quantized, indices, loss = layer(
quant_in,
[
self.inited[quantizer_index],
self.cluster_size[quantizer_index],
self.embed[quantizer_index],
self.embed_avg[quantizer_index],
],
)
if self.q0_ds_ratio > 1 and quantizer_index == 0:
quantized = F.interpolate(quantized, size=[tt])
indices = F.interpolate(indices.unsqueeze(1).float(), size=[tt]).squeeze(1).long()
residual = residual - quantized
quantized_out = quantized_out + quantized
all_indices.append(indices)
all_losses.append(loss)
all_sub_quants.append(quantized)
# sync buffers after one forward step
# distrib.broadcast_tensors(self.buffers())
out_losses, out_indices, out_sub_quants = map(torch.stack, (all_losses, all_indices, all_sub_quants))
return quantized_out, out_indices, out_losses
def encode(self, x: torch.Tensor, n_q: int | None = None) -> torch.Tensor:
residual = x
all_indices = []
n_q = n_q or len(self.layers)
for i, layer in enumerate(self.layers[:n_q]):
indices = layer.encode(residual, [self.inited[i], self.cluster_size[i], self.embed[i], self.embed_avg[i]])
quantized = layer.decode(indices, [self.inited[i], self.cluster_size[i], self.embed[i], self.embed_avg[i]])
residual = residual - quantized
all_indices.append(indices)
out_indices = torch.stack(all_indices)
return out_indices
def decode(self, q_indices: torch.Tensor) -> torch.Tensor:
quantized_out = torch.tensor(0.0, device=q_indices.device)
for i, indices in enumerate(q_indices):
layer = self.layers[i]
quantized = layer.decode(indices, [self.inited[i], self.cluster_size[i], self.embed[i], self.embed_avg[i]])
quantized_out = quantized_out + quantized
return quantized_out
class DistributedGroupResidualVectorQuantization(nn.Module):
"""Efficient distributed group residual vector quantization implementation.
Follows Algorithm 1. in https://arxiv.org/abs/2305.02765
Group Then rvq
"""
def __init__(
self,
*,
num_groups,
num_quantizers,
quantize_dropout: bool = False,
rand_num_quant: list | None = None,
**kwargs,
):
super().__init__()
self.rvqs = nn.ModuleList(
[
DistributedResidualVectorQuantization(
num_quantizers=num_quantizers,
quantize_dropout=quantize_dropout,
rand_num_quant=rand_num_quant,
**kwargs,
)
for _ in range(num_groups)
]
)
self.num_groups = num_groups
def forward(self, x, n_q: int | None = None):
x_lst = torch.chunk(x, chunks=self.num_groups, dim=1)
all_quantized_out = []
all_indices = []
all_losses = []
for mod, item in zip(self.rvqs, x_lst):
quantized_out, out_indices, out_losses = mod(item, n_q)
all_quantized_out.append(quantized_out)
all_indices.append(out_indices)
all_losses.append(out_losses)
out_losses = torch.stack(all_losses, dim=1).mean(dim=1)
return torch.cat(all_quantized_out, dim=1), torch.stack(all_indices, dim=1), out_losses
def encode(self, x: torch.Tensor, n_q: int | None = None) -> torch.Tensor:
x_lst = torch.chunk(x, chunks=self.num_groups, dim=1)
return torch.stack([mod.encode(item, n_q) for mod, item in zip(self.rvqs, x_lst)], dim=1)
def decode(self, q_indices: torch.Tensor) -> torch.Tensor:
q_indices_lst = torch.chunk(q_indices, chunks=self.num_groups, dim=1)
return torch.cat([mod.decode(item.squeeze(1)) for mod, item in zip(self.rvqs, q_indices_lst)], dim=1)
@@ -0,0 +1,403 @@
# Copyright 2026 The Alibaba Qwen team.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
import copy
import operator
from itertools import accumulate
import onnxruntime
import sox
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchaudio.compliance.kaldi as kaldi
from librosa.filters import mel as librosa_mel_fn
from torch import Tensor
from .core_vq import DistributedGroupResidualVectorQuantization
from .whisper_encoder import Conv1d, ConvTranspose1d, WhisperEncoder
def dynamic_range_compression_torch(x, c=1, clip_val=1e-5):
return torch.log(torch.clamp(x, min=clip_val) * c)
def spectral_normalize_torch(magnitudes):
output = dynamic_range_compression_torch(magnitudes)
return output
class MelSpectrogramFeatures(nn.Module):
"""
Calculate the BigVGAN style mel spectrogram of an input signal.
Args:
filter_length (int): The number of samples in the filter window,
used for the Fourier Transform. Default is 1024.
hop_length (int): The number of samples between successive frames
(stride of the STFT). Default is 160.
win_length (int): The length of the window function applied to each frame,
usually less than or equal to the filter length. Default is 640.
n_mel_channels (int): The number of Mel-frequency channels to output
from the Mel-scale spectrogram. Default is 80.
mel_fmin (int): The minimum frequency (in Hz) of the Mel-scale spectrogram.
Default is 0.
mel_fmax (int): The maximum frequency (in Hz) of the Mel-scale spectrogram.
Default is 8000.
sampling_rate (int): The sampling rate of the audio data (in Hz).
Default is 16000.
sampling_rate_org (int, optional): The original sampling rate of the audio
data before any resampling (in Hz), if applicable. Default is None.
padding (str): The padding mode for the input signal. 'center' pads the signal
symmetrically around its center. Default is 'center'.
"""
def __init__(
self,
filter_length=1024,
hop_length=160,
win_length=640,
n_mel_channels=80,
mel_fmin=0,
mel_fmax=8000,
sampling_rate=16000,
sampling_rate_org=None,
padding="center",
use_db=False,
):
super().__init__()
if padding not in ["center", "same"]:
raise ValueError("Padding must be 'center' or 'same'.")
self.padding = padding
self.filter_length = filter_length
self.hop_length = hop_length
self.win_length = win_length
self.n_mel_channels = n_mel_channels
self.mel_fmin = mel_fmin
self.mel_fmax = mel_fmax
self.sampling_rate = sampling_rate
self.sampling_rate_org = sampling_rate_org if sampling_rate_org is not None else sampling_rate
self.mel_basis = {}
self.hann_window = {}
def forward(self, audio: torch.Tensor, **kwargs) -> torch.Tensor:
with torch.no_grad():
feats = self.extract(audio, **kwargs)
return feats
def extract(self, audio, **kwargs):
if len(audio.shape) == 3:
audio = audio.squeeze(1) if audio.shape[1] == 1 else audio.squeeze(2)
assert len(audio.shape) == 2
y = audio
if len(list(self.mel_basis.keys())) == 0:
mel = librosa_mel_fn(
sr=self.sampling_rate,
n_fft=self.filter_length,
n_mels=self.n_mel_channels,
fmin=self.mel_fmin,
fmax=self.mel_fmax,
)
self.mel_basis[str(self.mel_fmax) + "_" + str(y.device)] = torch.from_numpy(mel).float().to(y.device)
self.hann_window[str(y.device)] = torch.hann_window(self.win_length).to(y.device)
y = torch.nn.functional.pad(
y.unsqueeze(1),
(int((self.filter_length - self.hop_length) / 2), int((self.filter_length - self.hop_length) / 2)),
mode="reflect",
)
y = y.squeeze(1)
spec = torch.stft(
y,
self.filter_length,
hop_length=self.hop_length,
win_length=self.win_length,
window=self.hann_window[str(y.device)],
center=False,
pad_mode="reflect",
normalized=False,
onesided=True,
return_complex=True,
)
spec = torch.view_as_real(spec)
spec = torch.sqrt(spec.pow(2).sum(-1) + (1e-9))
spec = torch.matmul(self.mel_basis[str(self.mel_fmax) + "_" + str(y.device)], spec)
spec = spectral_normalize_torch(spec)
return spec
class XVectorExtractor(nn.Module):
def __init__(self, audio_codec_with_xvector):
super().__init__()
option = onnxruntime.SessionOptions()
option.graph_optimization_level = onnxruntime.GraphOptimizationLevel.ORT_ENABLE_ALL
option.intra_op_num_threads = 1
providers = ["CPUExecutionProvider"]
self.ort_session = onnxruntime.InferenceSession(
audio_codec_with_xvector, sess_options=option, providers=providers
)
self.tfm = sox.Transformer()
self.tfm.norm(db_level=-6)
self.mel_ext = MelSpectrogramFeatures(
filter_length=1024,
hop_length=160,
win_length=640,
n_mel_channels=80,
mel_fmin=0,
mel_fmax=8000,
sampling_rate=16000,
)
def extract_code(self, audio):
with torch.no_grad():
norm_audio = self.sox_norm(audio)
norm_audio = torch.from_numpy(copy.deepcopy(norm_audio)).unsqueeze(0)
feat = kaldi.fbank(norm_audio, num_mel_bins=80, dither=0, sample_frequency=16000)
feat = feat - feat.mean(dim=0, keepdim=True)
norm_embedding = self.ort_session.run(
None, {self.ort_session.get_inputs()[0].name: feat.unsqueeze(dim=0).cpu().numpy()}
)[0].flatten()
norm_embedding = F.normalize(torch.from_numpy(norm_embedding), dim=0)
ref_mel = self.mel_ext.extract(audio=norm_audio)
return norm_embedding.numpy(), ref_mel.permute(0, 2, 1).squeeze(0).numpy()
def sox_norm(self, audio):
wav_norm = self.tfm.build_array(input_array=audio, sample_rate_in=16000)
return wav_norm
class WhisperEncoderVQ(WhisperEncoder):
def __init__(
self,
n_mels: int,
n_ctx: int,
n_state: int,
n_head: int,
n_layer: int,
n_window: int = 1500,
output_dim: int = 512,
grad_checkpointing: bool = False,
enable_mp: bool = False,
audio_sequence_parallel: bool = False,
audio_vq_layers: int = -1,
audio_vq_type: str = "NULL",
audio_vq_codebook_size: int = 4096,
audio_vq_pe: bool = False,
audio_vq_commit_loss: float = 0.0,
audio_vq_out_commit_loss: float = 0.0,
audio_vq_no_quantize: bool = False,
audio_vq_ff_layer: int = 0,
audio_vq_threshold_ema_dead_code: float = 0.1,
audio_vq_codebook_dim: int = None,
audio_vq_ds_rate: int = None,
):
super().__init__(
n_mels,
n_ctx,
n_state,
n_head,
n_layer,
n_window,
output_dim,
grad_checkpointing,
enable_mp,
audio_sequence_parallel,
)
self.audio_vq_layers = audio_vq_layers
self.audio_vq_type = audio_vq_type
self.audio_vq_codebook_size = audio_vq_codebook_size
self.audio_vq_pe = audio_vq_pe
self.audio_vq_commit_loss = audio_vq_commit_loss
self.audio_vq_out_commit_loss = audio_vq_out_commit_loss
self.audio_vq_no_quantize = audio_vq_no_quantize
self.audio_vq_ff_layer = audio_vq_ff_layer
if audio_vq_layers > 0:
self.vq_feature_dim = self.n_state
self.audio_vq_ds_rate = 1
else:
raise NotImplementedError(f"Unsupported audio_vq_layers: {audio_vq_layers}")
if self.audio_vq_ds_rate == audio_vq_ds_rate:
self.audio_vq_downsample = nn.Identity()
self.audio_vq_upsample = nn.Identity()
else:
assert audio_vq_ds_rate % self.audio_vq_ds_rate == 0
stride = audio_vq_ds_rate // self.audio_vq_ds_rate
self.audio_vq_downsample = Conv1d(
self.vq_feature_dim, self.vq_feature_dim, kernel_size=stride, stride=stride
)
self.audio_vq_upsample = ConvTranspose1d(
self.vq_feature_dim, self.vq_feature_dim, kernel_size=stride, stride=stride
)
self.audio_vq_ds_rate = audio_vq_ds_rate
if audio_vq_type == "GRVQ":
self.audio_quantizer = DistributedGroupResidualVectorQuantization(
codebook_size=audio_vq_codebook_size,
dim=self.vq_feature_dim,
codebook_dim=self.vq_codebook_dim if audio_vq_codebook_dim is None else audio_vq_codebook_dim,
num_groups=1,
num_quantizers=1,
kmeans_init=False,
threshold_ema_dead_code=audio_vq_threshold_ema_dead_code,
)
else:
raise NotImplementedError(f"Unsupported audio_vq_type: {audio_vq_type}")
if self.audio_vq_pe:
self.project_after_vq_pe = nn.Linear(self.n_state, self.n_state)
def _calc_quantize_activities(self, indices):
indices_onehot = F.one_hot(indices.long().flatten(), self.audio_vq_codebook_size).sum(dim=0)
vq_num_activities = sum(indices_onehot > 0)
vq_num_tokens = sum(indices_onehot)
return {
"vq_num_activities": vq_num_activities,
"vq_num_tokens": vq_num_tokens,
}
def _do_quantize(self, x, pe=None, y=None):
"""
x: torch.Tensor, shape = (T, D)
q: torch.Tensor, shape = (T, D)
i: torch.Tensor, shape = (T)
"""
if self.audio_vq_out_commit_loss > 0:
x_teacher = x.clone()
x = x.unsqueeze(0)
x = self.audio_vq_downsample(x.transpose(1, 2))
x = x.transpose(1, 2)
vq_stats = {}
if self.audio_vq_type == "GRVQ":
if self.training:
raise NotImplementedError
else:
indices = self.audio_quantizer.encode(x)
x = self.audio_quantizer.decode(indices)
indices = indices.squeeze(2).squeeze(1)
vq_stats.update(self._calc_quantize_activities(indices))
x, indices = x.squeeze(0), indices.squeeze(0)
if self.audio_vq_pe:
x = x + pe
x = self.project_after_vq_pe(x)
x = self.audio_vq_upsample(x.unsqueeze(0).transpose(1, 2))
x = x.transpose(1, 2).squeeze(0)
if self.audio_vq_out_commit_loss > 0:
vq_out_commit_loss = F.mse_loss(x_teacher.detach(), x)
vq_stats["vq_out_commit_loss"] = vq_out_commit_loss * self.audio_vq_out_commit_loss
return x, indices, vq_stats
def forward(
self,
x_list: list[Tensor],
audio_mellens: list[int],
audio_aftercnnlens: list[int],
audio_seqlens: list[int],
return_indices=False,
audio_pitches=None,
):
"""
x : torch.Tensor, shape = (n_mels, n_ctx)
the mel spectrogram of the audio
"""
aftercnn_x_list = []
pe_for_vq_list = []
for each_x in x_list:
each_x_split_list = each_x.split(self.n_window * 2, dim=1)
for each_x_split in each_x_split_list:
each_x_split = F.gelu(self.conv1(each_x_split))
each_x_split = F.gelu(self.conv2(each_x_split))
each_x_split = each_x_split.permute(1, 0) # L,D
each_positional_embedding_split = self.positional_embedding[: each_x_split.shape[0]]
aftercnn_x_list.append(each_x_split + each_positional_embedding_split.to(each_x_split.dtype))
pe_for_vq_split = self.positional_embedding[: each_x_split.shape[0] // self.audio_vq_ds_rate]
pe_for_vq_list.append(pe_for_vq_split.to(each_x_split.dtype))
pe_for_vq = torch.cat(pe_for_vq_list, dim=0)
x = torch.cat(aftercnn_x_list, dim=0)
output_list = []
for item in audio_aftercnnlens:
while item > self.n_window:
output_list.append(self.n_window)
item -= self.n_window
output_list.append(item)
cu_seqlens = list(accumulate(output_list, func=operator.add, initial=0))
cu_seqlens = torch.Tensor(cu_seqlens).to(device=x.device, dtype=torch.int32)
layer_id = 0
for block in self.blocks:
layer_id += 1
x = block(x, cu_seqlens=cu_seqlens)
if self.audio_vq_layers == layer_id: # vq inside encoder
x, indices, vq_stats = self._do_quantize(x, pe_for_vq)
if return_indices:
return x, indices
if self.avg_pooler:
x_list = x.split(audio_aftercnnlens, dim=0)
token_x_list = []
for x in x_list:
x = x.permute(1, 0)
x = self.avg_pooler(x)
x = x.permute(1, 0)
token_x_list.append(x)
x = torch.cat(token_x_list, dim=0)
x = self.ln_post(x)
x = self.proj(x)
output = torch.zeros((x.size(0) + len(audio_seqlens) * 2, x.size(1)), device=x.device, dtype=x.dtype)
audio_seqlens_acc = list(accumulate(audio_seqlens, func=operator.add, initial=0))
start_ids = torch.tensor(audio_seqlens_acc[:-1], device=x.device, dtype=torch.int32)
end_ids = torch.tensor(audio_seqlens_acc[1:], device=x.device, dtype=torch.int32) - 1
audio_tokens_mask = torch.ones(output.size(0), device=x.device, dtype=torch.bool)
audio_tokens_mask[start_ids] = False
audio_tokens_mask[end_ids] = False
output[start_ids] = self.audio_bos_eos_token.weight[0].to(x.dtype)
output[end_ids] = self.audio_bos_eos_token.weight[1].to(x.dtype)
output[audio_tokens_mask] = x
if self.audio_vq_type != "NULL":
return output, vq_stats
return output
@@ -0,0 +1,385 @@
# Copyright 2026 The Alibaba Qwen team.
# SPDX-License-Identifier: Apache-2.0
#
# 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.
import math
import operator
import os
from functools import cache
from itertools import accumulate
import numpy as np
import torch
import torch.nn.functional as F
from torch import Tensor, nn
try:
from flash_attn.flash_attn_interface import flash_attn_varlen_func as flash_attn_varlen_func
except ImportError:
try:
from flash_attn.flash_attn_interface import flash_attn_unpadded_func as flash_attn_varlen_func
except ImportError:
print(
"\n********\nWarning: flash-attn is not installed. "
"Will only run the manual PyTorch version. "
"Please install flash-attn for faster inference.\n********\n "
)
flash_attn_varlen_func = None
N_FFT = 400
HOP_LENGTH = 160
@cache
def mel_filters(device, n_mels: int) -> torch.Tensor:
"""
load the mel filterbank matrix for projecting STFT into a Mel spectrogram.
Allows decoupling librosa dependency; saved using:
np.savez_compressed(
"mel_filters.npz",
mel_80=librosa.filters.mel(sr=16000, n_fft=400, n_mels=80),
mel_128=librosa.filters.mel(sr=16000, n_fft=400, n_mels=128),
)
"""
assert n_mels in {80, 128}, f"Unsupported n_mels: {n_mels}"
filters_path = os.path.join(os.path.dirname(__file__), "assets", "mel_filters.npz")
with np.load(filters_path, allow_pickle=False) as f:
return torch.from_numpy(f[f"mel_{n_mels}"]).to(device)
def log_mel_spectrogram(
audio: str | np.ndarray | torch.Tensor,
n_mels: int = 80,
padding: int = 0,
device: str | torch.device | None = None,
):
"""
Compute the log-Mel spectrogram of
Parameters
----------
audio: Union[str, np.ndarray, torch.Tensor], shape = (*)
The path to audio or either a NumPy array or Tensor containing the audio waveform in 16 kHz
n_mels: int
The number of Mel-frequency filters, only 80 is supported
padding: int
Number of zero samples to pad to the right
device: Optional[Union[str, torch.device]]
If given, the audio tensor is moved to this device before STFT
Returns
-------
torch.Tensor, shape = (80, n_frames)
A Tensor that contains the Mel spectrogram
"""
if not torch.is_tensor(audio):
audio = torch.from_numpy(audio)
if device is not None:
audio = audio.to(device)
if padding > 0:
audio = F.pad(audio, (0, padding))
window = torch.hann_window(N_FFT).to(audio.device)
stft = torch.stft(audio, N_FFT, HOP_LENGTH, window=window, return_complex=True)
magnitudes = stft[..., :-1].abs() ** 2
filters = mel_filters(audio.device, n_mels)
mel_spec = filters @ magnitudes
log_spec = torch.clamp(mel_spec, min=1e-10).log10()
log_spec = torch.maximum(log_spec, log_spec.max() - 8.0)
log_spec = (log_spec + 4.0) / 4.0
return log_spec
def get_T_after_cnn(l_in, dilation=1):
for padding, kernel_size, stride in eval("[(1,3,1)] + [(1,3,2)] "):
l_out = l_in + 2 * padding - dilation * (kernel_size - 1) - 1
l_out = 1 + l_out // stride
l_in = l_out
return l_out
def get_mel_audio(audio, padding=False, audio_vq_ds_rate=1, n_mels=128):
audio_len = len(audio)
if padding:
reduction = 160 * 2 * audio_vq_ds_rate
audio_pad = math.ceil(audio_len / reduction) * reduction - audio_len
mel = log_mel_spectrogram(audio, n_mels=n_mels, padding=audio_pad)
else:
mel = log_mel_spectrogram(audio, n_mels=n_mels) # [F,T]
return mel
def sinusoids(length, channels, max_timescale=10000):
"""Returns sinusoids for positional embedding"""
assert channels % 2 == 0
log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
inv_timescales = torch.exp(-log_timescale_increment * torch.arange(channels // 2))
scaled_time = torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
return torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1)
class Conv1d(nn.Conv1d):
def _conv_forward(self, x: Tensor, weight: Tensor, bias: Tensor | None) -> Tensor:
return super()._conv_forward(x, weight.to(x.dtype), None if bias is None else bias.to(x.dtype))
class ConvTranspose1d(nn.ConvTranspose1d):
def _conv_forward(self, x: Tensor, weight: Tensor, bias: Tensor | None) -> Tensor:
return super()._conv_forward(x, weight.to(x.dtype), None if bias is None else bias.to(x.dtype))
class Linear(nn.Linear):
def forward(self, x: Tensor) -> Tensor:
return F.linear(x, self.weight.to(x.dtype), None if self.bias is None else self.bias.to(x.dtype))
class MultiHeadAttention(nn.Module):
def __init__(self, n_state: int, n_head: int):
super().__init__()
self.n_head = n_head
self.query = Linear(n_state, n_state)
self.key = Linear(n_state, n_state, bias=False)
self.value = Linear(n_state, n_state)
self.out = Linear(n_state, n_state)
self.use_flash_attention = True
def forward(
self,
x: Tensor,
cu_seqlens=None,
):
q = self.query(x)
k = self.key(x)
v = self.value(x)
if self.use_flash_attention:
if flash_attn_varlen_func is None:
x = self.qkv_attention_manual(q, k, v, cu_seqlens=cu_seqlens)
else:
if q.dtype not in [torch.float16, torch.bfloat16]:
x = self.qkv_attention_manual(q, k, v, cu_seqlens=cu_seqlens)
self.use_flash_attention = False
else:
x = self.qkv_flash_attention(q, k, v, cu_seqlens=cu_seqlens)
else:
x = self.qkv_attention_manual(q, k, v, cu_seqlens=cu_seqlens)
output = self.out(x)
return output
def qkv_flash_attention(self, q: Tensor, k: Tensor, v: Tensor, cu_seqlens=None):
n_ctx, n_state = q.shape
# scale = (n_state // self.n_head) ** -0.25
q = q.view(n_ctx, self.n_head, -1) # (batch_size, seqlen, nheads, headdim)
k = k.view(n_ctx, self.n_head, -1)
v = v.view(n_ctx, self.n_head, -1)
max_seqlen = (cu_seqlens[1:] - cu_seqlens[:-1]).max().item()
x = flash_attn_varlen_func(q, k, v, cu_seqlens, cu_seqlens, max_seqlen, max_seqlen, dropout_p=0.0)
x = x.reshape(n_ctx, n_state)
return x
def qkv_attention_manual(self, q: Tensor, k: Tensor, v: Tensor, cu_seqlens: Tensor):
n_ctx, n_state = q.shape
head_dim = n_state // self.n_head
scale = head_dim**-0.5
q = q.view(n_ctx, self.n_head, head_dim)
k = k.view(n_ctx, self.n_head, head_dim)
v = v.view(n_ctx, self.n_head, head_dim)
seqlens = (cu_seqlens[1:] - cu_seqlens[:-1]).tolist()
batch_size = len(seqlens)
max_seqlen = max(seqlens)
q_padded = torch.zeros(batch_size, max_seqlen, self.n_head, head_dim, dtype=q.dtype, device=q.device)
k_padded = torch.zeros_like(q_padded)
v_padded = torch.zeros_like(q_padded)
for i in range(batch_size):
start_idx = cu_seqlens[i]
end_idx = cu_seqlens[i + 1]
seq_len = seqlens[i]
q_padded[i, :seq_len] = q[start_idx:end_idx]
k_padded[i, :seq_len] = k[start_idx:end_idx]
v_padded[i, :seq_len] = v[start_idx:end_idx]
q_padded = q_padded.transpose(1, 2)
k_padded = k_padded.transpose(1, 2)
v_padded = v_padded.transpose(1, 2)
attn_mask = torch.arange(max_seqlen, device=q.device)[None, :] < torch.tensor(seqlens, device=q.device)[:, None]
attn_mask = attn_mask.unsqueeze(1).unsqueeze(2)
attn_mask = attn_mask.masked_fill(attn_mask == 0, -torch.finfo(q.dtype).max)
attn_scores = torch.matmul(q_padded, k_padded.transpose(-2, -1)) * scale
attn_scores = attn_scores + attn_mask
attn_weights = F.softmax(attn_scores, dim=-1)
context = torch.matmul(attn_weights, v_padded)
context = context.transpose(1, 2).contiguous().view(batch_size, max_seqlen, n_state)
output_packed = torch.cat([context[i, : seqlens[i]] for i in range(batch_size)], dim=0)
assert output_packed.shape == (n_ctx, n_state)
return output_packed
class ResidualAttentionBlock(nn.Module):
def __init__(self, n_state: int, n_head: int, enable_mp: bool = False, sequence_parallel: bool = False):
super().__init__()
n_mlp = n_state * 4
self.attn_ln = nn.LayerNorm(n_state)
self.mlp_ln = nn.LayerNorm(n_state)
self.attn = MultiHeadAttention(n_state, n_head)
self.mlp = nn.Sequential(Linear(n_state, n_mlp), nn.GELU(), Linear(n_mlp, n_state))
def forward(self, x: Tensor, cu_seqlens=None):
x = x + self.attn(self.attn_ln(x), cu_seqlens=cu_seqlens)
x = x + self.mlp(self.mlp_ln(x))
return x
class WhisperEncoder(nn.Module):
def __init__(
self,
n_mels: int,
n_ctx: int,
n_state: int,
n_head: int,
n_layer: int,
n_window: int = 1500,
output_dim: int = 512,
grad_checkpointing: bool = False,
enable_mp: bool = False,
audio_sequence_parallel: bool = False,
):
super().__init__()
self.conv1 = Conv1d(n_mels, n_state, kernel_size=3, padding=1)
self.conv2 = Conv1d(n_state, n_state, kernel_size=3, stride=2, padding=1)
self.register_buffer("positional_embedding", sinusoids(n_ctx, n_state))
self.n_layer = n_layer
self.n_mels = n_mels
self.blocks = nn.ModuleList(
[
ResidualAttentionBlock(n_state, n_head, enable_mp=enable_mp, sequence_parallel=audio_sequence_parallel)
for _ in range(n_layer)
]
)
self.ln_post = nn.LayerNorm(n_state)
self.avg_pooler = nn.AvgPool1d(2, stride=2)
self.proj = torch.nn.Linear(n_state, output_dim)
self.audio_bos_eos_token = nn.Embedding(2, output_dim)
self.output_dim = output_dim
self.grad_checkpointing = grad_checkpointing
self.enable_mp = enable_mp
self.n_head = n_head
self.n_state = n_state
self.n_window = n_window
self.audio_sequence_parallel = audio_sequence_parallel
self.tp_world_size = 1
self.set_audio_sync()
def set_audio_sync(self):
for name, param in self.named_parameters():
if not name.startswith("blocks"):
setattr(param, "audio_sync", True)
def forward(
self, x_list: list[Tensor], audio_mellens: list[int], audio_aftercnnlens: list[int], audio_seqlens: list[int]
):
"""
x : torch.Tensor, shape = (n_mels, n_ctx)
the mel spectrogram of the audio
"""
aftercnn_x_list = []
for each_x in x_list:
each_x_split_list = each_x.split(self.n_window * 2, dim=1)
for each_x_split in each_x_split_list:
each_x_split = F.gelu(self.conv1(each_x_split))
each_x_split = F.gelu(self.conv2(each_x_split))
each_x_split = each_x_split.permute(1, 0) # L,D
each_positional_embedding_split = self.positional_embedding[: each_x_split.shape[0]]
aftercnn_x_list.append(each_x_split + each_positional_embedding_split.to(each_x_split.dtype))
x = torch.cat(aftercnn_x_list, dim=0)
output_list = []
for item in audio_aftercnnlens:
while item > self.n_window:
output_list.append(self.n_window)
item -= self.n_window
output_list.append(item)
cu_seqlens = list(accumulate(output_list, func=operator.add, initial=0))
cu_seqlens = torch.Tensor(cu_seqlens).to(device=x.device, dtype=torch.int32)
layer_id = 0
for block in self.blocks:
layer_id += 1
x = block(x, cu_seqlens=cu_seqlens)
if self.avg_pooler:
x_list = x.split(audio_aftercnnlens, dim=0)
token_x_list = []
for x in x_list:
x = x.permute(1, 0)
x = self.avg_pooler(x)
x = x.permute(1, 0)
token_x_list.append(x)
x = torch.cat(token_x_list, dim=0)
x = self.ln_post(x)
x = self.proj(x)
output = torch.zeros((x.size(0) + len(audio_seqlens) * 2, x.size(1)), device=x.device, dtype=x.dtype)
audio_seqlens_acc = list(accumulate(audio_seqlens, func=operator.add, initial=0))
start_ids = torch.tensor(audio_seqlens_acc[:-1], device=x.device, dtype=torch.int32)
end_ids = torch.tensor(audio_seqlens_acc[1:], device=x.device, dtype=torch.int32) - 1
audio_tokens_mask = torch.ones(output.size(0), device=x.device, dtype=torch.bool)
audio_tokens_mask[start_ids] = False
audio_tokens_mask[end_ids] = False
output[start_ids] = self.audio_bos_eos_token.weight[0].to(x.dtype)
output[end_ids] = self.audio_bos_eos_token.weight[1].to(x.dtype)
output[audio_tokens_mask] = x
return output
def lock(self, layers: int):
self.conv1.requires_grad_(False)
self.conv2.requires_grad_(False)
for i in range(min(layers, len(self.blocks))):
self.blocks[i].requires_grad_(False)
@@ -48,6 +48,11 @@ _OMNI_MODELS = {
"qwen3_omni_code2wav",
"Qwen3OmniMoeCode2Wav",
),
"Qwen3TTSForConditionalGeneration": (
"qwen3_tts",
"qwen3_tts",
"Qwen3TTSModelForGeneration",
),
}
_VLLM_OMNI_MODELS = {
@@ -0,0 +1,22 @@
stage_args:
- stage_id: 0
stage_type: llm # Use llm stage type to launch OmniLLM
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: qwen3_tts
model_arch: Qwen3TTSForConditionalGeneration
worker_cls: vllm_omni.worker.gpu_generation_worker.GPUGenerationWorker
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
enforce_eager: true
trust_remote_code: true
async_scheduling: false
enable_prefix_caching: false
engine_output_type: audio # Final output: audio waveform
gpu_memory_utilization: 0.1
distributed_executor_backend: "mp"
max_num_batched_tokens: 1000000
final_output: true
final_output_type: audio
@@ -308,8 +308,11 @@ class GPUGenerationModelRunner(OmniGPUModelRunner):
{"model_outputs": out.detach().to("cpu").contiguous() if out is not None else None}
)
elif isinstance(multimodal_outputs, dict):
mm_payload = {}
for key, out in multimodal_outputs.items():
pooler_output.append({key: out.detach().to("cpu").contiguous() if out is not None else None})
if out is not None and isinstance(out, torch.Tensor):
mm_payload[key] = out.detach().to("cpu").contiguous()
pooler_output.append(mm_payload)
else:
raise RuntimeError("Unsupported diffusion output type")
output = OmniModelRunnerOutput(