BreezeRT:将 TTS 服务快到极致

BreezeRT 通过多阶段调度、渐进分块、CUDA Graph 和播放期限预测,降低 Breeze TTS 2 在高并发下的首包延迟。

BreezeRT:超低延迟 TTS 服务

BreezeRT 让 Breeze TTS 2 在高并发下也能快速输出首包。我们从请求调度、音频分块和 GPU 执行三处减少等待,再用 Predictor 安排首音和后续音频的输出时机:新请求尽快开口,正在播放的语音也能接得上。

BreezeRT 优化概览

优化效果
1. 统一多阶段调度减少等待
2. 渐进分块与增量解码首个请求的 TTFB: ~2500 → 150 ms
3. Depth 编译与 CUDA Graph首个请求的 TTFB: ~150 → 100 → 35 ms
4. Predictor:首音与播放期限调度300 个请求中存在潜在播放缺口的请求数: 14 → 0

推理框架对比

本文测试均在 H100 上完成。我们使用同一个 Breeze TTS 2 模型,对比 PyTorch、vLLM-Omni、SGLang-Omni 和 BreezeRT 在 1–16 路并发下的首包延迟。

H100 上 1–16 路并发的首段音频延迟 p50 与 p95。
首段音频延迟(TTFB)p50 / p95,数值越低越好。

我们为 Breeze TTS 2 适配并调优了 PyTorch、vLLM-Omni 和 SGLang-Omni,图中展示的是目前测得的各框架最佳结果。这些适配尚未合入对应的开源仓库。

1. 统一多阶段调度

Breeze TTS 2 先执行文本编码和 Backbone Prefill。生成每帧语音时,Backbone 预测第一个音频码,Depth Decoder 补齐其余 15 个码,Codec 再将音频码转换为波形。各阶段的计算量、执行频率和适合的批量大小不同。

并发请求往往处于不同阶段:A 已有音频码,解码后就能播放;B 还在生成下一帧;C 刚到达,等着首音。如果一直处理 A,B 和 C 就只能等。调度器需要在这些请求之间切换,而不是等一句话全部生成完。

BreezeRT 将文本处理、Backbone Prefill、Backbone Decode、Depth 和音频解码分开调度。一个运行时统一管理请求进度、缓存和待输出音频;每完成一个阶段,就重新选择接下来执行的阶段和请求。

请求在 Backbone、Depth Decoder 和 Codec 阶段共享 GPU 的时序。
请求在 GPU 上分阶段执行,输出音频块后继续生成。

调度器先按优先级选请求,再将同一阶段的工作合批。各阶段独立设置批量上限,Codec 和 Backbone 不必使用相同的批量大小。新请求可以穿插处理,已有流也能及时解码。后面的分块、Graph 重放和期限调度都由这个运行时执行。

2. 渐进分块与增量解码

首块音频越大,用户就要等越多帧生成后才能听到声音。但如果每次只输出一帧,Codec 和调度器又得频繁执行。请求一多,这部分开销就不能忽略。

BreezeRT 让音频块逐步增大,例如 1、2、4、8、16 帧。开头用小块尽快开始播放,积累一定音频缓冲后,再用大块减少解码次数。

Codec 也需要记住之前处理过的内容,避免每次输出都重算历史音频。BreezeRT 为每个请求保存 Transformer 的滑动窗口 KV、因果卷积历史和转置卷积的重叠尾部,只处理新增的帧。分块大小改变时,这些缓存仍能保留上下文,让相邻音频块平滑衔接。

在辅助对照中,首块固定为 16 帧时,第一个请求约 2500 ms 收到音频;改为渐进分块后约为 150 ms

3. Depth 编译与 CUDA Graph

首块缩小后,CPU 提交计算的开销就更显眼了。生成一帧语音要执行很多次模型操作,GPU 算得快,也可能在两次提交之间空等。BreezeRT 用编译减少计算开销,再用 CUDA Graph 一次提交已捕获的 GPU 操作。

CUDA Graph 重放要求张量形状固定,但请求的文本长度、批量大小和音频块大小并不固定。为所有组合抓图会占用大量显存,因此 BreezeRT 只保留一组常用形状,把输入拆分或填充到这些形状后执行。

Text Encoder 将实际 token 打包,再按请求行数和 token 容量拆分,减少填充;不同请求之间的注意力仍然隔离。Backbone Prefill 按 token 分块,用分页 KV 保存各请求的生成历史,从而复用已有的 Graph 和缓冲。

文本打包和分块 Backbone Prefill 复用固定形状的 CUDA Graph。
文本打包和分块 Prefill 让不同长度的输入复用 CUDA Graph。

Depth Decoder 每帧要补齐 15 个音频码。BreezeRT 整体编译 Decoder 堆栈和输出头,将可合并的逐元素计算融合为更少的 Kernel,减少中间张量读写和 Kernel 启动次数。并发请求越多,这些重复操作的开销越值得优化。KV、位置和输入输出缓冲预先分配,完整自回归循环则捕获为一张图,一次重放按依赖顺序执行全部 15 步。

15 次主机提交的 Depth 循环与 BreezeRT 单次图重放对比。
一次 Graph 重放提交完整的 15 步 Depth 循环。

文本编码、Backbone、Depth 和音频解码都使用 Graph,常用形状在启动时预热。每个请求仍保留独立的采样参数和随机状态。在分块相同的辅助对照中,第一个请求的首包延迟约为 150 ms;加入 Depth 编译后降至 100 ms,再加入全阶段抓图后降至 35 ms

4. Predictor:首音与播放期限调度

新请求等着开口,已有流等着下一块音频。只优先处理新请求,正在播放的语音可能断流;为已有流提前生成太多,又会让新请求排队。Predictor 根据播放期限决定先处理谁。

新请求优先启动。已有流的播放期限由首块输出时间和累计音频时长估算。Predictor 按批量大小记录各阶段近期耗时,并留出余量,判断剩余的 Backbone、Depth 和 Codec 工作能否按时完成。

例如,A 已经可以解码,B 再生成一步就能和 A 合批。如果合批能节省时间,且不会错过 A 的播放期限,调度器就等 B;否则先处理 A。如果来不及凑齐目标音频块,也可以先输出已生成的帧,避免播放器空等。

Predictor 只做轻量的耗时估计。在一组 300 请求的对照中,有潜在播放缺口的请求从 14 个减少到 0 个;避免缺口所需的最大额外缓冲从约 330 ms 降至 0 ms