方案:DSW + 灵骏智算 + Vllm部署
一、入口
aliyun.com -> 控制台 -> 人工智能平台PAI -> 左侧菜单栏 -> 模型开发与训练 -> 交互式建模(DSW) -> 开发机实例(新建实例) -> 编辑实例或者变更配置
二、编辑实例或者变更配置
I、配置 “资源信息”
资源配额:灵骏智算
资源规格:自选

II、配置 “环境信息”
1、镜像配置
选择“镜像地址”
egslingjun-registry.cn-wulanchabu.cr.aliyuncs.com/egslingjun/inference-xpu-pytorch:26.04-v2.1.0-vllm0.23.0-torch2.10-cu130-20260710
自带适配ppu芯片的pytorch框架、ubuntu系统、vllm加速引擎、python包等基础配置,无需自己后期再安装

储存挂载 & 挂载路径:
储存挂载,选择同vpc局域网的OSS。(aliyun.com -> 控制台 -> 对象储存OSS -> Bucket列表 -> 选择同区域的Bucket实例 -> 上传文件 -> ossutil -> 命令行工具ossutil 2.0 -> 下载windows对应架构的命令行程序 -> )
1.1 下载ossutil命令行工具


1.2 解压工具
1) 解压ossutil-2.3.0-windows-amd64.zip
2) 进入ossutil.exe所在路径,在此处打开 PowerShell 窗口,执行命令,配置远程连接设置信息
.\ossutil.exe config -e oss-cn-wulanchabu.aliyuncs.com

3)、配置
直接按回车键即可。
按完回车后,程序会依次让输入以下三项信息:
accessKeyID:粘贴你的阿里云 AccessKey ID。

accessKeySecret:粘贴你的阿里云 AccessKey Secret(输入时屏幕不会显示字符,这是正常的,输完直接回车)。
Please enter Region : cn-wulanchabu (华北6(乌兰察布)的地域id)
language:输入 CH 并回车,将工具语言设置为中文。(如果有的话)
Please enter Endpoint:直接回车
全部输完后,如果看到类似 "保存成功" 的提示,就说明配置完成了,可以开始上传文件了。

1.3 下载模型到本地
1) git 下载
获取 PaddleOCR-VL-1.6 模型文件,最推荐且官方的渠道是 Hugging Face 或 GitHub。由于该模型文件较大(通常包含多个权重文件和配置文件),直接下载整个文件夹比单个文件更稳妥。
使用 Git LFS (推荐)
git lfs install
git clone https://huggingface.co/PaddlePaddle/PaddleOCR-VL-1.6
2) 拉取模型timeout(替代方法)
这是国内访问 Hugging Face 时非常典型的网络超时问题,根本原因是 Hugging Face 的服务器位于境外,直接连接常被防火墙拦截或 DNS 污染。你不需要翻墙,用以下两种方法都能解决:
1.3.1 方法一:使用魔搭社区(ModelScope)
下载(推荐,速度最快),(还是用git工具,推荐)
魔搭是阿里推出的国内模型托管平台,PaddleOCR-VL-1.6 已同步上架,下载速度可达满速。
安装魔搭命令行工具
pip install modelscope
一键下载这些模型到当前目录
modelscope download --model 'PaddlePaddle/PaddleOCR-VL-1.6' --local_dir './PaddleOCR-VL-1.6'
modelscope download --model Qwen/Qwen3-Reranker-4B --local_dir ./Qwen3-Reranker-4B
modelscope download --model Qwen/Qwen3-Embedding-4B --local_dir ./Qwen3-Embedding-4B
1.3.2 打包
在windows本地打包文件夹为tar包,方便直接在网页端直接上传。超过5G的文件,可以分两批上传
tar -cvf Qwen3-4B-Instruct-2507-incomplete.tar ./Qwen3-4B-Instruct-2507-incomplete
1.3.3 解压:
tar -xvf bge-reranker-large.tar
1.3.4 改名:
mv Qwen3-4B-Instruct-2507-incomplete Qwen3-4B-Instruct-2507
方法二:使用 Hugging Face 镜像站
如果坚持用 HF,可替换为国内镜像源,避免直连超时。
设置环境变量指向镜像站
export HF_ENDPOINT=https://hf-mirror.com
再用 git clone 下载
git clone https://hf-mirror.com/PaddlePaddle/PaddleOCR-VL-1.6
1.4 ossutil工具上传模型到OSS
一键上传模型文件夹
现在,拉取模型文件夹PaddleOCR-VL-1.6到本地D盘的software\models下,运行下面这条命令:
.\ossutil.exe cp -r "D:\software\models\PaddleOCR-VL-1.6" oss://你的Bucket名称/py/PaddleOCR-VL-1.6/ -j 10
.\ossutil.exe cp -r "D:\software\models\Qwen3-Embedding-4B" oss://你的Bucket名称/py/Qwen3-Embedding-4B/ -j 10
.\ossutil.exe cp -r "D:\software\models\Qwen3-Reranker-4B" oss://你的Bucket名称/py/Qwen3-Reranker-4B/ -j 10
参数解释
-r: 代表递归,把文件夹里的所有东西都传上去。
"D:…\PaddleOCR-VL-1.6": 你本地模型的绝对路径(如果路径有空格一定要加引号)。
oss://…: 你要传到的云端目标路径。
-j 10: 关键参数,开启 10 个线程并发上传,速度会快很多!
2、挂载路径
就是DSW运行终端同步文件的路径,上传到OSS目录下的模型权重文件会自动同步到DSW终端的挂载路径。

