[New Model] Add support for tencent/Covo-Audio-Chat (#2293)

Signed-off-by: Dnoob <dxpouo@gmail.com>
Signed-off-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
authored and GitHub committed 2026-05-12 14:39:58 +08:00
1 parent e40076e872
commit d7ea5d5979
25 files changed
+2844

No files matched your search

+1
View File
@@ -68,6 +68,7 @@ th {
| `HunyuanVideo15Pipeline` | HunyuanVideo-1.5-T2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | ✅︎ | ✅︎ | | |
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | |
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | |
| `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | |
|`DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | |
| `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
✅︎ indicates the model is supported on that backend. Empty cells mean not listed as supported on that backend.
@@ -0,0 +1,76 @@
# Covo-Audio-Chat (Offline Inference)
## 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.
> **Note**
> Covo-Audio code2wav requires `torchdiffeq`. Install it with: `pip install torchdiffeq`
## Run examples
Get into the example folder:
```bash
cd examples/offline_inference/covo_audio
```
### Audio input chat
Using the default audio asset:
```bash
python end2end.py
```
Using a custom audio file:
```bash
python end2end.py --audio-path /path/to/audio.wav
```
Using a local model:
```bash
python end2end.py -m /path/to/Covo-Audio-Chat --output-dir ./my_output
```
### Command-line Arguments
| Argument | Short | Default | Description |
|----------|-------|---------|-------------|
| `--model-name` | `-m` | `tencent/Covo-Audio-Chat` | Model path or HuggingFace model ID |
| `--text` | `-t` | `请回答这段音频里的问题。` | Text prompt / question for the audio |
| `--audio-path` | `-a` | default audio asset | Path to local audio file |
| `--sampling-rate` | | `16000` | Sampling rate for audio loading (Hz) |
| `--output-dir` | | `./output_audio` | Output directory for generated files |
| `--num-prompts` | | `1` | Number of prompts to generate |
| `--stage-configs-path` | | (auto) | Path to stage configs YAML file |
| `--log-stats` | | `false` | Enable detailed statistics logging |
| `--stage-init-timeout` | | `300` | Stage initialization timeout (seconds) |
| `--batch-timeout` | | `5` | Batching timeout (seconds) |
| `--init-timeout` | | `300` | Overall initialization timeout (seconds) |
| `--shm-threshold-bytes` | | `65536` | Shared memory threshold (bytes) |
## Pipeline
Covo-Audio-Chat uses a 2-stage pipeline:
- **Stage 0 (fused_thinker_talker):** The 7B LLM generates interleaved text and audio tokens in a single autoregressive pass.
- **Stage 1 (code2wav):** A BigVGAN-based vocoder converts the extracted audio codes into a 24kHz WAV waveform.
## Output
The script generates two files per request in the output directory:
- `{request_id}.txt` -- prompt and generated text
- `{request_id}.wav` -- generated audio (24kHz WAV)
## FAQ
If you encounter `ModuleNotFoundError: No module named 'librosa'`, install it with:
```bash
pip install librosa
```
## Environment
- GPU: 1x A100 (80 GiB)
- Stage 0 (7B LLM): ~16 GiB VRAM
- Stage 1 (BigVGAN vocoder): ~2 GiB VRAM
@@ -0,0 +1,222 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
This example shows how to use vLLM-Omni for running offline inference
with the correct prompt format on Covo-Audio-Chat.
Usage:
python end2end.py --audio-path /path/to/audio.wav
"""
import os
import soundfile as sf
from vllm.assets.audio import AudioAsset
from vllm.multimodal.media.audio import load_audio
from vllm.sampling_params import SamplingParams
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.entrypoints.omni import Omni
from vllm_omni.model_executor.models.covo_audio.prompt_utils import (
COVO_AUDIO_INPUT_PREFIX,
build_covo_audio_chat_prompt,
)
SEED = 42
def get_audio_query(
question: str | None = None,
audio_path: str | None = None,
sampling_rate: int = 16000,
) -> dict:
if question is None:
question = "请回答这段音频里的问题。"
user_content = COVO_AUDIO_INPUT_PREFIX + question
prompt = build_covo_audio_chat_prompt(user_content)
if audio_path is None:
audio_data = AudioAsset("mary_had_lamb").audio_and_sample_rate
else:
import numpy as np
audio_signal, sr = load_audio(audio_path, sr=sampling_rate)
audio_data = (audio_signal.astype(np.float32), sr)
return {
"prompt": prompt,
"multi_modal_data": {"audio": audio_data},
"modalities": ["audio"],
}
def main(args):
query_result = get_audio_query(
question=args.text,
audio_path=args.audio_path,
sampling_rate=args.sampling_rate,
)
omni = Omni(
model=args.model_name,
stage_configs_path=args.stage_configs_path,
log_stats=args.log_stats,
stage_init_timeout=args.stage_init_timeout,
batch_timeout=args.batch_timeout,
init_timeout=args.init_timeout,
shm_threshold_bytes=args.shm_threshold_bytes,
)
# Stage 0: fused_thinker_talker
# stop_token_ids=[151645] (<|im_end|>) and ignore_eos=True are required
# so the model generates interleaved text+audio tokens before stopping.
thinker_sampling_params = SamplingParams(
temperature=0.0,
top_p=1.0,
top_k=-1,
max_tokens=2048,
seed=SEED,
detokenize=True,
repetition_penalty=1.05,
stop_token_ids=[151645],
ignore_eos=True,
)
# Stage 1: code2wav (audio codes, not real token IDs — skip detokenize)
code2wav_sampling_params = SamplingParams(
temperature=0.0,
top_p=1.0,
top_k=-1,
max_tokens=2048,
seed=SEED,
detokenize=False,
repetition_penalty=1.1,
)
sampling_params_list = [
thinker_sampling_params,
code2wav_sampling_params,
]
prompts = [query_result for _ in range(args.num_prompts)]
omni_outputs = omni.generate(prompts, sampling_params_list)
output_dir = args.output_dir
os.makedirs(output_dir, exist_ok=True)
for stage_outputs in omni_outputs:
output = stage_outputs.request_output
if stage_outputs.final_output_type == "text":
request_id = output.request_id
text_output = output.outputs[0].text
prompt_text = output.prompt
out_txt = os.path.join(output_dir, f"{request_id}.txt")
lines = [
"Prompt:\n",
str(prompt_text) + "\n",
"vllm_text_output:\n",
str(text_output).strip() + "\n",
]
try:
with open(out_txt, "w", encoding="utf-8") as f:
f.writelines(lines)
except Exception as e:
print(f"[Warn] Failed writing text file {out_txt}: {e}")
print(f"Request ID: {request_id}, Text saved to {out_txt}")
elif stage_outputs.final_output_type == "audio":
request_id = output.request_id
audio_tensor = output.outputs[0].multimodal_output.get("audio")
if audio_tensor is None:
continue
output_wav = os.path.join(output_dir, f"{request_id}.wav")
audio_numpy = audio_tensor.float().detach().cpu().numpy()
if audio_numpy.ndim > 1:
audio_numpy = audio_numpy.flatten()
sf.write(output_wav, audio_numpy, samplerate=24000, format="WAV")
print(f"Request ID: {request_id}, Audio saved to {output_wav}")
omni.close()
def parse_args():
parser = FlexibleArgumentParser(description="Offline inference demo for Covo-Audio-Chat")
parser.add_argument(
"--model-name",
"-m",
type=str,
default="tencent/Covo-Audio-Chat",
help="Model path or HuggingFace model ID.",
)
parser.add_argument(
"--text",
"-t",
type=str,
default=None,
help="Text prompt / question for the audio.",
)
parser.add_argument(
"--audio-path",
"-a",
type=str,
default=None,
help="Path to local audio file. Uses default asset if not provided.",
)
parser.add_argument(
"--sampling-rate",
type=int,
default=16000,
help="Sampling rate for audio loading (default: 16000).",
)
parser.add_argument(
"--stage-configs-path",
type=str,
default=None,
help="Path to stage configs YAML file.",
)
parser.add_argument(
"--log-stats",
action="store_true",
default=False,
help="Enable writing detailed statistics.",
)
parser.add_argument(
"--stage-init-timeout",
type=int,
default=300,
help="Timeout for initializing a single stage in seconds.",
)
parser.add_argument(
"--batch-timeout",
type=int,
default=5,
help="Timeout for batching in seconds.",
)
parser.add_argument(
"--init-timeout",
type=int,
default=300,
help="Timeout for initializing stages in seconds.",
)
parser.add_argument(
"--shm-threshold-bytes",
type=int,
default=65536,
help="Threshold for using shared memory in bytes.",
)
parser.add_argument(
"--output-dir",
default="./output_audio",
help="Output directory for generated files.",
)
parser.add_argument(
"--num-prompts",
type=int,
default=1,
help="Number of prompts to generate.",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
main(args)
@@ -0,0 +1,93 @@
# Covo-Audio-Chat
## Installation
Please refer to [README.md](../../../README.md)
> **Note**
> Covo-Audio code2wav requires `torchdiffeq`. Install it with: `pip install torchdiffeq`
## Run examples (Covo-Audio-Chat)
### Launch the Server
```bash
CUDA_VISIBLE_DEVICES=0 vllm serve tencent/Covo-Audio-Chat --omni \
--trust-remote-code --port 18091
```
> The default deploy config is located at `vllm_omni/deploy/covo_audio.yaml` and is loaded automatically by the model registry — no `--deploy-config` flag needed for default use.
### Send Request
Get into the example folder:
```bash
cd examples/online_serving/covo_audio
```
#### Audio input chat
Using the default audio asset:
```bash
python openai_chat_completion_client.py
```
Using a custom audio file:
```bash
python openai_chat_completion_client.py --audio-path /path/to/audio.wav
```
#### Streaming mode
```bash
python openai_chat_completion_client.py --audio-path /path/to/audio.wav --stream
```
### Command-line Arguments
The Python client supports the following arguments:
- `--prompt` (or `-p`): Text prompt (default: `"请回答这段音频里的问题。"`)
- `--audio-path`: Path to audio file. Supports local file paths or HTTP/HTTPS URLs. Common audio formats: WAV, MP3, OGG, FLAC, M4A. Uses the default audio asset if omitted
- `--output-audio-path` (or `-o`): Output path for generated audio (default: `./audio_0.wav`)
- `--model`: Model name for the API. Auto-detected from server if omitted
- `--stream`: Enable streaming mode
## Pipeline
Covo-Audio-Chat uses a 2-stage pipeline:
- **Stage 0 (fused_thinker_talker):** The 7B LLM generates interleaved text and audio tokens in a single autoregressive pass.
- **Stage 1 (code2wav):** A BigVGAN-based vocoder converts the extracted audio codes into a 24kHz WAV waveform.
## Environment
- GPU: 1x H100 (80 GiB)
- Stage 0 (7B LLM): ~16 GiB VRAM
- Stage 1 (BigVGAN vocoder): ~2 GiB VRAM
## Important: System Prompt
Covo-Audio-Chat requires a specific system prompt to enable interleaved text+audio generation. Without it, Stage 0 produces text-only tokens, and the output audio will be **silent**.
The system prompt must include the instruction:
```
请用文本和音频进行对话,交替生成5个文本token和15个音频token,音频部分使用发音人:default_female
```
The full system prompt is defined in `vllm_omni/model_executor/models/covo_audio/prompt_utils.py` (`COVO_AUDIO_SYSTEM_PROMPT`).
## FAQ
If you encounter `ModuleNotFoundError: No module named 'librosa'`, install it with:
```bash
pip install librosa
```
### Audio output is silent (0.1s, all zeros)
This means Stage 0 did not generate any audio tokens. Most likely causes:
1. **Missing system prompt** -- see the "Important: System Prompt" section above. The model needs the interleaved generation instruction in the system message.
2. **Missing `ignore_eos: true` / `stop_token_ids: [151645]`** in Stage 0 sampling params -- without these, the model stops at `<|endoftext|>` before generating audio tokens.
@@ -0,0 +1,216 @@
"""
Client example for Covo-Audio-Chat with vllm-omni.
Usage
-----
# Start the server first:
# CUDA_VISIBLE_DEVICES=0 vllm serve tencent/Covo-Audio-Chat --omni --trust-remote-code
# Audio input chat (uses default audio asset if --audio-path not provided):
python openai_chat_completion_client.py
python openai_chat_completion_client.py --audio-path /path/to/audio.wav
# Streaming mode:
python openai_chat_completion_client.py --stream
"""
import base64
import os
import sys
import time
from openai import OpenAI
from vllm.assets.audio import AudioAsset
from vllm.utils.argparse_utils import FlexibleArgumentParser
from vllm_omni.model_executor.models.covo_audio.prompt_utils import (
COVO_AUDIO_SYSTEM_PROMPT,
)
openai_api_key = "EMPTY"
openai_api_base = "http://localhost:18091/v1"
client = OpenAI(api_key=openai_api_key, base_url=openai_api_base)
SEED = 42
def encode_base64_content_from_file(file_path: str) -> str:
"""Encode a local file to base64 format."""
with open(file_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")
def get_audio_url(audio_path: str | None) -> str:
"""Convert an audio path to a data URL for the API."""
if not audio_path:
return AudioAsset("mary_had_lamb").url
if audio_path.startswith(("data:", "http://", "https://")):
return audio_path
if not os.path.exists(audio_path):
raise FileNotFoundError(f"Audio file not found: {audio_path}")
ext = audio_path.rsplit(".", 1)[-1].lower() if "." in audio_path else "wav"
mime_map = {
"wav": "audio/wav",
"mp3": "audio/mpeg",
"mpeg": "audio/mpeg",
"ogg": "audio/ogg",
"flac": "audio/flac",
"m4a": "audio/mp4",
}
mime = mime_map.get(ext, "audio/wav")
b64 = encode_base64_content_from_file(audio_path)
return f"data:{mime};base64,{b64}"
def build_audio_messages(audio_path: str | None, prompt: str) -> list[dict]:
"""Build messages for an audio chat request."""
audio_url = get_audio_url(audio_path)
return [
{"role": "system", "content": COVO_AUDIO_SYSTEM_PROMPT},
{
"role": "user",
"content": [
{"type": "audio_url", "audio_url": {"url": audio_url}},
{"type": "text", "text": prompt},
],
},
]
def get_model_name(override: str | None = None) -> str:
"""Return the model name to use in API calls."""
if override:
return override
# Auto-detect from the server
models = client.models.list()
return models.data[0].id
def run(args) -> None:
model_name = get_model_name(args.model)
thinker_params = {
"temperature": 0.0,
"top_p": 1.0,
"top_k": -1,
"max_tokens": 2048,
"seed": SEED,
"detokenize": True,
"repetition_penalty": 1.05,
"stop_token_ids": [151645],
"ignore_eos": True,
}
code2wav_params = {
"temperature": 0.0,
"top_p": 1.0,
"top_k": -1,
"max_tokens": 2048,
"seed": SEED,
"detokenize": False,
"repetition_penalty": 1.05,
}
extra_body = {
"sampling_params_list": [thinker_params, code2wav_params],
}
messages = build_audio_messages(args.audio_path, args.prompt)
start = time.perf_counter()
if not args.stream:
completion = client.chat.completions.create(
messages=messages,
model=model_name,
extra_body=extra_body,
stream=False,
)
elapsed = time.perf_counter() - start
for choice in completion.choices:
if choice.message.audio:
audio_bytes = base64.b64decode(choice.message.audio.data)
out_path = args.output_audio_path
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with open(out_path, "wb") as f:
f.write(audio_bytes)
print(f"Audio saved to {out_path} ({len(audio_bytes)} bytes)")
elif choice.message.content:
print("Chat completion output from text:", choice.message.content)
print(f"Elapsed: {elapsed:.2f}s", file=sys.stderr)
else:
stream = client.chat.completions.create(
messages=messages,
model=model_name,
extra_body=extra_body,
stream=True,
)
audio_idx = 0
for chunk in stream:
modality = getattr(chunk, "modality", None)
elapsed = time.perf_counter() - start
for choice in chunk.choices:
content = getattr(choice.delta, "content", None) if hasattr(choice, "delta") else None
if not content:
continue
if modality == "audio":
audio_bytes = base64.b64decode(content)
out_dir = os.path.dirname(args.output_audio_path) or "."
out_path = os.path.join(out_dir, f"audio_{audio_idx}.wav")
os.makedirs(out_dir, exist_ok=True)
with open(out_path, "wb") as f:
f.write(audio_bytes)
print(f"[{elapsed:.2f}s] Audio chunk saved to {out_path}", file=sys.stderr)
audio_idx += 1
elif modality == "text":
print(content, end="", flush=True)
print() # trailing newline
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.2f}s", file=sys.stderr)
def parse_args():
parser = FlexibleArgumentParser(description="Covo-Audio-Chat client example")
parser.add_argument(
"--prompt",
"-p",
type=str,
default="请回答这段音频里的问题。",
help="Text prompt.",
)
parser.add_argument(
"--audio-path",
type=str,
default=None,
help="Path to audio file. Uses default audio asset if omitted.",
)
parser.add_argument(
"--output-audio-path",
"-o",
type=str,
default="./audio_0.wav",
help="Output path for generated audio.",
)
parser.add_argument(
"--model",
type=str,
default=None,
help="Model name for the API. Auto-detected from server if omitted.",
)
parser.add_argument(
"--stream",
action="store_true",
help="Enable streaming mode.",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
run(args)
+2
View File
@@ -29,6 +29,8 @@ recipes/
Qwen-Image on `1x A100 80GB`, including optional step-wise continuous batching replay
- [`Qwen/Qwen3-Omni.md`](./Qwen/Qwen3-Omni.md): online serving recipe for
multimodal chat on `1x A100 80GB`
- [`Tencent/Covo-Audio-Chat.md`](./Tencent/Covo-Audio-Chat.md): online
serving recipe for audio chat on `1x A100 80GB`
- [`Qwen/Qwen3-TTS.md`](./Qwen/Qwen3-TTS.md): text-to-speech serving recipe
for Qwen3-TTS (CustomVoice / VoiceDesign / Base) on `1x H100/A100 80GB`
- [`LTX/LTX-2.md`](./LTX/LTX-2.md): text-to-video and image-to-video serving
+92
View File
@@ -0,0 +1,92 @@
# Covo-Audio-Chat for audio chat on 1x A100 80GB
## Summary
- Vendor: Tencent
- Model: `tencent/Covo-Audio-Chat`
- Task: Audio chat — audio (and optional text) input, interleaved text + audio output
- Mode: Online serving with the OpenAI-compatible API (offline inference also supported)
- Maintainer: Community
## When to use this recipe
Use this recipe when you want a known-good starting point for serving
`tencent/Covo-Audio-Chat` with vLLM-Omni on a single 80 GB-class GPU and
validate the deployment with the bundled client example.
## References
- Related example under `examples/`:
[`examples/online_serving/covo_audio/README.md`](../../examples/online_serving/covo_audio/README.md)
- Offline end-to-end script:
[`examples/offline_inference/covo_audio/end2end.py`](../../examples/offline_inference/covo_audio/end2end.py)
- Deploy config:
[`vllm_omni/deploy/covo_audio.yaml`](../../vllm_omni/deploy/covo_audio.yaml)
- Pipeline topology:
[`vllm_omni/model_executor/models/covo_audio/pipeline.py`](../../vllm_omni/model_executor/models/covo_audio/pipeline.py)
## Hardware Support
This recipe currently documents one tested reference configuration for
CUDA GPU serving. The fused thinker+talker stage and the BigVGAN code2wav
stage both run on the same physical device.
## GPU
### 1x A100 80GB
#### Environment
- OS: Linux
- Python: 3.12
- Driver / runtime: NVIDIA CUDA 12.x with an A100 80 GB GPU
- vLLM version: Match the repository requirements for your checkout
- vLLM-Omni version or commit: Use the commit you are deploying from
- Extra Python deps: `torchdiffeq`, `librosa`
#### Command
Install the two runtime-only deps that are not pulled in by vLLM-Omni itself:
```bash
pip install torchdiffeq librosa
```
Start the server from the repository root:
```bash
CUDA_VISIBLE_DEVICES=0 vllm serve tencent/Covo-Audio-Chat --omni \
--trust-remote-code --port 18091
```
The deploy config at `vllm_omni/deploy/covo_audio.yaml` is loaded
automatically by the model registry.
#### Verification
With the server ready, send a test request via the bundled client:
```bash
cd examples/online_serving/covo_audio
python openai_chat_completion_client.py
```
Expected: the client prints the generated text and writes an output WAV
file to the current directory.
#### Notes
- Memory usage: Stage 0 (7B LLM) ~16 GiB + stage 1 (BigVGAN vocoder)
~2 GiB, both resident on the same device.
- Key flags: `--omni` and `--trust-remote-code` are both required.
- System prompt: the bundled `openai_chat_completion_client.py` and
`end2end.py` already include `COVO_AUDIO_SYSTEM_PROMPT` (defined in
`vllm_omni/model_executor/models/covo_audio/prompt_utils.py`) in the
system message by default. When integrating your own client, make sure
to include it — without it, stage 0 emits text-only tokens and the
output audio is silent.
- Stage 1 runs with `enforce_eager: true` and `dtype: float32` — see the
header of `deploy/covo_audio.yaml` for the specific reasons.
- Known limitations: this starter recipe covers the single-GPU online-serving
path only; multi-GPU layouts and platform-specific overrides are not
documented here.
@@ -0,0 +1,47 @@
"""
E2E tests for Covo-Audio-Chat model with audio input and audio/text output.
"""
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_audio
from tests.helpers.stage_config import get_deploy_config_path
from vllm_omni.model_executor.models.covo_audio.prompt_utils import (
COVO_AUDIO_INPUT_PREFIX,
build_covo_audio_chat_prompt,
)
models = ["tencent/Covo-Audio-Chat"]
stage_config = get_deploy_config_path("covo_audio.yaml")
test_params = [(model, stage_config) for model in models]
def get_question():
return "请回答这段音频里的问题。"
@pytest.mark.full_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards={"cuda": 1})
@pytest.mark.parametrize("omni_runner", test_params, indirect=True)
def test_audio_to_audio(omni_runner, omni_runner_handler) -> None:
"""
Test audio input → text + audio output.
Deploy Setting: default yaml
Input Modal: text + audio
Output Modal: audio
"""
audio = generate_synthetic_audio(2, 1, 16000)["np_array"]
if len(audio.shape) == 2:
audio = audio.squeeze()
user_content = COVO_AUDIO_INPUT_PREFIX + get_question()
prompt = build_covo_audio_chat_prompt(user_content)
request_config = {
"prompts": prompt,
"audios": (audio, 16000),
"modalities": ["audio"],
}
omni_runner_handler.send_omni_request(request_config)
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
E2E Online tests for Covo-Audio-Chat model.
"""
import os
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
import pytest
from tests.helpers.mark import hardware_test
from tests.helpers.media import generate_synthetic_audio
from tests.helpers.runtime import OmniServerParams, dummy_messages_from_mix_data
from tests.helpers.stage_config import get_deploy_config_path
from vllm_omni.model_executor.models.covo_audio.prompt_utils import (
COVO_AUDIO_SYSTEM_PROMPT,
)
model = "tencent/Covo-Audio-Chat"
stage_config_path = get_deploy_config_path("covo_audio.yaml")
test_params = [
pytest.param(
OmniServerParams(
model=model,
stage_config_path=stage_config_path,
server_args=["--trust-remote-code"],
),
id="default",
),
]
@pytest.mark.full_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100"}, num_cards=1)
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
def test_audio_to_audio_001(omni_server, openai_client) -> None:
"""
Input Modal: text + audio
Output Modal: audio
Input Setting: stream=False
Datasets: single request
"""
audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(2, 1, sample_rate=16000)['base64']}"
messages = dummy_messages_from_mix_data(
system_prompt={"role": "system", "content": COVO_AUDIO_SYSTEM_PROMPT},
audio_data_url=audio_data_url,
content_text="请回答这段音频里的问题。",
)
request_config = {
"model": omni_server.model,
"messages": messages,
"modalities": ["audio"],
"stream": False,
}
openai_client.send_omni_request(request_config)
+4
View File
@@ -49,6 +49,10 @@ _OMNI_PIPELINES: dict[str, tuple[str, str]] = {
"vllm_omni.model_executor.models.qwen3_tts.pipeline",
"QWEN3_TTS_PIPELINE",
),
"covo_audio": (
"vllm_omni.model_executor.models.covo_audio.pipeline",
"COVO_AUDIO_PIPELINE",
),
"bagel": (
"vllm_omni.model_executor.models.bagel.pipeline",
"BAGEL_PIPELINE",
+46
View File
@@ -0,0 +1,46 @@
# Covo-Audio-Chat deploy: fused_thinker_talker → code2wav on a single GPU.
# Verified on 1x A100-80G.
#
# Fields omitted from a stage fall back to StageDeployConfig defaults (see
# vllm_omni/config/stage_config.py). Non-default choices in this file:
# * async_chunk: false — pipeline defines no async-chunk processor funcs.
# * Stage 1 max_num_batched_tokens=2048, max_model_len=2048 — audio code
# sequences are short; the 32k default is unnecessary here.
# * Stage 1 dtype: float32 — BigVGAN vocoder weights are fp32; the default
# bf16 triggers mat1/mat2 dtype mismatch during warmup.
# * Stage 1 enforce_eager: true — the code2wav forward pass performs a
# GPU→CPU sync (int(code.flatten()[0]) in covo_audio.py) which is
# illegal during CUDA graph capture.
async_chunk: false
trust_remote_code: true
stages:
- stage_id: 0
max_num_seqs: 1
gpu_memory_utilization: 0.5
mm_processor_cache_gb: 0
devices: "0"
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 2048
seed: 42
repetition_penalty: 1.1
- stage_id: 1
max_num_seqs: 1
gpu_memory_utilization: 0.2
enforce_eager: true
dtype: float32
max_num_batched_tokens: 2048
max_model_len: 2048
mm_processor_cache_gb: 0
devices: "0"
default_sampling_params:
temperature: 0.0
top_p: 1.0
top_k: -1
max_tokens: 2048
seed: 42
repetition_penalty: 1.1
@@ -64,6 +64,7 @@ _QWEN3_TTS_MODEL_STAGES = {"qwen3_tts"}
_FISH_TTS_MODEL_STAGES = {"fish_speech_slow_ar"}
_COSYVOICE3_TTS_MODEL_STAGES = {"cosyvoice3_talker"}
_OMNIVOICE_TTS_MODEL_STAGES = {"omnivoice_generator"}
_COVO_AUDIO_MODEL_STAGES = {"fused_thinker_talker"}
_VOXCPM_TTS_MODEL_STAGES = {"latent_generator", "vae"}
_VOXCPM2_TTS_MODEL_STAGES = {"latent_generator"}
_MING_TTS_MODEL_STAGES = {"ming_tts"}
@@ -74,6 +75,7 @@ _TTS_MODEL_STAGES: set[str] = (
| _FISH_TTS_MODEL_STAGES
| _COSYVOICE3_TTS_MODEL_STAGES
| _OMNIVOICE_TTS_MODEL_STAGES
| _COVO_AUDIO_MODEL_STAGES
| _VOXCPM_TTS_MODEL_STAGES
| _VOXCPM2_TTS_MODEL_STAGES
| _MING_TTS_MODEL_STAGES
@@ -325,6 +327,7 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
and getattr(getattr(self._tts_stage, "engine_args", None), "model_stage", None) == "fish_speech_slow_ar"
)
self._fish_speech_tokenizer = None
self._covo_audio_tokenizer = None
self._is_cosyvoice3 = (
self._tts_stage is not None
@@ -490,6 +493,10 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
return "cosyvoice3"
if model_stage in _OMNIVOICE_TTS_MODEL_STAGES:
return "omnivoice"
if model_stage in _COVO_AUDIO_MODEL_STAGES:
model_arch = getattr(self._tts_stage.engine_args, "model_arch", None)
if model_arch and "CovoAudio" in model_arch:
return "covo_audio"
if model_stage in (_VOXCPM_TTS_MODEL_STAGES | _VOXCPM2_TTS_MODEL_STAGES):
has_vae_stage = any(
getattr(getattr(stage, "engine_args", None), "model_stage", None) == "vae"
@@ -1874,6 +1881,41 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
"mm_processor_kwargs": mm_kwargs,
}
# ---- Covo-Audio helpers ----
def _build_covo_audio_prompt(
self,
request: OpenAICreateSpeechRequest,
) -> dict[str, Any]:
"""Build a chat-style prompt for Covo-Audio-Chat.
Covo-Audio requires a specific system prompt that instructs the model
to interleave text and audio tokens in its output. We render the
messages through the chat template and pass prompt_token_ids so that
the engine does not need to re-tokenize.
"""
from transformers import AutoTokenizer
from vllm_omni.model_executor.models.covo_audio.prompt_utils import (
build_covo_audio_prompt_token_ids,
)
if self._covo_audio_tokenizer is None:
model_name = self.engine_client.model_config.model
try:
self._covo_audio_tokenizer = AutoTokenizer.from_pretrained(
model_name,
trust_remote_code=True,
)
except Exception as exc:
raise RuntimeError(f"Failed to load Covo-Audio tokenizer from '{model_name}': {exc}") from exc
prompt_ids = build_covo_audio_prompt_token_ids(
self._covo_audio_tokenizer,
request.input,
)
return {"prompt_token_ids": prompt_ids}
def _apply_cosyvoice3_dynamic_tokens(
self,
sampling_params_list: list,
@@ -2014,6 +2056,9 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
prompt["lang"] = request.language
if request.instructions:
prompt["instruct"] = request.instructions
elif self._tts_model_type == "covo_audio":
prompt = self._build_covo_audio_prompt(request)
tts_params = {}
elif self._tts_model_type == "voxcpm2":
# voxcpm2 doesn't use `_apply_uploaded_speaker` because the prompt builder needs the
# raw waveform tuple for prefill-length accounting, not a base64 data URL.
@@ -2100,6 +2145,8 @@ class OmniOpenAIServingSpeech(OpenAIServing, AudioMixin):
request_id = request_id or f"speech-{random_uuid()}"
if self._is_fish_speech:
model_type = "fish_speech"
elif self._tts_model_type == "covo_audio":
model_type = "covo_audio"
elif self._tts_model_type == "voxtral_tts":
model_type = "voxtral_tts"
elif self._tts_model_type == "cosyvoice3":
Whitespace-only changes.
@@ -0,0 +1,72 @@
# Copyright 2026 Tencent.
from transformers.configuration_utils import PretrainedConfig
# audio_token_index: token IDs >= this value are audio codes.
# Matches config.audio_token_index and len(tokenizer) = 151671.
# Note: the LLM total vocab_size is 168055 (includes audio tokens), which
# is different from this boundary value.
COVO_AUDIO_TOKEN_INDEX = 151671
class CovoAudioCode2WavConfig(PretrainedConfig):
model_type = "covo_audio_code2wav"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.wav_input_sr = 24000
self.n_speakers = 1
self.n_styles = 1
self.token2latent = {
"upsample_factor": 4,
"token_vocab_size": 16384,
"token_input_dim": 512,
"z_dim": 64,
"spkr_embed_dim": 1024,
"spkr_mask_ratio": 0.0,
"bert_mask_rate0": 0.7,
"bert_mask_rate1": 1.0,
"random_maskrate": 0.3,
"cfg_dropout": 0.2,
"sigma": 1e-05,
"transformer": {
"num_layers": 12,
"hidden_size": 1024,
"ffn_hidden_size": 4096,
"num_heads": 16,
"modulation": True,
"alibi_bias": False,
"rotary_bias": True,
"qk_norm": False,
"max_position_embeddings": 4096,
"attn_dropout": 0.0,
"dropout": 0.1,
},
}
self.wavegan = {
"type": "BigVGANFlowVAE",
"upsample_rates": [5, 3, 2, 2, 2, 2],
"upsample_kernel_sizes": [10, 6, 4, 4, 4, 4],
"upsample_initial_channel": 1536,
"resblock": "1",
"resblock_kernel_sizes": [3, 7, 11],
"resblock_dilation_sizes": [[1, 3, 5], [1, 3, 5], [1, 3, 5]],
"downsample_rates": [2, 2, 2, 2, 3, 5],
"downsample_channels": [12, 24, 48, 96, 192, 384, 768],
"activation": "snakebeta",
"snake_logscale": True,
"latent_dim": 64,
"use_flow": True,
"use_vae": True,
"kl_weight": 5,
"causal": True,
"flow_hidden_channels": 256,
}
self.inference = {
"s_steps": 10,
"cfg_alpha": 1.0,
"dynamic_the": False,
"rescale_logits": False,
}
@@ -0,0 +1,426 @@
# Copyright 2026 Tencent.
from collections.abc import Iterable, Mapping, Sequence
from typing import Any
import numpy as np
import torch
from torch import nn
from transformers import BatchFeature, WhisperFeatureExtractor
from vllm.config import VllmConfig
from vllm.config.multimodal import BaseDummyOptions
from vllm.inputs import MultiModalDataDict
from vllm.model_executor.models import SupportsPP
from vllm.model_executor.models.interfaces import SupportsMultiModal
from vllm.model_executor.models.utils import init_vllm_registered_model
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import MultiModalFieldConfig
from vllm.multimodal.parse import MultiModalDataItems, MultiModalDataParser
from vllm.multimodal.processing import (
BaseDummyInputsBuilder,
BaseMultiModalProcessor,
BaseProcessingInfo,
ProcessorInputs,
PromptReplacement,
PromptUpdateDetails,
)
from vllm.sequence import IntermediateTensors
from vllm.v1.sample.metadata import SamplingMetadata
from vllm_omni.model_executor.custom_process_mixin import CustomProcessMixin
from vllm_omni.model_executor.models.output_templates import OmniOutput
from vllm_omni.model_executor.models.utils import add_prefix_to_loaded_weights
# Max tokens per audio: Whisper 30s → 3000 mel frames → encoder 2x + adapter 8x = 16x → 188
MAX_AUDIO_TOKENS = 188
def _calc_audio_num_tokens(num_samples: int, sample_rate: int) -> int:
"""Calculate the number of audio tokens for a given audio.
Matches the original Covo-Audio calc_seq_len: 4 rounds of stride-2
downsampling (Whisper encoder 2x + AudioAdapter 3-layer 8x = 16x total).
"""
centiseconds = num_samples * 100 // sample_rate
seq_len = centiseconds
for _ in range(4):
seq_len = (seq_len + 1) // 2
return seq_len
class CovoAudioProcessingInfo(BaseProcessingInfo):
def get_hf_config(self):
return self.ctx.get_hf_config()
def get_hf_processor(self, **kwargs):
return None
def get_feature_extractor(self, **kwargs):
return WhisperFeatureExtractor(
feature_size=128,
sampling_rate=16000,
)
def get_data_parser(self) -> MultiModalDataParser:
return MultiModalDataParser(
target_sr=16000,
)
def get_supported_mm_limits(self) -> Mapping[str, int | None]:
return {"audio": None}
def get_mm_max_tokens_per_item(self, seq_len, mm_counts):
return {"audio": MAX_AUDIO_TOKENS}
class CovoAudioDummyInputsBuilder(BaseDummyInputsBuilder[CovoAudioProcessingInfo]):
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
num_audios = mm_counts.get("audio", 0)
return (" <|begofcAUDIO|><|cAUDIO|><|endofcAUDIO|>" * num_audios).strip()
def get_dummy_mm_data(
self,
seq_len: int,
mm_counts: Mapping[str, int],
mm_options: Mapping[str, BaseDummyOptions] | None = None,
) -> MultiModalDataDict:
num_audios = mm_counts.get("audio", 0)
if num_audios == 0:
return {}
# 30s at 16kHz = Whisper's max input length, ensures profile_run
# allocates memory for the worst-case MAX_AUDIO_TOKENS (188) tokens.
dummy_audio = np.zeros((16000 * 30,), dtype=np.float32)
return {"audio": [(dummy_audio, 16000)] * num_audios}
def get_dummy_processor_inputs(
self,
seq_len: int,
mm_counts: Mapping[str, int],
mm_options: Mapping[str, BaseDummyOptions] | None = None,
) -> ProcessorInputs:
dummy_text = self.get_dummy_text(mm_counts)
dummy_mm_data = self.get_dummy_mm_data(seq_len, mm_counts, mm_options)
dummy_mm_items = self.info.parse_mm_data(dummy_mm_data)
return ProcessorInputs(
prompt=dummy_text,
mm_data_items=dummy_mm_items,
)
class CovoAudioMultiModalProcessor(BaseMultiModalProcessor[CovoAudioProcessingInfo]):
def _hf_processor_applies_updates(self, prompt_text, mm_items, hf_processor_mm_kwargs, tokenization_kwargs) -> bool:
return False
def _call_hf_processor(
self, prompt: str, mm_data: Mapping[str, object], mm_kwargs: Mapping[str, Any], tok_kwargs: Mapping[str, object]
) -> BatchFeature:
tokenizer = self.info.get_tokenizer()
feature_extractor = self.info.get_feature_extractor()
if isinstance(prompt, str):
prompt_ids = tokenizer.encode(prompt)
else:
prompt_ids = prompt
audios = mm_data.get("audios", []) or mm_data.get("audio", [])
if not audios:
return BatchFeature(dict(input_ids=[prompt_ids]), tensor_type="pt")
raw_audios = []
num_tokens_list = []
for item in audios:
if isinstance(item, tuple):
audio_array, _sr = item
else:
audio_array = item
raw_audios.append(audio_array)
# MultiModalDataParser resamples to target_sr=16000, so always
# compute token count at 16 kHz regardless of the original sr.
num_tokens_list.append(_calc_audio_num_tokens(len(audio_array), 16000))
features = feature_extractor(
raw_audios,
sampling_rate=16000,
return_tensors="pt",
)
return BatchFeature(
data={
"input_ids": torch.tensor([prompt_ids], dtype=torch.int64),
"audio_features": features["input_features"],
"audio_num_tokens": torch.tensor(num_tokens_list, dtype=torch.int64),
},
tensor_type=None,
)
def _get_mm_fields_config(
self,
hf_inputs: BatchFeature,
hf_processor_mm_kwargs: Mapping[str, object],
) -> Mapping[str, MultiModalFieldConfig]:
return {
"audio_features": MultiModalFieldConfig.batched("audio"),
"audio_num_tokens": MultiModalFieldConfig.batched("audio"),
}
def _get_prompt_updates(
self,
mm_items: MultiModalDataItems,
hf_processor_mm_kwargs: Mapping[str, object],
out_mm_kwargs,
) -> Sequence[PromptReplacement]:
tokenizer = self.info.get_tokenizer()
vocab = tokenizer.get_vocab()
audio_token_id = vocab["<|cAUDIO|>"]
out_mm_data = out_mm_kwargs.get_data()
def get_replacement(item_idx: int):
num_tokens_tensor = out_mm_data.get("audio_num_tokens")
if num_tokens_tensor is not None:
num_tokens = int(num_tokens_tensor[item_idx])
else:
num_tokens = MAX_AUDIO_TOKENS
return PromptUpdateDetails.select_token_id(
[audio_token_id] * num_tokens,
embed_token_id=audio_token_id,
)
return [
PromptReplacement("audio", "<|cAUDIO|>", get_replacement),
]
@MULTIMODAL_REGISTRY.register_processor(
CovoAudioMultiModalProcessor,
info=CovoAudioProcessingInfo,
dummy_inputs=CovoAudioDummyInputsBuilder,
)
class CovoAudioForConditionalGeneration(
nn.Module,
SupportsPP,
SupportsMultiModal,
CustomProcessMixin,
):
requires_raw_input_tokens: bool = True
@classmethod
def get_placeholder_str(cls, modality: str, i: int) -> str | None:
if modality.startswith("audio"):
return "<|begofcAUDIO|><|cAUDIO|><|endofcAUDIO|>"
return None
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
self.has_preprocess = False
self.have_multimodal_outputs = True
self.vllm_config = vllm_config
self.model_stage = vllm_config.model_config.model_stage
if self.model_stage == "fused_thinker_talker":
self.has_preprocess = True
self.set_custom_preprocess(self.fused_thinker_talker_preprocess)
self.fused_thinker_talker = init_vllm_registered_model(
vllm_config=vllm_config,
hf_config=vllm_config.model_config.hf_config,
architectures=["CovoAudioLLMModel"],
)
self.code2wav = None
self.model = self.fused_thinker_talker
elif self.model_stage == "code2wav":
self.code2wav = init_vllm_registered_model(
vllm_config=vllm_config,
architectures=["CovoAudioCode2WavModel"],
)
self.fused_thinker_talker = None
self.model = self.code2wav
else:
raise ValueError("Invalid model stage")
self.make_empty_intermediate_tensors = (
self.fused_thinker_talker.make_empty_intermediate_tensors
if self.model_stage == "fused_thinker_talker"
else lambda *args, **kwargs: None
)
def fused_thinker_talker_preprocess(
self,
input_ids: torch.Tensor | None,
input_embeds: torch.Tensor | None,
**info_dict: dict,
):
if input_embeds is None and input_ids is not None:
input_embeds = self.fused_thinker_talker.embed_input_ids(input_ids)
return input_ids, input_embeds, info_dict
def embed_multimodal(self, **kwargs: object):
if self.model_stage == "fused_thinker_talker":
return self.fused_thinker_talker.embed_multimodal(**kwargs)
return None
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal: bool = False,
) -> torch.Tensor:
if self.model_stage == "code2wav":
hidden_size = self.vllm_config.model_config.get_hidden_size()
return torch.zeros(
input_ids.numel(),
hidden_size,
dtype=self.vllm_config.model_config.dtype,
device=input_ids.device,
)
return self.model.embed_input_ids(input_ids, multimodal_embeddings, is_multimodal=is_multimodal)
def compute_logits(
self,
hidden_states: torch.Tensor,
sampling_metadata: SamplingMetadata | None = None,
) -> torch.Tensor | None:
if isinstance(hidden_states, OmniOutput):
hidden_states = hidden_states.text_hidden_states
return self.model.compute_logits(hidden_states)
@staticmethod
def _module_device(module: nn.Module) -> torch.device:
try:
return next(module.parameters()).device
except StopIteration:
# No parameters; fall back to buffers or cpu
for _, buf in module.named_buffers(recurse=True):
return buf.device
return torch.device("cpu")
def generate_codes(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
**kwargs: object,
):
llm_dev = self._module_device(self.fused_thinker_talker)
# Fallback: dummy input_ids matching embedding length
if input_ids is None:
input_ids = torch.zeros(inputs_embeds.shape[1], dtype=torch.long, device=llm_dev).unsqueeze(
0
) # (1, seq_len)
# Thinker (ensure inputs on thinker's device)
if input_ids is not None and input_ids.device != llm_dev:
input_ids = input_ids.to(llm_dev)
if positions is not None and positions.device != llm_dev:
positions = positions.to(llm_dev)
if inputs_embeds is not None and inputs_embeds.device != llm_dev:
inputs_embeds = inputs_embeds.to(llm_dev)
# Run llm
llm_output = self.fused_thinker_talker(
input_ids=input_ids,
positions=positions,
intermediate_tensors=intermediate_tensors,
inputs_embeds=inputs_embeds,
**kwargs,
)
# CovoAudio interleaves audio tokens directly in the LLM output
# token stream; the stage input processor extracts them from
# token_ids, so there is no separate codec output here.
return None, llm_output
def generate_audio(self, code: torch.Tensor):
code2wav_dev = self._module_device(self.code2wav)
if isinstance(code, torch.Tensor):
code_tensor = code.to(dtype=torch.long, device=code2wav_dev)
else:
code_tensor = torch.as_tensor(code, dtype=torch.long, device=code2wav_dev)
if code_tensor.ndim == 2 and code_tensor.shape[0] == 1:
code_tensor = code_tensor.squeeze(0)
with torch.inference_mode():
audio_tensor = self.code2wav(input_ids=code_tensor)
return audio_tensor
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights for all components of the omni model."""
loaded_weights: set[str] = set()
# code2wav weights are loaded in its __init__ via torch.load;
# skip iterating safetensors entirely for this stage.
if self.code2wav:
c2w_loaded = self.code2wav.load_weights(iter([]))
c2w_loaded = add_prefix_to_loaded_weights(c2w_loaded, "code2wav")
loaded_weights.update(c2w_loaded)
return loaded_weights
llm_weights = ((k, v) for k, v in weights if k.startswith(("llm", "encoder", "audio_adapter")))
if self.fused_thinker_talker:
llm_loaded = self.fused_thinker_talker.load_weights(llm_weights)
llm_loaded = add_prefix_to_loaded_weights(llm_loaded, "fused_thinker_talker")
loaded_weights.update(llm_loaded)
return loaded_weights
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
sampling_metadata: SamplingMetadata | None = None,
**kwargs: object,
) -> torch.Tensor | IntermediateTensors | OmniOutput:
"""Route forward to the active stage.
- fused_thinker_talker: run Whisper + LLM → text hidden states
(audio tokens are interleaved in the output token stream).
- code2wav: run BigVGAN vocoder on audio codes → waveform.
"""
if self.model_stage == "fused_thinker_talker":
_, text_hidden_states = self.generate_codes(
input_ids=input_ids,
positions=positions,
inputs_embeds=inputs_embeds,
intermediate_tensors=intermediate_tensors,
**kwargs,
)
return OmniOutput(
text_hidden_states=text_hidden_states.reshape(-1, text_hidden_states.shape[-1]),
multimodal_outputs={},
)
if self.model_stage == "code2wav":
code = (
input_ids
if input_ids is not None
else torch.zeros(
inputs_embeds.shape[0],
dtype=torch.long,
device=inputs_embeds.device,
)
)
# Text-only response: stage processor sends a single sentinel [-1]
# token. Skip BigVGAN inference and return a short silence so the
# serving layer produces a valid WAV instead of noise.
if code.numel() == 1 and int(code.flatten()[0]) == -1:
# 0.1s of silence at 24 kHz
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": torch.zeros(1, 2400, device=code.device)},
)
audio_tensor = self.generate_audio(code)
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={
"model_outputs": audio_tensor.reshape(1, -1) if audio_tensor is not None else audio_tensor
},
)
@@ -0,0 +1,126 @@
# Copyright 2026 Tencent.
import os
from collections.abc import Iterable
import numpy as np
import torch
from torch import nn
from vllm.config import VllmConfig
from vllm.model_executor.models import SupportsPP
from vllm.v1.outputs import SamplerOutput
from vllm.v1.sample.metadata import SamplingMetadata
from vllm.v1.sample.sampler import Sampler
from .config_covo_audio import CovoAudioCode2WavConfig
from .token2wav import JsonHParams, Token2WavDecoder
class CovoAudioCode2WavForConditionalGeneration(nn.Module, SupportsPP):
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
self.vllm_config = vllm_config
self.config = vllm_config.model_config.hf_config
model_name = vllm_config.model_config.model
if os.path.isdir(model_name):
model_path = model_name
else:
from huggingface_hub import snapshot_download
model_path = snapshot_download(model_name)
token2wav_path = os.path.join(model_path, "token2wav")
code2wav_config = CovoAudioCode2WavConfig()
model_config = JsonHParams(
**{
"token2latent": code2wav_config.token2latent,
"wavegan": code2wav_config.wavegan,
"wav_input_sr": code2wav_config.wav_input_sr,
"global_mean_var": os.path.join(token2wav_path, "global_mean_var.npy"),
}
)
self.decoder = Token2WavDecoder(model_config)
ckpt = torch.load(
os.path.join(token2wav_path, "model.pt"),
map_location="cpu",
weights_only=True,
)
self.decoder.load_state_dict(ckpt)
self.decoder.eval()
self.infer_config = code2wav_config.inference
self._decoder_moved = False
self._sampler = Sampler()
# Load bundled speaker prompt files for voice timbre control.
prompt_dir = os.path.join(os.path.dirname(__file__), "speaker_prompt")
self._prompt_token = torch.from_numpy(np.load(os.path.join(prompt_dir, "prompt_token.npy"))).long()
self._prompt_latent = torch.from_numpy(np.load(os.path.join(prompt_dir, "prompt_latent.npy"))).float()
self._spkr_embed = torch.from_numpy(np.load(os.path.join(prompt_dir, "speaker_embed.npy"))).float()
def _ensure_on_device(self, device: torch.device) -> None:
"""Move decoder and prompt tensors to *device* once."""
if self._decoder_moved:
return
self.decoder = self.decoder.to(device)
self._prompt_token = self._prompt_token.to(device)
self._prompt_latent = self._prompt_latent.to(device)
self._spkr_embed = self._spkr_embed.to(device)
self._decoder_moved = True
def forward(self, input_ids: torch.Tensor, **kwargs):
self._ensure_on_device(input_ids.device)
data = {
"target_token": input_ids.unsqueeze(0),
"sample_rate": 24000,
"prompt_token": self._prompt_token,
"prompt_latent": self._prompt_latent,
"spkr_embed": self._spkr_embed,
}
audio = self.decoder.inference(data, **self.infer_config)
return audio
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal: bool = False,
) -> torch.Tensor:
hidden_size = self.vllm_config.model_config.get_hidden_size()
return torch.zeros(
input_ids.numel(),
hidden_size,
dtype=self.vllm_config.model_config.dtype,
device=input_ids.device,
)
def compute_logits(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor | None:
if hidden_states.numel() == 0:
return None
batch_size = hidden_states.shape[0] if hidden_states.ndim > 0 else 1
vocab_size = self.config.llm_config.vocab_size
return torch.zeros(
(batch_size, vocab_size),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
def sample(
self,
logits: torch.Tensor,
sampling_metadata: SamplingMetadata,
) -> SamplerOutput | None:
if logits is None or logits.numel() == 0:
return None
return self._sampler(logits=logits, sampling_metadata=sampling_metadata)
def load_weights(
self,
weights: Iterable[tuple[str, torch.Tensor]],
) -> set[str]:
# Weights are loaded in __init__ via torch.load
# Return all decoder state_dict keys so vLLM knows they are initialized
return {f"decoder.{k}" for k in self.decoder.state_dict()}
@@ -0,0 +1,152 @@
# Copyright 2026 Tencent.
from collections.abc import Iterable
import torch
from torch import nn
from transformers.models.whisper.modeling_whisper import WhisperEncoder
from vllm.config import VllmConfig
from vllm.model_executor.models.interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
from vllm.model_executor.models.utils import AutoWeightsLoader, init_vllm_registered_model, maybe_prefix
from vllm.sequence import IntermediateTensors
class DownsampleLayer(nn.Module):
"""
Downsample layer with 1D convolution and linear layers.
"""
def __init__(self, input_dim, output_dim, hidden_dim=2048):
super().__init__()
self.conv1d = nn.Conv1d(in_channels=input_dim, out_channels=input_dim, kernel_size=3, stride=2, padding=1)
self.linear1 = nn.Linear(input_dim, hidden_dim)
self.relu1 = nn.ReLU()
self.linear2 = nn.Linear(hidden_dim, output_dim)
self.relu2 = nn.ReLU()
def forward(self, x):
# x: (B, T, C)
x = x.transpose(1, 2) # -> (B, C, T)
x = self.conv1d(x) # -> (B, C, T // 2)
x = x.transpose(1, 2) # -> (B, T // 2, C)
x = self.relu1(x)
x = self.linear1(x) # -> (B, T // 2, hidden_dim)
x = self.relu2(x)
x = self.linear2(x) # -> (B, T // 2, output_dim)
return x
class AudioAdapter(nn.Module):
"""
Audio adapter with downsample layers.
"""
def __init__(self, input_dim, output_dim, downsample=8):
"""
Args:
input_dim (int): input feature dimension (number of channels)
output_dim (int): output feature dimension
downsample (int): total downsampling factor, must be a power of 2
"""
super().__init__()
assert downsample >= 2 and (downsample & (downsample - 1)) == 0, "downsample must be a power of 2"
num_layers = (
downsample.bit_length() - 1
) # calculate how many downsampling steps are needed to reach the target factor
layers = []
in_dim = input_dim
for i in range(num_layers):
is_last = i == num_layers - 1
out_dim = output_dim if is_last else input_dim
layers.append(DownsampleLayer(in_dim, out_dim))
in_dim = out_dim
self.downsample_layers = nn.ModuleList(layers)
def forward(self, x):
"""
Args:
x (Tensor): shape (B, T, C),C=input_dim
Returns:
Tensor: shape (B, T // downsample, output_dim)
"""
for layer in self.downsample_layers:
x = layer(x)
return x
class CovoAudioLLMForConditionalGeneration(nn.Module, SupportsPP, SupportsMultiModal):
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
self.llm = init_vllm_registered_model(
vllm_config=vllm_config,
prefix=maybe_prefix(prefix, "llm"),
architectures=["Qwen2ForCausalLM"],
)
self.encoder = WhisperEncoder(config.encoder_config)
self.audio_adapter = AudioAdapter(
config.whisper_feats_dim, config.llm_config.hidden_size, config.adapter_downsample
)
self.make_empty_intermediate_tensors = self.llm.make_empty_intermediate_tensors
def get_language_model(self) -> torch.nn.Module:
return self.llm
def embed_input_ids(
self,
input_ids: torch.Tensor,
multimodal_embeddings=None,
is_multimodal=None,
) -> torch.Tensor:
return super().embed_input_ids(
input_ids,
multimodal_embeddings=multimodal_embeddings,
is_multimodal=is_multimodal,
)
def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
audio_features = kwargs["audio_features"]
audio_num_tokens = kwargs.get("audio_num_tokens")
# Match encoder dtype (bfloat16) since audio_features from WhisperFeatureExtractor are float32
encoder_dtype = next(self.encoder.parameters()).dtype
audio_features = audio_features.to(dtype=encoder_dtype)
feats = self.encoder(audio_features).last_hidden_state
features = self.audio_adapter(feats)
# Truncate each audio to its actual length (Whisper pads all to 30s)
# Return a list of per-item tensors since lengths may differ.
batch_size = features.shape[0]
result = []
for i in range(batch_size):
if audio_num_tokens is not None:
n = int(audio_num_tokens[i])
result.append(features[i, :n, :])
else:
result.append(features[i])
return result
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: object,
) -> torch.Tensor | IntermediateTensors:
if intermediate_tensors is not None:
inputs_embeds = None
hidden_state = self.llm(input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds)
return hidden_state
def compute_logits(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor | None:
return self.llm.compute_logits(hidden_states)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
return loader.load_weights(weights)
@@ -0,0 +1,50 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Covo-Audio pipeline topology (frozen).
Stage 0: fused_thinker_talker — multimodal understanding + interleaved text/audio
token generation (AR).
Stage 1: code2wav — audio codes → waveform (generation).
"""
from vllm_omni.config.stage_config import (
PipelineConfig,
StageExecutionType,
StagePipelineConfig,
)
_PROC = "vllm_omni.model_executor.stage_input_processors.covo_audio"
COVO_AUDIO_PIPELINE = PipelineConfig(
model_type="covo_audio",
model_arch="CovoAudioForConditionalGeneration",
stages=(
StagePipelineConfig(
stage_id=0,
model_stage="fused_thinker_talker",
execution_type=StageExecutionType.LLM_AR,
input_sources=(),
final_output=True,
final_output_type="text",
owns_tokenizer=True,
requires_multimodal_data=True,
engine_output_type="latent",
sampling_constraints={
"detokenize": True,
"stop_token_ids": [151645],
"ignore_eos": True,
},
),
StagePipelineConfig(
stage_id=1,
model_stage="code2wav",
execution_type=StageExecutionType.LLM_GENERATION,
input_sources=(0,),
final_output=True,
final_output_type="audio",
engine_output_type="audio",
custom_process_input_func=f"{_PROC}.llm2code2wav",
sampling_constraints={"detokenize": False},
),
),
)
@@ -0,0 +1,56 @@
"""Shared Covo-Audio prompt constants and construction helpers."""
from __future__ import annotations
from typing import Any
COVO_AUDIO_INPUT_PREFIX = "<|begofcAUDIO|><|cAUDIO|><|endofcAUDIO|>"
COVO_AUDIO_SYSTEM_PROMPT = (
'你是"小腾",英文名是"Covo",由腾讯开发的AI助手。\n'
"1、请使用简洁、口语化的语言和用户聊天,"
"你的态度积极、耐心,像一位值得信赖的朋友。\n"
"2、不要使用列表或编号,避免输出网址、表情符号和复杂的公式。\n"
"3、不评价竞争对手,不发表主观政治观点,"
"针对色情类、政治类、恐怖类、歧视类、暴力类的用户问题,"
"你要妥善应对潜在的安全风险,并给出幽默,情绪安抚以及安全的劝导。\n"
"请用文本和音频进行对话,交替生成5个文本token和15个音频token,"
"音频部分使用发音人:default_female"
)
def build_covo_audio_chat_prompt(
user_content: str,
system_prompt: str = COVO_AUDIO_SYSTEM_PROMPT,
) -> str:
"""Build a raw chat-template prompt string for Covo-Audio-Chat.
Used by offline inference and tests where the prompt is passed as a
plain string (tokenization happens downstream).
"""
return (
f"<|im_start|>system\n{system_prompt}<|im_end|>\n"
f"<|im_start|>user\n{user_content}<|im_end|>\n"
f"<|im_start|>assistant\n"
)
def build_covo_audio_prompt_token_ids(
tokenizer: Any,
user_text: str,
system_prompt: str = COVO_AUDIO_SYSTEM_PROMPT,
) -> list[int]:
"""Build tokenized prompt IDs for Covo-Audio-Chat.
Used by the OpenAI-compatible serving layer where prompt_token_ids
are passed directly to the engine.
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_text},
]
return tokenizer.apply_chat_template(
messages,
tokenize=True,
add_generation_prompt=True,
)
@@ -0,0 +1,996 @@
# Copyright 2026 Tencent.
# token2wav: audio token codes -> waveform (inference only)
# Pipeline: Token -> Latent (flow matching) -> Waveform (BigVGAN)
import math
from collections import OrderedDict, namedtuple
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from torch import pow, sin
from torch.nn import Parameter
from torch.nn.utils import weight_norm
# --------------- Utilities ---------------
class JsonHParams:
def __init__(self, **kwargs):
for k, v in kwargs.items():
if isinstance(v, dict):
v = JsonHParams(**v)
if isinstance(v, str) and v.lower() in ["non", "none", "nil", "null"]:
v = None
self[k] = v
def keys(self):
return self.__dict__.keys()
def items(self):
return self.__dict__.items()
def values(self):
return self.__dict__.values()
def __len__(self):
return len(self.__dict__)
def __getitem__(self, key):
return getattr(self, key)
def __setitem__(self, key, value):
return setattr(self, key, value)
def __contains__(self, key):
return key in self.__dict__
def __repr__(self):
return self.__dict__.__repr__()
def get(self, key, default=None):
return self.__dict__.get(key, default)
def exists(val):
return val is not None
def eval_decorator(fn):
def inner(model, *args, **kwargs):
was_training = model.training
model.eval()
out = fn(model, *args, **kwargs)
model.train(was_training)
return out
return inner
def init_weights(m, mean=0.0, std=0.01):
classname = m.__class__.__name__
if classname.find("Conv") != -1:
m.weight.data.normal_(mean, std)
def get_padding(kernel_size, dilation=1):
return int((kernel_size * dilation - dilation) / 2)
def modulate(x, shift, scale):
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
def rotate_half(x):
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
@torch.autocast(enabled=False, device_type="cuda")
def apply_rotary_pos_emb(pos, t):
return t * pos.cos() + rotate_half(t) * pos.sin()
if "sinc" in dir(torch):
sinc = torch.sinc
else:
def sinc(x: torch.Tensor):
return torch.where(
x == 0, torch.tensor(1.0, device=x.device, dtype=x.dtype), torch.sin(math.pi * x) / math.pi / x
)
def kaiser_sinc_filter1d(cutoff, half_width, kernel_size):
even = kernel_size % 2 == 0
half_size = kernel_size // 2
delta_f = 4 * half_width
A = 2.285 * (half_size - 1) * math.pi * delta_f + 7.95
if A > 50.0:
beta = 0.1102 * (A - 8.7)
elif A >= 21.0:
beta = 0.5842 * (A - 21) ** 0.4 + 0.07886 * (A - 21.0)
else:
beta = 0.0
window = torch.kaiser_window(kernel_size, beta=beta, periodic=False)
if even:
time = torch.arange(-half_size, half_size) + 0.5
else:
time = torch.arange(kernel_size) - half_size
if cutoff == 0:
filter_ = torch.zeros_like(time)
else:
filter_ = 2 * cutoff * window * sinc(2 * cutoff * time)
filter_ /= filter_.sum()
return filter_.view(1, 1, kernel_size)
# --------------- Basic Layers ---------------
class Dropout(nn.Module):
def __init__(self, p: float = 0.5, inplace: bool = False, force_drop: bool = False, **kwargs):
super().__init__()
if p < 0.0 or p > 1.0:
raise ValueError(f"dropout probability has to be between 0 and 1, but got {p}")
self.p = p
self.inplace = inplace
self.force_drop = force_drop
def forward(self, x, **kwargs):
return F.dropout(x, p=self.p, training=True if self.force_drop else self.training, inplace=self.inplace)
class EmbeddingTable(nn.Embedding):
def __init__(self, num_embeddings: int, embedding_dim: int, pad_id=-1, **kwargs):
super().__init__(num_embeddings, embedding_dim, **kwargs)
nn.init.normal_(self.weight, 0.0, embedding_dim**-0.5)
self.pad_id = pad_id
self.output_dim = embedding_dim
def forward(self, x):
if self.pad_id is not None:
mask = x == self.pad_id
x = x.masked_fill(mask, 0)
outputs = super().forward(x)
if self.pad_id is not None:
outputs = outputs.masked_fill(mask.unsqueeze(-1), 0.0)
return outputs
class Linear(nn.Linear):
def __init__(
self,
in_channels: int,
out_channels: int,
bias: bool = True,
w_init_gain: str = "linear",
activation=None,
**kwargs,
):
super().__init__(in_channels, out_channels, bias=bias)
self.activation = activation if activation is not None else nn.Identity()
self.output_dim = out_channels
if w_init_gain is not None:
if isinstance(w_init_gain, str):
gain = nn.init.calculate_gain(w_init_gain)
else:
gain = w_init_gain
nn.init.xavier_uniform_(self.weight, gain=gain)
if bias:
nn.init.constant_(self.bias, 0.0)
def forward(self, x, **kwargs):
return self.activation(super().forward(x))
class Conv1d(nn.Conv1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int = 1,
stride: int = 1,
dilation: int = 1,
groups: int = 1,
padding_mode: str = "zeros",
bias: bool = True,
padding=None,
causal: bool = False,
bn: bool = False,
activation=None,
w_init_gain=None,
input_transpose: bool = False,
**kwargs,
):
self.causal = causal
if padding is None:
if causal:
padding = 0
self.left_padding = dilation * (kernel_size - 1)
else:
padding = get_padding(kernel_size, dilation)
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
dilation=dilation,
groups=groups,
padding_mode=padding_mode,
bias=bias,
)
self.in_channels = in_channels
self.transpose = input_transpose
self.bn = nn.BatchNorm1d(out_channels) if bn else nn.Identity()
self.activation = activation if activation is not None else nn.Identity()
if w_init_gain is not None:
nn.init.xavier_uniform_(self.weight, gain=nn.init.calculate_gain(w_init_gain))
def forward(self, x):
if self.transpose or x.size(1) != self.in_channels:
assert x.size(2) == self.in_channels
x = x.transpose(1, 2)
self.transpose = True
if self.causal:
x = F.pad(x.unsqueeze(2), (self.left_padding, 0, 0, 0)).squeeze(2)
outputs = self.activation(self.bn(super().forward(x)))
return outputs.transpose(1, 2) if self.transpose else outputs
class ConvTranspose1d(nn.ConvTranspose1d):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int,
stride: int = 1,
output_padding: int = 0,
groups: int = 1,
bias: bool = True,
dilation: int = 1,
padding=None,
padding_mode: str = "zeros",
causal: bool = False,
input_transpose: bool = False,
**kwargs,
):
if padding is None:
padding = 0 if causal else (kernel_size - stride) // 2
if causal:
assert padding == 0, "padding is not allowed in causal ConvTranspose1d."
assert kernel_size == 2 * stride, "kernel_size must be equal to 2*stride in Causal ConvTranspose1d."
super().__init__(
in_channels,
out_channels,
kernel_size,
stride=stride,
padding=padding,
output_padding=output_padding,
groups=groups,
bias=bias,
dilation=dilation,
padding_mode=padding_mode,
)
self.causal = causal
self.stride = stride
self.transpose = input_transpose
def forward(self, x):
if self.transpose or x.size(1) != self.in_channels:
assert x.size(2) == self.in_channels
x = x.transpose(1, 2)
self.transpose = True
x = super().forward(x)
if self.causal:
x = x[:, :, : -self.stride]
return x.transpose(1, 2) if self.transpose else x
class ConvPositionEmbed(nn.Module):
def __init__(self, hidden_size: int, kernel_size: int = 31, groups: int = 16):
super().__init__()
self.conv = Conv1d(
hidden_size, hidden_size, kernel_size, groups=groups, input_transpose=True, activation=nn.GELU()
)
def forward(self, x, mask=None):
if mask is not None:
mask = mask[..., None]
x = x.masked_fill(~mask, 0.0)
x = self.conv(x)
if mask is not None:
x = x.masked_fill(~mask, 0.0)
return x
# --------------- Anti-aliasing (BigVGAN) ---------------
class LowPassFilter1d(nn.Module):
def __init__(
self,
cutoff=0.5,
half_width=0.6,
stride: int = 1,
padding: bool = True,
padding_mode: str = "replicate",
kernel_size: int = 12,
):
super().__init__()
if cutoff < -0.0:
raise ValueError("Minimum cutoff must be larger than zero.")
if cutoff > 0.5:
raise ValueError("A cutoff above 0.5 does not make sense.")
self.kernel_size = kernel_size
self.even = kernel_size % 2 == 0
self.pad_left = kernel_size // 2 - int(self.even)
self.pad_right = kernel_size // 2
self.stride = stride
self.padding = padding
self.padding_mode = padding_mode
filter = kaiser_sinc_filter1d(cutoff, half_width, kernel_size)
self.register_buffer("filter", filter)
def forward(self, x):
_, C, _ = x.shape
if self.padding:
x = F.pad(x, (self.pad_left, self.pad_right), mode=self.padding_mode)
out = F.conv1d(x, self.filter.expand(C, -1, -1), stride=self.stride, groups=C)
return out
class UpSample1d(nn.Module):
def __init__(self, ratio=2, kernel_size=None):
super().__init__()
self.ratio = ratio
self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size
self.stride = ratio
self.pad = self.kernel_size // ratio - 1
self.pad_left = self.pad * self.stride + (self.kernel_size - self.stride) // 2
self.pad_right = self.pad * self.stride + (self.kernel_size - self.stride + 1) // 2
filter = kaiser_sinc_filter1d(cutoff=0.5 / ratio, half_width=0.6 / ratio, kernel_size=self.kernel_size)
self.register_buffer("filter", filter)
def forward(self, x):
_, C, _ = x.shape
x = F.pad(x, (self.pad, self.pad), mode="replicate")
x = self.ratio * F.conv_transpose1d(x, self.filter.expand(C, -1, -1), stride=self.stride, groups=C)
x = x[..., self.pad_left : -self.pad_right]
return x
class DownSample1d(nn.Module):
def __init__(self, ratio=2, kernel_size=None):
super().__init__()
self.ratio = ratio
self.kernel_size = int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size
self.lowpass = LowPassFilter1d(
cutoff=0.5 / ratio, half_width=0.6 / ratio, stride=ratio, kernel_size=self.kernel_size
)
def forward(self, x):
return self.lowpass(x)
class Activation1d(nn.Module):
def __init__(
self, activation, up_ratio: int = 2, down_ratio: int = 2, up_kernel_size: int = 12, down_kernel_size: int = 12
):
super().__init__()
self.up_ratio = up_ratio
self.down_ratio = down_ratio
self.act = activation
self.upsample = UpSample1d(up_ratio, up_kernel_size)
self.downsample = DownSample1d(down_ratio, down_kernel_size)
def forward(self, x):
x = self.upsample(x)
x = self.act(x)
x = self.downsample(x)
return x
# --------------- Activations ---------------
class SnakeBeta(nn.Module):
def __init__(self, in_features, alpha=1.0, alpha_trainable=True, alpha_logscale=False):
super().__init__()
self.in_features = in_features
self.alpha_logscale = alpha_logscale
if self.alpha_logscale:
self.alpha = Parameter(torch.zeros(in_features) * alpha)
self.beta = Parameter(torch.zeros(in_features) * alpha)
else:
self.alpha = Parameter(torch.ones(in_features) * alpha)
self.beta = Parameter(torch.ones(in_features) * alpha)
self.alpha.requires_grad = alpha_trainable
self.beta.requires_grad = alpha_trainable
self.no_div_by_zero = 0.000000001
def forward(self, x):
alpha = self.alpha.unsqueeze(0).unsqueeze(-1)
beta = self.beta.unsqueeze(0).unsqueeze(-1)
if self.alpha_logscale:
alpha = torch.exp(alpha)
beta = torch.exp(beta)
x = x + (1.0 / (beta + self.no_div_by_zero)) * pow(sin(x * alpha), 2)
return x
# --------------- Attention ---------------
AttentionConfig = namedtuple("AttentionConfig", ["enable_flash", "enable_math", "enable_mem_efficient"])
class RotaryEmbedding(nn.Module):
def __init__(self, dim, theta=50000):
super().__init__()
inv_freq = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))
self.register_buffer("inv_freq", inv_freq)
@property
def device(self):
return self.inv_freq.device
@torch.autocast(enabled=False, device_type="cuda")
def forward(self, t):
if not torch.is_tensor(t):
t = torch.arange(t, device=self.device)
t = t.type_as(self.inv_freq)
freqs = torch.einsum("i , j -> i j", t, self.inv_freq)
freqs = torch.cat((freqs, freqs), dim=-1)
return freqs
class MultiHeadAttention(nn.Module):
def __init__(
self,
hidden_size: int,
num_heads: int = 8,
qkv_bias: bool = False,
qk_norm: bool = False,
attn_drop: float = 0.0,
dropout: float = 0.0,
max_position_embeddings: int = 4096,
norm_layer: nn.Module = nn.LayerNorm,
rotary_bias: bool = False,
**kwargs,
):
super().__init__()
assert hidden_size % num_heads == 0, "hidden_size should be divisible by num_heads"
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
self.scale = self.head_dim**-0.5
self.max_position_embeddings = max_position_embeddings
self.rotary_bias = rotary_bias
self.q_proj = Linear(hidden_size, hidden_size, bias=qkv_bias)
self.k_proj = Linear(hidden_size, hidden_size, bias=qkv_bias)
self.v_proj = Linear(hidden_size, hidden_size, bias=qkv_bias)
self.q_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
self.k_norm = norm_layer(self.head_dim) if qk_norm else nn.Identity()
self.attn_drop = Dropout(attn_drop)
self.o_proj = Linear(hidden_size, hidden_size)
self.o_dropout = Dropout(dropout)
self.cpu_config = AttentionConfig(True, True, True)
device_properties = torch.cuda.get_device_properties(torch.device("cuda"))
if device_properties.major == 8 and device_properties.minor == 0:
self.cuda_config = AttentionConfig(True, True, True)
else:
self.cuda_config = AttentionConfig(False, True, True)
if self.rotary_bias:
self.rotary = RotaryEmbedding(self.head_dim)
def forward(self, q, k=None, v=None, mask=None):
k = k or q
v = v or q
B, L, C = q.shape
B, S, C = v.shape
if mask is not None:
if mask.ndim == 2:
assert L == S
mask = rearrange(mask, "b j -> b 1 1 j")
mask = mask.expand(-1, self.num_heads, L, -1)
elif mask.ndim == 3:
assert mask.size(1) == L and mask.size(2) == S
mask = mask.unsqueeze(1).expand(-1, self.num_heads, -1, -1)
q, k, v = self.q_proj(q), self.k_proj(k), self.v_proj(v)
q = rearrange(q, "b n (h d) -> b h n d", h=self.num_heads)
k = rearrange(k, "b n (h d) -> b h n d", h=self.num_heads)
v = rearrange(v, "b n (h d) -> b h n d", h=self.num_heads)
q, k = self.q_norm(q), self.k_norm(k)
config = self.cuda_config if q.is_cuda else self.cpu_config
attn_bias = torch.zeros(B, self.num_heads, L, S, dtype=q.dtype, device=q.device)
if self.rotary_bias:
if L == S:
rotary_emb = self.rotary(L)
q, k = map(lambda x: apply_rotary_pos_emb(rotary_emb, x), (q, k))
else:
q_rotary_emb = self.rotary(L)
k_rotary_emb = self.rotary(S)
q = apply_rotary_pos_emb(q_rotary_emb, q)
k = apply_rotary_pos_emb(k_rotary_emb, k)
if mask is not None:
attn_bias.masked_fill_(mask.logical_not(), float("-inf"))
with torch.backends.cuda.sdp_kernel(**config._asdict()):
out = F.scaled_dot_product_attention(
q, k, v, attn_mask=attn_bias, dropout_p=self.attn_drop.p if self.training else 0.0
)
out = rearrange(out, "b h n d -> b n (h d)")
out = self.o_dropout(self.o_proj(out))
return out
# --------------- DiT Modules ---------------
class TimestepEmbedder(nn.Module):
def __init__(self, hidden_size, frequency_embedding_size=256):
super().__init__()
self.mlp = nn.Sequential(
Linear(frequency_embedding_size, hidden_size, bias=True),
nn.SiLU(),
Linear(hidden_size, hidden_size, bias=True),
)
self.frequency_embedding_size = frequency_embedding_size
@staticmethod
def timestep_embedding(t, dim, max_period=10000):
half = dim // 2
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half).to(
device=t.device
)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
return embedding
def forward(self, t):
t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
t_emb = self.mlp(t_freq)
return t_emb
class FinalLayer(nn.Module):
def __init__(self, hidden_size, output_size):
super().__init__()
self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True))
self.norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.linear = Linear(hidden_size, output_size, bias=True)
def forward(self, x, c, mask=None):
shift, scale = self.adaLN_modulation(c).chunk(2, dim=1)
x = modulate(self.norm(x), shift, scale)
x = self.linear(x)
return x
# --------------- Transformer ---------------
class Mlp(nn.Module):
def __init__(self, hidden_size, ffn_hidden_size=4096, act_layer=nn.GELU, dropout=0.0, **kwargs):
super().__init__()
self.fc1 = Linear(hidden_size, ffn_hidden_size)
self.act = act_layer()
self.fc2 = Linear(ffn_hidden_size, hidden_size)
self.drop = Dropout(dropout)
def forward(self, x, **kwargs):
x = self.fc1(x)
x = self.act(x)
x = self.drop(x)
x = self.fc2(x)
x = self.drop(x)
return x
class TransformerBlock(nn.Module):
def __init__(
self,
attention: nn.Module,
ffn: nn.Module,
hidden_size: int = 1024,
modulation: bool = False,
eps: float = 1e-6,
**kwargs,
):
super().__init__()
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=not modulation, eps=eps)
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=not modulation, eps=eps)
self.attn = attention
self.ffn = ffn
self.modulation = modulation
if modulation:
self.modulation_layer = nn.Sequential(nn.SiLU(), Linear(hidden_size, 6 * hidden_size, bias=True))
nn.init.constant_(self.modulation_layer[-1].weight, 0.0)
nn.init.constant_(self.modulation_layer[-1].bias, 0.0)
def forward(self, x, condition=None, mask=None):
if condition is None:
assert not self.modulation, "Without global condition, must set modulation to False"
else:
assert self.modulation, "With global condition, must set modulation to True"
shift_attn, scale_attn, gate_attn, shift_ffn, scale_ffn, gate_ffn = self.modulation_layer(condition).chunk(
6, dim=1
)
if condition is not None:
x = x + gate_attn.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_attn, scale_attn), mask=mask)
else:
x = x + self.attn(self.norm1(x), mask=mask)
if condition is not None:
x = x + gate_ffn.unsqueeze(1) * self.ffn(modulate(self.norm2(x), shift_ffn, scale_ffn))
else:
x = x + self.ffn(self.norm2(x), mask=mask)
return x
# --------------- Flow Matching: Token -> Latent ---------------
class Token2latentFlowMatching(nn.Module):
def __init__(self, config):
super().__init__()
self.config = config
self.model_dim = config.transformer.hidden_size
self.token_input_dim = config.get("token_input_dim", self.model_dim)
self.target_dim = config.z_dim
self.spkr_embed_dim = config.get("spkr_embed_dim", 512)
self.cond_proj = Linear(self.model_dim + self.spkr_embed_dim, self.model_dim)
self.token_pad_id = -1
if config.upsample_factor > 1:
self.token_proj = nn.Sequential(
Linear(self.token_input_dim, self.model_dim, bias=True),
ConvTranspose1d(
self.model_dim,
self.model_dim,
stride=config.upsample_factor,
kernel_size=config.upsample_factor * 2,
),
)
else:
self.token_proj = Linear(self.token_input_dim, self.model_dim)
self.transformer_input_proj = Linear(self.model_dim + self.target_dim * 2, self.model_dim)
self.time_embedder = TimestepEmbedder(self.model_dim)
self.conv_embed = ConvPositionEmbed(hidden_size=self.model_dim, kernel_size=31, groups=16)
self.blocks = nn.ModuleList()
for _ in range(config.transformer.num_layers):
attn_block = MultiHeadAttention(**config.transformer)
ffn_block = Mlp(act_layer=lambda: nn.GELU(approximate="tanh"), **config.transformer)
self.blocks.append(TransformerBlock(attn_block, ffn_block, **config.transformer))
self.output_layer = FinalLayer(config.transformer.hidden_size, self.target_dim)
def cond_mask_spkr_embed(self, x, spkr_embed):
b, device = x.size(0), x.device
if not exists(spkr_embed):
spkr_embed = torch.zeros(b, self.spkr_embed_dim, device=device, dtype=x.dtype)
return spkr_embed
def vectorfield_forward(self, inputs, times, self_attn_mask, g_cond=None):
t = self.time_embedder(times)
if g_cond is None:
c = t
else:
cond_inp = torch.cat([t, g_cond], dim=-1)
c = self.cond_proj(cond_inp)
ut = self.transformer_input_proj(inputs)
ut = self.conv_embed(ut, mask=self_attn_mask) + ut
for block in self.blocks:
ut = block(ut, c, mask=self_attn_mask)
ut = self.output_layer(ut, c)
return ut
@eval_decorator
@torch.no_grad()
def inference(
self,
*,
token: torch.Tensor,
prefix_target: torch.Tensor | None = None,
spkr_embed: torch.Tensor | None = None,
s_steps: int | None = 10,
cfg_alpha: float | None = 2.0,
rescale_logits: bool = False,
**kwargs,
):
b, device = token.size(0), token.device
assert b == 1, "Only support batch_size == 1 when inference"
token = self.token_proj(token)
ge = self.cond_mask_spkr_embed(token, spkr_embed)
tgt_lens = token.size(1)
latent_cond = torch.zeros(b, tgt_lens, self.target_dim).to(device)
if prefix_target is not None:
latent_cond[:, : prefix_target.size(1), :] = prefix_target
sample, trajectory = self.sample(
tokens=token, audio=latent_cond, steps=s_steps, alpha=cfg_alpha, g_cond=ge, rescale_logits=rescale_logits
)
return sample
def sample(self, tokens, audio, steps, alpha=None, g_cond=None, rescale_logits=False):
noise = torch.randn([audio.size(0), audio.shape[1], self.target_dim], device=audio.device)
times = torch.linspace(0, 1, steps, device=audio.device)
def solver(t, z):
if alpha is None:
output = torch.cat([tokens, audio, z], dim=-1)
return self.vectorfield_forward(inputs=output, times=t.unsqueeze(0), self_attn_mask=None, g_cond=g_cond)
tokens_empty = torch.zeros(*audio.shape[:2], self.model_dim, device=tokens.device, dtype=tokens.dtype)
audio_empty = audio
tokens_t = torch.cat([tokens_empty, tokens], dim=0)
audio_t = torch.cat([audio_empty, audio], dim=0)
audio_noizy_t = torch.cat([z, z], dim=0)
t_t = torch.stack([t, t], dim=0)
c = g_cond
if g_cond is not None:
c = torch.cat([g_cond, g_cond], dim=0)
output = torch.cat([tokens_t, audio_t, audio_noizy_t], dim=-1)
predicted_mix = self.vectorfield_forward(inputs=output, times=t_t, self_attn_mask=None, g_cond=c)
predicted_conditioned = predicted_mix[1].unsqueeze(0)
predicted_unconditioned = predicted_mix[0].unsqueeze(0)
prediction = predicted_conditioned + (predicted_conditioned - predicted_unconditioned) * alpha
prediction_rescaled = predicted_conditioned.std() * (prediction / prediction.std())
if rescale_logits:
return prediction_rescaled
else:
return prediction
try:
from torchdiffeq import odeint
except ImportError:
raise ImportError("Covo-Audio code2wav requires `torchdiffeq`. Install it with: pip install torchdiffeq")
trajectory = odeint(solver, noise, times, atol=1e-5, rtol=1e-5, method="midpoint")
return trajectory[-1], trajectory
class Token2latentFlowMatchingWithEmbed(Token2latentFlowMatching):
def __init__(self, config, **kwargs):
super().__init__(config)
self.vocab_size = config.token_vocab_size
self.token_embedding = EmbeddingTable(
num_embeddings=self.vocab_size, embedding_dim=self.token_input_dim, pad_id=self.token_pad_id
)
def inference(
self, *, token, prefix_target=None, spkr_embed=None, s_steps=10, cfg_alpha=2, rescale_logits=False, **kwargs
):
token = self.token_embedding(token)
return super().inference(
token=token,
prefix_target=prefix_target,
spkr_embed=spkr_embed,
s_steps=s_steps,
cfg_alpha=cfg_alpha,
rescale_logits=rescale_logits,
**kwargs,
)
# --------------- Vocoder: Latent -> Waveform (BigVGAN) ---------------
class AMPBlock1(torch.nn.Module):
def __init__(self, h, channels, kernel_size=3, dilation=(1, 3, 5), causal=True):
super().__init__()
self.h = h
self.convs1 = nn.ModuleList(
[
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[0], causal=causal)),
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[1], causal=causal)),
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=dilation[2], causal=causal)),
]
)
self.convs1.apply(init_weights)
self.convs2 = nn.ModuleList(
[
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, causal=causal)),
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, causal=causal)),
weight_norm(Conv1d(channels, channels, kernel_size, 1, dilation=1, causal=causal)),
]
)
self.convs2.apply(init_weights)
self.num_layers = len(self.convs1) + len(self.convs2)
self.activations = nn.ModuleList(
[
Activation1d(activation=SnakeBeta(channels, alpha_logscale=h.snake_logscale))
for _ in range(self.num_layers)
]
)
def forward(self, x):
acts1, acts2 = self.activations[::2], self.activations[1::2]
for c1, c2, a1, a2 in zip(self.convs1, self.convs2, acts1, acts2):
xt = a1(x)
xt = c1(xt)
xt = a2(xt)
xt = c2(xt)
x = xt + x
return x
class BigVGANFlowVAE(nn.Module):
def __init__(self, h):
super().__init__()
self.config = h
self.h = h
causal = h.causal
self.hop_size = np.prod(h.downsample_rates)
self.num_kernels = len(h.resblock_kernel_sizes)
self.num_upsamples = len(h.upsample_rates)
self.conv_pre = weight_norm(Conv1d(h.latent_dim, h.upsample_initial_channel, 7, 1, causal=False))
self.ups = nn.ModuleList()
for i, (u, k) in enumerate(zip(h.upsample_rates, h.upsample_kernel_sizes)):
self.ups.append(
nn.ModuleList(
[
weight_norm(
ConvTranspose1d(
h.upsample_initial_channel // (2**i),
h.upsample_initial_channel // (2 ** (i + 1)),
k,
u,
causal=causal,
)
)
]
)
)
self.resblocks = nn.ModuleList()
for i in range(len(self.ups)):
ch = h.upsample_initial_channel // (2 ** (i + 1))
for j, (k, d) in enumerate(zip(h.resblock_kernel_sizes, h.resblock_dilation_sizes)):
self.resblocks.append(AMPBlock1(h, ch, k, d, causal=causal))
activation_post = SnakeBeta(ch, alpha_logscale=h.snake_logscale)
self.activation_post = Activation1d(activation=activation_post)
self.conv_post = weight_norm(Conv1d(ch, 1, 7, 1, causal=causal))
for i in range(len(self.ups)):
self.ups[i].apply(init_weights)
self.conv_post.apply(init_weights)
def inference_from_latents(self, x, do_sample=True, noise_scale=1.0):
if self.h.use_vae and do_sample:
assert x.size(1) == self.h.latent_dim * 2, f"Input must be like [B, D, H], got {x.shape}"
m_q, logs_q = torch.split(x, self.h.latent_dim, dim=1)
x = m_q + torch.randn_like(m_q) * torch.exp(logs_q) * noise_scale
else:
assert x.size(1) == self.h.latent_dim, f"Input must be like [B, D, H], got {x.shape}"
x = self.conv_pre(x)
for i in range(self.num_upsamples):
for i_up in range(len(self.ups[i])):
x = self.ups[i][i_up](x)
xs = None
for j in range(self.num_kernels):
if xs is None:
xs = self.resblocks[i * self.num_kernels + j](x)
else:
xs += self.resblocks[i * self.num_kernels + j](x)
x = xs / self.num_kernels
x = self.activation_post(x)
x = self.conv_post(x)
x = torch.tanh(x)
return x
# --------------- Token2WavDecoder ---------------
class Token2WavDecoder(nn.Module):
def __init__(self, config, **kwargs):
super().__init__()
self.config = config
self.wavegan = BigVGANFlowVAE(config.wavegan)
self.wavegan_hop_size = np.prod(config.wavegan.downsample_rates)
self.global_mean_var = getattr(config, "global_mean_var", None)
if self.global_mean_var is not None:
mean_var_data = torch.from_numpy(np.load(self.global_mean_var)).float().squeeze()
global_mean, global_var = mean_var_data.chunk(2, 0)
self.register_buffer("global_mean", global_mean)
self.register_buffer("global_var", global_var)
self.token2latent = Token2latentFlowMatchingWithEmbed(config.token2latent)
self.upsample_factor = self.token2latent.config.get("upsample_factor", 1)
self.wav_input_sr = config.get("wav_input_sr", 24000)
self.trainable_module = ["wavegan", "token2latent"]
def state_dict(self):
param_dict = OrderedDict()
for name in self.trainable_module:
state = self.get_submodule(name).state_dict(prefix=f"{name}.")
param_dict.update(state)
return param_dict
def load_state_dict(self, param_dict):
for name in self.trainable_module:
module_state = OrderedDict()
name_len = len(name)
for k, v in param_dict.items():
if k.startswith(f"{name}."):
new_k = k[name_len + 1 :]
module_state[new_k] = v
self.get_submodule(name).load_state_dict(module_state, strict=False)
@torch.no_grad()
def preprocess_infer_data(self, data):
zero_spkr = data.get("zero_spkr", False)
token = data["target_token"]
prefix_target = None
res_prefix_len = 0
prompt_token = data.get("prompt_token", None)
prompt_latent = data.get("prompt_latent", None)
if exists(prompt_token):
token = torch.cat([prompt_token, token], dim=1)
if exists(prompt_latent):
prefix_target = prompt_latent
res_prefix_len = prompt_latent.shape[1]
spkr_embed = None
if not zero_spkr:
spkr_embed = data.get("spkr_embed", None)
res = {"token": token, "prefix_target": prefix_target, "spkr_embed": spkr_embed}
return res, res_prefix_len
@eval_decorator
@torch.no_grad()
def inference(self, data, **kwargs):
infer_data, prefix_len = self.preprocess_infer_data(data)
prefix_target = infer_data["prefix_target"]
res_latents = self.token2latent.inference(**infer_data, **kwargs)
if self.global_mean_var is not None:
res_latents = self.global_mean + res_latents * torch.sqrt(self.global_var)
if exists(prefix_target):
prefix_target = self.global_mean + prefix_target * torch.sqrt(self.global_var)
if exists(prefix_target):
res_latents[:, : prefix_target.shape[1]] = prefix_target
res_latents = res_latents[:, prefix_len:]
audio = self.wavegan.inference_from_latents(res_latents.transpose(1, 2), do_sample=False)
return audio
@@ -174,6 +174,32 @@ _OMNI_MODELS = {
"VoxtralTTSAudioGenerationForConditionalGeneration",
),
"VoxtralTTSAudioTokenizer": ("voxtral_tts", "voxtral_tts_audio_tokenizer", "VoxtralTTSAudioTokenizer"),
## covo_audio
"CovoAudioForCausalLM": (
"covo_audio",
"covo_audio",
"CovoAudioForConditionalGeneration",
),
"CovoAudioForConditionalGeneration": (
"covo_audio",
"covo_audio",
"CovoAudioForConditionalGeneration",
),
"CovoAudioModel": (
"covo_audio",
"covo_audio",
"CovoAudioForConditionalGeneration",
),
"CovoAudioLLMModel": (
"covo_audio",
"covo_audio_llm",
"CovoAudioLLMForConditionalGeneration",
),
"CovoAudioCode2WavModel": (
"covo_audio",
"covo_audio_code2wav",
"CovoAudioCode2WavForConditionalGeneration",
),
## MOSS-TTS-Nano
"MossTTSNanoForCausalLM": (
"moss_tts_nano",
@@ -0,0 +1,33 @@
# Copyright 2026 Tencent.
from typing import Any
from vllm_omni.inputs.data import OmniTokensPrompt
from vllm_omni.model_executor.models.covo_audio.config_covo_audio import COVO_AUDIO_TOKEN_INDEX
def llm2code2wav(
source_outputs: list[Any],
prompt: Any = None,
requires_multimodal_data: bool = False,
) -> list[OmniTokensPrompt]:
talker_outputs = source_outputs
code2wav_inputs = []
for i, talker_output in enumerate(talker_outputs):
output = talker_output.outputs[0]
token_ids = output.token_ids
audio_codes = [t - COVO_AUDIO_TOKEN_INDEX for t in token_ids if t >= COVO_AUDIO_TOKEN_INDEX]
if not audio_codes:
audio_codes = [-1]
code2wav_inputs.append(
OmniTokensPrompt(
prompt_token_ids=audio_codes,
multi_modal_data=None,
mm_processor_kwargs=None,
)
)
return code2wav_inputs