跳转到内容

第 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 的输入位置数刚能选择的输出
预填充八个提示位置8Token 1
解码,处理 token 19Token 2
解码,处理 token 210Token 3

发出 token 1 的瞬间,并不会自动生成它在每一层的 K/V;那是下一次前向计算完成的工作。分清两者,才能避免在请求结束时把缓存位置多算一个。具体服务停止后可能保留或丢弃不同数量的状态,trace 必须标明测的是哪个时点。

已知提示
  1. 模型之外0 到达 → 排队至 30 ms
  2. 处理步骤预填充位置 1—8
  3. 一组数字8 个位置的 K/V:512 字节
  4. 文本片段150 ms:发出输出 1

TTFT 包含排队:150−0 = 150 ms。

第一次解码
  1. 文本片段把输出 1 作为新输入
  2. 处理步骤追加新 K/V,查询 9 个位置
  3. 一组数字9 个位置:576 字节
  4. 文本片段190 ms:发出输出 2

刚发出的 ID 还没有经过各层算出自己的 K/V。

后续输出
  1. 文本片段235 ms:输出 3,缓存长 10
  2. 文本片段270 ms:输出 4
  3. 处理步骤间隔 40、45、35 → 均值 40 ms

四个输出形成三个后续间隔;只有一个输出时没有 TPOT。

图 30.1:先选出输出 token,下一步才把它作为输入处理。 本书原创图示 · CC BY-SA 4.0。

图 30.1 把输入和输出时间线接在一起。缓存保存的是逐层数值上下文,不是以前写出的答案字符串。它免去了旧 K/V 的重复计算,但普通完整注意力中的新查询仍需与保留上下文交互。因此,即使有缓存,前缀越长,解码工作也可能越多。

例 30.2:一份给定 trace,不是硬件基准。 所有时间使用同一时钟,以毫秒计。请求在 0 到达,30 开始预填充,四个输出 token 分别在 150、190、235、270 发出。本例假定发出后没有传输延迟;若测真实用户体验,网络传输和缓冲还要另记时间戳。

等到首个 token 共用 150−0=150150-0=150 ms,称为 首 token 延迟(time to first token,TTFT) 。其中包含 30 ms 排队,以及给定的 120 ms“预填充开始到首次发出”区间。只算 150−30=120150-30=120 ms 会遗漏排队,回答的是另一种问题。

后续三段间隔为 190−150=40190-150=40、235−190=45235-190=45、270−235=35270-235=35 ms,平均 (40+45+35)/3=40(40+45+35)/3=40 ms。四个输出 token 只形成三个相邻间隔,这就是分母取 3 的原因。到达时间为 a,发出时间为 t1,…,tnt_1,\ldots,t_n,且至少发出两个 token 时,定义:

TTFT=t1−a,TPOT=tn−t1n−1.\mathrm{TTFT}=t_1-a,\qquad \mathrm{TPOT}=\frac{t_n-t_1}{n-1}.

本章的 每输出 token 时间(time per output token,TPOT) 特指首 token 之后的平均间隔。代回本例,得 (270−150)/(4−1)=40(270-150)/(4-1)=40 ms。有些报告采用别的口径;只写“延迟”,不写边界,就无法比较。倒数 1000/40=251000/40=25 输出 token/秒,描述的是首 token 之后的速度,不是完整请求吞吐,也不是多请求服务吞吐。

请求在 270 ms 结束,若把完整耗时除以四个 token,就得到 67.5 ms。这个平均值包含最初等待,与前面的量不同。若只发出一个 token,TTFT 仍存在,但没有相邻输出间隔,TPOT 应记为缺失,不能记为零。完全没有发出 token 的失败请求应进入错误计数;偷偷丢掉它,会让延迟和质量都显得更好。

例 30.3:一份能逐项数清的小缓存。 给定两层模型,每层两个 KV 头,每头四个坐标,每个坐标占 2 字节。一个请求处理完八个位置,缓存占用为:

2×1×2×8×2×4×2=512 bytes.2\times1\times2\times8\times2\times4\times2=512\text{ bytes}.

各因子依次为 K 与 V 两类、请求数、层数、位置数、KV 头数、头宽度、每个值的字节数。每多处理一个位置,增加 2×2×2×4×2=642\times2\times2\times4\times2=64 字节,因此表 30.1 三行分别需要 512、576、640 字节。

若 B 个请求已处理长度都为 T,模型有 L 层、HkvH_{\mathrm{kv}} 个 KV 头,头宽 d,每个值 b 字节,则理想数据量为:

MKV=2BLTHkvdb.M_{\mathrm{KV}}=2BLTH_{\mathrm{kv}}db.

每个维度都数不同的一组值。查询头数不另外出现,第 26 章已说明 GQA 的查询头为何可以多于 KV 头。请求长度不同时,用所有请求已处理长度的总和替换 BT。实际分配块、填充、元数据和为未来保留的容量,可能让占用更大。

再给定 L = 24、Hkv=8H_{\mathrm{kv}}=8、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 缓存既不保存取消订单的权限,也不是保证政策始终正确的长期知识来源。

权重或缓存太大时,量化(quantization) 用更少的数值级别保存近似值。要理解质量与存储的取舍,先看清映射规则。