III 访问配置

1 安全组
入方向 -> 创建安全组\选择现有安全组 -> 入方向 -> 增加规则 -> 访问来源:选择DSW的ip地址 -> 访问目的:加入端口 -> 添加描述 -> 提交
vpc-ip地址查询:
交互式建模(DSW)- 进入DSW实例 – 拉到页面底部

服务名称:自选(8000、8001、8002)
监听窗口:自选
创建vpc内访问域名:勾选
2 检查网络情况(内外网连接不上)
可以连通内外网的话,可以直接通过vllm 或者docker直接拉魔搭社区或者hugging face的模型文件,部署模型服务。否则就需要动用OSS去挂载了
去安全组放开对应的ip白名单
测试公网连通性 (百度)
ping -c 4 baidu.com
测试阿里云内网连通性 (ModelScope 服务器 IP)
ping -c 4 120.55.166.178
下载python包
-
安装守护脚本依赖
pip install psutil -
安装vllm推理框架(自动装torch/transformers等)
pip install vllm
三、启动命令demo解释
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/PaddleOCR-VL-1.6 \
--trust-remote-code \
--port 8000 \
--tensor-parallel-size 1 \
--allowed-local-media-path /mnt/data/ \
--gpu-memory-utilization 0.15 \
--max-model-len 4096 \
--root-path /paddleocr/predict
>> /mnt/workspace/log-dir/PaddleOCR-VL-1.6.log 2>&1
I vLLM启动命令参数解释:
整条命令是:用vLLM框架,把PaddleOCR-VL-1.6多模态OCR模型封装成兼容OpenAI接口格式的REST API服务,监听8000端口;适配灵骏96GB PPU单卡环境
-
入口主程序
python -m vllm.entrypoints.openai.api_server
vLLM 官方内置的 OpenAI 兼容接口服务入口
作用:启动一个 Web 服务,对外提供和 OpenAI ChatCompletion、Vision(多模态识图)一模一样的 API 格式,可以用 openai python sdk 直接调用这个 OCR 多模态模型,不用自己封装接口。 -
–model /mnt/data/PaddleOCR-VL-1.6
含义:指定要加载的模型本地路径
路径:服务器挂载目录 /mnt/data/ 下存放的 PaddleOCR-VL-1.6 权重文件夹
要求:该目录内必须包含完整模型权重、config.json、tokenizer 配置文件 -
–trust-remote-code
核心作用:允许执行模型仓库里自定义的 Python 代码
PaddleOCR-VL 不属于原生 HuggingFace 标准 LLM 架构,有自己定制的模型推理代码、视觉编码逻辑;
不加这个参数,vLLM 会拒绝加载第三方自定义模型代码,直接启动报错。 -
–port 8000
API 服务监听端口,就是你刚才查询的 8000 端口
服务启动后:
本地访问:http://127.0.0.1:8000/v1/chat/completions
内网其他机器访问:http://服务器内网IP:8000/v1/chat/completions -
–tensor-parallel-size 1
张量并行大小(TP 并行)
TP:把模型权重拆分到多张 GPU 上并行计算,用于超大显存需求大模型
数值 = 1:只用单张 PPU 卡(96GB)运行模型,不拆分权重到多卡
你当前只分配了 1 张 GPU,固定写 1 即可;多卡场景才改成 2/4/8。 -
–allowed-local-media-path /mnt/data/
多模态专用参数(图文输入必备)
vLLM 多模态接口支持传入本地图片路径让模型读图 OCR;
这个参数是白名单目录:
仅允许加载 /mnt/data/ 目录下的图片文件,禁止读取服务器其他路径文件(安全限制,防止越权读取系统文件)。
刚好我的模型、图片素材都放在 /mnt/data 下,匹配我的存储目录。 -
–gpu-memory-utilization 0.35
GPU 显存占用上限利用率(关键调优参数)
取值范围:0 ~ 1
0.35 = 最多占用当前 PPU 总显存的 35%
你的卡是 96GB 显存:96 * 0.35 = 33.6GB
vLLM 会预留剩余大量显存给 KV 缓存、并发请求、图片特征缓存;
设得越低:支持并发数越多、不容易 OOM 显存溢出;
设太高(0.8/0.9):显存几乎全给模型权重,并发请求一多就会爆显存崩溃。
0.35 对于 4B 级多模态 OCR 模型非常保守稳定。 -
–max-model-len 4096
上下文最大序列长度
限制整个对话 + 图片编码 + 文本输出总 token 上限:4096 tokens
OCR 场景:图片编码 + 识别文字内容一般用不满 4096,足够使用;
数值越大,KV 缓存需要显存越多,并发能力下降。 -
–root-path /paddleocr/predic
API 服务根路径前缀(反向代理 Nginx 路径配置用)
默认接口地址:http://ip:8000/v1/xxx
配置 root-path 后,完整访问地址变为:
http://ip:8000/paddleocr/predic/v1/xxx
适用场景:
Nginx 反向代理、AI网关转发时需要统一路径前缀;
一台机器部署多个 AI 服务,通过路径区分不同模型接口;
如果不需要路径前缀,可以删掉这个参数。
补充实用注意事项(适配你的 PAI 灵骏环境)
四 AI网关设置
I 添加服务
入口:AI网关 – 实例 – 服务 – 创建服务 – 配置服务信息
服务来源:固定地址
服务名称:ocr-server
服务地址:172.29.1.155:8000
TLS模式:关闭

