vLLM-Omni TTS 推理加速实战手册
写给「懂 TTS 部署、听过 vLLM、但没用过 vLLM-Omni」的你。
本文全程围绕语音合成(TTS)推理加速展开,视觉/图像/视频等能力只在必要时一笔带过。
目录
- 写在前面:用你已经懂的东西,理解你要学的东西
- [一、快速开始:跑通一个 Qwen3-TTS 模型](#一、快速开始:跑通一个 Qwen3-TTS 模型)
- [1.1 环境准备](#1.1 环境准备)
- [1.2 启动 Qwen3-TTS 服务](#1.2 启动 Qwen3-TTS 服务)
- [1.3 发送你的第一个 TTS 请求](#1.3 发送你的第一个 TTS 请求)
- [1.4 跑一次基准测试,看懂性能数字](#1.4 跑一次基准测试,看懂性能数字)
- 二、项目整体认知
- [2.1 vLLM-Omni 到底是什么](#2.1 vLLM-Omni 到底是什么)
- [2.2 它和原生 vLLM 是什么关系](#2.2 它和原生 vLLM 是什么关系)
- [2.3 技术分层架构](#2.3 技术分层架构)
- [2.4 一个 TTS 请求的生命周期](#2.4 一个 TTS 请求的生命周期)
- [2.5 横向对比:原生 vLLM vs vLLM-Omni](#2.5 横向对比:原生 vLLM vs vLLM-Omni)
- [三、代码结构与模块详解(聚焦 TTS)](#三、代码结构与模块详解(聚焦 TTS))
- [3.1 项目目录地图](#3.1 项目目录地图)
- [3.2 核心模块逐一拆解](#3.2 核心模块逐一拆解)
- [3.3 高频调优场景代码定位速查表](#3.3 高频调优场景代码定位速查表)
- 四、核心加速技术原理全面详解
- [4.1 通用加速技术(复用/改造自 vLLM)](#4.1 通用加速技术(复用/改造自 vLLM))
- [4.2 TTS/音频专属加速技术](#4.2 TTS/音频专属加速技术)
- [4.3 加速技术之间的关系](#4.3 加速技术之间的关系)
- [五、开源 TTS 模型适配完整工程指南](#五、开源 TTS 模型适配完整工程指南)
- [5.1 接入前必须理解的概念](#5.1 接入前必须理解的概念)
- [5.2 标准接入步骤](#5.2 标准接入步骤)
- [5.3 适配后的性能优化方向](#5.3 适配后的性能优化方向)
- [5.4 高频故障排查清单](#5.4 高频故障排查清单)
- 六、附录
写在前面:用你已经懂的东西,理解你要学的东西
在开始之前,先把「你已经懂的」和「你即将学的」做个对照。这份文档的每一个新概念,我都会先拿你熟悉的 TTS 知识当跳板。
| 你已经懂的(TTS 部署经验) | 你即将学的(vLLM-Omni 里的对应物) |
|---|---|
| 文本前端(g2p/音素) → 文本编码器 → 声学模型/Flow Matching → 声码器 → PCM | 同样这条链路,但被拆成可独立调度的 Stage(阶段) |
| 一个 TTS 请求独占 GPU,串行推理 | 连续批处理(Continuous Batching):多个请求在同一块 GPU 上交错执行 |
| decoder 推理时为每条序列存一份 attention 的 K/V,占一大块连续显存 | PagedAttention:把这块显存切成「页」,按需分配 |
| 离线脚本跑批量合成、用 FastAPI 包一个推理接口 | Omni.generate() 离线接口 + vllm-omni serve 在线服务(OpenAI 兼容) |
| 给同一个人合成很多句话,每次都重新跑一遍参考音频编码 | 音频特征缓存:参考音频/说话人向量只算一次,后续复用 |
| Flow Matching / DiT 解码很慢,要分块流式出音频 | 分块流式解码(chunked streaming) + 跨 stage 异步流水线(async_chunk) |
记住一句话,贯穿全文:
vLLM-Omni 的本质 = 把 vLLM「高效的文本 LLM 推理引擎」能力,扩展到「语音/音频/图像」的多阶段流水线上。它会 vLLM 就会 vLLM-Omni(官方原话:"If you use vLLM, then you know how to use vLLM-Omni from Day 0")。
还有一个你需要现在就知道的「现实校准」,避免你读到后面产生错误预期(这一点非常重要,官方文档里都写了,但容易忽略):
- ⚠️ 当前版本(0.14.0)的纯 TTS 在线服务,还不支持「流式音频」 。也就是说,现在的
/v1/audio/speech接口是「整段音频生成完一次性返回」,不是边生成边推流。流式能力还在路上(官方 RFC #938)。下文讲到流式时,我会明确区分「已实现的内部机制」和「对外服务还没开放」。 - ⚠️ 官方默认 stage 配置里 TTS 的并发是
max_batch_size: 1,也就是单请求串行。多请求并发需要你改配置(第 5.3 节会讲)。 - ⚠️ 官方默认所有 stage 都是
enforce_eager: true(关闭 CUDA Graph),原因是框架目前主要在 eager 模式下验证稳定。 - ⚠️ 音频路径里没有任何自定义 CUDA 算子 ------RVQ、STFT、BigVGAN、Flow Matching ODE 全是纯 PyTorch 实现(唯一的例外是一个用 ONNX Runtime 跑的说话人向量提取器
campplus.onnx)。
这些不是缺点,而是一个 2025 年 11 月才发布、2026 年 2 月才出第一个稳定版的新框架的真实现状。理解了现状,你才能正确评估它、用好它。
一、快速开始:跑通一个 Qwen3-TTS 模型
这一节的目标只有一个:让你先把服务跑起来、听到声音,建立感性认识。原理全部留到后面。
1.1 环境准备
硬件要求:
- NVIDIA GPU,建议 ≥ 24GB 显存(Qwen3-TTS-1.7B 本身很小,但 framework 有开销);架构支持 Ampere(30系)/Hopper(H100)/Ada(40系)。
- AMD ROCm 也支持(本节以 NVIDIA 为主)。
操作系统:
- ⚠️ 必须 Linux 。官方文档
docs/getting_started/installation/gpu.md:10-12明确写:vLLM-Omni is currently not natively supported on Windows.。你现在在 Windows 上看这份文档没关系,但部署请用 Linux 服务器。
Python: 3.12
安装步骤(来自 docs/getting_started/quickstart.md:14-30):
bash
# 1. 建虚拟环境
uv venv --python 3.12 --seed
source .venv/bin/activate
# 2. 先装上游 vLLM(指定版本,因为 vLLM-Omni 依赖它)
uv pip install vllm==0.15.0 --torch-backend=auto
# 3. 克隆 vLLM-Omni 并以开发模式安装
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
uv pip install -e .
💡 类比:你以前部署 CosyVoice 时,要先装
torch、再装transformers、再 clone CosyVoice 仓库。这里一样的套路:先装 vLLM(相当于你的 torch 底座),再装 vLLM-Omni(相当于你的 CosyVoice)。
验证安装:
bash
# 这个命令能正常返回帮助信息,就说明 vllm-omni 装好了
vllm-omni serve --help
⚠️ 常见坑:如果
vllm-omni命令找不到,检查(1)虚拟环境是否激活(2)vllm==0.15.0版本是否对得上(vLLM-Omni 对上游版本很敏感)。
1.2 启动 Qwen3-TTS 服务
vLLM-Omni 官方原生支持三个 Qwen3-TTS 变体(见 docs/models/supported_models.md:40-42):
| 模型 | 用途 | HuggingFace ID |
|---|---|---|
| CustomVoice | 用预置音色(如 Vivian/Ryan)合成 | Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice |
| VoiceDesign | 用自然语言描述想要的音色风格 | Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign |
| Base | 声音克隆(给一段参考音频,克隆音色) | Qwen/Qwen3-TTS-12Hz-1.7B-Base |
我们用 CustomVoice 走一遍。最省事的方式是直接跑官方提供的启动脚本(examples/online_serving/qwen3_tts/run_server.sh:33-40):
bash
vllm-omni serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_tts.yaml \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.9 \
--trust-remote-code \
--enforce-eager \
--omni
逐个参数解释(这是你最该先搞懂的):
| 参数 | 值 | 对 TTS 的意义 |
|---|---|---|
vllm-omni serve <MODEL> |
第一个位置参数 | 要加载的模型,等价于你部署时的 model_name_or_path |
--omni |
开关 | 必须加 。它告诉 CLI「走 vLLM-Omni 多阶段流水线」。漏了它,命令会回退到上游纯文本 vLLM,TTS 路由完全不生效。判断逻辑在 vllm_omni/entrypoints/cli/main.py:12:if "--omni" not in sys.argv: 走上游 |
--stage-configs-path |
YAML 路径 | TTS 部署的核心配置文件 。它定义了「这个模型拆成几个 stage、每个 stage 用什么调度器、占多少显存」。TTS 用 qwen3_tts.yaml(下文详解) |
--host/--port |
地址 | 同 FastAPI,服务监听地址 |
--gpu-memory-utilization 0.9 |
0~1 浮点 | vLLM 能用多少比例的 GPU 显存。TTS 模型本身小,但 KV cache、激活值要占地方 |
--trust-remote-code |
开关 | 加载需要执行远程代码的 HF 模型时必须开(Qwen3-TTS 需要) |
--enforce-eager |
开关 | 强制 eager 模式,不用 CUDA Graph。TTS 默认就是 true(见 1.1 节现实校准) |
还有一组 Omni 专属参数 (定义在 vllm_omni/entrypoints/cli/serve.py:76-132 的 omni_config_group):
| 参数 | 默认 | 含义 |
|---|---|---|
--stage-init-timeout |
300 | 单个 stage 初始化超时(秒)。stage 多、模型大时调大 |
--init-timeout |
600 | 整体所有 stage 初始化超时 |
--shm-threshold-bytes |
65536 | 跨 stage 数据传输时,大于这个阈值的数据走共享内存(SHM)而非进程队列 |
--batch-timeout |
10 | stage 攒批的超时(秒)。影响首包延迟 |
--worker-backend |
multi_process |
stage 间执行后端,可选 ray(分布式) |
TTS 的 stage 配置文件长什么样? 看 vllm_omni/model_executor/stage_configs/qwen3_tts.yaml(全文就 23 行):
yaml
stage_args:
- stage_id: 0
stage_type: llm # 用 LLM 引擎类型
runtime:
devices: "0" # 用 GPU 0
max_batch_size: 1 # ⚠️ 当前并发=1(串行)
engine_args:
model_stage: qwen3_tts # 标识:这是个 TTS 模型
model_arch: Qwen3TTSForConditionalGeneration # 注册表里的架构类名
worker_type: generation # 用「生成型」worker(一步出结果,非自回归)
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
enforce_eager: true
enable_prefix_caching: false
engine_output_type: audio # 最终输出:音频波形
gpu_memory_utilization: 0.1 # 这台 stage 只用 10% 显存
distributed_executor_backend: "mp"
max_num_batched_tokens: 1000000 # 音频 token 序列很长,故意放大上限
final_output: true
final_output_type: audio
💡 你不需要现在就看懂每一行。先记住两个关键点:
- Qwen3-TTS 在这里只占一个 stage(不像 Omni 模型有 3 个 stage)------因为它本身就是「文本直接到音频」的端到端模型。
worker_type: generation+OmniGenerationScheduler------ 这告诉框架「这个模型不是逐 token 自回归的,而是『一步前向就出结果』」。这是 vLLM-Omni 为 TTS/扩散模型专门做的 fast-path(详见第 4 节)。
启动后你会看到类似 vLLM 的启动日志,最后出现 Uvicorn running on http://0.0.0.0:8000 就说明服务起来了。
先做个健康检查:
bash
# 列出可用音色
curl http://localhost:8000/v1/audio/voices
# 返回类似:{"voices": ["cherry","Ethan","chelsie", ...]}
# 服务健康
curl http://localhost:8000/health
1.3 发送你的第一个 TTS 请求
vLLM-Omni 的 TTS 接口是 OpenAI 兼容的 /v1/audio/speech (就是 OpenAI 那个 TTS API 的格式)。路由注册在 vllm_omni/entrypoints/openai/api_server.py:775。
用 curl:
bash
curl -X POST http://localhost:8000/v1/audio/speech \
-H "Content-Type: application/json" \
-H "Authorization: Bearer EMPTY" \
-d '{
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"input": "你好,这是通过 vLLM-Omni 加速合成的第一句话。",
"voice": "Vivian",
"response_format": "wav"
}' \
--output hello.wav
# 听一下
play hello.wav # 或用任意播放器
请求体字段(OpenAICreateSpeechRequest,定义在 vllm_omni/entrypoints/openai/protocol/audio.py:7-57):
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
input |
str | 必填 | 要合成的文本 |
model |
str | 可选 | 单模型部署时可省 |
voice |
str | 可选 | 说话人(如 Vivian/Ryan),大小写不敏感 |
instructions |
str | 可选 | 风格/情感指令(如「用愤怒的语气说」) |
response_format |
wav/pcm/flac/mp3/aac/opus | wav |
输出音频格式 |
speed |
0.25~4.0 | 1.0 | 变速不变调(用 librosa 实现,见 audio_utils_mixin.py:76-93) |
task_type |
CustomVoice/VoiceDesign/Base | 可选 | 任务类型 |
language |
str | 可选 | Auto/Chinese/English/... |
ref_audio |
str | Base 必填 | 声音克隆的参考音频(URL 或 base64 data URL) |
ref_text |
str | 可选 | 参考音频的转录文本(ICL 模式用) |
x_vector_only_mode |
bool | 可选 | 只用说话人向量,不走上下文学习克隆 |
max_new_tokens |
1~4096 | 可选 | 生成 token 上限 |
用 Python(参考 examples/online_serving/qwen3_tts/openai_speech_client.py:65-91):
python
import httpx
resp = httpx.post(
"http://localhost:8000/v1/audio/speech",
headers={"Authorization": "Bearer EMPTY"},
json={
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"input": "这是用 Python 调用的合成结果。",
"voice": "Vivian",
"response_format": "wav",
},
timeout=300.0,
)
with open("out.wav", "wb") as f:
f.write(resp.content) # ⚠️ 注意:响应是完整的一段音频字节流
⚠️ 关于「流式」的重要澄清 :上面两段代码都是「一次性拿到完整音频」。
你可能会问:TTS 不都该流式吗?OpenAI 的
/v1/audio/speech不是支持stream_format: "sse"吗?答案是:当前版本的 vLLM-Omni 会直接拒绝
sse。证据在vllm_omni/entrypoints/openai/protocol/audio.py:52-57,校验函数会抛错:"'sse' is not a supported stream_format yet. Please use 'audio'."。服务端在vllm_omni/entrypoints/openai/serving_speech.py:303直接返回Response(content=完整音频字节, media_type=...)。所以目前 TTS 是「整段生成完再返回」 。流式是内部已经有机制(见第 4.2 节的
chunked_decode_streaming和async_chunk),只是对外服务接口还没开放。这是框架演进路线上的已知限制。
验证输出正确:把 wav 存下来听,确认音色/内容/语言正确即可。
1.4 跑一次基准测试,看懂性能数字
vLLM-Omni 自带两套 benchmark:
- 离线管线基准 (vLLM-Omni pipeline vs HF Transformers):
benchmarks/qwen3-omni/ - 在线服务基准 (
vllm bench serve,含音频专属指标)
先看在线服务怎么压测(对你日常评估 TTS 服务最有用):
bash
# 用 vllm 自带的 bench serve 子命令压测(vLLM-Omni 扩展了音频指标)
vllm bench serve \
--backend vllm-omni \
--base-url http://localhost:8000 \
--model Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--num-prompts 100 \
--percentile-metrics ttft,tpot,itl,e2el,audio_ttfp,audio_rtf,audio_duration
输出里的关键指标解读 (指标定义在 vllm_omni/benchmarks/metrics/metrics.py:11-27):
| 指标 | 全称 | 对 TTS 的意义 |
|---|---|---|
| TTFT | Time To First Token | 文本侧首 token 延迟。对纯 TTS 意义不大 |
| TPOT | Time Per Output Token | 每生成一个 token 的平均耗时 |
| AUDIO_TTFP | Time To First Packet(音频首包延迟) | TTS 最关键的延迟指标------用户从发请求到听到第一个音要等多久 |
| AUDIO_RTF | Real Time Factor | 生成 1 秒音频需要多少秒。<1 就是「实时」(生成比播放快) |
| audio_throughput | audio duration / s | 每秒能产出多少秒的音频(吞吐) |
官方公布的性能数字 (来自 docs/cli/bench/serve.md,注意:这是 Qwen3-Omni 全模态模型,含 ASR+TTS,不是纯 TTS,仅供参考量级):
| 场景 | Token 吞吐 | TTFT | TPOT | 音频 RTF |
|---|---|---|---|---|
| 单流 | 73.04 tok/s | 124.76 ms | --- | --- |
| 并发 | 2603 tok/s(约 35×) | 117.15 ms | 0.73 ms | 0.47 |
读这张表你能得到两个直觉:
- 并发下吞吐提升约 35 倍(73 → 2603 tok/s)------这正是连续批处理的威力(第 4.1 节)。
- AUDIO_RTF = 0.47 意味着生成速度约是实时的 2.1 倍------说明这个模型/框架组合已经能支撑实时语音交互(但前提是流式接口开放后)。
关于「vLLM-Omni 比 HF Transformers 快多少」,官方放了一张图
benchmarks/qwen3-omni/vllm-omni-vs-hf.png(README:70-74标注 "actual experiment results"),论文 arXiv:2602.02204 有详细数字。纯 Qwen3-TTS 的独立对比数字仓库里没有给出 ,要自己跑benchmarks/qwen3-omni/下的脚本复现。
离线基准复现命令(benchmarks/qwen3-omni/):
bash
# HF Transformers 基准
bash benchmarks/qwen3-omni/transformers/eval_qwen3_moe_omni_transformers.sh
# vLLM-Omni 基准
bash benchmarks/qwen3-omni/vllm_omni/eval_qwen3_moe_omni.sh
二、项目整体认知
2.1 vLLM-Omni 到底是什么
一句话:vLLM-Omni 是在 vLLM 之上扩展的「全模态(Omni-Modality)统一推理与服务平台」。
- 发布时间:2025 年 11 月首发,2026 年 2 月发布第一个稳定版 0.14.0(
README.md:18-22)。 - 论文:arXiv:2602.02204《vLLM-Omni: Fully Disaggregated Serving for Any-to-Any Multimodal Models》。
它要解决的三个核心问题 (docs/design/architecture_overview.md:14-19):
- 非文本输出 :vLLM 原本只会输出 token(文本),Omni 让它能输出图像、音频、视频。
- 非自回归结构:vLLM 原本只支持「逐 token 生成」的自回归模型,Omni 扩展到 **DiT(Diffusion Transformer)**等「一步或迭代并行生成」的结构------这恰好是很多 TTS vocoder 和声音生成模型的范式。
- 与 vLLM 核心无缝集成:尽可能复用 vLLM 已有的 KV cache、调度、量化等优化,而不是另起炉灶。
在 TTS 场景下的核心价值主张:
把 TTS 推理从「1 个请求独占 1 张 GPU、串行处理」的传统模式,变成「多请求并发批处理 + 多阶段流水线并行」的高吞吐模式。
典型 TTS 落地场景:
- 实时 TTS 流式服务:在线客服、语音助手、有声读物边合成边播放。(⚠️ 注意 1.3 节:当前纯 TTS 在线流式接口未开放,但内部机制已就绪)
- 离线批量语音合成:把几万条文本一次性喂进去,靠连续批处理榨干 GPU。
- 语音对话(Omni 模型):ASR 听懂 → LLM 思考 → TTS 说出来,一条流水线搞定(Qwen3-Omni 这种)。
2.2 它和原生 vLLM 是什么关系
这是你最该搞清楚的一件事。结论先行:vLLM-Omni 没有 fork(分叉)vLLM,而是用「继承 + monkey-patch(运行时打补丁)」的方式做最小侵入式扩展。
直接复用 vLLM 的部分(基本没动):
- vLLM 的引擎主循环(
LLMEngine的 step/prefill/decode)------完全复用,连代码都没改。 - PagedAttention 算子、KV cache 管理(
KVCacheManager)。 - 量化引擎(AWQ/GPTQ/FP8/compressed-tensors)。
- Worker、ModelRunner 的骨架。
- OpenAI 兼容的 API server 框架。
vLLM-Omni 新增/改造的部分(Omni 独有):
- 多 Stage 编排层 (
Omni/AsyncOmni/OmniStage)------这是 vLLM 完全没有的东西。vLLM 只有一个引擎,Omni 在它之上加了「多引擎流水线编排」。 - OmniConnector:stage 之间的数据传输抽象(共享内存/跨节点)。
- 定制调度器 :
OmniARScheduler(自回归 stage)、OmniGenerationScheduler(一步生成 stage,用于 TTS code2wav)。 - 音频前后处理:mel 谱、RVQ codec、声码器、流式拼接。
- monkey-patch 机制 (
vllm_omni/patch.py):导入 vllm_omni 时,自动把 vLLM 里的若干类替换成 Omni 版本。
「继承关系」一图看懂 (文字版,源图 docs/source/architecture/vllm-omni-main-architecture.png):
你的业务代码 (TTS 服务)
│ 调用
▼
┌─────────────────────────────────────────────┐
│ Omni 编排层 (vLLM-Omni 新增) │
│ Omni / AsyncOmni ←── 离线/在线入口 │
│ │ 管理 │
│ ▼ │
│ OmniStage (stage 0) → OmniConnector → OmniStage (stage 1) → ...
│ (每个 stage 独立进程、独立 GPU、独立引擎) │
└─────────────────────────────────────────────┘
│ 每个 stage 内部包裹一个
▼
┌─────────────────────────────────────────────┐
│ vLLM 引擎 (复用,未改主循环) │
│ LLMEngine ── 调度器 ── Worker ── ModelRunner │
│ + PagedAttention + KV cache + 量化 │
└─────────────────────────────────────────────┘
「monkey-patch」具体干了啥 (vllm_omni/patch.py:18-33,在 vllm_omni/__init__.py:16 导入时自动执行):
它把 vLLM 里的这些类替换成 Omni 加强版:
| vLLM 原始类 | 替换为 | 加了什么 |
|---|---|---|
Request |
OmniRequest (vllm_omni/request.py:14) |
prompt_embeds、external_req_id、additional_information(携带 stage 间张量) |
TokensPrompt |
OmniTokensPrompt (vllm_omni/inputs/data.py) |
additional_information |
EngineCoreRequest/Output |
Omni* (vllm_omni/engine/__init__.py:56,76) |
多模态输出字段 |
MRotaryEmbedding |
OmniMRotaryEmbedding |
多模态位置编码 |
💡 类比你熟悉的:这就像你给一个 Python 类用
@property或装饰器加功能,而不是把整个类复制一份出来改。好处是 vLLM 升级了,Omni 跟着升级的代价很小。
「LLMEngine 本身没被 fork」的铁证 (vllm_omni/entrypoints/omni_llm.py:158):
python
class OmniLLM:
def __init__(self, ...):
# 直接调原生 vLLM 创建引擎
self.llm_engine = LLMEngine.from_engine_args(engine_args=engine_args, ...)
# 只换了两个 processor(数据进出引擎的切面)
self.llm_engine.output_processor = MultimodalOutputProcessor(...)
self.llm_engine.input_processor = OmniInputProcessor(...)
调度器则是通过 engine_args.scheduler_cls(YAML 里那个字符串路径)被 vLLM 自己的配置机制加载进来的------完全不用改 LLMEngine 代码。
2.3 技术分层架构
从上到下六层,每层标注它在 TTS 推理里干啥:
┌──────────────────────────────────────────────────┐
│ 服务接口层 │ ← TTS 用户请求入口
│ /v1/audio/speech、/v1/audio/voices (OpenAI 兼容) │ api_server.py / serving_speech.py
├──────────────────────────────────────────────────┤
│ 多 Stage 编排层 (Omni 独有) │ ← 决定请求怎么在 stage 间流动
│ Omni / AsyncOmni (Orchestrator) │ omni.py / async_omni.py
│ OmniStage (每 stage 独立进程/GPU) │ omni_stage.py
│ OmniConnector (stage 间数据传输: SHM) │ distributed/omni_connectors/
├──────────────────────────────────────────────────┤
│ 模型推理层 │ ← TTS 模型真正计算的地方
│ Qwen3-TTS / Thinker+Talker+Code2Wav 等 │ model_executor/models/
├──────────────────────────────────────────────────┤
│ 加速引擎层 │ ← 提速的核心
│ PagedAttention / 连续批处理 / KV cache / 量化 │ core/sched/ + 复用 vLLM
│ 投机采样 / CUDA Graph / 算子融合 │
├──────────────────────────────────────────────────┤
│ 前后处理层 │ ← TTS 输入输出的转换
│ 文本 tokenize / RVQ codec / Mel 谱 / BigVGAN │ model_executor/models/qwen3_tts/
│ 流式音频拼接 / 说话人向量提取 │
├──────────────────────────────────────────────────┤
│ 硬件通信层 │ ← GPU 上的底层操作
│ CUDA / ROCm / NCCL / 显存管理 │ worker/ + platforms/
└──────────────────────────────────────────────────┘
2.4 一个 TTS 请求的生命周期
这是把上面六层「串起来」看的最佳方式。以 Qwen3-TTS(单 stage)为例,一个请求从进入到返回的全过程(对应代码在 vllm_omni/entrypoints/omni.py:619 的 _run_generation()):
[客户端] POST /v1/audio/speech {"input":"你好", "voice":"Vivian"}
│
▼
[服务接口层] serving_speech.py:183 create_speech()
│ 把 OpenAI 请求转成内部 prompt dict:
│ {"prompt":"<|im_start|>assistant\n你好<|im_end|>...",
│ "additional_information":{"task_type":["CustomVoice"], "text":["你好"], "speaker":["Vivian"], ...}}
▼
[编排层] Omni.generate() / AsyncOmni.generate()
│ 生成 request_id,submit 到 stage_list[0]
▼
[OmniStage 0: code2wav] (omni_stage.py)
│ stage worker 进程从队列取请求
│ 按max_batch_size攒批 → 喂给内部 vLLM engine
▼
[加速引擎层] OmniGenerationScheduler (omni_generation_scheduler.py:39)
│ fast-path: 一次性分配所有 input token (音频序列长)
▼
[模型推理层] Qwen3TTSModelForGeneration.forward() (qwen3_tts.py:91)
│ 分发到 generate_custom_voice()
│ 内部: 文本→token→talker生成声学token→speech_tokenizer.decode→BigVGAN→波形
▼
[前后处理层] 音频 PCM 张量 → OmniOutput(multimodal_outputs={"model_outputs":audio, "sr":24000})
▼
[编排层] MultimodalOutputProcessor (output_processor.py:430 _process_audio_output)
│ 从 multimodal_outputs 提取音频张量
▼
[服务接口层] audio_utils_mixin.py:24 create_audio()
│ PCM → wav/pcm/mp3 编码 (soundfile)
▼
[客户端] 收到完整音频字节流 (Response, media_type=audio/wav)
💡 对比你熟悉的传统 TTS 部署:传统方式里,这条链路是你自己用 Python 串起来的(g2p → encoder → decoder → vocoder,一个个函数调用)。vLLM-Omni 把它装进了一个「带调度、带批处理、带 stage 管理」的框架里------单请求时看起来一样,多请求并发时差别就出来了(见第 4 节)。
如果是 Omni 模型(如 Qwen3-Omni,3 个 stage),请求会在 stage 间流动:
stage 0 (thinker, AR): 多模态理解 + 生成文本/COT
│ OmniConnector (SHM) 传 latent
▼
stage 1 (talker, AR): 文本嵌入 → RVQ codec codes
│ OmniConnector (SHM) 传 codec codes
▼
stage 2 (code2wav, generation): codec codes → 音频波形
│
▼
最终输出 (text + audio)
stage 之间通过 engine_input_source 和 custom_process_input_func 串联(详见第 5 节)。
2.5 横向对比:原生 vLLM vs vLLM-Omni
| 对比维度 | 原生 vLLM(纯文本) | vLLM-Omni(TTS) |
|---|---|---|
| 输入类型 | token ids | 文本 + 可选参考音频(声音克隆) |
| 输出类型 | token ids(离散文本) | 音频 token + 波形 PCM(连续 + 计划流式) |
| 核心模型 | LLM | LLM + Audio Encoder + Vocoder(如 BigVGAN) |
| 生成范式 | 纯自回归(逐 token) | 自回归(AR)+ 一步生成(generation,用于 code2wav) |
| 批处理 | 纯文本连续批处理 | 文本+音频混合批处理;generation stage 有专门 fast-path |
| 调度策略 | 单一 Scheduler | 多 stage 各自一个调度器,stage 间靠 connector 串联 |
| 加速重点 | KV cache、PagedAttention | KV cache + 跨 stage KV 转移 + 音频特征缓存 + 分块流式 |
| 显存分布 | 模型权重 + KV cache | 模型权重 + KV cache + 音频缓存 + 每 stage 独立显存配额 |
| 服务协议 | HTTP/gRPC(文本流) | HTTP SSE/gRPC(文本流已支持;音频流接口待开放) |
| 部署形态 | 单引擎进程 | 多 stage = 多进程,可跨 GPU/节点 |
三、代码结构与模块详解(聚焦 TTS)
本章只讲跟 TTS 推理相关的目录和模块。视觉/图像相关的一律跳过。
3.1 项目目录地图
vllm-omni/
├── vllm_omni/ ★ 核心源码包
│ ├── __init__.py 导出 Omni / AsyncOmni / OmniModelConfig;导入时触发 patch.py
│ ├── patch.py ★ monkey-patch:替换 vLLM 的 Request/Prompt 等类
│ ├── request.py OmniRequest(带 prompt_embeds/additional_information)
│ ├── outputs.py OmniRequestOutput / OmniModelRunnerOutput
│ ├── engine/ ★ 引擎层(继承 vLLM EngineArgs/InputProcessor/OutputProcessor)
│ │ ├── arg_utils.py OmniEngineArgs / AsyncOmniEngineArgs(加 stage 相关参数)
│ │ ├── input_processor.py OmniInputProcessor(序列化 prompt_embeds/additional_info)
│ │ └── output_processor.py MultimodalOutputProcessor(音频/多模态输出路由)
│ ├── core/ ★ 调度与执行核心
│ │ └── sched/ ★ Omni 定制调度器(继承 vLLM Scheduler)
│ │ ├── omni_ar_scheduler.py OmniARScheduler(自回归 stage)
│ │ ├── omni_generation_scheduler.py OmniGenerationScheduler(TTS code2wav!)
│ │ └── output.py OmniNewRequestData 等
│ ├── worker/ ★ Worker 层(继承 vLLM GPUWorker)
│ │ ├── gpu_worker 派生 GPUARWorker / GPUGenerationWorker
│ │ ├── gpu_model_runner.py OmniGPUModelRunner(prompt_embeds overlay、M-RoPE)
│ │ └── gpu_ar_model_runner.py GPUARModelRunner(两阶段 execute/sample)
│ ├── entrypoints/ ★ 服务与编排入口
│ │ ├── omni.py ★ Omni 类(同步 Orchestrator,离线入口)
│ │ ├── async_omni.py AsyncOmni(异步,在线服务用)
│ │ ├── omni_stage.py ★ OmniStage(单 stage 生命周期、队列、进程)
│ │ ├── omni_llm.py OmniLLM(包装原生 LLMEngine)
│ │ ├── stage_utils.py SHM 读写、IPC 编解码
│ │ ├── cli/ ★ vllm-omni 命令行(serve/benchmark)
│ │ └── openai/ ★ OpenAI 兼容 API
│ │ ├── api_server.py 路由注册(/v1/audio/speech 等)
│ │ ├── serving_speech.py ★ TTS speech 接口实现
│ │ └── audio_utils_mixin.py 音频编码(wav/pcm/mp3)
│ ├── model_executor/ ★ 模型实现与加载
│ │ ├── models/ ★ 各模型实现(按架构分目录)
│ │ │ ├── registry.py ★ 模型注册表(_OMNI_MODELS 字典)
│ │ │ ├── qwen3_tts/ ★ Qwen3-TTS 实现(本文重点)
│ │ │ ├── qwen3_omni/ Qwen3-Omni 3-stage
│ │ │ ├── qwen2_5_omni/ Qwen2.5-Omni 3-stage(含 BigVGAN vocoder)
│ │ │ └── output_templates.py OmniOutput(forward 返回类型)
│ │ ├── stage_configs/ ★ Stage 配置 YAML(qwen3_tts.yaml 等)
│ │ └── stage_input_processors/ ★ stage 间数据转换函数
│ ├── inputs/ 输入数据结构(OmniTokensPrompt 等)
│ ├── distributed/ ★ 分布式与 stage 间通信
│ │ └── omni_connectors/ ★ OmniConnector(SHM/Mooncake/Yuanrong)
│ ├── diffusion/ 扩散模型(图像/视频,TTS 可跳过)
│ ├── config/ ★ OmniModelConfig 配置
│ ├── platforms/ 硬件平台(CUDA/ROCm/NPU/XPU)
│ ├── metrics/ ★ 性能统计(stats.py)
│ └── benchmarks/ bench serve 指标定义
├── examples/ ★ 示例(必看)
│ ├── offline_inference/
│ │ ├── qwen3_tts/ ★ 离线 TTS(end2end.py)
│ │ └── qwen3_omni/ 离线全模态
│ └── online_serving/
│ └── qwen3_tts/ ★ 在线 TTS 服务(run_server.sh)
├── benchmarks/ ★ 性能基准(vs HF Transformers)
├── docs/ ★ 官方文档(design/configuration/getting_started)
├── tests/ 测试(含 e2e stage_configs)
└── pyproject.toml / setup.py 打包
3.2 核心模块逐一拆解
每个模块按 作用 → 源码路径 → 入口类/函数 → 关键执行逻辑 讲。
(1)多 Stage 编排器(Omni / AsyncOmni / OmniStage)
作用:把一个多阶段模型(或单阶段)组织成一条流水线,管理请求在 stage 间的流动、批处理、资源分配。这是 vLLM 完全没有的层。
源码:
vllm_omni/entrypoints/omni.py------ 同步编排器(离线用)vllm_omni/entrypoints/async_omni.py------ 异步编排器(在线服务用)vllm_omni/entrypoints/omni_stage.py------ 单 stage 抽象
入口:
Omni类(omni.py:492),导出于vllm_omni/__init__.py:28Omni.generate()(omni.py:549)→ 内部_run_generation()(omni.py:619)
OmniStage 关键属性 (omni_stage.py:226-266):
stage_id、stage_type("llm"/"diffusion")model_stage("qwen3_tts"/"thinker"/"talker"/"code2wav")engine_input_source(上游 stage 列表,如[0])final_output、final_output_type("text"/"audio")custom_process_input_func(stage 间数据转换函数)
关键执行逻辑 (_run_generation(),omni.py:717-878):
1. 校验 sampling_params_list 长度 == stage 数 (omni.py:630)
2. 为每个请求生成 request_id (omni.py:651)
3. 确定请求终止于哪个 stage (get_final_stage_id_for_e2e, omni.py:658)
4. 把请求 submit 到 stage_list[0] (omni.py:688)
5. 进入调度循环 while completed_requests < total_requests (omni.py:717):
a. 轮询每个 stage 的 try_collect() 取已完成结果 (omni.py:719)
b. 反序列化(SHM 或 inline)(omni.py:738)
c. stage.set_engine_outputs() 存到 stage 对象 (omni.py:777)
d. 若该 stage 是 final_output → 构造 OmniRequestOutput 并 yield (omni.py:779)
e. 否则 next_stage.process_engine_inputs() 派生下游输入 → try_send_via_connector 转发 (omni.py:824)
f. 无进展时 time.sleep(0.005) 让出 CPU (omni.py:877)
Stage worker 进程内的攒批 (omni_stage.py:626 的 _stage_worker):
- 从
in_q.get()取 task - 按
max_batch_size和batch_timeout攒批(omni_stage.py:770-833) - 喂给内部
stage_engine.generate(),逐 request 把结果out_q.put()
💡 TTS 并发怎么调? 关键就是改 stage config YAML 里的
runtime.max_batch_size(当前默认 1)。但注意:单请求串行是当前官方验证过的稳定配置,调大并发需要你充分测试音质和稳定性。
(2)调度器(Omni 的「改造点」之一)
作用 :决定「哪些请求、以什么顺序、凑成多大的 batch」送进引擎。Omni 的调度器继承 vLLM 的 Scheduler,不是从零写。
继承关系 (docs/design/module/ar_module.md:24-39):
vllm.v1.core.sched.scheduler.Scheduler (VLLMScheduler)
├── OmniARScheduler (core/sched/omni_ar_scheduler.py:41)
│ 用于 thinker/talker 等自回归 stage
└── OmniGenerationScheduler (core/sched/omni_generation_scheduler.py:25)
用于 code2wav 等一步生成 stage(TTS!)
OmniARScheduler(自回归 stage 用):
- 重写
schedule()(omni_ar_scheduler.py:154):调super().schedule()后,把NewRequestData包成OmniNewRequestData(附加prompt_embeds、additional_information),并处理跨 stage 的 chunk。 - 重写
update_from_output()(omni_ar_scheduler.py:202):加 KV cache 转移触发、put_chunk()推送 latent 到下游。
OmniGenerationScheduler (TTS code2wav 专用 ,omni_generation_scheduler.py:25):
- 完全重写
schedule()(omni_generation_scheduler.py:39) ,不调 super:走「Diffusion/generation fast-path」------一次性把请求所有 input token 调度掉(:80-100),预算不足才回退super().schedule()(:157)。 - 重写
update_from_output()(:272):一步就标记FINISHED_STOPPED(:357),释放资源。
💡 为什么 TTS code2wav 要单独的 fast-path? 因为 code2wav 不是「逐 token 生成」,它是「拿到全部 codec codes 后一次性算出整段音频」。vLLM 原生的 prefill/decode 循环对它不适用,所以要专门写一个「一次性调度」的路径。
连续批处理在哪实现? ------不在 Omni 层,而是复用 vLLM 引擎核心 。每个 stage 内部的 OmniLLM/AsyncOmniLLM 包的是原生 vLLM.v1.engine.llm_engine.LLMEngine。vLLM 的 step() 循环 + Scheduler.schedule() 内部的 running/waiting 队列管理就是 continuous batching。Omni 在 stage worker 层做的是跨请求攒批 (max_batch_size + batch_timeout),然后一次性喂给 vLLM engine,由 vLLM 内部做 token 级别的连续批处理。
(3)模型加载与实现(TTS 模型怎么进来的)
注册机制 :基于字典的声明式注册(不是 @register 装饰器)。
源码 :vllm_omni/model_executor/models/registry.py
_OMNI_MODELS字典(registry.py:3-56):架构名 →(模块目录, 模块文件, 类名)。OmniModelRegistry = _ModelRegistry(...)(registry.py:65-82):构建懒加载注册表,组合 vLLM 的_VLLM_MODELS和_OMNI_MODELS。
Qwen3-TTS 的注册条目(registry.py:51-55):
python
"Qwen3TTSForConditionalGeneration": ("qwen3_tts", "qwen3_tts", "Qwen3TTSModelForGeneration")
Qwen3-TTS 实现 (vllm_omni/model_executor/models/qwen3_tts/qwen3_tts.py):
Qwen3TTSModelForGeneration(:65):vLLM 可运行的封装器。__init__(:66):通过Qwen3TTSModel.from_pretrained加载 HF 模型,设self.have_multimodal_outputs = True。forward(:91):分发到generate_custom_voice/generate_voice_design/generate_voice_clone(:137-148)。compute_logits(:272):返回None(非自回归)。load_weights(:259):空操作 ------权重在from_pretrained里用 HF 的AutoModel加载,不走 vLLM 的 loader。
HF 顶层模型 (modeling_qwen3_tts.py):
Qwen3TTSForConditionalGeneration(:1796):持有self.talker(:1803)、self.speaker_encoder(ECAPA-TDNN,:277,仅 Base 模型)、self.speech_tokenizer(:1810,懒加载)。from_pretrained(:1838):加载主模型后,额外下载speech_tokenizer/子目录(:1871),并加载Qwen3TTSTokenizer(:1892)。extract_speaker_embedding(audio, sr)(:1918):建 mel →self.speaker_encoder→ 说话人向量(声音克隆用)。
💡 重要差异 :Qwen3-TTS 的权重加载走的是 HF 的
AutoModel,不是 vLLM 的权重加载器 (所以load_weights是空操作)。这是它跟一般 vLLM 文本模型最大的不同之一------也意味着 TTS 的量化/加载优化目前不像纯文本 LLM 那么成熟。
(4)音频前后处理(TTS 的「输入输出转换」)
输入处理(文本→token、参考音频、说话人向量):
- 文本 tokenize:
qwen3_tts.py:508(_tokenize_texts)。 - 声音克隆 prompt 构建:
qwen3_tts.py:589(create_voice_clone_prompt)------标准化参考音频 →speech_tokenizer.encode取ref_codes(:668)→ 重采样 →extract_speaker_embedding(:693)。支持两种模式:- x-vector only:只用说话人向量。
- ICL(上下文学习) :需要
ref_text,用参考音频的 codec codes 做上下文。
- 说话人向量提取有两条路径:
- ECAPA-TDNN (
modeling_qwen3_tts.py:277,PyTorch,Base 模型用)。 - campplus.onnx (
qwen3_tts/tokenizer_25hz/vq/speech_vq.py:144,ONNX Runtime CPU,25Hz codec 路径用)。
- ECAPA-TDNN (
输出处理(声学 token → mel → PCM 流式拼接):
不同模型的 vocoder 实现不一样:
| 模型 | 声码器实现 | 关键文件 |
|---|---|---|
| Qwen2.5-Omni | DiT(流匹配,RungeKutta4 ODE)+ BigVGAN | qwen2_5_omni/qwen2_5_omni_token2wav.py:1446 |
| Qwen3-Omni | codec → 波形(无 mel 阶段,1280× 上采样) | qwen3_omni/qwen3_omni_code2wav.py:34 |
| Qwen3-TTS | codec → mel(DiT) → 波形(BigVGAN),通过 speech_tokenizer.decode | qwen3_tts/qwen3_tts_tokenizer.py:261 |
流式分块拼接(保证音频 chunk 边界无裂纹/pop 音):
- Qwen2.5-Omni:
qwen2_5_omni_token2wav.py:1564(process_little_chunk)、:1632(_process_chunk_for_50hz)。 - Qwen3-Omni:
qwen3_omni_code2wav.py:160(chunked_decode,left_context_size=25重叠去伪影)、:199(chunked_decode_streaming)。
音频编解码(RVQ codec / Mel):
Qwen3TTSTokenizer(qwen3_tts_tokenizer.py:46):25Hz/12Hz speech tokenizer 封装,encode(:210)/decode(:261)。- RVQ 核心(纯 PyTorch):
tokenizer_25hz/vq/core_vq.py(EuclideanCodebook:114、VectorQuantization:245、DistributedResidualVectorQuantization:334)。 WhisperEncoderVQ(speech_vq.py:189)、MelSpectrogramFeatures(speech_vq.py:41)。
引擎输出路由 :vllm_omni/engine/output_processor.py:430(_process_audio_output)------从 multimodal_outputs 里按 ("audio","audios","wav","waveform","audio_pcm","pcm") 等键提取音频张量(:434),暴露为 pooling_output。
⚠️ 再次强调 :音频路径没有任何自定义 CUDA 算子 。RVQ、STFT、BigVGAN、Kaiser-sinc 上采样、flow-matching ODE 全是纯 PyTorch。唯一的非 PyTorch 是
campplus.onnx(ONNX Runtime CPU)。这影响你对「算子级优化空间」的预期------目前 TTS 的优化主要在调度和流水线层,不在底层 kernel。
(5)服务接口(HTTP/OpenAI 兼容)
CLI 入口:
vllm_omni/entrypoints/cli/main.py:12------ 检测--omni标志分流。vllm_omni/entrypoints/cli/serve.py:41------OmniServeCommand,name="serve"。
路由注册 (vllm_omni/entrypoints/openai/api_server.py):
:772------ 先移除上游默认的/v1/audio/speech。:775-800------ 注册新的POST /v1/audio/speech(create_speech)。:803-819------ 注册GET /v1/audio/voices(list_voices,返回可用音色)。
TTS speech 接口实现 :vllm_omni/entrypoints/openai/serving_speech.py:40(OmniOpenAIServingSpeech(OpenAIServing, AudioMixin))。
_TTS_MODEL_STAGES = {"qwen3_tts"}(:21)------靠这个判断是否是 TTS 模型。_load_supported_speakers(:47)------从hf_config.talker_config.spk_id加载音色。create_speech(:183)------主流程:校验 → 拼 prompt → 调engine_client.generate(output_modalities=["audio"])→ 提取音频 → 编码返回。
音频编码 (audio_utils_mixin.py:50-57):
python
supported_formats = {
"wav": ("WAV", "audio/wav", {}),
"pcm": ("RAW", "audio/pcm", {"subtype": "PCM_16"}),
"flac": ("FLAC", "audio/flac", {}),
"mp3": ("MP3", "audio/mpeg", {}),
"aac": ("AAC", "audio/aac", {}),
"opus": ("OGG", "audio/ogg", {"subtype": "OPUS"}),
}
底层用 soundfile 编码;speed != 1.0 时用 librosa.effects.time_stretch 变速不变调(:76-93)。
gRPC 接口:当前 TTS 路径主要暴露 HTTP/OpenAI 兼容接口;gRPC 更通用在 Omni 的 chat 路径。TTS 日常用 HTTP 即可。
(6)Stage 间通信(OmniConnector)
作用:stage 之间传数据(latent、codec codes、KV cache)。
双层架构:
- 轻量通知走
mp.Queue(omni.py:107-108的_stage_in_queues/_stage_out_queues)。 - 重量数据走 OmniConnector。
三种实现 (vllm_omni/distributed/omni_connectors/connectors/):
| Connector | 适用 | 文件 |
|---|---|---|
| SharedMemoryConnector(默认) | 单机,用 /dev/shm + 文件锁 |
shm_connector.py:18 |
| MooncakeConnector | 多节点(RDMA/SHM) | mooncake_connector.py |
| YuanrongConnector | 多节点 | yuanrong_connector.py |
SHM 阈值机制 (stage_utils.py:144,174,218):序列化后 > shm_threshold_bytes(默认 65536)的数据写共享内存,否则走进程队列。
⚠️ 当前所有 connector 都是 D2H2D(device→host→device) 模式------数据要先拷到 CPU 再传。未来才有 D2D(直接 GPU 间传输,用 NCCL/UCX)。这意味着跨 stage 传输大张量(如 latent)目前有 CPU 序列化开销,是延迟优化点。
(7)配置层(OmniModelConfig)
源码 :vllm_omni/config/model.py:35(OmniModelConfig(ModelConfig))。
关键字段(config/model.py:62-75):
| 字段 | 默认 | 含义 |
|---|---|---|
stage_id |
0 | stage 编号 |
async_chunk |
False | 跨 stage 异步流式分块(TTS 流式关键开关) |
model_stage |
"thinker" | thinker/talker/code2wav/qwen3_tts |
model_arch |
"Qwen2_5Omni..." | 注册表里的架构类名 |
engine_output_type |
None | text/audio/latent(输出路由) |
hf_config_name |
None | stage 专属 hf config(如 talker_config) |
stage_connector_config |
SharedMemoryConnector | stage 间通信后端 |
omni_kv_config |
None | KV 跨 stage 转移配置 |
命令行参数:vllm_omni/engine/arg_utils.py:48(OmniEngineArgs)、:154(AsyncOmniEngineArgs)。其余 TTS/audio/concurrency 参数继承自 vLLM 的 EngineArgs(max_num_seqs、max_num_batched_tokens、gpu_memory_utilization 等)。
3.3 高频调优场景代码定位速查表
| 调优场景 | 主要修改文件 | 核心入口 | 改动要点 |
|---|---|---|---|
| 调整 TTS 并发上限 | vllm_omni/model_executor/stage_configs/qwen3_tts.yaml |
runtime.max_batch_size |
当前=1(串行),调大需充分测稳定性;同时关注 gpu_memory_utilization 是否够 |
| 优化首包延迟 | stage config + entrypoints/cli/serve.py:120 |
--batch-timeout、async_chunk |
减小 batch_timeout;Omni 模型开 async_chunk: true(用 qwen3_omni_moe_async_chunk.yaml) |
| 改 TTS 前后处理 | model_executor/models/qwen3_tts/qwen3_tts.py |
generate_custom_voice:956、create_voice_clone_prompt:589 |
改 mel 参数/声码器;注意 forward 分发逻辑 |
| 换/加音色 | serving_speech.py:47 + 模型 hf_config |
_load_supported_speakers |
音色来自 hf_config.talker_config.spk_id |
| 新增自定义 TTS 接口 | entrypoints/openai/api_server.py + serving_speech.py |
路由注册 :775 |
仿照 create_speech 加路由;复用 engine_client.generate(output_modalities=["audio"]) |
| 调整显存分配 | stage config YAML | engine_args.gpu_memory_utilization |
单 GPU 多 stage 共享时,各 stage 配额之和 ≤ 1(见 docs/configuration/gpu_memory_utilization.md) |
| 改跨 stage 数据传输 | distributed/omni_connectors/ |
SharedMemoryConnector:shm_connector.py:18 |
调 shm_threshold_bytes;多节点换 Mooncake/Yuanrong |
| 看性能瓶颈 | vllm_omni/metrics/stats.py |
OrchestratorAggregator:388 |
跑 --log-stats,看 e2e_stage_N_wall_time_ms、transfers_total_time_ms 找瓶颈 stage |
四、核心加速技术原理全面详解
本章是全文技术深度最高的部分,不简化,每一项都详细展开。
统一四点结构:① 解决了什么痛点(用 TTS 经验描述)② 底层原理 + TTS 适配改造 ③ 源码落地 ④ 性能收益 + 触发条件。
先给你一张「复用 vs 改造」总览表(贯穿本章):
| 加速技术 | 复用 vLLM | Omni 改造点 | 对 TTS 的价值 |
|---|---|---|---|
| PagedAttention | 算子 + KVCacheManager | 调度器做 block_ids 截断支持跨 stage KV 迁移 | 高(code2wav 之外的自回归 stage) |
| 连续批处理 | 调度骨架 | 为一步生成 stage 写 fast-path | 极高(多请求并发的核心) |
| 动态调度 | 调度器基类 | token 预算 + 显存回退 | 高 |
| KV cache / Prefix caching | 完整功能 | TTS 路径默认关;用跨 stage KV 转移替代 | 中(latent 输出不支持 prefix) |
| 权重量化 | 算子 + 校验 | 仅透传 quant_config | 中(TTS 权重走 HF loader,收益有限) |
| 投机采样 | EAGLE/ngram | 新增 Qwen3 MTP(codec 多码本一次出) | 高(talker stage) |
| CUDA Graph | 包装器 | YAML 默认 enforce_eager | 低(当前默认关) |
| 算子融合 | RMSNorm/QKV/RoPE/MRoPE | 无自写 kernel | 中(仅自回归部分) |
| 音频/说话人缓存 | mm encoder cache | 自有 prompt_embeds_cpu、VoiceClonePromptItem 预提取复用 | 极高(声音克隆场景) |
| 流式分块 | --- | Omni 自有:chunked_decode_streaming + async_chunk | 极高(实时性) |
4.1 通用加速技术(复用/改造自 vLLM)
技术 1:PagedAttention(KV Cache 分页管理)
① 解决了什么痛点
你部署原生 TTS 时,decoder 在推理每个 token 时都要存这个 token 对应的 K(Key)和 V(Value)向量,用来算 attention。传统做法是为每条序列预分配一块连续显存(按最大长度算)。问题:
- 如果序列比预期短,预留的显存白白浪费(内部碎片)。
- 如果序列比预期长,得重新分配更大的块再拷贝(昂贵的显存搬运)。
- 多条序列并发时,显存里东一块西一块,碎片化严重,能并发的请求数大打折扣。
② 底层原理 + TTS 适配
PagedAttention 借用了操作系统虚拟内存的分页思想:把 KV cache 的显存切成固定大小的「页(block)」(比如每页存 16 个 token 的 KV),按需分配。一条序列用到哪页申请哪页,序列的 KV cache 用一张「页表」逻辑串联起来。
💡 类比:传统做法像「给每个客人预留一整间包厢,不管他来几个人」;PagedAttention 像「餐厅按人头拼桌,来几个坐几个位置」。同样的场地,能服务的客人多得多。
TTS 场景的适配改造:
- AR stage(thinker/talker)的 KV cache 管理完全复用 vLLM 的 PagedAttention。
- 跨 stage KV 转移(Omni 独有):当 thinker stage 算完,它的 KV cache 可以「转移」给下游 stage 复用,而不是释放掉重来。Omni 调度器为此做了 block_ids 的截断/拷贝。
③ 源码落地
| 内容 | 位置 |
|---|---|
| 分页块分配(generation stage 调用) | vllm_omni/core/sched/omni_generation_scheduler.py:82-86, 128-132(self.kv_cache_manager.allocate_slots(...)) |
| block_ids 截断(跨 stage KV 迁移用) | vllm_omni/core/sched/omni_ar_scheduler.py:564-587(_mark_request_for_kv_transfer) |
| KV 释放 | omni_ar_scheduler.py:462(self.kv_cache_manager.free(req)) |
| 跨 stage KV 转移管理 | vllm_omni/distributed/omni_connectors/kv_transfer_manager.py:47(OmniKVTransferManager) |
注意:PagedAttention 算子本身完全复用 vLLM,Omni 没有重写 kernel。Omni 只在调度器层做了 block_ids 的拷贝/截断以支持跨 stage KV 转移。
④ 性能收益 + 触发条件
- 收益 :显存碎片从「按最大长度预留」降到「按实际使用」,典型场景下能支持的并发请求数提升数倍(vLLM 原论文报告 KV cache 显存浪费从 ~60-80% 降到 <4%)。
- 触发条件:默认开启,无需配置(vLLM v1 架构内置)。
- TTS 注意:Qwen3-TTS 这种「一步生成」模型,KV cache 的作用主要在 talker 的自回归部分;code2wav 阶段不是逐 token 的,PagedAttention 价值相对小。
技术 2:连续批处理(Continuous Batching)
① 解决了什么痛点
你跑传统 TTS 批处理(static batching)时:凑够 N 个请求组成一个 batch,一起送 GPU。问题:这 N 个请求里,有的句子长有的短,短的先算完了,却得等最长的那个算完,整个 batch 才能释放 ------GPU 大量时间在「陪跑」短请求。更糟的是,得等 batch 凑满才开始算,第一个请求被拖着白白等了凑批时间。
② 底层原理 + TTS 适配
连续批处理(也叫 iteration-level batching)的核心:不按 batch 粒度调度,按「每一步(iteration)」粒度调度 。每算完一个 token 步骤,调度器就重新看一遍队列------有请求完成了就踢出去,有新请求来了就插进来。请求即来即算、即完即走,GPU 几乎不停闲。
💡 类比:static batching 像「大巴车,凑满 40 人才发车,有人中途下车也不停」;continuous batching 像「公交车,每站都停,随时上下客,车一直在跑」。吞吐量天差地别。
TTS 场景适配:
- AR stage(thinker/talker):直接复用 vLLM 的连续批处理(
omni_ar_scheduler.py:154调super().schedule())。 - generation stage(code2wav,TTS 核心)专门写了 fast-path :因为 code2wav 是「一步生成」,vLLM 原生 prefill/decode 循环不适用,Omni 写了
OmniGenerationScheduler.schedule()(omni_generation_scheduler.py:39-261),一次性把请求所有 input token 调度进去,单步出结果。
③ 源码落地
| 内容 | 位置 |
|---|---|
| AR stage 连续批处理(调父类) | omni_ar_scheduler.py:154-155 |
| generation stage fast-path(边到边连续批) | omni_generation_scheduler.py:39-261(running 队列 65-100,waiting 队列 108-149) |
| 单步即完结语义 | omni_generation_scheduler.py:356-375(update_from_output 设 FINISHED_STOPPED) |
| stage worker 跨请求攒批 | omni_stage.py:770-833(max_batch_size + batch_timeout) |
举个 TTS 调度时序例子(帮你直观理解):
假设 code2wav stage,max_batch_size=4,同时来了 3 个合成请求(A=2秒音频、B=5秒、C=1秒):
时刻 t0: A,B,C 到达 → stage worker 攒批成 [A,B,C],一次性送进 OmniGenerationScheduler
时刻 t1: GPU 一步算出 A,B,C 全部音频 → 三个请求同时 FINISHED → out_q
时刻 t2: 又来 D,E → 攒批 [D,E] → 送 GPU
(注:code2wav 是一步生成,所以「连续批」体现为「攒多个请求一起算一步」。如果是 talker 这种自回归 stage,则是真正的「逐 token 步骤级别」连续批处理,长短请求交错。)
④ 性能收益 + 触发条件
- 收益 :官方 Qwen3-Omni 数据(
docs/cli/bench/serve.md):单流 73 tok/s → 并发 2603 tok/s(约 35×) ,TPOT 从 481ms 降到 0.73ms。 - 触发条件 :
max_batch_size > 1(⚠️ Qwen3-TTS 默认是 1,你要改 stage config 才能享受并发红利)。- 请求有足够并发度(得真的有多个请求同时来)。
- 显存够(PagedAttention 是基础,没它并发不起来)。
- TTS 注意:纯 Qwen3-TTS(单 stage)的并发收益需要你自行测试验证(官方 35× 是 Omni 模型数据)。
技术 3:动态调度(Dynamic Scheduling)
① 解决了什么痛点
固定调度策略(比如「先来先服务」或「按固定 batch size」)在面对请求长度差异大、显存压力波动的场景时表现差:要么短请求被长请求阻塞,要么显存吃紧时还硬塞请求导致 OOM。
② 底层原理 + TTS 适配
动态调度:调度器在每一步实时评估「token 预算还剩多少」「显存还能不能塞下新请求」,据此决定是「从 waiting 队列拉新请求」还是「暂停拉取」。请求长度、显存余量、batch 构成都被动态考虑。
TTS 适配:
- token 预算动态分配:
omni_generation_scheduler.py:47, 80-81, 126-127(token_budget = self.max_num_scheduled_tokens,num_new_tokens = min(required_tokens, token_budget))。 - 显存压力回退:
:87-92, 133-138(new_blocks is None时 break,避免 OOM)。 - 每个 stage 独立配
max_num_batched_tokens(TTS 音频 token 序列长,所以 code2wav 配了 1000000 这种大值)。
③ 源码落地
| 内容 | 位置 |
|---|---|
| 调度器基类 | omni_ar_scheduler.py:41(OmniARScheduler(VLLMScheduler)) |
| token 预算动态分配 | omni_generation_scheduler.py:47, 80-81, 126-127 |
| 显存压力回退 | omni_generation_scheduler.py:87-92, 133-138 |
| 每 stage 独立 token 上限 | stage config max_num_batched_tokens |
④ 性能收益 + 触发条件
- 收益:避免 OOM 崩溃;在混合长短请求时保持高吞吐(短请求不被长请求完全拖死)。
- 触发条件:默认开启(vLLM 调度器内置行为)。
- TTS 调参 :code2wav 的
max_num_batched_tokens设很大(1000000)是因为音频序列动辄几千 token,不能被默认的小上限卡住。
技术 4:KV Cache 优化 / Prefix Caching
① 解决了什么痛点
很多请求有公共前缀(比如同一个系统 prompt、同一个说话人 prompt 前缀)。传统做法每个请求都重新算一遍这些前缀的 KV cache,浪费算力。
② 底层原理 + TTS 适配
Prefix caching:把公共前缀的 KV cache 缓存下来,后续请求命中相同前缀就直接复用,跳过 prefill 计算。
TTS 场景的关键事实:
- ⚠️ 所有 TTS/Omni stage config 里
enable_prefix_caching: false(qwen3_tts.yaml:15、qwen3_omni_moe.yaml:24,54,84等)。 - 原因(
docs/configuration/stage_configs.md:207-211):输出 latent(hidden states)的 stage 不支持 prefix caching。而 Omni 模型的 thinker/talker 输出 latent,所以不能开。 - Omni 的替代方案 :跨 stage KV 转移(
OmniKVTransferManager)。当一个 stage 算完,KV cache 可以转移给下游复用,而不是丢弃。
③ 源码落地
| 内容 | 位置 |
|---|---|
| Prefix caching 开关 | vLLM EngineArgs.enable_prefix_caching(TTS 默认 false) |
| 关闭原因 | docs/configuration/stage_configs.md:207-211 |
| 跨 stage KV 转移(替代方案) | omni_ar_scheduler.py:494-548(_should_transfer_kv_for_request + _free_request) |
| KV 转移配置 | config/model.py:75(omni_kv_config:need_send_cache、kv_transfer_criteria) |
| 转移触发条件 | omni_ar_scheduler.py:100-152(prefill_finished / special_token) |
④ 性能收益 + 触发条件
- Prefix caching 收益 :纯文本 LLM 场景能省 30-50% prefill 算力。但 TTS 路径当前默认关闭。
- 跨 stage KV 转移收益:Omni 多 stage 模型避免下游重复计算上游的 KV,是 Omni 流水线效率的关键。
- 触发条件 :prefix caching 需
enable_prefix_caching: true(TTS 慎用);KV 转移需配置omni_kv_config。
技术 5:权重量化(AWQ / GPTQ / FP8)
① 解决了什么痛点
模型权重存成 FP16/BF16,显存占用大、显存带宽压力大(推理往往是 memory-bound,瓶颈在把权重从显存搬到计算单元)。量化把权重压到 INT4/INT8/FP8,显存省一半到四分之一,带宽压力也降,吞吐提升。
② 底层原理 + TTS 适配
- AWQ/GPTQ:训练后量化,把权重压到 INT4/INT8,保留少量高精度缩放因子。
- FP8:用 8 位浮点(Hopper 架构原生支持),精度损失小。
TTS 场景的关键事实:
- 量化算子完全复用 vLLM(compressed_tensors/awq/gptq/fp8 通路)。
- TTS 子模块通过
quant_config参数透传给QKVParallelLinear等层。 - ⚠️ 但 Qwen3-TTS 的主权重走 HF 的
AutoModel加载(load_weights是空操作),所以 TTS 主体权重的量化收益目前有限------量化主要作用于 code predictor 等用 vLLM 线性层的子模块。 - ⚠️ 音频对量化更敏感:语音质量(MOS)受量化影响比文本更明显,需要评估。
③ 源码落地
| 内容 | 位置 |
|---|---|
| 量化配置注入(code predictor 注意力层) | vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_code_predictor_mtp.py:71-88(QKVParallelLinear(..., quant_config=...)) |
QuantizationConfig 导入 |
qwen3_omni_moe_code_predictor_mtp.py:32 |
| 量化校验 | vllm_omni/config/model.py:303(_verify_quantization()) |
④ 性能收益 + 触发条件
- 收益:INT4 量化约省 75% 权重显存,吞吐提升 1.5-3×(视模型和硬件)。
- 触发条件 :模型本身要有量化版权重(如
*-AWQ、*-FP8),通过--quantization参数指定。 - TTS 注意:务必评估量化后的音频 MOS,语音质量比文本更脆弱。
技术 6:投机采样(Speculative Decoding)
① 解决了什么痛点
大模型逐 token 生成,每出一个 token 都要跑一次完整 forward。但很多 token 是「容易预测的」(高频词、固定模式)------能不能先用个小模型快速「猜」一批,再让大模型一次性验证?猜对的就白赚,猜错的才重新算。
② 底层原理 + TTS 适配
- EAGLE/n-gram(vLLM 复用):用小 draft 模型或 n-gram 预测若干 token,大模型并行验证,命中则一次出多个 token。
- Omni 独有的 MTP(Multi-Token Prediction):Qwen3-Omni 的 codec 预测------一次预测多个 RVQ 码本层(残差预测),让 talker stage 一次出多层 codec codes。
TTS 场景:talker stage 生成 codec codes 时,用 MTP 一次出多码本,减少自回归步数。
③ 源码落地
| 内容 | 位置 |
|---|---|
| vLLM EAGLE/ngram 接入 | vllm_omni/worker/gpu_ar_model_runner.py:460-514(EagleProposer 导入于 :25) |
| spec_decode 统计上报 | omni_ar_scheduler.py:257-279、omni_generation_scheduler.py:329-345 |
| Omni MTP(codec 多码本一次出) | vllm_omni/model_executor/models/qwen3_omni/qwen3_omni_moe_code_predictor_mtp.py:1-7 |
| talker_mtp 调用(包进 CUDA Graph) | vllm_omni/worker/gpu_model_runner.py:1019-1026, 1050-1077(_talker_mtp_forward) |
④ 性能收益 + 触发条件
- 收益:EAGLE 在文本 LLM 上典型 2-3× 加速;MTP 让 codec 多码本一次出,减少 talker 步数。
- 触发条件 :需要 draft 模型或 MTP 权重;命中率依赖采样温度(TTS 常用高温度采样,命中率可能下降,需权衡)。
- 冲突限制:低温度/贪心解码命中率最高;高温度采样命中率下降。
技术 7:CUDA Graph 加速
① 解决了什么痛点
GPU 每次执行一个 kernel,CPU 都要发一次 launch 指令(launch overhead)。模型 forward 是成百上千个 kernel 串起来的,CPU 发指令的开销在小 batch、短计算时甚至比 GPU 算还慢------这叫 kernel launch bound。
② 底层原理 + TTS 适配
CUDA Graph:把一串 kernel 调用「录制」成一个图,之后一次 launch 整个图,消除逐 kernel 的 launch overhead。vLLM 把 forward + sampling 录制成图复用。
TTS 场景的关键事实:
- ⚠️ 所有 TTS/Omni stage config 默认
enforce_eager: true(qwen3_tts.yaml:12等),即默认关闭 CUDA Graph。 - 原因(
docs/configuration/stage_configs.md:189-193):框架目前主要在 eager 模式下验证稳定。 - 仅 talker 的 MTP 部分被包进 CUDA Graph(
gpu_model_runner.py:50-53)。
③ 源码落地
| 内容 | 位置 |
|---|---|
| CUDAGraphMode 判定 | vllm_omni/worker/gpu_ar_model_runner.py:14, 217, 267 |
| CUDAGraphWrapper 包装 talker_mtp | vllm_omni/worker/gpu_model_runner.py:5, 50-53 |
| 强制 eager(所有 stage) | stage config enforce_eager: true |
| 校验入口 | vllm_omni/config/model.py:304(_verify_cuda_graph()) |
④ 性能收益 + 触发条件
- 收益:小 batch 短序列场景,launch overhead 占比高,CUDA Graph 能带来 10-30% 提速。
- 触发条件 :
enforce_eager: false(但 ⚠️ TTS 当前默认 true,强行关闭需充分测试)。 - TTS 现状:目前享受不到,等框架支持非 eager 后再说。
技术 8:算子融合(Operator Fusion)
① 解决了什么痛点
模型里很多操作是连续的小算子(比如 RMSNorm 后接 attention,attention 里 Q/K/V 三个矩阵乘法)。每个算子都要读写一次显存。把多个小算子合并成一个大算子,减少显存读写往返,提速。
② 底层原理 + TTS 适配
典型融合:
- QKV 融合 :把 Q、K、V 三个独立矩阵乘法合并成一个(
QKVParallelLinear)。 - RMSNorm + 量化融合。
- RoPE(旋转位置编码)融合进 attention。
- MRoPE:多模态位置编码(音频/视频/图像联合)。
TTS 场景的关键事实:
- 所有融合算子直接复用 vLLM
vllm.model_executor.layers.*,Omni 没有自写任何 CUDA kernel。 - 这些融合主要作用于自回归部分 (thinker/talker 的 transformer 层);TTS 的音频专属算子(RVQ、STFT、BigVGAN)目前没有融合优化(都是纯 PyTorch 逐算子执行)。
③ 源码落地
| 内容 | 位置 |
|---|---|
| 融合 RMSNorm / RoPE / QKV | qwen3_omni/qwen3_omni_moe_code_predictor_mtp.py:26-33(RMSNorm、QKVParallelLinear、MergedColumnParallelLinear、get_rope) |
| q_norm/k_norm + Rotary 融合 | qwen3_omni_moe_code_predictor_mtp.py:89-100 |
| MRotaryEmbedding(多模态位置编码) | vllm_omni/worker/gpu_model_runner.py:10,_init_mrope_positions gpu_model_runner.py:63-119 |
④ 性能收益 + 触发条件
- 收益:QKV 融合约省 1/3 的显存读写;RMSNorm 融合省一次显存 round-trip。
- 触发条件:使用 vLLM 的标准 transformer 层即自动生效。
- TTS 注意:音频专属算子(BigVGAN 等)目前无融合,是潜在优化方向(需自己写 kernel)。
4.2 TTS/音频专属加速技术
这些是 vLLM-Omni 独有 或在 TTS 上特别重要的加速技术。
技术 9:音频特征 / 说话人嵌入缓存
① 解决了什么痛点
声音克隆(Base 模型)场景:你给一段参考音频,模型要先提取说话人向量(x-vector)和参考 codec codes。如果同一个说话人要合成很多句话,每次都重新提取一遍参考音频特征,纯属浪费------提取一次要跑一遍 Whisper 编码 + VQ + ECAPA/campplus,很贵。
② 底层原理 + TTS 适配
把参考音频的说话人向量、参考 codec codes 预提取出来缓存,后续同说话人的请求直接复用。VoiceClonePromptItem 容器封装这些预提取结果,可跨请求复用。
③ 源码落地
| 内容 | 位置 |
|---|---|
| 跨 stage prompt_embeds 缓存(CPU 暂存→prefill 覆盖) | vllm_omni/worker/gpu_model_runner.py:216(prompt_embeds_cpu)、:856-877(_collect_additional_information_for_prefill) |
| additional_information 跨步缓存 | gpu_model_runner.py:225-252, 783-855 |
| 多模态编码器缓存(vLLM 复用) | gpu_model_runner.py:145(self.encoder_cache.pop(mm_hash));大小由 mm_processor_cache_gb 控制(config/model.py:273) |
| 说话人向量/参考码预提取复用 | model_executor/models/qwen3_tts/qwen3_tts.py:50-62(VoiceClonePromptItem),预提取 create_voice_clone_prompt:589-706,跨请求复用 :807-815 |
| codec 内部 KV 复用(HF DynamicCache) | modeling_qwen3_tts.py:1064-1065 |
④ 性能收益 + 触发条件
- 收益:声音克隆场景,同说话人多句话合成时,省掉重复的参考音频编码(可能省 30-50% 单请求延迟,取决于参考音频长度)。
- 触发条件 :同说话人多次请求;业务层需要做 prompt_items 的缓存管理(按说话人 ID 缓存
VoiceClonePromptItem)。
技术 10:TTS 分块流式推理(Chunked Streaming)
① 解决了什么痛点
合成一段长音频(比如 30 秒),如果等整段算完才返回,用户要等好几秒才听到第一个音------首包延迟极高,体验差。而且一次性算长音频容易 OOM(中间激活值巨大)。
② 底层原理 + TTS 适配
分块流式:把长音频切成多个 chunk,算完一个 chunk 就推送一个,客户端边收边播。同时用「重叠窗口」(left_context)处理 chunk 边界,避免拼接处出现裂纹/pop 音。
两个层次:
- 单 stage 内的分块解码 :code2wav 把 codec codes 切块,逐块出波形(
chunked_decode_streaming)。 - 跨 stage 的异步流式 (
async_chunk):上游 stage 算出一部分,立刻通过 connector 推给下游,下游不用等上游全算完。这是降低端到端首包延迟的核心机制。
③ 源码落地
| 内容 | 位置 |
|---|---|
| codec 分块解码(去边界伪影) | qwen3_omni/qwen3_omni_code2wav.py:160-197(chunked_decode,chunk_size=300, left_context_size=25)、:199-230(chunked_decode_streaming) |
| 跨 stage 流式分块配置 | config/model.py:63(async_chunk)、engine/arg_utils.py:75,177 |
| connector chunk 收发 | distributed/omni_connectors/adapter.py:204-272(get_chunk)、:275-306(get_chunk_for_generation)、:308-352(put_chunk) |
| 启用 YAML | model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml(async_chunk: true) |
| 多模态输出累积/合并 | engine/output_processor.py(OmniRequestState.add_multimodal_tensor) |
| Qwen2.5 mel 分块拼接 | qwen2_5_omni_token2wav.py:1564(process_little_chunk)、:1632(_process_chunk_for_50hz) |
④ 性能收益 + 触发条件
- 收益:首包延迟从「等整段算完」降到「等第一个 chunk 算完」。例如 30 秒音频,可能从 ~10 秒首包降到 ~1-2 秒。
- 触发条件 :
- Omni 模型:stage config 设
async_chunk: true(用qwen3_omni_moe_async_chunk.yaml)。 - ⚠️ 纯 Qwen3-TTS 在线服务目前对外不开放流式 (
stream_format="sse"被拒绝,见 1.3 节)。分块机制在内部已实现,但/v1/audio/speech接口还没接上流式输出(RFC #938 待完成)。
- Omni 模型:stage config 设
- 现状提醒:内部机制就绪 ≠ 对外可用。等流式接口开放后,这项技术对实时 TTS 是杀手锏。
技术 11:多模态编码器并行 / Stage 流水线重叠
① 解决了什么痛点
Omni 模型有多个 stage(thinker→talker→code2wav)。如果严格串行(thinker 全算完 → talker 开始 → code2wav 开始),下游 stage 在上游算的时候一直空闲,GPU 利用率低。
② 底层原理 + TTS 适配
Pipeline 并行:stage 之间重叠执行 。上游算出一部分就推给下游,下游立刻开始算自己的部分,上下游形成流水线。配合 async_chunk,stage 间通过 connector 流式传递 chunk。
标准模式(async_chunk: false,window_size: -1):stage 严格串行,上游完整完成才触发下游。
异步模式(async_chunk: true):stage 间流式重叠。
③ 源码落地
| 内容 | 位置 |
|---|---|
| async_chunk 开启 | stage config async_chunk: true(qwen3_omni_moe_async_chunk.yaml:7) |
| put_chunk / get_chunk | adapter.py:308(put_chunk)、:204(get_chunk) |
| chunk 处理函数 | stage_input_processors/qwen3_omni.py:86(thinker2talker_async_chunk)、:208(talker2code2wav_async_chunk) |
| 异步 orchestrator 协调 | async_omni.py:362(_process_async_results) |
| runtime edges 配置 | stage config runtime.edges(window_size、max_inflight) |
④ 性能收益 + 触发条件
- 收益:多 stage 模型整体吞吐提升(stage 重叠执行,减少空闲);首包延迟下降(下游提前开始)。
- 触发条件 :多 stage 模型(Omni);设
async_chunk: true;注意 chunk 边界处理正确性。 - Qwen3-TTS(单 stage)不涉及此项------它是单 stage,没有 stage 间重叠问题。
技术 12:Mel 谱 / 声学 Token 高效编解码
① 解决了什么痛点
音频是连续波形(每秒 24000 个采样点),直接喂给模型数据量太大。需要高效的「音频 ↔ 离散表示」转换:编码时把波形压成紧凑的声学 token(codec codes),解码时还原。
② 底层原理 + TTS 适配
- RVQ(Residual Vector Quantization):把连续特征量化成多层(如 8 层/16 层)残差码本,第一层粗粒度,后续层补细节。大幅压缩音频表示。
- Mel 谱:STFT + mel 滤波器组,把波形转成对数 mel 频谱(声码器输入)。
- Whisper 编码器 + VQ:先用 Whisper 编码音频,再 VQ 量化。
③ 源码落地
| 内容 | 位置 |
|---|---|
| RVQ 核心(纯 PyTorch) | qwen3_tts/tokenizer_25hz/vq/core_vq.py(EuclideanCodebook:114、VectorQuantization:245、DistributedResidualVectorQuantization:334) |
| Whisper + VQ 编码器 | speech_vq.py:189(WhisperEncoderVQ) |
| Mel 谱特征 | speech_vq.py:41(MelSpectrogramFeatures)、whisper_encoder.py:119(get_mel_audio) |
| speech tokenizer 封装 | qwen3_tts_tokenizer.py:46(Qwen3TTSTokenizer,encode:210/decode:261) |
④ 性能收益 + 触发条件
- 收益:这是「能跑」的基础而非「加速」------把音频压到离散 token 才能用 LLM 范式生成。Qwen3-TTS 用 12Hz/25Hz tokenizer(每秒 12/25 个 token),远小于原始 24000 采样点。
- 触发条件:模型自带,无需配置。
- 优化空间:当前这些算子是纯 PyTorch,没有 kernel 优化------是潜在加速方向。
技术 13:音频批处理维度对齐 / Mel 长度对齐
① 解决了什么痛点
多个 TTS 请求的音频长度不同,拼成一个 batch 时,tensor 维度对不齐(batch 要求是规整矩阵)。直接 padding 会浪费计算;处理不好会维度报错。
② 底层原理 + TTS 适配
- mel 长度对齐:确保 mel 帧数是 codec 扩展因子(
repeats)的倍数(DiT 采样器需要)。 - 音频 tensor 合并时,形状不一致时跳过
torch.cat,避免报错。
③ 源码落地
| 内容 | 位置 |
|---|---|
| mel 长度对齐 | qwen2_5_omni/audio_length.py:25(cap_and_align_mel_length,确保 mel 长度是 repeats 倍数) |
| DiT 采样器使用 | qwen2_5_omni_token2wav.py:1275 |
| 音频 tensor 合并(形状不一致跳过 cat) | engine/output_processor.py:111-119(_consolidate_multimodal_tensors,特殊处理 "audio" 键) |
④ 性能收益 + 触发条件
- 收益:避免维度报错;减少无效 padding 计算。
- 触发条件:多请求并发批处理时自动生效。
4.3 加速技术之间的关系
依赖关系(哪些技术是其他技术的基础):
PagedAttention (分页 KV cache)
│ 是基础
▼
连续批处理 (多请求并发) ←─ 需要 PagedAttention 才能高效并发
│ 配合
▼
动态调度 (按显存/长度实时决策)
Prefix caching (前缀复用) ── 与连续批处理叠加,进一步提升吞吐
└─ 但 TTS latent 路径不支持,用「跨 stage KV 转移」替代
权重量化 ── 独立,可与所有技术叠加(省显存 → 更多并发)
投机采样/MTP ── 独立,减少自回归步数
CUDA Graph ── 独立,减少 launch overhead(⚠️ TTS 默认关)
算子融合 ── 独立,减少显存读写(仅自回归部分)
音频缓存 (VoiceClonePromptItem) ── 独立,声音克隆场景省参考编码
分块流式 (chunked_decode + async_chunk) ── 独立,降首包延迟
多技术叠加的综合收益:
- PagedAttention + 连续批处理 + 动态调度 → 并发吞吐提升数十倍(官方 35×)。
- 量化 + PagedAttention → 显存省更多 → 并发更高(叠加放大)。
- async_chunk + 分块解码 → 端到端首包延迟大幅下降。
冲突/限制场景:
| 冲突 | 说明 |
|---|---|
| Prefix caching × latent 输出 | 输出 latent 的 stage 不支持 prefix caching(TTS 默认关) |
| 投机采样 × 高温度采样 | TTS 常用 temperature=0.9 等高温度,spec decoding 命中率下降 |
| 量化 × 音频质量 | 量化对语音 MOS 影响比文本大,需评估 |
| CUDA Graph × 当前 TTS | 框架默认 eager,强行关 eager 需测试稳定性 |
| 并发 × 音质/稳定性 | max_batch_size 调大需充分测试,可能影响音质或触发 OOM |
五、开源 TTS 模型适配完整工程指南
本章教你「把一个新 TTS 模型接进 vLLM-Omni」。以官方
docs/contributing/model/adding_omni_model.md为蓝本,结合源码实战。
5.1 接入前必须理解的概念
① 「模型」在 vLLM-Omni 里的抽象
一个模型 = 一个统一模型类 (按 model_stage 分支初始化不同 stage)+ 各 stage 组件实现 + stage config YAML + stage input processors(stage 间转换)。
② 模型在框架里的三个组成部分如何注册
| 部分 | 注册位置 | 作用 |
|---|---|---|
| LLM/统一类 | model_executor/models/registry.py 的 _OMNI_MODELS |
架构名 → 实现类 |
| Audio Encoder / Vocoder | 统一类内部组合(如 talker 持有 audio_tower,code2wav 持有 vocoder) | 作为 stage 的子模块 |
| Stage 拓扑 | stage config YAML(stage_configs/) |
定义有几个 stage、怎么连 |
③ 模型配置文件的关键扩展字段
| 字段 | 含义 |
|---|---|
model_stage |
thinker/talker/code2wav/自定义 |
model_arch |
registry 里注册的架构类名 |
engine_input_source |
上游 stage id 列表(流水线连接) |
custom_process_input_func |
stage 间数据转换函数路径 |
final_output / final_output_type |
是否最终输出、输出类型(text/audio) |
hf_config_name |
stage 专属 hf config(如 talker_config) |
④ 两个核心数据结构
OmniOutput(model_executor/models/output_templates.py:7):模型forward()返回的类型,含text_hidden_states、multimodal_outputs: dict、next_token_id。音频以multimodal_outputs["model_outputs"](波形张量)形式呈现。OmniTokensPrompt(inputs/data.py):stage 间传递的输入,含prompt_token_ids、additional_information(携带 embeddings/hidden states)、multi_modal_data。
5.2 标准接入步骤
以接入一个新的 TTS/Omni 模型为例(参照 Qwen3-Omni 的实现)。
目录结构 (adding_omni_model.md:28-47):
vllm_omni/model_executor/models/your_tts_model/ # 模型目录
├── __init__.py # 导出主类
├── your_tts_model.py # 统一模型类(按 model_stage 分支)
├── your_tts_thinker.py # 各 stage 实现(如果是单 stage TTS,可省)
├── your_tts_talker.py
└── your_tts_code2wav.py
vllm_omni/model_executor/stage_input_processors/
└── your_tts_model.py # stage 间数据转换(多 stage 才需要)
vllm_omni/model_executor/stage_configs/
└── your_tts_model.yaml # stage 配置
标准步骤表:
| 步骤 | 任务 | 新增/修改文件 | 关键动作 |
|---|---|---|---|
| 1 | 建模型目录 | model_executor/models/your_tts_model/ |
仿 qwen3_tts 目录结构 |
| 2 | 实现统一模型类 | your_tts_model.py |
按 model_stage 分支初始化;实现 forward()、load_weights()、compute_logits() |
| 3 | 实现各 stage 组件 | *_talker.py、*_code2wav.py |
继承 vLLM 基类(如 Qwen3MoeForCausalLM),实现 SupportsMultiModal/SupportsPP 接口 |
| 4 | 创建 __init__.py |
your_tts_model/__init__.py |
from .your_tts_model import YourModelForConditionalGeneration |
| 5 | 注册模型 | model_executor/models/registry.py 的 _OMNI_MODELS |
加 "YourArch": ("your_tts_model", "your_tts_model", "YourModelForGeneration") |
| 6 | 写 stage config | stage_configs/your_tts_model.yaml |
定义 stage 数、调度器、worker、显存 |
| 7 | 写 stage input processors(多 stage) | stage_input_processors/your_tts_model.py |
实现 thinker2talker、talker2code2wav 等转换函数 |
| 8 | 测试验证 | tests/e2e/ |
单请求验证 → 并发压测 → 与原生推理音频质量对齐 |
步骤 2 关键代码模板 (统一模型类,参照 qwen3_omni.py):
python
class YourModelForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
def __init__(self, *, vllm_config, prefix=""):
super().__init__()
self.have_multimodal_outputs = True
config = vllm_config.model_config.hf_config
self.model_stage = vllm_config.model_config.model_stage # 关键:按 stage 分支
if self.model_stage == "code2wav": # TTS 单 stage 就这一支
self.code2wav = init_vllm_registered_model(
vllm_config=vllm_config.with_hf_config(config.code2wav_config, ...),
prefix=maybe_prefix(prefix, "code2wav"),
...
)
self.model = self.code2wav
def forward(self, ...):
# ... 处理输入、调用子模型、生成音频 ...
return OmniOutput(
text_hidden_states=None,
multimodal_outputs={"model_outputs": audio_tensors, "sr": sample_rate},
)
def load_weights(self, weights):
# 按前缀(code2wav./talker.)分离权重加载
...
步骤 6 stage config 模板 (单 stage TTS,仿 qwen3_tts.yaml):
yaml
stage_args:
- stage_id: 0
stage_type: llm
runtime:
devices: "0"
max_batch_size: 1
engine_args:
model_stage: your_tts
model_arch: YourModelForConditionalGeneration # 必须匹配 registry
worker_type: generation # 一步生成用 generation,自回归用 ar
scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
enforce_eager: true
trust_remote_code: true
engine_output_type: audio
gpu_memory_utilization: 0.5
max_num_batched_tokens: 1000000
final_output: true
final_output_type: audio
步骤 7 stage input processor 模板 (多 stage 才需要,仿 qwen3_omni.py:492):
python
def talker2code2wav(stage_list, engine_input_source, prompt=None, requires_multimodal_data=False):
"""把 talker 的输出转成 code2wav 的输入。"""
source_stage_id = engine_input_source[0]
talker_outputs = stage_list[source_stage_id].engine_outputs
code2wav_inputs = []
for talker_output in talker_outputs:
output = talker_output.outputs[0]
# 从 talker 的 multimodal_output 提取 codec codes
codec_codes = output.multimodal_output["code_predictor_codes"].to(torch.long).reshape(-1).tolist()
code2wav_inputs.append(
OmniTokensPrompt(prompt_token_ids=codec_codes, multi_modal_data=None)
)
return code2wav_inputs
5.3 适配后的性能优化方向
| 方向 | 怎么做 | 注意 |
|---|---|---|
| 自定义算子兼容 | TTS 特有算子(如你的 vocoder)目前是 PyTorch,如需更快可写 CUDA kernel | 仓库当前无音频 CUDA kernel 先例,需自研 |
| 多模态批处理兼容 | 调 max_batch_size,处理不同时长音频的 padding/对齐 |
用 cap_and_align_mel_length 思路 |
| KV cache 调优 | AR stage 调 max_num_seqs、gpu_memory_utilization;考虑跨 stage KV 转移 |
latent 输出 stage 不能开 prefix caching |
| 流式前后处理对齐 | 实现 chunked_decode_streaming,用 left_context 重叠去边界伪影 |
保证 chunk 边界无缝(听感检查) |
| 量化适配 | 评估 INT4/INT8/FP8 对音频 MOS 的影响 | 音频比文本敏感,建议 FP8 起步 |
| 并发稳定性 | 逐步调大 max_batch_size,压测观察 OOM、音质退化 |
当前官方默认 1,调大需充分测 |
5.4 高频故障排查清单
| 故障现象 | 根因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 显存溢出 (OOM) | gpu_memory_utilization 过高;多 stage 共享 GPU 配额超 1.0;max_num_batched_tokens 过大 |
看 nvidia-smi;检查各 stage gpu_memory_utilization 之和 |
降 gpu_memory_utilization;多 stage 分配到不同 GPU;减 max_num_batched_tokens(参考 docs/configuration/gpu_memory_utilization.md) |
| 多模态特征维度不匹配 | stage 间 tensor shape 没对齐;mel 长度不是 repeats 倍数 |
在 stage input processor 打印 shape;检查 cap_and_align_mel_length |
用 cap_and_align_mel_length 对齐;在转换函数里 reshape |
| 流式响应断流/音频不完整 | stream_format="sse" 被拒(当前不支持);chunk 边界处理错误 |
看是否误用 sse;检查 chunked_decode_streaming 的 left_context |
当前用 stream_format="audio"(整段);等 RFC #938;检查 chunk 重叠逻辑 |
| 推理速度比原生还慢 | enforce_eager=true 关了优化;并发=1 没发挥批处理;stage 间 SHM 传输开销大 |
跑 --log-stats 看 e2e_stage_N_wall_time_ms 和 transfers_total_time_ms |
调大 max_batch_size;多 stage 开 async_chunk;定位最慢 stage |
| 模型权重加载失败 | load_weights 实现错误;HF AutoModel 加载失败;trust_remote_code 没开 |
看加载日志;确认权重前缀与 load_weights 的分离逻辑一致 |
加 --trust-remote-code;检查权重 key 前缀(code2wav./talker.) |
| 输出音频有杂音/裂纹 | chunk 边界伪影;量化精度损失;mel 归一化参数不对 | 对比原生推理的波形/MOS;检查 left_context_size |
增大 left_context_size;降量化精度;核对 mel 参数 |
| 并发上去后延迟指数增长 | stage 间队列堆积;SHM 阈值不当;某个 stage 成瓶颈 | 看 TransferEdgeStats(stats.py:58-71)的 in_flight_time_ms |
增大瓶颈 stage 的 max_batch_size;调 shm_threshold_bytes;给瓶颈 stage 单独 GPU |
| 量化后音频质量严重下降 | INT4 对音频太激进;量化层选错 | 对比量化前后的 MOS/频谱图 | 用 FP8 替代 INT4;只量化非关键层;提高量化group size |
--omni 没生效 |
命令漏了 --omni,回退到上游纯文本 vLLM |
看启动日志是否走了 Omni 路由 | 确认命令含 --omni(cli/main.py:12 的判断) |
| stage 初始化超时 | 模型大/网络慢下权重;stage_init_timeout 太小 |
看是哪个 stage 卡住 | 调大 --stage-init-timeout(默认 300)和 --init-timeout(默认 600) |
六、附录
6.1 关键术语速查表
| 术语 | 简短定义 |
|---|---|
| Stage | vLLM-Omni 的推理阶段单元,每个是独立进程/引擎。如 thinker/talker/code2wav |
| OmniConnector | stage 间数据传输抽象(默认 SharedMemoryConnector) |
| OmniStage | 单 stage 的封装类(omni_stage.py:226) |
| Omni / AsyncOmni | 多 stage 编排器(同步离线 / 异步在线) |
| AR stage | 自回归 stage(逐 token 生成),用 OmniARScheduler |
| generation stage | 一步生成 stage(如 code2wav),用 OmniGenerationScheduler |
| PagedAttention | KV cache 分页管理(复用 vLLM) |
| Continuous Batching | 连续批处理,请求即来即算即走(复用 vLLM) |
| Prefix caching | 公共前缀 KV 复用(TTS 默认关) |
| async_chunk | 跨 stage 异步流式分块(降首包延迟) |
| engine_output_type | stage 输出类型:text/audio/latent |
| RVQ | 残差向量量化,把音频压成多层 codec codes |
| BigVGAN | mel → 波形的声码器 |
| DiT | Diffusion Transformer(流匹配生成 mel) |
| x-vector | 说话人向量(ECAPA-TDNN 或 campplus 提取) |
| MTP | Multi-Token Prediction(Omni 投机采样,codec 多码本一次出) |
| TTFT / AUDIO_TTFP | 首 token / 首音频包延迟 |
| RTF | Real Time Factor,生成 1 秒音频所需秒数(<1 即实时) |
| D2H2D | 当前 connector 的 device→host→device 传输模式 |
| monkey-patch | 运行时替换 vLLM 类的机制(patch.py) |
6.2 推荐学习路径
从本文档出发的后续深入顺序:
- 先跑通:照第 1 节把 Qwen3-TTS 服务跑起来,发请求听到声音。
- 读 stage config :精读
docs/configuration/stage_configs.md和qwen3_tts.yaml,理解每个字段。 - 读架构 :
docs/design/architecture_overview.md+docs/design/module/ar_module.md(继承关系图)。 - 读 TTS 实现 :
vllm_omni/model_executor/models/qwen3_tts/qwen3_tts.py,跟一遍 forward。 - 读调度器 :
vllm_omni/core/sched/omni_generation_scheduler.py(TTS fast-path)。 - 读适配指南 :
docs/contributing/model/adding_omni_model.md,尝试接一个自己的小 TTS 模型。 - 跑 benchmark :
benchmarks/qwen3-omni/,对比 vLLM-Omni vs HF Transformers。 - 读论文:arXiv:2602.02204,理解完全解耦架构的设计动机和性能结果。
- 关注社区 :
#sig-omniSlack 频道(slack.vllm.ai)、RFC #938(TTS 流式进展)。
6.3 参考资料
官方文档(仓库内 docs/):
docs/getting_started/quickstart.md------ 快速开始docs/getting_started/installation/gpu.md------ GPU 安装docs/design/architecture_overview.md------ 架构总览docs/design/module/ar_module.md------ AR 模块设计(继承关系图)docs/design/feature/disaggregated_inference.md------ 解耦推理与 Connectordocs/configuration/stage_configs.md------ Stage 配置全字段说明docs/configuration/gpu_memory_utilization.md------ 显存计算docs/models/supported_models.md------ 支持的模型列表docs/contributing/model/adding_omni_model.md------ 添加新模型指南docs/cli/bench/serve.md------ bench serve(含音频指标)
关键源码索引:
| 关注点 | 文件 |
|---|---|
| 包导出 + patch | vllm_omni/__init__.py、vllm_omni/patch.py |
| Omni 编排器 | entrypoints/omni.py、async_omni.py、omni_stage.py |
| TTS 服务接口 | entrypoints/openai/serving_speech.py、api_server.py、audio_utils_mixin.py |
| 调度器 | core/sched/omni_ar_scheduler.py、omni_generation_scheduler.py |
| Qwen3-TTS 实现 | model_executor/models/qwen3_tts/qwen3_tts.py、modeling_qwen3_tts.py |
| Stage 配置 | model_executor/stage_configs/qwen3_tts.yaml |
| 模型注册 | model_executor/models/registry.py |
| Stage 间转换 | model_executor/stage_input_processors/qwen3_omni.py |
| Connector | distributed/omni_connectors/ |
| 性能统计 | vllm_omni/metrics/stats.py、benchmarks/metrics/metrics.py |
外部资料:
- 项目仓库:https://github.com/vllm-project/vllm-omni
- 官方文档:https://vllm-omni.readthedocs.io
- 论文:arXiv:2602.02204《vLLM-Omni: Fully Disaggregated Serving for Any-to-Any Multimodal Models》
- vLLM 原项目:https://github.com/vllm-project/vllm (PagedAttention 原论文:SOSP 2023)
最后一句:vLLM-Omni 还很年轻(2026/02 才稳定版),TTS 路径的某些能力(在线流式、高并发默认配置、音频 CUDA 算子)还在演进中。但它「继承 vLLM、复用其全部加速基础设施、在其上加多 stage 流水线」的设计思路是扎实的。对 TTS 工程师来说,现在上手能享受到 vLLM 成熟的 KV cache/连续批处理红利,同时为未来的流式/分布式能力做好架构准备。