从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 40 分钟 页内实验 ×3
C13 推理:生成为什么慢、怎么变快 第 1 课

同一个模型,读 prompt 很快、逐字生成很慢,差在哪?

prefill 与 decode:生成到底慢在哪
KV cache 已经把每一步的计算省到只算 1 个 token,生成却还是慢。按算力估,锚点在 H100 上每秒能出近 2 万个 token;可每出 1 个 token 都要把 51.25 GB 权重从显存读一遍,最多 65 个,算力只用了 0.34%。屋顶线把这件事说清:读 prompt 时一次读权重能算成百上千个 token,逐字生成时只算 1 个。
❓

本课一问:同一个模型、同一张卡,读一段 prompt 很快,一个字一个字地往外吐却慢得多。KV cache 早就让每一步只算 1 个新 token 了——生成到底慢在哪?

这一课先不碰:很多请求同时解码时 KV 怎么放、怎么排(第 2 课);最后一步怎么从概率分布里挑出一个字(岔路课);怎么一个字都不改地变快(第 3 课)、怎么少读字节(第 4 课)。KV cache 的机制本身在 C3 第 8 课,这里只用它的结论。

从已有出发

贯穿例句当作一次请求:提示「小明把苹果给了」用 Qwen3.8 真 tokenizer 切成 4 个 token(小明 · 把 · 苹果 · 给了),模型要接着写出「小王」。服务端先把这 4 个 token 一次喂进去(prefill),顺手把每层的 K、V 存进缓存;之后每一步只喂上一步生成的 1 个 token(decode):

从已有出发 · 一次请求在服务端走过的路 点任一格看锚点 Qwen3.8-27B 上的真实量
decode 循环:每个 token 走一圈
旁路:

C3 第 8 课算过账:有了缓存,decode 每一步只为新 token 算一份 K、V,计算量从随长度平方增长降到线性。按说已经很省了。可 C3 那一课最后留了一个问题:既然每步只算一个 token,生成为什么还是慢?点开上面「读权重」那一格,答案已经露出了一角。

先自己设计:按算力估一下生成速度

最直接的估法:生成 1 个 token 要做多少次乘加,除以显卡每秒能做多少次。锚点每生成 1 个 token,矩阵乘大约是 2 × 25,623,939,584 次浮点运算:25,623,939,584 是参与矩阵乘的参数(C11 第 1 课的口径,去掉只查不乘的 embedding、视觉塔、MTP,这里连 RMSNorm 的增益也不算),每个权重一次乘、一次加。这就是 C6 第 3 课的「推理约 2N」,只是 C6 的 N 取文本主干 26,895,998,464,含 embedding 查表。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案按算力估:FLOPs ÷ 峰值算力
生成 1 个 token:只按算力估

🎯

试三件事:① 先按上面「只按算力」的数,想一想单个用户每秒能收到多少个字,再点「看它在哪失败」;② 在两个组件里把 H100 换成 A100:按算力估的数掉到约三分之一,按字节估的只掉到约五分之三——决定速度的是带宽;③ 换成 H200:算力和 H100 一模一样,带宽大 43%,生成就快约 43%。

改进:用屋顶线看「算」和「读」谁先到顶

任何一次计算都有两个下界:算完这些 FLOPs 至少要多久,读完这些字节至少要多久。实际耗时不会比两者中较大的那个更短 。把两个下界放在一起,就是屋顶线模型:

屋顶线:一次计算最快能多快(Williams et al. 2009)
≥ max(, ) ⇔ 比较 与
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
两边同除以字节数就能看出:I 小于 π ÷ β 时,读的时间更长——无论算力多强,时间都由带宽决定。

那 prefill 为什么快?因为它一次处理很多 token,权重读一遍,却能给所有 token 用。拿锚点 FFN 里的一个矩阵 Wup(真实形状 5,120 × 17,408)对比两种情况:

prefill:4 个 token 一起乘同一个权重矩阵(矩阵乘矩阵,GEMM)
H = X · Wup
输入参数(可学习)输出
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
T 一次喂进去的 token 数(prefill 时 = 提示长度) 4 4
d 隐藏维度 hidden_size 5120 6
f FFN 中间维度 intermediate_size 17,408 8
FLOPs = 2·T·d·f,字节 ≈ 2·d·f(权重)→ 算术强度 ≈ T。维度取自 anchor-model.json。
decode:每步只有 1 个新 token(矩阵乘向量,GEMV)
h = x · Wup
输入参数(可学习)输出
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
1 每步只处理 1 个 token 1 1
d 隐藏维度 5120 6
f FFN 中间维度 17,408 8
字节和上面一样多,FLOPs 只剩 1/T → 算术强度 ≈ 1。整个模型的每一个矩阵都是这样。

同一个矩阵、同样多的字节,prefill 读一次能算 T 行,decode 读一次只算 1 行。贯穿例句的账(H100 屋顶线上界):prefill 这 4 个 token 约 15.34 ms,decode 出「小王」这 1 个 token 约 15.39 ms——读 4 个和写 1 个几乎一样贵,因为两者都是把 51.25 GB 权重读一遍。如果提示有 2,048 个 token,prefill 约 107 ms,摊到每个 token 只有 0.052 ms,是逐字生成的约 295 分之一。

把两种情况都落到屋顶线上,自己拖一拖:

算法模拟器 · 屋顶线定位(锚点真实 config + 公开硬件规格,浏览器现算)
屋顶线:prefill 与 decode 各落在哪

🎯

试四件事:① 把 prompt 长度 T 从 4 拖到 295 再拖到 2,048:绿点沿着斜线爬到拐点就上了平顶——prefill 在 T 约 295 以上才是算力受限,贯穿例句那 4 个 token 同样被带宽卡住。② 把 decode 的 B 从 1 往上拖(上下文保持 4,096):虚线圈(只算权重)按 B 一路往右,实心蓝点却越走越慢,最后停在强度约 93 附近;右边的字节条里,紫色的 GDN 状态和橙色的 KV 越来越粗——batch 只摊得薄权重,每个请求自己的 KV 与状态摊不薄。B 超过 58 时下面出现红字:80 GB 已经放不下了。③ 切到 Qwen3-32B:它没有固定状态,但每 token 的 KV 是锚点的 4 倍,把上下文拖到 32K 看谁的字节条先被 KV 占满。④ 把紫色的小点(decode 注意力)和 C3 第 9 课对上:它的强度恰好等于 GQA 的组大小 6——每读一份 K、V,有 6 个 Q 头来用。

🔍 锚点的 48 层 GDN:固定大小的状态,读起来反而比 KV 还多?

C11 第 4 课讲过,锚点 64 层里只有 16 层存 KV(每 token 65,536 字节),另外 48 层 GDN 每个请求只存一份大小固定的状态:151 MB(= 144 MiB,C11 的写法;按 FP32)。它不随上下文变长,可 decode 每一步都要把整块状态读出来、更新、再写回去,也就是约 302 MB 的流量。要等上下文长到约 4,608 个 token,16 层 KV 的读取量才追上它(C11 第 4 课的 768 比的是存储、对象是「这 48 层若也做全注意力」的 KV;这里比的是每步读写流量、对象是实际存 KV 的 16 层)。所以对锚点来说,短对话的每步字节账里,「状态」比「KV」更重;K3 报告谈投机解码时也专门说过,状态流量在大 batch 的线上服务里占主导 。口径:状态按 FP32 计,是 R2 调研与 frontier-models.json 的口径(同系列 2.4T 模型的 config 写明 linear_state_dtype 为 float32);若服务框架用 BF16 存,流量减半,组件里可以切换。

🔍 DeepSeek-V3 为什么把 prefill 和 decode 放在不同的机器上?

DeepSeek-V3 报告 §3.4 写了它的线上部署:prefill 阶段的最小部署单元是 4 个节点 32 张卡,decode 阶段是 40 个节点 320 张卡,两者用不同的并行方式;报告明说解码阶段每个专家的 batch 通常在 256 个 token 以内,「瓶颈是访存而非计算」 。一个算力受限、一个带宽受限,放在一起就得互相将就。这件事在第 5 课(P/D 分离)还会回来。

⚠ 常见误解

⚠

「生成慢,是因为模型算得多。」KV cache 早就让每一步只为 1 个新 token 算了;锚点这一步的矩阵乘只要 0.052 ms。慢在读:51.25 GB 权重每一步都要读一遍。

⚠

