MiraEcho
全部文章

语音模型已经变成 LLM 了,于是你那套算 GPU 的方法失效了

约 8 分钟
  • TTS
  • ASR
  • 推理服务
  • 容量

有那么几年,语音和语言模型是两门手艺。ASR 是编码器加 CTC 或注意力解码器,TTS 是声学模型加声码器,两者的部署方式也跟普通神经网络一样:输入定长、算力固定、延迟可以从 FLOPs 和 batch size 推出来。

现在大家真正在跑的东西已经不是这个了。今天线上跑的这批——Whisper 系的 ASR、CosyVoice、Fish Speech、VoxCPM,以及音频进音频出的 omni 模型——本质上都是自回归 Transformer,带着 KV cache 一步一个地预测离散音频 token。末端的声码器在整个算力账里是个零头,真正贵的部分全在 decode。

建模上的收益已经被讲烂了:几秒参考音频克隆音色、长句子里韵律不塌、一套架构吃多语种。但很少有人讲运维上的后果,而那才是会打到你值班手机上的东西:你跑的已经不是一个语音服务,而是一个恰好吐音频的 LLM 推理服务。 你为前者积累的经验,放到后者身上大多是错的。

RTF 不再是模型的属性

实时率(生成一秒音频要花多少秒算力)是每家语音厂商都会公布、每个买方都拿来做规划的数。RTF 0.15 听起来像一个干净的容量单位:一张卡比实时快约七倍,那就算六到七路并发再留点余量。

这道算术的前提是 RTF 是个常数。而对一个用 continuous batching 服务的自回归模型来说,它是「卡上还有谁」的函数。

decode 是显存带宽瓶颈。只跑一路的时候,你要把整个权重矩阵过一遍显存才吐出一个 token,GPU 大部分时间在空转——这既是公布的单路 RTF 那么好看的原因,也是加第二路几乎不要钱的原因。并到八路,每一步的耗时跟一路时差不太多,于是总吞吐几乎线性上涨,而单路 RTF 几乎不动。这一段是容量规划体感最爽的区间:加人不要钱。

然后它就停了。过了某个 batch size,步耗时开始随 batch 增长,卡上每一路的单路 RTF 同步劣化——包括那些早就在跑、刚才还好好的。这条曲线不是缓坡撞墙,是一路平的然后突然不平了;拐点在哪,取决于模型大小、序列长度,以及你还剩多少显存放 KV cache。

所以厂商那个数诚实的写法是:空卡、并发 1 时 RTF 0.15。它告诉你这个模型什么体格,它不告诉你一张卡能装多少用户。

「跑满了」听起来是什么样

下面这一段是语音和聊天真正分岔的地方,也是 LLM 那套服务经验不能照搬过来的原因。

一个文本 LLM 接口过了拐点,无非是所有人的 token 慢一点到。界面还在流式输出,答案还是会给完,多数用户的感受最多是「今天有点卡」。劣化是成比例的,是能扛的。

语音底下有一条硬线。播放一旦开始,客户端就在以一秒一秒的速度消费音频,生成必须一直跑在它前面,否则缓冲区见底。越过那条线,你得到的不是「稍微慢一点的音频」,而是一个词读到一半的卡顿——任何听者都会把它读成产品坏了,而不是产品忙。同样是慢 20%,聊天用户根本不会察觉,而一路原本 1.3 倍实时的流会被摁到 1.0 倍以下。

而且它不是只打到一个人。continuous batching 意味着边际上多进来的那一路,会向同 batch 的每一路收税。把你顶过拐点的那个用户没事,正说到一半的所有人才是听见崩的那批。这种「新负载去损伤已有会话、而不是排在它们后面」的倒置,是服务语音和服务文本之间最要命的一条运维差异。

你真正该规划的那个数

既然如此,吞吐就是错的目标。该拿来做规划的是:在 p99 意义上、每个 chunk 的生成速度仍然跑赢实时播放并且有余量的最大并发数。

