第 31 章 高效服务:缓存复用、内核、推测解码与批处理
31.1 第一个答案没结束,另外两位用户就到了
Section titled “31.1 第一个答案没结束,另外两位用户就到了”差旅助手接连收到三个请求。一个模型可以依次回答,但第二、第三位用户要等第一位结束。让某项数学运算更快可能有帮助,却不会自动决定下一位服务谁。复用前缀只可能帮助共享前缀的请求;让另一个模型提前猜测,也可能因为接受的 token 太少而得不偿失。
本章保持一份工作负载不变,先定位每种优化改了哪一层。模型(model) 规定权重和计算;执行引擎(engine) 执行运算、管理设备状态;服务进程(server) 接收、排队、调度请求并返回结果;应用(application) 组织证据并判断怎样才算答对。这些职责可以在一个进程,也可以分布在多个进程里;区分的是责任,不是在规定部署图必须怎么画。
31.2 宣布加速之前,先列出时间表
Section titled “31.2 宣布加速之前,先列出时间表”例 31.1:完整给定的工作负载。 A 在 0 ms 到达,输入四个 token,要求三个输出;B 在 20 ms 到达,输入四个 token,要求两个输出;C 在 40 ms 到达,输入两个 token,要求一个输出。这些是给定计数,包含完整分词后的输入,在两份调度中完全相同。六个输出都完成,没有错误、取消或网络延迟。
表 31.1:给定的输出发出时间,使用同一时钟,单位 ms
| 请求 | 到达时间 | 输入/输出数 | 串行调度发出时间 | 连续调度发出时间 |
|---|---|---|---|---|
| A | 0 | 4 / 3 | 100, 140, 180 | 100, 150, 200 |
| B | 20 | 4 / 2 | 260, 300 | 150, 200 |
| C | 40 | 2 / 1 | 360 | 180 |
这两份时间表是教学给定值,不是某框架的实测,也不是仅凭 token 数预测的耗时。它们展示一种可能的取舍。不能把差值同时归因于四种优化;若要证明某项优化造成加速,应在相同工作负载下只改变一个因素,进行受控测量。
沿用第 30 章定义,串行调度中 B 的 TTFT 为 ms,连续调度中为 ms。A 的首 token 后间隔,串行是 40、40 ms,连续则是 50、50 ms。B 更早开始拿到内容,A 的后续 token 却慢了一些。
表 31.2:按时间表推导的逐请求指标
| 请求 | 串行 TTFT | 连续 TTFT | 串行 TPOT | 连续 TPOT |
|---|---|---|---|---|
| A | 100 ms | 100 ms | 40 ms | 50 ms |
| B | 240 ms | 130 ms | 40 ms | 50 ms |
| C | 320 ms | 140 ms | 缺失 | 缺失 |
C 只有一个输出,不能虚构一个零 TPOT 拉低平均值。还应保留分布或逐请求结果,因为一个均值可能掩盖 C 的漫长等待。只有三个请求时,尾部百分位几乎只是从三个观察值里挑一个,不能证明生产环境的尾延迟可靠。
31.3 复用相同前缀,不是复用“差不多的意思”
Section titled “31.3 复用相同前缀,不是复用“差不多的意思””设 A 的输入 ID 为 [10,11,12,13],B 为 [10,11,12,14],C 为 [20,21]。A、B 共享前三个 ID。在权重、位置设置及其他计算条件相同时,这段前缀的因果 K/V 也相同。前缀缓存(prefix cache) 把已算出的前缀状态保留到后续请求,使 B 在状态可用后复用它,只处理不同的后缀。这里最长共享前缀是三个位置;意思相似,或在提示别处出现相同片段,都不够。
这不表示 B 在 20 ms 到达时已经可以命中 A 的缓存。A 必须先算出并提供相关状态,服务还必须保留它。引擎可以共享物理缓存块,在分支追加时复制相关块,而非总复制全部数值。缓存淘汰可能让机会消失;长前缀占着的容量,也会挤占活动请求可用空间。
差旅更新例中,含 600 元旧规则的前缀,并不与含 750 元新规则的前缀完全相同。缓存键应标识 token 内容与计算版本,包括有关适配器和位置约定,不能只用文档标题。复用也必须处于应用允许的访问边界内。两位用户提交相同前缀,不会因此赋予其中一人访问另一人的私有内容或元数据的权利。
前缀缓存节省部分预填充工作,不同于第 30 章单请求内部的 KV 复用,也不同于直接返回旧答案。生成新后缀的工作并不会自动省掉。前缀是否命中,都不会改变给定的到达时间和输入长度;实际调度测量才会显示影响。
31.4 注意力函数相同,中间数据少搬一些
Section titled “31.4 注意力函数相同,中间数据少搬一些”完整注意力可能产生很大的位置对分数矩阵。直接实现会把大中间量写到相对慢的设备内存,随后再读回来做 softmax 和值混合。FlashAttention 把工作分成适合片上快速存储的小块,合并归一化统计,避免在较慢内存里物化完整分数矩阵。这是引擎/内核层面的改变,没有删除模型允许的注意力位置对。
例 31.2:合并块统计,不能等权平均块结果。 对一个查询,给定未归一化权重 ,标量值 。完整加权和为 ,权重总和 8,输出 1.5。分成两块后,第一块分子 10、分母 6,第二块分子 2、分母 2,合并仍得 。
若把两块已经归一化的结果等权平均,就得到 ,因为两块概率质量不同,所以算错了。真正的在线 softmax 还跟踪当前最大值,最大值变化时重新缩放累加量,保持指数计算稳定。这个小例只解释为什么可以合并充分的中间统计、不保存完整矩阵,并不是完整内核算法。
在精确算术下,FlashAttention 计算相同的注意力函数;浮点归约顺序不同,仍可能产生数值差异。内存搬运减少,不代表完整注意力的位置对算术已变成随长度线性增长。收益取决于硬件、支持的数据布局、序列长度及所选内核。一个很短的 CPU 请求可能把大部分时间花在别处,名称本身推不出普遍加速倍数。
31.5 先猜几步,再让目标模型核验
Section titled “31.5 先猜几步,再让目标模型核验”标准解码在输出 token 之间有顺序依赖。推测解码(speculative decoding) 让较便宜的提议过程先写几个 token,再让目标模型一起评估这些候选下文。若足够多的提议被接受,一次目标模型调用就能前进多个位置;某步被拒绝后,后面依赖它的猜测要丢弃。提议成本、核验成本、接受率和批处理条件共同决定是否省时。
目标采用贪心时,可以在提议 token 与目标模型在该前缀下的贪心选择一致时接受。目标采用随机采样时,仅接受目标最高分项,不足以保留目标分布,还需纠正规则。
例 31.3:拒绝后为何要从剩余概率中补抽。 候选顺序 Paris、Lyon。目标分布 ,提议分布 ,两者都已包含声明的生成策略。先从 q 抽样:提议 Paris 时,以 的概率接受;提议 Lyon 时总接受,因为目标给 Lyon 的质量不少于提议分布。
被接受的 Paris 概率质量为 ,Lyon 为 0.2。剩下 0.2 拒绝质量全部来自 Paris 提议,而目标还需要把这 0.2 全补到 Lyon。目标减提议后取正部,得 ,归一化为 。从这个剩余分布补抽,就完整恢复目标:Paris 0.6,Lyon 。
对提议概率 q 为正的候选 x,接受概率是 ;拒绝后按 p−q 的正部重新抽取。若 p、q 相同,则全部接受,不会去抽一个总和为零的剩余分布。如果错误地在拒绝后直接从 p 重抽,Paris 最终会变成 ,已经改变目标分布。
这段论证在算法假设下保留的是目标分布,不是与另一串随机数逐位相同。核验时采用不同温度或筛选,会破坏假设。本书复现双候选纠正算例,没有交付 GPU 推测服务,也没有把论文速度作为自己的结果。
31.6 每次迭代后,都可以更换活动批次
Section titled “31.6 每次迭代后,都可以更换活动批次”批处理可以共享昂贵的权重读取,并组织高效矩阵运算,但请求长度不同。如果固定批次要等所有成员结束,短请求可能完成后仍占着位置,新请求则一直在外面排队。连续批处理(continuous batching) 在执行迭代之间调整活动集合:移除完成请求,容量允许时接纳等待请求。Orca 的迭代级调度是这条思路的早期系统实例。
表 31.1 的连续调度中,C 在 180 ms 完成后,A、B 仍活动,到 200 ms 完成。这与动态改变活动工作相容,却没有规定每一次内核启动,也不能证明某个具体调度算法。接纳规则仍须限制活动 token、缓存容量,并考虑公平性。很长的预填充与解码混在一起,可能拖慢已有用户;把预填充分块是一种选择,也有自己的开销和取舍。
- 模型之外A:0 ms,输入 4/输出 3
- 模型之外B:20 ms,输入 4/输出 2
- 模型之外C:40 ms,输入 2/输出 1
时间均为给定场景,不是框架实测。
- 文本片段A:100 → 140 → 180
- 文本片段B:260 → 300
- 文本片段C:360
6 / .360 ≈ 16.667 输出 token/秒;A 后续间隔 40 ms。
- 文本片段A:100 → 150 → 200
- 文本片段B:150 → 200
- 文本片段C:180
6 / .200 = 30 输出 token/秒;A 后续间隔 50 ms。共同 .400 秒窗口中,两者均为 15。
图 31.1 直观保留了矛盾:连续调度让最后完成时间提前,却拉大 A 的相邻 token 间隔。面对请求队列,应用可能偏好这种安排;若用户流式速度有硬要求,也可能拒绝。“吞吐更多”不能替应用作这个决定。
31.7 除以秒之前,先选统计窗口
Section titled “31.7 除以秒之前,先选统计窗口”例 31.4:两种都成立的吞吐问题。 从第一个请求到达到最后完成,串行调度用 0.360 秒产生六个输出 token, 输出 token/秒;连续调度用 0.200 秒产生同样六个, 输出 token/秒。两者各用自己的完成窗口,衡量这批有限工作多快清空。
若两次实验都用 0—400 ms 的固定观察窗口,则两者都是 输出 token/秒。没有矛盾,只是分母变了,较快方案在窗口内闲置更久。报告应公开输入/输出计数、到达、预热、失败、并发和窗口端点。要测持续服务吞吐,还需在测量期间持续提供工作。
再给定单个资源每小时 2 美元,恰好按各自清空窗口收费,没有最低计费时长。串行成本为 美元,连续为 美元。换算成每千输出 token,约为 0.033333、0.018519 美元。这是虚构资源费率下的估算,不是 API 价格或实际账单。若已预留固定一小时,这六个 token 多快结束,所付时长都相同;人力、闲置容量和失败请求还可能占主要成本。
核心实践是阅读固定工作负载并推导这些指标,由标量实现核查。框架配置、GPU 实验属于选做,不能仅因某字段已打开就声称加速。现在我们有了很多资源指标,却还缺最重要的计数:到底有多少请求产生了合格答案?选模型之前,必须先写评测契约。
31.8 本章小结
Section titled “31.8 本章小结”前缀缓存复用匹配输入的既有状态,FlashAttention 改变注意力内部的数据搬运,推测解码核验提议,连续批处理改变哪些请求一起迭代。它们作用于不同层,也可能互相影响。比较时固定工作负载,明确 TTFT、首 token 后 TPOT、统计窗口内吞吐及成本假设,并同时检验答案质量。
31.9 习题
Section titled “31.9 习题”- 用表 31.1 计算两种调度下 A、B 的完整请求耗时。谁更早结束,谁反而更晚?
- 两种调度都改用固定 500 ms 窗口,吞吐是多少?为何不同于清空窗口的结果?
- 例 31.3 改为 、。给出接受概率和拒绝后的分布,再核查最终质量。
- 两个提示包含同样的词,顺序不同,前缀缓存能否直接判命中?即使 ID 相同,还必须匹配什么?
- 框架报告注意力内存减少,解码时间却几乎没改善。给出两种合理瓶颈解释,以及能够区分它们的测量。
- 服务通过把每个请求的首 token 都拖到应用限时之外,换来更多完成 token。在 TTFT 有硬上限的契约下,算改善了吗?
31.10 参考解答
Section titled “31.10 参考解答”- 串行 A 为 180 ms,B 为 ms;连续 A 为 200 ms,B 为 ms。B 的请求耗时缩短 100 ms,A 却增加 20 ms。同一调度改变,对不同用户的影响不同。
- 都为 输出 token/秒。共同窗口计入完成后的闲置时间,各自清空窗口没有计入。离开窗口定义就不能比较。
- 接受概率为 ,接受质量为 0.3、0.4,拒绝质量 0.3。正剩余为 ,所以每次拒绝都补成 Lyon,最终质量为 0.3、0.7。
- 不能。必须匹配 token 位置和从开头开始的共享部分,而非词袋。模型/适配器、位置设置和有关计算条件也须一致,并且访问边界允许复用。
- 可能由权重读取或非注意力运算占主导,也可能是排队主导端到端延迟。应在同一工作负载下,对比内核/设备计时与请求排队、输出时间戳。内存分配减少本身定位不了耗时瓶颈。
- 不算。硬上限是门槛,不能靠更多吞吐补偿。应同时报告吞吐提升与违反的上限,寻找另一种调度,或先明确修订契约再重新比较。
31.11 参考文献
Section titled “31.11 参考文献”- Dao 等,FlashAttention,2022。第 3 节与算法 1 解释分块注意力、在线归一化和减少内存搬运。
- Leviathan 等,Fast Inference from Transformers via Speculative Decoding,2023。第 2.3 节规定保留目标采样分布的接受及剩余纠正规则。
- Yu 等,Orca,2022。第 3 节解释迭代级请求调度,第 6.2 节明确限制了普遍延迟结论。
- Kwon 等,Efficient Memory Management for Large Language Model Serving with PagedAttention,2023。第 4 节解释支撑分配与共享决策的分块 KV 存储。