详解vLLM启动时的权重加载→profiling→KV block分配链路,gpu_memory_utilization参数过高/过低分别导致不同错误及解决思路。
vLLM 加载权重、profile 一次前向传播,然后尝试把剩余内存切分成 KV 块。如果剩余内存为零,它会拒绝启动,并提示你调高那个参数——而这个参数往往本身就是问题的根源。
ValueError: No available memory for the cache blocks. Try increasing
`gpu_memory_utilization` when initializing the engine.
这个错误在引擎初始化阶段抛出,此时服务器还没有绑定端口,所以还没有任何请求被服务过。这个消息及其背景可以在 vLLM 的 issue 追踪器里看到——issue 2248 是关于此问题的长期讨论线程,issue 5274 则是揭示了这个陷阱的 issue:一个高的值会导致 OOM 崩溃,一个低的值会导致这个错误,而两者之间的窗口可能很窄。
了解启动顺序是值得的,因为每个步骤都对应一种不同的耗尽方式。
权重加载。固定成本,可以预先计算:参数量乘以每个参数的字节数,再加上一些缓冲区。
Profile 运行执行。vLLM 以它配置的最大批处理大小和序列长度运行一次虚拟前向传播,并记录峰值内存。这就是激活成本,它随 --max-num-batched-tokens 缩放。
CUDA graph 被捕获,除非设置了 --enforce-eager。Graph 捕获持有自己的内存。
剩余的部分成为 KV 块。预算公式是 gpu_memory_utilization × 总量 - 上述所有开销。如果结果为零或负数,你就会收到这个错误。
关键细节在第四行,这也是同一个参数在两台机器上表现不同的原因:gpu_memory_utilization 是设备总内存的 fraction,而不是剩余内存的 fraction。在一张空闲的 80 GB 卡上设为 0.9,vLLM 计划使用 72 GB。在同一张卡上、已有 30 GB 被另一个进程占用的情况下仍设为 0.9,vLLM 仍然计划使用 72 GB,而实际只有 50 GB 可用——它会在此时发现这个问题,或者更糟糕的是,在后续崩溃时才发现。
在修改任何参数之前先运行 nvidia-smi。你属于哪种情况通常在其输出中就能看到。
nvidia-smi 显示空闲内存很大,卡上没有其他进程,而你传入了一个保守的值比如 0.5。这就是这个错误消息的建议所针对的情况。调高它。
nvidia-smi 列出了第二个 PID——可能是 notebook 内核、上一个没有退出的 vLLM、或者第二个容器。因为 fraction 是相对于总内存的,vLLM 已经过度承诺了。杀掉那个进程;不要调高 fraction,否则会把这个问题转化成一个 OOM 崩溃。
权重字节数接近整张卡。一个 70B fp16 模型大约是 70e9 × 2 = 140 GB,还没算其他任何东西,所以它根本不适合放在单张 80 GB 设备上——无论怎么调 utilization 参数都不行;这是 tensor-parallel 或量化的问题,而不是参数问题。
issue 5274 的贡献者们描述了空闲内存在 70B 的解码器层中逐步下降,即使算术上说应该有空间,最后也只剩下一小部分。这个最难区分,而 --enforce-eager 是最值得首先尝试的方案。
vLLM 有另一种启动失败,错误信息看起来相似,但修复方向相反。当 KV 缓存已分配但太小、无法容纳请求长度的一个序列时,会出现那个错误——消息中会写明模型的最大序列长度和缓存能存储的 token 数,并指向 --max-model-len。如果你看到那个错误,降低 --max-model-len 就可以修复。如果你看到的是 cache-blocks 错误,说明分配器根本没拿到任何内存,缩短上下文不会凭空变出被另一个进程占用的内存。
vLLM 的 V1 引擎改变了内存计算方式,措辞和参数界面在各个版本之间都有变动——有报告称某些模型在 V0 上能正常启动但在 V1 上不行。在生产环境中锁定 vLLM 版本,并在升级一个正常工作的配置之前检查发布说明。
先清空卡。运行 nvidia-smi,然后终止任何不属于这台服务器的东西。一次崩溃的上一次运行经常还持有它的内存分配。
逐步调高 fraction——0.85,然后 0.90——把 0.95 视为天花板。剩余部分不是浪费;它吸收分配器的碎片和 CUDA 上下文。
添加 --enforce-eager。它跳过 CUDA graph 捕获,省去那些 graph 持有的内存,代价是每步有一些开销。如果单独使用这个参数就能让服务器启动,你就知道 graph 是那个边际成本。
减小 --max-num-batched-tokens 或 --max-num-seqs。两者都会缩小 profile 峰值,而峰值是在计算 KV 预算之前要被减去的。
如果这些都没有留下足够空间,说明模型对这台设备来说太大了。跨更多 GPU 提高 --tensor-parallel-size,或者使用量化的 checkpoint——量化在推理时付出的代价就是你所做的权衡。
还有一个杠杆是人们最后才会想到、但应该更早想到的,那就是缓存本身的宽度。--kv-cache-dtype fp8 以 8 位而不是 16 位存储 keys 和 values,这大致上使每个 token 的 KV 缓存成本减半,因此大致上使相同剩余内存能获得的块数量翻倍。这是一个质量权衡,不是每个模型都适用,但这是这个列表中唯一一个增加 KV 容量的选项,而不是仅仅从别处找内存。这个权衡的一般形态在 KV cache page 中有描述。
如果可以的话,在做任何这些操作之前先做一下计算。权重字节数 = 参数量乘以每个参数的字节数:一个 7B fp16 模型大约是 7e9 × 2 = 14 GB,fp8 大约是 7 GB,4-bit 大约是 3.5 GB 加上量化元数据。从 gpu_memory_utilization × 总量 中减去它,再减去几 GB 的激活和 CUDA graph 开销,剩下的就是你的真实 KV 预算。如果纸上算出来是负数,运行时会也是负数,没有任何参数组合能改变这一点——你需要考虑不同的设备、更多的设备、或者更小的 checkpoint。
最后关于顺序的一句:在按以下顺序运行两条命令,因为在第一个服务器还没有释放设备之前启动第二个服务器是导致你改了一个参数却得出错误结论(错误地认为参数没用)的最常见方式,然后就会回到这个错误。
nvidia-smi --query-compute-apps=pid,used_memory --format=csv
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--enforce-eager