在 vLLM-Omni 中工程化 TTS 推理
Engineering TTS Inference in vLLM-Omni
vLLM-Omni 将 TTS 推理支持从全模态模型扩展到 Qwen3-TTS、VoxCPM2、Fish Speech S2 Pro 和 Higgs Audio V3。针对 TTS 流水线中 Talker 与 Code2Wav 阶段计算特征差异、流式输出延迟预算及吞吐量平衡等工程问题,团队采用阶段分离与连接器分块、批量解码预处理、全前向 torch.compile、CFM/LocDiT 解码尾部批处理、GPU 驻留解码状态及模型特定 q_len=1 attention 等优化。在 H20 GPU 上,Qwen3-TTS 音频吞吐量提升 61.5% 至 42.88 audio-s/s,VoxCPM2 请求吞吐量提升 158.8% 至 10.83 req/s,Fish Speech S2 Pro 达 23.72 audio-s/s,Higgs Audio V3 达 35.26 audio-s/s。
vLLM-Omni 最初支持全模态模型,此后已扩展到多个文本转语音系统,包括 Qwen3-TTS、VoxCPM2、Fish Speech S2 Pro 和 Higgs Audio V3。本文描述了我们在适配和优化这些模型时遇到的具体工程问题、采用的解决方案以及背后的工程权衡。
TTS 推理与传统 LLM 推理的区别
TTS 和纯文本 LLM 推理都使用自回归模型,但服务瓶颈不同。
TTS 是一个流水线,通常包含多个模型阶段。 一个典型的 TTS 系统至少有两个阶段:一个 Talker 自回归地预测编解码器 token,一个 Code2Wav 模块从这些编解码器 token 重建波形音频。这些阶段的计算特征差异很大。Talker 是一个延迟受限的单 token 解码工作负载,而 Code2Wav 是一个吞吐受限的并行解码器。如果调度器对两个阶段采用相同方式处理,Talker 的延迟会阻塞 Code2Wav 的输入,而 Code2Wav 的并行性也无法充分利用。延迟和吞吐量都会受到影响。
流式输出有严格的延迟预算。 在语音合成中,用户期望在几百毫秒内听到第一个音频包。连接层必须支持分块流式传输,而块大小直接影响 TTFP(Time To First Audio Packet,首次音频包到达时间)。如果块太小,Code2Wav 没有足够的上下文来保持音频在块边界处的连续性。如果块太大,首包延迟将变得不可接受。
吞吐量仍然至关重要。 在线服务中,单个 GPU 能支持的并发数,以及每壁钟秒能生成的音频秒数,直接决定了部署成本。TTS 吞吐量优化比 LLM 吞吐量优化更复杂,因为 Talker 和 Code2Wav 有不同的瓶颈,并且它们之间的连接层增加了自身的传输成本。提高吞吐量意味着平衡两个阶段,同时消除各自内部的瓶颈。

