在PAI-EAS上使用PPU部署模型服务

更新时间:
复制为 MD 格式

在模型开发训练完成后,如果您想要在其他应用中调用,可以使用PAI-EAS将模型部署为服务。

了解PAI-EAS

模型在线服务EAS(Elastic Algorithm Service)是PAI提供的模型在线服务平台,可支持您一键部署模型为在线推理服务或AI-Web应用。它提供了弹性扩缩容、资源组管理、版本控制、资源监控等功能,可以支撑您以较低的资源成本获取高并发且稳定的在线算法模型服务。更多内容请参见EAS概述

前提条件

在使用PAI将模型部署为服务之前,请开通PAI并购买PPU资源

使用PAI-EAS部署模型

PAI-真武810E镜像

说明
  • 为方便客户快速在PAI Serverless上使用ml.gp7vf.16.40xlarge资源(真武810E),发布PAI Serverless官方镜像,集成真武810E、高网、PAI等各层能力,提供开箱即用的体验和最优的性能表现。

  • 本镜像当前处于内测阶段,使用过程中请务必履行保密义务。

  • 本镜像仅支持在PAI Serverless平台内(包含DSW、DLC、EAS等模块)使用。

推理镜像获取方式及详细说明,请参见推理镜像

准备模型

PAI支持对象存储OSS,NAS以及智算CPFS等存储介质,您可以将模型上传到有权限访问的存储实例中,供模型部署时使用。

方式一:上传模型到对象存储OSS

本文后续部署流程采用本方式。

  1. 登录对象存储OSS控制台,单击创建Bucket,参数说明请参见创建存储空间

    image.png

  2. 新建目录并单击上传文件上传模型文件。

    image.png

  3. 在部署EAS服务时,存储挂载选择OSS以获取模型。

    image

方式二:使用NAS或智算CPFS中的模型

以下说明如何通过DSW将模型保存到NAS或智算CPFS中。

  1. 登录PAI控制台左上角选择支持PPU资源的地域,本文以乌兰察布为例为您介绍操作步骤,左侧菜单栏单击交互式建模(DSW)> 新建实例,详情请参见使用PAI-DSW创建PPU开发环境

    说明

    目前支持PPU资源的地域包括:乌兰察布、北京、上海、杭州。

    image

  2. 在参数页面的数据集挂载区域,添加自定义数据集,然后新建数据集,按需选择存储类型。

    imageimage

  3. 进入DSW实例,在Notebook上传模型文件。

    image.png

    另外也可以通过DSW下载模型文件到存储介质中,

    image.png

    pip install modelscope
    modelscope download --model Qwen/Qwen2.5-7B-Instruct --local_dir ./

通过EAS控制台部署模型

  1. 选择对应的工作空间,进入EAS服务页面,单击部署服务

    image

  2. 选择自定义部署,进入部署页面。

    image

  3. 配置模型服务相关信息,以下仅列举关键参数,更多参数说明请参见EAS控制台自定义部署参数说明

    • 服务名称:部署的模型服务名称,建议涵盖作者、模型、参数规模、使用资源、框架等信息。

    • 部署方式:选择镜像部署

    • 镜像配置:使用PPU专属的镜像。例如:选择官方镜像,搜索并选择vllm:0.10.0-xpu1.6.1。更详细的镜像说明请参见PAI镜像

    • 存储挂载:假设模型存储在OSS,路径为oss://examplebucket******/modelscope_qwen/Qwen/Qwen3-8B,则

      • Uri:模型所在的OSS路径,如:oss://examplebucket******/modelscope_qwen/

      • 挂载路径:默认/mnt/data/ 即可。

    • 运行命令:结合使用框架填写模型部署执行命令。以上述模型挂载路径为例,启动命令为:vllm serve /mnt/data/Qwen/Qwen3-8B --port 9000

    • 端口:9000。结合执行命令填写,注意需要和容器内使用的端口保持一致。

      重要

      框架拉起服务的命令中的 TP 值需要和服务部署时每个实例需要的 GPU 卡数相符,否则将报错!

    • 资源类型:选择资源配额

    • 资源配额:选择创建的PPU资源配额。

    • 实例数:表示提供服务的Pod数量,最小为1,结合资源量按需填写。

    • 部署资源:表示每个Pod需要的资源数量,包括CPU、内存和真武810E卡数的信息。按需配置GPU、CPU、内存等规格参数。如:GPU1、CPU16。

    • 专有网络配置:结合实际情况选择需要的专有网络VPC,主要用于其他云上产品的使用。它为用户提供了一个隔离的网络环境,让用户可以在自己定义的网络空间中部署和使用云资源。

    • 交换机:用于连接通信不同的云资源,是构建VPC网络的重要组件之一,允许同一VPC下的不同实例间或与外部网络间的通信。

    • 安全组:一种虚拟防火墙,具备状态检测和数据包过滤功能,用于在云端划分安全域。通过配置入站规则和出站规则来控制进出ECI(弹性容器实例)、ECS(弹性计算服务)等实例的数据流。每个实例必须隶属于至少一个安全组以确保其网络安全。

  4. 完成参数配置后,单击部署。当服务处于运行中时,代表部署成功。

【可选】通过JSON文件部署模型

在部署页面点击JSON独立部署选项。按照格式将相应的表单配置以JSON文件的形式填写即可。详细信息可参考自定义部署

JSON文件示例

上述通过控制台部署的服务可以通过此JSON文件等价部署。

{
    "metadata": {
        "name": "test_***",
        "instance": 1,
        "workspace_id": "1****",
        "quota_id": "quota120***",
        "resource_burstable": false,
        "cpu": 16,
        "gpu": 1,
        "memory": 64000
    },
    "containers": [
        {
            "image": "eas-registry-vpc.cn-wulanchabu.cr.aliyuncs.com/pai-eas/pai-quickstart:xpu1.6.1-vllm0.10.0",
            "script": "vllm serve /mnt/data/Qwen/Qwen3-8B --port 9000",
            "port": 9000
        }
    ],
    "storage": [
        {
            "oss": {
                "path": "oss://examplebucket******/modelscope_qwen/",
                "readOnly": false
            },
            "mount_path": "/mnt/data/"
        }
    ],
    "options": {
        "priority": 9
    }
}

PAI-EAS模型服务调用

PAI-EAS模型服务提供公网和VPC访问,根据客户端所在网络环境选择,详见调用方式概览

调用方式

EAS部署模型服务后,系统会自动生成调用地址和Token。您可以单击服务名称进入概览页签,在基本信息区域,单击查看调用信息获取。

image

通过该调用信息可以进行调用测试,示例如下

curl -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: GI1YWYxNmQwYjMzMDM1YTNxxxxYjIzZTVlZGQ0NDJ*******" \
    -d '{"prompt":"hello world", "stream":"true"}' \
    http://1095312831******.cn-wulanchabu.pai-eas.aliyuncs.com/api/predict/test****/v1/completions
import json
import requests
from typing import Dict, List

url = "http://1095312831******.cn-wulanchabu.pai-eas.aliyuncs.com/api/predict/test****/v1/completions"
prompt = "hello world"
req = {
    "prompt": prompt,
    "stream": True,
    "temperature": 0.0,
    "top_p": 0.5,
    "top_k": 10,
    "max_tokens": 300,
}
response = requests.post(
    url,
    json=req,
    headers={"Content-Type": "application/json", "Authorization": "GI1YWYxNmQwYjMzMDM1YTNlMmFmNmEzYjIzZTVlZGQ0NDJ*******"},
    stream=True,
)
for chunk in response.iter_lines(chunk_size=8192, decode_unicode=False):
    msg = chunk.decode("utf-8")
    if msg.startswith('data'):
        info = msg[6:]
        if info == '[DONE]':
            break
        else:
            resp = json.loads(info)
            print(resp['choices'][0]['text'], end='', flush=True)

服务压测

压测脚本参考:

压测脚本1

#!/bin/bash
#
# Usage:
# set the cocurrency and related sample size list in the script, generally size = 20~50 * cocurrency
# $ bash ./auto_bench_qwen.sh -D {benchmark_data_path} -T /root/Qwen-7B -O {result_dir_path} -S ws://localhost:8081/generate_stream
# The results of each concurrency run will be saved in the reslut folder named by result_bladellm_qwen_chat_CONCURRENCY_${Concurrency}_date.md}.

