Skip to content

昇腾评测环境上手教程

面向 MiniCPM × 昇腾推理优化挑战赛的参赛者,讲清楚怎么连上分配给你的环境、怎么把官方评测跑起来、 怎么判断这一轮跑得对不对。不涉及任何具体的优化手段。

文中所有主机地址、端口、路径都是占位符,请替换成你自己拿到的坐标:

<JUMP_USER>@<JUMP_HOST>:<JUMP_PORT> 跳板机
<TARGET_USER>@<TARGET_HOST>:<TARGET_PORT> 目标机
<PROJECT_ROOT> 你的项目根目录

不要急着装东西。先花五分钟确认你连的是对的机器、环境是完整的。

Terminal window
ssh -J <JUMP_USER>@<JUMP_HOST>:<JUMP_PORT> \
-p <TARGET_PORT> <TARGET_USER>@<TARGET_HOST> \
'hostname; date -u +%FT%TZ'

-J 是 OpenSSH 的跳板语法(等价于手写 ProxyCommand),比先登跳板再登目标稳得多, 也便于脚本化。若你的 OpenSSH 版本较老不支持 -J,用:

Terminal window
ssh -o ProxyCommand="ssh -W %h:%p -p <JUMP_PORT> <JUMP_USER>@<JUMP_HOST>" \
-p <TARGET_PORT> <TARGET_USER>@<TARGET_HOST> 'hostname'

一次跑完,把结果存下来当基准:

Terminal window
echo "=== hostname ==="; hostname
echo "=== NPU ==="; npu-smi info
echo "=== CANN ==="; ls /usr/local/Ascend/ascend-toolkit/ 2>/dev/null
cat /usr/local/Ascend/ascend-toolkit/latest/version.cfg 2>/dev/null
echo "=== CPU/内存 ==="; nproc; free -g | head -2
echo "=== NUMA ==="; lscpu | grep -i numa
echo "=== 磁盘 ==="; df -h /workspace /tmp 2>/dev/null
echo "=== Python ==="; which -a python3; python3 -V

重点看三件事:

  • 卡数。npu-smi info 显示几张卡,直接决定官方配置里设备相关项能不能照抄默认值。 很多官方默认配置是按 8 卡机写的;如果你只有 2 张,照抄会让评测去访问不存在的卡。
  • 卡是否干净。有没有别的进程占着显存。开跑前应当是空的。
  • 磁盘余量。模型权重、评测产物(尤其是音频和视频解码缓存)很吃空间,跑满会以很难看懂的 报错形式出现。

后面所有自动化都依赖免密。把公钥装好:

Terminal window
ssh-copy-id -o ProxyCommand="ssh -W %h:%p -p <JUMP_PORT> <JUMP_USER>@<JUMP_HOST>" \
-p <TARGET_PORT> <TARGET_USER>@<TARGET_HOST>

然后在本地 ~/.ssh/config 里固化,之后 ssh myenv 一条命令就够,VSCode Remote-SSH 也能直接用:

Host myenv
HostName <TARGET_HOST>
Port <TARGET_PORT>
User <TARGET_USER>
ProxyJump <JUMP_USER>@<JUMP_HOST>:<JUMP_PORT>
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
ServerAliveInterval 20
ServerAliveCountMax 3
ConnectTimeout 20

ServerAliveInterval / ServerAliveCountMax 不是可选项。跳板链路上闲置连接会被中间设备静默掐断, 没有 keepalive 的话表现是「敲一个回车就卡死」。

两台机器的镜像和代码往往不同,而连错机器不会报错,只会在错误的环境上跑出看着正常的错结果。 建议在每台机器的项目根放一个标记文件:

Terminal window
echo A > <PROJECT_ROOT>/.WHICH

然后所有自动化脚本先校验再执行:

Terminal window
ssh myenv "head -1 <PROJECT_ROOT>/.WHICH | grep -qx 'A' || { echo '连错机器' >&2; exit 90; }; <你的命令>"

用一个专门的退出码(例如 90)表示身份校验失败,脚本里遇到它必须立即停手,不重试、不用 || true 吞掉。


2.1 推荐:单条非交互 SSH + 本地脚本

Section titled “2.1 推荐:单条非交互 SSH + 本地脚本”

自动化一律用一条命令连过去、执行、返回:

Terminal window
ssh myenv 'cd <PROJECT_ROOT> && ./some_script.sh'

不要写成先 ssh 到跳板、再 ssh 到目标的两段交互式登录。那种方式在脚本里几乎无法判断 自己当前在哪台机器上,而且两层 TTY 会互相干扰。

非交互任务要明确关掉标准输入,否则某个程序一旦试图读终端就会永久挂住:

Terminal window
ssh myenv 'command' < /dev/null

同理,非交互场景不要加 -t 强制分配 TTY。

配好 §1.3 的 ~/.ssh/config 之后直接选 myenv 即可。适合读代码、改代码。 但不要用它跑长任务 —— 窗口一关或网络一抖,任务就没了。长任务见 §6。

Terminal window
# 上传
scp -O -o ProxyJump=<JUMP_USER>@<JUMP_HOST>:<JUMP_PORT> \
-P <TARGET_PORT> local.sh <TARGET_USER>@<TARGET_HOST>:<PROJECT_ROOT>/local.sh
# 下载
scp -O -o ProxyJump=<JUMP_USER>@<JUMP_HOST>:<JUMP_PORT> \
-P <TARGET_PORT> <TARGET_USER>@<TARGET_HOST>:<PROJECT_ROOT>/result.json ./

配好 ~/.ssh/config 后可简化为 scp -O local.sh myenv:<PROJECT_ROOT>/。

-O 让 scp 走传统协议。部分环境的 SFTP 子系统不可用,不加 -O 会报一个跟权限完全无关的错。


3. 一个很容易踩、又很难自己想明白的坑:SSH 输出缓冲

Section titled “3. 一个很容易踩、又很难自己想明白的坑:SSH 输出缓冲”

如果你把远端命令的输出直接管道给本地处理,或者用了本地 timeout:

Terminal window
timeout 60 ssh myenv 'some_slow_command' | tail -20 # 危险

远端命令没结束之前,本地可能一个字都拿不到。一旦本地 timeout 触发把 ssh 杀了, 远端已经打印的全部内容都会丢失,你会看到一个空输出,误以为命令没执行。

稳妥写法是先把输出落到远端文件,再单独取回:

Terminal window
ssh myenv 'some_slow_command > /tmp/probe.log 2>&1; echo $? > /tmp/probe.rc'
ssh myenv 'cat /tmp/probe.rc; tail -50 /tmp/probe.log'

短命令也建议这么做 —— 递归 grep、find 这类看着快、实际可能跑几分钟的命令最容易中招。


按官方通知获取指定的代码分支。开始改之前,先记下三样东西:

Terminal window
cd <PROJECT_ROOT>
git rev-parse HEAD > /tmp/base_commit.txt
git status --porcelain > /tmp/base_status.txt
git diff > /tmp/base_diff.patch

工作树很可能本来就带有未提交的改动(官方预置的适配补丁等)。 永远不要用 git reset --hard 或 git checkout -- . 来「清理」环境,那会把这些一起抹掉, 而且往往无法恢复。要还原只还原你自己动过的文件。

官方会列出一批不可修改的文件(评测目录、评测 CLI、对应的 CMakeLists 等)。 这些文件改了就等于成绩无效,即使你只是「临时改一下做个实验」也很容易忘记还原。

建议开工时先把它们的校验和记下来,收工时核对:

Terminal window
sha256sum <官方列出的每个不可修改文件> > /tmp/readonly.sha256
# 收工
sha256sum -c /tmp/readonly.sha256

评测配置文件如果在不可修改目录内,就按原样使用;需要改环境相关项时, 复制一份到目录外,用官方提供的环境变量指向你的副本(见 §5.2)。


以下命令都在项目根目录执行。具体参数名以你拿到的 run_all.sh --help 和官方说明为准, 这里给的是通用用法。

Terminal window
./evaluation/run_all.sh --tasks <任务名>

常用参数:

参数 作用
--tasks a,b 只跑指定任务,逗号分隔
--smoke N 只跑 N 个样本,快速验证链路是否通
--no-build 跳过构建,直接用现有二进制

第一次跑一定先用 --smoke 小样本把链路走通,确认能出数再上全量。全量动辄几小时, 在一个链路没通的配置上跑全量是纯浪费。

不要在官方配置上反复 sed -i 改来改去,很容易改乱且不可追溯。每个实验复制一份:

Terminal window
cp evaluation/config.env <PROJECT_ROOT>/my_config.env
# 只改环境相关项,比如设备编号、路径
EVAL_CONFIG=<PROJECT_ROOT>/my_config.env ./evaluation/run_all.sh --tasks <任务名>

写配置和校验配置要用同一种解析方式。 常见错误是用 shell 写、用自制正则读, 引号和空格的差异会让你以为配置生效了其实没有。正确做法是直接 source 出来看:

Terminal window
( set -a; source my_config.env; set +a; echo "DEVICE=$YOUR_DEVICE_VAR" )

只有当你确信当前二进制就是当前源码 + 当前编译选项构建出来的才能用。 下列任一情况都不能用:

  • 刚改过源码
  • 刚改过编译选项、CMakeLists、依赖库
  • 不清楚现有二进制是谁、什么时候构建的

尤其注意:build/CMakeCache.txt 会保留上一次的配置项。如果你之前手动加过某个 cmake 选项, 之后即使用官方那条不带该选项的 cmake -B build -S .,缓存里的值仍然生效。 结果是源码看着是官方的、命令看着是官方的,二进制里却夹带着非官方的编译选项。

高价值的结论请用全新 build 目录复现,或者至少核对一遍:

Terminal window
grep -E '<你关心的选项名>|CMAKE_BUILD_TYPE' build/CMakeCache.txt

评测产物一般落在带时间戳的目录下:

evaluation/output/YYYYMMDD_HHMMSS/
├── build.log
├── <任务>.log
├── metrics_<任务>.json
└── ...

metrics_<任务>.json 里通常有一个字段指向真正的详细报告路径。用它来定位报告, 不要用 ls -t | head -1 猜 —— 并发任务或上一轮的残留目录会让你选错, 而选错之后所有分析都建立在别人的数据上。

Terminal window
python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['metrics']['report'])" \
evaluation/output/<时间戳>/metrics_<任务>.json

如果报告尚未生成,可以在开跑前打一个时间标记,之后只找比它新的文件:

Terminal window
touch /tmp/run_start.marker
# ... 跑评测 ...
find evaluation -name '*report*.json' -newer /tmp/run_start.marker

6. 长任务:怎么跑才不会被断线带走

Section titled “6. 长任务:怎么跑才不会被断线带走”

全量评测可能几小时。绝对不要挂在前台 SSH 上。

6.1 推荐:setsid + nohup + 全重定向

Section titled “6.1 推荐:setsid + nohup + 全重定向”
Terminal window
ssh myenv 'cd <PROJECT_ROOT> && \
setsid nohup ./evaluation/run_all.sh --tasks <任务名> \
> /tmp/job.log 2>&1 < /dev/null & \
sleep 2; echo LAUNCHED'

三个要素缺一不可:

  • setsid 让进程脱离当前会话,断线不会收到 SIGHUP
  • nohup 忽略挂断信号
  • stdin/stdout/stderr 全部重定向。只写 nohup cmd & 是不够的:子进程仍持有 SSH 的管道, 会导致 SSH 迟迟不返回,或者断线后输出丢失
Terminal window
ssh myenv -t 'tmux new -As work'
# 在 tmux 里正常跑,Ctrl-B D 脱离,断线后重连接着看
ssh myenv -t 'tmux attach -t work'

tmux 适合交互式调试;setsid 适合脚本化批量任务。

Terminal window
ssh myenv 'tail -50 /tmp/job.log'
ssh myenv 'pgrep -af run_all.sh || echo NOT_RUNNING'

不要只靠 ps | grep 判断,grep 会匹配到自己。用 pgrep -af 并确认匹配到的确实是你的进程。


评测会拉起一个推理服务再连它。如果报连接/读取超时,按顺序查:

Terminal window
npu-smi info # 卡上有没有别人的残留进程占着显存
ss -ltnp | grep '<端口>' || true # 端口是否被占
pgrep -af '<服务进程名>' || true # 有没有上一轮没退干净的服务

最常见的原因就是上一轮的服务没退干净,占着端口和显存。清掉再跑通常就好了。

