学习进度 · 登录后可记录
⚠️ 重要声明
- 本教程适配 Notebook 云平台环境
- 服务端口:18004
- 环境:Ubuntu 22.04 | CANN 8.5 | Python 3.11.14
- 下载源:仅 AtomGit
本节课的目标是:在昇腾 NPU 上完成 Qwen2.5-Omni-7B 多模态大模型的端到端部署——从模型获取、服务启动,到图片/音频/视频/混合多模态 API 调用跑通。
很多开发者习惯了部署纯文本模型(如 Qwen3-8B),第一次接触 Omni 多模态模型时,会遇到很多新问题:
这节课,我将从第一性原理出发,让你真正理解"为什么要这样操作"。
Qwen2.5-Omni 是通义千问家族中的多模态旗舰模型,它的核心能力是同时感知和理解文本、图像、音频、视频四种模态:

| 对比维度 | 纯文本模型(如 Qwen3-8B) | Omni 多模态模型 |
|---|---|---|
| 输入类型 | 纯文本 | 文本 + 图片 + 音频 + 视频 |
| Base64 处理 | ❌ 不需要 | ✅ 常用 |
| 媒体 URL | ❌ 不需要 | ✅ 支持 |
| 并发数 | 4 | 4 |
| 模型大小 | 16GB | ~15GB |
| API 格式 | 标准 ChatML | 扩展多模态格式 |
| max-model-len | 32768 | 16000 |
当你第一次启动 vLLM 时,可能会遇到:
这节课的脚本设计了一套完整的防护机制:


为什么要先装 atomgit,而不是直接 pip install vllm?
正确的部署顺序是:
这样做的好处是:模型在本地,运行时直接加载本地路径,避免了网络波动导致的下载失败。
💻 Notebook 实操
# 安装atomgit SDK
!pip install -U atomgit -i https://mirrors.huaweicloud.com/repository/pypi/simple📖 图文说明
为什么要重启内核?
安装新的 Python 包后,必须重启 Jupyter Notebook 的内核才能加载新包。
如果不重启,可能会遇到
ModuleNotFoundError: No module named 'atomgit'错误。原理:
- Jupyter Notebook 的内核是一个长期运行的 Python 进程
- 新的包安装在文件系统中,但内核的内存空间还没有加载新包
- 重启内核 = 重新启动 Python 进程 = 加载最新的包
💻 Notebook 实操
# restart the kernel
get_ipython().kernel.do_shutdown(restart=True)⚠️ 注意事项
- 执行此命令后,当前的变量和输出都会丢失
- 内核重启后需要从头重新执行代码
- 重启完成后需要重新执行
pip install命令
下载命令详解:
snapshot_download("hf_mirrors/Qwen/Qwen2.5-Omni-7B", local_dir='/opt/atomgit/Qwen2.5-Omni-7B')
hf_mirrors/Qwen/Qwen2.5-Omni-7B 是模型在 AtomGit 上的路径
hf_mirrors = HuggingFace 镜像Qwen = 阿里云团队Qwen2.5-Omni-7B = 模型名称local_dir='/opt/atomgit/Qwen2.5-Omni-7B' 指定了模型的本地保存位置
💻 Notebook 实操
# 下载模型
from atomgit_hub import snapshot_download
snapshot_download(
"hf_mirrors/Qwen/Qwen2.5-Omni-7B",
local_dir = '/opt/atomgit/Qwen2.5-Omni-7B'
)⚠️ 注意事项
- 首次下载需要约 15GB 带宽,请确保网络稳定
- 如果下载中断,重复执行会自动断点续传
- 不要手动中断下载,否则可能导致文件损坏