量它花不了一个下午:

  1. 挑一份有代表性的负载——真实的句长、真实的参考音频、你线上实际在用的音色。长尾句长在这里很重要,因为 KV cache 是随它涨的。
  2. 并发爬坡:1、2、4、8、16 路,每一档跑够时间进入稳态。
  3. 每一档都记录每一路每个 chunk 的到达时间戳,算出每墙上秒交付了多少秒音频,按路统计,看 p99 而不是均值。
  4. 找到 p99 跌破 1.2 倍的那个并发数,那是你的天花板。不要取 1.0 倍——那是悬崖边,你得给网络抖动、给一句超长文本、给一次 GC 停顿留位置。

结果通常有两点让人意外。一是它往往明显低于按吞吐算出来的容量,因为吞吐饱和发生在音质塌掉之后很久。二是在这个并发下 GPU 可能只用了一半左右,在监控面板上看着像浪费——那正是「不卡顿」的标价。如果有人要求你把利用率提上去,你要拒绝的就是这笔交易。

与其优雅降级,不如直接拒绝

推论:到了天花板就拒掉下一路,别把它收进来让所有人一起慢。

这跟文本服务的直觉正好相反。文本那边,多收一个请求、把痛苦摊薄通常是对的;而对流式音频来说,摊薄痛苦是把一个被拒的会话换成十个能听出来坏掉的会话。连接时被拒是可恢复的——可以重试、可以退到备用音色、可以给排队位次;一句话读到第九秒卡了,不可恢复,而且那才是用户会记住的东西。

具体做法是在模型前面放一个真实的队列并设并发上限,而不是指望推理引擎的内部调度替你收拾。同时要盯对信号:在 continuous batching 下,引擎的队列深度会一直是零,而音质在同时崩塌——因为根本没有东西在排队,全都收进来了,全都在变慢。队列深度会一路健康到用户开始投诉。真正会在电话打来之前先动的指标,是 p99 的 chunk 到达间隔。

同一个根因带出的两个小坑

混部并不像你希望的那样可行。 LLM 推理引擎通常在启动时就把一大块显存预留成 KV cache,所以一张卡上放聊天模型和 TTS 不是一场协商——先起的那个把它要的全拿走,后起的要么起不来要么反复颠簸。这事很无聊,但仍然会让团队丢掉一天,而且通常正好是要上线那天。实时语音就给它单独一张卡,或者把显存比例显式钉死,并且在真出事之前先把这个组合测一遍。

ASR 继承了同一套性质。 走自回归解码器的流式识别同样有随 batch 变化的步耗时,而且还多一个 TTS 没有的旋钮:每个 chunk 在吐出之前要看多少右侧上下文。上下文越多、准确率越高、延迟越大——而这个设置会跟负载互相作用,因为一个本来就迟到的 chunk,早就把它的延迟预算花完了。如果你的链路是 ASR → LLM → TTS,那么现在这三段共享同一种失效模式,端到端预算也不再是三个独立均值之和,它由离自己那个拐点最近的那一段主导。

结论

语音模型 token 化换来了实打实的质量,同时把运维上的问题从「模型够不够快」挪到了「这张卡在音频崩掉之前能装多少路」。这是两个问题,答案也不一样。

别再拿模型卡上的 RTF 做规划。做并发爬坡、量 p99 的每 chunk 交付速度、把上限设在它还稳稳高于实时的地方,然后在那里卡住准入。剩下的利用率就认了——另一种选择是一张在图上很高效、在耳朵里已经坏掉的 GPU。

如果你压根不想自己养这条曲线,托管接口大体就是干这个的——MiraEcho 的流式合成可以免费开始用,并发上限是我们的事,我们负责让它诚实。

用 MiraEcho 把它做出来

流式 TTS、实时 ASR、声音克隆,一个 API Key 全都有,免费开始。

免费开始使用