对应 Maps to:原课没有对应的一次课。第 1 讲直接给出了”每 token 约 2N 次运算""KV 字节 = 2 × 层数 × KV 头数 × 头维度 × 字节”这些结论,这篇前置讲把它们逐项推导出来,并补上推理引擎里除了 forward 以外的部分:采样、停止条件、分词与流式输出。 依据说明:模型结构来自公开的 config.json 和技术报告(Meta Llama 3 / 3.1 / 3.2、Mistral-7B、Qwen2.5、Mixtral、DeepSeek-V2 / V3)。注意力变体依据 N. Shazeer, Fast Transformer Decoding: One Write-Head is All You Need(2019,MQA)、J. Ainslie et al., GQA(EMNLP 2023)、DeepSeek-AI, DeepSeek-V2(2024,MLA)。FLOPs 的写法参照 J. Kaplan et al., Scaling Laws for Neural Language Models(2020)。采样依据 A. Fan et al.(2018,top-k)、A. Holtzman et al., The Curious Case of Neural Text Degeneration(ICLR 2020,top-p)、M. Nguyen et al.(2024,min-p)、N. Keskar et al.(2019,CTRL 的重复惩罚),以及 FlashInfer 团队 2025 年的博客 Sorting-Free GPU Kernels for LLM Sampling。分词依据 R. Sennrich et al.(ACL 2016,BPE)和 GPT-2 的字节级 BPE。增量反分词的写法参照 vLLM 和 HF TGI 的开源实现。 配套代码 Code:
code/prereq2/(numpy + 标准库;6 个脚本 + 2 个共享模块;全部 1 秒左右跑完) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一篇,你应该能够:
- 画出一次 decoder-only Transformer forward 的数据流,说出每一步的张量形状。
- 只看一个模型的 config.json,就能算出它的参数量(包括共享嵌入、QKV 偏置、MoE 的总参数和激活参数),并和官方数字对上。
- 推导每个 token、每次 prefill 的 FLOPs,说清注意力项什么时候变得重要,“2N”里的 N 应该用哪个数。
- 推导每个 token 的 KV cache 字节数,解释 MHA、MQA、GQA、MLA 各省了什么、代价是什么;能复述 MLA 的”吸收”技巧为什么成立。
- 做显存预算:权重放不放得下、KV 能放多少 token、最多能同时服务多少个请求;算出 TTFT、TPOT 的硬件下界和每百万 token 的成本下界。
- 说出温度、top-k、top-p、min-p、重复惩罚、束搜索各自做什么,以及它们在推理系统里的代价;解释为什么 top-p 可以不排序。
- 解释流式输出时为什么不能逐个 token 解码,停止字符串被拆开时怎样处理。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲不展开 Out of scope |
|---|---|
| 第 1 讲 §2–§3:prefill / decode、KV cache、roofline | 模型怎样训练、为什么这样设计(只讲推理时的账) |
| 前置讲 P1(建议):算术强度、Tensor Core、数值格式 | 注意力内核的实现(第 4 讲) |
| Transformer 大概是什么:注意力、MLP、残差 | MoE 的路由和部署(第 8 讲);量化方法(附录讲 A) |
| 概率的基本概念:分布、期望、对数 | 投机解码的采样(第 5 讲,本讲给出它需要的采样基础) |
0.3 本讲地图 Map
§1 为什么要会算账
§2 一次 forward 经过什么:逐层的数据流与张量形状
§3 参数量:逐项推导;共享嵌入、偏置、MoE 的总参数与激活参数 ← 代码 01
§4 FLOPs:每个 token;prefill 的平方项;"2N"里的 N ← 代码 01、02
§5 KV cache:每 token 的字节;MHA / MQA / GQA / MLA;MLA 的吸收技巧 ← 代码 03
§6 显存预算、延迟下界与成本计算器 ← 代码 02、03、06
§7 采样:温度、top-k、top-p、min-p、重复惩罚、束搜索;不排序的 top-p;可复现性 ← 代码 04
§8 停止条件、分词与流式输出 ← 代码 05
§9 与推理系统的联系(只想快速浏览的读者读这一节)
§10–14 小结、常见误区、练习、延伸阅读、下一篇
0.4 记号 Notation
沿用第 1 讲: 参数量, 提示长度, 批大小, 隐藏维度, 层数, query 头数, KV 头数, 每头维度, 峰值算力, 显存带宽。本篇新增:
| 符号 | 含义 | Llama-3-8B |
|---|---|---|
| 词表大小 | 128,256 | |
| MLP(SwiGLU)的中间维度 | 14,336 | |
| GQA 的组大小:几个 query 头共用一个 KV 头 | 4 | |
| 一个 token 能看到的上下文长度(含它自己) | — | |
| 每个数的字节数(BF16 = 2,FP8 = 1) | 2 | |
| 、 | MLA 的潜向量维度、共用的 RoPE key 维度 | (DeepSeek-V3:512、64) |
| 采样温度 | — |
0.5 关于数字 · About the numbers
本篇的数字都是算出来的,不是实测:参数量、FLOPs、KV 字节是从模型结构按公式算的(并且和官方公布的参数量、第 1 讲 tiny_llm 里真的数出来的运算次数核对过);时间和成本是”硬件下界”,用的是第 1 讲的纸上 GPU,多卡按理想张量并行、忽略通信。采样和分词的演示是真的跑出来的,但用的是合成的分布和一小段原创语料。
1. 为什么要会算账 · Why learn the arithmetic
推理系统里几乎每一个决策,背后都是一道算术题:
- 这个模型要几块卡?——先算权重多少字节、KV 每 token 多少字节。
- 用户最快能多快拿到第一个字、之后每个字多快?——算 prefill 的 FLOPs、decode 每步要读的字节。
- 一块卡最多能同时服务多少人?每百万 token 成本多少?——算显存还剩多少给 KV、满批时每步多久。
- 为什么新模型都用 GQA、DeepSeek 要发明 MLA?——算 KV cache 占多少显存、decode 时读它要多久。
好消息是:这些数全部能从一个模型的 config.json(层数、隐藏维度、头数、中间维度、词表大小)和 GPU 的规格表算出来,不需要跑任何模型。本篇就是这本账的推导。我们会发现,第 1 讲的几个经验公式都只是这本账的”主项”;不同的结构(共享嵌入、MoE、MLA)会让主项变形,长上下文会让”次要项”变成主项。
账本之外,推理引擎每一步还要做几件和 forward 无关、却同样会出错、会花时间的事:从 logits 里采样出下一个 token,检查停止条件,把 token 变回文字流式地发给用户。§7、§8 讲这些。
2. 一次 forward 经过什么 · What a forward pass does
现在的大语言模型几乎都是 decoder-only 的 Transformer(Llama、Qwen、Mistral、DeepSeek……)。一次 forward 处理一段新 token(prefill 时是整个提示,decode 时是 1 个),设这一步有 个新 token:
token id(t 个整数)
│ embedding:查表,取词嵌入表的 t 行 → (t, d)
▼
┌─────────────────── 重复 n_layers 层 ───────────────────┐
│ h = RMSNorm(x) │
│ q = h·W_q k = h·W_k v = h·W_v │ q: (t, n_h·d_h) k, v: (t, n_kv·d_h)
│ q, k ← RoPE(q, k, 位置) │ 按位置旋转,让点积只依赖相对位置
│ 把 k、v 写进 KV cache;取出全部 S 个 token 的 K、V │
│ o = softmax(q·Kᵀ/√d_h + 因果掩码)·V │ 每个 query 头和它那一组的 KV 头做注意力
│ x = x + o·W_o │ 残差连接 (t, d)
│ h = RMSNorm(x) │
│ x = x + (SiLU(h·W_gate) ⊙ h·W_up)·W_down │ SwiGLU MLP,中间维度 d_ff
└─────────────────────────────────────────────────────────┘
│ RMSNorm;只取需要 logits 的位置(生成时是最后一个)
▼
logits = h·W_lm_head → (1, V)
│ 惩罚、温度、截断、采样(§7)
▼
下一个 token id
几个组件的作用(只说推理时需要知道的):
- RMSNorm:把每个 token 的向量除以它的均方根,再乘一个可学习的缩放向量。几乎没有参数,但是一个逐元素 + 归约的算子(P1 §5:memory-bound,要和残差相加融合)。
- RoPE(旋转位置编码):把 q、k 的每一对维度按”位置 × 频率”的角度旋转。两个旋转后的向量做点积,结果只依赖它们的相对位置。它没有参数,但有两个推理上的后果:KV cache 里存的是旋转后的 k(所以前缀缓存要求位置也相同,第 3 讲);它会破坏 MLA 的”吸收”技巧(§5.4)。
- 注意力:新 token 的 q 和 cache 里全部 个 token 的 k 做点积,softmax 之后对 v 加权求和。GQA 时几个 query 头共用一个 KV 头(§5)。
- SwiGLU MLP:两个”上投影”(gate 和 up),gate 过 SiLU 后和 up 逐元素相乘,再”下投影”回 维。三个矩阵,所以参数是 。
- lm_head:把最后的隐藏向量投影到词表上,得到 个 logits。有的模型让它和词嵌入表共享同一个矩阵(tied embeddings)。
第 1 讲的 tiny_llm.py 就是按这张图实现的(只是位置编码用了可学习的表,而不是 RoPE),可以对照着读它的 forward。
3. 参数量 · Parameter count
3.1 逐项推导
所有参数都在矩阵里(RMSNorm 的缩放向量可以忽略)。按上图逐项数:
| 部分 | 参数量 | Llama-3-8B |
|---|---|---|
| 词嵌入表 | 5.25 亿 | |
| 每层注意力 | 4,194 万 | |
| 每层 MLP | 1.76 亿 | |
| lm_head | (共享时为 0) | 5.25 亿 |
| RMSNorm | 27 万 |
代码 01 演示 1 把 Llama-3-8B 逐个矩阵列出来:每层 2.18 亿(注意力 0.42 亿 + MLP 1.76 亿),32 层共 69.8 亿,加上嵌入表和 lm_head 各 5.25 亿,一共 8,030,261,248 个——和官方的 8.03 B 完全一致。同一套公式数第 1 讲 tiny_llm 的参数(它多一张可学习的位置编码表),得到 122,688 个,和它实际数组的元素个数一样。
几个值得注意的比例:MLP 占了约 70%( 是 的 3.5 倍,又有三个矩阵);注意力只占约 17%,因为 GQA 让 、 只有 的 1/4;嵌入表和 lm_head 合起来占 13%。
3.2 公开模型的核对
代码 01 演示 2 用同一套公式算了 9 个公开模型,和官方数字的误差都在 0.5% 以内:
| 模型 | 注意力 | 账本 | 官方 | 嵌入 + 头 | 注意力 | MLP / 专家 |
|---|---|---|---|---|---|---|
| Llama-3.2-1B(共享嵌入) | GQA | 1.236 B | 1.23 B | 21.3% | 13.6% | 65.2% |
| Mistral-7B | GQA | 7.242 B | 7.24 B | 3.6% | 18.5% | 77.8% |
| Qwen2.5-7B(QKV 偏置) | GQA | 7.616 B | 7.61 B | 14.3% | 10.8% | 74.9% |
| Llama-3-8B | GQA | 8.030 B | 8.03 B | 13.1% | 16.7% | 70.2% |
| Llama-3-70B | GQA | 70.554 B | 70.6 B | 3.0% | 17.1% | 79.9% |
| Llama-3.1-405B | GQA | 405.853 B | 405 B | 1.0% | 17.7% | 81.3% |
| Mixtral-8x7B | GQA + MoE | 46.703 B | 46.7 B | 0.6% | 2.9% | 96.6% |
| DeepSeek-V3 | MLA + MoE | 671.026 B | 671 B | 0.3% | 1.7% | 98.0% |
- 小模型里嵌入表很显眼:词表大小(12.8 万)和模型大小无关,Llama-3.2-1B 即使共享了嵌入表,也有 21% 的参数在里面;405B 只有 1%。Mistral-7B 和 Llama-3-8B 的层结构一模一样,参数差的 8 亿几乎全在词表上(3.2 万 vs 12.8 万)。
- 共享嵌入的模型,词嵌入表和 lm_head 是同一个矩阵,只算一次;Qwen2.5 的 Q、K、V 带偏置,多出几千个参数——账本要照顾到这些细节才对得上。
3.3 MoE:总参数与激活参数
MoE(混合专家,第 8 讲)把每层的一个 MLP 换成很多个”专家”MLP,每个 token 只经过其中 top-k 个。所以有两个参数量:
- 总参数:所有专家都要常驻显存。显存看总参数。
- 激活参数:每个 token 实际用到的参数(注意力 + top-k 个专家 + 共享专家 + 嵌入)。计算看激活参数。
代码 01 演示 3:Mixtral-8x7B 总参数 46.7 B、激活 12.9 B(27.6%);DeepSeek-V3 总参数 671.0 B、激活约 37.6 B(官方 37 B,5.6%)。DeepSeek-V3 的 671 B 里 97.4% 是路由专家(58 层 × 256 个专家),MLA 注意力只有 11.4 B。用 FP8 存它要 671 GB,8 块 80 GB 的 H100 都放不下(§6.1);但每个 token 的运算量只相当于一个 37 B 的稠密模型。
4. FLOPs · How much computation
4.1 矩阵乘法的运算量
一个 矩阵乘 矩阵:每个输出元素做 次乘法和 次加法,一共 次运算。所以一个 token()经过一个 的权重矩阵,就是 次运算——每个参数一次乘、一次加。把一层里所有矩阵加起来,一个 token 的线性部分就是 2 × 参数量。这就是”每 token 约 2N”的来历。
4.2 每个 token 的 FLOPs
decode 时生成一个 token(它能看到 个 token,含自己)的完整账:
注意力项的来历:每个 query 头和 个 key 各做一次 维的点积( 次运算),再把 个 value 加权求和(又是 次)。它只和 query 头数有关,和 KV 头数无关:GQA 省的是 KV 的存储和读取,不省运算。Kaplan 等人的缩放定律论文给出的前向运算量是 :注意力项只有一行,相当于只数了 ;这里把乘 V 的那一半也算上,所以系数是 4。
代码 02 演示 1:注意力项随上下文线性增长,什么时候追上其余全部?
| 模型 | “2N”项(含 lm_head) | 注意力 @ 8K | 注意力 @ 128K | 持平的上下文 |
|---|---|---|---|---|
| Llama-3.2-1B | 2.5 G | 1.1 G(30%) | 17.2 G(87%) | 18.9K |
| Llama-3-8B | 15.0 G | 4.3 G(22%) | 68.7 G(82%) | 28.6K |
| Llama-3-70B | 139.0 G | 21.5 G(13%) | 343.6 G(71%) | 53.0K |
| Llama-3.1-405B | 807.5 G | 67.6 G(8%) | 1.1 T(57%) | 97.8K |
上下文几千时注意力只是零头;到了几万、十几万,它就成了主项。不过别忘了 decode 是 memory-bound 的:注意力真正的代价是读 KV,而不是这些运算(第 1 讲、P1 §5.4)。
4.3 prefill:注意力的平方项
prefill 一个长度为 的提示,第 个位置能看到 个 token,所以注意力的总运算量是 ——随提示长度平方增长:
(只为最后一个位置算 logits。)代码 02 演示 2(Llama-3-8B):
| 提示长度 | 总 FLOPs | 注意力占比 | 平均每 token ÷ 2N |
|---|---|---|---|
| 2,048 | 29.7 T | 3.7% | 1.04 |
| 8,192 | 131.9 T | 13.3% | 1.15 |
| 32,768 | 738.9 T | 38.1% | 1.62 |
| 131,072 | 6.3 P | 71.1% | 3.46 |
128K 的提示,平均每个 token 的运算量是”2N”的 3.5 倍。这就是长上下文的 prefill 越来越贵、第 10 讲的稀疏注意力要专门降低这一项的原因。
这个公式和第 1 讲 tiny_llm 里用 OpCounter 真的数出来的运算次数逐个相等(提示长度 16、64、200)。tiny_llm 的 只有 64,“2N”项很小,200 个 token 时注意力就占了 20%(Llama-3-8B 到 2,048 个 token 也只有 3.7%):模型越小,注意力的相对代价越大。
4.4 “2N”里的 N 到底是哪个数
代码 01 演示 4 比较”2 × 总参数”和实际的 FLOPs(上下文 1,不含注意力项):
| 模型 | 2 × 总参数 | 2 × 激活参数 | 实际 FLOPs | 实际 ÷ 2N_总 |
|---|---|---|---|---|
| Llama-3.2-1B(共享嵌入) | 2.5 G | 2.5 G | 2.5 G | 1.00 |
| Llama-3-8B | 16.1 G | 16.1 G | 15.0 G | 0.93 |
| Mixtral-8x7B | 93.4 G | 25.8 G | 25.5 G | 0.27 |
| DeepSeek-V3 | 1.3 T | 75.1 G | 73.3 G | 0.055 |
- 稠密模型的”2N”略高估:词嵌入表只是查表(取一行),不做矩阵乘法。共享嵌入的 1B 模型正好 1.00:那一张表在 lm_head 里恰好做了一次矩阵乘法。
- MoE 用总参数就完全错了。准确的说法是:每 token 的 FLOPs ≈ 2 × 参与矩阵乘法的激活参数 + 注意力项。
顺带一提:训练时每个 token 的运算量约为 (前向 ,反向约 )。推理只有前向,所以是 。
5. KV cache · The KV cache
5.1 每个 token 存什么、存多大
KV cache 存的是每个历史 token 在每一层算出的 key 和 value(RoPE 之后的),这样新 token 做注意力时不用重算它们(第 1 讲 §2)。每个 token、所有层加起来:
(2 是 K 和 V 各一份。)Llama-3-8B: 字节 = 128 KiB。代码 03 演示 1:
| 模型 | 种类 | KV 头 / query 头 | 每 token KV | 假如是 MHA | 节省 | 32K 对话 |
|---|---|---|---|---|---|---|
| Llama-3.2-1B | GQA | 8 / 32 | 32 KiB | 128 KiB | 4× | 1.07 GB |
| Qwen2.5-7B | GQA | 4 / 28 | 56 KiB | 392 KiB | 7× | 1.88 GB |
| Llama-3-8B | GQA | 8 / 32 | 128 KiB | 512 KiB | 4× | 4.29 GB |
| Llama-3-70B | GQA | 8 / 64 | 320 KiB | 2.5 MiB | 8× | 10.74 GB |
| Llama-3.1-405B | GQA | 8 / 128 | 504 KiB | 7.9 MiB | 16× | 16.91 GB |
| DeepSeek-V3 | MLA | 1 个潜向量 | 68.6 KiB | 4.8 MiB | 71× | 2.30 GB |
一段 32K token 的对话,Llama-3-8B 要 4.3 GB 的 KV——和模型权重(16 GB)是同一个量级。KV cache 是推理系统的头号资源(第 3、9 讲),所以模型设计者想方设法缩小它。
5.2 MHA、MQA、GQA
- MHA(multi-head attention,原始的 Transformer):每个 query 头有自己的 K、V 头,。
- MQA(multi-query attention,Shazeer 2019):所有 query 头共用一个 K、V 头,。Shazeer 的出发点正是推理:decode 受限于读 K、V 的带宽,KV 小 倍,读得就快 倍。代价是质量有所下降、训练也不太稳定。
- GQA(grouped-query attention,Ainslie 等,2023):折中——把 query 头分成 组,每组共用一个 KV 头。Llama 3 系列都是 8 个 KV 头。
GQA 不是近似。代码 03 演示 2 在 tiny_llm 上验证:一个 GQA 模型(4 个 query 头、2 个 KV 头),和”把每个 KV 头复制 2 份”得到的 MHA 模型,生成的 40 个 token 完全相同(logits 的差只有 ,是 float32 的舍入),但 MHA 的 KV cache 大一倍。GQA 只是”不把重复的 K、V 存两遍”。
但一个按 MHA 训练好的模型,不能直接改成 GQA。演示 2 的第二部分把一个 MHA 模型的 KV 头在组内取平均(这是 GQA 论文的初始化方法),不再训练:GQA 只剩 9/40 个 token 和原模型相同,MQA 只剩 3/40。(tiny_llm 是随机权重,这个数字会夸大真实模型上的损失,只说明”改结构会改变输出”。)GQA 论文的做法是先这样初始化,再用原预训练约 5% 的算力继续训练(uptraining);现在的模型大多从一开始就按 GQA 训练。
GQA 对推理还有一个影响:decode 注意力的算术强度约等于组大小 (每读一个 K、V,被 个 query 头各用一次;第 4 讲 §4.1)。Llama-3-8B 是 4,Llama-3-70B 是 8,都远低于屋脊点。
5.3 MLA:只缓存一个潜向量
DeepSeek-V2 提出的 MLA(multi-head latent attention)走得更远:每个 token 每层只缓存一个 维的潜向量 (DeepSeek-V3:),用到时再用”解压”矩阵得到每个头的 key 和 value:
128 个头、每个头的 key 192 维、value 128 维,如果直接存要 个数;MLA 只存 512 + 64 = 576 个,小 71 倍(那 64 个数是什么,见 §5.4)。
问题是:如果每一步都要把 个历史 token 的潜向量解压成 、,运算量就爆炸了。吸收(absorb)技巧解决这个问题。第 个头的注意力分数:
输出:
先把 query 变到 维的潜空间,直接和缓存的潜向量做点积;先在潜空间里加权求和,最后再乘一次 (它还能并进输出投影 )。这只是矩阵乘法的结合顺序不同(),结果完全相同。代码 03 演示 3 用 float64 验证:两种算法的输出最大差 。对 DeepSeek-V3(上下文 4,096、decode 一步、一层),朴素算法光解压就要 137 GFLOP,吸收后只剩注意力本身的约 1.1 GFLOP。
吸收后,MLA 的 decode 就像一个”所有 128 个头共用一份 576 维 KV”的 MQA:每读一个字节的 KV,被 128 个头各用一次。演示 3 算了 decode 注意力的算术强度:
| 模型 | decode 注意力的强度(FLOP / KV 字节) |
|---|---|
| Llama-3-8B(GQA,) | 4 |
| Llama-3-70B(GQA,) | 8 |
| DeepSeek-V3(MLA) | 242 |
MLA 让 decode 注意力第一次接近 compute-bound(H100 的屋脊点 295)。代价是:只有一份潜向量,张量并行没法按头切开它,每块卡都得存完整的 KV(第 8 讲 §5)。
5.4 RoPE 为什么破坏吸收
RoPE 要把 key 乘一个依赖它自己位置 的旋转矩阵 :。中间的 随每个历史 token 变化,没法和 合成一个固定的矩阵,吸收就失效了。DeepSeek 的办法是”解耦的 RoPE”:内容部分(不旋转)用潜向量 + 吸收;另外为每个 token 缓存一个所有头共用的 64 维 key,只有它做 RoPE。这就是每层 512 + 64 个数里那 64 个的来历。
6. 显存预算、延迟下界与成本 · Memory, latency bounds, and cost
6.1 显存:权重 + KV + 其他
一块卡的显存要放:
- 权重:总参数 × 每个数的字节(多卡张量并行时平均切开);
- KV cache:剩下的大部分空间,决定能同时服务多少 token;
- 其他:激活(每一步的中间结果,用完就释放)、logits、CUDA Graph 和通信的缓冲区、碎片。推理引擎通常只让自己用显存的约 90%(vLLM 的默认值)。
代码 03 演示 4(显存用 90%,理想张量并行;KV 按头切开,KV 头数少于卡数时复制;MLA 的潜向量每块卡存一份):
| 部署 | 每卡权重 | KV 容量 | 4K 上下文 | 32K | 128K |
|---|---|---|---|---|---|
| Llama-3-8B · BF16 · 1×H100 | 16.06 GB | 426,784 token | 104 个请求 | 13 | 3 |
| Llama-3-70B · BF16 · 8×H100 | 17.64 GB | 1,327,186 | 324 | 40 | 10 |
| Llama-3.1-405B · FP8 · 8×H100 | 50.73 GB | 659,360 | 160 | 20 | 5 |
| DeepSeek-V3 · FP8 · 8×H100 | 83.88 GB | 放不下权重 | — | — | — |
| DeepSeek-V3 · FP8 · 8×H200 | 83.88 GB | 1,224,433 | 298 | 37 | 9 |
- 上下文越长,能同时服务的请求越少:Llama-3-8B 单卡在 4K 上下文时能放 104 个请求的 KV,128K 时只剩 3 个。显存是批大小的天花板(第 1、3 讲)。
- “要几块卡”首先是一道显存题:8 块 H100 一共只有 576 GB 可用,放不下 671 GB 的 FP8 DeepSeek-V3;换成每块 141 GB 的 H200 才行。
- logits 是隐藏的显存炸弹:logits 通常用 FP32 存,词表 128,256 时每个位置 512 KB。decode 批 256 只要最后一个位置,共 131 MB;但如果要返回一个 8,192 token 的提示里每个位置的对数概率(评测、打分常用),就是 4.2 GB。推理引擎通常分块计算或限制这类请求。
6.2 延迟的下界
不管引擎写得多好,都快不过硬件:
代码 02 演示 3(提示 2,048;decode 上下文 2,048;batch 1;100% 峰值、忽略通信):
| 模型 · 精度 · 硬件 | TTFT ≥ | TPOT ≥ | 单用户 token/s ≤ |
|---|---|---|---|
| Llama-3.2-1B · BF16 · 1×H100 | 4.31 ms | 0.76 ms | 1,320 |
| Llama-3-8B · BF16 · 1×H100 | 30.02 ms | 4.87 ms | 205 |
| Llama-3-8B · FP8 · 1×H100 | 15.01 ms | 2.44 ms | 410 |
| Llama-3-70B · BF16 · 4×H100 | 72.26 ms | 10.58 ms | 95 |
| Llama-3.1-405B · FP8 · 8×H100 | 105.06 ms | 15.16 ms | 66 |
| DeepSeek-V3 · FP8 · 8×H200(只读被选中的专家) | 11.49 ms | 0.98 ms | 1,021 |
一个 8B 的 BF16 模型在 H100 上,每个用户每秒最多约 200 个 token。想更快只有三条路:字节更少(FP8、量化)、带宽更多(多卡张量并行、更新的 GPU)、每读一次权重产出更多 token(投机解码,第 5 讲)。MoE 在 batch 1 时只需读被选中的专家,所以 671 B 的 DeepSeek-V3 的 TPOT 下界和一个 40 B 左右的稠密模型相当;但批一大,不同的 token 选中不同的专家,几乎所有专家都要读(第 8 讲)。
一个请求的时间花在哪里?(代码 02 演示 4,Llama-3-8B、H100、batch 1 的下界)聊天(提示 1,000 / 输出 300)时 decode 占端到端时间的 99%;摘要(8,000 / 200)占 89%;推理模型(500 / 8,000)几乎 100%;只有”32,000 的提示、100 的输出”时 prefill 才过半。prefill 一次处理整个提示,效率高;decode 每步只产出一个 token,却要把权重读一遍。
6.3 成本计算器
把显存、延迟、吞吐放在一起,就得到一个”推理成本计算器”(llm_ledger.serving_estimate):
- 显存 → 最大并发 = min(引擎的批上限, KV 容量 ÷ 每个请求的 token 数);
- 满批时 decode 一步的时间 → 吞吐上界 = 最大并发 ÷ TPOT;
- 成本下界 = 每秒的 GPU 价格 ÷ 每秒输出的 token 数(按每 GPU 小时 3 美元)。
代码 06 演示 1(聊天:提示 1,000、输出 300)节选:
| 部署 | 最大批 | TPOT@1 | TPOT@满批 | token/s ≤ | 美元 / 百万 token ≥ |
|---|---|---|---|---|---|
| Llama-3-8B · BF16 · 1×H100 | 256 | 6.16 ms | 20.51 ms | 12,480 | 0.067 |
| Llama-3-8B · FP8 · 1×H100 | 256 | 3.33 ms | 10.51 ms | 24,367 | 0.034 |
| Llama-3-70B · BF16 · 2×H100 | 6 | 26.50 ms | 26.86 ms | 223 | 7.460 |
| Llama-3-70B · FP8 · 2×H100 | 256 | 13.50 ms | 22.47 ms | 11,391 | 0.146 |
| Llama-3-70B · BF16 · 4×H100 | 256 | 13.50 ms | 22.47 ms | 11,391 | 0.293 |
| Llama-3-70B · BF16 · 8×H100 | 256 | 7.00 ms | 11.49 ms | 22,286 | 0.299 |
- 0.067 美元 / 百万 token 是下界:第 17 讲用完整的模拟器(真实的调度、SLO、prefill 的干扰)算出 Llama-3-8B 在 H100 上的混合负载约 0.11 美元。
- 70B 的 BF16 权重 141 GB,2 块 H100 放得下权重却几乎放不下 KV,最大批只有 6、成本贵了几十倍;FP8 让 2 块卡就够用。量化常常首先是为了”少用几块卡”。
- 卡越多,每个用户越快,但每百万 token 未必更便宜:4 块卡 0.293 美元、8 块卡 0.299 美元,吞吐翻倍,钱也翻倍。
负载不同,成本差很多(演示 2、3):长文档问答(16,000 / 500)时 Llama-3-8B 单卡只能同时服务 25 个请求,成本涨到 0.866 美元;推理模型(500 / 8,000)每百万 token 0.285 美元,是聊天的 4.3 倍(每个请求要预留 8,500 个 token 的 KV,批只有 50),一个请求的成本是聊天的约 114 倍——推理模型贵在”说得多”。
代码 06 演示 4 把批大小单独拿出来(Llama-3-8B、BF16、1×H100、聊天):批从 1 增大到 256,总吞吐提高约 77 倍、成本降到 1/77,而每个用户的速度只从 162 降到 49 token/s;显存最多放 328 个请求。decode 受限于带宽时,多服务一个请求几乎是免费的——这就是推理服务”延迟 vs 成本”的基本权衡。
7. 采样 · Sampling
forward 的终点是 个 logits 。怎样从它们得到下一个 token,是一个产品问题(输出要多确定、多有创意),也是一个系统问题(每一步、每个请求都要做,批里每个请求的参数还可能不同)。
7.1 从 logits 到分布
- 贪心(greedy,温度 0):直接取 logit 最大的 token。确定、快,但开放式生成时容易陷入重复。
- 温度(temperature):。 让分布更尖, 让分布更平。它不改变哪些 token 有机会,只改变机会的大小。
- top-k(Fan 等,2018):只在概率最大的 个 token 里采样。不管分布是尖是平都留 个——分布很尖时留多了,很平时又可能留少了。
- top-p / nucleus(Holtzman 等,2020):按概率从大到小排,保留累积概率刚好达到 的最小集合。候选集合的大小随分布自适应。
- min-p(Nguyen 等,2024):保留概率 ≥ 最大概率的 token。按”相对最可能 token 的比例”截断。
代码 04 演示 1 在一个合成的分布上(词表 32,000,Zipf 分布:第 可能的 token 概率 ∝ ,模仿真实语言模型”头部几个 token 占大半、尾巴很长”的形状)比较:
| 参数 | 候选 token 数 | 熵(nats) | 最可能的概率 |
|---|---|---|---|
| 贪心 | 1 | 0 | 1.000 |
| (原始分布) | 32,000 | 3.14 | 0.384 |
| 32,000 | 1.41 | 0.654 | |
| 32,000 | 7.30 | 0.091 | |
| top_k = 50 | 50 | 2.30 | 0.429 |
| top_p = 0.9 | 54 | 2.33 | 0.427 |
| + top_p = 0.9 | 5 | 0.92 | 0.712 |
| min_p = 0.05 | 7 | 1.43 | 0.531 |
| + top_p = 0.9 | 10,705 | 6.65 | 0.101 |
| + min_p = 0.05 | 20 | 2.53 | 0.278 |
最有意思的是最后两行:高温时 top-p 会放进大量长尾 token(温度把尾巴抬高了,要一万多个 token 才凑够 90%),而 min-p 只留 20 个。这正是 min-p 论文的动机:想要高温带来的多样性,又不想让模型从长尾里抽出胡言乱语。
7.2 顺序与默认值
这些操作的先后顺序会改变结果。代码 04 演示 2:同样写着 temperature = 1.5、top_p = 0.9,“先升温、再截断”留下 10,705 个候选,“先在原始分布上截断、再升温”只留 54 个;两个分布的总变差距离 0.54。不同的推理引擎和库在处理顺序、默认值(例如 top-k 默认不限还是 50)、重复惩罚统计哪些 token(只算生成的,还是连提示一起算)上并不完全一致。把一个应用从一个引擎搬到另一个,“同样的参数”可能产生明显不同的输出——评测时要固定引擎和版本。
7.3 重复惩罚
代码 04 演示 3 比较三种写法(token 0 出现过 3 次、logit +2;token 1 出现过 1 次、logit −1):
- repetition_penalty(CTRL,Keskar 等,2019;HF transformers 的写法):出现过的 token,logit 为正时除以 、为负时乘以 。 时 +2.00 → +1.54、−1.00 → −1.30。它是乘性的,只看”出现过没有”,对 logit 接近 0 的 token 几乎没有作用。
- frequency_penalty(OpenAI API):logit 减去 出现次数。出现 3 次的 token 0 被扣 1.5。
- presence_penalty:只要出现过,就减去固定的 。
系统上,这些惩罚每一步都要按每个请求的历史修改 logits:引擎要为批里每个请求维护一份 token 计数,在 GPU 上做一次”按下标修改”。
7.4 top-p 一定要排序吗?
top-p 的直接写法是:把整个词表按概率排序、算累积和、截断。词表 12.8 万时这要约 220 万次比较,大批时每一步要为几百个请求各排一次;在 GPU 上排序要好几个内核、多遍读写,相当昂贵。
FlashInfer 团队 2025 年的博客给出了一个不排序的办法——拒绝采样:
- pivot = 0;
- 在 {概率 > pivot 的 token} 里按概率抽一个 (逆变换采样:累积和 + 二分查找,不需要排序);
- 如果”比 更可能的 token 的总概率”< ,说明 在 top-p 集合里,接受;否则把 pivot 提到 (扔掉所有不比 可能的 token),回到第 2 步。
为什么结果正确:被拒绝的 不在 top-p 集合 里,而 里每个 token 都比它更可能(否则 也会在 里),所以提高 pivot 永远不会扔掉 的成员;“在一个包含 的集合里按概率抽、落进 才接受”,结果恰好是 上按概率的分布。top-k 和 min-p 只是把接受条件换掉。
代码 04 演示 4(词表 1,000,各抽 2 万次):
| 设置 | 平均轮数 | 最多轮数 | 拒绝采样的总变差 | 直接抽样的总变差(噪声基线) |
|---|---|---|---|---|
| Zipf 1.5,top_p = 0.9 | 1.10 | 4 | 0.008 | 0.011 |
| Zipf 1.5,top_p = 0.5 | 1.62 | 8 | 0.003 | 0.001 |
| Zipf 1.0,top_p = 0.9 | 1.10 | 5 | 0.043 | 0.043 |
| Zipf 1.5,top_k = 20 | 1.16 | 4 | 0.005 | 0.004 |
拒绝采样和排序截断的分布一致(总变差和”直接从正确分布抽同样次数”的噪声是同一个量级),平均只要一两轮:大部分时候第一次抽到的就在集合里,被拒绝时 pivot 一下子跳过一大片不可能的 token。每一轮只是一次并行的”比较 + 求和”,可以在一个 GPU 内核里完成。FlashInfer 还用”双 pivot”给轮数加了上界,避免个别请求拖慢整批;博客报告在 vLLM 的单 H100 配置上,采样的总时间减少了一半以上。
7.5 束搜索
束搜索(beam search)每一步保留累积对数概率最高的 个候选序列:每个候选的每个可能的下一个 token 都打分,在 个扩展里取最高的 个。代码 04 演示 5(tiny_llm,生成 20 个 token):
| 束宽 | 序列的 log p | forward 次数 | 复制 KV 的字节 | 同时存在的 KV |
|---|---|---|---|---|
| 1(贪心) | −42.77 | 21 | 0 | 57 KB |
| 2 | −40.95 | 41 | 591 KB | 115 KB |
| 4 | −40.54 | 81 | 1.32 MB | 229 KB |
| 8 | −39.96 | 161 | 2.80 MB | 459 KB |
束宽越大,找到的序列整体概率越高:贪心每一步选最好的,整体却未必最好。代价是 倍的 forward(在 GPU 上可以拼成一批,但占 个批位)和 份 KV;一个父候选分出几个子候选时,除了第一个可以直接接手,其余都要复制父候选的 KV——第 3 讲的写时复制(copy-on-write)让子候选共享父候选的块,只复制最后一块。开放式的对话生成很少用束搜索(容易产生重复、乏味的文本),它主要用在翻译、语音识别这类”有标准答案”的任务里。
7.6 可复现性
用户给了随机种子,就期望”同样的输入、同样的种子、同样的输出”。代码 04 演示 6:请求 A 分别单独、排第一、排第三地和其他请求拼批。每个请求用自己的随机数生成器(按它的种子创建)时,A 抽到的 token 三次都一样;整批共用一个生成器时,A 抽到什么取决于排在它前面的请求消耗了多少随机数(三次里出现了两种结果)。所以推理引擎要为每个带种子的请求单独维护随机数状态。但这还不够:前置讲 P1 §7.4 说过,批的组成不同会让 logits 本身有微小的数值差异;要做到完全可复现,两件事都要处理(第 17 讲的”批不变性”)。
7.7 和投机解码的关系
第 5 讲的投机解码要做”拒绝采样式”的验证:比较草稿模型和目标模型给同一个 token 的概率。这要求两边用同一套采样处理(温度、top-p……)得到的分布,否则验证出来的分布就不是用户要的那个。这也是为什么有的不排序采样内核(只返回一个 token、不给出完整的截断分布)和投机解码的验证不好配合。
8. 停止条件、分词与流式输出 · Stopping, tokenization, and streaming
8.1 停止条件
一个请求在下面任何一种情况下结束:
- 生成了结束 token(EOS,或者聊天模板里”一轮结束”的特殊 token);
- 达到了 max_tokens(最多生成多少个 token),或者达到了模型的上下文上限;
- 生成的文字里出现了用户指定的停止字符串(例如 “\n\nUser:”)。
前两种按 token 判断,很简单;第三种按字符判断,麻烦就来了(§8.4)。停止条件决定了请求什么时候释放它的 KV cache 和批位——第 2 讲说过,输出长度事先未知,正是推理调度难的根源。
8.2 分词:BPE
模型看到的不是字符,而是 token。现在最常用的分词方法是 BPE(byte-pair encoding,Sennrich 等,2016):
- 从最小的单位开始(GPT-2 以后多数模型用字节:256 个基本 token,所以任何文本都能编码);
- 统计训练语料里相邻 token 对的出现次数,把最常见的一对合并成一个新 token,记下这条合并规则;
- 重复,直到词表达到目标大小(Llama 3 是 128,256 个)。
编码时先做预分词(用正则表达式把文本切成”词”,合并不跨词),再对每个词按学到的先后顺序应用合并规则。
代码 05 演示 1 在一段一千六百多个字符的原创语料(英文、中文、代码、数字混合)上训练 BPE:
| 样例 | 字符数 | 0 次合并 | 50 次 | 150 次 | 300 次 | 全部 327 次 |
|---|---|---|---|---|---|---|
| 英文 | 73 | 1.00 | 0.71 | 0.60 | 0.51 | 0.51 |
| 中文 | 21 | 3.00 | 2.67 | 2.14 | 1.86 | 1.81 |
| 代码 | 45 | 1.00 | 0.69 | 0.49 | 0.40 | 0.40 |
| 数字 | 50 | 1.00 | 0.68 | 0.58 | 0.50 | 0.50 |
(表里是每个字符平均几个 token。)0 次合并时一个字节一个 token:英文一个字符 1 个,中文一个汉字 3 个(UTF-8 里汉字占 3 个字节)。合并越多,常见的片段(例如学到的 ” the”、“oken”)变成一个 token。我们的语料太小,中文最多只降到 1.81 token / 字;真实的分词器在海量语料上训练,中文每字约 1 个 token 甚至更少。但不同的分词器差别很大,同一段文字在不同模型上的 token 数可以差一倍以上——而 API 按 token 计费,上下文上限也按 token 算。比较两个模型的”每百万 token 价格”时,要先换算到同样的文字。
8.3 半个汉字
字节级 BPE 能编码任何文本,代价是:训练语料里不常见的字没有被合并成整字,它的字节会散落在一个或几个 token 里。代码 05 演示 2:在我们的分词器上,“流式输出时,半个汉字会变成乱码。“(16 个字符、48 个字节)被切成 37 个 token,其中 33 个不是完整的字符(只是一两个字节)。模型处理 token 没有任何问题;问题出在把 token 变回文字、流式地发给用户的时候。
8.4 流式输出:增量反分词
推理服务通常流式返回结果:每生成一个 token,就把对应的文字发给用户。代码 05 演示 3 比较三种写法(要输出的文字:“答案:缓存命中率 93% 🚀,延迟下降了。“,36 个 token):
| 写法 | 用户看到的文字 | ”�” 数 |
|---|---|---|
| (a) 每个 token 单独解码,再拼起来 | ��������缓存�������� 93% ����,延��下�����。 | 27 |
| (b) 每步把全部 token 重新解码,发出多出来的部分 | ���缓存��� 93% �,延�下��。 | 10 |
| (c) 增量反分词 | 答案:缓存命中率 93% 🚀,延迟下降了。 | 0 |
- (a) 半个汉字、半个 emoji 单独解码就是 ”�“(U+FFFD,“无法解码的字节”),拼起来乱码还在。用 SentencePiece 一类分词器的模型还有另一个问题:前导空格编码在 token 里,单独解码一个 token 会丢掉或多出空格。
- (b) 最终的完整文字是对的,但流式输出时已经发出去的 ”�” 收不回来;而且每一步都要把全部 token 重新解码,1,000 个 token 的回答一共要解码约 50 万个 token,工作量随长度平方增长。
- (c) 增量反分词(vLLM、HF TGI 的做法):记住两个位置
prefix_offset(之前的 token 已经定稿)和read_offset(之前的文字已经发出去)。每来一个 token,只解码prefix_offset之后的几个 token;如果新解码出的文字比上次长、而且末尾不是 ”�“(说明没有半个字符),就把多出来的部分发出去,两个位置往前挪;否则先不发,等下一个 token 把字符补全。它从不输出 ”�“,每一步的工作量是常数;代价是这些字符会晚一个 token 才出现在用户屏幕上。
反分词在 CPU 上做,大批时每一步要为几百个请求各做一次。第 16 讲 SGLang 把它放进单独的进程(detokenizer manager),就是为了不让它拖慢调度循环。
8.5 停止字符串被拆开时
代码 05 演示 4:模型输出”答案是 42。\n\nUser: 下一个问题”,停止字符串是 “\n\nUser:“,它被拆在 “\n\nUs” 和 “er: 下一个” 两个片段里:
| 写法 | 用户看到的文字 | 停下了吗 |
|---|---|---|
| (a) 只在每个新片段里找停止字符串 | 答案是 42。\n\nUser: 下一个问题 | 否 |
| (b) 在全部文字里找,但片段一到就发出去 | 答案是 42。\n\nUs | 是 |
| (c) 缓冲区末尾可能是停止字符串的开头时,先扣下不发 | 答案是 42。 | 是 |
(a) 找不到被拆开的停止字符串,模型会一直生成,把下一轮的 “User:” 也编出来;(b) 能停下来,但 “\n\nUs” 已经泄露给用户了;(c) 只要缓冲区末尾可能是某个停止字符串的开头,就先扣下,确认不是才放出去。
停止字符串、增量反分词都是”按字符”工作的;而模型、KV cache、调度都是”按 token”工作的。这两个世界之间的翻译,是推理引擎里很容易出错、也很容易被忽视的地方。
9. 与推理系统的联系 · Connections to inference systems
有系统背景的读者可以只读这一节。
| 本篇的账 | 在推理系统里的体现 | 哪一讲 |
|---|---|---|
| 参数量、总参数 vs 激活参数 | 要几块卡;MoE 的显存与计算的矛盾 | 第 6、8 讲 |
| 每 token 约 2N + 注意力项 | 批处理的收益;长上下文的代价 | 第 1、2、10 讲 |
| prefill 的平方项 | 长提示的 TTFT;分块 prefill;稀疏注意力 | 第 7、10 讲 |
| 每 token 的 KV 字节 | 显存决定批大小;分页、前缀缓存、分层存储 | 第 3、9 讲 |
| GQA 的组大小、MLA 的潜向量 | decode 注意力的强度;张量并行在 KV 头数处撞墙 | 第 4、8 讲 |
| 显存预算、logits | 最大并发;准入控制;prompt logprobs 的限制 | 第 2、3 讲 |
| TTFT / TPOT 的下界 | 评估一个系统离极限多远;SLO 的可行性 | 第 1、17 讲 |
| 成本计算器 | 部署方案的选择;量化”少用几块卡” | 第 6、17 讲、附录讲 A |
| 采样参数、顺序、默认值 | 跨引擎的结果差异;评测要固定引擎 | 第 17 讲 |
| 不排序的采样 | 大批、大词表时的采样开销 | 第 4 讲(FlashInfer) |
| 每个请求的随机数状态 | 可复现性;批不变性 | 第 17 讲 |
| 束搜索、并行采样 | KV 的共享与写时复制 | 第 3 讲 |
| 采样的分布 | 投机解码的验证 | 第 5 讲 |
| 停止条件 | 输出长度未知 → 迭代级调度 | 第 2 讲 |
| 增量反分词、停止字符串 | CPU 开销;独立的反分词进程 | 第 16 讲 |
10. 小结 · Summary
- 一次 forward:嵌入 → × (RMSNorm、QKV 投影、RoPE、写 KV、注意力、输出投影、残差、RMSNorm、SwiGLU、残差) → RMSNorm → lm_head → 采样。
- 参数量能从 config.json 逐项算出,9 个公开模型都和官方数字对得上。MLP 占大头;小模型里嵌入表占比大;MoE 的显存看总参数、计算看激活参数。
- 每 token 的 FLOPs = 2 × 参与矩阵乘法的激活参数 + lm_head + 注意力项(随上下文线性增长,Llama-3-8B 约 29K 时追上其余全部);prefill 的注意力项随提示长度平方增长。
- 每 token 的 KV = 2 × 层数 × KV 头数 × 头维度 × 字节。GQA 和”复制 KV 头的 MHA”数学上等价,只是不存重复的 K、V;MLA 只缓存潜向量,靠”吸收”在潜空间里做注意力,让 decode 注意力接近 compute-bound。
- 显存预算决定最大并发:上下文越长,能同时服务的请求越少;“要几块卡”首先是显存题。TPOT 的下界是字节 ÷ 带宽,TTFT 的下界是 FLOPs ÷ 算力;批越大,每 token 越便宜。
- 采样:温度改变形状,top-k / top-p / min-p 截断候选;顺序和默认值会改变结果;top-p 可以用拒绝采样代替排序;每个请求要有自己的随机数状态。
- 流式输出要用增量反分词(不输出半个字符),停止字符串要扣下”可能的前缀”——字符和 token 两个世界之间的翻译很容易出错。
11. 常见误区 · Common pitfalls
- “参数量是一个要背的数字”。它能从 config.json 算出来,而且算的过程会告诉你参数在哪里(MLP 占 70%、嵌入表在小模型里占 20%)。
- “MoE 模型 671 B,所以每个 token 要 1.3 T 次运算”。计算看激活参数(约 37 B),显存才看总参数。
- “GQA 减少了注意力的计算量”。它减少的是 KV 的存储和读取;每个 query 头仍然要和每个 key 做点积。
- “GQA 是 MHA 的近似”。对一个按 GQA 训练的模型,它和”复制 KV 头的 MHA”完全等价;只有把一个按 MHA 训练好的模型直接改成 GQA 才会改变输出。
- “上下文翻倍,prefill 时间翻倍”。短提示时近似如此;长提示时注意力的平方项占主导,翻倍会接近翻四倍。
- “temperature = 1.5、top_p = 0.9 在哪个引擎里都一样”。处理顺序、默认值、惩罚的统计范围都可能不同。
- “设了种子,结果就一定可复现”。还要每个请求有自己的随机数状态,以及批不变的内核。
- “每来一个 token 就把它解码发出去”。半个字符会变成乱码,前导空格会出错;要用增量反分词。
- “同一段文字,各家模型的 token 数差不多”。分词器不同,token 数可以差一倍以上,按 token 计价时要先换算。
12. 练习 · Exercises
练习 1(如果 Llama-3-8B 用 MHA)。把 Llama-3-8B 的 KV 头数从 8 改成 32(其余不变)。参数量变成多少?每 token 的 KV 变成多少?在一块 H100 上,4K 上下文时最多能同时服务多少个请求(原来是 104 个)?
参考答案
、 各从 变成 ,每层多 万,32 层多 8.05 亿,共约 8.84 B(param_breakdown(replace(LLAMA3_8B, n_kv_heads=32)) 给出 8.836 B)。KV 从 128 KiB 变成 512 KiB / token。权重多了约 1.6 GB,KV 容量变成约 万 token,4K 上下文时只能服务约 25 个请求——GQA 让并发提高了约 4 倍。
练习 2(70B 的长提示)。Llama-3-70B prefill 一个 8,192 token 的提示,总 FLOPs 是多少?注意力占多少?在 8 块 H100 上(BF16,100% 峰值,忽略通信),TTFT 的下界是多少?
参考答案
prefill_flops(LLAMA3_70B, 8192):约 1.21 PFLOP,注意力约 7.3%(70B 的”2N”项大,注意力的相对占比比 8B 小)。下界 ms。真实的张量并行还有通信,实际会更慢。
练习 3(Qwen2.5-7B 的长对话)。Qwen2.5-7B 每 token 的 KV 是多少?50 个 32K 上下文的请求要多少 KV?一块 H100 放得下吗?
参考答案
字节(56 KiB)。50 × 32,768 × 57,344 ≈ 94 GB,单卡放不下。权重约 15.2 GB,剩下的 KV 容量约 99 万 token,32K 上下文最多约 30 个请求。它只有 4 个 KV 头,已经比 Llama-3-8B 省了一半多。
练习 4(DeepSeek-V2 的 KV)。DeepSeek-V2 有 60 层,MLA 的潜向量 512 维、RoPE key 64 维。每 token 的 KV(BF16)是多少?为什么 MLA 必须把 RoPE 的部分单独拿出来?
参考答案
字节,约 67.5 KiB。RoPE 让 key 乘一个依赖它自己位置的旋转矩阵,这个矩阵随每个历史 token 变化,没法并进固定的解压矩阵,“吸收”就失效了(§5.4)。所以内容部分用潜向量 + 吸收,另存一个所有头共用的小 RoPE key。
练习 5(手算采样)。下一个 token 的概率是 [0.5, 0.2, 0.15, 0.1, 0.05]。(a) top_p = 0.8 保留哪些?min_p = 0.25 呢?top_k = 2 呢?(b) 用 §7.4 的拒绝采样做 top_p = 0.6,第一轮就被接受的概率是多少?平均要几轮?
参考答案
(a) top-p 0.8:累积 0.5、0.7、0.85,保留前三个(0.85 第一次 ≥ 0.8)。min-p 0.25:门槛 0.125,保留 0.5、0.2、0.15。top-k 2:0.5、0.2。(b) top-p 0.6 的集合是 {0.5, 0.2}(比 0.15 更可能的总概率 0.7 ≥ 0.6,所以 0.15 不在集合里),第一轮接受的概率 0.7。被拒绝时:抽到 0.15,下一轮只剩 {0.5, 0.2},必然接受;抽到 0.1 或 0.05,下一轮还可能再被拒绝一次。精确计算平均约 1.33 轮。
练习 6(反分词的工作量)。一个 1,000 个 token 的回答,“每步全部重新解码”一共要解码多少个 token?增量反分词大约多少?批 256 时差多少?
参考答案
个;增量反分词每步只解码最近两三个 token,约 2,000–3,000 个。批 256 时每一步要为 256 个请求各做一次,前者到回答后半段每步要解码几十万个 token,CPU 很可能跟不上 GPU(第 16 讲)。
练习 7(换价格)。代码 06 的表按每 GPU 小时 3 美元算出 Llama-3-8B FP8 单卡聊天的成本下界是 0.034 美元 / 百万 token。如果 GPU 价格是 2 美元 / 小时呢?如果把最大批从 256 限制到 64(为了 TPOT 的 SLO)呢?
参考答案
成本和价格成正比: 美元。批 64 时满批 TPOT 变短、吞吐变小:用代码 06 演示 4 的方法(step_cost 算批 64 的 TPOT,FP8 时权重和 KV 都是 1 字节)可以算出:批 64 时 TPOT 约 5.1 ms,吞吐约 12,500 token/s,是批 256 时的一半左右,成本约 0.066 美元,约翻倍。SLO 越严,能用的批越小,每 token 越贵——这就是第 1、17 讲的 goodput。
13. 延伸阅读 · Further reading
- C. Chen (kipply). Transformer Inference Arithmetic. 博客,2022。本篇的很多推导在这篇博客里有更简洁的版本。
- J. Kaplan et al. Scaling Laws for Neural Language Models. 2020。表 1 的参数与 FLOPs 账。
- N. Shazeer. Fast Transformer Decoding: One Write-Head is All You Need. 2019(MQA)。
- J. Ainslie et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. EMNLP 2023。
- DeepSeek-AI. DeepSeek-V2: A Strong, Economical, and Efficient Mixture-of-Experts Language Model. 2024(MLA 与解耦的 RoPE);DeepSeek-V3 Technical Report. 2024。
- A. Holtzman et al. The Curious Case of Neural Text Degeneration. ICLR 2020(top-p)。
- M. Nguyen et al. Turning Up the Heat: Min-p Sampling for Creative and Coherent LLM Outputs. 2024。
- FlashInfer 团队. Sorting-Free GPU Kernels for LLM Sampling. 博客,2025。
- R. Sennrich, B. Haddow, A. Birch. Neural Machine Translation of Rare Words with Subword Units. ACL 2016(BPE)。
- A. Karpathy 的 minbpe 项目:一个可读的 BPE 实现。
14. 下一篇 · Next
前置讲 P3:操作系统基础。本篇反复出现”显存放不下怎么办""哪些请求先跑""缓存满了扔掉谁”——这些问题操作系统在几十年前就遇到过。P3 讲进程调度(FCFS、SJF、SRPT、轮转、多级反馈队列)、虚拟内存与分页(碎片、页表、写时复制、换出)、缓存与淘汰(LRU、LFU、Belady 最优)、前缀树与引用计数,并把每个概念对应到推理系统里的一个设计:PagedAttention 就是分页,RadixAttention 就是”基数树 + LRU”,抢占就是进程调度。