阿里云模型部署PAI平台使用手册 – DSW

内容纲要

方案:DSW + 灵骏智算 + Vllm部署

一、入口

aliyun.com -> 控制台 -> 人工智能平台PAI -> 左侧菜单栏 -> 模型开发与训练 -> 交互式建模(DSW) -> 开发机实例(新建实例) -> 编辑实例或者变更配置

二、编辑实例或者变更配置

I、配置 “资源信息”

资源配额:灵骏智算
资源规格:自选

file

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包等基础配置,无需自己后期再安装

file

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

1.1 下载ossutil命令行工具

file

file

1.2 解压工具

1) 解压ossutil-2.3.0-windows-amd64.zip

2) 进入ossutil.exe所在路径,在此处打开 PowerShell 窗口,执行命令,配置远程连接设置信息

.\ossutil.exe config -e oss-cn-wulanchabu.aliyuncs.com

file

3)、配置

直接按回车键即可。

按完回车后,程序会依次让输入以下三项信息:

accessKeyID:粘贴你的阿里云 AccessKey ID。

file

accessKeySecret:粘贴你的阿里云 AccessKey Secret(输入时屏幕不会显示字符,这是正常的,输完直接回车)。

Please enter Region : cn-wulanchabu (华北6(乌兰察布)的地域id)

language:输入 CH 并回车,将工具语言设置为中文。(如果有的话)

Please enter Endpoint:直接回车

全部输完后,如果看到类似 "保存成功" 的提示,就说明配置完成了,可以开始上传文件了。

file

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终端的挂载路径。

file

III 访问配置

file

1 安全组

入方向 -> 创建安全组\选择现有安全组 -> 入方向 -> 增加规则 -> 访问来源:选择DSW的ip地址 -> 访问目的:加入端口 -> 添加描述 -> 提交

vpc-ip地址查询:
交互式建模(DSW)- 进入DSW实例 – 拉到页面底部

file

服务名称:自选(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包

  1. 安装守护脚本依赖

    pip install psutil
  2. 安装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单卡环境

  1. 入口主程序
    python -m vllm.entrypoints.openai.api_server
    vLLM 官方内置的 OpenAI 兼容接口服务入口
    作用:启动一个 Web 服务,对外提供和 OpenAI ChatCompletion、Vision(多模态识图)一模一样的 API 格式,可以用 openai python sdk 直接调用这个 OCR 多模态模型,不用自己封装接口。

  2. –model /mnt/data/PaddleOCR-VL-1.6
    含义:指定要加载的模型本地路径
    路径:服务器挂载目录 /mnt/data/ 下存放的 PaddleOCR-VL-1.6 权重文件夹
    要求:该目录内必须包含完整模型权重、config.json、tokenizer 配置文件

  3. –trust-remote-code
    核心作用:允许执行模型仓库里自定义的 Python 代码
    PaddleOCR-VL 不属于原生 HuggingFace 标准 LLM 架构,有自己定制的模型推理代码、视觉编码逻辑;
    不加这个参数,vLLM 会拒绝加载第三方自定义模型代码,直接启动报错。

  4. –port 8000
    API 服务监听端口,就是你刚才查询的 8000 端口
    服务启动后:
    本地访问:http://127.0.0.1:8000/v1/chat/completions
    内网其他机器访问:http://服务器内网IP:8000/v1/chat/completions

  5. –tensor-parallel-size 1
    张量并行大小(TP 并行)
    TP:把模型权重拆分到多张 GPU 上并行计算,用于超大显存需求大模型
    数值 = 1:只用单张 PPU 卡(96GB)运行模型,不拆分权重到多卡
    你当前只分配了 1 张 GPU,固定写 1 即可;多卡场景才改成 2/4/8。

  6. –allowed-local-media-path /mnt/data/
    多模态专用参数(图文输入必备)
    vLLM 多模态接口支持传入本地图片路径让模型读图 OCR;
    这个参数是白名单目录:
    仅允许加载 /mnt/data/ 目录下的图片文件,禁止读取服务器其他路径文件(安全限制,防止越权读取系统文件)。
    刚好我的模型、图片素材都放在 /mnt/data 下,匹配我的存储目录。

  7. –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 模型非常保守稳定。

  8. –max-model-len 4096
    上下文最大序列长度
    限制整个对话 + 图片编码 + 文本输出总 token 上限:4096 tokens
    OCR 场景:图片编码 + 识别文字内容一般用不满 4096,足够使用;
    数值越大,KV 缓存需要显存越多,并发能力下降。

  9. –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模式:关闭

file

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 批量下载:

  1. 创建一个干净的目录存放离线包
    mkdir -p ~/cron_offline_pkg
cd ~/cron_offline_pkg
  1. 执行递归下载(核心命令)
    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 打包离线包

下载完成后,检查目录里的文件数量(通常会有几十个包),然后将其打包:

  1. 查看下载了多少个包
ls -l *.deb | wc -l
  1. 打包成 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 报错:

file

过滤危险包,只安装 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 设置凌晨重启的优点

  1. 彻底回收显存碎片、释放堆积占用(最关心的点)
    完全清理显存碎片、归还全部占用的GPU显存
    vLLM 长期连续运行(几天)会出现两类显存堆积问题:
    显存碎片(内存碎片化)
    vLLM 的 PagedAttention 分页缓存、KV 缓存池会不断申请、释放小块显存,时间久了 GPU 显存被拆成大量零散空隙;剩余空闲显存总量看着不少,但没有连续大块空间,新来的长文本请求会触发 OOM、被迫降吞吐、推理变慢。
    进程一旦杀掉:GPU 驱动会强制回收该进程占用的所有显存,碎片直接清零;重启后重新初始化 KV 缓存、内存池,显存排布规整,推理性能回到刚开机最佳状态。
    隐性显存泄漏
    少数场景下:模型权重加载插件、视觉编码器、多轮会话缓存、第三方依赖库会存在微小内存泄漏,日积月累显存占用越来越高;重启进程可以彻底根除泄漏。

  2. 规避长时间运行引发的进程隐性 bug
    vLLM、CUDA 驱动长期不间断运行会偶发小问题:
    后台异步请求队列积压、请求状态卡死
    端口监听异常、偶尔无法接入 API
    视觉模型(你用的 PaddleOCR-VL 多模态)缓存异常,识别精度小幅下降
    每日重启相当于给服务复位,把所有运行时状态重置为初始干净环境,大幅降低突发故障概率。

  3. 重置内部统计、会话缓存
    长期运行会堆积大量过期会话上下文、历史请求元数据,重启清空这些冗余数据,降低 CPU 内存占用。

  4. 日志自动删除
    情况 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 等),全都统一沿用这套设计。

file

最终前端调用的路由地址(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 – 变更配置 – 新增服务和端口配置 – 入方向(新增规则)

file

III、AI网关(配置服务)

file

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()

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部