24GiB显卡跑四路32K上下文:KV Cache显存账本与优化实战
你盯着日志看模型权重一点一点加载完长出一口气觉得一张 24 GiB 卡的活终于干完了。然后你顺手把上下文开到了 32K并发调成四路GPU 直接给你变红屏。这种场景我不止一次在本地部署的群里看到提问人往往会补一句“权重好不容易装进 24 GiB 了四路 32K 上下文还装得下吗”答案是大概率装不下而且问题不一定出在权重上。这个问题不仅折磨跑 LLM 的人下载 dinov3、sam3、yolov8 这类视觉大模型权重时同样会遇到——权重文件下得下来未必上得了卡。真正的显存杀手是运行时才产生的 KV Cache。这篇文章就把这笔账完整算一遍包括公式、实测量级、以及 24 GiB 卡上还剩下哪些腾挪手段。1. 先把账本摊开24 GiB 里挤着的可不只是权重1.1 显存预算的四个去处很多人以为显存占用就等于模型权重大小这是第一个误区。一个正在跑推理的进程显存里至少住着四类东西。第一是模型权重这个最直观。权重大小 参数量 × 精度字节数。比如 13B 参数FP16 精度下就是 13×226GB 十进制换算成二进制 GiB 约 24.2GiB正好对得上标题里的“24 GiB”。BF16 和 FP16 一样FP8 减半INT4 再减一半左右。第二是 KV Cache这是长上下文的隐形吞噬者。每个 token 在注意力计算中产生的 Key 和 Value 都要缓存下来供后续 token 查询而且只增不减直到这条序列结束。第三是中间激活值。前向计算时每一层会产出大量临时张量短文本时只有几百 MB一旦 prefill 一个几万 token 的超长 prompt峰值能冲到几个 GB。FlashAttention 和激活重计算能压一部分但框架为了保证吞吐通常会留出更多缓冲。第四是运行时开销。CUDA context 默认要吃几百 MBPyTorch 的缓存分配器会预留碎片空间驱动和桌面环境也要占一部分。很多人在 24 GiB 卡上把所有能设的参数都填满结果一启动就 OOM就是没给这部分留活路。1.2 为什么不能只盯着权重这一个数字权重是“固定房租”KV Cache 是“按人头按天数算的水电”。房租再便宜水电费也能把你耗破产。推理框架启动时通常会按gpu_memory_utilization参数划走一块显存比如 vLLM 默认 0.9也就是 24 GiB 卡先预留约 21.6GiB 给模型和 KV Cache剩下 2.4GiB 是框架和 CUDA context 的保底空间。我实际部署时习惯把gpu_memory_utilization调到 0.92 左右但不会超过 0.95。因为 I/O 队列、显存换页、驱动自身的临时分配都需要余量。换句话说真正能自由支配的预算往往只有 2122GiB。这么一算如果权重本身已经 24GiB那你连权重都塞不满更别提 KV Cache。所以“权重装进 24 GiB”这个说法通常意味着权重本身经过了一定压缩或者你用的模型权重本来就在 1416GiB 级别剩了七八个 GiB 给长上下文。这篇文章后面所有推演都以“权重已经成功装入 24 GiB 卡”为前提看 KV Cache 到底还能不能挤进去。2. 单路 32K 已不轻松KV Cache 的体积公式与实测量级2.1 KV Cache 是怎么一块砖一块砖盖起来的先解释一个最小但关键的概念在多头注意力里每个输入位置会算出 Query、Key、Value 三组向量。生成下一个 token 时新的 Query 要和之前所有 token 的 Key 做点积再对 Value 加权求和。所以历史位置的 Key 和 Value 必须留着不能算完就扔。这就是 KV Cache 存在的根本原因。KV Cache 的体积公式非常规整KV Cache 字节数 2 × 层数 × KV 头数 × 每头维度 × 序列长度 × 精度字节数这里乘 2 是因为 Key 和 Value 各一套。精度字节数在 FP16/BF16 下是 2FP8/INT8 下是 1。层数就是模型配置里的num_hidden_layersKV 头数则是num_key_value_heads。这里有个架构差异必须点出来。老式模型像 LLaMA-2 全系都是 MHAKV 头和注意力头一样多32 个头就存 32 份 KV。后来的 Llama-3、Qwen2.5 这类模型大多转向 GQAKV 头只有 4 个或 8 个KV Cache 直接缩到 MHA 的四分之一到八分之一。这也是为什么同一个 32K 上下文不同模型显存表现天差地别。2.2 五种主流模型架构的逐项测算直接套公式算几组真实数据比背结论更有用。下表按 FP16/BF16 精度计算单位全部取 GiB2 的 30 次方。模型代表层数KV 头数每头维度每 Token KV1K 上下文单路 32K四路 32KLLaMA-2-7B (MHA)3232128512KiB0.51664LLaMA-2-13B (MHA)4040128800KiB0.7825100Llama-3.1-8B (GQA)328128128KiB0.125416Qwen2.5-7B (GQA)28412856KiB0.0551.87Qwen2.5-14B (GQA)488128192KiB0.19624Qwen2.5-32B (GQA)648128256KiB0.25832这表拿出来很直观。单路 32K 这一个条件就已经把老式 MHA 模型逼到了墙角LLaMA-2-13B 单路 32K 的 KV Cache 是 25GiB比整张 24 GiB 卡还大。你就算把权重换成 4bit 量化、腾出 20 个 G单路都跑不起来。而 GQA 模型就从容很多。Llama-3.1-8B 单路 32K 只要 4GiBQwen2.5-14B 是 6GiB。这也是为什么我这两年给人推荐本地模型一律先看是不是 GQA老 MHA 架构在同代能力下已经没有任何长上下文性价比。2.3 容易被忽略的“输出也占额度”另一个常见的认知偏差是以为“32K 上下文”只算输入。实际上 KV Cache 按实际序列长度累计prompt 里的每个输入 token 和生成出的每个输出 token 都要缓存。假设一个请求输入 28K生成 4K那 KV Cache 按 32K 长度计算一视同仁。如果你输入 2K、输出 30K那实际 KV 量接近满窗口和“32K 全用于输入”的代价几乎一样。 所以评估能不能跑要看你业务里 prompt 和 generation 的分布而不是窗口上限。很多客服场景 prompt 固定 1.5K、输出 2K哪怕窗口开 32K实际 KV 只有 3.5K 的量远够不着天花板。3. 四路并发权重共用一份KV Cache 却要一人一桌3.1 连续批处理的显存共享逻辑四路并发在推理框架里就是 batch size 为 4四个不同请求同时走一遍前向。这里权重是共享的加载一份所有人都用这也是并发吞吐能上去的根本原因。但 KV Cache 不行。每个请求的序列内容不同KV 缓存自然各算各的。用自助餐厅打比方菜是公共的谁都能夹但人的胃容量是个人的吃了多少就得消化多少。权重是一个锅KV Cache 是四个人的碗。总显存需求基本是总显存 ≈ 权重 Σ(每个请求的实际序列长度 × 每 token KV) 中间激活 运行时注意这个 Σ 不是取最大值而是把所有并发请求的 KV 加起来。四路 32K就接近四份单路 32K 的 KV。3.2 最坏情况推演四路满载 32K 要吞多少显存现在回到标题的核心权重 24GiB、四路 32K24 GiB 卡到底行不行。我列三种典型情况。第一种LLaMA-2-13B FP16权重正好约 24GiB。四路 32K 的 KV 是 100GiB。加上权重和激活总需求超过 124GiB。这已经不是“能不能挤”而是物理上差了四倍。想都不用想。第二种Llama-3.1-8B BF16权重约 15GiB。四路 32K 的 KV 是 16GiB。加起来 31GiB超了。但如果把 KV Cache 精度降到 FP8KV 变 8GiB总需求 23GiB 左右勉强能塞进一台余量充足的机器。第三种Qwen2.5-14B如果权重用 FP8 压缩到约 14GiBKV 保持 BF16 是 24GiB总需求 38GiB超。KV 也降 FP8KV 变 12GiB总需求 26GiB还是超一点。这时候就得把四路降成三路或者换成 Qwen2.5-7B。从这三组数据能看出一个趋势目标“四路满载 32K”在 24 GiB 卡上只有 8B 级 GQA 模型配合 KV 量化才可能实现。权重本身到了 24GiB 的 13B 老 MHA连单路都没戏。3.3 平均序列长度才是日常救星上面算的是最坏情况四路全部满载 32K。真实业务里这种极端场景极少。绝大多数时候用户发过来的 prompt 几百到几千 token输出几百到一千 token平均序列长度可能只有 3~5K。拿 Qwen2.5-14B 举例。KV 每 token 192KiB四路平均 4K 的实际序列长度KV 总量是 3GiB。哪怕权重用 BF16 占 27GiB 放不下换成 FP8 的 14GiB 权重3GiB KV总共 17GiB四路 32K 窗口敞着跑也毫无压力。这就是“窗口开 32K”和“每路都跑满 32K”的本质区别。很多卡得刚刚好的部署靠的不是极限优化而是请求根本没到窗口上限。规划显存时先看统计分布不要被一个理论峰值吓死但压测时必须按峰值测这也是后面要说的经验。4. 把 KV Cache 挤进剩余显存的几张实战牌4.1 引擎选型PagedAttention 如何减少浪费传统推理框架给每个请求预分配一整块连续的 KV 空间哪怕当前只写了几百 token也按最大长度预留。长上下文场景下这种预留浪费非常严重。vLLM 的 PagedAttention 把 KV Cache 切成固定大小的页按需分配类似操作系统的虚拟内存。四路请求各用各的页碎片的概率大幅下降。实际使用时重点关注三个参数。gpu_memory_utilization决定给模型和 KV Cache 的总预算max_num_seqs是同时处理的序列数上限max_num_batched_tokens控制一次 prefill 最多吃多少 token调小一点能压低长 prompt 带来的激活峰值。一个可以参考的启动参数python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --gpu-memory-utilization 0.92 \ --max-model-len 32768 \ --kv-cache-dtype fp8 \ --max-num-batched-tokens 8192 \ --max-num-seqs 4 \ --enable-prefix-caching这套配置下 Qwen2.5-7B 的权重约 14.2GiB四路 32K 的 KV 在 FP8 下约 3.5GiB总占用约 18GiB剩余的给激活和运行时跑起来比较稳。4.2 KV Cache 精度降级从 2 字节压到 1 字节KV Cache 也用 FP8 或 INT8是最直接、见效最快的一招。同样一组模型KV 精度从 FP16 降到 FP8理论上 KV 显存直接减半。还是用上面的表。Llama-3.1-8B 四路 32K 从 16GiB 变成 8GiBQwen2.5-14B 从 24GiB 变成 12GiB。对于目的是“把四路塞进 24GiB 卡”的场景这 12GiB 的差距往往就是生死线。代价也客观存在。我做 RAG 场景压测时FP8 KV 在绝大多数任务上困惑度变化可以忽略但某些对数字敏感、需要逐位比对的生成任务上可能偶尔出现个别 token 偏差。如果你的应用容忍度低建议先在验证集上跑一轮对比再决定开不开。vLLM 里用--kv-cache-dtype fp8开启llama.cpp 则通过--cache-type-k q8_0 --cache-type-v q8_0控制。4.3 前缀缓存多路相似请求的共享大招如果四路请求共享相同前缀比如同一个 system prompt、同一份 RAG 参考文档框架可以缓存这只算一次后续请求直接复用。这省下的不是一点点。举个例子四个请求都询问同一篇 10K token 文档的不同问题前缀完全相同长度 10K。对 Qwen2.5-14B 来说每 token KV 是 192KiB10K 前缀的 KV 是 1.8GiB。没有前缀缓存四路各算各的仅前缀部分就消耗 7.2GiB开启缓存后这份 1.8GiB 只占一次四路额外只为各自新增的几个百 token 买单。RAG 场景下这个收益极其可观强烈建议部署时打开--enable-prefix-caching。4.4 权重侧腾挪最后几 GB 的抠法KV 优化的空间榨干后最后看一眼权重。如果权重正好卡在 24GiB 这条线上比如 13B 模型 FP16那降到 FP8 可以腾出 12GiB 给 KV再往下 GGUF Q6/Q4 还能更省但质量损失得自己权衡。不过我的建议是如果是老 MHA 架构模型不要在量化上死磕太多换个 GQA 模型架构省下的 KV Cache 远大于压缩权重省下的那几 GB。架构选对了后面每一步都轻松。5. 24 GiB 卡跑四路 32K 的最终判断与踩坑清单5.1 一张决策表看懂“可跑/不可跑”把前面的推演汇总成一张决策表组合条件是“权重装入 24GiB 卡目标四路并发且单路满载 32K”。这里的预算按 22GiB 可自由支配计算预留 2GiB 给运行时。方案权重占用KV(BF16)KV(FP8)总需求(BF16 KV)总需求(FP8 KV)四路 32KLLaMA-2-13B FP1624 GiB100 GiB50 GiB124 GiB74 GiB不可跑Llama-3.1-8B BF1615 GiB16 GiB8 GiB31 GiB23 GiBFP8 KV 可跑Qwen2.5-7B BF1614 GiB7 GiB3.5 GiB21 GiB17.5 GiB可跑Qwen2.5-14B FP814 GiB24 GiB12 GiB38 GiB26 GiB不可跑三路可Qwen2.5-32B INT419 GiB32 GiB16 GiB51 GiB35 GiB不可跑两路都紧张这张表再次说明那个核心结论24 GiB 卡上四路满载 32K 的现实边界基本划在 7B8B 级 GQA 模型。想上 14B 级别就得把并发降到三路或以下或者接受更短的输出长度。5.2 实测中容易翻车的四个细节第一别忘记 CUDA context 和桌面预留。有一回我自信满满配了gpu_memory_utilization 0.95结果 vLLM 启动就崩日志提示 CUDA out of memory。后来才发现是桌面环境占了几百 MB加上框架缓存把最后的余量吃没了。纯命令行部署不带桌面环境才能把利用率往上顶。第二长 prompt 的首 token 延迟极高。窗口 32K 不代表你要把 32K 一次喂进去max_num_batched_tokens设太大prefill 阶段激活值直接暴涨。我压测时遇到过 prefill 一个 32K 的请求瞬间吃掉 6GiB 中间激活的情况。安全做法是把max_num_batched_tokens压到 8192 或 4096让框架分批处理长 prompt。第三KV 量化后算子兼容性要验证。FP8 KV 在部分老算子路径上可能触发回退速度反而变慢。启动后看一眼日志里的 kernel 选择或者用两轮相同请求对比首 token 延迟和每秒生成 token 数别光看显存变少了就以为万事大吉。第四日志里的 KV Cache 利用率要常看。vLLM 启动后会打印KV Cache Size和Maximum CPU/GPU utilization等信息加上nvidia-smi实时观察实际占用。我发现很多 OOM 不是算错了预算而是请求峰值比统计平均高出一截预留空间不够直接被顶爆。压测时把并发拉满、把单路长度跑到接近窗口上限比什么理论估算都准。5.3 我自己的部署惯例这套问题我处理过很多次最后的操作路径基本固定先看模型架构是不是 GQA再按公式算每 token KV接着统计业务的平均和 P95 序列长度最后压测时把单路长度顶到窗口上限、并发调到目标值全程盯nvidia-smi和日志中的 KV 利用率。如果发现权重恰好卡在整卡边缘我会优先换 KV 精度而不是继续量化权重。KV 减半立竿见影而权重量化省下的空间经常一两个 G还要担质量风险。如果换了 KV 精度还差一口气就把并发从四路降到三路同时把max_num_seqs写死防止框架自行放大并发把显存吃掉。实测下来这种“峰值算一遍、余量留两成”的做法比任何花式优化都稳。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →