4、访存瓶颈分析:模型权重的显存占用、KV Cache的显存占用、访存带宽对延迟的影响
聊到大模型推理,很多人第一反应是“算力够不够”。
但我做了这么多年性能优化,说实话,真正卡脖子的往往不是算力,而是访存。你想想看,GPU算力这几年翻了多少倍?但显存带宽的增长速度,根本跟不上。这就导致了一个很尴尬的局面——计算单元经常在“等数据”。
这一节,我们就来掰扯清楚三个核心问题:
- 模型权重到底吃了多少显存?
- KV Cache又是怎么把显存撑爆的?
- 访存带宽这个“隐形天花板”怎么影响延迟?
4.1 模型权重的显存占用:一个简单的乘法
先说权重。这个其实很好算。
一个模型,参数总量是 N,每个参数用 B 个字节存储。那么权重占用的显存就是:
显存占用 = N × B
举个例子。LLaMA-7B,70亿参数。如果用FP16(2字节),那就是:
7 × 10⁹ × 2 = 14 GB
嗯,14GB。一张A100 80G还能扛得住。但如果是LLaMA-65B呢?
65 × 10⁹ × 2 = 130 GB
130GB,单卡根本放不下。这时候就得做模型并行,或者用量化。
关键点:权重占用的显存是“一次性”的。模型加载完,这块内存就固定了。它不会随着推理过程动态增长。
我个人习惯,在评估一个模型能不能部署时,第一件事就是算这个数。如果单卡放不下,后面的优化方案都得重新考虑。
4.2 KV Cache的显存占用:动态增长的“显存黑洞”
权重是固定的,但KV Cache不是。它随着序列长度动态增长。这才是真正的“显存黑洞”。
KV Cache存的是什么?说白了,就是Transformer解码时,每一层、每个注意力头产生的Key和Value矩阵。为什么要存?因为自回归生成时,后面的token需要跟前面所有的token做注意力计算。如果不缓存,每次都得重新算一遍,那计算量就炸了。
它的显存占用公式稍微复杂一点:
KV Cache显存 = 2 × 层数 × 注意力头数 × 序列长度 × 每个头的维度 × 字节数
注意前面那个“2”,是因为Key和Value各存一份。
我举个例子,大家感受一下:
| 模型 | 层数 | 头数 | 头维度 | 序列长度 | KV Cache (FP16) |
|---|---|---|---|---|---|
| LLaMA-7B | 32 | 32 | 128 | 2048 | 2 × 32 × 32 × 2048 × 128 × 2 = 1.07 GB |
| LLaMA-7B | 32 | 32 | 128 | 4096 | 2.15 GB |
| LLaMA-13B | 40 | 40 | 128 | 4096 | 3.36 GB |
看到了吗?序列长度翻倍,KV Cache也翻倍。而且这还只是一个请求的占用。如果同时服务多个用户,这个数字还要乘以并发数。
我曾经踩过的坑:有一次上线一个对话模型,测试时单请求延迟没问题。结果一上生产,并发一上来,显存直接OOM。查了半天,就是KV Cache没控制好。后来加了显存监控和动态batch调度,才稳住。
4.3 访存带宽:被忽视的延迟瓶颈
好,显存占用算清楚了。但还有一个更隐蔽的问题——访存带宽。
你想想看,GPU要计算一个矩阵乘法,首先得把数据从显存搬到计算单元(SM)里。这个搬运的速度,就是访存带宽。A100的显存带宽大约是2 TB/s,听起来很快对吧?但跟它的算力(312 TFLOPS)一比,就完全不够看了。
我给大家算一笔账:
假设我们要做一个矩阵乘法,输入是 [1, 4096],权重是 [4096, 4096]。这个计算需要读取权重:
4096 × 4096 × 2 = 33.5 MB
以A100的带宽来算,读取时间大约是:
33.5 MB / 2 TB/s ≈ 16.7 μs
而计算这个矩阵乘法,A100只需要大约:
2 × 4096 × 4096 / 312 TFLOPS ≈ 0.1 μs
看到了吗?访存时间比计算时间高了两个数量级。这就是典型的“访存瓶颈”。
核心结论:大模型推理,绝大多数算子是“访存密集型”的,而不是“计算密集型”的。延迟的瓶颈,不在计算,而在数据搬运。
4.4 一张图看懂访存瓶颈
下面这张SVG图,把整个访存瓶颈的逻辑串起来了。我建议你仔细看看,尤其是“权重读取”和“KV Cache读写”这两条路径。
4.5 怎么量化访存瓶颈?
光定性分析不够,我们得定量。这里有个常用的指标叫“计算访存比”(Arithmetic Intensity)。
公式很简单:
计算访存比 = 计算量 (FLOPs) / 访存量 (Bytes)
这个比值越高,说明算子越“计算密集”,GPU算力能充分发挥。比值越低,说明算子越“访存密集”,带宽就成了瓶颈。
我拿大模型里最常见的两个算子来对比:
| 算子 | 计算量 (FLOPs) | 访存量 (Bytes) | 计算访存比 | 瓶颈类型 |
|---|---|---|---|---|
| 矩阵乘法 (M=N=K=4096) | ~137 GFLOPs | ~67 MB | ~2048 | 计算密集 |
| Attention (S=2048, D=128) | ~0.5 GFLOPs | ~8 MB | ~64 | 访存密集 |
| LayerNorm (D=4096) | ~8 KFLOPs | ~16 KB | ~0.5 | 严重访存密集 |
看到了吗?LayerNorm这种小算子,计算访存比只有0.5。说白了,GPU大部分时间都在等数据,真正算的时间微乎其微。
我的建议:做性能分析时,别只看算力利用率。先看看“计算访存比”。如果这个值低于100,那大概率是访存瓶颈。这时候堆算力没用,得从减少访存入手。
4.6 实战:怎么测访存瓶颈?
理论说完了,来点实际的。我一般用NVIDIA的Nsight Systems来抓profile。重点关注两个指标:
- Memory Throughput:显存带宽的实际利用率
- Compute Utilization:计算单元的利用率
如果Memory Throughput接近峰值(比如A100的2 TB/s),但Compute Utilization很低(比如低于30%),那基本可以断定是访存瓶颈。
举个例子,我之前优化一个LLaMA-7B的推理服务,Nsight抓出来的数据是这样的:
Memory Throughput: 1.8 TB/s (90% of peak)
Compute Utilization: 15%
嗯,很明显。GPU的显存带宽快跑满了,但计算单元大部分时间在闲着。这就是典型的访存瓶颈。
后来怎么解的?做了两件事:
- 把权重从FP16量化到INT8,访存量直接减半
- 把多个小算子融合成一个,减少中间结果的读写
优化后,Memory Throughput降到了1.2 TB/s,但Compute Utilization提升到了45%。延迟反而降了40%。
核心思路:访存瓶颈的优化,不是让带宽跑得更快,而是让每次搬运的数据发挥更大的价值。说白了,就是“少搬数据,多算数”。
好了,这一节的内容就到这。访存瓶颈是个大话题,但核心逻辑其实不复杂。记住三个数:权重占多少、KV Cache占多少、带宽够不够用。把这三点理清楚,性能优化的方向就有了。