
最近在业务里遇到一个很典型的需求让系统能够“看懂图片”。输入是产品截图、工单图片、纸质单据照片模型不仅要理解图里的内容还要给出描述或者回答。纯文本大模型肯定做不到最终我们选了 Qwen2-VL 配合 vLLM 来做服务化部署。这套方案把单卡和多卡场景都跑通了后面还写了一套爬虫脚本去批量调用模型接口算是把部署、压测、稳定性验证的完整流程走了一遍。这篇文章会把整个过程拆开来讲包括环境选型、启动参数、多卡并行原理、多模态请求体构造以及各种能预见的坑。适合刚接触视觉语言模型部署、想在本地或内网环境快速拉起服务并接业务的人。1. 部署前的整体规划为什么是 Qwen2-VL 配合 vLLM1.1 多模态模型落地的现实约束实际业务里的图片大多属于内部数据比如工单截图、产品页、扫描件不可能全部发到外部接口去处理私有化部署几乎是唯一选择。多模态开源模型里Qwen2-VL 的视觉理解能力比较均衡能处理单图、多图和视频输入在中文场景下的 OCR、版面理解也表现不错。它有几个不同的规格2B 可以放在小显存环境里验证链路7B 适合作为日常服务72B 交给算力更充足的机器。而且 vLLM 原生支持这类模型架构直接把权重路径传给 vLLM 就能运行不需要额外改模型代码。真正决定部署方案的往往是显存所以硬件约束必须先说清楚。1.2 vLLM 为什么更适合服务化部署如果直接用推理框架逐条处理请求每来一个用户都要重新加载权重、独占显存做前向计算GPU 利用率非常低。vLLM 做对了三件事第一把 KV cache 分成固定大小的块按需分配解决了显存碎片化的问题第二实现了连续批处理当一个请求在生成 token 的间隙其他请求可以插进来一起计算吞吐量提升非常明显第三自带一个兼容常用大模型 API 格式的 HTTP 服务层业务侧直接用 HTTP JSON 就能调用不用自己拼底层协议。这三点的组合让 vLLM 成为目前私有化部署推理服务的主流方案。尤其是长上下文和并发请求同时存在时连续批处理带来的收益非常可观。1.3 整体链路与资源估算整条链路是模型权重文件 → vLLM 进程加载模型对外暴露 HTTP 端口→ 业务脚本批量请求。资源评估方面一个粗略但好用的经验是半精度下每 10 亿参数大约占 2GB 显存。7B 模型权重约 14GB一张 24GB 的卡能放下但留给 KV cache 的余量取决于上下文长度72B 模型权重约 144GB单卡无论如何都放不下必须上多卡。除了权重还要考虑 KV cache、图片变换出来的视觉 token、激活值缓存。部署前先按这个量级估算一下能少踩很多弯路。2. 环境准备与模型权重获取先把“原料”备好2.1 硬件、驱动与基础软件版本基线显存是最核心的约束其次是显存带宽。想要流畅跑 7B 级别模型建议至少一张 24GB 显存的卡跑 72B 则要准备 4 张 80GB 以上的卡。系统层面显卡驱动需要支持当前使用的 CUDA 版本一般建议 CUDA 12.1 以上。Python 版本建议 3.10 或 3.11太老的版本容易在安装 vLLM 时遇到编译问题。建议先把 PyTorch 装好确认torch.cuda.is_available()返回正常再安装 vLLM。这个顺序很重要先装 vLLM 再装 PyTorch很容易碰到版本不匹配然后报一堆算子相关的错误。vLLM 更新很快不同版本的参数和算子支持有差异建议用相对新的稳定版本不要盲目追最新。2.2 权重获取与目录结构确认Qwen2-VL 的权重可以从开源模型托管站点下载选择本地网络访问顺畅的渠道就好。下载完成之后权重目录里必须包含 config.json、tokenizer.json、tokenizer_config.json以及一个或多个 safetensors 文件。启动前一定要检查目录完整性重点看 config.json 里的 model_type 和 vision_config 是否正常。如果是从某些渠道拉下来的文件可能在传输中出现截断启动时才报错到那时再排查就比较被动了。稳妥做法是下载后先做一次哈希校验或者直接用模型仓库自带的校验记录比对。vLLM 加载模型时会先读取 config.json如果格式不对或者缺少视觉相关配置进程会在加载阶段直接退出。2.3 启动前的三项快速检查第一确认 GPU 能被系统识别执行nvidia-smi能看到卡的数量和显存大小。第二确认 PyTorch 能看到 GPU在 Python 里执行python -c import torch; print(torch.cuda.is_available(), torch.cuda.device_count())如果输出 False 或者设备数为 0说明驱动或 CUDA 环境有问题。第三确认端口没有被占用vLLM 默认监听 8000 端口如果机器上已经有服务占用要么关掉旧进程要么启动时改端口。这三项检查一分钟就能完成但能避免后面启动失败时在错误方向上折腾。3. 单卡部署实操从启动命令到第一个接口响应3.1 新版命令与最小启动参数vLLM 新版直接提供了一个vllm serve命令早期版本需要写更长的一串启动脚本。先看一个最小启动示例vllm serve /data/models/Qwen2-VL-7B-Instruct \ --served-model-name qwen2vl \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --dtype auto这里几个关键参数要说明一下。--served-model-name是给接口层使用的模型别名调用 API 时 model 字段必须写这个值不设置的话默认用真实模型名实际使用中很容易因为名字不一致导致接口 404。--max-model-len控制模型能接受的最大上下文长度包括文本和视觉 token这个值直接影响 KV cache 预留大小通常设为 8192 或 16384 够用如果业务里没有超长文档没必要拉太高。--gpu-memory-utilization表示 vLLM 最多可以占用多少比例的显存默认 0.9一般建议 0.85 到 0.9太低会浪费显存太高则可能把显存撑爆。--dtype auto让系统根据硬件自动选择半精度格式。3.2 验证服务模型列表与首轮多模态请求启动之后日志里会看到 vLLM 打印模型加载和端口监听信息。先在另一个终端验证服务是否正常curl http://localhost:8000/v1/models返回结果里能看到模型列表。接着构造第一个多模态请求。图片有两种传法传公网可访问的图片 URL或者传 base64 编码的 Data URI。本地文件建议用 base64避免服务端还需要出网拉图。示例代码如下import requests import base64 image base64.b64encode(open(test.jpg, rb).read()).decode() payload { model: qwen2vl, messages: [ { role: user, content: [ {type: image_url, image_url: {url: fdata:image/jpeg;base64,{image}}}, {type: text, text: 描述这张图片的内容} ] } ] } resp requests.post(http://localhost:8000/v1/chat/completions, jsonpayload, timeout120) result resp.json() print(result[choices][0][message][content])多模态请求的 content 必须是数组不能是一个普通字符串。数组里每个元素包含 type 字段可以是 image_url也可以是 text。很多第一次接触的人把 content 直接写成字符串vLLM 会返回 400 错误。3.3 单卡场景下的参数调节经验单卡部署并不只是一个启动命令的事情参数之间是相互制约的。显存总量固定时max-model-len 越大KV cache 占用越多留给并发 batch 的空间就越少。如果一张卡既想处理长文档又想同时服务多个请求很可能出现启动成功后一旦并发上来就 OOM。实际项目里建议先根据业务最长输入设置 max-model-len再观察显存占用最后调整 gpu-memory-utilization。另外--max-num-seqs参数可以控制单个 batch 内最多并行处理的序列数默认值在某些版本下可能偏大。并发不高时反而应该调小一点避免多个请求挤在同一 batch 里拖慢单个请求的响应速度。4. 多卡部署张量并行原理、启动参数与显存分配4.1 什么时候必须从单卡切到多卡两种情况需要多卡一是模型权重本身超出单卡显存比如 72B 半精度需要 144GB 以上二是单卡吞吐跑不满业务峰值需要通过并行推理提升并发能力。如果只是显存放不下用 CPU offload 或量化也可以缓解但性能损失明显。多卡部署能让多个 GPU 同时承担推理计算吞吐往往能翻几倍。但也要清楚多卡不是万能的卡间通信会成为新的瓶颈尤其当模型较小而卡数较多时通信开销可能超过并行收益。4.2 张量并行与流水线并行怎么理解vLLM 支持两种并行方式。张量并行是把模型权重切分到多张卡上每一层都分布在所有卡里前向计算时卡与卡之间需要同步交换数据适合单机多卡。流水线并行则是按层切分模型的第 1 到第 N 层放在卡 A第 N1 到第 M 层放在卡 B数据一层一层往下传适合跨机场景但存在流水线气泡利用率可能不如张量并行。对大多数单机多卡场景来说优先使用张量并行。实际启动时通过--tensor-parallel-size指定使用多少张卡比如 2 表示两块卡组成一个并行组。卡数不是随意定的要能被模型内部结构整除常见选择是 2、4、8。4.3 多卡启动命令与通信环境变量多卡启动和单卡很类似核心差异是指定并行参数。以两张卡为例vllm serve /data/models/Qwen2-VL-7B-Instruct \ --served-model-name qwen2vl \ --tensor-parallel-size 2 \ --pipeline-parallel-size 1 \ --max-model-len 12288 \ --gpu-memory-utilization 0.9启动完成后日志中能看到张量并行相关信息。多卡之间通信依赖 NCCL 库如果通信初始化失败配置环境变量往往能解决export NCCL_DEBUGINFO export NCCL_P2P_DISABLE1NCCL_DEBUG 会打印通信细节方便定位是哪一步卡住。NCCL_P2P_DISABLE 主要用在某些不支持 GPU 间直接内存访问的环境强制走显存拷备路径牺牲一点速度换取稳定。还有一种情况是只有普通以太网而没有专用高速网络此时可能需要设置NCCL_IB_DISABLE1关闭远程直接内存访问否则 NCCL 会在初始化时等待超时。4.4 多卡场景的显存分配与并发上限多卡部署时gpu-memory-utilization 对每张卡分别生效建议保持默认 0.9如果某张卡的显存被其他进程占用可以单独调低。vLLM 会在启动时根据模型权重、已配置的 max-model-len 和 gpu-memory-utilization 自动计算 KV cache 大小日志里能看到分配结果。多卡下的并发控制同样靠 max-num-seqs但这个参数是全局的作用于整个并行组而不是单卡叠加。还要注意多卡的总 KV cache 是逻辑上共享的不要简单用“单卡显存 × 卡数”去算可用 KV cache实际值要看 vLLM 日志打印。如果发现一张卡显存使用特别高而另一张很低多半是通信或并行配置出现了问题。5. 爬虫请求批量调用多模态接口的适配层实现5.1 请求体结构从文本到多模态vLLM 暴露的接口兼容常见的聊天补全协议核心是 POST /v1/chat/completions。纯文本请求的 content 可以是一个数组也可以是一个普通字符串但多模态请求必须使用数组形式。一个图片请求体包含两个关键部分model 字段必须与启动时设置的 served-model-name 一致content 数组里的每个对象要么是 image_url 类型要么是 text 类型。image_url 支持传入公网 URL 或 base64 Data URI。公网 URL 的好处是请求体体积小缺点是 vLLM 进程需要能访问外网而且图片网站可能有限流导致拉图超时。base64 方式最稳定缺点是图片越大请求体越大服务端解析也会更慢。如果要传多张图就在 content 数组里多放几个 image_url模型会按顺序理解。视频可以拆帧成多个 image_url 传入。调 max_tokens 时要注意多模态输入的视觉 token 已经占用了上下文长度给生成的文本留下合理空间即可。5.2 批量抓取与并发请求的工程化所谓“爬虫请求”本质就是写一个 HTTP 客户端去批量调用模型接口。最简单的场景是预先准备一批图片 URL逐个请求模型。如果图片列表来自网页可以先写一个爬虫提取图片链接再用同样的会话去请求模型。批量调用不能用一个 for 循环串行跑那样吞吐太低应该用线程池并发。示例思路from concurrent.futures import ThreadPoolExecutor, as_completed def call_one_image(model_name, image_path, prompt): # 读取图片转 base64 # 构造 payload # 请求 vLLM 接口 # 返回内容或异常 pass with ThreadPoolExecutor(max_workers8) as pool: futures [pool.submit(call_one_image, qwen2vl, path, prompt) for path in image_paths] for future in as_completed(futures): result future.result() # 收集结果记录失败项并发数不要盲目拉高。vLLM 本身有 batch 能力并发请求越多单 batch 越大吞吐会上升但单请求延迟也会增加。如果业务要求的是稳定单请求延迟并发数要保守一点如果追求整体吞吐可以逐步提高并发观察显存占用和响应时间的变化。一个稳妥做法是客户端做限速比如固定每秒只发起 N 个请求避免把模型服务打垮。5.3 流式输出的解析与半结构化结果对话场景中用户往往希望模型边生成边输出。请求体里加一个stream: true服务端会返回 SSE 格式的流式数据。使用 requests 请求时设置streamTrue然后逐行解析import json resp requests.post(http://localhost:8000/v1/chat/completions, jsonpayload, streamTrue, timeout120) for line in resp.iter_lines(): if not line: continue text line.decode(utf-8) if not text.startswith(data: ): continue data text[6:] if data [DONE]: break try: obj json.loads(data) delta obj[choices][0][delta].get(content) if delta: print(delta, end, flushTrue) except json.JSONDecodeError: continue流式响应每次返回的是一个片段通过 delta.content 累积起来才是完整文本。边界是固定的data: [DONE]收到它就表示生成结束。流式接口的超时设置要比普通接口更宽松因为第一个 token 可能需要较长的思考时间客户端如果按普通接口的 30 秒超时极容易误判。5.4 一个可直接套用的批量调用脚本综合上面几点可以整理成一个可复用的适配层。这个脚本适合批量处理一批本地图片或图片 URL输出每个图片对应的模型回复同时带超时和重试逻辑。核心注意点有三个图片读取和 base64 编码尽量在子线程中做好避免阻塞请求线程失败请求要记录原因不要直接丢弃服务端一旦返回非 200 状态码要打印响应体因为 vLLM 的错误信息基本都在 body 里。另外建议把结果写成 JSON 文件方便后续核对而不是只打印到终端。6. 常见问题与排查技巧实录6.1 显存 OOM这是遇到最多的问题。模型加载阶段就 OOM说明单卡显存放不下模型要么换小规格模型要么开启量化要么增加卡数。运行时 OOM 则多半是并发太高或上下文太长处理方式是把 max-model-len 调低、把 gpu-memory-utilization 从 0.9 降到 0.85同时调大 max-num-seqs 的下限而不是盲目并发。如果是量化方案AWQ/GPTQ 这类格式可以把 7B 模型权重压到 8GB 左右但多模态部分对量化的支持因版本而异视觉编码器在极端量化下可能有轻微精度损失需要实测。6.2 接口返回 400/404 的常见原因400 错误绝大多数是请求体格式问题尤其是 content 传成了字符串而不是数组或者 image_url 缺少 url 字段。404 错误通常是 model 字段和 served-model-name 不一致启动时设置的别名是 qwen2vl请求里写的却是别的名字自然找不到模型。422 错误属于字段校验失败比如 temperature 超出范围、messages 里缺少 role。排查这类问题最快的方式是打印 vLLM 的响应 body错误信息会写得非常明确。6.3 多卡通信失败与 NCCL 配置多卡部署时如果日志卡在 NCCL 初始化或者报出无法连接 peer 的信息先确认各卡是否都空闲再检查驱动和 NCCL 版本。部分环境不支持 GPU 间的直接内存访问设置NCCL_P2P_DISABLE1可以绕过去。如果是多机部署或者网络环境普通按要求设置好NCCL_IB_DISABLE也有帮助。多卡通信问题最常见的根源是网络权限而不是模型本身。6.4 上下文长度与视觉 token 估算图片进入模型后会先被切分成多个 patch再转换成视觉 token。Qwen2-VL 采用动态分辨率策略同一张图在不同尺寸下产生的 token 数不同。一张 1000 像素级别的普通图片通常会产生数百到上千个视觉 token具体取决于图像内容和分辨率。这导致一个常见的误判文本很短请求看起来很小但图片 token 已经占了很大上下文。如果 max-model-len 设置得不够服务端会直接报错。排查时可以看 vLLM 日志中每个请求的 prompt_tokens 字段它会明确告诉你这个请求消耗了多少上下文长度。6.5 性能排查与日志分析vLLM 提供了 metrics 接口返回 Prometheus 格式的指标可以看到吞吐、延迟、当前排队请求数。GPU 利用率不高时先看是不是卡在 CPU 的数据预处理环节比如图片解码、base64 转码、token 化在大规模并发下会占用不少 CPU。如果 GPU 利用率高但延迟依然很长多半是 max-model-len 设置过大导致 KV cache 碎片化或者并发数过高导致 batch 内序列过多。日志方面启动时重点看模型加载阶段有没有 warning运行中重点看错误栈和 OOM 信息。最后说几个实际操作中积累下来的体会。第一先跑通单卡再考虑多卡。很多问题其实出在请求体格式上多卡并行只会让排查更困难。第二max-model-len 不要贪大能覆盖业务最长输入就够了剩下的显存留给 KV cache 去换吞吐。第三客户端必须做超时和重试vLLM 处理长 prompt 或大图时首 token 延迟会比较长客户端默认 30 秒超时很容易误杀请求。第四多模态请求的 content 一定用数组这是我在实际对接中见过最多的一种低级错误。暂时就写到这里这套链路目前运行稳定后续如果再压一下视频输入场景有结论了再回来补充。