算力上限和带宽瓶颈这两个词,最近在 AI Infra 的讨论里出现的频率非常高。我刚接触大模型推理加速时,最大的困惑不是“怎么调参数”,而是连一个基本问题都答不上来:这块 GPU 到底能榨出多少 token?为什么看着算力有几百 TFLOPS,跑起 7B 模型却常常只有一两百 token/s,差距大到让人怀疑表格写错了。
后来我把注意力从“参数调优”转回“量化估算”,发现只要用三个公式,就能把大模型的推理上限、带宽瓶颈和并发饱和点算得明明白白。这篇文章想把这套方法分享出来,顺便结合我最近在看的 nano-vllm 这类教学项目,做一个从公式到实测的对照思路。适合刚开始做模型部署、搞推理服务选型,或正在排查 GPU 利用率不高的同学。
1. 先搞清楚算力上限:模型参数和 FLOPs 之间的关系
1.1 理论峰值和实际可用算力之间,隔了一个“每 token 成本”
看 GPU 规格表的时候,H100、A100 的 TFLOPS 数字都很吓人,但那是跑矩阵乘法的峰值,还是靠 fp16/fp8 才能摸到的理论值。真正的大模型推理,每个 token 要经过一层又一层 Transformer,每层都要对权重矩阵做矩阵乘法,这个计算量可以直接和模型参数量挂钩。
业界一个很常用的近似:Transformer 推理每生成一个 token,前向传播的 FLOPs 约为参数总量的 2 倍。也就是:
FLOPs_per_token ≈ 2 × PP 是模型参数量。为什么是 2 而不是 3 或者 6?训练一个 token 大概需要 6P FLOPs,因为要算前向、反向还有参数更新;推理只做前向,每个参数大概对应一次乘加操作,一次乘法一次加法就是 2 FLOPs。LayerNorm、激活函数、attention 这些也会消耗一点算力,但在整体比重里很小,入门阶段先忽略,误差通常在 10% 以内。
有了这个漏斗,第一个公式就出来了。
1.2 公式一:GPU 上单流解码的“算力天花板”
T_compute = F / (2 × P)F 是 GPU 的可用浮点算力,单位 FLOPs/s;P 是模型参数量;T_compute 就是假设完全受算力约束时,每秒最多能解码的 token 数。
拿一张 A100 80GB 来算笔账。A100 在 FP16/BF16 下大概有 312 TFLOPS,也就是 312 × 10^12 FLOPs/s。模型用 7B 参数,那么:
- FLOPs_per_token = 2 × 7 × 10^9 = 14 × 10^9
- T_compute = 312 × 10^12 / 14 × 10^9 ≈ 22285 token/s
两万多的 token/s,看起来很夸张。但真去跑一个单请求,实际根本到不了这个数,甚至差两个数量级。问题不在于算力不够,而在于推理过程中还有一个比算力更先卡住你的东西:显存带宽。
2. 真正的拦路虎:HBM 带宽瓶颈
2.1 权重在显存里躺着,每个 token 都要重新“背”一遍
Transformer 推理和传统 DL 推理最大的区别是自回归:生成了上一个 token,才能算下一个 token。每生成一个 token,都要把权重从显存里完整读一遍。权重不能一直放在 GPU 的 SRAM 里,因为 SRAM 只有几十 MB,模型权重有几十 GB,必须放在 HBM 里反复读取。
这个“反复读取”就是带宽瓶颈的来源。GPU 的理论算力再高,如果数据从 HBM 到计算单元的速度跟不上,核心就只能干等。
公式二写出来非常直白:
T_bandwidth = HBM_BW / (P × bytes_per_param)P × bytes_per_param 就是模型权重在显存里的字节数。FP16/BF16 下每个参数占 2 字节,FP8 占 1 字节,INT8 也是 1 字节。
回到刚才的例子。A100 80GB 的 HBM 带宽大约在 2TB/s 这个量级。7B 模型如果按 FP16 部署,权重占 14GB:
- T_bandwidth = 2000 × 10^9 / (7 × 10^9 × 2) ≈ 142 token/s
你没看错,单 Token 解码的带宽上限就是一百多 token/s。这和很多人在 A100 上跑 7B 模型测到的单请求性能几乎一致。此时 GPU 算力虽然还有两万 token/s 的潜力,但权重搬运速度只允许它每秒处理一百多个 token。
这也解释了一个部署中很常见的现象:把模型从 FP16 换成 INT8 后,吞吐能接近翻倍。因为每个参数从 2 字节变成 1 字节,HBM 需要搬运的数据少了一半,带宽瓶颈自然宽了一倍。所以做推理优化,不要一上来就盯着 kernel 调优,先看看你用的精度合不合理。
2.2 KV Cache 也要吃带宽:长上下文下不能忽略
权重是第一个带宽大户,KV Cache 是第二个。生成 token 时,Key 和 Value 向量要不断写入显存,下一轮计算还要把它们读出来参与 attention。KV Cache 的大小和网络结构强相关。
简化来看,单条序列每个 token 占用的 KV Cache 字节数大约是:
KV_bytes_per_token = 2 × L × kv_heads × head_dim × dtype_size其中 L 是层数。举个例子,一个近似 LLaMA 风格的 7B 模型,32 层、hidden size 4096、32 个 KV head、每个 head 的维度是 128,那么:
- 每个 token 的 KV = 2 × 32 × 32 × 128 × 2 = 512KB
这个数字细想很吓人。2048 token 的上下文,KV Cache 就要 1GB。要是并发处理几十条长序列,KV Cache 会把显存吃得非常快。这也是为什么 vLLM 要专门做 PagedAttention 和 KV Cache 管理,而不是让每个请求随便分配一整块连续显存。
KV Cache 对带宽的影响在单请求场景下不明显,因为 512KB 和 14GB 权重比起来实在太小;但当 batch 很大、上下文很长时,KV Cache 的总读写量会逐渐追上来,甚至成为新的瓶颈。后面算并发临界点的时候,也要把这一项纳入考虑。
2.3 把两个公式放一起:A100 跑 7B 的真实基调
对比一下两个上限:
- 算力上限:约 22285 token/s
- 带宽上限:约 142 token/s
两者差了 150 倍。单请求解码的瓶颈几乎由带宽决定,测出来快接近 142 token/s 就是很健康的状态。如果连这个数都差得远,那问题不在模型或框架,而在服务链路、kernel 启动开销、batch 策略这些工程细节。
不过这里有个容易误导人的点:T_bandwidth 是单 Token 流上的极限。真实推理服务不会只服务一个用户,GPU 同时处理很多请求时,算力上限会重新变成主角。
3. 第三个公式:要填满 GPU 算力,到底需要多少并发
3.1 从“带宽受限”切到“算力受限”的临界批量
decode 阶段每个 step,服务端要处理一批 token,每个 token 都要做一次完整的权重矩阵乘。假设 batch size 是 B,那么这一步的总计算量是 B × 2P;而权重读取量只有一份 P × bytes_per_param,因为多个请求共用同一份权重,HBM 只需要把权重搬运一次,但计算单元要针对每个请求都用一次。
所以随着 batch 变大,计算时间线性增长,内存搬运时间却基本不变。总不至于永远被带宽卡住,总有某个 batch size 让计算时间大于内存搬运时间。让两者相等时的批量大小,就是临界点:
B* = (F × bytes_per_param) / HBM_BW这个公式很有意思:它等于 GPU 的“算术强度”乘以字节大小,是硬件本身的性质,和模型参数量的关系反而不大。同一个 GPU 上,7B 和 70B 模型的临界批量基本相同,因为计算量和搬运量都随参数量同步增长。KV Cache 会在 batch 很大时改变这个结论,但作为一阶估算够用。
拿 A100 80GB 继续算。F = 312 × 10^12,BW = 2000 × 10^9,FP16 下 bytes_per_param = 2:
B* = 312e12 × 2 / 2000e9 ≈ 312也就是说,大概 312 路并发请求同时 decode 时,计算时间和权重搬运时间才打平;在此之前,GPU 的算力是闲置的。这也解释了为什么推理服务必须做 continuous batching 而不是简单地并发几个线程:十几路请求的并发下,GPU 依然在等显存,根本触不到算力天花板。
3.2 把 KV Cache 加进临界批量,结论会怎么变
上面的 B* 只考虑了权重。如果把 KV Cache 的读写算进去,decode 阶段每 step 的内存搬运量大致是:
Mem_per_step = P × bytes_per_param + B × KV_bytes_per_token × 2乘以 2 是因为每个 token 的 KV 要先读旧值,再写新值。带入临界条件:
B*_kv = (F × bytes_per_param) / (HBM_BW - B × KV_bytes_per_token × 2 / 时间步)实际计算不用这么绕,更简单的方法是把 KV 读写量当成附加项,直接加在内存时间上迭代估算。用上面那个 512KB/token 的例子,300 路并发时 KV Cache 读写量大约 300MB,和 14GB 权重比还差得远,所以不会显著改变 312 这个量级。但如果你跑的是 70B 模型、长上下文、4096 并发,KV 读写就可能变成和权重等量级的存在。
3.3 用 nano-vllm 观察“并发如何吃掉空闲算力”
公式算到这里,下一步自然是动手验证。我最近在看 nano-vllm 这类专为学习而做的项目,它把 vLLM 里的核心机制抽出来做成最小实现,包括 PagedAttention、continuous batching、KV Cache 管理这些,代码量比 vLLM 本体小得多,特别适合对着源码看关键功能。
在 nano-vllm 里做验证,可以朴素一些:先用 7B 模型跑单请求,记录 decode 的 token/s,应该接近 T_bandwidth;然后逐渐增加模拟并发序列,观察总吞吐曲线。你会发现:
- 并发从 1 加到 32,总吞吐缓慢上升,但单路 token/s 明显下降
- 并发继续加到 200~300,总吞吐开始逼近 T_compute 的某一个百分比
- 超过一定并发后,KV Cache 显存先不够用,总吞吐不再线性增长
这个过程比看任何论文都直观。nano-vllm 这样的项目适合把公式里每个变量和代码里的每个数据结构对上号,比如 block table 对应 KV Cache 的分页管理,continuous batching 对应调度器里的队列状态。
4. 实操中怎么用这三个公式做容量预估和压测对照
4.1 一张表快速算清典型模型部署
把公式换算到常见模型规模上,可以产出一张非常实用的估算表。前提都按 A100 80GB、FP16/BF16 精度、带宽 2TB/s、算力 312TFLOPS 来算。7B 模型权重约 14GB,13B 约 26GB,70B 约 140GB,后面两个在大 Batch 下还要考虑张量并行。
| 模型规模 | 权重大小 | T_bandwidth | T_compute | 显存可容纳的 KV 预算 |
|---|---|---|---|---|
| 7B | 14GB | 约 142 token/s | 约 22285 token/s | 比较宽裕,单卡可跑长上下文 |
| 13B | 26GB | 约 76 token/s | 约 12000 token/s | 单卡还剩 50GB 左右,看 KV head 数量 |
| 70B | 140GB | 需多卡,单卡约 1/8 权重,带宽瓶颈不变 | 约 2228 token/s | 单张卡都不够放,必须多卡并行 |
注意 70B 那一行有个新手容易搞错的地方。用张量并行把 140GB 权重切到 8 张卡上时,每张卡只读 1/8 的权重,但每张卡也只用 1/8 的带宽口径,所以单序列的 token/s 不会因为多卡而变成 8 倍。真正被多卡并行放大的是显存容量,让 70B 模型能跑起来。
4.2 压测前先做的几个动作
- 第一步:确认模型精度和参数。用
model.get_memory_footprint()这类接口直接看权重的实际显存占用,别用手算的参数猜。 - 第二步:确认 GPU 的 HBM 带宽。不同型号差异很大,A100 约 1.9~2.0TB/s,H100 约 3.35TB/s,消费级卡更小,不能拿旗舰规格乱套。
- 第三步:把公式算出来的数字写进压测预期里。比如 7B FP16 跑 A100,单请求 decode 低于 120 token/s,先查服务本身,别怀疑 GPU 不行。
实测中很难看到纯带宽极限,因为每次 kernel 启动、CUDA graph 捕获、调度器切换都会有开销。常规的经验是:能把理论 T_bandwidth 的 70%~90% 跑出来,已经很好了;跑到 90% 以上,基本可以认为链路优化到位。
4.3 利用 CUDA 工具验证瓶颈位置
如果压测结果和公式差很多,用 nsight compute 抓一下 kernel 就很清楚了。关注两个指标:
- SM 利用率:如果很低,但 Memory Throughput 接近 100%,说明是明显的带宽受限,继续增大 batch 或降低精度才有收益。
- SM 利用率很高,但吞吐没有明显增长:说明已经接近算力上限,该考虑换更小模型或量化方案。
我在 nano-vllm 这类项目里常用的方式是先写一个只测 GEMV 的小 kernel,单独看看权重读取能达到多少 GB/s。用这个数字代替理论带宽,公式会贴合现实得多。很多时候瓶颈不是 HBM 本身,而是 kernel 没有把数据搬到位,比如未对齐访问、小的 tile size、过多的 launch overhead。
5. 常见问题与排查技巧实录
5.1 为什么压测值比公式低很多
这是问得最多的场景。先说结论:公式算的是理想上限,不是现实目标。如果你压测单请求 decode 只有 60 token/s,而公式给出 142 token/s,大概率是下面几种情况。
一是把 prefill 和 decode 混在一起统计了。首次生成之前要处理提示词,prefill 阶段计算密度高,但整体耗时也不小。如果压测脚本把“接收第一个 token 的时间”也算进总吞吐,数字会难看。正确做法是把 TTFT 和单 token 延迟分开计,只统计 decode 阶段的稳定输出速率。
二是 batch 策略太保守。很多部署框架默认限制了最大并发数。如果同时只有几路请求,GPU 一直处于带宽受限的等待状态,算力完全闲在那里。把 continuous batching 打开,或者手动提高 max-num-seqs,再看总吞吐变化。
三是 KV Cache 显存碎片导致的调度阻断。vLLM 的 PagedAttention 用固定块管理 KV Cache,但每块大小、preemption 策略、max-model-len 设置都会影响实际并发能力。如果显存有空间但总是 OOM,优先检查 max-model-len 和 KV 块大小。
5.2 为什么并发增加到一定程度后,总吞吐反而不涨
这是典型的从带宽受限切换到算力受限的信号。A100 跑 7B 的临界批量大概在 300 附近,当 batch 在这个量级附近时,算力开始饱和。再往上加并发,单条序列能分到的算力被摊薄,总吞吐会进入平台期。
如果平台期的总吞吐远低于 T_compute 的预期,需要检查 attention kernel 的实测性能。nano-vllm 里最值得研究的就是 attention 和 GEMV 这两个算子,因为它们一个负责 KV Cache 搬运,一个负责权重读取。把这两个 kernel 用 ncu 测透,基本就能定位性能上限在哪里。
5.3 三个公式的适用范围和局限
这套公式适合做容量预估和瓶颈定位,不适合替代完整性能建模。它假设模型权重能被完整放进显存,假设计算单元能持续跑满,假设 HBM 带宽恒定。真实系统里还有 CUDA graph 的捕获开销、显存不足触发 offload、多机通信等因素,都会让实际值偏离。
另外,量化对公式影响很大。FP8 或 INT8 下 bytes_per_param 变成 1,T_bandwidth 直接翻倍,B* 减半。如果模型做了 4bit 量化,字节数变成 0.5,那带宽上限还能再翻一倍。这不代表量化没有代价,准确率损失和反量化开销也要一起权衡。
我在实际项目里已经养成习惯:任何模型部署前,先花十分钟把三个公式算一遍,再决定精度、并发上限和压测目标。踩过几次坑之后发现,90% 的“性能差”其实在跑压测之前就能用公式预判出来。公式不会帮你把 GPU 从 60分 优化到 100分,但它能让你心里有数,知道到底应不应该继续做 kernel 层面的优化,还是换个精度、换个模型、换张卡更划算。