vLLM-Omni TTS 服务流水线
本文的其余部分首先概述我们使用的优化技术,然后以 Qwen3-TTS 作为完整优化路径的主要示例。接着,我们使用 VoxCPM2、Higgs Audio V3 和 Fish Speech S2 Pro 来说明为什么不同的 TTS 架构需要不同的服务策略。
优化概述
不同的 TTS 模型有不同的瓶颈。vLLM-Omni 并非对每个模型都应用固定的优化方案。相反,我们根据每个模型的流水线结构、解码状态、批次形状和数值约束来选择优化方法。
| 技术 | 适用模型 | 重要性 |
|---|---|---|
| 阶段分离与连接器分块 | Qwen3-TTS, Higgs Audio V3 | 允许独立调整 Talker 延迟和 Code2Wav 吞吐量。 |
| 批量解码预处理 | Qwen3-TTS | 减少 Talker 解码热路径中每个请求重复的 Python 工作。 |
全前向 torch.compile |
VoxCPM2 | 让 Dynamo 看到更多 MiniCPM4 前向循环,减少 Python 到编译的边界。 |
| CFM/LocDiT 解码尾部批处理 | VoxCPM2 | 将许多微小的每请求扩散调用转换为更大的 GPU 批次。 |
| GPU 驻留解码状态 | Higgs Audio V3 | 将多码本状态更新移出 Python 循环,减少同步。 |
| 模型特定的 q_len=1 attention | Fish Speech S2 Pro | 专门处理纯解码 attention,而不是为通用的 paged/varlen 路径付费。 |
关键点在于,并非每种优化都适用于所有 TTS 架构。工程挑战在于为正确的模型形态选择正确的杠杆。
Qwen3-TTS:一条完整的优化路径
Qwen3-TTS 是 Qwen 团队的一个语音生成模型系列。它使用离散的多码本语言模型架构和一个 12 Hz 的 tokenizer 进行声学压缩和高保真重建1。它的三个变体——Base(用于声音克隆)、CustomVoice(用于预定义说话人,支持基于指令的情感和风格控制)和 VoiceDesign(用于根据自然语言描述的音色、情感和韵律设计新声音)——共享相同的两阶段架构:Talker 自回归地预测编解码器 token,Code2Wav 并行解码它们。Qwen3-TTS 的 Code2Wav 是一个轻量级的非 DiT 解码器,不需要扩散模型使用的迭代去噪循环。
在本文讨论的四个模型中,Qwen3-TTS 具有最标准的流水线形状:Talker → 连接器 → Code2Wav。这使其成为贯穿 TTS 推理优化全过程的一个有用示例。
1. 流式传输:将连接器块与 Code2Wav 解码窗口解耦
第一个优化目标是流式输出。在早期的 Qwen3-TTS 实现中,连接器流式块和 Code2Wav 解码块绑定到同一个块参数,主要是 codec_chunk_frames。
连接器将编解码器 token 从 Talker 发送到 Code2Wav。在流式模式下,如果连接器发送非常小的块,Code2Wav 也会看到非常小的解码块,这会损害跨块的音频连续性。如果我们为了音频质量而增加块大小,首包延迟就会增加。
我们通过引入独立的参数来解耦这两个职责:
codec_chunk_frames:连接器流式块大小,控制 Talker 到 Code2Wav 的传输节奏。decode_chunk_frames和decode_left_context_frames:Code2Wav 内部解码窗口和左侧上下文,与连接器分块保持独立。initial_codec_chunk_frames:一个较小的初始编解码器块,以便 Code2Wav 能更早启动,之后的块恢复为常规大小。
通过这种设计,连接器可以使用小块来减少首包延迟,而 Code2Wav 保持一个 300 帧的解码窗口加上 25 帧的左侧上下文以保持跨块质量。这两个旋钮可以独立调节2。

