第 2 章 显存账本
先修:第 1 章 · 深度:L2 · 预计阅读:30 分钟
一句话:一张卡能同时跑几条 8K 请求,是算出来的,不是试出来的。
一张 80GB 的卡,到底能放几条
二十个同事一起发 8K 合同,你手上是一张 80GB 的卡。
直觉是 80 除以 4,能放 20 条。一上线,OOM。
账有三笔:权重、缓存、余量。第一笔固定,第二笔随并发线性涨,第三笔最容易被忘。
第一笔:权重,不随请求数变
7B 参数按 FP16 存,每个参数 2 字节:
7 × 10⁹ × 2 = 14 GB ≈ 13 GiB
换成 INT8 是 7 GB,INT4 是 3.5 GB。这笔账只跟模型有关,来一个请求和来一百个请求,它都不动。
顺便把单位定死。GB 是十进制的 10⁹ 字节,GiB 是二进制的 2³⁰。差 7%,平时没感觉,卡在 45 GiB 对 48GB 的时候就要命。下面权重写 GB,显存账写 GiB,混着算的时候先统一。
第二笔:缓存,随并发线性涨
KV cache 的完整式子:
KV_bytes = 2 × L × H_kv × d_head × T × B × bytes_per_element
| 符号 | 含义 | 7B 例子 |
|---|---|---|
| 2 | Key 和 Value 各一份 | 固定 |
| L | 层数 | 32 |
| H_kv | KV 头数 | 32(GQA 时更少) |
| d_head | 每个头的维度 | 128 |
| T | 序列长度,输入加已生成 | 8192 |
| B | 同时处理的请求数 | 1 到 20 |
| bytes | FP16 是 2,INT8 是 1 | 2 |
一条 8K 请求:
2 × 32 × 32 × 128 × 8192 × 1 × 2 = 4,294,967,296 字节 = 4 GiB
二十条就是 80 GiB。单这一笔就超过了整张 80GB 卡。
换个结构,账立刻不一样。Llama-3-8B 参数量差不多,但只有 8 个 KV 头。同样 8K:
4 GiB × (8 / 32) = 1 GiB
二十条 20 GiB,放得下。参数一样,并发差四倍,差在头数。
再往上走一层。70B 级模型,80 层、8 个 KV 头、FP16:
2 × 80 × 8 × 128 × 8192 × 2 = 2.5 GiB 一条
缓存不是问题了,权重 140 GB 才是:至少要两张 80GB 卡,然后跨卡通信冒出来。
第三笔:余量
剩下的位置给三样东西:激活值、框架自己的开销、碎片。
推理不像训练那样要存一堆中间结果,但每层还是会临时分配几十上百 MB,并发越高越明显。框架本身也要占:vLLM 启动时按 gpu_memory_utilization(默认 0.9)先把 90% 的显存圈走,建成 KV 池。
按 80GB 算,账是这样的:
可用 72 GiB − 权重 13 − 余量 3 = 56 GiB
56 / 4 = 14 条
二十四 GB 的卡:
可用 22 − 13 − 3 = 6 GiB
6 / 4 = 1 条,剩 2 GiB
同样换成 8 个 KV 头的模型:
6 / 1 = 6 条
这就是为什么"24GB 能不能跑 7B"不是一个能直接回答的问题。要问的是哪一张 7B、多长上下文、几条并发。
为什么能加载不等于能服务
你在 llama.cpp 里加载 13 GiB 的模型,看到 model loaded,以为大功告成。那只是说权重放得下。
服务端还要回答另一个问题:缓存放哪、放多少。vLLM 一启动就会打印 KV cache 块数,这个数字才是你真正的并发上限。max_model_len 设成 32K,调度器就按 32K 给每个请求留位置,可用并发会直接掉下来——具体行为随版本变,这里只有 B 级证据,按你手上的版本核一遍日志。
算术强度:把"先撞哪堵墙"算出来
第 1 章说 decode 撞带宽、prefill 撞算力,那是个观察。这一节把它变成除法。
算术强度是每读一字节能做多少次浮点运算。模型的每步运算量除以读取字节数,就是它;硬件的峰值算力除以显存带宽,是机器的平衡点。前者小于后者,带宽受限;大于,算力受限。
decode 一步,生成一个 token:
运算量 ≈ 2 × 7 × 10⁹ = 14 GFLOP
读取量 ≈ 14 GB(权重)
算术强度 ≈ 1 FLOP / 字节
A100 的平衡点:
312 TFLOPS / 2,039 GB/s ≈ 153 FLOP / 字节
1 远小于 153,所以 decode 一定卡在带宽上,换更强的算力也没用。
prefill 一次处理 8K:
运算量 ≈ 2 × 7 × 10⁹ × 8192 ≈ 114 TFLOP
算术强度 ≈ 8,150 FLOP / 字节
8,150 远大于 153,算力受限。同一个模型,两段代码,两个结论。
什么时候这个账本会算错
五种情况,遇到了要重算。
量化只压权重。INT4 把 13 GiB 变成 3.5,但缓存还是 FP16 的 4 GiB。省下的 9.5 GiB 只能换两条半请求,不是四倍并发。
MoE 的账不一样。总参数很大,但每个 token 只走几个专家。权重必须全放,算力却按激活参数算,所以"参数量 ÷ 显存"这个除法在这里失效。
多卡张量并行。权重切开了,缓存也切开了,但每张卡还要留通信缓冲,实际可用的显存比纸面少。
前缀缓存命中。二十个人用同一个 system prompt,那部分缓存只存一份,实际占用低于估算。
输出长度不可控。"最大 512"是你写的,用户不遵守。按 512 算的账,遇到一个 4K 输出的请求就崩。
经验:先算三条线
拿到任何一张卡、任何一个模型,先算三行:权重多少、一条请求的缓存多少、余量留多少。
三行里错一行,batch 上限就是错的,后面调参全是在错的基线上调。这三行十分钟能算完,比压测一小时便宜。
练习
六道题,先闭卷,再翻 ANSWERS.md。
- 7B 模型用 GQA,8 个 KV 头,8K 上下文,20 条并发。缓存多少 GiB?
- 一张 24GB 卡,7B、32 个 KV 头、8K,最多几条并发?把三条式子写出来。
- 把权重压到 INT4 之后,24GB 卡的并发为什么没有涨四倍?
- 找错:"80GB 卡能放 20 条 8K 请求,因为 4 × 20 = 80。"
- 用第 1 章的 14 GB 权重和 A100 的 2,039 GB/s,算 decode 和 prefill 的算术强度,判断各撞哪堵墙。
- 起一次 vLLM,从启动日志里找到 KV cache 块数,用块数乘块大小,反算你这张卡能放几条 8K 请求。
这一章就记一句
显存账本三行:权重固定,缓存按 2·L·H_kv·d·T·B·字节 线性涨,余量留 2–4 GiB。80GB 卡配 7B、8K,实际只能并 14 条;24GB 卡只能 1 条。
真只记一句:并发上限是算出来的,不是试出来的;算错了,后面所有调参都是在错的基线上调。
下一章把这 14 条塞满:batching 怎么把权重读取摊薄,为什么总吞吐涨了、单条却更慢,以及尾延迟到底从哪里来。