II 创建Model API
入口:AI网关 – 实例 – Model API – 创建Model API – 图片生成
协议:自定义HTTP
API名称:ocr-api
最后点击确定
配置AI网关和DSW和连接
入口:进入model api实例:ocr-api -> 路由列表 -> 创建路由 -> 配置路由 -> 添加 -> 复制路径path信息
路由名称:ocr-router
路径path: 前缀是 /paddleocr/paddleocr/predict
服务类型:单服务
服务列表:
服务名称:ocr-server
模型名称:透传
比如:
openai_api_base = "http://ip:8000/paddleocr/paddleocr/predict/v1"
III 测试
情况1:关注DSW后台是否有打印200成功信息,有则成功访问
情况2:有打印报错信息,但是报错404 not found,通常是路由不正确,停下DSW服务,增加一条参数:
–root-path /paddleocr/predic
API 服务根路径前缀(反向代理 Nginx 路径配置用)
默认接口地址:http://ip:8000/v1/xxx
配置 root-path 后,完整访问地址变为:
http://ip:8000/paddleocr/predic/v1/xxx
适用场景:
Nginx 反向代理、AI网关转发时需要统一路径前缀;
一台机器部署多个 AI 服务,通过路径区分不同模型接口;
如果不需要路径前缀,可以删掉这个参数。
补充实用注意事项(适配你的 PAI 灵骏环境)
五、安装cron(DSW没公网,只有vpc局域网,只有OSS挂载)
I、开服务器
在阿里云开一台对应系统版本24.04版本的便宜服务器,执行以下命令
1 更新软件源并安装解析工具,确保服务器的软件列表是最新的,并安装专门用于解析依赖树的工具 apt-rdepends:
sudo apt-get update
sudo apt-get install -y apt-rdepends
2 创建目录并递归下载所有依赖包
使用 apt-cache depends –recurse 来递归挖掘所有依赖,并配合 apt-get download 批量下载:
- 创建一个干净的目录存放离线包
mkdir -p ~/cron_offline_pkg
cd ~/cron_offline_pkg
- 执行递归下载(核心命令)
sudo apt-get download $(apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances cron | grep "^\w" | sort -u)
解释:这个命令会列出 cron 的所有底层依赖,过滤掉说明文字,去重后一次性下载所有的 .deb 包。
3 打包离线包
下载完成后,检查目录里的文件数量(通常会有几十个包),然后将其打包:
- 查看下载了多少个包
ls -l *.deb | wc -l
- 打包成 tar.gz 文件
tar -zcvf cron_offline_pkg.tar.gz *.deb
4 下载到和DSW在同一个局域网下的本机windows电脑上
scp root@ip:~/cron_offline_pkg/cron_offline_pkg.tar.gz .
到阿里云的oss拖拽界面,将将生成的cron_offline_pkg.tar.gz上传到阿里云OSS
5 上传至 OSS 并在 DSW 中安装
在 PAI-DSW 的 Terminal 中,进入挂载的 OSS 目录并解压:
替换为您的实际挂载点
cd /mnt/data
mkdir /mnt/data/cron-dir
cd /mnt/data/cron-dir
tar -zxvf /mnt/data/cron-dir/cron_offline_pkg.tar.gz
批量安装并修复依赖:
sudo dpkg -i *.deb
6 报错:

过滤危险包,只安装 cron 相关
进入 deb 解压目录,排除 systemd 相关 deb,只安装 cron + libpam-modules
只安装cron、libpam-modules,跳过systemd两个包
dpkg -i cron_3.0pl1-184ubuntu2_amd64.deb libpam-modules_1.5.3-5ubuntu5.6_amd64.deb
II 检查安装是否成功
正确验证 cron 是否真正可用的方法:
1 检查 cron 守护进程是否在运行
which crontab 返回 /usr/bin/crontab
crontab 二进制程序已经安装成功,你可以执行 crontab -e、crontab -l 编辑定时任务。
which crontab
2 启动cron 后台守护进程(cron 服务)
第一步:尝试直接启动服务,在终端输入以下命令:
后台常驻运行(推荐,当前会话有效)
/usr/sbin/cron &
尾部 & 放入后台。
DSW 容器重启后必须重新执行这条命令,容器环境无法设置开机自启。
第二步:验证 cron 是否正常运行
查看cron进程,有数字输出代表进程正在运行;无输出代表未启动。
pgrep cron
III 创建一个测试任务验证功能
最后一步:实战验证(防止“假死”),创建一个测试任务(每分钟往文件里写个时间):
echo "* * * * * date >> /tmp/cron_success_check.log" | crontab -
等待 60~90 秒。检查结果:
cat /tmp/cron_success_check.log
如果看到了时间输出(例如 Fri Jul 31 …):恭喜,完全修复成功,可以放心使用 cron 了。
如果文件不存在或为空:说明进程虽然在跑,但调度功能有问题(可能是权限或日志目录问题)。
测试完后,清理掉这个测试任务:
crontab -r
IV 定期清空日志
crontab -e
每周日,3点开始,10分钟为一个周期,依次清除这些log日志
0 3 * * 0 > /mnt/workspace/log-dir/Qwen3-4B-Instruct-2507.log
10 3 * * 0 > /mnt/workspace/log-dir/PaddleOCR-VL-1.6.log
20 3 * * 0 > /mnt/workspace/log-dir/bge-reranker-large.log
30 3 * * 0 > /mnt/workspace/log-dir/Qwen3-Embedding-8B.log
40 3 * * 0 > /mnt/workspace/log-dir/Qwen3-Reranker-8B.log
V、设置凌晨3点重启
1 编辑脚本
vim xx,进入就是命令行模式,字母i,进入插入模式。编辑完之后,esc退出到命令行模式,输入:wq 写完保存并退出
vim ocr-server.sh
2 脚本内容
#!/bin/bash
run_vllm(){
pkill -f "vllm.entrypoints.openai.api_server"
sleep 2
# 无 >> 重定向,日志全部打印控制台
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/PaddleOCR-VL-1.6 \
--trust-remote-code \
--port 8000 \
--tensor-parallel-size 1 \
--allowed-local-media-path /mnt/data/ \
--gpu-memory-utilization 0.15 \
--max-model-len 4096 \
--root-path /paddleocr/predict
}
run_vllm
while true
do
H=$(date +%H)
if [ "$H" = "03" ];then
echo "【$(date)】凌晨3点重启服务"
run_vllm
sleep 3600
fi
sleep 60
done
3 执行脚本
bash ocr-server.sh
VI 设置凌晨重启的优点
-
彻底回收显存碎片、释放堆积占用(最关心的点)
完全清理显存碎片、归还全部占用的GPU显存
vLLM 长期连续运行(几天)会出现两类显存堆积问题:
显存碎片(内存碎片化)
vLLM 的 PagedAttention 分页缓存、KV 缓存池会不断申请、释放小块显存,时间久了 GPU 显存被拆成大量零散空隙;剩余空闲显存总量看着不少,但没有连续大块空间,新来的长文本请求会触发 OOM、被迫降吞吐、推理变慢。
进程一旦杀掉:GPU 驱动会强制回收该进程占用的所有显存,碎片直接清零;重启后重新初始化 KV 缓存、内存池,显存排布规整,推理性能回到刚开机最佳状态。
隐性显存泄漏
少数场景下:模型权重加载插件、视觉编码器、多轮会话缓存、第三方依赖库会存在微小内存泄漏,日积月累显存占用越来越高;重启进程可以彻底根除泄漏。 -
规避长时间运行引发的进程隐性 bug
vLLM、CUDA 驱动长期不间断运行会偶发小问题:
后台异步请求队列积压、请求状态卡死
端口监听异常、偶尔无法接入 API
视觉模型(你用的 PaddleOCR-VL 多模态)缓存异常,识别精度小幅下降
每日重启相当于给服务复位,把所有运行时状态重置为初始干净环境,大幅降低突发故障概率。 -
重置内部统计、会话缓存
长期运行会堆积大量过期会话上下文、历史请求元数据,重启清空这些冗余数据,降低 CPU 内存占用。 -
日志自动删除
情况 1:你坚持日志只打印控制台、不落地日志文件(你的习惯)
日志完全不会保存,重启后历史日志直接全部消失
前台终端 /screen 里的滚动日志:终端缓冲区只能保存一小段历史内容;
vLLM 进程杀死重启瞬间:之前所有打印出来的运行日志全部清空,从头开始输出新日志;
优点:永远不会堆积日志占用磁盘;
缺点:前一天的报错、调用记录彻底找不到,无法复盘问题。
六、部署PaddleOCR-VL-1.6
调查:
PPU单卡显存:96GB
模型权重文件大小:1.79 GB
最低启动分配显存大小:
gpu-memory-utilization 0.04
96 * 0.04 = 3.84 GB
黄金平衡点:
0.15是时延平稳度和显存开销的(每日凌晨低谷重启 + 10并发常规调用)
共计占用 14.4GB
I 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/PaddleOCR-VL-1.6 \
--trust-remote-code \
--port 8000 \
--tensor-parallel-size 1 \
--allowed-local-media-path /mnt/data/ \
--gpu-memory-utilization 0.15 \
--max-model-len 4096 \
--root-path /paddleocr/predict
>> /mnt/workspace/log-dir/PaddleOCR-VL-1.6.log 2>&1
II 测试命令
import base64
import requests
from openai import OpenAI
##### API 配置 #####
openai_api_key = "key"
openai_api_base = "http://ai-network-qwen.top/paddleocr/paddleocr/predict/v1"
client = OpenAI(
api_key=openai_api_key,
base_url=openai_api_base,
)
models = client.models.list()
model = models.data[0].id
def encode_base64_content_from_url(content_url: str) -> str:
"""Encode a content retrieved from a remote url to base64 format."""
with open(content_url, "rb") as image_file:
result = base64.b64encode(image_file.read()).decode("utf-8")
return result
# Task-specific base prompts
TASKS = {
"ocr": "OCR:",
"table": "Table Recognition:",
"formula": "Formula Recognition:",
"chart": "Chart Recognition:",
}
image_path = r"D:\projects\code\test\test_img.png"
messages = [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:png;base64,{encode_base64_content_from_url(image_path)}"
}
},
{
"type": "text",
"text": TASKS["ocr"]
}
]
}
]
response = client.chat.completions.create(
model=model,
messages=messages,
temperature=0.0,
)
print(f"Generated text: {response.choices[0].message.content}")
七、 部署Qwen3-Embedding-4B
调查:
PPU单卡显存:96GB
模型权重文件大小:7.5 GB = 7.8%
Model loading took 7.55 GiB
最低启动分配显存大小:
设置0.08时,
gpu-memory-utilization 0.08
96 * 0.08 = 7.68 GB
日志信息:
Available KV cache memory: -0.64 GiB
设置0.09时,
gpu-memory-utilization 0.09
96 * 0.09 = 8.64 GB
日志信息:
Available KV cache memory: 0.32 GiB
黄金平衡点:
0.1是时延平稳度和显存开销的(每日凌晨低谷重启 + 10并发常规调用)
共计占用 9.6GB
vLLM 中:
Chat 对话:并发量完全由 KV Cache 剩余大小 决定
Embedding 向量推理:不占用 KV 缓存,并发上限只受 CPU 预处理、网络带宽、token 编码速度限制
10个并发属于极低负载,0.09 / 0.10 两个参数跑起来性能没有任何区别,唯一差距就是服务稳定性。
Model loading took 7.55 GiB memory and 3.782044 seconds
Available KV cache memory: 1.27 GiB
I、部署embedding模型服务,路由填写逻辑:
1、Model API
AI网关 -> Model API -> 创建Model API:
- 协议:OpenAI兼容 (embeddings,比如qwen3.6-35B-A3B这个模型,后缀就是/v1/chat/completions、/v1/completions、/v1/responses。换成anthropic公司的协议就是:/v1/messages)
- 域名:AInetwork.top(HTTP)
- BasePath:/embedding4b (勾选 “转发至后端服务时移除”)
进入Model API:qwen3-embedding-4b -> 路由列表 -> 编辑路由 -> 查看:路径(Path),三部分(BasePath根路径:embedding4b + 第一版接口协议规范:v1 + openAI协议后缀:embeddings)组成
v1 = Version 1,第 1 版接口协议规范,就是接口版本号
OpenAI、Anthropic、所有兼容 OpenAI 格式的大模型后端(vLLM、SGLang、Ollama、通义千问、DeepSeek 等),全都统一沿用这套设计。

