同一个模型,读 prompt 很快、逐字生成很慢,差在哪?
本课一问:同一个模型、同一张卡,读一段 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):
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 查表。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
两个下界取大的那个。按算力的那个下界只有 0.052 ms,按字节的那个是 15.30 ms,差了约 295 倍。生成的时间几乎全花在「把权重从显存搬到计算单元」上。
试三件事:① 先按上面「只按算力」的数,想一想单个用户每秒能收到多少个字,再点「看它在哪失败」;② 在两个组件里把 H100 换成 A100:按算力估的数掉到约三分之一,按字节估的只掉到约五分之三——决定速度的是带宽;③ 换成 H200:算力和 H100 一模一样,带宽大 43%,生成就快约 43%。
改进:用屋顶线看「算」和「读」谁先到顶
任何一次计算都有两个下界:算完这些 FLOPs 至少要多久,读完这些字节至少要多久。实际耗时不会比两者中较大的那个更短 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条前提:峰值算力与峰值带宽是硬上限,每个字节至少从显存读一次。它给的是上界(最快能多快),实际还要加上内核启动、调度等开销。 出处Williams, Waterman & Patterson 2009(CACM,需联网) ↗需联网 。把两个下界放在一起,就是屋顶线模型:
那 prefill 为什么快?因为它一次处理很多 token,权重读一遍,却能给所有 token 用。拿锚点 FFN 里的一个矩阵 Wup(真实形状 5,120 × 17,408)对比两种情况:
| 维度 | 含义 | 真实规模 | 小规模 |
|---|---|---|---|
| T | 一次喂进去的 token 数(prefill 时 = 提示长度) | 4 | 4 |
| d | 隐藏维度 hidden_size | 5120 | 6 |
| f | FFN 中间维度 intermediate_size | 17,408 | 8 |
| 维度 | 含义 | 真实规模 | 小规模 |
|---|---|---|---|
| 1 | 每步只处理 1 个 token | 1 | 1 |
| d | 隐藏维度 | 5120 | 6 |
| f | FFN 中间维度 | 17,408 | 8 |
同一个矩阵、同样多的字节,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 分之一。
把两种情况都落到屋顶线上,自己拖一拖:
试四件事:① 把 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 的线上服务里占主导 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方自报;K3 用的是 KDA,与 Qwen3.8 的 GDN 同属带固定状态的线性注意力。 出处Kimi K3 报告 §5.4.2(本地 PDF) ↗ 。
🔍 DeepSeek-V3 为什么把 prefill 和 decode 放在不同的机器上?
DeepSeek-V3 报告 §3.4 写了它的线上部署:prefill 阶段的最小部署单元是 4 个节点 32 张卡,decode 阶段是 40 个节点 320 张卡,两者用不同的并行方式;报告明说解码阶段每个专家的 batch 通常在 256 个 token 以内,「瓶颈是访存而非计算」 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条官方自述的部署经验,不是对照实验。 出处DeepSeek-V3 报告 §3.4.2(本地 PDF) ↗ 。一个算力受限、一个带宽受限,放在一起就得互相将就。这件事在第 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 个这样的请求。
在演进地图上的位置
- 每一步把整段重新喂进模型
- KV cache
- 批处理:一次解码多个请求
- 连续批处理(迭代级调度)
- PagedAttention:KV 分块按需分配
- 投机解码:草稿猜、目标一次验
- 让模型自己当草稿:MTP 与专用草稿器
- 量化:每个权重少读几位
- 按 TTFT / TPOT / goodput 调度:分块 prefill、P/D 分离
- 采样:温度 → top-k → top-p → min-p
- 少存 KV:GQA / MLA / 线性注意力
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 说清「生成为什么慢」:同样的 FLOPs,prefill 一次读权重算 T 个 token,decode 只算 1 个;decode 的速度由带宽决定,不由算力决定 |
| ② 数学上为什么? | 屋顶线不等式与算术强度是记账 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条给定峰值算力与带宽,时间下界由两者中较大者决定;锚点的 FLOPs 与字节由 config 现算。 出处Williams et al. 2009(需联网) ↗需联网 ;GEMV 的算术强度 ≈ 1、GEMM ≈ T 是矩阵乘的计数 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条2·T·d·f 次运算 ÷ 2·d·f 字节。 出处待补 |
| ③ 实验上呢? | 大模型的解码受带宽限制是业界反复报告的现象:Leviathan 等写明「推理常常不是被算力卡住,而是被显存带宽卡住」,DeepSeek-V3 的部署说明写明解码阶段瓶颈在访存 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条属于提出方的工程经验,不是本课复现的实验;本课的毫秒数是屋顶线上界,不是实测。 出处DeepSeek-V3 §3.4.2;Leviathan 2022 §1(本地 PDF) ↗ 。我们 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。下面是最常被问到的几个:
- 「那 MoE 模型呢?它每个 token 只用一小部分参数。」 batch 1 时只要读被选中的专家,字节确实少;可 batch 一大,不同 token 选中不同专家,要读的专家越来越多。DeepSeek-V3 解码时每张卡只放 1 个专家,正是为了让每个专家攒够 token(报告 §3.4.2)。MoE 的账在 C11 第 1 课。
- 「我们自己的小模型测出来,有 cache 只比没 cache 快 1.2 倍左右,和这里说的对不上?」 对不上才对:3.7M 参数的模型整份权重只有约 15 MB,每一步的耗时主要是 GPU 调度的固定开销,屋顶线在这个规模不成立;同一个模型放到 CPU 上,cache 就快 2.9–5.8 倍。数据和解释见实验课。
- 「为什么不把 prefill 也一个 token 一个 token 地做,省得峰值那么高?」 那等于把 T 次读权重替换掉 1 次,prefill 会慢 T 倍。反过来,生成能不能也「一次算好几个」?第 3 课的投机解码就是这个思路。