权重装进了 24 GiB,四路 32K 上下文还装得下吗?
模型权重已经加载,短问题也能回答,于是把服务目标改成四路长上下文。接下来遇到的却是缓存不足、请求等待,或者在某个峰值阶段显存不够。第一反应往往是:“权重才占一部分,剩下的显存为什么不够?”
加载成功只验证了当时那笔开销。生成中的历史信息也要留在 GPU 上,运行时还会需要临时空间。要判断四路长请求是否可行,需要回答的是:扣掉权重和非缓存峰值后,每张卡到底还有多少空间,能够同时保留多少 token 的 KV?
下面用 Qwen2.5-7B-Instruct 的公开配置算一笔账。24 GiB 显存、权重与运行开销都是明确设定的教学条件,没有加载该模型、运行 vLLM 或做 GPU 压测。算式用于排除不成立的容量承诺,不作为这款模型在某张显卡上的实测结果。
先从每张卡的预算里扣掉非 KV 开销
先固定单位:1 GB 是 10⁹ 字节,1 GiB 是 2³⁰ 字节。本文的 24 GiB 指假设运行时可见总量恰为 24 × 2³⁰ 字节,不是把任何厂商标称的“24 GB”直接代入。实际机器应读取字节数再换算。
对某一张 GPU,先写一个用于规划的简式:
可规划的 KV 空间 ≈ 本实例显存预算 − 本卡权重 − 其他非 KV 峰值预留
“其他”包括该执行配置下的激活与临时工作区、图捕获等非权重开销,不能用空闲时截图代替峰值。若权重已经单列,其他项就不要再把同一权重算一遍。这里是分账方法,不是保证每项峰值会在相同时刻达到最大,也不是某个版本内部变量的逐一映射。
vLLM v0.28.0 的自动预算路径提供了直接证据。在 GPU Worker 源码中,KV 可用量的计算包含下面三项:
self.requested_memory
- profile_result.non_kv_cache_memory
- cudagraph_memory_estimate_applied
这是赋值表达式中的连续项,省略了外侧变量和括号,不能单独运行。它说明引擎会从请求的预算里扣除已分析的非 KV 开销及适用的 CUDA Graph 估算,而不是把权重加载后看见的全部空闲量直接交给缓存。vLLM GPU Worker 源码
现在设定:单卡 24 GiB,显式选择利用率参数 0.9,权重预算记为 15 GiB,其他非 KV 峰值预留 2 GiB。后两项只是本例输入,不是 Qwen 官方公布或本文测得的占用;真实部署必须用当前精度、加载布局与执行配置重新测量。也没有用模型名里的“7B”乘两字节,冒充精确运行时权重。
于是本例得到:
实例预算 = 24 × 0.9 = 21.6 GiB
KV 规划空间 = 21.6 − 15 − 2 = 4.6 GiB
预算外的 2.4 GiB 不再重复算作 KV 可用空间。本例也没有把它承诺为能抵挡任意峰值的安全保证。
这里的 0.9 是主动设定。核验时 v0.28.0 文档默认值为 0.92;gpu_memory_utilization 描述当前实例的预算比例,不是监控面板上的 GPU 忙碌百分比。另一个参数 kv_cache_memory_bytes 直接指定每 GPU的 KV 字节量;设置后会忽略前述比例的自动预算方式,不能把两者当成可叠加的额度。引擎参数说明
四路 32K,需要缓存的是 131,072 个 token
KV Cache 保存后续生成还会使用的注意力 Key 和 Value。对普通全注意力、相同结构的各层,采用相同 K/V 维度和精度、不考虑共享时,可以从数据形状推导:
KV 字节数 = 2 × 层数 × KV head 数 × head 维度 × 每元素字节 × 已缓存 token 总数
开头的 2 是 Key 与 Value。并发序列长度不同时,总 token 数应该逐条相加。它既不是本轮新计算的 token 数,也不只是输入长度:已处理的 prompt 和生成历史都可能占缓存。队列里尚未获得 KV 的请求则不能按“已驻留”重复计入。
Qwen 官方配置给出了这个例子需要的原始字段:
"hidden_size": 3584,
"num_attention_heads": 28,
"num_hidden_layers": 28,
"num_key_value_heads": 4
这四行从配置不同位置选取,非连续摘录。由此得到 head 维度 3584 ÷ 28 = 128;GQA 使用 4 个 KV heads,不能把 28 个 Query heads 代入 KV 公式。配置同时记录 use_sliding_window: false,本例不套用滑动窗口缓存缩减。Qwen2.5-7B-Instruct 配置
再明确选择 BF16 KV,每元素 2 字节,不启用 KV 量化、前缀共享或缓存卸载,TP=1、PP=1。每 token 在完整模型各层合计需要:
2 × 28 × 4 × 128 × 2 = 57,344 字节 = 56 KiB
若一个序列已经缓存 32,768 个 token,就是 1.75 GiB。本文用“32K”指这 32,768 个已缓存总 token,不是 32K 输入之外还免费附送输出空间。四条这样的序列合计 131,072 个 token,KV 为 7 GiB。
| 同时驻留的序列状态 | 总缓存 token | 理想 KV 量 |
|---|---|---|
| 1 条,各 32,768 | 32,768 | 1.75 GiB |
| 2 条,各 32,768 | 65,536 | 3.5 GiB |
| 3 条,各 32,768 | 98,304 | 5.25 GiB |
| 4 条,各 32,768 | 131,072 | 7 GiB |
与 4.6 GiB 预算比较,第三条已经超过,四条更缺 2.4 GiB。权重能加载与这组请求不能全部保持目标 KV 状态,可以同时成立。
表中是张量载荷的理想值,不含块取整、对齐等实现成本;不能把“3.5 小于 4.6”写成两路一定能稳定服务。vLLM 的缓存规范按块描述存储,实际可用块数仍要从引擎配置和启动结果确认。KV Cache 存储规范
反过来,四条连接也不意味着始终占 7 GiB。它们若尚未增长到目标长度,实际用量会较小。本表问的是目标状态能否同时驻留,不能拿短输入试通的结果证明长尾请求组合也成立。当前配置的上下文上限是另一个限制;本例只算到配置中 32,768 的量级,不宣称完成上下文扩展。
加一张卡之前,先看 KV 实际分到了哪里
两张卡的总显存可以写在资产表里,但同一个缓存分配不会自动跨进另一张卡的空闲区。容量计算必须落实到每个执行 rank——参与执行的进程及其设备——所持有的权重、层和 KV heads。
若第二张卡运行另一个完整副本,它会重新承担该副本的权重和运行开销。它能接走别的请求,却不会替第一张卡保存一条请求缺少的 KV。因此,不能把“2 × 24 GiB”直接替换进刚才单副本的算式。
若采用 TP,并且模型与实现允许按 KV heads 均匀切分,本例 4 个 KV heads 在 TP=2 时每 rank 两个,在 TP=4 时每 rank 一个。只看同一组四路 32K 的 KV 载荷,每 rank 分别是 3.5 GiB 和 1.75 GiB。这里仅说明 KV 分账变化;权重、其他峰值和通信开销要按新的布局重新记录,不能假设所有项目一律除以 TP。
尤其不能无限除下去。vLLM 的 QKVParallelLinear 在 TP 数量不小于 KV head 总数时,把本 rank 的 KV head 数设为 1,并计算 head 的复制份数。也就是说,适用这种路径的 GQA 模型可能复制 KV heads,而不是把一个 head 继续切成任意小数。QKV 并行层源码
候选并行度还要满足 Query heads 等切分限制;本例有 28 个 Query heads,不能见到八张卡就直接列 TP=8 的均分预算。这里不推荐更大 TP,只要求先核实实际布局,再把每 rank 的 KV 数填回公式。采用 PP 时同样应使用本 rank 真正持有的层,而不是只用整机总显存盖过局部超额。
把“能加载”改成一份有条件的容量结论
回到四路长请求的目标,现在能够写出的结论是:“在 24 GiB、0.9、15 GiB 权重和 2 GiB 其他预留这些假设下,BF16 KV 的 7 GiB 理想需求超过 4.6 GiB 预算。”它足以否决这组假设下的同时驻留承诺,却不足以宣布某个具体 GPU 必然 OOM。
引擎可能通过等待或抢占等方式维持运行。vLLM 优化文档说明 KV 空间不足可能触发抢占;这不等同进程必然崩溃,也不说明请求延迟仍能满足目标。vLLM 抢占说明
下一次调整,只需围绕这张容量账改一笔并重算。减少同时驻留的长度或数量,会降低 KV 需求,但也缩小服务承诺;改变权重表示只会直接影响权重一项,不自动改变选定的 BF16 KV。选择更低精度 KV,则必须重新核实模型、后端支持与质量,不能把理想字节减半直接称为可上线收益。增加设备则要重新核实分片与复制,而非相加总显存。
最终保留一份按 rank 的记录:设备可见字节、模型配置与精度、预算方式、实测权重、当前负载下的非 KV 峰值、引擎可用 KV 块,以及目标序列长度总和。把本例的假设字段逐个换成实际证据,才知道缺口究竟在权重、临时峰值还是活跃缓存。
容量账通过后,仍要用目标输入输出长度与并发检查延迟、失败和内存峰值;这里没有完成这一步。它的价值是让测试从一个明确的可行候选开始,而不是从“权重加载成功,所以四路长上下文应该也行”开始。