Qwen3-TTS 连接器块解耦
2. 吞吐量:阶段 0 解码预处理
解决流式延迟后,下一个瓶颈是吞吐量。第一个障碍是 Talker 解码循环。每个 Qwen3-TTS Talker 解码步骤都需要请求级别的预处理:说话人嵌入准备、trailing_text 维护和输入嵌入构建。在 c=1 时,此开销很小。在 c=64 时,每个解码步骤都要循环处理 64 个请求,Python 端的循环和 tensor 切片成为明显的瓶颈。
为了定位成本,我们在 H20 × 2 上使用声音克隆在 c=64 时对 Talker 解码进行了性能分析。在更广泛的热路径优化之前的预热运行中,模型外部的 Python 和 runner 端工作——包括 preprocess_decode_batch、make_omni_output、process_additional_info、build_mm_cpu 和记账同步——每个解码步骤在毫秒级别。单独看,每步几毫秒似乎不大,但一个话语可能需要大约 200 个解码步骤。在 c=64 时,这个成本在整个序列中重复出现,成为端到端延迟的重要组成部分。
GPU 利用率也说明了同样的问题。使用 nvidia-smi 在 c=64 时采样,阶段 0 和阶段 1 的基线平均 GPU 利用率分别约为 14% 和 6%。GPU 经常在等待 Python 调度、小 tensor 分配和 kernel 启动开销,而不是计算。这就是为什么瓶颈不是原始 GPU FLOPs,而是服务路径开销。
第一个具体目标是说话人嵌入准备。在 Qwen3-TTS 声音克隆模式下,每个请求使用参考音频提取说话人嵌入,然后在解码期间执行 mel/STFT 工作。原始路径在 CPU 上为每个请求计算 mel 频谱图并将其复制到 GPU。在高并发下,这变成了许多小的 CPU 到 GPU 传输和 kernel 启动。我们将 mel 基线和窗口缓冲区缓存在 GPU 上,并在 GPU 上批量执行 mel/STFT 计算,从而消除了重复的 CPU 工作和 H2D 传输。
下一个目标是 trailing_text。在解码期间,Talker 维护一个 trailing_text 滑动窗口,用于缓存已生成 token 的嵌入。每个解码步骤追加当前 token 嵌入并移除最旧的 token。原始实现使用 tensor 切片和拼接,频繁分配新 tensor。优化后的路径跟踪一个偏移量,并且仅当偏移量超过阈值或到达缓冲区末尾时才进行压缩(_TRAILING_TEXT_COMPACT_MIN_FRAMES = 64)。中间的解码步骤通过偏移量进行索引,无需分配新 tensor。
批量化的 preprocess_decode_batch 路径消除了每个请求解码开销的一个主要来源3。下面的最终吞吐量数据来自堆叠的优化路径,包括阶段 0 批处理、连接器更改、异步 D2H、runner 热路径清理和 CUDA Graph 调优423。在最终的堆叠运行中,H20 × 2 上的 Qwen3-TTS 音频吞吐量从 26.55 audio-s/s 提高到 42.88 audio-s/s(+61.5%),而 P99 E2EL 从 17.7 秒下降到 9.0 秒。

Qwen3-TTS 阶段 0 调度整合
上面的跟踪窗口显示了批处理阶段 0 预处理对服务路径的影响:解码热路径中 CPU 启动调用更少,小 GPU kernel 切片更少,而不是声称更高的 GPU 利用率。
3. 热路径清理
预处理批量化后,剩余的性能分析显示了许多小的 Python 开销。每个都很小,但在高频率的 c=64 解码循环中累积起来。
req_id_to_index 之前使用 req_ids.index(),将查找变成了每个解码步骤内的 O(N²) 列表扫描。将其替换为字典后,查找变为 O(1)。非流式请求不需要在编排器中遍历每个输出的流式路径,因此我们提前跳过该路径。编解码器禁止的掩码被预计算到一个缓冲区中,允许 compute_logits 直接使用 masked_fill,而不是每次都重建掩码4。
Qwen3-TTS 在多个地方使用 CUDA Graph。Talker 代码预测器根据部署配置有自己的图路径。这里我们重点介绍 Code2Wav 解码器的 CUDA Graph。解码器输入形状为 (batch, num_quantizers, codec_frames)。在分块解码中,codec_frames 只有一小部分值:流式块加左侧上下文、非流式 decode_chunk_frames + decode_left_context_frames(300 + 25 = 325)以及尾部块。这些值可以在预热期间枚举。CUDAGraphDecoderWrapper 按 (batch_size, frames) 捕获图,并在推理时使用 bisect_left 选择最近的填充桶。如果没有匹配的图,则回退到 eager 执行。
在使用 qwen3_tts.yaml 进行的重复 c=16 测试中,Code2Wav CUDA Graph 命中率从 88% 开始,在连续五轮后稳定在 81% 左右。主要的单样本形状,如 (1, 98) -> 169、(1, 73) -> 73、(1, 123) -> 169 和 (1, 325) -> 325,都命中了捕获的桶。回退主要来自批次大小 > 1 的形状,如 (2, 98, 169) 和 (8, 73, 73)。在整个运行过程中,stream_capture_fallbacks=0,因此没有因流捕获失败而导致回退。
4. 数值精度:代码预测器的 fp32 对齐
Talker 代码预测器有一个对精度敏感的路径。它处理非常短的序列,通常是 2-8 个 token,并重复执行 prefill。vLLM 在 bfloat16 下的融合 kernel 可能与参考实现略有不同。在这个短序列、高频率的路径中,这些小的差异会累积,并在数十步后影响音频质量。
解决方案是拆分代码预测器层,并将选定的操作保留在 fp32 中:RMSNorm 方差、RoPE cos/sin、attention 和 QKV 投影使用 PyTorch 原生实现,以与参考路径进行位级对齐。
5. 验证
堆叠这些优化后,H20 × 2 上 c=64 的声音克隆 Qwen3-TTS 音频吞吐量提高了 61.5%,而 P99 端到端延迟下降了近一半。完整数据在性能部分。
我们还使用 H20 × 2、声音克隆和流式输出运行了一次热并发扫描:
| c | Mean TTFP | Mean E2E | P50 TTFP | P50 E2E |
|---|---|---|---|---|
| 1 | 70.61ms | 564ms | 70.61ms | 564ms |
| 8 | 268.75ms | 1.55s | 287.15ms | 1.70s |
| 16 | 451.32ms | 2.62s | 516.15ms | 2.75s |
| 32 | 637.43ms | 5.05s | 634.22ms | 5.10s |
| 64 | 1127.93ms | 8.73s | 1051.05ms | 8.78s |
从 c=1 到 c=64,E2E 从 0.56 秒增长到 8.73 秒,而不是线性增长 64 倍。热的高并发服务摊薄了固定成本,但在 c=64 时,Talker 和调度路径仍然成为排队的主要来源。这就是为什么热路径清理和 CUDA Graph 仍然很重要。
VoxCPM2:单阶段混合 TTS
VoxCPM2 是 OpenBMB 的一个无 tokenizer 的 TTS 模型。它采用扩散-自回归混合设计,并在 AudioVAE V25 的潜在空间中运行。其 Talker 是一个四部分级联:
MiniCPM4 (28 层, PagedAttention) → FSQ → MiniCPM4 ResidualLM (8 层) → LocDiT (CFM 求解器) → AudioVAE
LocDiT 执行 CFM(Conditional Flow Matching,条件流匹配)扩散去噪,AudioVAE 重建 48 kHz 波形音频。在 vLLM-Omni 中,VoxCPM2 并未拆分为多个运行时阶段。相反,它作为单阶段 AR TTS 流水线运行:MiniCPM4、FSQ、ResidualLM、LocDiT 和 AudioVAE 都在一个模型实例内执行,模型直接输出音频。这避免了阶段间的潜在传输,并使解码尾部 CFM/LocDiT 和 VAE 路径的跨请求批处理更容易。

VoxCPM2 单阶段混合流水线
与 Qwen3-TTS(一个两阶段 Talker 到 Code2Wav 流水线)不同,VoxCPM2 的优化集中在两个问题上:如何让 28 层的 MiniCPM4 更快,以及如何在高并发下阻止 CFM/LocDiT 未充分利用 GPU。
探索 torch.compile
28 层的 MiniCPM4 是 VoxCPM2 Talker 中最重的部分,因此第一个优化目标是 torch.compile。效果最好的路径并非我们最初预期的那样。
第一次尝试是分别编译每一层的 mlp 和 o_proj:28 层 × 2 个模块 = 56 个编译区域,使用 fullgraph=True6。问题在于 Dynamo 无法跨编译区域边界进行优化。每个边界增加一次 Python → 编译 → Python 的转换,56 个区域意味着每个解码步骤有多次转换。
然后,我们将整个 Model.forward 包装在 torch.compile 中,使用 fullgraph=False7。这让 Dynamo 能够看到完整的 28 层循环。PagedAttention 仍然会导致图断裂,但 Dynamo 只需要记忆少量子图。每步调度从许多小区域减少为几个较大区域。RTF 从大约 0.21 下降到大约 0.13,这是 VoxCPM2 最大的单一优化。
为了量化这一点,我们对三种配置进行了性能分析:eager、逐层编译和全图/统一图。逐层编译减少了部分 kernel 数量和 kernel 时间,但启动次数没有下降。全图/统一图是关键步骤:cudaLaunchKernel 计数下降了约 71%,kernel 事件下降了约 30%,kernel 时间下降了约 27%。单请求 E2E 在逐层编译时下降了约 2.6%,在全图时下降了约 6.5%。