PYTHON=python
Now=$(date +"%Y%m%d_%H%M%S")

ConcurrencyList=(10)
SizeList=(50)

Dir=$(realpath $(dirname -- "${BASH_SOURCE[0]}"))
BenchmarkDir=$(dirname ${Dir})

DataPath=/mnt/workspace/test/benchmarks/llm_bench-main-39a4911348bf5baa44ab1976cec73c9ca62ce4f5/test_prompt/prompt_source_en.txt
TokenizerPath=/mnt/workspace/test/qwen-ckpts/Qwen2.5-72B
ResultDir=${BenchmarkDir}/result/${Now}
ServerUrl=http://10953xxxx.cn-wulanchabu.pai-eas.aliyuncs.com/api/predict/test_qwen_2_5_72b_vllm/v1/completions

while [[ "$1" != "" ]]; do
    case $1 in
        -D | --data )
            shift
            DataPath=$1
            ;;
        -T | --tokenizer )
            shift
            TokenizerPath=$1
            ;;
        -O | --output )
            shift
            ResultDir=$1
            ;;
        -S | --server )
            shift
            ServerUrl=$1
            ;;
        * )
            ;;
    esac
    shift
done

if [ ! ${#ConcurrencyList[*]} == ${#SizeList[*]} ]; then
  echo "ConcurrencyList and SizeList should have the same length."
  exit 1
fi

if [ ! -d ${ResultDir} ]; then
    mkdir -p ${ResultDir}
fi

export PYTHONPATH=./
for ((i=0;i<${#ConcurrencyList[*]};i++)) do
    LogDir=${ResultDir}/log/concurrency_${ConcurrencyList[i]}
    CacheDir=${ResultDir}/cache/concurrency_${ConcurrencyList[i]}
    if [ ! -d ${LogDir} ]; then
        mkdir -p ${LogDir}
    fi
    if [ ! -d ${CacheDir} ]; then
        mkdir -p ${CacheDir}
    fi

    $PYTHON ${BenchmarkDir}/benchmark.py \
        --mode CONCURRENCY \
        --concurrency ${ConcurrencyList[i]} \
        --prompt_source_path ${DataPath} \
        --prompt_mode normal \
        --model_max_output_len 3000 \
        --mean 300 \
        --cat_prompt_chatml \
        --size ${SizeList[i]} \
        --model_max_input_len 450 \
        --model_max_sequence_len 3450 \
        --max_new_tokens 3000 \
        --tokenizer ${TokenizerPath} \
        --trust_remote_code \
        --verbose \
        --log \
        --log_level INFO \
        --timeout 300 \
        --stop prompt \
        --framework openai \
        --model_type qwen_chat \
        --temperature 0.85 \
        --top_p 0.8 \
        --presence_penalty 1.7 \
        --stop_tokens 151643 151644 151645 \
        --url ${ServerUrl} \
        --result_dir ${ResultDir} \
        --log_dir ${LogDir} \
        --cache_dir ${CacheDir} \
        --auth NTVmNjk2NWJhZjxxxxxxx==
done

操作完毕可见如下示意图。

image.png

获取完整压测工具如下:bench_toolkit.tar.gz

压测脚本2:输出长度为[128, 256, 512, 1024, 2048, 4096],输出长度随机。

压测脚本2

#!/bin/bash
#
# Usage:
# $ bash ./auto_bench_qwen.sh -D {benchmark_data_path} -T /root/Qwen-7B -O {result_dir_path} -S ws://localhost:8081/generate_stream
# The results of each concurrency run will be saved in the result folder named by result_bladellm_qwen_chat_CONCURRENCY_${Concurrency}_${Size}_${Now}.md.

PYTHON=python3
Now=$(date +"%Y%m%d_%H%M%S")

# Define concurrency and size pairs
declare -A ConcurrencySizeMap=(
    [10]=128
    [20]=256
    [40]=512
    [80]=1024
    [160]=2048
    [320]=4096
)

Dir=$(realpath $(dirname -- "${BASH_SOURCE[0]}"))
BenchmarkDir=$(dirname ${Dir})
model_config['protected_namespaces'] = ()


DataPath=/mnt/workspace/THU/benchmarks/benchmarks/llm_bench-main-39a4911348bf5baa44ab1976cec73c9ca62ce4f5/test_prompt/shareGPT_llama2_all_82780.json
TokenizerPath=/mnt/workspace/THU/qwen-ckpts/Qwen2.5-14B
ResultDir=${BenchmarkDir}/result/${Now}
ServerUrl=http://1095312831785714.cn-wulanchabu.pai-eas.aliyuncs.com/api/predict/test_thu_ppu_1211/v1/completions

IgnoreEos=false

while [[ "$1" != "" ]]; do
    case $1 in
        -D | --data )
            shift
            DataPath=$1
            ;;
        -T | --tokenizer )
            shift
            TokenizerPath=$1
            ;;
        -O | --output )
            shift
            ResultDir=$1
            ;;
        -S | --server )
            shift
            ServerUrl=$1
            ;;
        --ignore_eos )
            shift
            IgnoreEos=$1
            ;;
        * )
            ;;
    esac
    shift