最终前端调用的路由地址(url)为:
http://AInetwork.top/embedding4b/v1/embeddings
测试:
在DSW部署模型服务的服务器上,另开一个控制台终端命令行窗口,用这个地址:http://localhost:8001/v1/embeddings ,让大模型生成一个简单的调用命令,查看响应码是否为200。
是200,请求正常的话,说明 http://AInetwork.top/v1/embeddings 等于 http://localhost:8001/v1/embeddings
调用不同,请检查这个模型的openai协议是否需要填写模型字段,需要写的话,应该写DSW启动服务时,填写的模型完整名称。
2、服务:
服务 -> 创建服务
服务来源:固定地址
服务名称:embedding-server
服务地址:DSW ip:端口号
TLS模式:关闭
II、新增服务和端口配置 & 放开对应的ip和端口访问
DSW – 变更配置 – 新增服务和端口配置 – 入方向(新增规则)

III、AI网关(配置服务)

1 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/Qwen3-Embedding-4B \
--trust-remote-code \
--port 8001 \
--gpu-memory-utilization 0.10 \
--max-model-len 2048 \
>> /mnt/workspace/log-dir/Qwen3-Embedding-4B.log 2>&1
2 测试命令
curl -X POST http://localhost:8001/v1/embeddings \
-H "Content-Type: application/json" \
-d '{
"model": "/mnt/data/Qwen3-Embedding-4B",
"input": "这是一段用于测试向量化效果的文本。"
}'
八、 部署bge-reranker-large
调查:
PPU单卡显存:96GB
模型权重文件大小:
Model loading took 1.05 GiB memory
Capturing CUDA graphs (mixed prefill-decode, PIECEWISE): 0%|
最低启动分配显存大小:
重排模型(Reranker):是一次性前向传播(One-pass Forward)的。它把 Query 和 Document 拼接在一起,一次性送入模型,直接输出一个相关性分数,然后就结束了。它不需要“一个字一个字地生成”,所以不存在随着时间步动态增长的 KV Cache。
I 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/bge-reranker-large \
--trust-remote-code \
--port 8002 \
--gpu-memory-utilization 0.04 \
--max-model-len 512 \
>> /mnt/workspace/log-dir/bge-reranker-large.log 2>&1
II 测试
curl -X POST http://localhost:8002/v1/rerank \
-H "Content-Type: application/json" \
-d '{
"model": "/mnt/data/Qwen3-Reranker-4B",
"query": "中国的首都是哪里?",
"documents": ["北京是中国的首都。", "地球在围绕太阳公转。"]
}'
九、 部署Qwen3-4B-Instruct-2507
调查:
PPU单卡显存:96GB
模型权重文件大小:7.5GB
Model loading took 7.61 GiB memory
Available KV cache memory: 0.58 GiB
Capturing CUDA graphs (mixed prefill-decode, PIECEWISE): 100%
最低启动分配显存大小:
gpu-memory-utilization 0.11
模型最大的上下文上限262k:–max-model-len 262144
设置32k,gpu-memory-utilization 0.2
Available KV cache memory: 10.35 GiB
I 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/Qwen3-4B-Instruct-2507 \
--trust-remote-code \
--port 8003 \
--gpu-memory-utilization 0.2 \
--max-model-len 32768 \
>> /mnt/workspace/log-dir/Qwen3-4B-Instruct-2507.log 2>&1
II 测试
from openai import OpenAI
client = OpenAI(
api_key="秘钥",
base_url="http://域名/instruct/v1", # 请确认你的 base_url 路径是否正确
)
try:
# 使用非流式进行最简单的测试
response = client.chat.completions.create(
model="/mnt/data/Qwen3-4B-Instruct-2507", # 替换为你实际可用的模型名
messages=[{"role": "user", "content": "你好"}],
max_tokens=50,
stream=False # 先关闭流式,方便排查
)
# 检查返回内容是否为 None
content = response.choices[0].message.content
if content is None:
print("警告:模型返回了 None,可能是触发了安全过滤或模型未生成内容。")
else:
print("模型正常回复:", content)
except Exception as e:
print(f"发生错误: {e}")
十、 部署Qwen3-Embedding-8B
–gpu-memory-utilization 0.15
启动模型服务所需:
Model loading took 14.11 GiB memory
Available KV cache memory: -0.77 GiB
I 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/Qwen3-Embedding-8B \
--trust-remote-code \
--port 8001 \
--gpu-memory-utilization 0.20 \
--max-model-len 2048 \
>> /mnt/workspace/log-dir/Qwen3-Embedding-8B.log 2>&1
十一、 部署Qwen3-Reranker-8B
gpu分配比例计算
模型权重大小:15.3 GB = 0.16
系统开销:1.2G = 0.012
Model loading took 15.26 GiB
0.16 Available KV cache memory: -1.17 GiB
0.17 Available KV cache memory: -0.21 GiB
最低就是:0.18
定位:0.25
Available KV cache memory: 7.44 GiB
I 启动命令
python -m vllm.entrypoints.openai.api_server \
--model /mnt/data/Qwen3-Reranker-8B \
--runner pooling \
--dtype bfloat16 \
--trust-remote-code \
--port 8004 \
--gpu-memory-utilization 0.25 \
--max-model-len 2048 \
--served-model-name Qwen3-Reranker-8B \
--hf-overrides '{"architectures": ["Qwen3ForSequenceClassification"], "classifier_from_token": ["no", "yes"], "is_original_qwen3_reranker": true}' \
>> /mnt/workspace/log-dir/Qwen3-Reranker-8B.log 2>&1
解释:
不是单一参数 "开关",而是一组参数共同作用让 vLLM 把模型识别为打分模型,从而注册 <code>/v1/rerank</code> 端点。核心逻辑链如下:
使能 /v1/rerank 的参数链路
--runner pooling
↓ (选择 pooling 运行器,而非默认的 generate 运行器)
--hf-overrides: architectures=["Qwen3ForSequenceClassification"]
↓ (把原始 CausalLM 覆盖为分类架构,路由到分类推理路径)
--hf-overrides: classifier_from_token=["no", "yes"] + is_original_qwen3_reranker=true
↓ (触发 Qwen3-Reranker 专用的 logits 差分打分逻辑)
模型被识别为 score 模型 → API Server 自动注册 /v1/rerank 端点
逐个字段的作用
表格
| 参数 | 是否必需 | 作用 |
|---|---|---|
| <code>–runner pooling</code> | 最关键 | 不写这个,vLLM 默认走 <code>generate</code> 运行器,模型被当生成模型,根本不会注册 /v1/rerank |
| <code>architectures</code> | 必需 | 覆盖为 <code>Qwen3ForSequenceClassification</code>,让 vLLM 走分类推理代码路径,而不是生成路径 |
| <code>classifier_from_token</code> | 必需 | 告诉 vLLM 用 "no"/"yes" 两个 token 的 logits 差值作为相关性分数(Qwen3-Reranker 的打分原理) |
| <code>is_original_qwen3_reranker</code> | 必需 | 标记这是原始版 Qwen3-Reranker,触发 vLLM 内部针对该模型的特殊处理(特殊的 chat template 格式化、分数计算等) |
一句话总结
<code>–runner pooling</code> 是入口开关(决定走 pooling 模式而非生成模式),<code>–hf-overrides</code> 里的三个字段是身份凭证(让 vLLM 认出这是 Qwen3-Reranker 打分模型)。两者缺一不可,少任何一个 <code>/v1/rerank</code> 都不会注册或返回错误结果。
十二、 常用命令
I 启动命令
1、启动服务
nohup /usr/local/bin/python /mnt/workspace/embedding-server.py >/dev/null 2>&1 &
2、测试端口号是否在监听
ss -tlnp | grep 8001
3. 实时查看日志(最稳妥排查方式)
tail -f /tmp/embedding-log.log
4. 杀进程
pkill -f "vllm.entrypoints.openai.api_server.*8001"
4. 配合ss -tlnp | grep 8001,查询线程id杀进程
kill -9 301727
5. 查看显存占用情况
在终端执行以下命令,查看是哪个进程占用了显存:
nvidia-smi
十三、 模版启动文件(embedding)
import subprocess
import logging
import sys
import time
import threading
import os
import signal
import socket
from datetime import datetime, timedelta
from logging.handlers import TimedRotatingFileHandler
# 前置依赖校验
try:
import psutil
except ImportError:
print("错误:未安装psutil依赖,请执行命令:pip install psutil", file=sys.stderr)
sys.exit(1)
# ====================== 核心配置区(按需自定义修改) ======================
LOG_FILE = "/tmp/embedding-log.log"
RESTART_TIME = "03:10" # 每日凌晨3点10分定时重启释放显存、KV缓存
PYTHON_PATH = "/usr/local/bin/python"
GPU_MEM_UTIL = "0.1"
SERVICE_PORT = 8001
VLLM_CMD = [
PYTHON_PATH, "-m", "vllm.entrypoints.openai.api_server",
"--model", "/mnt/data/Qwen3-Embedding-4B",
"--trust-remote-code",
"--port", str(SERVICE_PORT),
"--tensor-parallel-size", "1",
"--allowed-local-media-path", "/mnt/data/",
"--gpu-memory-utilization", GPU_MEM_UTIL,
"--max-model-len", "4096"
]
HEALTH_URL = f"http://127.0.0.1:{SERVICE_PORT}/v1/embeddings"
MAX_RETRY_TIMES = 5 # 进程连续崩溃最大重试次数
COOLDOWN_SEC = 300 # 重试耗尽后冷却锁定时长(5分钟)
RESTART_WINDOW_SEC = 120 # 定时重启前后2分钟时间窗口
SERVER_MAX_WAIT = 30 # 等待服务端口就绪最大时长
MONITOR_SLEEP_SEC = 0.5 # 监控线程轮询间隔
# =========================================================================
# 日志系统初始化
logger = logging.getLogger("embedding_server_manager")
logger.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - [EMBED-MGR] - %(levelname)s - %(message)s')
# 控制台输出
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
# 日志文件按天切割,保留7天日志
file_handler = TimedRotatingFileHandler(
LOG_FILE, when="midnight", interval=1, backupCount=7, encoding="utf-8"
)
file_handler.setFormatter(formatter)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# 全局多线程共享变量(互斥锁保护读写)
current_process: subprocess.Popen | None = None
vllm_pgid: int | None = None
proc_lock = threading.Lock()
retry_count = 0
is_manual_restart = False
def kill_all_vllm():
"""
安全进程清理逻辑:
1. 优雅终止当前脚本托管的完整vllm进程组(主进程+所有子worker)
2. 仅兜底清理ppid=1孤儿残留进程,不会误杀机器上其他业务vLLM服务
"""
global current_process, vllm_pgid
with proc_lock:
# 第一步:杀掉自身托管的进程组
if vllm_pgid is not None:
try:
os.killpg(vllm_pgid, signal.SIGTERM)
logger.info(f"发送TERM信号至进程组 {vllm_pgid},等待进程优雅退出")
start_wait = time.time()
while time.time() - start_wait < 10:
try:
os.getpgid(vllm_pgid)
except ProcessLookupError:
break
time.sleep(0.5)
else:
os.killpg(vllm_pgid, signal.SIGKILL)
logger.warning(f"进程组{vllm_pgid}超时未退出,强制Kill终止")
except ProcessLookupError:
logger.info(f"进程组 {vllm_pgid} 早已不存在,无需操作")
except Exception as e:
logger.error(f"终止进程组异常: {str(e)}")
# 第二步:遍历进程,只清理孤儿+自身残留进程
for proc in psutil.process_iter(["pid", "ppid", "pgid", "cmdline"]):
try:
info = proc.info
pid = info["pid"]
ppid = info["ppid"]
pgid = info.get("pgid", -1)
cmd_list = info.get("cmdline", [])
cmd_str = " ".join(cmd_list)
# 只筛选vllm向量服务进程
if "vllm.entrypoints.openai.api_server" not in cmd_str:
continue
# 清理范围:自身进程组残留 或者 系统孤儿进程(ppid=1)
if pgid == vllm_pgid or ppid == 1:
proc.terminate()
proc.wait(timeout=3)
logger.info(f"清理残留vllm进程 pid={pid}")
except (psutil.NoSuchProcess, psutil.AccessDenied, KeyError):
continue
except Exception as e:
logger.error(f"清理进程异常: {str(e)}")
current_process = None
vllm_pgid = None
logger.info("vLLM进程清理全部完成")
def log_consumer(stream, name: str, log_level: int):
"""读取子进程stdout/stderr日志,区分日志级别输出"""
for raw_line in iter(stream.readline, b""):
text = raw_line.decode("utf-8", errors="ignore").strip()
if not text:
continue
msg = f"[{name}] {text}"
if log_level == logging.WARNING:
logger.warning(msg)
elif log_level == logging.ERROR:
logger.error(msg)
else:
logger.info(msg)
def is_port_listen(port: int, timeout: int = 1) -> bool:
"""检测本地端口是否处于监听状态"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect(("127.0.0.1", port))
sock.close()
return True
except (socket.timeout, ConnectionRefusedError):
sock.close()
return False
def wait_server_ready(max_wait_sec: int) -> bool:
"""循环等待服务端口就绪"""
start_ts = time.time()
while time.time() - start_ts < max_wait_sec:
if is_port_listen(SERVICE_PORT):
logger.info(f"vLLM服务端口 {SERVICE_PORT} 监听就绪")
return True
time.sleep(1)
logger.error(f"等待端口{SERVICE_PORT}就绪超时{max_wait_sec}s,跳过预热步骤")
return False
def warm_up_service():
"""服务启动后执行推理预热,消除首次请求延迟;无curl时自动跳过"""
if not wait_server_ready(SERVER_MAX_WAIT):
return
logger.info("开始执行向量服务推理预热")
curl_cmd = [
"curl", "-s", "-X", "POST", HEALTH_URL,
"-m", "10", "--retry", "2", "--retry-delay", "2",
"-H", "Content-Type: application/json",
"-d", '{"input":"test向量预热文本","model":"/mnt/data/Qwen3-Embedding-4B"}'
]
try:
res = subprocess.run(curl_cmd, capture_output=True, text=True, timeout=15)
if res.returncode == 0:
logger.info("预热请求执行成功,推理链路正常")
else:
logger.warning(f"预热请求失败 stdout:{res.stdout} stderr:{res.stderr}")
except FileNotFoundError:
logger.warning("系统未安装curl命令,跳过服务预热")
except Exception as e:
logger.warning(f"预热过程发生异常: {str(e)}")
def start_vllm():
"""启动vLLM向量服务,加锁保证单实例运行,保存进程组ID"""
global current_process, retry_count, vllm_pgid
with proc_lock:
if current_process is not None:
logger.warning("vLLM服务正在运行,跳过重复启动")
return
logger.info("开始拉起vLLM向量嵌入服务")
try:
current_process = subprocess.Popen(
VLLM_CMD,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
start_new_session=True # 创建独立进程组,方便整体杀死
)
# 短暂延时,规避子进程未初始化完成导致pgid读取失败
time.sleep(0.2)
vllm_pgid = os.getpgid(current_process.pid)
logger.info(f"vLLM进程组ID已记录: {vllm_pgid}")
# 后台线程消费子进程日志
threading.Thread(
target=log_consumer,
args=(current_process.stdout, "STDOUT", logging.INFO),
daemon=True
).start()
threading.Thread(
target=log_consumer,
args=(current_process.stderr, "STDERR", logging.WARNING),
daemon=True
).start()
retry_count = 0
warm_up_service()
except Exception as e:
logger.error(f"vLLM服务启动失败: {str(e)}")
current_process = None
vllm_pgid = None
retry_count += 1
def trigger_restart():
"""定时重启入口:清理进程+重新拉起服务"""
global is_manual_restart
logger.warning(f"抵达每日定时重启时间 {RESTART_TIME},执行重启释放显存")
with proc_lock:
is_manual_restart = True
kill_all_vllm()
time.sleep(3)
start_vllm()
is_manual_restart = False
def get_next_restart_delay_sec() -> float:
"""计算距离下一次定时重启还需要休眠多少秒"""
hh, mm = map(int, RESTART_TIME.split(":"))
now = datetime.now()
target_today = now.replace(hour=hh, minute=mm, second=0, microsecond=0)
if now < target_today:
next_target = target_today
else:
next_target = target_today + timedelta(days=1)
delta = next_target - now
return delta.total_seconds()
def time_check_job():
"""定时任务线程:精准每日定点重启,无无效轮询"""
while True:
delay = get_next_restart_delay_sec()
logger.info(f"距离下次定时重启剩余 {round(delay/60, 2)} 分钟")
time.sleep(delay)
now = datetime.now()
hh, mm = map(int, RESTART_TIME.split(":"))
target = now.replace(hour=hh, minute=mm, second=0, microsecond=0)
window_start = target - timedelta(seconds=RESTART_WINDOW_SEC // 2)
window_end = target + timedelta(seconds=RESTART_WINDOW_SEC // 2)
# 落在时间窗口内才执行重启
if window_start <= now <= window_end:
trigger_restart()
# 窗口冷却,防止短时间重复触发
time.sleep(RESTART_WINDOW_SEC + 10)
def monitor_process():
"""常驻监控线程:检测进程崩溃自动重启,锁外休眠杜绝死锁"""
global current_process, retry_count, vllm_pgid
while True:
time.sleep(MONITOR_SLEEP_SEC)
need_delay_start = False
with proc_lock:
# 手动重启期间暂时跳过监控
if is_manual_restart:
continue
# 进程不存在,判断是否需要重试拉起
if current_process is None:
if retry_count < MAX_RETRY_TIMES:
logger.warning(f"vLLM进程意外丢失,5秒后重启 ({retry_count}/{MAX_RETRY_TIMES})")
need_delay_start = True
else:
logger.error(f"连续崩溃{MAX_RETRY_TIMES}次,锁定{COOLDOWN_SEC//60}分钟禁止自动重启")
time.sleep(COOLDOWN_SEC)
retry_count = 0
continue
else:
# 检测进程是否异常退出
exit_code = current_process.poll()
if exit_code is not None:
logger.error(f"vLLM进程异常退出,退出码:{exit_code}")
current_process = None
vllm_pgid = None
# 锁外执行休眠,休眠完毕重新加锁拉起进程,消除并发竞态
if need_delay_start:
time.sleep(5)
with proc_lock:
start_vllm()
if __name__ == "__main__":
logger.info("=" * 65)
logger.info("Embedding VLLM 守护管理器 启动成功")
logger.info(f"日志存储路径: {LOG_FILE}")
logger.info(f"服务监听端口: {SERVICE_PORT}")
logger.info(f"每日定时重启: {RESTART_TIME} ± {RESTART_WINDOW_SEC//2}秒窗口")
logger.info(f"GPU显存占用上限: {GPU_MEM_UTIL}")
logger.info("=" * 65)
# 启动定时任务线程(已修复变量名错误)
timer_thread = threading.Thread(target=time_check_job, daemon=True)
timer_thread.start()
# 首次启动vllm服务
start_vllm()
# 主线程进入常驻监控循环
monitor_process()