清理时不要用宽泛的 pkill -f,它可能匹配到你当前的 SSH 命令行、控制脚本本身,甚至别人的任务。 先列出来确认:

Terminal window
pgrep -af '<服务进程名>'
kill <确认过的 PID>

冷启动加载大模型权重本身也要时间,第一次跑慢是正常的;但如果每次都超时,那是残留没清干净。

7.2 进程退出码是 134,但结果其实是好的

Section titled “7.2 进程退出码是 134,但结果其实是好的”

CANN 运行时在产物已经写完之后的清理阶段 abort 是常见现象,进程最终返回 134。 所以下面这种判断是错的:

Terminal window
if ./evaluation/run_all.sh --tasks <任务名>; then echo OK; else echo FAILED; fi # 不可靠

正确做法是退出码和产物都记下来,以产物为准:

Terminal window
set +e
./evaluation/run_all.sh --tasks <任务名> > run.log 2>&1
rc=$?
set -e
echo "$rc" > run.rc
# 以产物判定
if [ -f "<预期的产物路径>" ] && python3 -c "import json;json.load(open('<产物路径>'))"; then
touch SUCCESS
else
touch FAILED
fi

退出码仍然要保留 —— 它能反映真正的运行期异常 —— 只是不能当作唯一依据。

7.3 报告生成了,不代表这一轮有效

Section titled “7.3 报告生成了,不代表这一轮有效”

一次评测可能顺利结束、JSON 也能解析,但里面的样本数是 0。开始分析之前先确认这一轮跑完整了:

  • 样本数 / 事件数是否等于预期
  • 各类计数字段之间是否自洽
  • 音频、文本等输出产物的数量是否对得上
  • 日志里有没有大量报错被吞掉

跑通 ≠ 跑对。 把「这一轮是否有效」的判断写成脚本自动执行,比每次靠眼睛看可靠得多。

如果你要做前后对比,还要额外确认两次跑的工作量是一样的(样本数、生成长度等计数字段相等)。 计数不同就说明两次做的事不同,速度对比没有意义。

7.4 设备编号:进程外和进程内不是一回事

Section titled “7.4 设备编号:进程外和进程内不是一回事”

评测框架通常会给服务进程设置可见设备,例如:

env["ASCEND_RT_VISIBLE_DEVICES"] = str(gpu_id)

这之后,进程内部只看得到一张卡,它的逻辑编号是 0。也就是说:

  • 你在外面指定的是物理卡 gpu_id
  • 进程里代码写的 gpu:0 指向的是那张物理卡
  • 进程里写 gpu:1 会指向一个不存在的设备

排查设备问题时不要看配置解析的打印,要看 backend 实例的日志(它会打印实际初始化了哪个后端), 再和 npu-smi info 里的进程分布对照。

数据集、评分模型往往需要单独下载。链路跑不通时先确认资产是不是真的在:

Terminal window
ls -lL <数据集目录> # -L 跟随软链,避免把软链本身的大小当成文件大小
du -sh <数据集目录>

注意用 ls -lL:软链接本身只有几十字节,ls -l 看起来像是个空文件,容易误判成下载失败。


建议控制脚本开头加 set -Eeuo pipefail,但必须知道哪些正常行为会返回非零, 在 set -e 下直接把整个脚本杀掉:

写法 问题
n=$(grep -c PAT f) 零匹配时打印 0 但退出码是 1,脚本当场退出
grep -q PAT f 裸写 没匹配到就退出
diff a b / cmp a b 文件不同返回 1
kill -0 $pid 进程不存在返回非零
find ... | grep ... | head -1 pipefail 下 head 提前关管道会让上游收到 SIGPIPE
read var 读到 EOF 返回非零
函数最后一条命令返回非零 整个函数返回非零

安全写法:

Terminal window
n=$(grep -c PAT f || true)
if grep -q PAT f; then echo 有; else echo 无; fi
set +e
some_command
rc=$?
set -e

另外,CANN 的环境脚本可能引用未定义变量,在 set -u 下会直接失败:

Terminal window
set +u
source /usr/local/Ascend/ascend-toolkit/set_env.sh
set -u

只要你动了源码或二进制,就必须保证能原样还原。推荐骨架:

#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=<PROJECT_ROOT>
FILES=("$ROOT/path/to/file1" "$ROOT/path/to/file2")
cd "$ROOT"
sha256sum "${FILES[@]}" > /tmp/before.sha256
git status --porcelain > /tmp/before.status
restore() {
local rc=$?
trap - EXIT INT TERM
set +e
git checkout -- "${FILES[@]}" 2>/dev/null
# 如果改动会影响二进制,这里要重新构建回原样
git status --porcelain > /tmp/after.status
if cmp -s /tmp/before.status /tmp/after.status; then
echo RESTORE_OK
else
echo RESTORE_FAILED >&2
diff /tmp/before.status /tmp/after.status
fi
exit "$rc"
}
trap restore EXIT INT TERM
# 实验主体

要点:

  • trap 一进来先保存 $?,再取消自身 trap 防止递归
  • handler 内部 set +e,否则第一个还原动作失败就还原不了后面的
  • 还原后用 git status --porcelain 前后对比来确认,比人眼可靠
  • 还原失败要显式报错,它的优先级高于实验结果本身

如果实验会新建文件,单独记录清单,还原时只删记录过的文件,不要用通配符扫目录。


Terminal window
nproc # 先看有多少核
cmake --build build -j 32 # 日常增量构建

核多不等于可以拉满。机器上可能还有别人的任务,-j$(nproc) 在几百核的机器上会把内存和 I/O 打爆。 先看负载再决定。

编译会占满 CPU、内存带宽、页缓存和磁盘 I/O。即使 NPU 是空的,也会显著影响:

  • 服务启动时间
  • Python / CANN 初始化
  • 数据加载
  • 端到端延迟

性能测量期间不要并发编译、跑另一个评测、做 profile,或者跑大规模递归文件扫描。

Terminal window
cat /proc/loadavg
npu-smi info

负载明显偏高时等一等再跑。在忙碌的机器上测出来的数字既不能用来做决策, 也不能用来和之前的结果比较。

同一个配置在不同时间跑出来的结果会有差异,服务重启前后尤其明显(缓存冷热、CPU 调度、 其它任务的干扰都会影响)。一次测量不能说明问题。 任何你打算据以做决定的数字,都应该在不同时间重复测几次,看它稳不稳定。


  • 确认连的是正确的机器
  • npu-smi info 卡上没有残留进程
  • 端口没被占
  • 负载正常,没有并发的编译 / 评测
  • 记录 git rev-parse HEAD、git status --porcelain、不可修改文件的 sha256
  • 确认改动真的进了正在执行的那条路径 —— 编译成功不等于生效, 要有运行期证据(日志字段、符号表、计数器)
  • 核对 build/CMakeCache.txt 没有夹带上一轮的选项
  • 二进制 sha256 有变化(没变说明根本没重新构建)
  • 不只看退出码,看产物
  • 报告能解析,且样本数 / 事件数符合预期
  • 日志里没有被吞掉的错误
  • 若做对比,两次的工作量计数字段相等
  • 还原所有改过的文件,并用 sha256 / git status 核对
  • 不可修改文件的 sha256 与开工时一致
  • 服务进程已退出,卡上没有残留
  • 还原失败时优先报错,不要带着未还原的工作树离开

附:一条能直接抄的最小工作流

Section titled “附:一条能直接抄的最小工作流”
Terminal window
# 0. 配好 ~/.ssh/config 里的 myenv(见 1.3)
# 1. 环境自检
ssh myenv 'hostname; npu-smi info; nproc; df -h /workspace'
# 2. 小样本跑通链路
ssh myenv 'cd <PROJECT_ROOT> && \
setsid nohup ./evaluation/run_all.sh --tasks <任务名> --smoke 2 \
> /tmp/smoke.log 2>&1 < /dev/null & sleep 2; echo LAUNCHED'
# 3. 看进度
ssh myenv 'tail -40 /tmp/smoke.log'
# 4. 找报告
ssh myenv 'ls -td <PROJECT_ROOT>/evaluation/output/*/ | head -1'
# 5. 确认这一轮有效(样本数、事件数是否符合预期)
ssh myenv 'python3 -m json.tool <报告路径> | head -40'
# 6. 链路通了再上全量,同样用 setsid 起

祝顺利。