Token 间抖动:你的 p95 仪表盘看不见的流式传输 UX 失败
你的延迟仪表盘显示一切正常。p95 的首字延迟(TTFT)低于 800ms 的目标。p99 的总生成时间也在 4 秒的预算之内。然而,一位资深 PM 转发了一个支持线程:“助手在回答中途卡住了大约三秒钟”,“它停顿了一下,然后突然吐出一整段文字”,“我以为它死机了”。本周有三位用户因为同样的投诉卸载了应用。团队中没人能在笔记本电脑上重现这个问题,而且你记录的每一项指标都显示系统运行健康。
能解释这个 Bug 的指标正是你没在测量的那个:连续 Token 之间时间间隔的分布。一个看起来很完美的 p95 总时长可能会掩盖这样一种流:其中 8% 的响应在生成中途包含一个 2.5 秒的停顿。对于一个看着字符实时出现的用户来说,这种停顿意味着系统出故障了,而不仅仅是慢。你的仪表盘测量的是电影的总时长,而你的用户正在观看电影。
TTFT 和 TPOT 是快照;流式传输是连续体验
标准的 LLM 推理指标 —— 首字延迟(TTFT)、端到端延迟(E2EL)和每个输出 Token 的时间(TPOT) —— 是为基准测试设计的,而不是为了用户体验(UX)。TTFT 告诉你用户在看到任何内容前等待了多久。E2EL 告诉你完整回答花费了多久。TPOT 的定义是 (E2EL - TTFT) / output_tokens,它是每个 Token 的平均解码间隔。
平均值是一个陷阱。TPOT 是一个从开始和结束时间戳导出的标量;它无法代表分布情况。一个在 4 秒内生成 200 个 Token 但中间有一次 2 秒停顿的响应,与一个以 20ms 稳定节奏生成 200 个 Token 的响应,具有相同的 TPOT。但用户体验到的却是完全不同的产品。一个感觉是“活的”;另一个感觉先是“死了”,然后又复活了。
你真正需要的是将流式 Token 间延迟(ITL)作为一等信号 —— 在 Token 边界进行测量,记录为每个流的分布,并汇总为跨流的分布之分布。Anyscale 和 NVIDIA 都指出:ITL 的方差(通常被称为抖动,jitter)对感知 UX 的损害远大于持续缓慢但稳定的流。用户可以接受 60ms 的稳定节奏;但他们无法接受 25ms 的节奏伴随周期性的 1.5 秒挂起,即便后者在墙上时钟的时间(wall clock time)上完成得更快。
一个健康的 ITL 图表看起来应该像一条窄窄的平带:p50、p95 和 p99 紧紧靠在一起。一旦 p99 在解码中途开始偏离 p50,你就遇到了一个 UX Bug,其波及范围对你仪表盘上的任何其他指标都是不可见的。
流中停顿究竟源自何处
一旦你开始测量 ITL 分布,接下来的问题就是什么导致了长尾尖峰。诚实的回答是“五种不同的原因,而你需要对它们进行归因”,因为缓解措施各不相同。
服务端调度抖动。 大多数托管推理引擎运行连续批处理(continuous batching):新请求在下一个解码步加入正在进行的批次。当一个大的预填充(prefill)请求在中途到达时,下一个解码步会被拉长,因为预填充正在被融合(或根据调度器进行分块)。于是你的流停顿了。你的代码没有问题,你服务商的聚合 p99 仪表盘可能看起来也不错,因为尖峰是针对每个流的,而不是针对每个请求的。
KV 缓存压力。 在长文本生成过程中,如果运行批次的 KV 缓存总内存超过了设备预算,调度器必须将某个流的缓存交换到主机内存,或者完全将其踢出(强制在恢复时重新计算),或者暂停它直到内存释放。这些情况中的每一种都会在受影响的流上表现为 ITL 尖峰 —— 而受影响的流往往比例不均地集中在长文本流上,而这正是用户投入最多的地方。
投机解码拒绝事件。 当草稿接受率高时,投机解码(Speculative decoding)感觉像是免费的午餐,但每一次被拒绝的草稿都是一次浪费的前向传播,用户在延迟上为此买单。在分布外(out-of-distribution)的提示词上,如果草稿模型与目标模型意见不一,你会看到接受率从 80% 掉到 30%,原本看起来 2 倍的加速变成了 1.2 倍的减速 —— 这种减速集中在响应的中段,即模型偏离了草稿模型训练分布的地方。
提供商与客户端之间的网络缓冲。 TCP 缓冲、中间代理以及浏览器自身的绘制调度都可能在传输过程中对 Token 进行批处理。服务端以稳定的节奏给你发了 50 个 Token,但浏览器因为觉得这样效率更高,在同一帧内把它们全显示出来了。对用户来说这种停顿是真实的,尽管没有任何推理组件导致它。
连接层故障。 移动网络、不稳定的 Wi-Fi 和 CDN 重定向都会注入与模型无关的停顿。关于不稳定网络下 LLM 流式传输的研究(例如 Eloquent 传输方案)表明,丢包期间的重传会被感知为模型卡顿,因为用户没有理由认为是其他原因。
分类非常重要,因为修复方法不同。KV 压力需要更小的批次或感知分页注意力(paged-attention-aware)的准入控制。投机解码拒绝需要针对你的流量组合定制草稿模型或基于置信度的回退。网络抖动需要客户端缓冲区。将所有这些都归为“p99 延迟表现糟糕”会掩盖今天到底是哪一个问题在困扰你。
必须要落地的指标:将 ITL 作为一等 SLI
你无法修复你看不见的问题,而从大多数团队提供的指标中,你根本无法察觉到 ITL 抖动。这种改变在描述上很简单,但在实现上略显繁琐。
在每个 token(或者如果你拿不到 token 粒度,就按 chunk)对客户端可见的那一刻记录一个时间戳。这些时间戳之间的差值就是单流 ITL 序列。将它们以每个流为单位生成直方图,并进行跨流聚合,从而得出:
- 单流 p99 ITL —— 单个响应内部最严重的时间间隔。这是用户感知到的“ 卡顿”指标。如果是 2 秒,意味着有人盯着屏幕感受到了画面冻结。
- 跨流 p99 的单流 p99 ITL —— 长尾中的长尾。它捕捉的是那些包含最严重卡顿的流,而不是那些卡顿恰好与其他流同步的流。
- 抖动比率 (Jitter ratio) —— 每个流的
p99_ITL / p50_ITL。如果比率超过 5,则预示着即使均值看起来正常,该流也是不稳定的。
测量位置至关重要。服务端 ITL 捕捉的是提供商的行为;客户端 ITL 捕捉的是用户实际获得的体验,包括网络效应。你需要两者,因为缓解措施往往作用于不同的层级。不要试图通过起始到结束的时间戳和 token 计数来推导 ITL;那样你得到的只是 TPOT,而这正是最初掩盖 Bug 的那个指标。
一个实用的 SLO 制定方案:将 ITL 有效吞吐 (goodput) 定义为整个 ITL 分布都保持在目标阈值以下的流的百分比(例如,没有单个 token 间隙超过 400ms)。这比基于 TPOT 的有效吞吐更苛刻,也更接近用户的真实感受。近期关于 LLM 推理 SLO 的研究明确主张采用这种具备“分布意识”的指标,而不是标量均值,因为标量版本无法检测到流的局部失效。
平滑处理是一种权衡,而非免费午餐
一旦你能察觉到抖动,接下来的决策就是你愿意付出多少成本来掩盖它。架构通常分为三类,每类都有不同的成本特征。
客户端平滑缓冲区 (Client-side smoothing buffers) 是成本最低的方案。客户端接收网络传 输的 token,并以稳定的速率将其释放给渲染层 —— 通常是每块 33ms 的显示频率,这能保持在屏幕刷新界限之上。你以流开始时增加几百毫秒的额外延迟为代价,换取了消除小于缓冲区深度的流中卡顿。Google 的 Bard 和一些研究系统(特别是 Andes)明确使用了这种方法。用户会觉得这更加平滑,即使端到端时间略微变差,因为用户的损失函数是方差,而非均值。
服务端“先接收后刷新”模式 (Server-side accept-then-flush patterns) 通过消耗略多一点的计算资源来隐藏微小的暂停。服务器将 token 暂存一个极短的时间窗口(例如 50ms),然后以固定的节奏刷新,从而完全屏蔽了客户端感知到的子窗口调度抖动。当你控制客户端并能保持客户端缓冲区较浅时,这种方法最为有效。
流中提供商故障转移 (Mid-stream provider failover) 是重量级的选项。流处理层的看门狗会检测 ITL 是否超过阈值(比如 1.5 秒没有 token),如果超过,则取消当前流,并以现有的对话上下文作为背景,向备用提供商重新发起剩余生成请求。如果做得好,这可以向用户掩盖提供商侧的卡顿;如果做得不好,则会产生连贯性断裂,导致答案的后半部分与前半部分对不上。请将此方案预留给那些卡顿会造成灾难性后果的场景(如语音、实时智能体),并接受其运维复杂性。
决策归结为哪个业务场景在为哪种失败买单。对于用户会随产随读的代码助手来说,平滑处理获益巨大 —— 反正他们也不会以每秒 50 个 token 的速度阅读,因此额外的几百毫秒是不可见的。而对于 TTS 实时读取流的语音智能体,则需要相反的权衡:尽量减少缓冲,接受一定的抖动,并使用能在不增加队头延迟的情况下实现平滑的服务端模式。
真正能捕捉抖动 Bug 的评测 (Evals)
大多数团队对流式传输使用的评测准则通常是“响应出来了吗?内容正确吗?”如果一个流生成了正确的文本,但在响应中间停顿了 2 秒,这种评测是无法发现问题的。为了在抖动回归影响用户之前捕捉到它们,你需要能够观察流的形态(不仅仅是内容)的合成评估工具。
在实践中有两种模式行之有效:
- 针对客户端的流回放 (Stream-replay)。记录真实的提供商流(时间戳 + 数据块),在受控位置注入合成暂停,并将其输入你的客户端渲染器。对渲染后的流进行用户感知质量评分 —— 可以通过人工评分小组,也可以通过模拟人类阅读流畅度判断的感知模型。这能将客户端的平滑能力与提供商的行为隔离开来,从而使任何一层的回归都能独立显现。
- 针对推理栈的注入抖动压力测试。在故意超配并发的情况下,对推理层运行代表性的流量组合,捕捉单流 ITL 分布,并以“跨流 p99 的单流 p99 ITL”不退化作为发布的准入条件。大多数提供商即使在 KV 压力特征发生变化时也能通过 TTFT/TPOT 回归测试;而这种基于抖动的测试则能捕捉到这种漂移。
构建这些评测并不难,但它需要你认同一个理念:流式传输是一种需要持续评估的连续体验。仅仅关注单一平均值的计分板,只会不断通过那些已经破坏了用户体验的版本。
架构层面的 实现
大多数团队之所以缺失这项指标,原因在于文化,而非技术。延迟仪表盘的术语体系继承自请求/响应系统,在这种系统中,起始和结束的两个时间戳就能捕捉到所有关键信息。流式传输颠覆了这一约定:响应不再是一个事件,而是一个过程,且用户的损失函数取决于该过程的 形态 (shape),而非其终点。你的 p95 只是一个快照。而你的用户正在看电影,快照无法检测到影片在剧情中段发生的卡顿。
未来的方向是像 SRE 处理其他百分位分布那样对待 ITL 分布:将其视为一级信号,并根据分布而非平均值来定义有效吞吐量 (goodput)。平滑缓冲、服务端刷新和故障转移策略将成为根据每个界面 (surface) 所权衡的目标而应用的设计选择。做得好的团队所交付的流式体验,即便在底层推理层参差不齐的情况下,依然能让用户感到平顺。而没做好的团队则会一直纳闷,为什么仪表盘显示全是绿色,但 NPS 却在下滑。
如果你想在下一个 Sprint 中落地一件事,那就是:在每次发布前检查的仪表盘中,加入单流 p99 ITL 指标。当你第一次观察它时,你一定会发现一个 Bug。这就是意义所在。
- https://docs.anyscale.com/llm/serving/benchmarking/metrics
- https://bentoml.com/llm/inference-optimization/llm-inference-metrics
- https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html
- https://www.digitalocean.com/blog/llm-inference-benchmarking
- https://arxiv.org/html/2407.07000v1
- https://arxiv.org/html/2401.12961v2
- https://ably.com/blog/token-streaming-for-ai-ux
- https://docs.vllm.ai/en/stable/design/metrics/
- https://arxiv.org/html/2410.14257v1
- https://last9.io/blog/your-llm-is-slower-than-you-think/
