摘要:单卡 V100 32G 上,把上下文长度逐档往上探,vLLM 卡在 131K、llama.cpp 冲到 230K。附两个引擎的显存账本、五档实测和「启动通过 ≠ 能跑」的教训。
前两篇我写了「从 4 到 64 tok/s」和「vLLM 的逆袭」,聊的都是速度。上一篇结尾我预告要单独讲「V100 上的长上下文梯子实测」,这篇就是兑现。
但大模型本地部署还有一个比速度更硬、更让人抓狂的指标------上下文到底能喂多长。
很多人以为「长上下文」就是改个 max-model-len 或 ctx-size 数字的事。真上手才知道:V100 只有 32G 显存,而上下文是显存杀手。改大一个数字,轻则启动报错,重则跑一半 OOM 崩给你看。
这篇文章,我把同一块 V100 上两个引擎(vLLM 和 llama.cpp)的上下文极限逐档探了一遍顶,告诉你它们各自卡在哪、为什么、怎么撑到极限。
一、先算一笔显存账:上下文到底「贵」在哪
上下文为什么吃显存?一句话:每个 token 都要在显存里留一份 KV Cache(存已算过的键值,省得重复算)。上下文越长,KV Cache 越大。
但关键是------两个引擎的 KV「单价」差一倍:
| 引擎 | KV 量化 | KV 密度 | 说明 |
|---|---|---|---|
| vLLM | fp8_e5m2 |
~64 KB/token | 只有 16 层全注意力占 KV |
| llama.cpp | q4_0 |
~32 KB/token | 量化更狠,密度更高 |
vLLM 的 KV 为什么贵?这个模型 64 层里 48 层是 Gated DeltaNet(线性注意力,不占 KV),只有 16 层全注意力占 KV。fp8 密度下算出来 ≈ 64 KB/token。llama.cpp 用 q4_0 把 KV 再压一半。
这笔账决定了后面所有结果:vLLM 的 KV 单价是 llama.cpp 的两倍,同样显存下,它能撑的上下文天然只有一半。
再看权重:vLLM 的 AWQ-INT4 权重占 22.4 GiB,32G 的卡只剩 ~7--10 GiB 给 KV。llama.cpp 的 GGUF Q4 权重约 16 GiB,KV 空间更宽裕。
二、vLLM:逐档探顶,卡在 131K
先看 vLLM(1Cat-vLLM SM70 分支)在 V100 上的五档实测。每一档我都让它启动,再看能不能真的跑(util = GPU 显存利用率):
| 档位 | 上下文 | 配置(并发/MTP/util) | 结果 |
|---|---|---|---|
| ① | 262144 | 1 / off / 0.95 | ❌ 启动报 ValueError(KV 需 8.33 GiB,不够) |
| ② | 262144 | 1 / off / 0.97 | 启动 ✅ 但发长 prompt 运行时 OOM(返回 500) |
| ③ | 131072 | 2 / on / 0.95 | ✅ 5.46 GiB / 136K tok / 34.6 tok/s |
| ④ | 131072 | 2 / off / 0.95 | ✅ 7.71 GiB / 234K tok / 19 tok/s |
| ⑤ | 262144 | 1 / off(graph) / 0.95 | ❌ inductor autotune OOM |
两个血泪教训:
教训一:「启动通过 ≠ 能跑」。 档位 ② 最坑------KV 分配通过、健康检查 200,看着好好的,结果发一个 ~24K token 的长 prompt 直接崩:
torch.OutOfMemoryError: Tried to allocate 106 MiB ... 51.50 MiB free
根因是 util 0.97 把显存占得太满,prefill 长 prompt 的激活缓冲无处分配,EngineCore 崩溃返回 500。这就是为什么生产档定在 util 0.95。
教训二:长上下文必须关 CUDA graph。 档位 ⑤ 想用 graph 模式跑 262K,结果 graph 的 torch.compile/inductor autotune 要固定 ~2.4 GB workspace,长上下文 + 高 util 直接 OOM。长上下文只能 --enforce-eager。
还有一个绕不开的取舍 :MTP 投机解码(提速命脉)会吃 ~2.2 GB 显存。看档位 ③ 和 ④------开了 MTP,速度 34.6 tok/s,但 KV 容量从 234K 掉到 136K,并发余量从近 2 路掉到 1 路。速度 vs 上下文,只能二选一。
三、llama.cpp:靠 q4_0 冲到 230K
llama.cpp 走的是另一条路。它的 KV 用 q4_0(密度翻倍),而且是 slot 制 --------parallel 2 开 2 个独立 slot,每个 slot 上下文固定、互不挤压,硬保证。
它的上下文扩容是一步步「挤」出来的:
| 版本 | 模型 + 上下文 | 显存占用 | 剩余余量 |
|---|---|---|---|
| R4 | Q4_K_M + 131K/请求 | 25.0 GiB | ~7 GiB |
| R5 | Q5_K_M + 131K/请求 | 28.2~28.7 GiB | ~4 GiB |
| R6 | Q4_K_M + 200k×2 | 30.7 GiB | 1.08 GB |
| R7 | Q4_K_M + 256k×2 | 30.2 GiB | 1.58 GB |
| R8(当前) | Q4_K_XL + 230k×2 | 29.9 GiB | 1.83 GB ✅ |
后三档显存为 nvidia-smi 实测,MiB 已换算为 GiB。
最惊险的一次 :Q4_K_XL 想要更大上下文,试了 256k×2,结果长 prefill 最坏余量只剩 188 MiB------两路满上下文 prefill 并发就可能 OOM。最后退回 230k×2,用 -10% 上下文换回 1.83 GB 余量,而解码速度完全不变(上下文长度不影响 decode 带宽)。
llama.cpp 同样有「启动通过 ≠ 能跑」的版本:「启动 free ≠ 真实最坏余量」。首个长 prompt 会触发一次性的 prompt-cache bump(~+390 MiB),启动时看着够,一跑长 prompt 就可能不够。所以改完 ctx-size 一定要跑一次长 prefill 探针,看 bump 之后的真实余量。
四、四方对比:谁才是长上下文之王
把两个引擎摆一起,同任务、冷态、单路独占 GPU(数据来自同一轮规范化测试):
| 配置 | 上下文 | 端到端 tok/s | 并发 |
|---|---|---|---|
| vLLM MTP-on | 131K / seqs2 | 46.4 | ~1 路满额 |
| vLLM MTP-off | 131K / seqs2 | 18.5 | 近 2 路 |
| llama.cpp 256k×2 | 256k×2 | 53.1 | 2 路硬保证(余量 188 MiB ⚠️) |
| llama.cpp 230k×2 | 230k×2 | 54.6 | 2 路硬保证(余量 1.83 GB ✅) |
几个关键结论:
- llama.cpp 在长上下文上反超了 vLLM------单路解码 53~55 tok/s,比 vLLM MTP-on 的 46 还快 15~18%。上一轮 R3 还是 vLLM 快(35.4 vs 21.4),原因在于 llama.cpp 换原生 MTP 后 draft 接受率从 0.55 提到 ~0.79。
- 上下文长度不影响解码速度:256k vs 230k 打平(53.1 vs 54.6),差的只是显存余量。
- 并发模型本质不同 :vLLM 是动态分页 KV(
max-num-seqs=2是上限,实际能塞几路看 KV 池),llama.cpp 是 slot 制(2 个 slot 硬保证,互不挤压)。
五、总结
一句话:单卡 V100 上,要长上下文 + 稳定并发,选 llama.cpp(q4_0 KV,230k×2,余量 1.83 GB);要 vLLM 生态和动态调度,接受 131K 上限。
背后的物理规律就一条:KV 密度决定上下文天花板。fp8 KV(64 KB/token)和 q4_0 KV(32 KB/token)差一倍,这一倍就是「卡 131K」和「冲 230K」的全部差距。
还有一句掏心窝的话,和前面几篇一脉相承:改上下文数字之前,先算清楚显存账,再逐档探顶,别信「启动成功」------启动成功和「能跑长 prompt」之间,隔着一次 OOM。
写在最后
以上就是单卡 V100 上两个引擎上下文上限的逐档实测。如果你也在「改个数字就 OOM」的坑里挣扎过,这篇文章希望能帮你少走弯路。
我会持续更新大模型本地部署、推理引擎调优、AI agents 的实战内容,点个「在看」或分享给同样在折腾本地部署的朋友,就是对我最大的支持。
感谢各位关注,欢迎访问我的 GitHub 主页:https://fellow99.github.io/