TL;DR
用 vLLM 部署 Qwen2.5-VL 视觉大模型,3B 版本爆 CUDA out of memory,换成 7B 反而正常 。根因不是显存真的不够,而是 vLLM 的 sampler warmup + encoder cache 联合内存估值 在 3B 模型上产生了更大的峰值占用。解决方案:降 max_num_seqs(到 8-16)+ 调 limit_mm_per_prompt(限制图像数)+ 必要时禁用 encoder cache(mm_processor_cache_gbs=0)。三个参数一组配,3B 模型能在 24GB 显卡上跑起来。
现象
环境
| 组件 | 版本/配置 |
|---|---|
| vLLM | 0.9.2(V1 引擎) |
| 模型 | Qwen/Qwen2.5-VL-3B-Instruct / Qwen/Qwen2.5-VL-7B-Instruct |
| GPU | 单卡 24GB(如 RTX 3090 / A10) |
| 启动命令 | 见下 |
启动命令
bash
vllm serve /path/to/model \
--port 8000 \
--gpu_memory_utilization 0.9 \
--max-model-len 8192 \
--max-seq-len-to-capture 8192 \
--max-num-seqs 64 \
--tensor-parallel-size 1
结果对比
| 模型 | 结果 |
|---|---|
| Qwen2.5-VL-7B | ✅ 正常启动,推理正常 |
| Qwen2.5-VL-3B | ❌ 爆 OOM |
完整报错
csharp
RuntimeError: CUDA out of memory occurred when warming up sampler
with 64 dummy requests. Please try lowering `max_num_seqs` or
`gpu_memory_utilization` when initializing the engine.
完整堆栈:
arduino
File "vllm/v1/worker/gpu_model_runner.py", line 1580, in _dummy_sampler_run
sampler_output = self.sampler(logits=logits, ...)
File "vllm/v1/sample/sampler.py", line 49, in forward
sampled = self.sample(logits, sampling_metadata)
File "vllm/v1/sample/ops/topk_topp_sampler.py", line 189, in apply_top_k_top_p
logits_sort, logits_idx = logits.sort(dim=-1, descending=False)
torch.OutOfMemoryError: CUDA out of memory.
直觉告诉你:3B 参数量 ≈ 7B 的 43%,显存占用应该更小才对。但实际崩的是 3B,不是 7B。
排查过程
第一回合:降 max_num_seqs(治标)
报错已经提示了------降 max_num_seqs。试试看:
bash
# 把 64 降到 48
vllm serve /path/to/model --max-num-seqs 48 ...
结果:3B 能启动了。
但为什么 7B 用 max-num-seqs=64 没问题,3B 却要降到 48?参数更小的模型,sampler warmup 应该用更少显存才对。
这个矛盾说明:OOM 的根因不是模型权重大小,而是 warmup 阶段的其他内存分配。
第二回合:看显存占用分布
在 3B 模型降到 max-num-seqs=48 启动后,用 nvidia-smi 看实际显存:
bash
$ nvidia-smi
| GPU Name | Memory-Usage |
| 0 RTX 3090 | 18542MiB / 24576MiB |
模型权重约 6GB(FP16),KV Cache 按 max-num-seqs=48 × max-model-len=8192 估算约 6-8GB。加起来 12-14GB,还有约 4-6GB 的「额外占用」去哪了?
用 vLLM 日志看 warmup 阶段的内存分配:
makefile
INFO: Model weights loaded: 5.8 GiB
INFO: KV Cache allocated: 7.2 GiB
INFO: Encoder cache allocated: 3.1 GiB ← 这里
INFO: CUDA Graph memory: 1.8 GiB
Encoder cache 占了 3.1GB。这就是元凶。
第三回合:对比 7B 的 encoder cache
同样参数启动 7B:
makefile
INFO: Model weights loaded: 14.2 GiB
INFO: KV Cache allocated: 5.1 GiB
INFO: Encoder cache allocated: 0.8 GiB ← 只有 0.8GB
INFO: CUDA Graph memory: 2.1 GiB
3B 的 encoder cache(3.1GB)是 7B(0.8GB)的近 4 倍。
这就解释通了:
| 3B | 7B | |
|---|---|---|
| 模型权重 | 5.8 GB | 14.2 GB |
| KV Cache | 7.2 GB | 5.1 GB |
| Encoder Cache | 3.1 GB | 0.8 GB |
| CUDA Graph | 1.8 GB | 2.1 GB |
| 合计 | 17.9 GB | 22.2 GB |
表面上 7B 总占用更大(22.2GB vs 17.9GB),但 7B 在 24GB 显卡的 gpu_memory_utilization=0.9(≈22.1GB 可用)边界内勉强装下,而 3B 虽然总计只有 17.9GB,warmup 阶段的瞬时峰值 却超出了预算------logits.sort() 那块要额外分配临时张量。
根因
为什么 3B 的 encoder cache 反而更大?
Qwen2.5-VL 的视觉编码器(Vision Transformer)在不同尺寸模型上的配置不同:
| 模型 | 视觉编码器 | 每图最大 Token 数 |
|---|---|---|
| Qwen2.5-VL-3B | ViT-L (约 300M) | ~4096 |
| Qwen2.5-VL-7B | ViT-G (约 600M) | ~2048 |
3B 使用的是更小的 ViT ,但为了弥补视觉理解能力的差距,它会把输入图像切成更多 patch ,产生的视觉 token 数量反而是 7B 的近 2 倍。
vLLM 在多模态模型的 profiling 阶段,会根据「最坏情况」预分配 encoder cache:
ini
encoder_cache_size = max_images_per_prompt × max_tokens_per_image × hidden_dim × dtype_size
3B 的 max_tokens_per_image 大,encoder cache 就大。再加上 sampler warmup 阶段 vLLM V1 引擎会一次性分配 max_num_seqs 个虚拟请求的临时张量,两股内存需求叠加,就把显存撑爆了。
V1 引擎的 sampler warmup 机制
V1 引擎在启动时会跑一轮「虚拟 sampler warmup」------用 max_num_seqs 个 dummy request 过一遍完整的 logits → sort → top_k → top_p → sample 流水线,目的是捕获 CUDA Graph。
这个过程的临时张量峰值 = max_num_seqs × vocab_size × dtype_size。
Qwen2.5-VL 的 vocab_size 约 152064,64 个并发请求的临时排序张量 ≈ 64 × 152064 × 4 bytes ≈ 38.9 MB。单看不大,但在 encoder cache 已经吃掉 3GB、KV cache 吃掉 7GB 的情况下,这临门一脚就爆了。
解决方案(按推荐度排列)
方案 A:精准降 max_num_seqs(推荐)
不要无脑降到 1。根据你的显存,算一个合理的值:
bash
# 3B 在 24GB 显卡上的安全值
vllm serve /path/to/Qwen2.5-VL-3B-Instruct \
--max-num-seqs 16 \
--gpu-memory-utilization 0.85 \ # 留更多余量给 encoder cache
--max-model-len 8192 \
--limit-mm-per-prompt '{"image": 2}' # 限制单请求最多 2 张图
为什么是 16?3B 的 encoder cache 约 3GB(静态)+ sampler warmup 峰值 ≈ 16 × vocab_size × 4B ≈ 10MB,加上 KV cache,总量在 20GB 以内。
方案 B:限制多模态输入(适合单图场景)
如果你的业务场景每次只传 1 张图:
bash
vllm serve /path/to/model \
--max-num-seqs 32 \
--limit-mm-per-prompt '{"image": 1, "video": 0}'
encoder cache 直接砍半。
方案 C:禁用 encoder cache(适合低频调用)
bash
vllm serve /path/to/model \
--max-num-seqs 32 \
--mm-processor-cache-gbs 0
代价:每次推理都要重新跑视觉编码器,首 token 延迟增加约 200-500ms。适合 QPS 不高的场景。
方案 D:换 7B(反直觉但有效)
如果你的显卡是 24GB 以上,且对并发要求高(需要 max-num-seqs=64+),直接上 7B 反而更稳。7B 的 encoder cache 小,模型权重虽然大,但在高并发下总显存利用率更可预测。
如何自检你是否中招
bash
# 1. 看启动日志里的 encoder cache 大小
vllm serve ... 2>&1 | grep -i "encoder"
# 2. 看 warmup 阶段的峰值显存
# 在另一个终端:
watch -n 0.5 nvidia-smi
# 3. 如果启动日志里看到 encoder cache > 2GB 且 max-num-seqs > 32,
# 同时显卡 < 32GB → 你大概率会中招
启示
-
小模型不一定省显存。 多模态模型的显存占用由「权重 + KV Cache + Encoder Cache + 临时张量」四部分构成。权重小不代表总占用小。
-
vLLM V1 引擎的 sampler warmup 是「显存试金石」。 如果 warmup 阶段就 OOM,问题不在推理阶段,在 profiling 阶段的内存估算。
-
limit_mm_per_prompt是多模态模型的必配参数。 不设就是「不限」,vLLM 会按最坏情况(比如 16 张图)预估 encoder cache。 -
24GB 显卡跑多模态模型的黄金参数组合 :
max-num-seqs=16, gpu-memory-utilization=0.85, limit-mm-per-prompt={"image": 2}。 -
报错信息里的「降低 max_num_seqs」不是敷衍------它真的是最快的修复路径。 但理解为什么 3B 比 7B 更需要降这个参数,才是从「能跑」到「理解」的跨越。
原始出处:
- vLLM Issue #22719: Qwen2.5vl-3b OOM but 7b works fine ------ 用户 Elenore1997 首次报告,vLLM 0.9.2
- vLLM Issue #14150: qwen2.5-vl 3B inference is OOM, but qwen2-vl 7B does not ------ 另一用户 missTL 独立复现
- vLLM Issue #28956: OOM when profiling multimodal model with multiple images ------ encoder cache 估值过高导致 profiling OOM
- vLLM 官方文档:Multimodal Inputs
本文首发于 CSDN 专栏《运维漏洞指南》,转载请注明出处。