延迟预算 — 从说完话的那一刻到首个声音
一句话总结
用户感受到的延迟,是从停止说话的那一刻到发出第一个声音。它是端点判定(音频时间)+ 最终结果 + 检索 + LLM 第一个片段 + TTS 第一个声音(挂钟时间)的总和。串行连接时,LLM 的全部和 TTS 的全部会相加;重叠运行时,只会加上第一个片段的时间。必须为每个阶段写出预算,并用分布(p50、p95)来对照,才能看出该缩短哪里。
为什么需要它
前面的模块中,各个部件分别测得的数字,单独看都很小——最终结果几十 ms,第一个 token 一百多 ms,TTS 的 RTF 是 0.04。但是连起来,就是以秒为单位的了。LabHub 口语练习的 3.3 秒,也是三个部件(ASR 0.4 + 模型 0.6 + TTS 2.3)的串行总和(backend/app/lang_talk.py)。比起哪个部件慢,怎样连接造成的差别更大。
工作原理
两种时钟。用户说话期间,流式 ASR 已经在转写了。所以那段时间不是延迟。延迟是从话说完之后开始的。但是,知道“话说完了”的那一刻(端点),比实际话尾要晚——VAD 看到结尾之后,还要再安静一个 gap 的时间才能确信(模块 2)。这一份要用音频时间来测量:端点 − 实际话尾。在本课程的查询中,gap 为 0.5 秒时,中位数约为 0.57 秒。端点之后的事情(最终结果、检索、LLM、TTS)用挂钟测量,把端点的那一刻定为 0。不要把两种时钟混在一起,最后再相加。
串行与重叠。串行是 LLM 把答案全部写完之后,TTS 再合成整体——第一个声音 = 最终结果 + 检索 + LLM 全部 + TTS 全部。重叠是在 LLM 写的过程中,一有片段生成就交给 TTS——第一个声音 = 最终结果 + 检索 +(直到第一个片段生成为止)+(第一个片段的合成)。在相同的五条查询上,第一个声音的中位数,串行是 1,814 ms,重叠是 1,593 ms(在集群 nuc1 节点上 2 个 CPU 的 Pod 里测得的值——不同节点会有所不同,所以请看实验中你自己测得的数字)。收益比预期小的原因,正如模块 7 中看到的,是 LLM 和 TTS 共用同一个 2 核。
用跟踪画出来。条形图比数字表读得更快。Chrome 的跟踪格式(Trace Event Format)是 {"name", "ph": "X", "ts", "dur", "tid"} 的事件列表,ts 和 dur 的单位是微秒。在 chrome://tracing 或 Perfetto 中打开,就能看到 LLM 条形与 TTS 条形重叠的区间。这种格式会延伸到模块 9 的跨度记录(span)。
看分布,而不是平均值。对话中的不快,来自偶尔出现的长时间沉默。所以要在中位数(p50)之外,同时看 p95。样本有 10 个时,p95 就是最慢的那个(最近秩)。样本很少时,插值得到的百分位数会生成并不存在的值,所以本课程使用最近秩。
预算。从“人与人对话中的轮次间隔大致是几百 ms”这一观察(模块 2)倒推分配,为每个阶段写出预算——例如:端点 700、最终结果 150、检索 100、第一个 token 400、第一个声音 600 ms。测得的中位数超出预算的阶段,就是该缩短的地方。有些部分无法通过模型缩短(端点);有些部分通过提示词缩短(第一个 token——缓存和较短的上下文);有些部分通过衔接缩短(第一个声音——重叠和较短的第一个片段)。
内存也是预算。这个 Pod 的限制是 2 GiB。Python 进程(流式 ASR、TTS、嵌入)最多用了约 370 MB,LLM 服务器用了约 730 MB(nuc1 实测,请求集中时 LLM 服务器升到了 830 MB)——加起来略超过一半。一旦再加载一个模型,或者把 LLM 升级到 1.5B,就会触到限制。超过之后,内核会杀死进程(OOM),用户听到的就是没有任何解释的沉默。
在现场相遇的样子
LabHub 口语练习把一轮对话连成“整段录音 → ASR → 模型 → TTS”,并且为了不争抢 GPU,每次只接收一个人占位。在这种结构下,要缩短 3.3 秒,比起让部件变快,更优先的是流式输入(由机器来判定端点)、模型流与 TTS 重叠、较短的第一句话。本模块会用小模型亲自测量这些效果。
测量时有一点要小心。这个 Pod 的限制是 CPU 2 核,但容器里的 nproc 说的是节点的核数(24)。numpy 使用的 BLAS 会按这个数量启动线程,在 2 核之内相互挤占——寻找回声延迟的 3,200 次短 np.dot,从 0.11 秒变成了 52 秒(制作这个镜像时的实测)。所以镜像把 OPENBLAS_NUM_THREADS=2 固定了下来。测量延迟的实验,会因为这样一个环境差异而相差几百倍。
下一项实验要做什么
用音频时间测量 12 条查询的端点延迟。把最终结果 → 检索 → LLM → TTS 串行连接起来测量,再用同样的查询重叠运行进行测量并比较。把一次重叠运行用 Chrome 跟踪格式画出来,并给出 10 条查询的用户体感延迟 p50/p95。把各阶段的预算与测量值对照,并测量两个进程的内存,与 Pod 的限制比较。