例 30.4:四个数,共用一个尺度。 给定 [−1.2,−0.2,0.7,1.8][-1.2,-0.2,0.7,1.8]。使用四位整数 −7 到 7,额外的一个有符号码不使用;尺度 s=0.5s=0.5,零点为 0。先除以 0.5,再舍入到最近整数,最后截到允许范围。恰好在中点时,取最近的偶数;这四个输入都不在中点上。

表 30.2:推导出的存储值与带符号误差

原值 xx / 0.5所存 q重建值 0.5q重建值 − 原值
−1.2−2.4−2−1.0+0.2
−0.2−0.400.0+0.2
0.71.410.5−0.2
1.83.642.0+0.2

这个对称映射写为:

q=clip⁡(round⁡(x/s),−7,7),x^=sq.q=\operatorname{clip}(\operatorname{round}(x/s),-7,7),\qquad \hat x=sq.

帽号表示从整数 q 重建的近似值。正尺度 s 决定相邻可表示值之间的距离。在未发生截断的范围内,最近舍入的误差不超过 s/2=0.25s/2=0.25。若遇异常大的 x = 8,原本要得到整数 16,却被截成 7,重建为 3.5,误差 −4.5,远超舍入误差界。那个界本来就不包含截断。

四个四位值紧凑打包后需 2 字节,再加一个 4 字节的 FP32 共享尺度,共 6 字节;原来的四个 FP32 值占 16 字节。因此,这个小块并没有实现八倍压缩。分组越大,尺度开销越容易摊薄;分组越小,可能更适合差异较大的范围,却增加元数据。一般仿射方案还使用零点,通过尺度乘整数偏移量重建实数。具体格式、分组方式,以及哪些张量被量化,都必须明确。

30.6 小误差,也可能改变一次离散选择

Section titled “30.6 小误差,也可能改变一次离散选择”

给定两个 logits,候选顺序 Paris、Lyon,分数为 [0.6,0.7][0.6,0.7]。贪心原本选 Lyon;用尺度 0.5 量化后,两者都映成 0.5,形成并列,按第 29 章顺序反而选 Paris。绝对误差不大,选中的 token 却变了;后续输入也随之变化。

因此,“四位”不是质量结论。只量化权重、量化激活、量化 KV,影响的是不同数组。反量化、打包和专用整数内核也会影响实际速度;存储位数少,不保证每项运算都更快。选择尺度的校准数据必须与最终评测分开,并在相同提示、任务标准和输出预算下比较。异常值和接近并列的决策都值得单独保留为失败例。

标量练习复现本章全部字节、时间戳和量化映射,没有声称交付量化的 MG-2 检查点或 GPU 性能实测。第 29、33 章实际运行的 MG-2 仍使用原来的 float64 完整前缀实现。本章预填充/解码 trace 是另一份独立教学场景,不能把其中 150 ms 的 TTFT 引作 Mini GPT 实测延迟。

至此,一个请求有了生命周期、逐渐增长的状态和数值表示。多个请求一起服务,还会引入另一层问题:调度可以复用上下文、重叠工作,却也可能让某个用户等得更久。下一章继续把这些差别保留下来。

预填充处理已知提示位置,提供首个输出的 logits;解码依次处理已选择的 token。KV 缓存在计算条件一致时复用各层的键和值。TTFT、TPOT 都要写明时间戳和分母。量化用表示误差换存储,舍入误差界只在未截断范围内成立,任务质量仍须测量。

  1. 请求在 10 ms 到达,分别在 110、160、220 ms 发出 token。计算 TTFT、两段间隔、TPOT 和完整请求耗时。
  2. 十个位置的提示完成预填充并选出首个输出后,缓存有几个位置?下一次前向计算处理这个输出后呢?
  3. 例 30.3 较大缓存改为四个 KV 头、四个同样长的请求,其余不变。求理想总字节数与 MiB。
  4. 按尺度 0.5 和本章并列规则量化 0.75、−0.75,再量化 5。为什么最后一个误差的成因不同?
  5. 某报告用完整请求耗时除以输出数,标成“首 token 之后的 TPOT”。应怎样修正?只有一个 token 时如何处理?
  6. 引擎把权重改成四位,但 KV 仍为每值 2 字节。哪项资源估算改变?为什么仍须重跑同一组政策更新题?
  1. TTFT 为 110−10=100110-10=100 ms,两段间隔为 50、60 ms;TPOT 为 110/2=55110/2=55 ms;完整耗时为 220−10=210220-10=210 ms。只有 TPOT 排除了最初等待。
  2. 预填充后十个位置,处理新 token 后十一个。选择本身,不是新 token 经过所有层的一次前向计算。
  3. KV 头减半,每请求从 384 MiB 降为一半,再乘四请求,共 768 MiB,即 805,306,368 字节。查询头不是额外因子。
  4. 缩放后为 1.5、−1.5,最近偶数舍入得 2、−2,重建为 1、−1。5 缩放后为 10,被截成 7,重建 3.5。它的 −1.5 误差来自超出支持范围,不只是相邻级别之间的舍入。
  5. 应以最后一次发出减去第一次发出,再除以输出数减一。只有一个 token 时,TPOT 记为缺失。若保留完整耗时平均值,应另列并明确命名。
  6. 理想权重数据量改变,还要加尺度和打包开销;给定的 KV 数据量不变。近似权重可能改变 logits 与所选 token,所以能否正确使用 750 元更新必须重新评测,不能沿用原精度的结论。