done

if [ ! -d ${ResultDir} ]; then
    mkdir -p ${ResultDir}
fi

export PYTHONPATH=./

for Concurrency in "${!ConcurrencySizeMap[@]}"; do
    Size=${ConcurrencySizeMap[$Concurrency]}
    LogDir=${ResultDir}/log/concurrency_${Concurrency}_size_${Size}
    CacheDir=${ResultDir}/cache/concurrency_${Concurrency}_size_${Size}
    if [ ! -d ${LogDir} ]; then
        mkdir -p ${LogDir}
    fi
    if [ ! -d ${CacheDir} ]; then
        mkdir -p ${CacheDir}
    fi

    # Generate a random string of specified size for testing
    RandomString=$(openssl rand -base64 $((Size / 8)) | head -c $Size)

    echo "Running benchmark with concurrency ${Concurrency} and size ${Size}"

    $PYTHON ${BenchmarkDir}/llm_bench-main-39a4911348bf5baa44ab1976cec73c9ca62ce4f5/benchmark.py \
        --mode CONCURRENCY \
        --concurrency ${Concurrency} \
        --prompt_mode default \
        #default,custom,fromfile,normal,shareGPT,chatml,code
        --prompt_source_list ${DataPath} \
        --size ${Size} \
        --prompt_source_path ${DataPath} \
        --model_max_input_len ${Size} \
        --max_new_tokens ${Size} \
        --tokenizer_dir ${TokenizerPath} \
        --trust_remote_code \
        --verbose \
        --log \
        --log_level INFO \
        --timeout 100 \
        --stop prompt \
        --framework openai \
        --request_type completion \
        --model_type qwen_chat \
        --temperature 0.85 \
        --top_p 0.8 \
        --presence_penalty 1.7 \
        --url ${ServerUrl} \
        --result_dir ${ResultDir} \
        --log_dir ${LogDir} \
        --cache_dir ${CacheDir} \
        --ignore_eos ${IgnoreEos} \
        --auth NWUzMTU2Y2E1ZDQ3ZGI5MmMwOThkMjYwNzRiMWUxMTE5Y2I0YzZmMQ== \
        
done

压测脚本3:输出长度为[128, 256, 512, 1024, 2048, 4096]。

压测脚本3

#!/bin/bash
#
# Usage:
# $ bash ./auto_bench_qwen.sh -D {benchmark_data_path} -T /root/Qwen-7B -O {result_dir_path} -S ws://localhost:8081/generate_stream
# The results of each concurrency run will be saved in the result folder named by result_bladellm_qwen_chat_CONCURRENCY_${Concurrency}_${Size}_${Now}.md.

PYTHON=python3
Now=$(date +"%Y%m%d_%H%M%S")

# Define concurrency and size pairs
declare -A ConcurrencySizeMap=(
    [10]=128
    [20]=256
    [40]=512
    [80]=1024
    [160]=2048
    [320]=4096
  
)


Dir=$(realpath $(dirname -- "${BASH_SOURCE[0]}"))
BenchmarkDir=$(dirname ${Dir})
model_config['protected_namespaces'] = ()


DataPath=/mnt/workspace/THU/benchmarks/benchmarks/llm_bench-main-39a4911348bf5baa44ab1976cec73c9ca62ce4f5/test_prompt/shareGPT_llama2_all_82780.json
TokenizerPath=/mnt/workspace/THU/qwen-ckpts/Qwen2.5-14B
ResultDir=${BenchmarkDir}/result/${Now}
ServerUrl=http://1095312831785714.cn-wulanchabu.pai-eas.aliyuncs.com/api/predict/test_thu_ppu_1211/v1/completions

IgnoreEos=false

while [[ "$1" != "" ]]; do
    case $1 in
        -D | --data )
            shift
            DataPath=$1
            ;;
        -T | --tokenizer )
            shift
            TokenizerPath=$1
            ;;
        -O | --output )
            shift
            ResultDir=$1
            ;;
        -S | --server )
            shift
            ServerUrl=$1
            ;;
        --ignore_eos )
            shift
            IgnoreEos=$1
            ;;
        * )
            ;;
    esac
    shift
done

if [ ! -d ${ResultDir} ]; then
    mkdir -p ${ResultDir}
fi

export PYTHONPATH=./

for Concurrency in "${!ConcurrencySizeMap[@]}"; do
    Size=${ConcurrencySizeMap[$Concurrency]}
    LogDir=${ResultDir}/log/concurrency_${Concurrency}_size_${Size}
    CacheDir=${ResultDir}/cache/concurrency_${Concurrency}_size_${Size}
    if [ ! -d ${LogDir} ]; then
        mkdir -p ${LogDir}
    fi
    if [ ! -d ${CacheDir} ]; then
        mkdir -p ${CacheDir}
    fi

    # Generate a random string of specified size for testing
    RandomString=$(openssl rand -base64 $((Size / 8)) | head -c $Size)

    echo "Running benchmark with concurrency ${Concurrency} and size ${Size}"

    $PYTHON ${BenchmarkDir}/llm_bench-main-39a4911348bf5baa44ab1976cec73c9ca62ce4f5/benchmark.py \
        --mode CONCURRENCY \
        --concurrency ${Concurrency} \
        --prompt_mode normal\
        --prompt_source_list ${DataPath} \
        --size ${Size} \
        --prompt_source_path ${DataPath} \
        --model_max_input_len ${Size} \
        --model_max_output_len ${Size} \
        --model_max_sequence_len 4096\
        --mean ${Size} \
        --max_new_tokens ${Size} \
        --tokenizer_dir ${TokenizerPath} \
        --trust_remote_code \
        --verbose \
        --log \
        --log_level INFO \
        --timeout 100 \
        --stop prompt \
        --framework openai \
        --request_type completion \
        --model_type qwen_chat \
        --temperature 0.85 \
        --top_p 0.8 \
        --presence_penalty 1.7 \
        --url ${ServerUrl} \
        --result_dir ${ResultDir} \
        --log_dir ${LogDir} \
        --cache_dir ${CacheDir} \
        --ignore_eos ${IgnoreEos} \
        --auth NWUzMTU2Y2E1ZDQ3ZGI5MmMwOThkMjYwNzRiMWUxMTE5Y2I0YzZmMQ== \
        
done

使用注意事项

vllm兼容性问题

vllm目前存在两个已知问题:

  1. 所有版本的vllm使用了mma ptx汇编语言实现awq量化算法,这部分能力在真武810E上并不兼容,需要从源码中注释后重新编译

  2. vllm 0.4.x开始支持的customer all reduce特性,与真武810E并不兼容,会导致服务hang住,并提示“asyncio.exceptions.CancelledError: Cancelled by cancel scope”错误

解决方案:

  1. 建议直接使用PAI-EAS在真武810E上拉起模型服务

  2. 如需使用vllm,使用PAI官方镜像内置的pip Registry安装真武810E版本,并在启动时加入--disable-custom-all-reduce参数