跳到主要内容

你的供应商通过更小的分块达成的 Tokens-Per-Second SLO

· 阅读需 12 分钟
Tian Pan
Software Engineer

你的供应商状态页面显示为绿色。每秒 token 数 (TPS) 仪表盘显示的曲线一如既往地平稳。SLA 报告显示你完全处于合同约定的速率范围内。然而,支持队列里挤满了用户,他们形容聊天输出“一跳一跳的”、“断断续续”、“比上周还差”。你的监控指标中没有任何一项能证实他们的说法,因为你的监控根本没有在测量他们真正在关注的东西。

这是没有人察觉到的供应商交付故障模式。他们没有突破速率限制,而是重新定义了单位。每秒到达的 token 数量没变,但它们是以单 token 块的形式流式传输的,而不是针对渲染器优化过的 4 token 块。平均吞吐量依然完好,但感知质量却被毁掉了。SLO 依然达标,因为 SLO 是针对网络传输(wire)制定的,而网络传输是供应商控制的那部分系统。

流式吞吐量是供应商定义的指标

你签署的关于“响应平均每秒 token 数”的合同听起来是一个固定值。其实不然。分母是固定的,但除此之外的每一个参数都是供应商可以调节的旋钮。分块大小(chunk size)、爆发形状(burst shape)、每块的开销、第一个 token 何时离开预填充(prefill)、队列峰值后解码追赶的激进程度——所有这些都会影响体验,但不会影响平均数值。

这种技巧最明显的版本是头部爆发(bursting at the head)。供应商在响应的前 200 ms 内排干缓冲的 token 批处理,然后以较低的解码速率缓慢流出长尾部分,同时依然能达到平均值。对于消费者来说,响应感觉是先冲刺然后停滞。网络指标看不见停滞,因为它是针对整个响应进行平均的。

更隐蔽的版本是降低分块粒度。昨天一个分块携带 4 个 token。今天,同样的后端更改提高了他们在负载下的批处理效率,但也把 SSE(Server-Sent Events)事件流压平成了单 token 块。你的渲染器在每个块上花费大约 2 ms 的开销——解析、修改状态、清理 markdown、运行高亮器、重新计算布局。在每块 4 个 token 时,这种成本是不可见的;但在每块 1 个 token 时,这种成本是致命的。供应商的吞吐量指标没有变化,但你的前端帧预算崩溃了。

核心指标问题是结构性的。Anyscale 和 BentoML 都发布过指南,将 token 间延迟方差(inter-token latency variance)视为决定流式传输是“流畅”还是“卡顿”的属性。然而,SLA 上出现的却是 TPS,因为这是供应商在异构集群中可以承诺的数字。合同针对的是可签约的内容进行优化,而不是针对感受进行优化。

更小分块的成本剖面

单 token 的 SSE 分块不仅仅是同一操作的更小碎片。它是一个独立的操作,拥有自己的固定成本。

在网络传输中,每个分块都带有 HTTP/2 帧头、SSE 事件分隔符和 JSON 外壳({"choices":[{"delta":{"content":"..."}}]})。这个外壳通常比它携带的 token 还要长。4 token 的分块将这些开销摊销到 4 个字节的负载中;单 token 的分块则需要支付 4 倍的开销。

在消费者端,每个分块都会触发解码并渲染循环。即使是一个构建良好、使用 requestAnimationFrame 批量更新的 React 前端,也无法让每个分块的解析、清理和 markdown 重新遍历变得完全免费。流式 markdown 解析器的存在正是因为另一种方案——在每个分块到达时重新解析整个文档——已经行不通了。缩小分块大小会使该解析器的调用频率增加三倍,而无需改变它需要产出的内容。

在移动设备上情况更糟。桌面浏览器可以平滑到 30fps 的单 token 节奏,会导致中端 Android 设备掉帧,因为每个分块都在进行唤醒、解析、布局、绘制——而绘制才是用户真正看到的部分。供应商的 TPS 仪表盘不知道你的设备群中包含手机。

在追踪存储(trace store)中,每个分块都是一个 span。如果你为了调试而记录每个分块的 SSE 事件——大多数团队在前六个月都会这么做——分块增加四倍意味着追踪存储成本增加四倍,而观察到的信息并没有相应增加。那个悄无声息翻了三倍的账单项正是没人关注的那一项。

后端调优是如何悄悄潜入的

没有任何公告。供应商并不是故意要降低你的体验。模式很平庸:服务端的一个变更通过在 token 完成解码时立即发射,而不是等待合并,从而提高了高并发下的吞吐量。他们那边的吞吐量数字上升了。在 95% 的负载下,他们的 P99 token 间延迟得到了改善。于是他们发布了。

他们没有测量的是消费者端的成本。他们的基准测试集群是一个从套接字读取字节然后丢弃的负载生成器。那个生成器不在乎字节是以 1 个块还是 4 个块到达的。但你在乎,因为你有一个渲染器。

当供应商为新的内容类型引入类型化事件生命周期时,同样的动态也会出现。当 Anthropic 在流式响应中加入思考块(thinking blocks)时,传输格式开始在每个内容块周围携带额外的 startstop 事件。总有效负载增长了,但有意义的 token 却没有增加。那些根据 2024 年网络协议格式手写 SSE 解析器的消费者,一觉醒来发现系统出现了无声的故障。供应商的 SLA 未受影响。

教训并不是说供应商是敌对的。教训是,供应商的优化面和消费者的感知面是不同的,而合同中的指标站在供应商这一边。任何在消费者端发生变化但在供应商仪表盘上没有显示的指标,对于那些有能力改变它的人来说都是不可见的。

消费者拥有的感知 SLO

加载中…
References:Let's stay in touch and Follow me for more thoughts and updates