vLLM部署Qwen2-VL实战:单卡多卡与批量图片请求 前阵子有一个做内容审核的朋友在群里问“我现在想用 vLLM 部署 Qwen2-VL单卡到底跑不跑得动多卡是不是直接改个参数就行还有我网页上攒了一批图片怎么用爬虫的方式批量请求它做识别”这个问题把三个最常见的坑一次踩全了。Qwen2-VL 作为目前综合能力比较能打的多模态模型部署链路其实不算复杂但真正落地的时候显存估算、多卡通信、图片请求格式这三个环节每一个都能让人卡上半天。这篇记录就是我反复部署 Qwen2-VL 的经验总结覆盖单卡、多卡和 HTTP 批量请求的完整链路适合刚接触 vLLM、又需要赶紧把多模态服务跑起来的人直接参考。1. 部署前必须想清楚的三个问题显存、vLLM版本与模型选择1.1 显存需求不是“模型参数大小”算出来的很多人犯的第一个错误是拿“模型几个 B 参数”直接对应显存。Qwen2-VL 系列有 2B、7B、72B 三个主流尺寸其中 7B 版在 bf16 精度下权重就要占 15GB 左右72B 版更是直奔 150GB。但这只是权重还没算推理时的 KV cache 和激活值。KV cache 跟max-model-len直接相关上下文越长缓存占用越大多模态图片输入又会把视觉 token 塞进上下文里所以同样是 7B开 2K 上下文和开 32K 上下文显存占用能差出好几个 G。我实测下来的大概情况是这样的模型尺寸bf16 权重占用推荐最小显存备注Qwen2-VL-2B约 4.5GB16GB单卡 16G 能流畅跑Qwen2-VL-7B约 15GB24GB单卡 24G 可跑但并发受限制Qwen2-VL-72B约 150GB4×A100 80G需要张量并行单卡根本放不下如果你只有单张 24GB 卡先跑 7B 是性价比最高的方案。我通常建议把--max-model-len从模型默认值往下调一些比如设成 16384 或 32768不追求满上下文给 KV cache 留出余量换来的是更高的并发吞吐。别一上来就想跑满 128K 上下文多模态场景里一张高清图可能就吃几千个 token你真正能留给对话的上下文没想象中那么多。1.2 vLLM 版本差异远比你想的关键vLLM 对 Qwen2-VL 的支持是分阶段的。早期版本需要带着--trust-remote-code启动模型代码里还有一堆自定义逻辑后来 vLLM 直接原生支持了 Qwen2-VL 的视觉编码器和 preprocessor启动命令变干净了各种奇怪的报错也少了。如果你用的是老版本可能连image_url请求都处理不好表现就是返回结果完全忽略图片内容。我的建议是直接安装当前最新稳定版 vLLM不要因为生产环境“求稳”而锁死老版本。多模态模型这块迭代非常快新版本通常会修复视觉 token 越界、图片尺寸处理错误这些老 bug。当然升版本之前要看 release notes因为 vLLM 的 CLI 参数偶尔会改名。比如早期版本的--image-input-type后来就被更通用的多模态配置参数取代了。你实在不想折腾就用官方提供的安装命令装指定版本然后记住这个版本配套的 transformers 版本不要乱动。1.3 Qwen2-VL 作为多模态模型的特殊性Qwen2-VL 和纯文本模型部署最大的区别在于输入里的图片会被拆成视觉 patch然后映射成视觉 token。也就是说图片不是作为一个整体“塞进去”的而是被编码成一段 token 序列参与自回归生成。这带来两个直接影响第一图片的分辨率越高、内容越复杂视觉 token 数量就越多对显存和延迟的冲击越大第二大量图片批量请求时视觉编码过程会成为明显的 CPU 和 GPU 负担不只是 LLM 部分在费资源。所以部署前要建立预期多模态服务的性能瓶颈往往在“图片预处理 视觉编码”这一层而不是单纯看模型多大多小。实际生产中遇到请求变慢先排查是不是单张图太大或者是不是视觉编码器没有充分利用 GPU。新版 vLLM 提供了多模态处理器缓存等参数能够复用图像预处理结果对重复模板类型的图片有不错的效果后面我会详细说。2. 单卡部署全流程启动命令、日志验证与第一个请求2.1 两条部署路线部署 Qwen2-VL 有两条主流路线一条是用vllm serve把它起成 HTTP 服务另一条是在 Python 脚本里直接实例化LLM类做离线推理。如果你最终需要接入业务系统、让其他程序通过请求调用我强烈建议走服务化路线。好处很明显vLLM 帮你把请求排队、连续批处理、并发控制都做了你只需要对一个标准 HTTP 端点发起 POST 请求。离线调用的方式也有它的场景——批量处理几千张本地图片时省去了 HTTP 开销但是需要自己处理并发和内存显存管理代码复杂度反而更高。所以我这篇的重心放在服务化部署上这也是标题里“爬虫请求”能跑通的前提。2.2 核心启动参数逐个拆解先用一条最基础的单卡启动命令vllm serve Qwen/Qwen2-VL-7B-Instruct \ --max-model-len 32768 \ --gpu-memory-utilization 0.92 \ --port 8000 \ --host 0.0.0.0每个参数我都解释一下因为后面排障全靠理解它们max-model-len模型允许的最大序列长度。它包含文本 token 和图片转换出来的视觉 token。27 级以下的小显存卡建议先设 16384 试试不够再加。gpu-memory-utilization允许 vLLM 占用的显存比例上限默认 0.9。我设 0.92 是因为给 CUDA context 和少量预留留了空间。千万别设成 1.0容易在申请显存的时候直接崩。host 0.0.0.0监听所有网卡地址这样局域网内其他机器也能访问。如果只是本机调试用127.0.0.1更安全。port服务端口默认 8000。如果你的机器处于一个没有外网直连的环境需要提前把模型下载到本地目录然后通过环境变量指定镜像源再用本地路径替换上面的模型名export HF_ENDPOINThttps://hf-mirror.com huggingface-cli download Qwen/Qwen2-VL-7B-Instruct --local-dir /data/models/qwen2-vl-7b vllm serve /data/models/qwen2-vl-7b --max-model-len 32768 --gpu-memory-utilization 0.922.3 启动日志和 curl 验证启动过程会刷一堆日志重点看三处。第一处是Loading model weights附近的耗时模型权重加载慢说明磁盘 IO 是瓶颈。第二处是类似GPU KV cache size: 12345 tokens的输出这个数字直接告诉你当前配置下能缓存多少 token如果太小压并发时很容易 OOM。第三处是最后的Application startup complete和Uvicorn running on http://0.0.0.0:8000看到这行才说明服务真正就绪。验证服务最简单的方式是 curlcurl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: Qwen/Qwen2-VL-7B-Instruct, messages: [{ role: user, content: [ {type: text, text: 这张图片里有什么}, {type: image_url, image_url: {url: https://example.com/test.jpg}} ] }], max_tokens: 512 }请求里的model字段要和启动命令里的模型名一致。如果启动时用的是本地路径这里也要填本地路径不填或者填错会返回 model not found 的错误。图片可以用公网 URL也可以用 base64 字符串后面讲爬虫请求时我会给出更完整的示例。2.4 什么时候该考虑升级多卡单卡部署跑通之后下一步要判断是不是需要多卡。主要有三种信号一、显存不够启动就报Not enough memory二、单图请求的延迟高到不可接受但这时候加卡不一定能解决因为可能是视觉编码瓶颈三、并发请求一多就排队超时这才是多卡或者多副本能救的。明确一点如果单卡已经能装下模型只是并发不够优先考虑增加服务副本做负载均衡而不是盲目上张量并行。3. 多卡部署实战张量并行、显存分布与常见坑3.1 张量并行为什么是必需品大模型的多卡部署方式主要有三种张量并行、流水线并行、数据并行。在 vLLM 里你用--tensor-parallel-size指定的是张量并行。张量并行的思路是把一个模型的权重矩阵按行或列切开分布到多张卡上每张卡只持有模型的一部分前向计算时通过集合通信把结果拼起来。这样一来单卡放不下的 72B 模型可以被拆到多张卡上运行。流水线并行则是把模型按层切开卡 0 算完前几层结果传给卡 1 算后面几层通信模式是点对点但存在 GPU 利用率不均衡的问题。数据并行则是每张卡上放完整模型各自处理不同请求适合“模型能放下、但要堆吞吐量”的场景。我打个比方张量并行像是把一本厚书拆成几册分给几个人大家同时翻开同一页拼出完整内容数据并行则是每个图书管理员手里都有一本完整复印本各忙各的。所以 7B 这种单卡能塞下的模型想提升吞吐优先考虑数据并行思路而不是把模型切成两半去跑张量并行——切了之后通信开销可能比收益还大。3.2 多卡启动命令与显存检查多卡部署 72B 模型的命令如下vllm serve Qwen/Qwen2-VL-72B-Instruct \ --tensor-parallel-size 4 \ --max-model-len 65536 \ --gpu-memory-utilization 0.90 \ --dtype bfloat16这里的--tensor-parallel-size 4表示把模型切成 4 份放到 4 张卡上。这个数值必须和你实际可用的 GPU 数量整除。比如你有 8 张卡设 4 是可以的如果只有 3 张卡设 4 启动就会直接报错。想只使用其中一部分卡先用环境变量限定CUDA_VISIBLE_DEVICES0,1,2,3 vllm serve ...启动后用nvidia-smi查看显存分布。正常情况下每张卡的显存占用应该差不多。如果出现某张卡明显偏高、某张卡几乎空闲多半是通信或者负载分配出了问题。先检查驱动版本是否一致再检查 vLLM 的分布式运行环境是否正常。3.3 多卡部署的常见坑第一个坑是卡间通信协议。张量并行非常依赖卡间带宽A100/A800 之间的 NVLink 带宽很充足但 4090 这类卡之间只能走 PCIe通信延迟高尤其在max-model-len较大、batch 较大的时候性能可能还不如单卡跑小模型。所以不是“多卡一定更快”。第二个坑是CUDA_VISIBLE_DEVICES的顺序。多卡机器上显卡的物理编号不一定从 0 开始连续你设置的这个环境变量会直接决定 vLLM 能看到哪几张卡。曾经遇到过一台机器有四张卡但只有两张被系统识别的情况排查了一会儿才发现是驱动环境下有一个卡被其他进程占着vLLM 默认会尝试使用所有可见 GPU。第三个坑是多卡时的量化选项。72B 模型跑 bf16显存非常紧张。如果显存不够可以考虑加载 AWQ 或 GPTQ 量化版本并在启动命令里追加--quantization awq。注意量化版本要和量化方式匹配加载原版模型却开了量化参数会直接报不兼容错误。4. “爬虫请求”落地批量图片采集与 vLLM 接口调用4.1 先理清“爬虫请求”这条链路标题里的“爬虫请求”实际项目场景是这样的你先有一个采集程序从公开网页上收集了一批图片 URL然后要把这些图片批量发给 vLLM 服务做视觉理解。整体链路是网页采集 - 图片过滤 - 编码或转 URL - 并发请求 - 结果落库。不要把“爬虫”想复杂了。vLLM 本身不会去爬网页它只负责接收请求、返回理解结果。真正的难点在于两个一是如何把网页图片干净地提取出来二是如何高效地把几千张图喂给模型服务而不把服务打崩。4.2 从公开网页采集图片的合规与过滤这里必须先把底线说清楚只能采集公开站点上允许访问的资源遵守目标站点的 robots 协议设置合理的请求间隔和 User-Agent不要做高频遍历也不要把采集到的数据用在超出个人项目测试的范围。这是技术问题更是安全合规问题别在小项目上栽跟头。一个简单的图片采集示例import requests from bs4 import BeautifulSoup from urllib.parse import urljoin headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 } resp requests.get(https://example.com/gallery, headersheaders, timeout10) soup BeautifulSoup(resp.text, html.parser) urls [] for img in soup.find_all(img): src img.get(src) if not src: continue full_url urljoin(resp.url, src) if full_url.lower().endswith((.jpg, .jpeg, .png, .webp)): urls.append(full_url) urls list(set(urls))注意几个细节网页图片经常是相对路径必须用urljoin拼成绝对地址图床经常给不同尺寸的裁剪 URL如果你只需要原图可能要再根据 URL 规则处理一下很多站点有防盗链直接下载会返回 403需要在下载请求里带上Referer头。4.3 Python 批量请求 vLLM 的完整示例图片拿到手之后有两种喂给 vLLM 的方式外链 URL 和 base64 字符串。用外链 URL 的时候vLLM 所在服务器要能访问到图片地址一旦源站加了防盗链请求就会失败。所以我更推荐先把图片下载到本地再转 base64 发给服务这样最稳。下面是完整的请求示例import base64 import json import time import requests ENDPOINT http://127.0.0.1:8000/v1/chat/completions MODEL_NAME Qwen/Qwen2-VL-7B-Instruct def encode_image(image_path): with open(image_path, rb) as f: return base64.b64encode(f.read()).decode(utf-8) def ask_vllm(image_path, prompt请描述这张图片的内容, max_tokens256): img_b64 encode_image(image_path) payload { model: MODEL_NAME, messages: [{ role: user, content: [ {type: text, text: prompt}, {type: image_url, image_url: { url: fdata:image/jpeg;base64,{img_b64} }} ] }], max_tokens: max_tokens, temperature: 0.2 } resp requests.post(ENDPOINT, jsonpayload, timeout120) resp.raise_for_status() data resp.json() return data[choices][0][message][content]这段代码里有几个容易忽略的点data:image/jpeg;base64,前面的 MIME 类型要和图片实际格式一致。图片是 PNG 却写成 JPEG部分情况下也能识别但最好按真实格式写。timeout120不要设太小。大图加长文本可能超过 30 秒设 120 秒是合理的。temperature0.2是我处理识别类任务时常用的值。做内容理解和信息抽取时随机性越低越好如果是做创意文案可以调高。如果你想一次批量处理一个图片目录加一个循环就行from pathlib import Path results [] for img_path in Path(./images).glob(*.jpg): desc ask_vllm(str(img_path)) results.append({image: str(img_path), description: desc}) print(f{img_path.name}: {desc[:50]}...)4.4 并发、限速和结果落库批量处理成百上千张图片时单线程循环太慢了。可以用线程池并发发请求但并发数不能无脑调高。vLLM 自己有连续批处理能力它会把多个请求拼成一个 batch 喂给 GPU所以发给它的并发请求数不等于 GPU 上的真实 batch 数。但你并发太高时排队在服务端的请求会占内存反而拖慢整体速度。我的经验是先用 4 个并发线程试水观察 GPU 利用率和单请求延迟再逐步增加。下面是带并发控制的版本from concurrent.futures import ThreadPoolExecutor, as_completed def handle_one(path): try: desc ask_vllm(str(path)) return path, desc, True except Exception as e: return path, str(e), False with ThreadPoolExecutor(max_workers4) as pool: tasks [pool.submit(handle_one, p) for p in Path(./images).glob(*.jpg)] for fut in as_completed(tasks): path, desc, ok fut.result() if ok: save_result(path, desc) else: print(ffailed: {path}, error: {desc})结果落库我用的是最简单的 JSONL每行一条结果方便后面清洗和分析def save_result(image_path, desc): record {image: str(image_path), description: desc, time: time.time()} with open(results.jsonl, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n)5. 踩坑记录与调优清单5.1 显存不足类的报错与处理运行中最常见的报错有两类。一类是ValueError: Not enough memory to allocate KV cache这类错误说明 KV cache 空间分配不出来优先处理顺序是降低--max-model-len、降低--gpu-memory-utilization、更换量化模型。我一般先把max-model-len从 32768 降到 16384效果立竿见影。另一类是torch.OutOfMemoryError: CUDA out of memory如果在运行中才冒出这种错误往往是并发过高某个请求的激活值瞬时超过预留空间。这时候要么降低--max-num-seqs要么给gpu-memory-utilization多留一点余量比如从 0.92 降到 0.85。5.2 图片输入相关的坑图片问题在我这边踩得最多。首先是大图。Qwen2-VL 内部会把图片缩放成多个 patch 处理但你给一张 8000×6000 的图预处理阶段就会耗费大量内存和时间请求很容易超时。建议在采集端先做压缩控制在 1024px 以内from PIL import Image img Image.open(image_path) img img.convert(RGB) img.thumbnail((1024, 1024)) img.save(resized.jpg, quality85)其次是没有扩展名的图片文件。很多网页图片 URL 不包含.jpg或者因为签名参数导致扩展名是别的下载下来后 Python 猜不到格式base64 编码时 MIME 写错vLLM 就会解码失败。处理方式是下载后直接用 PIL 打开并重存为统一格式不要依赖原始文件名。5.3 并发与服务配置调优新版本 vLLM 默认的并发上限其实不低普通场景下不用专门调。但如果你发现请求一多单请求延迟暴涨可以用这几个参数控制--max-num-seqs限制单个 batch 内最多有多少个序列默认通常较高。降它能让 GPU 更均匀地处理请求避免某个大请求卡住整批。--enable-prefix-caching开启前缀缓存。如果你大量请求共用同一段系统提示词或者同一个开场白这个参数能显著降低重复计算的延迟。--limit-mm-per-prompt限制每个请求中多模态内容项的数量。比如--limit-mm-per-prompt image5表示每个请求最多 5 张图。防止有人一次塞 50 张图把服务拖垮。另外生产环境一定要加鉴权。vLLM 支持--api-key参数设置了之后请求头需要带Authorization: Bearer API_KEY。别裸奔在公网上分分钟被人拿去刷量。5.4 生产化要注意的几个附加点如果用 Docker 部署 vLLM记住容器要设置共享内存。默认的/dev/shm大小可能只有 64MB多进程通信时直接报错。启动容器时加上docker run --gpus all --ipchost -p 8000:8000 -v /data/models:/models ...如果不想用--ipchost至少把--shm-size设置为 16g 以上。还有部署在云服务器上时记得检查安全组和防火墙是否放行了 8000 端口不然从外部怎么请求都不通还以为服务没起来。6. 最后的一点部署心得从单卡 7B 到多卡 72B再到批量图片请求的整套流程我前后折腾了很久。最大的体会是不要一上来就追求“部署一个最大最强的模型”而是先把 7B 模型在单卡上完整跑通包括 HTTP 请求、图片编码、并发批处理形成一个可以闭环的 pipeline再去考虑扩卡扩规模。很多时候你会发现瓶颈根本不在模型大小而在图片采集清洗和请求设计上。另外一个很有用的技巧是vLLM 服务自带一个/metrics端点输出 Prometheus 格式的指标。用脚本定期拉一下 token 吞吐、排队请求数、KV cache 使用率后面调参就有数据支撑了不用靠猜。最后提醒一句模型迭代快vLLM 更新也快遇到参数不生效或者报错先看 release notes别拿几个月前的经验硬套新版本。