📖 图文说明
这个脚本比纯文本模型(如 Qwen3-8B)的启动命令复杂得多,原因在于:
为什么要用 bash script 而非直接 !vllm serve?
因为 Notebook 云平台环境下,你可能会:
重复执行同一个 cell
内核重启后忘记进程还在跑
服务还在启动中就急着发请求
这个脚本用三层防护机制解决以上问题。
💻 Notebook 实操
%%bash
PORT=18004
MODEL_DIR=/opt/atomgit/Qwen2.5-Omni-7B
LOG=/opt/atomgit/vllm.log
PID_FILE=/opt/atomgit/vllm.pid
LOCK_FILE=/opt/atomgit/vllm_start.lock
PORT_HEX=$(printf '%04X\n' $PORT)
echo "🔍 检查 vLLM 状态..."
# -------------------------
# 防止“正在启动中”
# -------------------------
if [ -f "$LOCK_FILE" ]; then
echo "⏳ vLLM 正在启动中(LOCK 存在),请稍后..."
exit 0
fi
# 创建启动锁
touch "$LOCK_FILE"
# -------------------------
# PID 存活检查
# -------------------------
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "❌ vLLM 已运行,PID=$PID"
rm -f "$LOCK_FILE"
exit 0
else
echo "⚠️ 旧 PID 已失效,清理"
rm -f "$PID_FILE"
fi
fi
# -------------------------
# 端口检查(防已启动成功)
# -------------------------
if grep -i ":$PORT_HEX" /proc/net/tcp > /dev/null; then
echo "❌ vLLM 已在运行(端口 $PORT 占用)"
rm -f "$LOCK_FILE"
exit 0
fi
# -------------------------
# 启动 vLLM
# -------------------------
echo "🚀 启动 vLLM..."
nohup vllm serve "$MODEL_DIR" \
--served-model-name Qwen2.5-Omni-7B \
--host 0.0.0.0 \
--port $PORT \
--tensor-parallel-size 1 \
--dtype bfloat16 \
--compilation-config '{"custom_ops":["none", "+rms_norm", "+rotary_embedding"]}' \
--max-num-seqs 4 \
--max-model-len 16000 \
--gpu-memory-utilization 0.8 \
> "$LOG" 2>&1 &
echo $! > "$PID_FILE"
echo "⏳ vLLM 启动中,PID=$(cat $PID_FILE)"
# -------------------------
# 延迟释放锁(关键)
# -------------------------
sleep 2
rm -f "$LOCK_FILE"
echo "✅ 启动流程结束"| 参数 | 值 | 说明 |
|---|---|---|
--served-model-name | Qwen2.5-Omni-7B | API 调用时的模型标识名 |
--host | 0.0.0.0 | 监听所有网络接口 |
--port | 18004 | 服务端口 |
--tensor-parallel-size | 1 | 张量并行数,1=单卡 |
--dtype | bfloat16 | 数据精度,BF16 是昇腾优化的格式 |
--compilation-config | 见命令 | 动态编译配置,逐步优化推理性能 |
--max-num-seqs | 4 | 最大并发序列数 |
--max-model-len | 16000 | 最大上下文长度(Omni 多模态上下文较长) |
--gpu-memory-utilization | 0.8 | 显存使用比例 80% |
⚠️ Omni vs 纯文本模型的区别
Omni 模型的
max-model-len设置为 16000(而非纯文本的 32768),原因是:
- 多模态模型的输入包含图片/音频/视频的 token 表示
- 这些媒体 token 会占用上下文长度
- 设置过长的 max-model-len 会导致显存溢出
为什么要准备测试数据?
Omni 模型支持图片、音频、视频三种模态,我们需要准备测试数据来验证部署是否成功。
数据来源:AtomGit 上的标准测试集,包含:
image.jpg - 测试图片audio.wav - 测试音频video.mp4 - 测试视频💻 Notebook 实操
# 测试数据准备
!mkdir -p static
!curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/image.jpg -o /opt/atomgit/static/image.jpg
!curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/audio.wav -o /opt/atomgit/static/audio.wav
!curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/video.mp4 -o /opt/atomgit/static/video.mp4为什么要封装 call_api 函数?
直接用 requests.post 调用 API 会有很多边界情况需要处理:
ConnectionErrorTimeout这个封装函数统一处理了所有边界情况,并给出明确的错误提示和日志路径。
📖 图文说明
encode_file_to_base64()函数的作用:图片文件 → Base64 字符串 → 嵌入 JSON payload → API 调用
Base64 是一种将二进制文件转换为 ASCII 字符串的编码方式,这样就可以通过 JSON 格式传递二进制媒体文件了。
注意:
.replace('\n', '')是必须的,因为 Base64 字符串中的换行符会导致 JSON 解析失败。
💻 Notebook 实操
# call_api method
import base64
import requests
import json
import os
from requests.exceptions import ConnectionError, Timeout
# API 地址
BASE_URL = "http://localhost:18004/v1/chat/completions"
IMAGE_PATH = "/opt/atomgit/static/image.jpg"
VIDEO_PATH = "/opt/atomgit/static/video.mp4"
AUDIO_PATH = "/opt/atomgit/static/audio.wav"
VLLM_LOG_PATH = "/opt/atomgit/vllm.log"
def encode_file_to_base64(file_path: str) -> str:
"""将文件编码为base64字符串(去掉换行)"""
with open(file_path, 'rb') as f:
return base64.b64encode(f.read()).decode('utf-8').replace('\n', '')
def call_api(payload, url: str = BASE_URL, timeout: int = 10):
"""
调用 vLLM OpenAI 兼容接口
- 捕获端口不存在 / 服务未启动
- 统一返回 JSON
"""
headers = {"Content-Type": "application/json"}
try:
response = requests.post(
url,
headers=headers,
json=payload,
timeout=timeout
)
print(f"状态码: {response.status_code}")
# 正常返回
if response.status_code == 200:
return response.json()
# vLLM 返回的业务错误(例如模型未加载)
try:
return response.json()
except Exception:
return {
"error": f"vllm 返回非 JSON 响应,状态码 {response.status_code}",
"raw_response": response.text
}
except ConnectionError:
# 端口不存在 / 服务没起
return {
"error": "vllm serve 暂未启动,稍后重试,启动日志详情见:/opt/atomgit/vllm.log",
"detail": "Connection refused",
"log": VLLM_LOG_PATH
}
except Timeout:
# 服务在启动中或卡住
return {
"error": "vllm serve 响应超时,可能仍在加载模型,请稍后重试",
"detail": "Request timeout",
"log": VLLM_LOG_PATH
}
except Exception as e:
# 兜底
return {
"error": "调用 vllm 时发生未知异常",
"detail": str(e),
"log": VLLM_LOG_PATH
}
# base64数据
image_b64 = encode_file_to_base64(IMAGE_PATH)
video_b64 = encode_file_to_base64(VIDEO_PATH)
audio_b64 = encode_file_to_base64(AUDIO_PATH)
📖 图文说明
最基础的测试,验证 vLLM 服务是否正常运行。
messages格式说明:
role: system- 系统提示词,定义模型身份role: user- 用户输入
💻 新窗口 Notebook 实操
# 测试1: 纯文本对话
payload = {
"messages": [
{
"role": "system",
"content": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
},
{
"role": "user",
"content": "who are you?"
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))发送本地图片,以 Base64 编码形式传输。
# 读取本地图片并转为 Base64
def image_to_base64(image_path):
with open(image_path, 'rb') as f:
return base64.b64encode(f.read()).decode('utf-8')
# 替换为你的图片路径
image_path = 'example.jpg'
image_b64 = image_to_base64(image_path)
print(f"图片 Base64 长度: {len(image_b64)}")payload = {
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What's in this image?"},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_b64}"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))直接通过公网 URL 传输图片,无需本地存储。
payload = {
"model": "Qwen2.5-Omni-7B",
"messages": [
{
"role": "system",
"content": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
},
{
"role": "user",
"content": [
{"type": "text", "text": "图片中是什么?"},
{
"type": "image_url",
"image_url": {
"url": "https://bpic.588ku.com/audio_copy/cover_img/18/11/04/0910b3378fa47cc657b69237c8573e42.png"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# 测试4: 音频输入 (Base64编码, wav格式)
payload = {
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
}
]
},
{
"role": "user",
"content": [
{
"type": "input_audio",
"input_audio": {
"data": audio_b64,
"format": "wav"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# 测试5: 音频URL
payload = {
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "描述一下音频"
},
{
"type": "audio_url",
"audio_url": {
"url": "https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/test_audio/asr_example_zh.wav"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# 测试6: 视频输入 (Base64编码)
payload = {
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "视频里面都有什么?"
},
{
"type": "video_url",
"video_url": {
"url": f"data:video/mp4;base64,{video_b64}"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# 测试7: 视频URL
payload = {
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "描述一下视频"
},
{
"type": "video_url",
"video_url": {
"url": "https://vod.300hu.com/24/4c1f7a6atransbjngwcloud1oss/24ac9cd11020352573738127361/1097_4076_1_42660520d_f.mp4"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# content 支持同时传入多种媒体类型
"content": [
{"type": "text", "text": "问题(涉及所有媒体)"},
{"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}},
{"type": "video_url", "video_url": {"url": "https://example.com/video.mp4"}},
{"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}}
]# 测试8: 混合媒体(图片+视频+音频)
payload = {
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "You are Qwen, a virtual human developed by the Qwen Team, Alibaba Group, capable of perceiving auditory and visual inputs, as well as generating text and speech."
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "图片里面有什么?音频里面有什么?视频里面有什么?"
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{image_b64}"
}
},
{
"type": "video_url",
"video_url": {
"url": f"data:video/mp4;base64,{video_b64}"
}
},
{
"type": "input_audio",
"input_audio": {
"data": audio_b64,
"format": "wav"
}
}
]
}
]
}
result = call_api(payload)
print(json.dumps(result, indent=2, ensure_ascii=False))# 纯文本
payload = {
"messages": [
{"role": "user", "content": "问题"}
]
}
# 图片 + 文本
payload = {
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "问题"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}}
]
}
]
}
# 音频 + 文本
payload = {
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "问题"},
{"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}}
]
}
]
}
# 视频 + 文本
payload = {
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "问题"},
{"type": "video_url", "video_url": {"url": f"data:video/mp4;base64,{video_b64}"}}
]
}
]
}
# 全模态混合(最强形态)
payload = {
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "综合描述图片、音频、视频内容"},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_b64}"}},
{"type": "video_url", "video_url": {"url": f"data:video/mp4;base64,{video_b64}"}},
{"type": "input_audio", "input_audio": {"data": audio_b64, "format": "wav"}}
]
}
]
}# 启动后等待模型加载
import time
print("⏳ 等待模型加载...")
time.sleep(60) # 至少等待 1 分钟
# 然后再发请求
result = call_api(payload)# 1. 检查端口占用
!lsof -i :18004
# 2. 查看 vLLM 日志
!cat /opt/atomgit/vllm.log | tail -50
# 3. 杀掉旧进程
!kill -9 <PID>以下命令需在终端/Jupyter 中逐条执行
# 1. 安装 atomgit
pip install -U atomgit -i https://mirrors.huaweicloud.com/repository/pypi/simple# 2. 重启内核(执行后需重新运行所有 cell)
get_ipython().kernel.do_shutdown(restart=True)# 3. 下载模型
from atomgit_hub import snapshot_download
snapshot_download(
"hf_mirrors/Qwen/Qwen2.5-Omni-7B",
local_dir="/opt/atomgit/Qwen2.5-Omni-7B"
)# 4. 启动服务(防护脚本,一行命令)
PORT=18004 MODEL_DIR=/opt/atomgit/Qwen2.5-Omni-7B nohup vllm serve "hf://Qwen/Qwen2.5-Omni-7B" \
--port 18004 \
--tensor-parallel-size 1 \
--dtype bfloat16 \
--compilation-config '{"custom_ops":["none","+rms_norm","+rotary_embedding"]}' \
--max-num-seqs 4 \
--max-model-len 16000 \
--gpu-memory-utilization 0.8 &# 5. 下载测试数据
curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/image.jpg -o /opt/atomgit/static/image.jpg
curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/audio.wav -o /opt/atomgit/static/audio.wav
curl -L https://ai.gitcode.com/atomgit-ascend/qwen2.5-omni-7b/resolve/main/t-unit/video.mp4 -o /opt/atomgit/static/video.mp4登录后即可查看完整教程内容、运行代码和参
与学习互动
还没有账号?