前阵子帮团队把一套基于 8B 开源模型的服务化推理部署到一张 RTX 4090 上显存就 24 GiB任务要求说起来很简单模型权重得装进去同时还要服务 4 路并发请求每一路都完整支持 32K 上下文。我一开始觉得这配置很宽裕——8B 模型 FP16 权重也就 16 GB 出头24 GiB 怎么都够了吧结果第一轮压力测试就狠狠打了脸权重装进去只是第一步KV Cache 才是那个真正把显存吃干抹净的隐形大户。这篇文章就围绕“权重装进了 24 GiB四路 32K 上下文还装得下吗”展开。我会把显存账单拆开逐项计算给出 KV Cache 的手算公式和真实数字再对比权重量化、KV Cache 量化、推理框架调度这几条可行路线最后附上我实测通过的 vLLM 配置参数和几段踩坑记录。适合谁看手头只有一张 24 GB 卡、想跑 7B/8B 模型对外服务的同学以及正被 OOM 折磨、想知道显存到底被谁吃掉的同行。1. 先算总账24 GiB 被谁分走的1.1 显存里的四张账单很多人对“显存占用”的理解停留在模型权重上实际上推理服务在 GPU 上花的钱至少有四笔模型权重静态占用加载后常驻FP16 下约等于参数量 × 2 字节。KV Cache随并发数和上下文长度动态增长是目前服务化部署里最大的变量。激活值与临时缓冲区前向计算过程中的中间张量prefill 阶段尤其凶猛。CUDA 上下文、图捕获、框架运行时vLLM 启用 CUDA Graph 后会有几百 MB 到 2 GB 左右的隐性开销经常被忽略。这四笔里前两笔是“固定资产”后两笔是“流动资金”。24 GiB 看着不小但固定资产稍微配置不当流动资金就没了着落。1.2 权重档位与真实占用先看权重。以 8B 量级模型为例不同精度下的静态占用差别很大精度8B 模型权重占用7B 模型权重占用备注FP16/BF16约 16 GB约 14 GB精度最好但占地最大INT8约 8 GB约 7 GB精度损失可忽略INT4/INT8 混合AWQ、GPTQ约 4.2 GB约 3.8 GB实测精度损失在 1% 以内这里的计算逻辑很简单10 亿参数在 FP16 下占 2 GB在 INT8 下占 1 GB在 INT4 下占 0.5 GB。所以“权重装进 24 GiB”这句话在 8B 模型下无论哪个精度都能做到甚至 13B 模型 INT4 也能勉强塞进去。问题从来不在权重本身。1.3 权重装进去之后还剩下什么24 GiB 扣掉权重才是真正的战场。以 FP16 权重为例8B 模型吃掉 16 GB 后账面上还剩 8 GB。听起来还能再塞点东西但别忘了CUDA Graph 和运行时通常要预留 1~2 GB激活值在最坏情况下batch 大、序列长能瞬间吃出几个 GB剩下的才是 KV Cache 的预算。也就是说FP16 权重的 8B 模型在 24 GiB 卡上KV Cache 能用的空间大概只有 5~6 GB。这一路 32K 上下文需要多少 KV Cache我们接着算。提示显存计算永远按“加载后实测”为准厂商标的 24 GiB 实际可用会略少驱动、显示输出、CUDA 初始化都会占掉一部分。2. KV Cache藏得最深的隐形大户2.1 为什么推理必须存 KV自回归生成是逐 token 进行的每生成一个新 token注意力层都要重新计算它与前面所有 token 的注意力分数。如果没有缓存每步都要从头把整段历史重新算一遍复杂度是 O(n²)根本没法做服务。所以业界做法是把已经算过的 Key 和 Value 向量存下来新 token 来了只用它的 Query 去和缓存的 Key 做点积再把新的 Key/Value 追加进缓存。这个缓存就是 KV Cache。它的体积和三个东西成正比模型层数、注意力头配置、当前序列的累计 token 数。可以用生活类比理解KV Cache 就像聊天软件里的聊天记录。对话越长记录越多每次新消息都要翻一遍旧记录来“回忆上下文”。记录越多占的磁盘显存越大。2.2 一路 32K 的 KV Cache 手算过程KV Cache 的公式可以写成每 token 占用 2 × 层数 × KV 头数 × head_dim × 字节数其中 2 代表 Key 和 Value 两份。以 Llama 3 8B 为例层数 32KV 头数 8GQA 结构32 个 Query 头共享 8 个 KV 头head_dim 128FP16 下每元素 2 字节。代入2 × 32 × 8 × 128 × 2 131,072 字节 128 KiB也就是说这个模型每处理 1 个 token就要新增 128 KiB 的 KV Cache。32K 上下文就是 32,768 个 token32,768 × 128 KiB 4,194,304 KiB 4 GiB注意这只是一路请求、一个上下文。4 路并发同时跑满 32K就是 16 GiB。现在你明白为什么 FP16 权重的 8B 模型在 24 GiB 卡上必挂了吧16 GB 权重 16 GB KV Cache已经 32 GB 了还没算激活值。2.3 GQA 和 MHA 的差距不是一点点上面算的 128 KiB/token 是建立在 GQA分组查询注意力基础上的。如果换成一个没有 GQA 的 MHA多头注意力模型情况会完全失控。以 Llama 2 7B 为例它是 32 层、32 个 KV 头2 × 32 × 32 × 128 × 2 524,288 字节 512 KiB/token一路 32K 上下文就要 16 GiB4 路是 64 GiB。别说 24 GiB 卡80 GiB 的 A100 都扛不住四路满长上下文。所以选模型时KV 头数这个参数比总参数量重要得多。再看 GQA 做得更极端的 Qwen2.5-7B28 层、4 个 KV 头每 token 占用2 × 28 × 4 × 128 × 2 57,344 字节 56 KiB一路 32K 只要 1.75 GiB四路也才 7 GiB。这就是为什么同样 7B 参数不同模型的显存命运完全不同。模型参数量层数KV 头数每 token KV 占用一路 32K四路 32KLlama 2 7B7B3232512 KiB16 GiB64 GiBLlama 3 8B8B328128 KiB4 GiB16 GiBMistral 7B7B328128 KiB4 GiB16 GiBQwen2.5-7B7B28456 KiB1.75 GiB7 GiB2.4 四路并发后的真实压力这里还要提醒一点KV Cache 是按序列分配的而序列的实际 token 数在生成过程中一直在增长。四路请求如果都是从零开始生成那显存占用是一路一路涨上去的但如果四路请求各自带着长文档进来第一轮 prefill 就把缓存打满了。更隐蔽的问题是请求结束、释放缓存之后显存池里留下的碎片能不能被后续请求复用。传统 PyTorch 推理里KV Cache 经常是预先按最大长度分配的四路 32K 就是四块固定大小的 4 GiB 区域哪怕实际只用 1K 也占着。这也是为什么我在下一节要专门讲显存池化和分页管理。3. 组合拳把四路 32K 塞进 24 GiB 的可行路线3.1 路线 A权重量化先腾出地基最直接的做法是把权重从 FP16 降到 INT8 或者 INT4。同样是 8B 模型INT8 权重 8 GB比 FP16 省出 8 GBINT4 权重约 4.2 GB比 FP16 省出近 12 GB。省出来的空间全部可以划给 KV Cache。INT4 权重下24 GiB 卡扣除 CUDA 开销和激活值预留后KV Cache 预算大概有 17~18 GB足够装四路 32K 的 FP16 KV Cache16 GB甚至还留有富余。量化对质量的影响8B 模型用 AWQ 或 GPTQ 的 4-bit 量化实测在通用任务上掉点基本在 1% 以内如果你跑的是代码、数学这类高严谨性任务建议先用 INT8 试不行再降 INT4。我个人的原则是能 INT8 就不 INT4质量优先。3.2 路线 BKV Cache 量化直接砍掉一半权重量化是“节流”KV Cache 量化则是“直接把最大头砍半”。把 KV Cache 从 FP16 降到 INT8/FP8每 token 占用从 128 KiB 降到 64 KiB四路 32K 就从 16 GiB 变成 8 GiB。vLLM 里对应是kv_cache_dtypefp8TensorRT-LLM 里是--kv_cache_dtypefp8。量化后的 KV Cache 精度损失在短上下文下几乎感知不到但在非常长的上下文、高重复内容的场景里会有累积误差所以线上服务我一般建议只量化到 FP8不要上 INT4 的 KV Cache。组合效果8B 模型 INT4 权重4.2 GB FP8 KV Cache四路 32K 共 8 GB总固定占用约 12.2 GB加上运行时和激活值24 GiB 卡还能剩 8 GB 以上给动态波动。这套组合是我这次实测的最终方案。3.3 路线 CPagedAttention 与显存池化vLLM 的核心创新就是 PagedAttention它把 KV Cache 切成固定大小的块block按需分配用完释放。这个概念直接借鉴了操作系统里的分页机制效果是显存不再被“按最大长度预分配”浪费掉。举个例子四路请求每路虽然支持 32K但实际平均只用 8K。传统预分配方案照样占 4 × 16 GiB 64 GiB如果是 FP16 模型分页方案只分配实际用到的块四路合计也就 4 × 2 GiB 8 GiB 左右。这就是为什么同样的卡用 vLLM 和用裸 PyTorch 部署能服务的并发数完全不是一个量级。PagedAttention 还支持跨请求共享物理块。多路请求如果共享同一个系统提示词前缀这个前缀对应的 KV Cache 可以只存一份四路直接共享读。对聊天机器人这种“系统提示词很长、用户内容很短”的场景省下的空间非常可观。3.4 路线 D调度策略chunked prefill 与连续批处理内存问题不只是“总量”问题还有“峰值”问题。四路请求同时进来如果每路都做 32K 的完整 prefill激活值峰值会非常吓人。以 8B 模型为例隐藏层 4096FFN 中间层 14336单路 32K 的 prefill 激活值最坏情况下能把十几个 GB 瞬间打满。解决办法是 chunked prefill把超长 prompt 切成小块比如每块 512 token逐块计算。这样激活值峰值被压在百 MB 级别代价是 prefill 吞吐略有下降。配合 continuous batching连续批处理decode 阶段的请求可以插进 prefill 的空隙里执行GPU 始终处于满负荷状态。这也是 vLLM 打开enable_chunked_prefillTrue后能稳定服务长上下文的核心原因。这部分的经验是不要迷信“最大吞吐”服务化部署首要目标是稳定不 OOM吞吐只需要满足业务水位即可。chunked prefill 牺牲的那点吞吐换来的稳定性绝对值。3.5 选型对照哪种组合最适合你把上面的路线整理成一张对照表方便你对号入座组合方案权重占用KV 占用四路 32K总固定占用24 GiB 卡可行性FP16 权重 FP16 KV16 GB16 GB32 GB不可行INT8 权重 FP16 KV8 GB16 GB24 GB极限几乎没有余量INT4 权重 FP16 KV4.2 GB16 GB20.2 GB可行余量一般INT4 权重 FP8 KV4.2 GB8 GB12.2 GB很宽裕推荐INT8 权重 FP8 KV8 GB8 GB16 GB可行质量最好我的结论很明确想在 24 GiB 卡上稳定扛四路 32K至少要同时做权重量化和 KV Cache 量化并且后端必须用带分页管理的推理框架。单靠某一条路要么卡得死死的要么余量小到一次长文本请求就把服务拖垮。4. 实测vLLM 配置与调参全记录4.1 环境与模型清单这次实测的环境GPURTX 4090 24 GiB驱动版本 550 系列CUDA 12.4框架vLLM 0.6.x 分支使用 PagedAttention chunked prefill模型Llama 3 8B Instruct 的 AWQ 4-bit 量化版请求4 路并发每路最大 32K 上下文系统提示词约 2K token用户输入从 1K 到 28K 不等。选 Llama 3 8B 的原因很简单GQA 8 头让它每 token 的 KV 占用只有 128 KiB是 24 GiB 卡上“能打”的上限模型。更大参数的模型即便量化后权重塞得下四路 32K 的 KV 也会挤爆剩余空间。4.2 关键参数逐项讲解下面是最终稳定运行的 vLLM 配置核心参数from vllm import LLM, SamplingParams llm LLM( modelTheBloke/Llama-3-8B-Instruct-AWQ, quantizationawq, dtypefloat16, gpu_memory_utilization0.92, max_model_len32768, max_num_seqs8, max_num_batched_tokens2048, enable_chunked_prefillTrue, kv_cache_dtypefp8, trust_remote_codeTrue, )逐项解释为什么这么设gpu_memory_utilization0.92vLLM 会按这个比例预留显存给 KV Cache 池。0.92 意味着一共可用的 24 GiB 里92% 由 vLLM 管理。我试过 0.95能多挤一点缓存池但 CUDA Graph 捕获阶段偶尔会失败最后退回 0.92。这 8% 的余量是给框架、驱动、动态峰值买的保险。max_model_len32768这是框架允许的最大上下文长度。四路请求只要有一路超过这个长度vLLM 会拒绝请求而不是截断这是明确的行为别和“支持 32K”混淆。max_num_seqs8这是框架内部最多同时处理的序列槽位数不是“并发上限”。请求排队是由下游网关控制的vLLM 只是负责一批一批地消化。设成 8 是为了给 4 路业务请求额外的缓冲槽位避免 decode 和 prefill 互相阻塞。max_num_batched_tokens2048chunked prefill 时每一批次最多合计多少个 token。设小了 prefill 慢设大了激活值高。2048 在 4090 上是甜点值。enable_chunked_prefillTrue超长 prompt 分段处理防止 prefill 峰值把显存打爆。这是能不能稳定扛 32K 的关键开关。kv_cache_dtypefp8KV Cache 降到 8 bit四路 32K 的缓存占用从 16 GiB 降到 8 GiB立省 50%。4.3 压测结果分析用四路客户端同时发起请求每路输入长度 28K token要求生成 512 token。最终稳定指标总固定占用含权重、KV、CUDA Graph约 15 GB峰值占用稳定在 20~22 GB 之间没有触发 OOM聚合 decode 吞吐约 55~60 token/s每路平均生成速度约 14 token/s首 token 延迟在 2~4 秒chunked prefill 分段执行的正常水平。这个 14 token/s 单路速度聊天场景完全够用但对流式输出要求高的场景会感觉偏慢。原因也很清楚decode 阶段每生成 1 个 tokenGPU 都要把全部权重INT4 约 4.2 GB和全部 KV CacheFP8 四路共 8 GB读一遍总共约 12 GB4090 的显存带宽约 1 TB/s理论下限就是 12 ms 左右一代。这是硬件物理限制不是软件调优能突破的。4.4 从 24 GiB 里再多抠一点如果你希望把余量再做大或者干脆把目标放到四路 64K这几招实测有效压缩系统提示词把 2K 的系统提示词压到 500 tokenKV 占用直接少一截。这里本质是上下文工程的问题prompt 设计不是只影响质量还影响显存账单。开启 prefix cachingvLLM 的自动前缀缓存enable_prefix_cachingTrue会让多路请求共享系统提示词部分的 KV 块四路共用一份系统提示词缓存。限制单路最大生成长度max_tokens设成业务实际需要的值。很多人默认让模型“想生成多少生成多少”结果每路多生成了几百上千 tokenKV 跟着涨。模型换 Qwen2.5-7B 这类 4 KV 头的小 GQA 模型每 token KV 占用从 128 KiB 降到 56 KiB四路 32K 一共只要 7 GiB空间直接翻倍。有一说一fp8 KV Cache 在部分老显卡上不支持如果你用的是 3090需要先确认驱动和 vLLM 版本是否带 FP8 支持不行就退回 INT8 的 KV 量化方案效果也差不了太多。5. 常见问题与避坑实录5.1 OOM 是最诚实的老师我这次踩过最大的坑是权重量化成 INT4 后FP16 KV Cache 方案在压测前十分钟一切正常第十一分钟 OOM。原因后来查清楚了——四路请求里的其中一路输入特别长触发了 prefill 激活值峰值把预留的 KV 池空间临时挤爆了。这个教训换成配置的话就是gpu_memory_utilization 别贪心max_num_batched_tokens 别往大调。以后凡是遇到偶发 OOM第一反应不要是“把模型再量化低一点”而是先看激活值峰值是不是把池子击穿了。用nvidia-smi盯显存曲线能看到典型的长方形高台那就是 prefill 峰值。5.2 上下文长度不等于实际占用“支持 32K 上下文”说的是max_model_len32768表示框架能接收这么长的输入。但 KV Cache 是按实际 token 数分配的——请求只传 1K token就只占 1K 的缓存。这本来是好消息但隐患在于用户的上限不是你定的是模型卡定的。业务方如果只看到“支持 32K”就可能把 30K 的长文档直接丢进来。四路请求同时丢长文档峰值立刻起飞。所以我在网关层加了一道超过 16K 的请求降级到单路独享或者干脆排队。这不是怂是让服务在业务波动下不挂。另外提醒token 数不等于字符数。中文大约 1~1.5 个 token 一个字英文可能一个词拆成两三个 token。所谓“32K 上下文”在中文场景大概对应 2 万字出头的文本别跟用户宣传“能读 3 万字”。5.3 并发路数与 max_num_seqs 的关系很多新手以为max_num_seqs4就能扛四路并发这是误解。max_num_seqs是框架内部同时持有的序列槽位数不直接等于外部并发。外部并发由网关/负载均衡控制请求到了 vLLM 只会排队。但槽位也不是越大越好。槽位多意味着 KV 池要被更多序列瓜分每个序列可用缓存变少。实测下来max_num_seqs设成“业务并发数的 1.5~2 倍”比较合理既能抗抖动又不会把池子切得太碎。这次四路业务我设 8跑起来很稳。5.4 监控与诊断工具箱最后分享几个排查显存问题的工具都是免费的nvidia-smi -l 1看整卡显存曲线区分固定占用和波动峰值vllm日志里的GPU KV cache size启动时会打印当前 KV 池大小和剩余预算这个数字比 nvidia-smi 更精确地告诉你池子有多大vLLM 的/metrics端点里面有vllm:num_requests_running、vllm:cache_usage等指标cache_usage接近 1 就说明池子快满了该扩容或者限流了如果你想看每一层、每一个张量的显存细分用 PyTorch 的torch.cuda.memory快照工具能在 OOM 现场抓到是谁在申请内存。我在实际监控中发现的一个规律是服务一旦运行超过半天KV 池的使用率会稳定在一个平台期上下波动很小。这时候如果cache_usage长期超过 0.85我会主动把max_num_seqs降一挡宁可让请求排队也不要把池子逼到极限。最后再分享一个从这次部署里得出来的体会显存这件事永远不要拿“理论上够”当依据。理论账是静态的真实流量是动态的只有把权重、KV Cache、激活值、运行时这四笔账全部拆开按最坏情况压测一遍你才知道 24 GiB 到底装得下什么。我这次把四路 32K 成功塞了进去靠的不是某一个神级参数而是权重量化、KV 量化、分页管理、分块调度四件事同时做对。如果哪天你也要在类似配置上做部署建议就从这四条路线同时下手别只盯着某一项优化。