为什么 GPU 利用率显示 100%、推理还是慢:用算术强度与 Roofline 看清 prefill 和 decode 是两台不同的机器
把「大模型推理为什么慢」讲成一条可手算的账:算术强度(每搬 1 字节能算多少次浮点)决定你是撞「带宽墙」还是撞「算力墙」。本文按官方规格现算脊点——A100 80GB SXM 约 153 FLOP/字节、H100 SXM 稠密约 295 FLOP/字节(带稀疏 1,979 TFLOPS 口径下约 591),并推出两个结论:prefill 阶段的算术强度约等于输入长度,几百 token 以上就轻易越过脊点;decode 阶段的算术强度约等于并发批大小,7B 模型在 A100 80GB SXM 上要批到约 153 条(H100 稠密口径约 295)才可能变成算力瓶颈,而且这个数与模型规模无关。这也解释了为什么 70B 模型在 H100 上单条请求的理论天花板只有约 24 token/s,以及为什么 Databricks 在单张 A100 上的实测里,批大小从 1 加到 64 会让延迟涨 4 倍而吞吐涨 14 倍。
除另行标注外,本文引用的外部事实与页面核验均以 2026-10-22(北京时间)为时点;文中所有「本文自行推导」的算式都给出了假设与中间值,可以自己按一遍。
一、先打四个比方:把「算力」和「搬运」分开
如果你只记住一句话:大模型的推理速度,多半不是被「算得慢」卡住的,而是被「搬得慢」卡住的。
先讲一个厨房的比方。你有一台功率极大的灶(算力),但食材都锁在隔壁仓库里(显存)。每做一道菜,你都得先去仓库把食材搬过来。灶再猛,如果每一盘菜只用到一小撮食材、而跑一趟仓库要 40 秒,那你的出菜速度就完全由「跑仓库的速度」决定,跟灶的功率没关系。这就是带宽瓶颈:搬运量除以搬运速度,决定了下限。
第二个比方,来自搬砖。如果一次只搬一块砖,你的手推车(内存带宽)空跑一大半;如果把 100 块砖码满再走一趟,同样的路程搬走的砖多了 100 倍——但这一趟也更慢,因为装车、卸车的时间变长了。这就是**批处理(batching)**的两面性:它把「每趟搬多少」提上去,也把「单趟要多久」提上去。
第三个比方,说给做产品的同学听:带宽是高速公路的车道数,算力是发动机的马力。高峰期堵车,你换一台马力更大的车没用;反过来,路很空但你开的是拖拉机,那才是马力问题。判断自己堵在哪里,比换车重要得多。
第四个比方,是流水线与验收。大模型生成一句话分两段:先把你的问题整段读进去(这段可以并行,像把一整本书同时摊开读),然后一个词一个词往外蹦(这段只能串行,因为下一个词要等上一个词算完)。前者叫 prefill(预填充),后者叫 decode(解码)。这两段的瓶颈完全不是同一堵墙——这是本文最想讲清楚的一件事。
这一节想让你记住的一件事:速度 = 搬运量 ÷ 搬运速度,或者 = 计算量 ÷ 计算速度,哪一个先到顶,你就是被哪一个卡住。
二、一句话直觉,以及先把符号钉死
把上面四个比方合起来,工程上只需要一个量来判断「你现在撞的是哪堵墙」:每从显存搬 1 个字节,你能顺便做多少次浮点运算。这个比值叫算术强度(arithmetic intensity,Roofline 原始论文里叫 operational intensity,定义是「每字节 DRAM 流量的运算次数」)。比值高,说明你搬进来的数据被反复使用,机器有机会吃满算力;比值低,说明你搬一次只用一下,机器大部分时间在等数据。
先约定符号,后面所有推导都只用这张表:
| 记号 | 含义 | 典型值 / 单位 |
|---|---|---|
| 模型参数量 | 7B = | |
| 一次前向要处理的 token 数(prefill 是提示长度,decode 是 1) | 512 ~ 8192 | |
| 并发批大小(一次前向里塞了几条请求) | 1 ~ 256 | |
| 硬件峰值浮点算力 | TFLOPS( FLOP/s) | |
| 硬件峰值显存带宽 | TB/s( 字节/s) | |
| 算术强度 = 浮点运算数 ÷ 搬运字节数 | FLOP/字节 | |
| 脊点(ridge point)= | FLOP/字节 | |
| 每个参数占的字节数(fp16 为 2) | 字节 | |
| 、、、 | 层数、隐藏维、注意力头数、每头维度 | 无量纲 |
| MBU / MFU | 显存带宽利用率 / 模型算力利用率 | 百分比 |
这里要特别提醒一个新手最常搞混的点:TFLOPS 和 TB/s 是两个量纲,一个是「每秒算多少次」,一个是「每秒搬多少字节」,不能直接比大小。能比的是它们的比值,也就是脊点。
这一节想让你记住的一件事:不要问「这卡多少 T」,要问「这卡的算力除带宽是多少」——那个商才是分界线。
三、把机器画成一条屋顶线
Roofline 模型(Williams、Waterman、Patterson,UC Berkeley 技术报告 UCB/EECS-2008-134,2008 年 10 月 17 日)做的就是这件事:横轴放算术强度,纵轴放「实际能达到的性能」,画出来的形状像一面斜屋顶加一堵天花板。
可达性能的上界是两条线里更低的那个:
- 当 时,性能由带宽决定,性能随 线性上升——这是屋顶的斜边(带宽墙);
- 当 时,性能被算力封顶,再提高算术强度也没用——这是屋顶的平顶(算力墙)。
两条线的交点是脊点,把上式取等号即可解出:
还有一个实用的变形:如果你是带宽受限的,那么「每秒最多能搬多少个 token」就是
这个式子后面会反复用到,因为它把「性能」翻译成了「你能卖多少 token」。
flowchart LR
A["一个算子 / 一层网络"] --> B["数它做了多少浮点运算 FLOPs"]
A --> C["数它从显存搬了多少字节 Bytes"]
B --> D["算术强度 I = FLOPs / Bytes"]
C --> D
D --> E{"I 比脊点大还是小"}
E -- "I 小于脊点" --> F["带宽墙:加卡算力没用,要减少搬运(量化 / 融合 / 增大批)"]
E -- "I 大于脊点" --> G["算力墙:加并发或加算力有用(稀疏 / 更好的 kernel)"](顺带解释一下这个模型为什么叫「屋顶线」:横轴算术强度拉开后,可达性能先沿斜线上升、再被平顶压住,两条线围出的形状像一面屋顶,屋顶之上没有任何实现能跑到。)
这张图说明了什么:判断优化方向只需要两个数——这个算子算了多少、搬了多少;两者的商落在脊点哪一侧,直接决定你该往哪儿使劲。
这一节想让你记住的一件事:Roofline 不是精确预测器,是一张「先看哪堵墙」的决策图;算错优化方向的代价,通常比算得不够准大得多。
四、把三张真实显卡的脊点算给你看
以下硬件数字取自 NVIDIA 官方产品页(A100 / H100 的 80GB 两档),A100 40GB 一行取自 kipp.ly 的引用口径(其自注给出的实际值为 1.555 TB/s,正文计算取 1.5 TB/s);脊点的换算与除法是本文自行推导:
| 硬件(口径必须说清) | FP16 Tensor Core 算力 | 显存带宽 | 脊点(本文自行推导) |
|---|---|---|---|
| A100 40GB(HBM2) | 312 TFLOPS(带稀疏 624) | 1,555 GB/s(kipp.ly 自注的 A100 40GB 实际值;其正文计算用 1.5 TB/s) | 约 201 FLOP/字节 |
| A100 80GB SXM | 312 TFLOPS(带稀疏 624) | 2,039 GB/s | 约 153 FLOP/字节 |
| A100 80GB PCIe | 312 TFLOPS(带稀疏 624) | 1,935 GB/s | 约 161 FLOP/字节 |
| H100 SXM | 1,979 TFLOPS(页面脚注「* With sparsity」) | 3.35 TB/s | 带稀疏约 591;稠密折算约 295 |
逐步算一遍 A100 80GB SXM:
H100 SXM 的官方口径是「1,979 teraFLOPS *」,而页面脚注写明星号表示 With sparsity(带 2:4 结构化稀疏)。所以稠密场景要按一半折算:
这三行数字里藏着两个必须一起读的口径:①同一张 A100,40GB 与 80GB 的带宽并不相同(1,555 对 2,039 GB/s),只写「A100」而不写容量,脊点能差 30% 以上;②H100 页面上的 1,979 是带稀疏的营销口径,你的推理框架如果没有真的用上稀疏,实际天花板要按稠密折算——这是本文后面所有估算的基准。顺便说一句,公开社区里同型号 GPU 的实测 MBU 也远达不到 100%(Databricks 实测在批大小 1 时,2×H100 上 MBU 约 60%、4×A100-40GB 上约 55%),所以下面的「理论上限」永远要再打个折。
这一节想让你记住的一件事:报硬件数字必须连着「哪个容量、带不带稀疏」一起报,否则同一个型号能报出两个差 30%–100% 的脊点。
五、一个 Transformer 到底搬了多少、算了多少
要判断 Transformer 在脊点哪一侧,只需要两个估算。这不是严谨的算子计数,但误差通常在 10% 以内,足以定位瓶颈——kipp.ly 的《Transformer Inference Arithmetic》用的正是这套口径:每生成一个 token,每个参数大约贡献 2 次浮点运算(一次乘、一次加)。
于是单条请求、单个 token 的计算量约为
而它必须被从显存读进来的,至少是全部权重(这是下限,实际还要加 KV cache、激活与中间张量):
把两个式子相除,就得到 decode 阶段的算术强度(假设 fp16,,忽略 KV cache):
单条请求的算术强度是 1,而 A100 80GB 的脊点是 153——差了两个数量级。这就是「明明 GPU 利用率不高、但就是快不起来」的根本原因:你在用一台峰值 312 TFLOPS 的机器,做一件每搬进 1 个字节只做约 1 次浮点运算的事——要把 2,039 GB/s 喂饱只需要约 2.04 TFLOPS,连峰值的 1% 都不到。
批处理是唯一的杠杆。一次前向里塞进 条请求后,权重只搬一遍,却服务了 条:
结论非常干净:decode 阶段的算术强度数值上就等于批大小。要越过 A100 80GB 的脊点,理论上需要 ;要越过 H100 稠密口径的脊点,需要 。而现实中的在线服务,批大小常年在个位数到几十之间,所以 99% 的推理服务都在带宽墙这一侧。
既然在带宽墙上,就直接用带宽算天花板。以 fp16 的 70B 模型(权重约 GB)在 H100 SXM(3.35 TB/s)上为例:
同样的算法换成 7B 模型(权重 14 GB),单条请求的理论上限约为
这两个数字(约 24 token/s 与约 239 token/s)都是「只看权重搬运」的乐观上限,实际要再扣掉 KV cache、激活、attention 计算与 kernel 效率损失。但它们的量级解释了一个现象:为什么 70B 模型单条对话总感觉「一个字一个字往外挤」,而 7B 模型在小显卡上也能刷得飞快——这不是软件没调好,是物理搬运量的直接结果。
把同样的账套到三个常见规模上(全部为本文自行推导,按 fp16 权重、单请求、忽略 KV cache 与激活):
| 模型规模 | 权重字节(fp16) | 单请求 decode 上限(H100 SXM 3.35 TB/s) | 单请求 decode 上限(A100 80GB SXM 2.039 TB/s) | 要打满 H100 稠密算力所需批大小 |
|---|---|---|---|---|
| 7B | 14 GB | 约 239 token/s | 约 146 token/s | 约 295 |
| 70B | 140 GB | 约 23.9 token/s | 约 14.6 token/s | 约 295 |
| 405B | 810 GB | 约 4.1 token/s | 单卡放不下(需多卡切分) | 约 295 |
请注意最右列:所需批大小与模型规模无关,它只由硬件脊点决定(H100 稠密 989.5 TFLOPS ÷ 3.35 TB/s ≈ 295)。这是本文最反直觉的一条推论——「大模型更难受」不是因为它的算术强度更低,而是因为它的权重根本塞不进一张卡。
这一节想让你记住的一件事:decode 的算术强度约等于批大小(单条请求只有 1,离脊点差两个数量级),而越过脊点所需的批大小只由硬件脊点决定、与模型规模无关。
六、prefill 与 decode 是两台完全不同的机器
同一份权重、同一套 kernel,两个阶段的算术强度却由完全不同的量决定。prefill 一次处理 个 token,计算量变成 ,而权重仍然只搬一遍(这里的账只计权重侧矩阵乘:attention 的 计算、KV cache 写回、激活搬运都忽略,权重每步只读一次;与 decode 那一步写明假设的做法同源):
于是得到本文最对称的一对结论:
- prefill 的算术强度 ≈ 输入长度 :提示有 2,048 个 token,算术强度就是 2,048,远超 A100 80GB 的 153 与 H100 稠密的 295——此时 prefill 落在算力墙一侧。注意这是条件式结论: 只有越过脊点才成立,几十个 token 的极短提示()仍然先撞带宽。
- decode 的算术强度 ≈ 并发批数 :在线服务常见个位数,decode 长期落在带宽墙一侧——要把批开过上百条才能翻到算力侧,而那通常先被显存与延迟否掉。
flowchart TB
subgraph P["prefill:整段并行读入"]
P1["输入提示 T 个 token"] --> P2["一次前向,计算量约 2NT"]
P2 --> P3["算术强度约等于 T(数百到数千)"]
P3 --> P4["撞算力墙:受 TFLOPS 与 kernel 效率限制"]
P2 --> P5["产出并写入 KV cache"]
end
subgraph D["decode:逐 token 串行"]
D1["上一步输出 1 个 token"] --> D2["一次前向,计算量约 2Nb"]
D2 --> D3["算术强度约等于批大小 b(个位数到几十)"]
D3 --> D4["撞带宽墙:受显存带宽限制"]
D2 --> D5["读回不断增长的 KV cache"]
D5 --> D1
end
P5 -. "交接:KV cache 越写越长,decode 每步都要全读一遍" .-> D5这张图说明了什么:prefill 和 decode 走的是同一条模型路径,但一个被算力卡住、一个被带宽卡住——所以「优化推理」不能只做一件事,混着做还会互相打架(把批开大对 decode 有利,却让 prefill 的尾延迟变长)。
两个阶段对用户的意义也完全不同:prefill 决定首 token 延迟(TTFT),decode 决定每 token 间隔(TPOT)。Databricks 的实测给出了一个直观参照:输入 512 token、批大小 1 时,MPT-7B 在 1×A100-40GB 上 TTFT 为 46 ms,分到 4 卡张量并行后降到 26 ms(约 0.56 倍)——张量并行把每卡要算的东西(连同那份权重)一起拆小,所以首 token 变快;但收益是次线性的,代价是每层都要做一次跨卡通信。这也和「prefill 是算力墙」自洽:算力侧要分摊的是计算量,不是搬运量。
这一节想让你记住的一件事:prefill 看长度、decode 看批数;前者是算力问题,后者是带宽问题,别用同一套办法治。
把两阶段摊到用户体验上:TTFT 与 TPOT
工程上不会只说「快不快」,而是拆成两个可测的数:
这两个式子里的 与 是实际可达的算力与带宽(不是标称峰值),也就是把峰值乘上 MFU 或 MBU。把它们代进用户视角,会得到几个不那么直觉的结论:
| 你想优化的指标 | 加批有用吗 | 加算力(换卡)有用吗 | 真正的杠杆 |
|---|---|---|---|
| 首 token 延迟 TTFT(长提示) | 基本无用(变长还会变差) | 有用,且接近线性 | 更快的 prefill kernel、更短提示、前缀缓存 |
| 每 token 间隔 TPOT(短上下文) | 无用(带宽墙,批不增搬运) | 几乎无用 | 减少每步搬运字节:量化、KV 头共享 |
| 每 token 间隔 TPOT(长上下文大批) | 有用,直到撞显存 | 有用 | KV cache 管理(分页、压缩)、PD 分离 |
| 单卡能扛多少并发 | —— | 无用 | 显存容量与 KV cache 字节数 |
这张表最有用的一行是第二行:短上下文下的 TPOT 对「换更好的卡」几乎不敏感,因为它只认带宽与搬运字节。这也解释了为什么很多团队换了更强的卡、单条对话速度却没什么变化——他们买的算力,用在了自己不在的那堵墙上。
这一节想让你记住的一件事:先确认你要优化的是 TTFT 还是 TPOT,再决定花钱买算力还是买带宽。
七、把 batch 开大就救得回来吗
理论上,把批大小开过脊点就能从带宽墙走进算力墙,吞吐会继续涨。但代价是延迟,而且是线性甚至更快地涨。Databricks 那篇性能工程实践里给了一组实测:在单张 A100 上,把批大小拉到 64 换到最高吞吐时,延迟涨了 4 倍,吞吐涨了 14 倍(这是该文正文表述;其表 2 的 1×A100 行显示批 64 约 12 倍、批 128 才到 14 倍,本文按正文口径引用并在此标明差异)。
这笔账可以自己推一遍。批 时的单步时间是
在带宽墙一侧,第二项主导, 几乎不随 变——于是吞吐()近似线性增长,而每条请求的等待时间几乎不变。这就是批处理「白捡」的那一段。越过脊点后,第一项开始主导,,吞吐见顶,延迟继续线性恶化——这就是 Databricks 说的「过了脊点,批大小每翻一倍,只是延迟变长而吞吐不变」。
flowchart LR
S["批大小 1"] -->|"批翻倍,搬运量不变"| T1["吞吐近线性上升 延迟几乎不变"]
T1 -->|"继续加批"| T2["逼近脊点:吞吐增速放缓"]
T2 -->|"越过脊点"| T3["进入算力墙:吞吐封顶 延迟线性上升"]
T3 -->|"再加批"| T4["KV cache 撑爆显存 或 尾延迟不可接受"]
T4 --> T5["回退:要么降批、要么量化减字节、要么换更高带宽的卡"]这张图说明了什么:批处理不是免费的加速开关,它是一段「有上限的免费区间」;越过脊点之后你买到的是吞吐而不是速度,而且最终会被显存容量拦住。
所以真正能同时降价又提速的路子,都是减少必须搬的字节,而不是只加批:把权重从 fp16 压到 int8,字节数减半,同样带宽下的 decode 天花板就翻倍(这是本文按上式推导的方向性结论,具体收益要按量化方案实测);把多个算子融合进一个 kernel,省掉中间张量的来回搬运;用 FlashAttention 这类 IO 感知(IO-aware)算法,靠分块(tiling)减少 HBM 与片上 SRAM 之间的读写次数——它的论文明确把「少搬」作为核心贡献,而不是少算。这也解释了为什么很多「数学上更省」的近似注意力方法跑不出实际加速:它们省的是 FLOPs,而瓶颈根本不在 FLOPs。
这一节想让你记住的一件事:加批只在你还没到脊点时免费;越过脊点,唯一的普惠手段是「少搬字节」——量化、算子融合、IO 感知的 kernel。
八、KV cache:把带宽墙继续往前推的那只手
decode 阶段除了权重,还要读回历史所有 token 的 Key/Value 向量,这就是 KV cache。它的体积按 token 线性增长,公式(kipp.ly 的推导口径)是
最前面的 2 表示 K 和 V 各存一份。代入 kipp.ly 的算例( 层、、fp16、不做 GQA 的稠密注意力,此时 );这个规模对应的是 70B 级以上的稠密模型,小模型要按自己的层数与维度重算:7B 级模型(、)同口径约 0.5 MB/token,是这里例子的四分之一。
也就是说,一条 2,048 token 的上下文,光 KV cache 就要约 4 GB;若这张卡上还剩 16 GB 可用,那么把「批 × 长度」塞满大概只能到 8,000 个 token——批大小和上下文长度在抢同一块显存。这也正是 vLLM 那篇 PagedAttention 论文的问题起点:显存被碎片化与重复拷贝浪费掉,能塞下的批就小;它用类似操作系统虚拟内存分页的做法把 KV cache 按块管理,论文报告在同等延迟下把吞吐提高了 2–4 倍,序列越长、模型越大收益越明显。
KV cache 还有一个反直觉的后果:它会抬高 decode 阶段每步的搬运量,也就是把「带宽墙」进一步压低。批 、上下文长度 时,每步要搬的字节约为
于是 decode 的算术强度修正为
这个式子里有个容易被忽视的事实:当上下文很长、批也开大时,分母里的 KV 项会超过权重项,算术强度反而下降——你加了批,却因为搬了更多 KV 而没有换来相应的算力利用率。截至 2026 年,业界把 MQA / GQA / MLA 这些「共享 KV」的结构做成主流(GQA 即分组查询注意力:多个查询头共用一组 K/V 头,从而把 KV 头数 H 直接调小),本质上就是在减小 这一项(KV 头数变少,同样长度下 KV 字节数成比例下降),从而把 decode 拉回带宽墙更靠上的位置;PD 分离(prefill-decode disaggregation,把 prefill 与 decode 拆到不同实例上分别服务)则是承认「两台机器」这个事实,让各段用最合适的并行策略。
把这个 2 MB/token 的算例摊到不同上下文长度上(本文自行推导):
| 上下文长度 | 单条请求 KV cache | 若显存只剩 16 GB 可用,最多能并发多少条 |
|---|---|---|
| 2,048 token | 约 4 GB | 约 4 条 |
| 8,192 token | 约 16 GB | 约 1 条 |
| 32,768 token | 约 64 GB | 放不下(需 KV 压缩或多卡切分) |
这张表解释了一个常见的运营困惑:上下文窗口从 8K 提到 32K,能同时服务的人数可能掉一个数量级。窗口是产品能力,并发是成本,两者共用一个显存池。
这一节想让你记住的一件事:KV cache 会随上下文线性增长并挤占显存,它既限制你的批大小,又拉低 decode 的算术强度——所以长上下文服务的瓶颈往往比短上下文更早到来。
九、七个最常见的误解,逐个拆掉
| 误解 | 为什么错 | 更准确的说法 |
|---|---|---|
| 「GPU 利用率 100%,说明算力吃满了」 | 利用率统计的是「有任务在跑」,不等于 Tensor Core 在满负荷算;带宽墙上跑满的是搬运 | 看 MBU(带宽利用率)与 MFU(算力利用率)两个数,decode 阶段 MBU 接近上限、MFU 很低是正常的 |
| 「加卡就能加速」 | 张量并行只是把每卡的搬运量拆小,脊点几乎不变;同时还要付跨卡通信 | 单条请求的 decode 提速有限,加卡的真正收益是容纳更多并发、把批开大 |
| 「FLOPs 更少的方法一定更快」 | 在带宽墙上,省 FLOPs 不改变搬运量 | 先判断在脊点哪一侧;带宽墙上要省的是字节,不是运算 |
| 「模型越大越慢,是因为算得多」 | 70B 单条请求慢的主因是权重搬得多(140 GB 起) | 慢在搬运;这也解释了为什么把权重压到 int8 几乎能翻倍 |
| 「批开得越大越好」 | 越过脊点后吞吐封顶、延迟线性涨,还会撞显存容量 | 先算脊点对应的临界批大小,把它当上限而不是目标 |
| 「prefill 和 decode 可以套同一套优化」 | 一个算力墙、一个带宽墙 | 分开测 TTFT 与 TPOT,分别优化;必要时做 PD 分离 |
| 「上下文变长只是多算一点」 | 上下文变长会线性增大 KV cache,既抢显存又增加每步搬运 | 长上下文服务的瓶颈常常是容量与带宽,而不是注意力那点额外 FLOPs |
| 「首 token 慢就是模型太大」 | 首 token 由 prefill 决定,主要吃算力与提示长度,还受排队与调度影响 | 分开量 TTFT 与 TPOT,再决定换卡还是改调度 |
| 「厂商标的 TFLOPS 就是我能用的算力」 | 官方口径常带稀疏或特定精度,实测 MFU 通常远低于峰值 | 报数字必须带上「哪个精度、带不带稀疏」,并给实测利用率 |
这一节想让你记住的一件事:把「利用率」「算力」「带宽」三个词分清,能拆掉大半个推理优化领域里的伪命题。
十、边界与未解:这套算术什么时候不适用
第一,实测永远低于理论上限,而且是结构性的。Roofline 给的是上界,kernel 启动开销、小的矩阵形状、页表与碎片、注意力里的 softmax 与归约、跨卡通信都吃时间。Databricks 的实测里,批大小 1 时 MBU 也就 55%–60%,说明即便在最适合它的带宽墙上,也有四成带宽没被有效利用。
第二,「2 FLOPs per parameter per token」是粗估。它忽略了注意力部分的计算(长上下文时不可忽略)、忽略了激活与归一化的搬运,也忽略了不同实现里融合带来的实际差异。用它定位瓶颈没问题,用它预测绝对性能会有 10%–30% 的偏差。
第三,层级存储被简化成了一层。Roofline 原论文把 DRAM 之外都视为「被缓存过滤后」的流量,但真实 GPU 有 L2、有共享内存、有寄存器;FlashAttention 的全部收益正是来自「把该复用的留在片上」。所以「搬了多少字节」必须在目标存储层级上定义,写含糊了就会算出两个差好几倍的结论。
第四,稀疏、投机解码、MoE 都在改写分母。结构化稀疏能提高 的有效值;投机解码一次验证多个候选 token,相当于把 decode 的有效批变大;MoE 每个 token 只激活一部分参数,把 换成 。这些机制没有推翻 Roofline,只是把公式里的量换掉——判断方法不变,代进去的数要换。
第五,多卡之后带宽不是一个数。张量并行让每卡读的权重变少,但要周期性做 AllReduce;流水并行会让卡在等待中空转;机内 NVLink 与跨机网络差一个数量级。此时单卡脊点仍然成立,但整机的「有效带宽」要把通信算进去,工程上更常用实测的 MBU / MFU 曲线来定容量,而不是硬套公式。
第六,本文没有讨论算子级的数值精度差异。fp8 与 int8 的峰值口径、溢出保护、以及不同框架对稀疏的支持程度,都会改变可用的 ;同一张卡在「标称峰值」「框架可达峰值」「你的模型实测峰值」三个口径下可能差两倍以上。任何容量规划都应该用第三个口径。
第七,排队与调度不在 Roofline 的坐标系里。真实服务的延迟常常不是算出来的那一段,而是「等上一个请求算完」的那一段;连续批处理(continuous batching)与优先级调度对用户体验的影响,可以和一次量化升级相当。本文只谈单次前向的物理上限,不谈排队论。
第八,能效与成本是第三个轴。同样的 token/s,可以用两张低带宽卡或一张高带宽卡实现,成本可能差一倍;容量规划时要把「每百万 token 的成本」算进来,而不是只比单卡速度。本文给的所有天花板都是「单卡、单请求、理想带宽」下的物理边界。
第九,本文所有「理论上限」都是单请求、按峰值带宽、按稠密 fp16 算的乐观值,用于判断瓶颈方向;要给业务报 SLA,必须用目标硬件 + 目标框架 + 目标序列长度的实测曲线。
这一节想让你记住的一件事:Roofline 是判断方向的地图,不是性能预测器;任何一次「算出来比实测快」的推导,先怀疑自己漏了哪一层搬运。
十一、小结:五句话带走
- 判断瓶颈只需要一个比值:算术强度 与脊点 谁大谁小。
- A100 80GB SXM 的脊点约 153 FLOP/字节,H100 SXM 稠密口径约 295(带稀疏口径约 591)——报数字必须带上容量与稀疏口径。
- decode 的算术强度约等于批大小,单请求时只有 1,必然撞带宽墙;prefill 的算术强度约等于输入长度,提示长度越过脊点(A100 80GB 约 153、H100 稠密约 295)后才落在算力墙一侧,极短提示仍在带宽侧。
- 带宽墙上的天花板可以直接算:70B fp16 在 H100 上单请求约 24 token/s,7B 约 239 token/s;想要更高只能少搬字节,或把批开大并接受延迟。
- KV cache 会随上下文线性增长,既抢显存又拉低 decode 的算术强度——长上下文与大批量在抢同一块资源。
最后补一句关于「屋顶线」这个名字。它之所以被画成屋顶,不是为了好看,而是为了提醒你:屋顶之上没有性能。你在斜边上再怎么调代码,也只能贴着斜边往上走一点点;要真正抬高屋顶,只能换一种硬件配比(更高的带宽),或者减少要搬的东西(更小的权重、更省的 KV)。过去十年里推理优化的绝大部分「奇效」,本质上都是在做后一件事。
如果你手上有一台机器和一个模型,最省时间的自测顺序是:先量 TTFT 与 TPOT(分开),再用本文的式子算出理论上限,最后看「实测 / 理论上限」这个比值落在哪——落在 0.5 附近说明你已经在带宽墙上做得很好了,落在 0.1 附近才是软件还有空间。
把本文当成一张检查表,日常最值得贴在工作台上的就三行:
- 我的模型每 token 要搬多少字节(权重 + KV);
- 我的机器的脊点是多少(算力 ÷ 带宽);
- 我现在的批与长度,让算术强度落在脊点的哪一侧。
十二、五个常见追问
问:既然 decode 是带宽墙,为什么厂家还要拼命堆算力? 答:因为 prefill 是算力墙,训练更是算力墙。同一张卡要同时服务「读得很长」和「算得很多」两类负载,堆算力对首 token 和训练都直接有效;只是它解决不了单条 decode 的 TPOT——这也是为什么截至 2026 年,高带宽内存(HBM)的容量与带宽被单独当作卖点来讲。
问:那我把批开到 153 不就行了? 答:不行,两个原因。第一,批 153、每条 2,048 token 上下文的 KV cache 会先把显存吃光(按上文 70B 级模型每 token 约 2 MB 的算例,这是几百 GB 量级;7B 级模型是它的四分之一,同样是几十 GB);第二,即便塞得下,排队与尾延迟会让在线服务不可用。真正能做到大有效批的,是离线批处理、投机解码这类「一条请求里塞多个 token」的变通做法。
问:为什么有的服务 TTFT 很快、TPOT 很慢,有的反过来? 答:前者说明 prefill 的算力利用得好(或提示短、命中了前缀缓存),但 decode 的每步搬运量大(上下文长、KV 没共享);后者常见于把大模型拆到多卡上做张量并行——首 token 因为每卡搬运量变小而变快,一旦进入 decode,跨卡通信的固定开销就摊在每一个 token 上。
问:MoE 模型是不是绕开了这套算术? 答:绕开了一半。MoE 把「每 token 要用多少参数」从总参数 换成激活参数 ,于是搬运量与计算量都按激活部分算,decode 上限随之抬高;但路由带来的负载不均、专家并行的通信、以及「全部专家都得驻留显存」这三件事没有消失,容量约束往往比稠密模型更紧。
问:这套算法适用于端侧(手机、笔记本)吗? 答:适用,而且更好用。端侧的内存带宽通常只有几十 GB/s 量级,脊点极低(算力除以带宽的商很小),所以几乎一切负载都落在带宽墙上——这解释了为什么端侧优化的主流手段是量化与更小的模型,而不是提高算力。
问:我怎么知道自己的服务现在是带宽墙还是算力墙,而不是「代码写得烂」? 答:做一次「换变量」实验,成本很低:①把批大小翻倍,若吞吐近线性上升而 TPOT 基本不变,你在带宽墙且还没到顶;②把序列长度砍半,若 TPOT 明显下降,说明 KV 项占比大;③把权重换成 int8,若 TPOT 下降接近一半,说明确实卡在权重搬运。三个实验都指向同一个结论时,再谈换硬件才有依据。
这一节想让你记住的一件事:所有「为什么某个优化在我这里没用」的问题,最终都能回到「它在不在我当前撞的那堵墙上」。
十三、自测清单:六个问题问完就知道该干什么
| 你要回答的问题 | 怎么看 | 结论 |
|---|---|---|
| 我的单步时间离权重搬运下限差多少? | 实测 TPOT ÷(权重字节 ÷ 带宽) | 大于 3 倍 = 软件还有空间;接近 1.6 倍 = 已接近物理极限 |
| 我的算术强度落在脊点哪一侧? | 用本文的 、 估 | 低批短提示 decode 必然在带宽侧 |
| KV 项是否已经超过权重项? | 比较 与 | 超过则先做 KV 治理,而非加卡 |
| 加批还有免费区间吗? | 批翻倍看吞吐与 TPOT 变化 | 吞吐涨而 TPOT 不变 = 仍在免费区 |
| 换更强算力卡会有用吗? | 看你在算力侧还是带宽侧 | 带宽侧换卡收效极小 |
| 量化能带来多少? | 字节减半即上限翻倍(按上式推导) | 经验上常拿到 1.4–1.8 倍,达不到 2 倍(经验法则,非引用数据) |
这张表和附录里的 0.5、0.6、3 倍、1.6 倍等阈值都是经验法则(非引用数据),而且口径不同:0.5 与 0.6 是「实测吞吐 ÷ 理论下限」,1.6 倍与 3 倍是同一个比值的倒数(0.625 与 0.33);读数时先把比较对象对齐,再谈结论。
最后给三条可以直接抄的做法:
- 把「TTFT」与「TPOT」拆成两个指标分别设阈值,不要用一个「平均响应时间」糊住;
- 把「每 token 搬运字节数」当成一等公民写进容量文档,任何新模型上线先填这一栏;
- 把「脊点对应的临界批大小」算出来贴在值班文档上,超过它再加批就是在拿延迟换吞吐。
这一节想让你记住的一件事:六个问题里只要有两个答不上来,说明你还没量过自己的瓶颈,先别做优化。
附录:四步手算模板(拿你自己的机器算一遍)
这套模板不需要跑任何 benchmark,纸笔五分钟就能判断你的服务该往哪儿优化。以下步骤全部是本文自行推导,只依赖上文的公开规格。
第一步:写下硬件的两个峰值,并算脊点。
例如一张 A100 80GB SXM:。
第二步:写下模型每 token 的搬运字节数(下限)。
70B fp16 就是 GB;把模型压到 int8,这个数直接减半,这是唯一能线性抬高 decode 上限的操作。
第三步:算出单请求的 decode 上限,和你实测的 TPOT 对比。
70B fp16 在 H100 上: ms,对应约 23.9 token/s。若你实测 60 ms/token,比值约 0.70,说明带宽利用率已经不错;若实测 400 ms/token,比值 0.10,问题多半在软件(碎片、没有融合、序列长度失控),而不是硬件不够。
第四步:检查批与上下文是否互相挤死。
式中的因子 4 = 2(K 与 V 各一份)× 2 字节(fp16 每个元素占两字节);若改用 int8,这个因子变成 2。代一组数字:70B 级算例(、)下,批 8、上下文 4,096 token 时 KV 约 GB,已经超过 70B 权重本身的 140 GB 的三分之一——这类服务必须先把 KV 管好,再谈加卡。
把它加上权重字节,与可用显存比较:如果 KV 占了大头,那么先动 KV(分页管理、共享 KV 头、压缩),而不是先加卡——因为加卡并不会让每张卡上的 KV 变小。
四步走完,你通常会落在三种结论之一:①利用率已经 0.6 以上,别再抠软件,该考虑量化或换更高带宽的硬件;②利用率低于 0.3 且 KV 占比不高,先查 kernel 与碎片;③KV 占比超过权重,先做 KV 管理与长度治理。三种结论对应的钱花在完全不同的地方。
这一节想让你记住的一件事:先算物理上限,再看实测比值——这个比值告诉你该花钱在硬件、软件还是容量治理上。
📋 来源与核验记录
本文所有硬件规格与实测数字均来自下列一手页面,本轮均实际打开核对;标注「本文自行推导」的算式为本刊根据公开规格计算,假设已在正文就地写明。
- Samuel Williams, Andrew Waterman, David A. Patterson,《Roofline: An Insightful Visual Performance Model for Floating-Point Programs and Multicore Architectures》,UC Berkeley 技术报告 UCB/EECS-2008-134,2008-10-17(operational intensity 定义、可达性能上界)——https://www2.eecs.berkeley.edu/Pubs/TechRpts/2008/EECS-2008-134.pdf
- NVIDIA A100 产品规格页(该页规格表只列 80GB PCIe / 80GB SXM 两档:FP16 Tensor Core 312 TFLOPS、带稀疏 624 TFLOPS;显存带宽分别为 1,935 GB/s 与 2,039 GB/s。A100 40GB 的 1,555 GB/s 见来源 5 的自注)——https://www.nvidia.com/en-us/data-center/a100/
- NVIDIA H100 产品规格页(H100 SXM:FP16 Tensor Core 1,979 teraFLOPS,页面脚注「* With sparsity」;显存带宽 3.35 TB/s)——https://www.nvidia.com/en-us/data-center/h100/
- Databricks,《LLM Inference Performance Engineering: Best Practices》,2023-10-12(prefill/decode 两阶段、MBU 与 MFU 定义、7B fp16 在 TPOT 14ms 时对应约 1TB/s 与 MBU 50%(该段是 MBU 定义的示例计算,原文注明忽略 KV cache,非实测)、批大小 64 时延迟 4 倍而吞吐 14 倍、批大小 1 时 2×H100-80GB(TensorRT-LLM、512 输入)MBU 约 60% 对 4×A100-40GB 约 55%、输入 512 token 批 1 时 MPT-7B 的 TTFT 46ms→26ms)——https://www.databricks.com/blog/llm-inference-performance-engineering-best-practices
- kipp.ly,《Transformer Inference Arithmetic》,2022-03-30(每 token 每参数约 2 FLOPs、A100 312 TFLOPS 与 1.5 TB/s 口径——其自注另称「本应保留 1.555」、KV cache 字节公式与 2MB/token 算例、16GB 对应约 8000 token)——https://kipp.ly/transformer-inference-arithmetic/
- Woosuk Kwon 等,《Efficient Memory Management for Large Language Model Serving with PagedAttention》(vLLM),arXiv:2309.06180(KV cache 碎片与重复、同等延迟下吞吐提升 2–4 倍)——https://arxiv.org/abs/2309.06180
- Tri Dao 等,《FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness》,arXiv:2205.14135(IO 感知与分块、减少 HBM 与片上 SRAM 之间的读写次数)——https://arxiv.org/abs/2205.14135
- NVIDIA Technical Blog,《Mastering LLM Techniques: Inference Optimization》(模型执行常常是权重侧的带宽受限、in-flight batching 的动机;无公开发布日期,本轮抓取核验时点 2026-10-22)——https://developer.nvidia.com/blog/mastering-llm-techniques-inference-optimization/
本文自行推导清单(便于复核):A100 40GB / 80GB SXM / 80GB PCIe 与 H100 SXM 的脊点(312/1.555、312/2.039、312/1.935、989.5/3.35,其中 H100 稠密值按官方带稀疏口径折半);decode 算术强度 (按每 token 2N FLOPs、fp16 权重 2N 字节);prefill 算术强度 ;70B fp16 在 H100 上 140GB/3.35TB/s≈41.8ms 与约 23.9 token/s;7B fp16 的约 239 token/s;KV cache 每 token 2·64·8192·2≈2.0MB;带 KV 项的 decode 算术强度修正式;单步时间取「算力项与带宽项最大值」的分段式。这些数字用于判断瓶颈方向,不用作 SLA 承诺;与实测的偏差范围见「边界与未解」一节。