一、需求背景

X公司当前使用N公司H20计算卡做DeepSeek推理业务,希望使用昇腾卡做相关测试对比,如果性能较优、则考虑选择昇腾产品。

当前,我们需要基于客户的使用场景,进行对应的性能测试、并得到测试输出结果,以便争取客户。

1.1 测试条件

  1. 客户自主提供星火测试脚本,并非使用昇腾benchmark_tools,因此需要一定的适配工作。
  2. 测试模型为满血版DeepSeek-R1。
  3. 需要使用昇腾侧最新提供的算子包来做测试验证。
  4. 使用两台Ascend Snt9B服务器,部署模式为Lite Server。
  5. 最大token为8k,输入输出分别为1000与100,并发从1逐步增加到64。

1.2 评估维度

  1. 输出token总吞吐量 - output_throughput
  2. 首token时延TP99(ms)- p99_ttft_ms
  3. 平均首token时延(ms)- mean_ttft_ms
  4. 增量时延TP99(ms)- p99_tpot_ms
  5. 平均增量时延(ms)- mean_tpot_ms

二、测试实践

2.1 环境检查

在部署前,我们需要检查当前机器环境是否满足条件,参考文档:

2.2 模型权重文件拉取

一般而言,我们需要利用OBS通来从公网下载对应的模型权重文件。

这里为了节省时间,我们直接使用scp从其他机器节点拉取即可。
通常情况下,我们会将模型拉取放在宿主机的/home下,也可以创建一个单独的DeepSeek文件夹存放。

2.3 获取推理镜像

这里,我们使用命令:

docker pull registry-cbu.huawei.com/atelier/pytorch_2_1_ascend:pytorch_2.1.0-cann_8.0.0-py_3.10-hce_2.0.2412-aarch64-snt9b-xxxxx

在拉取时,可能遇到相关问题,参考文档:

2.4 启动容器

启动命令:

docker run -itd \
--device=/dev/davinci0 \
--device=/dev/davinci1 \
--device=/dev/davinci2 \
--device=/dev/davinci3 \
--device=/dev/davinci4 \
--device=/dev/davinci5 \
--device=/dev/davinci6 \
--device=/dev/davinci7 \
-v /etc/localtime:/etc/localtime  \
-v /usr/local/Ascend/driver:/usr/local/Ascend/driver \
-v /etc/ascend_install.info:/etc/ascend_install.info \
--device=/dev/davinci_manager \
--device=/dev/devmm_svm \
--device=/dev/hisi_hdc \
-v /var/log/npu/:/usr/slog \
-v /usr/local/sbin/npu-smi:/usr/local/sbin/npu-smi \
-v /sys/fs/cgroup:/sys/fs/cgroup:ro \
- v ${dir}:${container_work_dir} \
--net=host \
--name ${container_name} \
${image_id} \
/bin/bash

对于容器的启动参数,需要注意的是:

  • ${dir}为宿主机中权重文件所在的路径,这里不需要完全等同,只需要包含权重文件的路径即可:
    • 例如,我们将权重文件放在了/home/DeepSeek目录下,那么这里${dir}可以直接指定为/home
  • ${container_work_dir}为需要挂载到的容器目录,一般而言我们可以指定为/home/DeepSeek,或者/data/models,需要注意的是:
    • 该目录不能挂载到容器中的/home/ma-user目录,此目录为ma-user用户家目录。如果容器挂载到/home/ma-user下,拉起容器时会与基础镜像冲突,导致基础镜像不可用。
    • 上述目录的指定与映射,可以实现容器与宿主机的文件共享,能够让容器读取到权重文件,也方便我们将容器内生成的测试数据导出到宿主机中。
  • ${container_name}为容器名称,我们自定义即可,无特殊要求。
  • {image_id}为容器启动时需要使用到的镜像id,即我们在2.3步骤获取的推理镜像id,可以通过docker images来查询获取。

2.5 进入容器

2.5.1 root进入容器

docker exec -u root -it ${container_name}  /bin/bash

2.5.2 NPU卡挂载检查

启动推理服务之前检查卡是否被占用、端口是否被占用,是否有对应运行的进程。

npu-smi info

2.5.3 检查卡间通信

分别在两台机器容器内执行:

for i in $(seq 0 7);do hccn_tool -i $i -ip -g;done

而后测试一台机器能否访问访问到另一台机器的卡:

hccn_tool -i 0 -ping -g address 29.81.xx.xx

2.6 安装算子包

2.6.1 下载算子包

wget https://cmc-nkg-artifactory.cmc.tools.huawei.com/artifactory/hwcloud-artifact/cloudbuild2.0/10110504/20E000NV-2R1K-E680-0000-913VXAB0zhOu/xxxxx/AscendCloud-6.5.913-xxxx.zip

2.6.2 解压安装OPP

unzip -o -d /home/ma-user AscendCloud-6.5.913-xxx.zip
unzip -o -d /home/ma-user AscendCloud-OPP-6.5.913-xxxx.zip

mkdir AscendCloud-OPP-folder
unzip -o -d /home/ma-user/AscendCloud-OPP-folder AscendCloud-OPP-6.5.913-torch-2.1.0-py310-aarch64-xxxx.zip

# 执行命令:
pip install ascend_cloud_ops_custom-*.whl
pip install ascend_cloud_ops_atb-*.whl
pip install ascend_cloud_ops_cann-*.whl
bash ascend_cloud_ops_custom_opp-*.run
bash ascend_cloud_ops_ascend_turbo-*.run

2.6.3 解压安装LLM

mkdir AscendCloud-LLM-folder
unzip -o -d /home/ma-user/AscendCloud-LLM-folder AscendCloud-LLM-6.5.913-xxxx.zip

# 执行命令配置代理,注意不需要改任何内容:
git config --global http.version HTTP/1.1
git config --global http.sslVerify false
git config --global http.postBuffer 1244288000
export proxy=http://10.155.192.138:8080
export http_proxy=$proxy
export https_proxy=${http_proxy}
export no_proxy=127.0.0.1,localhost,mirrors.tools.huawei.com,mirrors.myhuaweicloud.com,7.223.xxx.xxx
pip config set global.index-url  http://7.223.xxx.xxx/pypi/simple 
pip config set install.trusted-host 7.223.xxx.xxx
pip config set global.timeout 120

2.6.4 LLM安装构建

# 进入到 llm_inference/ascend_vllm,并执行build.sh
cd llm_inference/ascend_vllm
sh build.sh

需要注意的是,在执行build.sh时,可能会出现下面报错:

error: RPC failed; curl 18 transfer closed with outstanding read data remaining
error: 6392 bytes of body are still expected
fetch-pack: unexpected disconnect while reading sideband packet
fatal: early EOF
fatal: fetch-pack: invalid index-pack output

这种问题可能是网络波动,也可能是http.postBuffer不足。
针对该问题,我们再次执行:

  • git config --global http.postBuffer 1244288000

而后多重试几次sh build.sh,一般而言都可以成功。

2.7 创建模型启动脚本

这里,我们在容器内同一个目录下创建两个脚本:

  • run_vllm.sh
  • ray_worker_checker.py

2.7.1 run_vllm.sh

# 服务启动脚本:
source /home/ma-user/.bashrc
# 获取通信网卡名称
RAY_HEAD_IP=$2
VPC_PREFIX=$(echo "$RAY_HEAD_IP" | cut -d'.' -f1-2)
POD_INET_IP=$(hostname -I | tr ' ' '\n' | grep -o "^$VPC_PREFIX\.[0-9]\+\.[0-9]\+" | head -n 1)
POD_NETWORK_IFNAME=$(ifconfig | grep -B 1 "$POD_INET_IP" | head -n 1 | awk '{print $1}' | sed 's/://')
# 指定通信网卡
export GLOO_SOCKET_IFNAME=$POD_NETWORK_IFNAME
export TP_SOCKET_IFNAME=$POD_NETWORK_IFNAME
# 指定HCCL
export HCCL_SOCKET_IFNAME=$POD_NETWORK_IFNAME
export RAY_EXPERIMENTAL_NOSET_ASCEND_RT_VISIBLE_DEVICES=1
export PYTORCH_NPU_ALLOC_CONF=expandable_segments:True
# 配置通信算法的编排展开位置在Device侧的AI Vector Core计算单元
export HCCL_OP_EXPANSION_MODE=AIV
# 指定可使用的卡
export ASCEND_RT_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
# 开启MTP需要注释掉MERGE_QKV环境变量
export MERGE_QKV=1
export FUSED_EXPERTS_V2=1
echo "start actual vllm serve............................"
if [ "$1" == "head" ]; then
 ray start --head --num-gpus=8
 #等待ray worker连接
 CURDIR=$(cd "$(dirname "$0")";pwd)
 python3 $CURDIR/ray_worker_checker.py
 python -m vllm.entrypoints.openai.api_server --model ${container_model_path} \
 --max-num-seqs=256 \
 --max-model-len=8192 \
 --max-num-batched-tokens=8192 \
 --tensor-parallel-size=16 \
 --block-size=128 \
 --host=0.0.0.0 \
 --port=8092 \
 --gpu-memory-utilization=0.95 \
 --num-scheduler-steps=8 \
 --multi-step-stream-outputs=true \
 --trust-remote-code \
 --distributed-executor-backend=ray \
 --enable-reasoning \
 --reasoning-parser deepseek_r1 \
 --served-model-name=DeepSeek-R1 \
 --guided-decoding-backend lm-format-enforcer
fi

if [ "$1" == "worker" ]; then 
 mkdir -p ~/.cache/huggingface/
 sudo cp -rf ${container_model_path}/modules/ ~/.cache/huggingface/modules/
 command="ray start --address='$RAY_HEAD_IP:6379' --num-gpus=8 &> /dev/null"
 echo $command
 while true; do
 eval $command
 if [ $? -eq 0 ]; then
 echo "succeed to connect to ray head node"
 break
 else
 echo "failed to connect to ray head node, wait 5s....."
 sleep 5
 fi
 done
 echo "hold process..."
 while :; do sleep 2073600; done
fi

在上述脚本中,需要注意的是:

  • ${container_model_path}是容器中模型存放的具体地址,例如在上面我们启动容器时,我们指定了与宿主机映射的地址为/home/DeepSeek,那么此时实际地址就是/home/DeepSeek/DeepSeek-R1-w8a8-0210-50
  • max-model-lenmax-num-batched-tokens分别表示推理时输入+输出tokens数量的最大值,以及prefill阶段,最多会使用多少token。
    • 在当前的客户需求下,我们需要测试的token为8k,那么数值就是8 * 1024 = 8192。
    • 以此类推,如果时32k的场景,数值就是32 * 1024。
  • port,服务部署的端口,这里我们使用的时8092。
  • served-model-name=DeepSeek-R1,指定当前使用的模型名称,这样后面在做接口与性能测试时,就不需要指定完整的模型路径地址了。
  • enable-reasoning,是否支持Reasoning,对于DeepSeek-R1需要添加该配置。
  • reasoning-parser deepseek_r1,是否支持Reasoning Content,对于DeepSeek-R1需要添加该配置。

2.7.2 ray_worker_checker.py

import ray
import time
def wait_ray_workers_ready():
    target_count = 2 # 需用户手动根据节点个数修改。4机部署为4,2机部署为2。
    ray.init(address='auto')
    while True:
        try:
            nodes = ray.nodes()
            if len(nodes) == target_count:
                print("ray workers are ready.")
                break
            else:
                print("ray workers not ready, check 10s later....")
                time.sleep(10)
        except Exception as e:
            print(e)
if __name__ == "__main__":
    wait_ray_workers_ready()

需要注意的时,上述所有步骤均需要在两台节点上完成,步骤内容一致。

2.8 启动服务

服务启动层面,我们需要先启动主节点:

sh run_vllm.sh head ${主节点ip}

等到主节点启动成功、显示等待worker连接时,再在worker节点上启动:

sh run_vllm.sh worker ${主节点ip}

此时我们只需要等待即可,具体启动进度可参考主节点终端页面的显示输出。

2.8.1 相关问题

在启动集群部署服务脚本时,经常会启动到一半报错:

ResourceWarning: Implicitly cleaning up
或者类似socket未关闭之类的问题。

究其原因,就是上一次启动的服务没有完全关闭,导致本次启动失败。

目前来看的解决方法,就是退出容器后,直接restart两个节点的容器,然后重新以root账户进入、执行run_vllm.sh脚本,一般而言就能解决问题、成功启动。

2.9 修改客户测试脚本

一般而言,我们在做性能测试时,使用的是LLM算子包中的bench_tools,不过这里,由于用户在H20上的测试使用的是个人脚本,为了拉齐结论,这里我们也同样需要使用该脚本。

脚本总体内容较多,这里我们重点看启动部分脚本:

2.9.1 bench_loop.sh

#!/bin/bash
set -ex
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
echo 'SCRIPT_DIR: ' $SCRIPT_DIR

# 定义 input 和 output 长度
INPUT_LEN=1000
OUTPUT_LEN=100

declare -a request_rates=(inf)
declare -a max_concurrency=(2)
declare -a num_prompts=(100)

DATA_JSON_PATH=$SCRIPT_DIR/ShareGPT_V3_unfiltered_cleaned_split.json
echo $DATA_JSON_PATH
if [ ! -f $DATA_JSON_PATH ]; then
    wget https://vemlp-demo-models.tos-cn-beijing.volces.com/vemlp-third-party-warehouse/ShareGPT_V3_unfiltered_cleaned_split.json
    mv ShareGPT_V3_unfiltered_cleaned_split.json $SCRIPT_DIR
fi

# export OPENAI_API_KEY="xxx"
i=0
RATE=${request_rates[$i]}
CONCURRENCY=${max_concurrency[$i]}
PROMPTS=${num_prompts[$i]}
echo "Running benchmark with request rate $RATE, concurrency $CONCURRENCY, prompts $PROMPTS"
python3 $SCRIPT_DIR/loop_test.py --backend vllm \
        --dataset-name random \
        --model DeepSeek-R1 \
        --tokenizer $SCRIPT_DIR/tokenizer/ \
        --dataset-path $DATA_JSON_PATH \
        --random-input-len $INPUT_LEN \
        --random-output-len $OUTPUT_LEN \
        --random-range-ratio 1 \
        --request-rate $RATE \
        --max-concurrency $CONCURRENCY \
        --num-prompts $PROMPTS \
        --base-url  http://主节点ip:端口号

上述脚本中,需要注意的是:

  • INPUT_LEN=1000与OUTPUT_LEN=100,分别表示文本的出入长度与输出长度。
  • max_concurrency为最大并发,这里该参数并不生效,并发量是在loop_test.py脚本里控制的。
  • export OPENAI_API_KEY是当我们基于MaaS模式测试时,需要填写的。
  • model DeepSeek-R1为我们启动服务时指定的served-model-name
  • base-url为主节点的访问ip地址,以及启动服务时的端口号。

关于星火脚本,更详细的信息可参考:

2.10 发起性能测试

完成脚本内容的修改后,我们直接在目录中执行:

bash bench_loop.sh

脚本一共会从1并发执行到64并发,总时长大约一个半小时。

需要注意的是,这里我们要在容器内、使用bash来执行。

2.11 数据转换与分析

在测试完成后,会在目录中生成一个jsonl文件,此时我们可以使用Python脚本、将其转换为csv表格,脚本内容:

import json
import csv

filename = './测试数据/0314/test_2/vllm_0314_100_1000_100.jsonl'

with open(filename, 'r', encoding='utf-8') as file:
    lines = file.readlines()

lines = [line.strip() for line in lines]

json_objects = [json.loads(line) for line in lines]

headers = set()
for obj in json_objects:
    headers.update(obj.keys())

with open('100-1.csv', 'w', newline='', encoding='utf-8') as csvfile:
    csvwriter = csv.DictWriter(csvfile, fieldnames=list(headers))
    csvwriter.writeheader()
    for obj in json_objects:
        csvwriter.writerow(obj)

需要注意的是,由于测试数据是按天来命名,因此如果测试过程跨天(凌晨12点),会自动分割、生成两份测试数据。

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