一般来说,tts模型推理的服务方式与llm不同。一个请求会依次经过参考音频编码器、AR部分的网络、帧级音频 token 采样路径和流式解码器。除了通常的每个阶段都有自己的正确性契约,而每一项优化都可能不经意之间改变系统的语义。
我的其中一项工作正是参考音频缓存(ref audio cache),一般现在的tts模型都会需要一段reference audio + prompt来达成voice cloning。这个缓存看起来很简单。对于给定的参考音频文件,计算内容哈希,编码一次音频,之后复用生成的 codec token。如果同一个说话人参考在许多请求中被复用,缓存就很有价值:因为缓存命中本质上只是一次内存查找,而省略了相对来说比较麻烦的encode环节。
但是缓存与批处理之间的交互似乎没那么简单。因为ref audio的长度并不相通,batch size也是一个config,我们发现不同的batch和padding长度,经过codec出来的valid token并非完全相同的。
于是第一个问题变得非常小:
encode([A])[0] 是否等于 encode([A, B])[0]?
答案是不等于。
重复的单独编码是稳定的,重复的批处理编码也是稳定的。这不是普通的运行间不确定性。模型对于固定的执行形状是确定性的。问题在于 batch 不变性:同一段音频在单独编码和作为批处理一部分编码时,可能产生不同的离散 codec token。这个区别很重要。如果输出在重复运行中是随机的,修复方向会指向种子控制、确定性内核或移除不确定性操作。但这里的系统更加微妙。从服务器的完整输入视角来看它是确定性的,但从单个请求的视角来看它是不确定性的,因为该请求的并发邻居改变了 batch 形状。
下一步是避免猜测。
一个诱人的解释是 padding 泄漏:也许第二段音频改变了 padding 长度、attention mask 或有效区域处理。我通过控制长度和 padding 内容来测试这一点。相同长度的配对仍然产生不匹配,所以这不仅仅是变长产物。然后我改变了 padding 内容:zero padding、随机 padding、重复末尾 padding。有效区域的 token 在这些 padding 变体中保持一致。这使得 attention mask 泄漏的可能性变小了。
为了测试这一点,我逐层追踪了 codec 编码器。第一个具体的分歧出现在 encoder.7.input_proj。线性层的输入是相同的,但输出在 BF16 精度量级上存在微小差异。然后我隔离了真实保存的输入和权重,并用一个独立的线性层调用复现了同样的差异。
这就是关键所在。偏移不是由高层 transformer 逻辑引起的,而是形状敏感的 GEMM 效应。根据input tensor不同,系统会选的不同的kernel去执行这次计算,会引起微笑的变动。在连续网络中,微小的 BF16 差异通常无害,但这条路径最终会经过rvq的量化。一个微小的激活变化可能跨过量化边界,变成一个不同的 token ID。至此,缓存 bug 变得清晰了。
问题不在于 MOSS-TTS 有缓存:缓存是有用的,应该保留。但是我们是否需要force一个仅基于内容的 key 配对了一个不变的的 value,不论input和batch都是什么形状。
有三种可能的设计方案。
第一种方案是保留内容 key,但每一次的 value 都是用 B=1 encode作为参考音频编码。在缓存未命中时,通过真正的单独路径编码参考音频,并存储该结果。这样缓存填充有一个稳定的含义,但是代价是假如缓存一直不能hit,那么单独encode的速度会比batch慢很多。本质上,是在极端情况下(冷启动,少复用ref audio)牺牲速度换取一致性。
第二种方案是在 key 中包含执行形状。这保留了批处理未命中填充的语义,但会导致缓存碎片化并且很难命中。input形状、dtype、设备、布局、后端、CUDA 版本、PyTorch 版本都会影响GEMM 算法的选择,这样的 key 不再是一个干净的内容寻址缓存。
第三种方案是对批处理路径禁用缓存。这很简单,但在真实服务负载下放弃了太多收益,尤其是当同一小组参考语音被大量复用时。
我倾向于第一种方案:使用真正单独的未命中填充的规范内容缓存。原因来自工作负载的角度。如果参考语音被大量复用,主要变量是缓存命中率,而不是未命中填充的吞吐量。例如,有 10 个参考语音和 10,000 个请求时,重要的是将参考编码器的代价大约支付 10 次,而不是 10,000 次。那 10 次首次填充是否是批处理的,远不如确保每个内容 key 有一个稳定的值重要。
这个实验改变了设计问题。我不再问”我们能否更快地批处理参考编码?“而是在问”这个缓存要在语义上正确,需要满足什么条件?”
这就是这项工作更广泛的教训。在 TTS 服务中,性能优化往往跨越阶段边界:批处理、缓存、流式传输、CUDA graph 和调度都在相互影响。一个局部的优化可能意外地改变系统的用户可见行为。
阅读完整文章:LMSYS。