第 30 章 模型运行:预填充、解码、KV 缓存与量化
30.1 为什么第一个 token 走的路不一样
Section titled “30.1 为什么第一个 token 走的路不一样”差旅助手的提示里已经放入更新后的 750 元限额。第 29 章说明拿到 logits 后怎样选 token,却还没有说明这些 logits 如何算出、为什么首个可见 token 可能等得更久,以及模型权重不变时,较长的对话为什么仍会多占内存。
这里的 inference 指模型运行 ,也就是把输入交给训练好的模型进行计算;reasoning 指解题推理 ,也就是通过论证步骤解决问题。生成一句问候、算一道题、给出错误回答,都会运行模型,但只有其中一部分可能包含有效的解题推理。本章测量计算阶段,不把耗时当作思考过程的证据。
30.2 先读已知前缀,再逐个处理新 token
Section titled “30.2 先读已知前缀,再逐个处理新 token”例 30.1:八个提示位置。 假设提示已经分词,占八个位置,包含所需特殊 token。这些位置的输入在开始时全部已知。因果解码器的每个位置只能使用自己及之前的位置,但因为输入 ID 已知,每层仍可把多个提示位置的工作组织在一起计算。最后一个提示位置提供输出 token 1 的 logits。这段初始处理称为 预填充(prefill)。
选出 token 1 后,它的 ID 成为新输入,下一步处理这个 ID,得到 token 2 的 logits,之后依次重复。这些逐步计算称为 解码(decode) 。未来输出 ID 尚未知,因此存在顺序依赖;处理器再多,也不能仅靠并行就提前知道尚未选出的 ID。
最简单的正确实现,是每一步重新运行不断增长的完整前缀。本书现有 Mini GPT 生成器就是这样做的,便于检查,却会重复计算许多旧中间值。在参数固定、位置约定相同的确定性因果模型里,追加未来 token 不会改变较早位置的表示,因为那个位置的注意力本来就不能向后看。这个性质提供了复用机会。
KV 缓存(KV cache) 保留各层已经处理过的位置所产生的键和值。一次新解码先算新位置的 Q、K、V,把新 K/V 加入缓存,再用新 Q 与保存的 K 比较,其中也包括它自己的新 K;随后用对应 V 混合,并继续经过后面的层。这样,不必仅仅为了再次获得旧 K/V,就把旧位置重算一遍。RoPE 等位置变换也必须保持一致:旧键产生时的处理方式,要与新查询的使用方式相容。
表 30.1:选出 token,还不等于已经算出它的缓存
| 刚完成的计算 | 已保存 K/V 的输入位置数 | 刚能选择的输出 |
|---|---|---|
| 预填充八个提示位置 | 8 | Token 1 |
| 解码,处理 token 1 | 9 | Token 2 |
| 解码,处理 token 2 | 10 | Token 3 |
发出 token 1 的瞬间,并不会自动生成它在每一层的 K/V;那是下一次前向计算完成的工作。分清两者,才能避免在请求结束时把缓存位置多算一个。具体服务停止后可能保留或丢弃不同数量的状态,trace 必须标明测的是哪个时点。
- 模型之外0 到达 → 排队至 30 ms
- 处理步骤预填充位置 1—8
- 一组数字8 个位置的 K/V:512 字节
- 文本片段150 ms:发出输出 1
TTFT 包含排队:150−0 = 150 ms。
- 文本片段把输出 1 作为新输入
- 处理步骤追加新 K/V,查询 9 个位置
- 一组数字9 个位置:576 字节
- 文本片段190 ms:发出输出 2
刚发出的 ID 还没有经过各层算出自己的 K/V。
- 文本片段235 ms:输出 3,缓存长 10
- 文本片段270 ms:输出 4
- 处理步骤间隔 40、45、35 → 均值 40 ms
四个输出形成三个后续间隔;只有一个输出时没有 TPOT。
图 30.1 把输入和输出时间线接在一起。缓存保存的是逐层数值上下文,不是以前写出的答案字符串。它免去了旧 K/V 的重复计算,但普通完整注意力中的新查询仍需与保留上下文交互。因此,即使有缓存,前缀越长,解码工作也可能越多。
30.3 从明确的时间边界开始计时
Section titled “30.3 从明确的时间边界开始计时”例 30.2:一份给定 trace,不是硬件基准。 所有时间使用同一时钟,以毫秒计。请求在 0 到达,30 开始预填充,四个输出 token 分别在 150、190、235、270 发出。本例假定发出后没有传输延迟;若测真实用户体验,网络传输和缓冲还要另记时间戳。
等到首个 token 共用 ms,称为 首 token 延迟(time to first token,TTFT) 。其中包含 30 ms 排队,以及给定的 120 ms“预填充开始到首次发出”区间。只算 ms 会遗漏排队,回答的是另一种问题。
后续三段间隔为 、、 ms,平均 ms。四个输出 token 只形成三个相邻间隔,这就是分母取 3 的原因。到达时间为 a,发出时间为 ,且至少发出两个 token 时,定义:
本章的 每输出 token 时间(time per output token,TPOT) 特指首 token 之后的平均间隔。代回本例,得 ms。有些报告采用别的口径;只写“延迟”,不写边界,就无法比较。倒数 输出 token/秒,描述的是首 token 之后的速度,不是完整请求吞吐,也不是多请求服务吞吐。
请求在 270 ms 结束,若把完整耗时除以四个 token,就得到 67.5 ms。这个平均值包含最初等待,与前面的量不同。若只发出一个 token,TTFT 仍存在,但没有相邻输出间隔,TPOT 应记为缺失,不能记为零。完全没有发出 token 的失败请求应进入错误计数;偷偷丢掉它,会让延迟和质量都显得更好。
30.4 数清缓存占用多少字节
Section titled “30.4 数清缓存占用多少字节”例 30.3:一份能逐项数清的小缓存。 给定两层模型,每层两个 KV 头,每头四个坐标,每个坐标占 2 字节。一个请求处理完八个位置,缓存占用为:
各因子依次为 K 与 V 两类、请求数、层数、位置数、KV 头数、头宽度、每个值的字节数。每多处理一个位置,增加 字节,因此表 30.1 三行分别需要 512、576、640 字节。
若 B 个请求已处理长度都为 T,模型有 L 层、 个 KV 头,头宽 d,每个值 b 字节,则理想数据量为:
每个维度都数不同的一组值。查询头数不另外出现,第 26 章已说明 GQA 的查询头为何可以多于 KV 头。请求长度不同时,用所有请求已处理长度的总和替换 BT。实际分配块、填充、元数据和为未来保留的容量,可能让占用更大。
再给定 L = 24、、d = 128、T = 4,096、B = 1、b = 2,乘积为 402,653,184 字节,即 384 MiB。两个同样长的独立请求,理想情况下需 768 MiB,还未包含权重、logits 和工作区。若只降低权重精度,缓存仍用每值 2 字节,这份 KV 计数就不会变。
缓存还有有效条件。把前缀中的 600 元旧政策改成 750 元,会改变输入;从改动位置起保存的 K/V,通常不能假装什么都没发生而继续复用。权重、适配器、tokenizer ID、位置或注意力规则变化,也可能使缓存失效。聊天应用保存的消息是文本记录,日后可以放回提示,并不会自动成为同一个 KV 缓存对象。答案缓存按复用规则保存以前的回答;KV 缓存既不保存取消订单的权限,也不是保证政策始终正确的长期知识来源。
30.5 用更少的位,存一个邻近值
Section titled “30.5 用更少的位,存一个邻近值”权重或缓存太大时,量化(quantization) 用更少的数值级别保存近似值。要理解质量与存储的取舍,先看清映射规则。
例 30.4:四个数,共用一个尺度。 给定 。使用四位整数 −7 到 7,额外的一个有符号码不使用;尺度 ,零点为 0。先除以 0.5,再舍入到最近整数,最后截到允许范围。恰好在中点时,取最近的偶数;这四个输入都不在中点上。
表 30.2:推导出的存储值与带符号误差
| 原值 x | x / 0.5 | 所存 q | 重建值 0.5q | 重建值 − 原值 |
|---|---|---|---|---|
| −1.2 | −2.4 | −2 | −1.0 | +0.2 |
| −0.2 | −0.4 | 0 | 0.0 | +0.2 |
| 0.7 | 1.4 | 1 | 0.5 | −0.2 |
| 1.8 | 3.6 | 4 | 2.0 | +0.2 |
这个对称映射写为:
帽号表示从整数 q 重建的近似值。正尺度 s 决定相邻可表示值之间的距离。在未发生截断的范围内,最近舍入的误差不超过 。若遇异常大的 x = 8,原本要得到整数 16,却被截成 7,重建为 3.5,误差 −4.5,远超舍入误差界。那个界本来就不包含截断。
四个四位值紧凑打包后需 2 字节,再加一个 4 字节的 FP32 共享尺度,共 6 字节;原来的四个 FP32 值占 16 字节。因此,这个小块并没有实现八倍压缩。分组越大,尺度开销越容易摊薄;分组越小,可能更适合差异较大的范围,却增加元数据。一般仿射方案还使用零点,通过尺度乘整数偏移量重建实数。具体格式、分组方式,以及哪些张量被量化,都必须明确。
30.6 小误差,也可能改变一次离散选择
Section titled “30.6 小误差,也可能改变一次离散选择”给定两个 logits,候选顺序 Paris、Lyon,分数为 。贪心原本选 Lyon;用尺度 0.5 量化后,两者都映成 0.5,形成并列,按第 29 章顺序反而选 Paris。绝对误差不大,选中的 token 却变了;后续输入也随之变化。
因此,“四位”不是质量结论。只量化权重、量化激活、量化 KV,影响的是不同数组。反量化、打包和专用整数内核也会影响实际速度;存储位数少,不保证每项运算都更快。选择尺度的校准数据必须与最终评测分开,并在相同提示、任务标准和输出预算下比较。异常值和接近并列的决策都值得单独保留为失败例。
标量练习复现本章全部字节、时间戳和量化映射,没有声称交付量化的 MG-2 检查点或 GPU 性能实测。第 29、33 章实际运行的 MG-2 仍使用原来的 float64 完整前缀实现。本章预填充/解码 trace 是另一份独立教学场景,不能把其中 150 ms 的 TTFT 引作 Mini GPT 实测延迟。
至此,一个请求有了生命周期、逐渐增长的状态和数值表示。多个请求一起服务,还会引入另一层问题:调度可以复用上下文、重叠工作,却也可能让某个用户等得更久。下一章继续把这些差别保留下来。
30.7 本章小结
Section titled “30.7 本章小结”预填充处理已知提示位置,提供首个输出的 logits;解码依次处理已选择的 token。KV 缓存在计算条件一致时复用各层的键和值。TTFT、TPOT 都要写明时间戳和分母。量化用表示误差换存储,舍入误差界只在未截断范围内成立,任务质量仍须测量。
30.8 习题
Section titled “30.8 习题”- 请求在 10 ms 到达,分别在 110、160、220 ms 发出 token。计算 TTFT、两段间隔、TPOT 和完整请求耗时。
- 十个位置的提示完成预填充并选出首个输出后,缓存有几个位置?下一次前向计算处理这个输出后呢?
- 例 30.3 较大缓存改为四个 KV 头、四个同样长的请求,其余不变。求理想总字节数与 MiB。
- 按尺度 0.5 和本章并列规则量化 0.75、−0.75,再量化 5。为什么最后一个误差的成因不同?
- 某报告用完整请求耗时除以输出数,标成“首 token 之后的 TPOT”。应怎样修正?只有一个 token 时如何处理?
- 引擎把权重改成四位,但 KV 仍为每值 2 字节。哪项资源估算改变?为什么仍须重跑同一组政策更新题?
30.9 参考解答
Section titled “30.9 参考解答”- TTFT 为 ms,两段间隔为 50、60 ms;TPOT 为 ms;完整耗时为 ms。只有 TPOT 排除了最初等待。
- 预填充后十个位置,处理新 token 后十一个。选择本身,不是新 token 经过所有层的一次前向计算。
- KV 头减半,每请求从 384 MiB 降为一半,再乘四请求,共 768 MiB,即 805,306,368 字节。查询头不是额外因子。
- 缩放后为 1.5、−1.5,最近偶数舍入得 2、−2,重建为 1、−1。5 缩放后为 10,被截成 7,重建 3.5。它的 −1.5 误差来自超出支持范围,不只是相邻级别之间的舍入。
- 应以最后一次发出减去第一次发出,再除以输出数减一。只有一个 token 时,TPOT 记为缺失。若保留完整耗时平均值,应另列并明确命名。
- 理想权重数据量改变,还要加尺度和打包开销;给定的 KV 数据量不变。近似权重可能改变 logits 与所选 token,所以能否正确使用 750 元更新必须重新评测,不能沿用原精度的结论。
30.10 参考文献
Section titled “30.10 参考文献”- Shazeer,Fast Transformer Decoding: One Write-Head is All You Need,2019。增量注意力实现明确了保存 K/V 和共享头的开销。
- Kwon 等,Efficient Memory Management for Large Language Model Serving with PagedAttention,2023。第 4.1 节区分逻辑 KV 内容与实际物理块分配。
- Jacob 等,Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference,2018。第 2.1 节定义尺度/零点映射;其移动网络测量不是本书的 LLM 结果。