VoxCPM2 编译调度时间线和计数器
时间线将分析器视图作为主要表面,而嵌入的全跟踪计数器显示了为什么逐层编译不够:启动计数保持平稳,直到全前向编译路径减少了 Python 到编译的边界。
我们还尝试了 mode="reduce-overhead",它启用了自动 CUDA Graph 捕获。这与 PagedAttention 的有状态 KV 缓存冲突。在图捕获期间,slot_mapping 变得固定;重放时可能会将 attention 结果写入错误的 KV 缓存位置,导致错误的停止 logits 和过早截断。
fullgraph=True 无法容忍来自 PagedAttention 和自定义精度边界的图断裂。fullgraph=False 在保持全前向视图的同时,允许这些边界回退到 eager 执行。
CFM/LocDiT 解码尾部批处理
单请求延迟改善后,高并发瓶颈转移到了 CFM/LocDiT。每个请求在 CFM 去噪期间运行一个 LocDiT attention/GEMM 工作负载,但每请求批次非常小,在 CFG 下通常为 B=2。这远不足以填满 GPU。在高并发下,独立运行 LocDiT 的请求使 GPU 利用率不足。
解决方案是跨请求批处理 CFM/LocDiT 解码尾部。我们收集来自多个请求的 lm_h、残差输出和前缀特征条件,然后作为一次批次运行 dit_proj、CFM/LocDiT、feat_encoder 和 stop_head,再将结果分散回请求状态。结合每三个潜在块进行一次 VAE 解码、批量 VAE 解码、合并的音频 D2H 复制以及 LocDiT 融合 QKV / 融合 gate-up MLP,H20 × 1 在 c=64 时的吞吐量从 4.19 req/s 提高到 10.83 req/s(+158.8%),音频吞吐量从 12.16 audio-s/s 提高到 33.07 audio-s/s(+172.0%)8。
CFM 的 Euler 积分循环内部也存在一个同步问题。在 0 维 GPU tensor 上调用 .item() 会强制 GPU 到 CPU 同步。原始路径在每个扩散步骤中执行四次此操作。以 10 个时间步和大约 60 个解码步骤计算,一个请求可能触发大约 2,400 次同步。将 .item() 替换为 GPU 端的 .copy_() 广播,可以从该循环中移除 CPU 参与。
VAE 解码也存在结构性问题。第一个实现使用了累积并重新解码的模式:每五步,它将所有先前生成的潜在块连接起来,并再次解码整个前缀。这使得总工作量变为 O(N²)。切换到滑动窗口解码,每次调用使用 12 帧的填充上下文和 4 个新帧,将工作量减少到 O(N)。长文本的 RTF 不再随文本长度增长;所有长度都保持在 RTF 0.132–0.138 左右7。
Higgs Audio V3:动态批次与多码本状态
Boson AI 的 Higgs Audio V3 支持超过 100 种语言和零样本声音克隆。在架构上,它具有几个重要特征:一个具有 36 层和 2560 隐藏大小的 Qwen3 骨干网络、GQA、融合的多码本嵌入(带有一个大的 [N × V, D] 矩阵和偏移查找),以及一个 MusicGen 风格的延迟模式 [0, 1, 2, ..., 7],带有 BOC/EOC 特殊 token。
其整体 Talker → Code2Wav 形状与 Qwen3-TTS 相似,但由于多码本预测和延迟模式,Talker 内部结构不同。
与 Qwen3-TTS 相比,Higgs v3 有不同的瓶颈。Qwen3-TTS 受限于 Python 热路径和流式块边界;Higgs v3 受限于复杂的多码本解码状态管理和 CUDA Graph 兼容性。
将解码状态移至 GPU
Higgs v3 的主要吞吐量提升来自于将每个请求的 Python dict 状态机迁移到 GPU 驻留的批量化 tensor9。状态包括 _decode_last_codes、_decode_has_codes、延迟计数、EOC 倒计时、生成完成标志以及相关的解码元数据。好处来自于减少 Python 每请求循环、减少 D2H 同步,以及将采样/状态更新逻辑移至批量化 GPU 热路径。在本文报告的基准测试中,35.26 audio-s/s 的结果是在单个 H20 上,c=16,使用 eager + 本地 MLP CUDA Graph 配置(而非 PIECEWISE 全解码图路径)测量的。
困难在于 vLLM 调度器可能在解码期间重新排序、缩减、完成或移除请求。行级状态不能假定等于请求级状态。音频 AR 状态比文本状态更复杂,因为延迟码本、EOC 降级和终止帧都具有语义含义。如果任何状态滞后一步,结果将是音频质量问题,而不是干净的崩溃。GPU 状态、CPU 覆盖状态和调度器 token 必须有一个单一的真实来源,否则停止语义将变得不一致。
使 CUDA Graph 适应动态批次形状
Higgs v3 Talker 解码路径的 CUDA Graph 捕获暴露了另一个问题。Talker 有一个音频反馈机制:前一个音频 token 的嵌入替换下一个延续 token 的嵌入。实现使用一个布尔掩码来选择哪些请求当前处于解码状态。生成的 tensor 形状取决于运行时有多少请求处于解码状态。
CUDA Graph 捕获需要固定的流操作和固定的输入/输出形状。一个布尔掩码选择,其输出形状取决于运行时数据,违反了这一要求。
解决方法是让 CUDA Graph 路径使用统一的单 token 解码批次。每个跨度长度为 1,因此 decode_mask 全部为 True。选择变为空操作并返回原始 tensor。图看到的是稳定的全批次形状,而不是依赖于数据的压缩形状。
本地 MLP CUDA Graph 与 PIECEWISE
本地 MLP CUDA Graph 仍然是 Higgs v3 最重要的图优化。它覆盖了 post_attention_layernorm + mlp 中的主要 GPU 成本。vLLM PIECEWISE CUDA Graph 看起来更完整,因为它可以覆盖更大的解码步骤。在实践中,Higgs v3 的多码本延迟模式使得 token 布局在不同解码步骤之间变化。嵌入查找和 attention 前索引操作是数据依赖的。PIECEWISE 要么在这些区域中断回 eager,要么需要额外的元数据同步。
在端到端测试中,PIECEWISE 需要禁用本地 MLP 图,而这种权衡得不偿失。Eager 加本地 MLP 图比 PIECEWISE 图更快。
一个被拒绝的流水线重叠设计
有一个被拒绝的设计仍然值得记录:单步音频流水线重叠。其想法是将音频流水线 D2H 复制与下一个解码步骤重叠,以减少 GPU 空闲时间。干运行通过了,但负载测试显示 vLLM 调度器可能在解码期间重新排序、缩减或完成请求。指向某行的游标可能会丢失其到请求的映射。这种游标滞后设计在动态批处理下结构上不安全;这不是一个边界条件错误。未来的重叠设计应以请求 ID 为键,并包含完成/移除的排空钩子。
Fish Speech S2 Pro:当通用 Attention 成为瓶颈
Fish Audio 的 Fish Speech S2 Pro 使用在超过 1000 万小时音频上训练的双 AR 架构,支持超过 80 种语言10。在 vLLM-Omni 中,Fish Speech S2 Pro 作为 slow_ar + Fast AR + DAC 解码器运行。slow_ar 沿时间轴预测语义码本,Fast AR 在每个解码步骤预测残差码本,DAC 解码器从 10 个码本重建波形音频。
与 Qwen3-TTS(其中 Python 预处理是主要瓶颈)不同,Fish Speech 的瓶颈在 GPU 端。在高并发下,q_len=1 的 attention 占主导地位。通用的 paged/varlen attention 带有用于 prefill、分块 prefill、解码和其他模型形状的形状检查和分支。对于 Fish 的纯解码形状,这种灵活性是开销。
模型特定的 Attention Kernel
在性能分析中,高并发下的 Fish slow_ar 大部分时间花在 q_len=1 的 SlowAR attention 以及 DAC 和运行时之间的数据交接上。通用 attention 必须支持许多形状。Fish 解码要窄得多:q_len=1、fp16/bf16、head_dim=128、块大小 16 以及 Fish 的 GQA 布局。
我们为 SlowAR 解码 attention 实现了一个 Fish 特定的 Triton kernel11。它不处理 prefill 或其他模型。如果请求不满足形状约束,则回退到原始 attention 路径。
该 kernel 有两个路径。最多 1024 个 token 的短序列使用标准在线 softmax 一次完成。网格为 (batch_size, num_kv_heads),每个程序处理一个批次行和一个 KV head 及其 Q heads。块大小硬编码为 16,与 vLLM 的 KV 缓存块大小匹配,因此块表查找是直接的 tl.load,无需额外的 gather 逻辑。长序列使用拆分-部分-合并路径:将序列拆分为段,独立计算部分 m/l/acc,然后使用在线 softmax 递推合并它们。这使参考音频的长上下文请求保持在快速路径上。
调度有一个微妙的细节。kernel 需要序列长度来选择短路径或长路径,但确切的序列长度在 GPU 上。将其读取到 CPU 会导致同步。相反,runner 根据已计算的 token 加上已调度的 token 计算一个 CPU 端的 seq_lens_cpu_upper_bound。上界始终至少等于真实序列长度。短路径不会读取不足,长拆分路径也不会覆盖不足。在 CUDA Graph 捕获期间,上界设置为 max_model_len,因此所有图路径都保持覆盖。

