Qwen 2.5 VL 模型应用入门
本篇将以两个简单的应用场景——验证码识别(图文输入)和视频理解(视频文本输入),向你介绍如何使用 Qwen 2.5 VL 模型。我们将会使用 vLLM 作为推理框架,使用示例则使用 Python 代码和 Cherry Studio。因此同时,本篇也将会简单介绍 vLLM 部署 Qwen 2.5 VL。
相关信息
本文中使用的环境:
- 操作系统:Ubuntu 24.04
- Cuda:12.8
- GPU: NVIDIA Tesla V100 32G x8
准备工作
在开始之前,我将假定你熟悉:
- Linux
- Docker
- Python
- 基础 vLLM 知识
- Hugging Face / 魔搭
依赖与环境
我将略过详细安装,仅列出你所需的东西。为了完成全部的本文内容,你需要:
服务环境(模型部署侧)
- Cuda
- vLLM(Docker 或 原生环境)
- Python 环境
- 互联网连接 / 下载完成的模型权重
客户端环境(应用侧)
- Python 环境
- Cherry Studio(或你熟悉的 LLM 应用)
- 一个你熟悉的 IDE 或编辑器
使用 vLLM 部署模型
相较于使用 vllm serve
,我更推荐使用 Docker。两种方式均可以参照下面的示例代码:
services:
qwen-vl:
container_name: qwen-vl
image: vllm/vllm-openai:v0.8.5.post1
# restart: always
shm_size: 8g
ports:
- "19093:19093"
volumes:
- "/data/models/Qwen/Qwen2.5-VL-32B-Instruct:/models/Qwen2.5-VL-32B-Instruct"
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["0", "1", "2", "3", "4", "5", "6", "7"]
capabilities: [gpu]
command: [
"--model",
"/models/Qwen2.5-VL-32B-Instruct",
"--served-model-name",
"Qwen2.5-VL",
"--trust-remote-code",
"--host",
"0.0.0.0",
"--port",
"19093",
"--tensor-parallel-size",
"8",
"--gpu-memory-utilization",
"0.9",
"--limit-mm-per-prompt",
'{"image": 70, "video": 0}',
"--mm-processor-kwargs",
'{"max_pixels": 1160960}',
"--dtype",
"float16",
"--no-enable-chunked-prefill",
"--max-model-len",
"65536",
# "--enforce-eager",
"--api-key",
"sk-xxxxx",
]
environment:
- VLLM_MM_INPUT_CACHE_GIB=4
vllm serve \
/root/autodl-tmp/Qwen2.5-VL-32B-Instruct \
--served-model-name Qwen2.5-VL \
--trust-remote-code \
--host 0.0.0.0 \
--port 19093 \
--tensor-parallel-size 8 \
--gpu-memory-utilization 0.9 \
--limit-mm-per-prompt '{"image": 50, "video": 0}' \
--mm-processor-kwargs '{"max_pixels": 1160960}' \
--dtype float16 \
--max-model-len 65536 \
--api-key sk-xxxxx
参数
在示例中,我们使用的是 Tesla V100 32G 的 8 卡服务器。你需要注意这些设置:
vLLM 版本
在 V100 上,v0.8.5.post1
版本的 vllm
镜像是我测试下来最合适的(最稳定同时最新的版本)。如果你使用较新的 GPU,建议自行测试版本兼容,使用较新的版本。
多模态配置
在参数中,我们配置了 --limit-mm-per-prompt '{"image": 50, "video": 0}'
,这表示每次请求最多可以处理 50 张图片和 0 个视频。同时,我们还通过 --mm-processor-kwargs '{"max_pixels": 1160960}'
设置了每张图片的最大像素数为 1160960(约 1280x907 分辨率)。
在实践中,32G 的 8 张 V100 最多只能处理 50 张这个大小的图像——预留了大约 5000 tokens 给 Prompt 和模型输出,每张 116w 像素的图像编码后约 1400 tokens,而 69w 像素将会降低到 ~800 tokens。
这两个参数需要根据你的显存、模型参数和实际应用场景进行调整,而使用 Flash Attention 2 的话,会带来更多变数,因此你只能通过测试来找到最适合你的参数。
注意
正确配置这两个参数是很重要的,当你不配置 max_pixels
时,不要配置过大的 limit-mm-per-prompt
。当你这么做了,并看到 vLLM 启动卡住、服务器内存占用持续升高时,就不要继续等下去了,直接 Ctrl+C 去调整参数。
性能参数
--no-enable-chunked-prefill
与 --enforce-eager
是两个性能相关的参数。前者会禁用分块预填充,后者会强制使用 Eager 模式。根据你的应用场景,你可以选择开启或关闭这两个参数。
对于大部分 GPU,不要设置 --no-enable-chunked-prefill
,这个参数出现在示例中是因为 V100 在分块预填充中存在 BUG,必须禁用。
更长的上下文
在部署模型时,你可能会希望获得更大的上下文 / 更高的生成速度。对于 Qwen 2.5 VL,在官方示例中给出了 YARN 配置用于增加上下文长度,但在我们的实践中建议你按需选择如下方案:
使用 YARN
当你主要使用长文本输入时,我们建议你选择 YARN 配置。它的生成速度会比接下来提及的 mrope 更快,但你会牺牲一些多模态处理质量。
// config.json
{
// ...,
"rope_scaling": {
"type": "yarn",
"rope_type": "yarn",
"factor": 2.0, // 调整 factor 获得 x 倍的上下文长度
"original_max_position_embeddings": 32768
}
}
你可以先尝试保留原本的 mrope_section
键,只修改 YARN 参数。但在我的实践中,vLLM 是不支持同时使用 YARN 和 mrope 的,因此你需要删除 mrope_section
键。
使用 mrope
当你主要处理很多的多模态输入时,你不应该使用 YARN(会牺牲多模态处理质量)。在这种情况下,你可以使用 mrope 来获得更长的上下文。
使用 mrope 则无需对原本的 config.json 做任何修改,只需要直接增加模型的最大上下文长度即可。在 vLLM 中就是直接配置更长的 --max-model-len
参数。
使用模型
验证码识别
在这个场景中,我们将使用 Qwen 2.5 VL 模型来识别验证码。我们将使用 Cherry Studio 来简单测试,你也可以选择任何你熟悉的 LLM 客户端。
# 已知信息
输入会是一个图像验证码,为多宫格布局,规定 X 轴位于图像顶端,向右为正,Y 轴位于图像左侧,向下为正(左上角坐标为(0,0))。
# 要求
你需要输出全部符合验证码要求的图像/图像分片的坐标,输出为一个二维数组,示例:
[[0,0][1,2]]
你可以直接使用上面的提示词,创建一个新的会话,然后前往 reCAPTCHA Demo 获取验证码,把验证码截图下来丢给模型,就可以看到模型给你的回答了。
视频理解
视频理解相对于图文任务就要复杂多了,我会先简单说说直接传入视频的方式,再向你详细讲讲在本地先通过 Qwen VL Utils 预处理视频,再请求模型的方式。这两者在效果上将会有质的区别。
直接传入视频
vLLM 仅支持传入 x_url
(x=image/video) 形式的 messages,因此你需要传入的是一个视频的可访问链接,我们假定有一个本地服务托管了 http://localhost:8000/video.mp4
这个视频文件。
使用这样的代码:
from openai import OpenAI
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{
"role": "user",
"content": [
{
"type": "video_url",
"video_url": {
"url": "http://localhost:8000/video.mp4",
},
},
{"type": "text", "text": "请总结视频的内容。"}
]
}
]
chat_response = client.chat.completions.create(
model="Qwen2.5-VL", # 如有不同模型名请替换
messages=messages,
temperature=0.2
)
print(chat_response.choices[0].message.content)
我可以告诉你的结论是,在总结性的任务,长度较短、内容较为单一的视频上,这样的调用效果尚可。但由于 vLLM 抽帧的实现,整个视频无论长短,大约只会被抽取最多 30 帧左右,因此在处理长视频、回答细节性问题时,这种方式的效果会很差。
预处理视频
为了解决上面的问题,我们可以 (官方也推荐) 对视频进行预处理,先于 vLLM 完成抽帧逻辑。
from qwen_vl_utils import process_vision_info
import os
import numpy as np
from PIL import Image
from io import BytesIO
import base64
from openai import OpenAI
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
def prepare_video_message(video_path: str, fps: float = 3.0):
"""
使用qwen_vl_utils对本地视频进行抽帧,将所有帧编码为base64格式,并构造成vLLM兼容的video_url消息结构。
"""
video_content = [{
"role": "user",
"content": [{
"type": "video",
"video": f"file://{os.path.abspath(video_path)}",
"fps": fps
}]
}]
image_inputs, video_inputs, video_kwargs = process_vision_info(video_content, return_video_kwargs=True)
assert video_inputs is not None, "video_inputs为None,视频抽帧失败"
# 将所有帧转为base64字符串
video_input = video_inputs.pop().permute(0, 2, 3, 1).numpy().astype(np.uint8)
base64_frames = []
for frame in video_input:
img = Image.fromarray(frame)
buffer = BytesIO()
img.save(buffer, format="jpeg")
frame_bytes = buffer.getvalue()
base64_str = base64.b64encode(frame_bytes).decode("utf-8")
base64_frames.append(base64_str)
# 按vLLM要求拼接为一条data:video/jpeg;base64,...消息
# 这样做是为了让 vLLM 不要再对视频进行抽帧
video_url_obj = {
"type": "video_url",
"video_url": {
"url": f"data:video/jpeg;base64,{','.join(base64_frames)}"
},
"fps": fps
}
return video_url_obj
video_filename = "test.mp4"
video_url_obj = prepare_video_message(video_filename, fps=8.0)
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{
"role": "user",
"content": [
video_url_obj,
{"type": "text", "text": "请总结视频的内容。"}
]
}
]
chat_response = client.chat.completions.create(
model="Qwen2.5-VL", # 如有不同模型名请替换
messages=messages,
temperature=0.2
)
print(chat_response.choices[0].message.content)
在上面的代码中,我们使用了官方提供的 qwen_vl_utils
库来处理视频。然后将抽取的帧全部转为 base64 编码的 JPEG 图像,并构造成 vLLM 兼容的 video_url
消息结构。
这样模型将会看到更多的视频细节(传入帧数显著变多了),并且可以更好地回答关于视频内容的问题。而代价就是更高的上下文占用与更慢的处理速度。
因此在处理视频时,你需要根据实际情况选择合适的抽帧率(fps
参数),以平衡性能和效果(以及上下文长度限制)。对于大部分视频,最好设置在 2~3
fps,能较好的均衡性能和效果。
额外谈谈
多模态索引
在本文的图文示例中,我们演示的是一个单图的场景,但很多时候我们需要处理多图,并希望能得到正确的索引顺序。典型场景例如多图输入,要求找出某个特征。
这时你会发现,再去直接使用多图输入,模型是无法正确识别“这张图是第几张”的,甚至无法正确回答你“总共输入了多少图片”。为了解决这个问题,我们可以在输入中添加视觉 ID:
Add ids for Multiple Image Inputs
By default, images and video content are directly included in the conversation. When handling multiple images, it's helpful to add labels to the images and videos for better reference. Users can control this behavior with the following settings:
在官方文档中给出了上述的描述,并给出了一个使用 Transformers 时的示例。而在使用 vLLM 后端时,建议自行实现相关的逻辑,通过额外的 text
消息来添加视觉 ID。例如:
content.extend([
{"type": "text", "text": f"图像 {i}: "},
{
"type": "image_url",
"image_url": {"url": encode_image(img_path)},
}
])
在每个图像前提供一个索引描述,这样模型就能理解每个图像的顺序和索引。
视频的时间轴
类似上面的描述,模型本身同样并不是设计于能注重视频的时间信息的,它很难回答你“这个视频里狗出现在几分几秒?”,甚至也无法正确回答你“你一共看到了几帧视频?”。
因此你可以用类似的方式,将帧索引传递给模型,随后根据帧索引和抽帧时的帧率计算时间信息。代码在此就不再赘述,原理和上面的视觉 ID 类似。
结语
通过本文,你应该已经了解了如何使用 Qwen 2.5 VL 模型进行图文和视频理解任务。我们使用了 vLLM 作为推理框架,并通过 Cherry Studio 和 Python 代码进行了简单的测试。以下是一些额外的资源和参考链接,供你进一步学习和探索: