小模型反而爆显存?Qwen2.5-VL 3B vs 7B 的 vLLM 显存真相——多模态 Profiling 机制深度拆解

本文案例来自 vLLM 官方 GitHub Issues(#22719#14150),适配至国内部署环境后验证。

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 → 你大概率会中招

启示

  1. 小模型不一定省显存。 多模态模型的显存占用由「权重 + KV Cache + Encoder Cache + 临时张量」四部分构成。权重小不代表总占用小。

  2. vLLM V1 引擎的 sampler warmup 是「显存试金石」。 如果 warmup 阶段就 OOM,问题不在推理阶段,在 profiling 阶段的内存估算。

  3. limit_mm_per_prompt 是多模态模型的必配参数。 不设就是「不限」,vLLM 会按最坏情况(比如 16 张图)预估 encoder cache。

  4. 24GB 显卡跑多模态模型的黄金参数组合max-num-seqs=16, gpu-memory-utilization=0.85, limit-mm-per-prompt={"image": 2}

  5. 报错信息里的「降低 max_num_seqs」不是敷衍------它真的是最快的修复路径。 但理解为什么 3B 比 7B 更需要降这个参数,才是从「能跑」到「理解」的跨越。


原始出处


本文首发于 CSDN 专栏《运维漏洞指南》,转载请注明出处。

相关推荐
starrysky8104 天前
THP 透明大页导致数据库延迟从 2ms 飙升到 2000ms——khugepaged 内存压缩风暴排查实录
angular.js
starrysky8107 天前
多Agent通信架构实战:从NATS消息总线到五大编排模式的生产落地——Agent通信协议篇
angular.js
starrysky8107 天前
RecursionError: maximum recursion depth exceeded —— 你的函数调用链,踩穿了 CPython 的安全气囊
angular.js
starrysky8109 天前
MemAvailable 还有 29GB,系统却报内存压力?——Ubuntu 24.04 CIFS 内核 Page Cache 泄漏排查实录
angular.js
starrysky8109 天前
KeyError: 'xxx' —— 字典里没这个键,但你的代码以为有
angular.js
starrysky8109 天前
FP8量化实战:vLLM与SGLang部署DeepSeek显存减半、吞吐翻倍——Agent推理引擎篇(二)
angular.js
starrysky8109 天前
vLLM与SGLang启动参数调优实战:从默认配置到生产级吞吐量翻倍——Agent推理引擎篇
angular.js
starrysky8109 天前
【03】ImportError: cannot import name 'X' —— 模块在,名字没了
angular.js
starrysky8109 天前
systemd-journald日志限速导致生产日志丢失:Suppressed XXXX messages完整排查
angular.js