本地离线模型服务遇到 vLLM 的 PagedAttention 显存碎片问题,通常表现为:显存占用看起来不高、但启动新任务时报 CUDA out of memory,或者持续批量推理时显存利用率逐渐走低。PagedAttention 只是把 KV cache 按块切分管理,能减少因长序列长度差异造成的内部碎片,但不会完全消除碎片。显存碎片仍然来自页表分配粒度、KV cache 预留策略、pre-allocated blocks、多请求调度时的空间交错,以及 PyTorch 缓存分配器碎片。下面按可操作顺序给出优化路径。
PagedAttention 缓解了长尾序列导致的 KV cache 碎片,但显存碎片并未消失。优先调整 gpu-memory-utilization、max-num-seqs、block-size,并检查模型预热和预分配行为。所有优化都需要通过启动日志和 nvidia-smi 观察显存分配曲线确认,不要凭感觉盲目调低利用率。离线场景下,更应关注显存复用率是否提升,而不是单纯压低显存占用。
先定位碎片来源,再动参数
本地离线服务使用 vLLM 时,显存碎片主要出现在三个层面:第一,KV cache 以 block 为单位分配,block 内未用完的部分就是内部碎片;第二,多个并发请求的 block 在显存页表中错位分布,导致空闲块之间不连续;第三,模型权重、激活值和 PyTorch 缓存的错峰分配,可能切割出一段无法被 vLLM 复用的空洞。定位方法是先启动一个固定 short prompt 的推理服务,记录启动日志中“GPU memory usage”和“KV cache size”,再逐步增加并发请求,同时用 nvidia-smi -l 1 观察显存占用变化。如果显存峰值远高于 KV cache 预估占用,且空闲显存零散,基本可以判断是预留和碎片问题,而不是算力不足。
调整核心参数,减小算子级和页级碎片
vLLM 启动参数中,影响显存碎片的主要有以下几个。每个参数都需要结合本地 GPU 显存总量、模型参数量和实际输入批次长度验证。
python -m vllm.entrypoints.openai.api_server \
`--model` /path/to/local/model \
`--gpu-memory-utilization` 0.92 \
`--max-num-seqs` 32 \
`--max-model-len` 4096 \
`--block-size` 16 \
`--swap-space` 4 \
`--enforce-eager` \
`--cpu-offload-gb` 0
其中 `--block-size` 决定 KV cache 块的 token 数。离线场景建议先用 16 或 32 跑一组输入长度差异较大的请求,观察日志中的“block manager”统计。如果多数 block 的实际占用率很低,说明内部碎片高,考虑减小 block-size;如果显存空洞频繁导致 preemption,再适当加大。注意,block-size 调小会增加页表开销,并非越小越好。
`--gpu-memory-utilization` 是 vLLM 可用的显存上限,不是预先占用量。离线任务如果没有并发高峰,可以先设低一些,但不要低于模型权重加激活值所需显存,否则 vLLM 会直接拒绝启动。通常建议从 0.85 起步,观察是否触发“CUDA out of memory”,再逐步提高到 0.95 左右。
`--max-num-seqs` 决定同时参与调度的序列数。多序列会增大块间交错,提高碎片概率。离线批量场景,优先用较小的 max-num-seqs(例如 16 或 32)跑批,而不是一次性塞入大批请求。这样能减少同时活动的 block 数量,让空闲块更容易连续。
验证显存复用效果,别只看总量
优化是否有效,需要观察 vLLM 日志中的“Prometheus metrics”以及实际的分配曲线。主要看两个可观测项:一是启动时打印的 KV cache size,它代表当前配置下可用的 KV cache 总量;二是运行中通过 /metrics 暴露的 vllm:num_preemptions_total 和 vllm:cache_usage_perc。前者持续增加说明显存碎片或调度压力导致频繁抢占,后者长期低于 50% 则说明 KV cache 利用率不足。
curl -s http://localhost:8000/metrics | grep vllm_cache_usage
也可以在一次稳定复现请求后,执行 nvidia-smi `--query-gpu`=memory.used,memory.total `--format`=csv,记录空闲显存是否呈零散分布。vLLM 没有直接输出“碎片数”的接口,但可以通过降低并发后显存是否大幅回落来判断。如果并发从 32 降到 4,显存只下降不到 10%,说明预分配和碎片占用过多,此时应优先调低 gpu-memory-utilization 或启用 eager 模式跳过 CUDA graph 的额外缓存。
针对离线场景的保守优化流程
- 先用默认参数启动服务,记录日志和显存曲线,确认碎片问题确实来自 KV cache 而非模型权重加载失败。
- 关闭 CUDA graph(加
`--enforce-eager`),观察显存释放效果。离线任务对首 token 延迟不敏感,eager 模式更利于显存回收。 - 逐步调低
`--gpu-memory-utilization`,每次降 0.02,直到触发 OOM 或日志警告,再回退 0.02 作为安全工作点。 - 用一组不同输入长度的请求测试,对比
block-size=16和32的cache_usage_perc,选择使用率高且抢占少的配置。 - 如果仍有碎片,给
max-num-seqs设置一个上限,例如 16,再跑同一组请求,观察 preememptions 是否归零。
整个过程不需要改动模型代码,只需调整启动参数。碎片优化有天花板:当 gpu-memory-utilization 已经接近模型实际需求,继续降低只会减少可用 KV cache,增加请求排队和时间开销。判断终点不是“显存占用低”,而是“在满足本次离线任务吞吐要求的前提下,不出现 OOM 和频繁抢占”。
常见误区与边界提醒
不要用“显存占用低”作为优化成功的标准。PagedAttention 的机制决定了显存中会保留大量空闲块用于后续复用;显存占用低可能只是没触发并发调度,不代表碎片被消除。也不要误以为调小 block-size 一定减少碎片,内部碎片减少的同时,页表和 block metadata 的额外开销可能抵消收益。对本地离线服务,如果单次推理能成功且多次重复不出现 OOM,通常不需要极限压榨显存。频繁重启服务换取“干净显存”并不适合长期离线任务,改用 `--swap-space` 把部分 KV cache 放到系统内存,能减少显存空洞,但会拖慢处理速度,只适合内存充裕且时间不敏感的批处理。