✏️ 重新学习 LLM 02:把推理过程写成方程
一篇写给非技术读者的科普文。读完你会理解:为什么所有现代大模型推理服务的最佳 batch size 都在「几千」这个量级,以及为什么长对话会让这个最佳值增大 —— 这个数字不是经验值而是从硬件参数算出来的物理结论。
前情提要:上一篇 大模型推理的工作原理、推理速度及推理成本,——从「搬东西 vs 算东西」说起 我们建立了核心直觉——推理耗时 = 搬东西耗时 + 算东西耗时,谁慢听谁的;batch 能摊销搬参数的成本,但摊不掉 KV cache。这一篇我们正式拿起笔,把那些直觉翻译成方程,看看 GPU 工程师每天在白板上画的那张图到底是什么。
一、给「搬」和「算」起名字
上一篇反复说的两件事,先各自起个名字:
- 搬东西所花的时间 → 叫它
T_memory(内存时间) - 算东西所花的时间 → 叫它
T_compute(计算时间)
那么生成一个 token 的实际耗时是多少?
是 max,不是加和。这个细节非常重要,第六节会专门讲。先记住这个结论:两件事谁慢听谁的。
🔑 这就是 roofline 分析。 名字听起来很学术,本质就是上面这一行公式。整个推理性能分析的故事,都从这一行开始。
二、把 T_compute 写出来
回想一下:算的工作量,除以工人速度,就是算的时间。
工人速度
GPU 每秒能做多少次乘法?这个数叫 FLOPS(每秒浮点运算数)。H100 大约是 2 × 10¹⁵,也就是每秒 2000 万亿次乘法。这是一个硬件常数——买回来就定了,软件改不了。
工作量
要算多少次乘法?取决于两件事:
- batch size(同时服务多少用户):每多一个用户,多一份计算。记作
B。 - 每个用户每个 token 要做多少乘法:正比于「模型有多少参数实际参与运算」。
第二个变量需要展开一下,因为它牵出了一个关键概念。
一个新概念:激活参数 vs 总参数
现代大模型大多是 MoE(混合专家) 架构:虽然总共有 700B 参数,但每生成一个 token,只有一小部分参数真的参与运算。
打个比方:你家有一本 700 页的菜谱,但今晚只做番茄炒蛋,只翻其中 37 页。
- 总参数
N_total:菜谱总共多少页(700 页) - 激活参数
N_active:今晚实际翻了多少页(37 页)
比如 DeepSeek V3:N_total = 671B,N_active = 37B。
🔑 为什么要分这两个?因为它们对应不同的成本:
- 算的成本 跟
N_active有关(只算翻开的那几页)- 搬的成本 跟
N_total有关(整本菜谱都要在显存里待着,而且每次还要把它读一遍)
这是 MoE 模型的精妙之处——用稀疏激活换便宜的计算,但搬运的代价没省。这一点会在第五节变得至关重要。
写出 T_compute
每个 token 要做大约 2 × N_active 次乘法(系数 2 来自数学,先不纠结),所以 batch=B 时:
其中 C = FLOPS / 2,可以理解为「工人有效速度」。
🔑 直觉:用户越多算的越多;激活参数越多每人要算的越多;工人越快时间越短。
三、把 T_memory 写出来
上一篇说过:搬两样东西 —— 模型参数 + KV cache。
搬参数
不管 batch 多大,整本菜谱都要被读一遍(每个 token 都用到所有层)。
- 要搬多少:
N_total - 传送带速度:memory bandwidth,记作
BW
H100 的 BW ≈ 3 TB/s,也是硬件常数。
注意:这一项里没有 B。 不管多少用户,搬参数的时间都一样——这就是 batch 能摊销参数搬运的根本原因。
搬 KV cache
每个用户都有自己的 KV「私人笔记」,摊不掉。
- 笔记里每个数值占多少字节:记作
b(小写 b,别和 batch 的大写 B 搞混)。它由数值精度决定:FP16/BF16 ≈ 2 字节、FP8 ≈ 1 字节、FP32 ≈ 4 字节。精度越低,每条记录越省地方 - 每个用户的对话长度:记作
L(context length) - B 个用户的 KV 总大小:正比于
B × L × b(这里把每 token 的元素数视为常数,量级简化,不影响读图)
这一项有 B——batch 越大要搬的 KV 越多,摊不掉。这个差别决定了一切。
把搬的部分合起来
这里是加和,因为两件事用的是同一条传送带——同一条传送带不能同时搬两样东西。这一点很关键,第六节再展开。
四、一张图看懂全部
现在我们有两个时间随 batch size 变化的函数。横轴是 B,纵轴是时间。
T_compute:过原点的直线
T_compute
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
│ ╱
└───────────► B
0T_memory:有截距的直线
T_memory
│ ╱
│ ╱
│ ╱ ← 斜率 = L × b / BW(KV 部分)
│ ╱
│ ╱
│ ╱
│ ╱
│╱ ← y 轴截距 = N_total / BW(参数部分)
│
└───────────► B
0实际耗时:取上面那条
两条线画在一起,T 是它们的 max——哪条在上面,听哪条的:
时间
│ ╱ ← T_compute (B × N_active/C)
│ ╱ 过原点
│ ╱
│ ╱
│ ╱ ╱ ← T_memory (N_total/BW + B×L×b/BW)
│ ╱ ╱ 与 y 轴交于 N_total/BW
│ ╱╱
│ ╳ ← 交叉点 = 甜蜜点
│ ╱╱
│ ╱ ╱
│ ╱ ╱
│ ╱ ╱
│ ╱
│ ╱
│ ╱
└──────────────────────► B
交叉点三个区域的物理意义
| 区域 | 谁主导 | 状态 | 意义 |
|---|---|---|---|
| 左边(B 小) | T_memory | memory-bound | 传送带在等工人 → 加 batch 是免费午餐,延迟几乎不变 |
| 交叉点 | T_memory = T_compute | 完美平衡 | 传送带和工人速度匹配,效率最高 |
| 右边(B 大) | T_compute | compute-bound | 工人忙不过来 → 再加 batch 没用,延迟开始线性上涨 |
🔑 优化推理的核心目标,就是把系统钉在交叉点附近。 这就是为什么所有推理服务都要做 batching、continuous batching、动态调度等等——都是为了让 B 落在那个甜蜜点。
五、解出甜蜜点:神秘的 300 是怎么来的
甜蜜点就是两条线交叉处,即 T_memory = T_compute。
为了让代数清爽,先简化一步:假设上下文不太长,KV 那部分相比参数那部分小到可以忽略(这个假设很快会回头检查)。也就是说:
让两边相等:
解出 B:
🔑 漂亮的事情:这个公式完美分成了两半——左边只跟模型有关,右边只跟硬件有关。两个世界互不干扰。
模型那一项:稀疏度的倒数
N_total / N_active 就是「整本菜谱 / 今晚翻的页数」——也就是稀疏度的倒数。
- 稠密模型(Dense):所有参数都用,
N_total = N_active,比值 = 1 - DeepSeek V3:
671B / 37B≈ 18 - 一些更稀疏的 MoE:可以到 30+
硬件那一项:神秘的 300
C / BW:把工人速度除以传送带速度。代入 H100 的数字(C ≈ 10¹⁵,BW ≈ 3 × 10¹²),单位换算后:
这就是工程师们口中那个神秘的 300。它是当代 GPU 的物理常数——不管 A100、H100、B100,这个比值都在 200 ~ 400 之间。
🔑 为什么这个比值这么大? 因为过去十年里,GPU 的算力涨得比内存带宽快得多。算力翻了几十倍,带宽只翻了几倍——比值就拉大到了 300 这个量级。这正是「现代 GPU memory-bound」的物理根源。
最终结果
综上,我们会获得甜蜜点公式(KV 可忽略时):
左边是硬件常数(≈ 300),右边是模型稀疏度倒数。
代入 DeepSeek V3:
这就是为什么大厂的推理服务永远把 batch 配在「几千」这个量级——这不是经验数,是从硬件参数算出来的物理结论。
六、两个容易绊倒人的细节
这套公式里有两个地方很反直觉,但理解之后会通。
细节 1:为什么 T = max(搬, 算),不是 T = 搬 + 算?
答案藏着 GPU 设计的核心思想——
🔑 GPU 里搬运电路和计算电路是两套独立硬件,可以并行工作。
打个比方:工厂里传送带工人(搬运电路)和装配工人(计算电路)是两组不同的人,可以同时干活——传送带在搬下一批材料的时候,装配工正在加工上一批。
情况 A:搬运慢、计算快(memory-bound)
搬运: ████████████████████ (20ms)
计算: ████ (4ms,然后干等)
总时间: 20ms ← 听搬运的
情况 B:搬运快、计算慢(compute-bound)
搬运: ████ (4ms,然后干等)
计算: ████████████████████ (20ms)
总时间: 20ms ← 听计算的两个工人重叠工作而不是接力工作,所以是 max,不是加和。所有现代 GPU/TPU/AI 加速器都做了这种重叠设计——这正是它们快的核心原因之一。
细节 2:为什么 T_memory 内部又是加和?
你可能立刻发现矛盾:上面说 T = max,那 T_memory = T_搬参数 + T_搬KV 为什么不也写成 max?
答案很简单:
🔑 判断准则:用同一个资源 → 加和;用不同资源 → max。
- T_memory 内部(搬参数 + 搬 KV):用同一条传送带(HBM 带宽),不能并行 → 加和
- T_memory vs T_compute:用两套不同硬件(传送带 vs 工人),可以并行 → max
搞清楚这个准则,整个推理性能分析的逻辑就稳了。
七、回头检查:长上下文怎么办?
第五节有一个偷懒——我们把 KV 那一项扔掉了。如果用户聊了超长对话(比如 200K token 的文档分析),KV 大到不能忽略,会发生什么?
几何上看:
- KV 项让 T_memory 的斜率变大(更陡)
- 而 T_compute 完全不变
- 两条线的交叉点会往右移
🔑 结论:长上下文场景需要更大的 batch 才能达到平衡。
这也解释了一个现实现象:处理长文档/长对话的服务,运营起来反而更看用户量。因为 KV 压力让甜蜜点右移,需要更多并发用户才能填满 GPU。如果用户量不够,机器就只能在低效区间里晃——这就是为什么长上下文 API 又贵又难做。
更进一步:当上下文长到某个临界点,KV 搬运时间会完全压过参数搬运时间,系统从「weight-bound」切换到「KV-bound」。这正是 Gemini 在 200K context 之后涨价 50% 的物理根源——临界点不是拍脑袋定的,是算出来的。
下面是一个交互式模拟器,亲手拉一拉 L、b、BW、N_active 这些滑杆,看 T_compute 与 T_memory 两条线的交点 B* 怎么左右移动:
T_compute / T_memory Batch Size Simulator
T_compute = B × N_active / C, T_memory = N_total / BW + B × L × b / BW
T_compute 的斜率是 N_active / C;T_memory 的斜率是 L × b / BW,截距是 N_total / BW。在超高带宽和超大参数量下,KV 项通常远小于权重读取项,所以调 L 和 b 不容易让总 T_memory 斜率明显变化。
N_active=8G,C=80T ops/s,N_total=12G,BW=200G B/s,L=8.19K,b=2B。
Compute / Memory 比值:2.13e-1。KV 项占比:0.02%。继续提高 L、提高 b、降低 BW,都能明显拉高 T_memory 的斜率。
八、一张表收尾
| 符号 | 含义 | 谁决定 |
|---|---|---|
B | batch size,同时服务的用户数 | 调度策略 |
L | 上下文长度 | 用户行为 |
N_total | 模型总参数 | 模型设计 |
N_active | 每 token 激活的参数 | 模型设计(稀疏度) |
b | KV 缓存每个元素的字节数(FP16≈2 / FP8≈1 / FP32≈4) | 数值精度 |
C | 有效计算速度(≈ FLOPS / 2) | 硬件常数 |
BW | 显存带宽 | 硬件常数 |
C/BW | ≈ 300,硬件天平 | 硬件常数 |
核心方程:
甜蜜点(KV 可忽略时):
九、留几个可以自己想的问题
- 如果一家公司声称用了「新算法」让推理速度快 10 倍,应该追问哪些问题?提示:他们改的是
N_active、b、还是C/BW? - 一个稠密模型(
N_total = N_active)的最优 batch 大约是多少?为什么稠密模型在长上下文场景下日子更难过? - 如果未来的 GPU 把
BW翻 10 倍,但C不变,C/BW会变成多少?甜蜜点会怎么移动?哪类模型会受益最大? - 一家服务商如果只有少量用户,但每个用户都聊超长对话,它的 batch 会被卡在哪个区间?利润率会怎样?