背景:不是跑 Demo,是要上产线
最近在做一个二维码缺陷质检的数据流水线,其中一层(L3 净化层)需要 VLM 判断两件事:
- 码制识别:QR / DPM 点阵码 / 其他条码 / 无码 / 无法判断
- 可见性判定:完整 / 部分可见 / 隐约可辨 / 完全不可见
判定结果直接决定下游标注数据的清洗和分级。硬件是一张 RTX 5070 Ti(16GB 显存) ,模型选了 Qwen2.5-VL-7B-Instruct 的 AWQ 量化版。
这不是跑 Demo,是每天要处理几万张裁剪图的批量任务,所以稳定性和显存控制比"能不能出结果"重要得多。
为什么是 7B + AWQ
7B 参数用 bf16 存储大约 14GB,加上推理时的 KV cache 和视觉编码器开销,16GB 卡基本塞不下,更别说上下文一长就 OOM。AWQ 量化后模型文件只有 6.5GB,留出足够空间给 KV cache 和批量推理。对"判定可见性"这种分类任务,量化带来的精度损失可以接受------毕竟 prompt 里已经把枚举定义写死了,模型只需要做规则性判断,不需要长文本生成。
最终跑通的环境配方
先说结论,这套组合在 5070 Ti 上是跑通的:
| 组件 | 版本 |
|---|---|
| 系统 | Ubuntu 24.04 |
| GPU | RTX 5070 Ti 16GB / 驱动 580.159.03 |
| CUDA Toolkit (nvcc) | 12.0 |
| Python | 3.11.16 |
| torch | 2.13.0+cu130 |
| transformers | 5.17.0 |
| vllm | 0.30.0 |
| flashinfer-python | 0.6.18 |
| 模型 | Qwen2.5-VL-7B-Instruct-AWQ(6.5GB) |
踩坑记录
坑一:flashinfer sampler 必须关掉
这是最实在的一个坑。代码里有一行看起来莫名其妙的环境变量,是踩过之后加的:
python
import os
os.environ.setdefault("VLLM_USE_FLASHINFER_SAMPLER", "0")
vLLM 默认会启用 flashinfer 的采样 kernel,但在当前这套版本组合(vllm 0.30 + flashinfer 0.6.18 + cu130)上会导致推理异常。关掉之后一切正常。
如果你也在类似环境遇到 vllm 生成结果异常、卡住或者崩溃,第一件事是把这个开关设为 0,能省掉半天排查。
坑二:CUDA Toolkit 版本和 torch 版本不需要"对齐"
这是很多教程误导人的点。机器上 nvcc --version 显示 CUDA 12.0,但 torch 是 2.13.0+cu130(CUDA 13.0 编译)------这是正常的,也是推荐的。
PyTorch 自带 CUDA 运行时,推理不需要系统 nvcc 和 torch 的 CUDA 版本一致。系统 nvcc 只有在编译自定义 CUDA 算子(比如手搓 flash-attn)时才需要匹配。如果你的场景只是"加载模型跑推理",千万不要为了"版本对齐"去重装系统 CUDA,纯属给自己找事。
坑三:16GB 显存的核心参数
在 16GB 卡上跑 7B 多模态模型,这几个参数决定了能不能活:
python
llm = LLM(
model=MODEL,
max_model_len=4096, # 上下文别开太大,KV cache 吃显存
gpu_memory_utilization=0.85, # 留 15% 给视觉编码器和碎片
limit_mm_per_prompt={"image": 1}, # 关键!限制每条 prompt 最多 1 张图
)
limit_mm_per_prompt 是 vLLM 多模态的隐藏坑:不限制的话,多图输入会让显存峰值不可控。我们的场景每条样本就一张裁剪图,限制成 1 最安全。
另外图片输入还要控制分辨率上限:
python
{"type": "image", "image": "file://" + cpath, "max_pixels": 602112}
602112(约 784×768)对"判断码图是否清晰"这种任务足够了,再大只会浪费显存。
坑四:多模态输入不能直接拼字符串
这是 vLLM + Qwen-VL 的常见错误。正确姿势是先用 processor 套模板,再用 process_vision_info 处理图片:
python
from vllm import LLM, SamplingParams
from transformers import AutoProcessor
from qwen_vl_utils import process_vision_info
processor = AutoProcessor.from_pretrained(MODEL)
messages = [{"role": "user", "content": [
{"type": "image", "image": "file://" + cpath, "max_pixels": 602112},
{"type": "text", "text": PROMPT},
]}]
prompt = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
img_data, _ = process_vision_info(messages)
inputs = {"prompt": prompt, "multi_modal_data": {"image": img_data[0]}}
outs = llm.generate([inputs], SamplingParams(temperature=0.0, max_tokens=200))
直接把图片路径塞进 prompt 字符串,模型只会"想象"一张图然后胡说八道。
坑五:输出解析要靠 prompt 纪律,不是靠正则碰运气
判定任务最怕模型输出自由发挥。三个约束必须写进 prompt:
- 枚举值定义写死,包括边界情况。比如"很糊但能辨认出码结构,判 qr 而不是 no_code"、"DataMatrix 没有回字定位角"------这些说明比模型自己猜有用得多。
- 只输出一行 JSON ,配合
temperature=0、max_tokens=200。 - 解析失败要有兜底:我们给失败样本单独记日志,而不是静默丢弃。
坑六:工程化细节比调参更影响能不能跑完
真跑几万条数据时,这几个处理是保命的:
- 断点续跑 :输出 jsonl 按
(图片, 目标id)去重,中断后重跑不重复计算 - 旋转框裁剪:二维码标注是旋转框,直接按外接矩形裁会带一堆背景,按角度旋转后再裁 + 10% padding 效果更好
- 失败不阻塞:单条样本裁剪失败或解析失败,记录后跳过,不中断整批
效果与结论
这套配置在 5070 Ti 上跑批量裁剪图推理是稳定可用的。对"码制分类 + 可见性分级"这种任务,7B AWQ 模型的判定结果可以直接进入人工抽检流程,替代了原来纯人工翻阅的工作量。
几个经验总结:
- 16GB 单卡跑 7B 多模态批量推理完全可行,关键是 AWQ 量化 + 显存参数收紧
- vLLM 的坑集中在版本组合 ,遇到怪问题先试
VLLM_USE_FLASHINFER_SAMPLER=0 - 多模态推理别省 processor 那一步
- prompt 里把枚举和边界写清楚,比换更大的模型有效
这套流水线后面还可以复用到文档识别、票据识别、缺陷分类等场景------本质上都是"给一张图 + 一个分类规则,让 VLM 输出结构化判定"。
标签建议:Qwen2.5-VL、vLLM、多模态、RTX 5070 Ti、模型部署