多模态模型的部署比纯文本模型多一层考量:输入不再只是 token 流,图片和视频都要经过视觉编码进入上下文。这篇把 Qwen3-VL-4B-Instruct 部署到 vLLM 上,依次验证日常问答、图片描述和视频推理三类负载,显存预算参考单张 RTX 5090 [1]。
模型定位
Qwen3-VL-4B-Instruct 是 Qwen3-VL 系列的视觉语言模型,文本理解与生成、视觉感知与推理、空间与视频动态理解三条腿都站得住 [1]。评测结果显示它在 STEM、VQA、OCR、视频理解和智能体任务上能与 GPT-5-Mini、Claude-4-Sonnet 掰手腕 [1]。注意它是非推理模型——想要带思考模式的版本,同尺寸有 Qwen3-VL-30B-A3B-Thinking,更大有旗舰级的 Qwen3-VL-235B-A22B-Thinking [1]。
多模态输入的显存开销值得提前理解:图片先切成 patch 再进视觉编码器,一张高分辨率图的 token 开销可达数千,视频更是按帧展开,一段几十秒的片段轻松吃掉上万 token 的上下文。所以 --max-model-len 的规划不能按纯文本对话的口径来——单图问答 8K 通常够用,多图或视频就要重新算账。这也是为什么同一张卡上,多模态实例的并发能力往往低于纯文本实例。
环境与安装
参考环境:Ubuntu 22.04、Python 3.12、CUDA 12.8、PyTorch 2.8.0 [1]。驱动、CUDA 与 PyTorch 编译版本三者必须匹配,装完先自检再继续 [1]。依赖四个包全部锁版本 [1]:
pip install vllm==0.11.0
pip install openai==2.3.0
pip install modelscope==1.30.0
pip install qwen_vl_utils==0.0.14qwen_vl_utils 是多模态输入的预处理件,后面视频推理那节会用到。模型从 ModelScope 下载,cache_dir 换成自己的路径 [1]:
# model_download.py
# 注意修改cache_dir为保存的路径
from modelscope import snapshot_download
model_dir = snapshot_download('Qwen/Qwen3-VL-4B-Instruct', cache_dir='请修改我!!!', revision='master')
print(f"模型下载完成,保存路径为:{model_dir}")服务启动与参数
单卡启动一条命令,每个参数都有明确含义 [1]:
CUDA_VISIBLE_DEVICES=0 python -m vllm.entrypoints.openai.api_server \
--model 请修改我!!!/Qwen/Qwen3-VL-4B-Instruct \
--served-model-name Qwen3-VL-4B-Instruct \
--max-model-len 8192 \
--tensor-parallel-size 1 \
--port 8085 \
--trust_remote_code \
--gpu_memory_utilization 0.9CUDA_VISIBLE_DEVICES 挑卡;--max-model-len 限制单请求的输入加输出总长,调大吃显存也更容易 OOM;--gpu_memory_utilization 是 vLLM 的显存占用目标比例,OOM 时可以往上加;--served-model-name 决定客户端请求里 model 字段该写什么;--trust_remote_code 必须开,否则部分模型初始化不了 [1]。看到 Application startup complete 就是启动成功,服务兼容 OpenAI 协议,SDK 直接能用 [1]。
应用场景上,这套接口能直接支撑的业务面很宽:票据和文档的 OCR 抽取、界面截图的理解与测试、电商图片的内容审核、教育场景的题目讲解,走的都是同一套 image_url 消息格式。接入方只需要把图片转成 URL 或 base64,协议层的改动为零——这是沿用 OpenAI 协议最大的工程红利。
图文调用
日常问答和图片描述走同一个 SDK 入口。先测一个考验逐字符处理能力的问题 [1]:
from openai import OpenAI
openai_api_key = "EMPTY"
openai_api_base = "http://127.0.0.1:8085/v1" # 使用正确的端口
daily_chat_message = "将“I love Qwen3-VL-4B-Instruct”这句话的所有内容反过来写"
# 实例化OpenAI client
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
# 日常问答
daily_chat_response = client.chat.completions.create(
model="Qwen3-VL-4B-Instruct",
messages = [
{
"role": "user",
"content": daily_chat_message
}
]
)
print(f"Qwen3-VL-4B-Instruct日常问答: {daily_chat_response.choices[0].message.content}")
print("-"*100)模型分步推理后给出了完全正确的逐字符反转:tcurtsnI-B4-LV-3newQ evol I [1]。图片描述则是把 image_url 和文本放进同一条消息 [1]:
# 图片描述
image_des_response = client.chat.completions.create(
model="Qwen3-VL-4B-Instruct",
messages = [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen-VL/assets/demo.jpeg"
}
},
{"type": "text", "text": "Describe this image."},
],
}
]
)
print(f"Qwen3-VL-4B-Instruct图片描述: {image_des_response.choices[0].message.content}")输出相当详尽:认出了黄色拉布拉多、黑白格子法兰绒衬衫、带彩色爪印的蓝色胸背带、金色晨昏光,连狗与主人击掌的互动都捕捉到了 [1]。人工核验两个回复都不错;和 Qwen2-VL-2B-Instruct 对同一张图的描述对比,Qwen3-VL-4B 在细节还原、情感氛围、结构组织、语言表现力四个维度全面胜出 [1]。多模态消息的关键差别就在 content 的结构——从字符串升级为数组,图片和文本按顺序混排,协议层面与 OpenAI 完全一致 [1]。
视频理解的输入管线还可以再拆细一点:process_vision_info 按模型要求的帧率抽帧,帧数由 video_kwargs 控制,返回的元数据里带着每帧的位置信息;prompt 里的 <|video_pad|> 占位符会被展开成与帧数对应的视觉 token 序列。想让模型看得更细,就提高抽帧密度,代价是上下文和显存同步上涨——视频任务的参数调优本质上是在清晰度与预算之间找平衡点。
视频离线推理
视频推理不用 serve 服务,改走离线 LLM 入口。先处理环境变量和导入 [1]:
import torch
from qwen_vl_utils import process_vision_info
from modelscope import AutoProcessor
from vllm import LLM, SamplingParams
import os
os.environ['VLLM_WORKER_MULTIPROC_METHOD'] = 'spawn'qwen_vl_utils 负责把视频消息拆成模型吃的多模态输入 [1]:
def prepare_inputs_for_vllm(messages, processor):
text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
# qwen_vl_utils 0.0.14+ reqired
image_inputs, video_inputs, video_kwargs = process_vision_info(
messages,
image_patch_size=processor.image_processor.patch_size,
return_video_kwargs=True,
return_video_metadata=True
)
print(f"video_kwargs: {video_kwargs}")
mm_data = {}
if image_inputs is not None:
mm_data['image'] = image_inputs
if video_inputs is not None:
mm_data['video'] = video_inputs
return {
'prompt': text,
'multi_modal_data': mm_data,
'mm_processor_kwargs': video_kwargs
}主流程里引擎初始化参数明显比服务模式激进:显存利用率直接拉到 0.99 [1]:
if __name__ == '__main__':
messages = [
{
"role": "user",
"content": [
{
"type": "video",
"video": "./space_woaudio.mp4",
},
{"type": "text", "text": "Describe this video. And guess what is the man going to do?"},
],
}
]
checkpoint_path = "请修改我!!!/Qwen/Qwen3-VL-4B-Instruct"
processor = AutoProcessor.from_pretrained(checkpoint_path)
inputs = [prepare_inputs_for_vllm(message, processor) for message in [messages]]
llm = LLM(
model=checkpoint_path,
trust_remote_code=True,
gpu_memory_utilization=0.99,
enforce_eager=False,
tensor_parallel_size=torch.cuda.device_count(),
seed=0
)采样参数同样收紧,temperature 0、max_tokens 只给 128 [1]:
sampling_params = SamplingParams(
temperature=0,
max_tokens=128,
top_k=-1,
stop_token_ids=[],
)
for i, input_ in enumerate(inputs):
print()
print('=' * 40)
print(f"Inputs[{i}]: {input_['prompt']=!r}")
print('\n' + '>' * 40)
outputs = llm.generate(inputs, sampling_params=sampling_params)
for i, output in enumerate(outputs):
generated_text = output.outputs[0].text
print()
print('=' * 40)
print(f"Generated Response: {generated_text!r}")视频预处理的吞吐约 1270 token/秒,生成约 78 token/秒 [1]。模型把视频内容识别为高科技任务控制中心,描述了一位穿 NASA 标志 Polo 衫的中年男子正在讲话比划——视频理解和推理能力都站得住,但输出被 max_tokens=128 截断 [1]。这正是视频负载的现实:视频帧吃显存凶猛,单卡环境下只能用极限参数换取任务跑通,截断是预算换来的代价 [1]。
部署建议收个尾:图文负载用 serve 模式对外提供服务,参数留足余量;视频负载优先离线批处理,显存利用率往顶上压、max_tokens 精打细算,任务级别的吞吐比单请求的完整输出更重要。
本文改编自 Datawhale 开源教程《self-llm》的 Qwen3-VL-4B-Instruct vLLM 部署一节[1],原文以 Apache-2.0 许可证发布。
来源
- 104-Qwen3-VL-4B-Instruct-vLLM | self-llmDatawhale self-llm (Apache-2.0)
引用本页: Qwen3-VL 多模态模型用 vLLM 部署. TellWorld, https://www.tellworld.net/zh/practice/practice-qwen3-vl-vllm. 更新于 2026-11-14