1. 显存账本:24 GiB 到底能装下什么
先把结论摆在前面:24 GiB 显存跑四路 32K 上下文的 Qwen2.5,能不能装下,取决于你选的是哪个尺寸的模型,以及 KV Cache 用什么精度存。这不是一个"能"或"不能"的二选一问题,而是一道算术题。我见过太多人上来就问"24G 能不能跑 32K",然后被一句"看情况"打发走,其实这个"情况"是可以精确算出来的。
1.1 显存被谁吃掉了
一张 24 GiB 的卡(比如 4090、3090、L4 这类),显存开销大致分四块:
- 模型权重:这是死账,加载完就固定占着,跟上下文长度无关。
- KV Cache:这是活账,随并发数和上下文长度线性增长,也是本文的主角。
- 激活值与临时缓冲:前向计算时的中间张量,跟 batch size 和序列长度相关,通常几百 MB 到 1 GB 量级。
- 框架开销:CUDA context、通信缓冲、显存碎片等,vLLM 这类框架一般预留 1~2 GiB。
很多人算显存只算权重,结果一上并发就 OOM,问题就出在 KV Cache 这块活账上。权重是"房租",KV Cache 是"水电费",房租固定,水电费按用量走,你开四路 32K,等于四台空调同时开满。
1.2 权重这一项,Qwen2.5 各尺寸的占用
Qwen2.5 是个系列,从 0.5B 到 72B 都有。我们按常见的几个尺寸,用 FP16/BF16(2 字节)和 INT4 量化(约 0.5 字节,含量化开销实际按 0.55 估)分别算权重占用:
| 模型尺寸 | 参数量 | BF16 权重 | INT4 权重(估) | 24G 是否放得下 |
|---|---|---|---|---|
| Qwen2.5-0.5B | 0.5B | ~1.0 GiB | ~0.3 GiB | 轻松 |
| Qwen2.5-1.5B | 1.5B | ~3.0 GiB | ~0.9 GiB | 轻松 |
| Qwen2.5-3B | 3B | ~6.0 GiB | ~1.8 GiB | 轻松 |
| Qwen2.5-7B | 7B | ~14.0 GiB | ~4.2 GiB | BF16 勉强,INT4 宽裕 |
| Qwen2.5-14B | 14B | ~28.0 GiB | ~8.4 GiB | BF16 放不下,INT4 可以 |
| Qwen2.5-32B | 32B | ~64.0 GiB | ~19.2 GiB | 只有 INT4 勉强 |
这张表是后面所有推算的基础。注意 BF16 权重按"参数量 × 2 字节"算,7B 就是 7 × 2 = 14 GiB,这是纯权重,还没算任何缓存。所以标题里"权重装进了 24 GiB"这句话,本身就限定了模型尺寸——7B 的 BF16 权重 14 GiB 是能装进 24 GiB 的,14B 的 BF16 就装不下了。这是第一个分水岭。
1.3 为什么 KV Cache 是决定性的
权重是固定的,KV Cache 是弹性的,所以真正决定"四路 32K 装不装得下"的,是 KV Cache 的算法。KV Cache 的本质是:Transformer 在自回归生成时,每个 token 的 Key 和 Value 都要缓存下来,避免重复计算。序列越长,缓存越大;并发越多,缓存份数越多。
它的计算公式是:
KV Cache 大小 = 2 × 层数 × KV头数 × 头维度 × 序列长度 × 并发数 × 精度字节数其中 2 代表 Key 和 Value 两份,层数、KV 头数、头维度是模型结构决定的,序列长度和并发数是你的业务决定的,精度字节数是你能调的。这个公式后面会反复用到,建议先记住。
2. KV Cache 的精确计算:四路 32K 到底要多少
现在进入正题。我们以 Qwen2.5-7B 为例,因为它是 24 GiB 卡上最现实的 BF16 选择。先看它的结构参数。
2.1 Qwen2.5-7B 的结构参数
Qwen2.5-7B 的关键结构(基于公开模型配置的常见值):
- 层数(num_hidden_layers):28
- 注意力头数(num_attention_heads):28
- KV 头数(num_key_value_heads):4(这是 GQA,分组查询注意力)
- 头维度(head_dim):128
- 隐藏维度:3584
这里有个关键点:Qwen2.5-7B 用的是 GQA,KV 头数只有 4,而不是 28。这是它能省显存的根本原因。如果是传统的 MHA(KV 头数等于注意力头数 28),KV Cache 会大 7 倍,四路 32K 根本别想。GQA 让 KV 头数从 28 降到 4,直接砍掉 85% 的 KV 开销,这是现代大模型能长上下文部署的核心设计之一。
2.2 单路 32K 的 KV Cache 计算
套公式,单路 32K(32768 token)的 KV Cache:
每 token 每层的 KV 大小 = 2 × KV头数 × 头维度 × 精度字节 = 2 × 4 × 128 × 2(BF16) = 2048 字节 = 2 KiB再乘以层数和序列长度:
单路 32K KV = 2 KiB × 28 层 × 32768 token = 2 × 28 × 32768 KiB = 1,835,008 KiB ≈ 1.75 GiB所以单路 32K 的 KV Cache 约 1.75 GiB。这个数字很关键,记住它。
2.3 四路 32K 的总账
四路并发,每路 32K:
四路 KV = 1.75 GiB × 4 = 7.0 GiB现在把账合起来(Qwen2.5-7B,BF16 权重):
| 项目 | 占用 |
|---|---|
| 模型权重(BF16) | ~14.0 GiB |
| KV Cache(4 × 32K,BF16) | ~7.0 GiB |
| 激活与临时缓冲 | ~0.5 GiB |
| 框架开销 | ~1.5 GiB |
| 合计 | ~23.0 GiB |
23.0 GiB,卡在 24 GiB 的边上。理论上装得下,实际上非常危险。因为显存碎片、CUDA context 实际占用、vLLM 的 block 管理粒度(通常按 16 个 token 一块分配,会有浪费),实际占用往往比理论值高 5%~10%。23 GiB 的理论值,实际很可能冲到 24.5 GiB 以上,直接 OOM。
所以我的判断是:Qwen2.5-7B BF16 + 四路 32K BF16 KV Cache,在 24 GiB 卡上属于"纸面能过、实测悬"的状态,不建议直接上生产。
2.4 那怎么办:三个可行的调整方向
既然卡在边上,就得做取舍。有三条路:
第一条,降 KV Cache 精度。把 KV Cache 从 BF16 降到 FP8,KV 占用直接减半,从 7.0 GiB 降到 3.5 GiB。总账变成 14 + 3.5 + 0.5 + 1.5 = 19.5 GiB,宽裕多了。FP8 的 KV Cache 对生成质量的影响,在多数任务上几乎感知不到,这是性价比最高的一招。
第二条,降权重精度。用 INT4/AWQ/GPTQ 量化权重,7B 权重从 14 GiB 降到约 4.2 GiB。总账变成 4.2 + 7.0 + 0.5 + 1.5 = 13.2 GiB,四路 32K 轻松装下,甚至还能再开几路。代价是量化会带来一定的质量损失,需要评估你的任务能不能接受。
第三条,降并发或降上下文。如果业务上四路不是硬需求,两路 32K 的 KV 只有 3.5 GiB,总账 14 + 3.5 + 0.5 + 1.5 = 19.5 GiB,也能过。或者保持四路但上下文降到 16K,KV 减半到 3.5 GiB,同样能过。
| 方案 | 权重 | KV Cache | 总占用 | 可行性 |
|---|---|---|---|---|
| 7B BF16 + 4×32K BF16 | 14.0 | 7.0 | ~23.0 | 悬 |
| 7B BF16 + 4×32K FP8 | 14.0 | 3.5 | ~19.5 | 稳 |
| 7B INT4 + 4×32K BF16 | 4.2 | 7.0 | ~13.2 | 很稳 |
| 7B BF16 + 2×32K BF16 | 14.0 | 3.5 | ~19.5 | 稳 |
| 7B BF16 + 4×16K BF16 | 14.0 | 3.5 | ~19.5 | 稳 |
这张表基本就是这道题的答案。如果你问我推荐哪个,我会选 FP8 KV Cache,因为它对质量影响最小,改动成本也最低。
3. vLLM 里怎么把这些参数落地
算清楚了账,接下来是怎么在 vLLM 里把它配出来。vLLM 是目前部署这类模型最常用的推理框架,它的显存管理核心是 PagedAttention,把 KV Cache 切成固定大小的 block 来管理,这也是它能高效利用显存的原因。
3.1 关键启动参数逐个说
一个典型的 vLLM 启动命令长这样:
vllm serve Qwen/Qwen2.5-7B-Instruct \ --max-model-len 32768 \ --gpu-memory-utilization 0.92 \ --max-num-seqs 4 \ --kv-cache-dtype fp8 \ --tensor-parallel-size 1 \ --port 8000逐个解释这些参数为什么这么设:
--max-model-len 32768:单条请求的最大上下文长度,设成 32K。这个值直接决定单路 KV 的上限,设大了浪费,设小了截断。--gpu-memory-utilization 0.92:vLLM 允许占用的显存比例。默认 0.9,我一般设 0.92~0.95。设太高容易和系统其他进程抢显存,设太低浪费。24 GiB 卡上 0.92 大约留 2 GiB 给系统和碎片。--max-num-seqs 4:最大并发序列数,这就是"四路"的来源。vLLM 会按这个值预留 KV Cache 空间。--kv-cache-dtype fp8:KV Cache 用 FP8 存,这是省显存的关键开关。注意不是所有卡都支持 FP8,需要确认你的 GPU 架构(Ada、Hopper 及更新的一般支持)。--tensor-parallel-size 1:单卡,不切分。
3.2 gpu-memory-utilization 到底怎么定
这个参数是新手最容易踩坑的地方。它的含义是"vLLM 最多用掉多少比例的显存",vLLM 会先加载权重,然后把剩余显存按这个比例拿来做 KV Cache。
假设 24 GiB 卡,权重占 14 GiB,系统和其他占 2 GiB,剩 8 GiB。如果gpu-memory-utilization设 0.92,vLLM 认为自己能用 24 × 0.92 = 22.08 GiB,减去权重 14 GiB,剩 8.08 GiB 给 KV Cache 和激活。四路 32K 的 KV 需要 7 GiB(BF16)或 3.5 GiB(FP8),FP8 下 8.08 GiB 是够的,BF16 下就非常紧。
提示:
gpu-memory-utilization不是越高越好。设 0.95 以上时,如果系统里有其他进程(比如监控、日志)也在用显存,很容易触发 OOM。我一般留 5%~8% 的余量。
3.3 怎么确认 KV Cache 实际分了多少
vLLM 启动时会在日志里打印 KV Cache 的分配情况,类似:
INFO: GPU KV cache size: 262,144 tokens INFO: Maximum concurrency for 32,768 tokens per request: 8.00x这行日志极其重要。Maximum concurrency就是"在 32K 上下文下最多能同时跑几路"。如果它显示 8.00x,说明你的显存能支持 8 路 32K,四路绰绰有余;如果显示 2.00x,那四路就会排队甚至拒绝。
我每次部署完第一件事就是看这行日志。它比任何理论计算都准,因为它是 vLLM 根据实际可用显存算出来的。如果这个值小于你的目标并发,就得回去调参数——降 KV 精度、降权重精度、或者降max-model-len。
3.4 一个容易忽略的坑:block 粒度浪费
vLLM 的 PagedAttention 按 block 分配 KV Cache,默认 block size 是 16 个 token。这意味着即使你只用了 1 个 token,也会占一个 block(16 token 的空间)。对于长上下文,这个浪费可以忽略;但对于大量短请求,浪费会累积。
如果你的场景是"少量长请求 + 大量短请求"混合,可以考虑调--block-size,但一般不建议动,默认值在多数场景下是最优的。真正要关注的是max-num-seqs和max-model-len的乘积——它决定了 KV Cache 的峰值需求。
4. 实测踩坑与排查实录
理论算得再漂亮,实测总会给你惊喜。下面是我在实际部署中遇到过的几个典型问题,以及排查思路。
4.1 启动就 OOM:权重都加载不进去
现象:vLLM 启动时直接报torch.cuda.OutOfMemoryError,连权重都没加载完。
原因:多半是模型尺寸选错了。比如在 24 GiB 卡上加载 Qwen2.5-14B 的 BF16 权重(28 GiB),必然 OOM。
排查:先确认模型权重的实际大小。看模型目录下*.safetensors文件的总大小,或者用du -sh看。BF16 权重约等于参数量 × 2 字节,7B 约 14 GiB,14B 约 28 GiB。
解决:换更小的模型,或者用量化版本(GPTQ/AWQ/INT4)。24 GiB 卡上,BF16 最多到 7B,INT4 可以到 14B。
4.2 启动成功但一请求就 OOM
现象:服务起来了,日志显示 KV Cache 也分了,但一发长请求就 OOM。
原因:max-model-len设得比实际 KV Cache 能支撑的长。比如你设了 32K,但显存只够分 20K 的 KV,请求一超过 20K 就崩。
排查:看启动日志里的Maximum concurrency for 32768 tokens per request。如果这个值小于 1,说明单路 32K 都跑不了。
解决:降max-model-len,或者降 KV 精度,或者降权重精度。三者选其一或组合。
4.3 并发上不去:请求排队严重
现象:服务不崩,但请求响应很慢,日志里Running: 4, Waiting: 20,大量请求在排队。
原因:max-num-seqs设小了,或者 KV Cache 不够支撑更多并发。vLLM 的调度器(scheduler)会优先跑能放进 KV Cache 的请求,放不下的就排队。
排查:看日志里的Running和Waiting数量。如果Waiting持续很高,说明并发能力不足。
解决:如果显存还有余量,调大max-num-seqs;如果显存不够,降 KV 精度或权重精度来腾空间。注意max-num-seqs调大后,KV Cache 需求也线性增长,要重新算账。
4.4 FP8 KV Cache 开了但没生效
现象:设了--kv-cache-dtype fp8,但显存占用没降。
原因:可能是 GPU 不支持 FP8,vLLM 静默回退到了 FP16/BF16。或者参数名写错了(不同 vLLM 版本参数名可能有差异)。
排查:看启动日志里有没有Using FP8 KV cache之类的提示。如果没有,就是没生效。
解决:确认 GPU 架构支持 FP8(Ada/Hopper 及更新)。如果不支持,只能走量化权重这条路。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决 |
|---|---|---|---|
| 启动即 OOM | 权重太大 | 看权重文件大小 | 换小模型或量化 |
| 请求即 OOM | max-model-len 过大 | 看 KV Cache 日志 | 降上下文或降精度 |
| 并发上不去 | max-num-seqs 小或 KV 不足 | 看 Running/Waiting | 调大并发或降精度 |
| FP8 没生效 | GPU 不支持或参数错 | 看启动日志 | 确认架构或改量化 |
| 显存碎片多 | 长时间运行 | 看显存占用曲线 | 定期重启或调 block-size |
注意:vLLM 的版本差异比较大,参数名和默认值在不同版本间可能变化。部署前一定先看对应版本的文档,别照搬旧教程。我踩过好几次"参数名对了但版本不认"的坑。
5. 几个延伸思考:不只是 7B 和 32K
这道题的本质是"显存预算分配",模型和上下文只是变量。把这套算法吃透,你可以套用到任何组合上。
5.1 换模型怎么算
换任何模型,只要拿到四个数:层数、KV 头数、头维度、精度,就能算 KV Cache。比如 Qwen2.5-14B,层数 48,KV 头数 8,头维度 128,单路 32K 的 KV 就是:
2 × 8 × 128 × 2 字节 × 48 层 × 32768 token = 2 × 8 × 128 × 2 × 48 × 32768 字节 ≈ 6.4 GiB四路就是 25.6 GiB,光 KV 就超过 24 GiB 了,所以 14B 在 24 GiB 卡上跑四路 32K,BF16 权重下完全不可能,必须量化权重 + FP8 KV。
5.2 换精度怎么算
KV Cache 精度从 BF16 到 FP8,占用减半;到 INT8,也减半;到 INT4,减到四分之一。但精度越低,生成质量损失越大。FP8 是目前性价比最高的选择,INT4 KV 在多数任务上已经开始能感知到质量下降了。
5.3 换硬件怎么算
如果是 48 GiB 卡(比如 A6000、L40S),预算翻倍,7B BF16 + 四路 32K BF16 就非常宽裕了,甚至能上 14B。如果是 80 GiB 卡(A100/H100),14B BF16 + 四路 32K 也没问题。显存预算翻倍,能玩的组合就多一个数量级。
5.4 一个反直觉的点:并发不是越多越好
很多人觉得并发越高吞吐越大,其实不然。当 KV Cache 接近显存上限时,vLLM 的调度器会频繁做 block 的换入换出,反而拖慢整体吞吐。找到"显存刚好用满但不溢出"的并发数,才是吞吐最优点。这个点通常比理论最大并发略低一点,需要实测调。
我在实际部署中的体会是,与其纠结"四路 32K 到底装不装得下",不如先把 KV Cache 的账算清楚,然后根据业务对质量、并发、上下文长度的优先级做取舍。多数情况下,FP8 KV Cache 是那个"既省显存又不太伤质量"的甜点选项,值得优先试。最后分享一个小技巧:部署完先别急着压测,用nvidia-smi盯着显存曲线跑几轮真实请求,看峰值占用和稳态占用差多少,这个差值就是你真正的安全余量。