Fish Speech 阶段 0 运行时形状(q_len=1 快速路径前后)
该跟踪是 Fish 路径在 q_len=1 attention 特化前后的本地运行时形状视图。旨在补充 kernel 设计讨论,而非替代基准测试数据。
快速路径仅适用于 Fish SlowAR attention 层。在模型加载时,我们遍历 model.layers,并将每个 attention 层的 impl.forward 替换为一个包装器,该包装器在约束匹配时分派到 Fish 快速路径。Prefill 请求、非 Fish 模型以及不支持的解码形状使用原始 attention 实现。
Fast AR 缓冲区重用与编译
Fish Speech Fast AR 是一个四层轻量级 transformer,在每个 slow_ar 步骤后预测残差码本。它维护一个每次调用的 KV 缓存:每个残差码本步骤仅解码一个新 token,并将 K/V 写入预分配的 _k_cache 和 _v_cache tensor。
每个 Fast AR 解码步骤投影 slow_ar 隐藏状态、嵌入当前语义 token、逐层运行 attention 和 MLP,并从 logits 采样。即使序列很短(最多 10 个 token),重复分配和重复 prefill 在 c=64 时也变得明显。
我们一次性分配 _embed_buf、_pos_ids、_k_cache 和 _v_cache 并重用它们。_embed_buf 的形状为 (batch_size, num_codebooks + 1, hidden_dim),覆盖一次 Fast AR 解码的所有时间步。_k_cache 和 _v_cache 按层、批次、KV head、序列位置和 head 维度预分配,因此 forward_one 可以就地写入和读取。
我们还使用 torch.compile 编译 Fast AR。与 VoxCPM2 MiniCPM4 不同,Fast AR 只有四层,因此编译开销很小。我们使用 fullgraph=False,因为 attention 使用 F.scaled_dot_product_attention 而非分页 attention,而 SDPA 可能在内部导致图断裂。Dynamo 只需要记忆几个子图。dynamic=True 允许编译结果处理批次大小变化。
DAC 与运行时端优化
DAC 和运行时端的更改包括几个较小的优化。编解码器负载传输从 Python list[int] 更改为 tensor 负载:直接序列化一个 2D 代码 tensor,而不是展开为 Python 整数,从而减少高并发下的分配和 GC 压力。fp16 DAC 支持将内存和计算减半。帧计数限制的 DAC 批处理限制了单次 DAC 前向中处理的帧数,防止一个长请求阻塞其他请求。异步块处理重叠了连接器传输和 DAC 计算:slow_ar 和 Fast AR 每个解码步骤产生一个 10 码本的编解码器帧,连接器批处理帧直到达到 codec_chunk_frames,DAC 解码器在处理当前块的同时,连接器累积下一个块。
性能数据
以下数据来自 vLLM-Omni cookbook 基准测试。指标:
- RTF:生成时间除以音频时长。低于 1 表示快于实时。
- TTFP:首次音频包到达时间。
- Tput:音频吞吐量,即每壁钟秒生成的音频秒数。
- E2EL:端到端延迟。
Qwen3-TTS (c=64, p=512, H20 × 2, 声音克隆)
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 音频吞吐量 | 26.55 audio-s/s | 42.88 audio-s/s | +61.5% |
| 中位数 E2EL | 9654ms | 5699ms | −41.0% |
| P99 E2EL | 17686ms | 8956ms | −49.4% |
| P99 TTFP | 7558ms | 5563ms | −26.4% |
VoxCPM2 (c=64, H20 × 1, CFM 批处理前后)
| 指标 | 优化前 | 优化后 | 变化 |
|---|---|---|---|
| 请求吞吐量 | 4.19 req/s | 10.83 req/s | +158.8% |
| 音频吞吐量 | 12.16 audio-s/s | 33.07 audio-s/s | +172.0% |
Fish Speech S2 Pro (H20, 单 GPU, c=64, Triton KV 缓存 + tensor 负载)
| 指标 | 值 |
|---|---|
| 音频吞吐量 | 23.72 audio-s/s |
| 请求吞吐量 | 5.95 req/s |
| 平均 TTFP | 899.67 ms |
| 平均 E2EL | 10.47 s |
Higgs Audio V3 (H20, 单 GPU, c=16, eager + 本地 MLP 图)
| 指标 | 值 |
|---|---|
| 请求吞吐量 | 5.18 req/s |
| 音频吞吐量 | 35.26 audio-s/s |
| 壁钟时间 | 96.5s |
| 相对于基线的加速比 | 2.70× |
致谢
我们感谢 Minghui Jiang、Yueqian Lin、Canlin Guo、Shunyang Li、Taichang Zhou、Yuekai Zhang、Juan Pablo Zuluaga、Nick Cao、Ruirui Yang、Wenjing Chen、Haiyan Wu、Han Gao、Hongsheng Liu 和 Roger Wang 的贡献和反馈。
参考文献
如果您对 TTS 推理优化感兴趣,请加入 vLLM Slack 的 #sig-omni 频道,或在 vLLM-Omni GitHub 上提交 issue。
脚注
Qwen3-TTS — QwenLM/Qwen3-TTS↩
VoxCPM2 — OpenBMB/VoxCPM↩
Fish Speech S2 Pro — fishaudio/fish-speech↩