「换一张算力更强的卡,生成就等比例变快。」batch 1 的生成看带宽。A100 SXM → H100 SXM,BF16 稠密算力从 312 到约 989.5 TFLOPS(× 3.17),带宽从 2.039 到 3.35 TB/s(× 1.64):按屋顶线,锚点每个 token 从约 25.1 ms 变成 15.3 ms,只快 1.64 倍。

⚠

「prefill 一定是算力受限的。」只有 prompt 足够长(H100 上约 295 个 token 以上)才是。贯穿例句只有 4 个 token,强度约 4,照样被带宽卡住——所以它和生成 1 个 token 几乎一样贵。

⚠

「把 batch 开得足够大,就能把算力吃满。」权重可以被整批共用,每个请求自己的 KV 与状态不行:锚点上下文 4,096 时每个请求每步要读写约 570 MB,batch 再大强度也只逼近约 93,到不了 295;何况 80 GB 只放得下 58 个这样的请求。

在演进地图上的位置

推理加速的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 每一步把整段重新喂进模型
  2. KV cache
  3. 批处理:一次解码多个请求
  4. 连续批处理(迭代级调度)
  5. PagedAttention:KV 分块按需分配
  6. 投机解码:草稿猜、目标一次验
  7. 让模型自己当草稿:MTP 与专用草稿器
  8. 量化:每个权重少读几位
  9. 按 TTFT / TPOT / goodput 调度:分块 prefill、P/D 分离
  10. 采样:温度 → top-k → top-p → min-p
  11. 少存 KV:GQA / MLA / 线性注意力
第一代KV cache自回归 Transformer 的标准做法(C3 第 8 课)你在这里
解决了旧 token 的 K、V 存下来,同样 1,000 个 token 只算 1,003 次;锚点每个 token 存 64 KiB。
卡在每生成 1 个 token 仍要把 51.25 GB 权重读一遍:H100 上最多 65 token/s,算力只用了 0.34%(第 1 课,屋顶线上界)。
数字口径:锚点 Qwen3.8-27B 的字节与时间是 H100 SXM 上的屋顶线上界(不是实测),由 anchor-model.json 现算;调度、投机、量化、采样的数字来自我们项目的 3 个小模型(章内 precomputed.json,脚本 gen_c13_precomputed.py,2026-09-26);vLLM、DeepSeek-V3 的数字出自本地 PDF,Orca、DistServe 出自论文原文,均为提出方自报。

证据三问

问本课的回答
① 它解决什么问题?说清「生成为什么慢」:同样的 FLOPs,prefill 一次读权重算 T 个 token,decode 只算 1 个;decode 的速度由带宽决定,不由算力决定
② 数学上为什么?屋顶线不等式与算术强度是记账 ;GEMV 的算术强度 ≈ 1、GEMM ≈ T 是矩阵乘的计数
③ 实验上呢?大模型的解码受带宽限制是业界反复报告的现象:Leviathan 等写明「推理常常不是被算力卡住,而是被显存带宽卡住」,DeepSeek-V3 的部署说明写明解码阶段瓶颈在访存 。我们 3.7M 的小模型在 MPS 上不符合这个规律(实验课),因为它小到每步的耗时主要是调度开销

一句话

📌

生成慢不是因为算得多,而是每个 token 都要把整个模型从显存读一遍;读 prompt 时一次读权重能算成百上千个 token,逐字生成时只算 1 个。

🚦

先停在这里:记住「decode 看带宽、prefill 看算力,拐点约 295」,就可以去第 2 课。
再深一层:展开上面两个 Dig(GDN 的状态流量、DeepSeek-V3 的分开部署),再读 How to Scale Your Model 的 Roofline 一章。

下一个缺口

batch 能把权重的读取摊薄:同样读一遍 51.25 GB,58 个请求各出 1 个 token,总吞吐就从约 65 个 token/s 涨到约 2,300 个。可是每个请求都要自己的 KV 与状态,长短不一、随时到来、随时结束。成百上千个请求的 KV cache 在显存里怎么放、怎么排队? → 第 2 课:成百上千个请求一起解码,KV cache 在显存里怎么放、怎么排?

解码循环还有最后一格没讲:算出 logits 之后,怎么挑出一个字?→ 岔路课:温度、top-k、top-p、min-p 各截掉了什么?

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C13-L1.md。下面是最常被问到的几个: