RTP-LLM

RTP-LLM 是阿里云的大模型推理引擎,C++ 引擎加 Python 前端,用 bazel 构建。本文在单张昇腾 NPU 上从源码构建它的 Ascend 分支,安装产出的 wheel,启动 HTTP 服务并完成一次生成请求。

Ascend 适配目前在上游的 feature/ascend 分支上,main 里还没有。本文的命令都在容器内以 root 执行,工作目录统一用 /root/rtp-llm-test。

前置条件

硬件

Atlas 800T / 900 A2 训练系列,芯片为 Ascend 910B。本文示例为单卡。

软件

类别

要求

CANN

toolkit 与驱动固件已安装,并可 source set_env.sh,9.1.0 或以上

gcc / g++

11

Python

3.10

PyTorch

torch 2.9.0 + torch_npu 2.9.0.post2

Bazel

6.4.0

模型

Qwen/Qwen3-0.6B

本文验证环境

项目

内容

机器

Atlas 900 A2,Ascend 910B,单卡

镜像

swr.cn-south-1.myhuaweicloud.com/ascendhub/cann:9.1.0-910b-ubuntu22.04-py3.12

阅读本文前,请先按 快速安装昇腾环境 准备好 CANN 与驱动。

1. 加载 CANN 环境

加载 CANN,并把 /usr/local/sbin 和 /usr/local/bin 加入 PATH。

export PATH=/usr/local/sbin:/usr/local/bin:$PATH
source /usr/local/Ascend/ascend-toolkit/set_env.sh

2. 检查环境是否就绪

2.1 确认 NPU 在线

npu-smi info

输出为设备列表,型号与数量因机器而异,至少一张卡且 Health 为 OK 即可。若提示找不到 npu-smi,回到 快速安装昇腾环境 检查驱动与设备挂载。

2.2 安装 bazelisk

构建由 bazelisk 拉起,Bazel 固定用 6.4.0。发行版通常不带 bazelisk,装到 /usr/local/bin。

case $(uname -m) in
  x86_64)  bazelisk_arch=amd64 ;;
  aarch64) bazelisk_arch=arm64 ;;
  *)       echo "unsupported arch: $(uname -m)"; exit 1 ;;
esac
if [ ! -x /usr/local/bin/bazelisk ]; then
  curl -fL --connect-timeout 20 --retry 8 --retry-all-errors --retry-delay 3 \
    -o /usr/local/bin/bazelisk \
    "https://github.com/bazelbuild/bazelisk/releases/download/v1.19.0/bazelisk-linux-${bazelisk_arch}"
  chmod +x /usr/local/bin/bazelisk
fi
export USE_BAZEL_VERSION=6.4.0
bazelisk version | grep -m1 'Build label'

输出结果如下:

Build label: 6.4.0

2.3 准备 Python 3.10

上游把解释器路径写死成 /opt/conda310/bin/python3(deps/pip.bzl、.bazelrc、WORKSPACE 等多处引用),所以 3.10 必须装在这个路径下。用发行版自带的 3.10 建 venv 即可。

if [ ! -x /opt/conda310/bin/python3 ]; then
  apt-get update -qq >/dev/null
  apt-get install -y -qq python3.10 python3.10-venv python3.10-dev >/dev/null
  python3.10 -m venv /opt/conda310
  /opt/conda310/bin/python3 -m pip install -q --upgrade pip
  # //3rdparty/py 的 genrule 直接从这个前缀拷 libpython3.10.so,venv 自身不复制
  mkdir -p /opt/conda310/lib
  ln -sf /usr/lib/aarch64-linux-gnu/libpython3.10.so /opt/conda310/lib/libpython3.10.so
fi
export PATH=/opt/conda310/bin:$PATH
python --version

输出结果如下,其中 xxx 表示实际的补丁版本号:

Python 3.10.xxx

3. 获取代码

克隆上游的 feature/ascend 分支。

cd /root/rtp-llm-test
rm -rf rtp-llm
git clone --depth 1 --branch feature/ascend https://github.com/alibaba/rtp-llm.git
cd rtp-llm
echo "branch $(git rev-parse --abbrev-ref HEAD)"
echo "HEAD $(git log -1 --format=%h)"

输出结果如下,其中 xxx 表示实际 commit 的短哈希:

branch feature/ascend
HEAD xxx

4. 适配 aarch64 构建输入

上游这套构建输入是按 x86_64 宿主写的:运行时依赖清单、lock 里的直链、bazel 自己的两个 http_archive 全都指向 x86 的安装包。不改的话 pip 要么直接拒装,要么一路跑到链接阶段才报 cannot find -ltorch_npu。x86 宿主跳过本节。

set -euo pipefail
cd /root/rtp-llm-test/rtp-llm

# 1) 运行时依赖清单:torch 换 aarch64 直链,torch_npu 钉到与本镜像配套的 post2
cat > deps/requirements_ascend_x86.txt <<'REQ'
-r requirements_base.txt
https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl
torch_npu==2.9.0.post2
torchvision==0.24.0
numpy==1.26.4
REQ

# 2) 取 aarch64 安装包,sha256 由安装包本身算出
mkdir -p /opt/rtp_wheels
TORCH_WHL=/opt/rtp_wheels/torch-2.9.0+cpu-cp310-cp310-manylinux_2_28_aarch64.whl
NPU_WHL=/opt/rtp_wheels/torch_npu-2.9.0.post2-cp310-cp310-manylinux_2_28_aarch64.whl
curl -fsSL -o "$TORCH_WHL" \
  "https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl"
curl -fsSL -o "$NPU_WHL" \
  "https://files.pythonhosted.org/packages/97/d8/685fc488350ba63e158620d3ea04367c908456f03a01f9a9cc158fec380b/torch_npu-2.9.0.post2-cp310-cp310-manylinux_2_28_aarch64.whl"
SHA_T=$(sha256sum "$TORCH_WHL" | awk '{print $1}')
SHA_N=$(sha256sum "$NPU_WHL" | awk '{print $1}')

# 3) lock 里的两条直链:URL 换平台后缀,hash 用上面算出的值;
#    旧 URL 与旧 hash 都从文件里读出来当锚点,上游改版本了也不会静默失效
LOCK=deps/requirements_lock_ascend_x86.txt
SHA_OLD_T=$(grep -A1 '^torch @ ' "$LOCK" | sed -n 's/.*--hash=sha256:\([0-9a-f]\{64\}\).*/\1/p')
SHA_OLD_N=$(grep -A1 '^torch-npu @ ' "$LOCK" | sed -n 's/.*--hash=sha256:\([0-9a-f]\{64\}\).*/\1/p')
test -n "$SHA_OLD_T" -a -n "$SHA_OLD_N"
sed -i -E 's#https://[^ ]*torch-2\.9\.0%2Bcpu[^ ]*x86_64\.whl#https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl#' "$LOCK"
sed -i -E 's#https://[^ ]*torch_npu-2\.9\.0[^ ]*x86_64\.whl#https://files.pythonhosted.org/packages/97/d8/685fc488350ba63e158620d3ea04367c908456f03a01f9a9cc158fec380b/torch_npu-2.9.0.post2-cp310-cp310-manylinux_2_28_aarch64.whl#' "$LOCK"
sed -i "s|$SHA_OLD_T|$SHA_T|" "$LOCK"
sed -i "s|$SHA_OLD_N|$SHA_N|" "$LOCK"

# 4) bazel 自己的两个 http_archive(deps/http.bzl)同样指向 x86 安装包
sed -i -E 's#https://[^"]*torch-2\.9\.0%2Bcpu[^"]*x86_64\.whl#https://download.pytorch.org/whl/cpu/torch-2.9.0%2Bcpu-cp310-cp310-manylinux_2_28_aarch64.whl#g' deps/http.bzl
sed -i -E 's#https://[^"]*torch_npu-2\.9\.0[^"]*x86_64\.whl#https://files.pythonhosted.org/packages/97/d8/685fc488350ba63e158620d3ea04367c908456f03a01f9a9cc158fec380b/torch_npu-2.9.0.post2-cp310-cp310-manylinux_2_28_aarch64.whl#g' deps/http.bzl
SHA_BZL_T=$(grep -A1 'name = "torch_cpu_ascend"' deps/http.bzl | sed -n 's/.*sha256 = "\([0-9a-f]\{64\}\)".*/\1/p')
SHA_BZL_N=$(grep -A1 'name = "torch_npu_ascend"' deps/http.bzl | sed -n 's/.*sha256 = "\([0-9a-f]\{64\}\)".*/\1/p')
test -n "$SHA_BZL_T" -a -n "$SHA_BZL_N"
sed -i "s|$SHA_BZL_T|$SHA_T|g" deps/http.bzl
sed -i "s|$SHA_BZL_N|$SHA_N|g" deps/http.bzl

# 5) wheel 元数据里的 torch_npu 版本(漏了这处,装 wheel 时会把 post2 降级成裸 2.9.0)
sed -i 's/"torch_npu==2\.9\.0"/"torch_npu==2.9.0.post2"/' arch_config/arch_select.bzl

# 6) 上游依赖冲突:transformers==5.2.0 与 xgrammar==0.2.5(要求 transformers<5)无法同时满足
sed -i 's/^xgrammar==0\.2\.5$/xgrammar==0.2.8/' deps/requirements_base.txt

# 7) 校验:lock 里 2 条 aarch64 直链、0 条 x86 直链、0 条 gitcode、旧 hash 清零;
#    http.bzl 里两个 ascend archive 各 1 条 aarch64 直链;两个安装包能正常解开
test "$(grep -c 'manylinux_2_28_aarch64.whl' "$LOCK")" = 2
test "$(grep -c 'manylinux_2_28_x86_64.whl' "$LOCK")" = 0
test "$(grep -c 'gitcode.com' "$LOCK")" = 0
test "$(grep -c "$SHA_OLD_T" "$LOCK")" = 0
test "$(grep -c "$SHA_OLD_N" "$LOCK")" = 0
test "$(grep -A6 'name = "torch_cpu_ascend"' deps/http.bzl | grep -c 'manylinux_2_28_aarch64.whl')" = 1
test "$(grep -A6 'name = "torch_npu_ascend"' deps/http.bzl | grep -c 'manylinux_2_28_aarch64.whl')" = 1
python - "$TORCH_WHL" "$NPU_WHL" <<'PY'
import sys
import zipfile

for path in sys.argv[1:]:
    assert zipfile.ZipFile(path).testzip() is None, path
PY
echo "aarch64 build inputs patched"

输出结果如下:

aarch64 build inputs patched

5. 安装运行时依赖

按 lock 装依赖,torch 与 torch_npu 在这一步装好。

cd /root/rtp-llm-test/rtp-llm
python -m pip install -r deps/requirements_ascend_x86.txt > /tmp/pip-deps.log 2>&1 \
  || { tail -n 60 /tmp/pip-deps.log; exit 1; }
python - <<'PY'
from importlib.metadata import version

for name in ('torch', 'torch-npu', 'torchvision', 'numpy'):
    print(name.replace('-', '_'), version(name))
PY

输出结果如下:

torch 2.9.0+cpu
torch_npu 2.9.0.post2
torchvision 0.24.0
numpy 1.26.4

6. 构建 wheel

6.1 放宽 Ubuntu 默认开启的警告

Ubuntu 的 gcc 默认打开 -Wformat-security,叠加仓里的 -Werror 会把第三方头文件里的一处 snprintf 用法升成硬错误。降级为警告即可,提示保留、构建不中断。

cd /root/rtp-llm-test/rtp-llm
if ! grep -q 'Wno-error=format-security' .bazelrc; then
  sed -i '/^build:ascend --copt="-D_GLIBCXX_USE_CXX11_ABI=1"$/a build:ascend --copt="-Wno-error=format-security"' .bazelrc
fi
sed -n '/Wno-error=format-security/p' .bazelrc

输出结果如下:

build:ascend --copt="-Wno-error=format-security"

6.2 编译

用 bazelisk 构建 wheel,目标 //rtp_llm:rtp_llm_whl,--config=ascend 选中 Ascend 后端。

cd /root/rtp-llm-test/rtp-llm
export USE_BAZEL_VERSION=6.4.0
bazelisk build //rtp_llm:rtp_llm_whl --config=ascend --jobs=32 > /tmp/build.log 2>&1 \
  && echo "build ok" || { tail -n 80 /tmp/build.log; exit 1; }

输出结果如下:

build ok

确认产物:

cd /root/rtp-llm-test/rtp-llm
ls bazel-bin/rtp_llm/*.whl > /dev/null && echo "whl ok"

输出结果如下:

whl ok

7. 安装 wheel 并加载算子

7.1 安装 wheel

把产出的 wheel 装进当前解释器。

cd /root/rtp-llm-test/rtp-llm
python -m pip install --no-deps --force-reinstall bazel-bin/rtp_llm/*.whl > /tmp/whl-install.log 2>&1 \
  && echo "whl installed" || { tail -n 40 /tmp/whl-install.log; exit 1; }

输出结果如下:

whl installed

7.2 加载 C++ 算子

自检 wheel 里的 C++ 引擎能否加载:

cd /tmp
python - <<'PY' > /tmp/ops-load.log 2>&1
from rtp_llm.ops import ensure_engine_ops_loaded

ensure_engine_ops_loaded()
print('OPS_LOAD_OK')
PY
grep -q 'OPS_LOAD_OK' /tmp/ops-load.log && echo "engine ops ok" \
  || { tail -n 40 /tmp/ops-load.log; exit 1; }

输出结果如下:

engine ops ok

8. 下载模型

用 0.6B 的稠密模型,Ascend 分支当前声明支持的模型族是 Qwen3。

cd /root/rtp-llm-test
python -m pip install -q huggingface_hub
python - <<'PY' > /tmp/model-dl.log 2>&1 \
  || { tail -n 40 /tmp/model-dl.log; exit 1; }
from huggingface_hub import snapshot_download
snapshot_download(repo_id='Qwen/Qwen3-0.6B', local_dir='/root/rtp-llm-test/models/Qwen3-0.6B')
print('model ready')
PY
# 这一步不该动 torch / torch_npu / torchvision 的版本
python - <<'PY'
from importlib.metadata import version

assert version('torch-npu') == '2.9.0.post2', version('torch-npu')
assert version('torchvision') == '0.24.0', version('torchvision')
PY
ls models/Qwen3-0.6B/config.json models/Qwen3-0.6B/*.safetensors > /dev/null
echo "model ok"

输出结果如下:

model ok

9. 启动服务并发一次请求

在 0 号卡上启动 HTTP 服务,端口 30000,等 /health 就绪后发一条生成请求。

cd /tmp
export ASCEND_RT_VISIBLE_DEVICES=0
python -m rtp_llm.start_server \
  --checkpoint_path=/root/rtp-llm-test/models/Qwen3-0.6B \
  --model_type=qwen_3 \
  --start_port=30000 > /tmp/server.log 2>&1 &
SERVER_PID=$!
trap 'kill $SERVER_PID 2>/dev/null' EXIT

for _ in $(seq 1 180); do
  curl -sf http://127.0.0.1:30000/health > /dev/null 2>&1 && break
  kill -0 $SERVER_PID 2>/dev/null || { echo "server died"; tail -n 60 /tmp/server.log; exit 1; }
  sleep 2
done

echo "health: $(curl -s http://127.0.0.1:30000/health)"
curl -s -XPOST http://127.0.0.1:30000 -H 'Content-Type: application/json' \
  -d '{"prompt":"hello, what is your name","generate_config":{"max_new_tokens":16}}' > /tmp/gen.json
python - <<'PY'
import json

with open('/tmp/gen.json', encoding='utf-8') as fh:
    data = json.load(fh)
aux = data['aux_info']
print('has_response', bool(data.get('response')))
print('output_len_positive', aux['output_len'] > 0)
print('finished', data.get('finished'))
PY

输出结果如下:

health: "ok"
has_response True
output_len_positive True
finished True

10. 更多用法