【昇腾踩坑】双卡 TP=2 一跑就整机挂死?从 RCU Stall 一路查到 HCCL 链路选错
目录
【昇腾踩坑】双卡 TP=2 一跑就整机挂死?从 RCU Stall 一路查到 HCCL 链路选错
平台:Phytium S5000C + 2×Atlas 300I A2 | 框架:vllm-ascend | 模型:Qwen3.6-35B-A3B(BF16,必须 TP=2)
关键词:昇腾 / Atlas 300I A2 / vllm-ascend / TP=2 / 张量并行 / HCCL / 整机卡死 / RCU Stall
一、背景
一台**飞腾(Phytium S5000C)平台 + 2 张 Atlas 300I A2(单卡 64 GB)**的服务器,要部署一个 70 GB 的 BF16 大模型 —— 单卡放不下,必须上双卡张量并行(TP=2)。
结果:
- 单卡独占:长时间稳定(跑过 34.6 小时、19.7 小时)
- 两卡各跑独立实例(零跨卡通信):稳定
- 双卡 TP=2:❌ 必挂,最快单次请求就整机失联
挂机后 SSH 全断,只能到机房重启。串口刷的是:
Sending IPI failed to CPU 74
rcu: rcu_sched kthread starved for 23534 jiffies!
rcu: Unless rcu_sched kthread gets sufficient CPU time, OOM is now expected behavior.
Kernel panic - not syncing: RCU Stall
SMP: failed to stop secondary CPUs 0-18,20-127
看着像硬件坏了 —— 但最后发现,其实只是 3 个环境变量的事。
二、环境信息
| 项 | 值 |
|---|---|
| 加速卡 | Atlas 300I A2 ×2(单卡 64 GB) |
| 主板 | Phytium S5000C |
| OS | openEuler 22.03 SP1(kernel 5.10,aarch64) |
| 驱动 / CANN | 驱动 25.5.1;宿主 CANN 8.5.0;容器 CANN 9.1.0 |
| 推理框架 | vllm-ascend v0.23.0.post1-openeuler |
| 模型 | 35B / A3B MoE,BF16,约 70 GB |
三、排查过程(逐层排除)
我的思路很简单:用最少的实验,一层层把"硬件坏了"这个可能性剥掉。
| # | 假设 | 实验 | 结论 |
|---|---|---|---|
| 1 | 两卡固件版本不一致 | 统一固件 | 排除(但必须修) |
| 2 | 跨 socket 通信路径差 | 把两卡移到同一 CPU/GPU 区 | 拓扑 SYS → PHB(改善,未解决) |
| 3 | PCIe 带宽不足 | lspci -vv 查链路 | x8(能力 x16)——是约束,不是根因 |
| 4 | ACS 阻断了 P2P | 查 ACSCtl | 已全放行 → 排除 |
| 5 | 供电/散热撑不住两卡 | 两卡各跑独立实例并行 → 稳定 | 排除 |
| 6 | 跨卡通信 | 单卡稳 + 双卡独立实例稳,唯一差异只剩跨卡通信 | 锁定方向 |
| 7 | 是硬件还是配置? | 脱离模型/框架的 20 行 all_reduce 探针 | 🎯 定位到 HCCL 配置 |
两个关键手段(强烈推荐)
① 对照实验把变量隔离干净
组 A:单卡单实例 → 稳
组 B:双卡各一个独立实例 → 稳 (零跨卡通信)
组 C:双卡 TP=2 协同 → 挂 (有跨卡通信)
A/B 都稳、C 挂 → 差异只剩"跨卡通信"。而且组 B 稳,还顺带排除了供电/散热/主板这一整类嫌疑。
② 写一个脱离框架的最小探针
不要一上来就调 vLLM 参数。先写一个只做 all_reduce 的 20 行脚本,不加载任何模型:
# hccl_probe.py
import os
import torch
import torch_npu # noqa: F401
import torch.distributed as dist
rank = int(os.environ["RANK"])
world = int(os.environ["WORLD_SIZE"])
dist.init_process_group("hccl", init_method="env://", world_size=world, rank=rank)
torch.npu.set_device(f"npu:{rank}")
x = torch.ones(2 * 1024 * 1024, dtype=torch.float32).npu() # 8 MiB
for i in range(20):
dist.all_reduce(x, op=dist.ReduceOp.SUM)
if rank == 0:
print(f"iter {i} ok x[0]={x[0].item()}", flush=True)
dist.barrier()
print(f"[rank {rank}] ALL 20 ALLREDUCE OK", flush=True)
运行(注意加 --network host,与 vLLM 环境保持一致):
docker run --rm --privileged --network host --ipc=host \
--device=/dev/davinci0 --device=/dev/davinci1 \
--device=/dev/davinci_manager --device=/dev/devmm_svm --device=/dev/hisi_hdc \
-v /usr/local/dcmi:/usr/local/dcmi \
-v /usr/local/Ascend/driver/lib64:/usr/local/Ascend/driver/lib64 \
-v /usr/local/Ascend/driver/version.info:/usr/local/Ascend/driver/version.info \
-v /usr/local/Ascend/driver/tools/hccn_tool:/usr/local/Ascend/driver/tools/hccn_tool \
-v /usr/local/bin/npu-smi:/usr/local/bin/npu-smi \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /home/bench/tools/hccl_probe.py:/probe/hccl_probe.py \
-e ASCEND_RT_VISIBLE_DEVICES=0,1 \
quay.io/ascend/vllm-ascend:v0.23.0.post1-openeuler \
bash -lc 'source /usr/local/Ascend/cann-9.1.0/set_env.sh 2>/dev/null || true;
torchrun --nproc_per_node=2 --master_port=29501 /probe/hccl_probe.py'
这一个实验的价值极高:如果它挂了,就直接证明了"与模型无关、与 vLLM 无关",把问题从业务层压到通信库层。
探针第一次运行 → 卡死整机
(顺便说:探针第一次跑,机器直接挂了 —— 这本身也是重要信息。)
四、根因
恢复后开着串口落盘 + dmesg 落盘再跑一次,这次拿到了明确报错:
HCCL function error: hcclCommInitRootInfoConfig(numRanks, &rootInfo, rank, config, &(comm->hcclComm_)), error code is 7
Communication_Error_Bind_IP_Port(EI0020): Failed to enable listening for the NPU network adapter socket.
Reason: The IP address x.x.x.199 and port 16666 have already been bound.
Solution: Check whether the single-card multi-process scenario is used.
If yes, configure the port number using the environment variable HCCL_NPU_SOCKET_PORT_RANGE.
HCCL 建链(bootstrap)时,默认走 NPU 网卡的 RoCE 路径,而这块主板没有卡间高速互联(无 HCCS),那条路走不通 → 建链阶段死等 → 拖垮 CPU → 整机失联。
用一句话概括:
HCCL 默认去试了"网卡"这条路,走进了死胡同;PCIe 才是这块板上唯一可用的卡间通道。
五、解决方案
在容器环境变量里加三项 —— 不改代码、不改 BIOS、不用换硬件:
environment:
- ASCEND_RT_VISIBLE_DEVICES=0,1
# ===== ★ 本平台必需:缺任一行,TP=2 会在 HCCL 建链阶段挂死整机 =====
- HCCL_INTRA_PCIE_ENABLE=1 # 允许节点内走 PCIe(本平台唯一可用的卡间通道)
- HCCL_INTRA_ROCE_ENABLE=0 # 禁止节点内走 RoCE(该路径不可用)
- HCCL_CONNECT_TIMEOUT=600 # 放宽建链超时(无 HCCS 时建链较慢)
| 变量 | 作用 |
|---|---|
HCCL_INTRA_PCIE_ENABLE=1 | 显式允许节点内用 PCIe 做集合通信 |
HCCL_INTRA_ROCE_ENABLE=0 | 禁止节点内走 RoCE(这条最可能是关键) |
HCCL_CONNECT_TIMEOUT=600 | 把建链超时放宽(无 HCCS 时建链慢,默认超时可能不够) |
六、验证结果
6.1 探针(加变量后)
iter 0 ok x[0]=2.0
iter 1 ok x[0]=4.0
...
iter 19 ok x[0]=1048576.0 ← 2^20,两卡数据确实参与了每次归约,数值正确
[rank 0] ALL 20 ALLREDUCE OK
[rank 1] ALL 20 ALLREDUCE OK
6.2 稳定性阶梯(6 档,累计 1.5 小时)
| 档 | 负载 | TTFT p50 | TPOT p50 | 输出 tok/s | 总 tok/s | failed |
|---|---|---|---|---|---|---|
| b1 | 512/128 @ 并发 1 | 1.1 s | 34.8 ms | 21.9 | 111 | 0 |
| b2 | 512/128 @ 并发 32 | 38.0 s | 41.0 ms | 92.0 | 467 | 0 |
| b3 | 512/128 @ 并发 64 | 48.3 s | 120.2 ms | 99.1 | 504 | 0 |
| b4 | 8192/512 @ 并发 32 | 27.8 s | 193.0 ms | 107.6 | 1831 | 0 |
| b5 | 129024/512 @ 并发 4 | 419 s | 156.0 ms | 4.1 | 1038 | 0 |
| b6 | 512/128 ×2000 @ 并发 64 | 52.2 s | 240.6 ms | 93.1 | 473 | 0 |
结论:
- 6 档全过,失败请求 0,内核故障计数全程 0(
IPI failed/rcu_sched/phytium pcie error/AER一次都没出现) - b6 以 2000 请求 @ 并发 64 连续跑 46 分钟未挂 —— 首次超过历史最快挂机时间(约 45 分钟)
- 短请求稳态延迟 1.5 秒(修复前 6.7~7.3 s/it,快了 4 倍多)
6.3 性能说明
满并发下 TTFT 偏高(p50 ~48 s)、126K 单次约 7 分钟 —— 因为两卡无 HCCS、PCIe 仅 x8(实际链路宽度只有能力的一半),跨卡通信只能经主机内存中转。这是平台物理条件决定的,不是缺陷;短请求体验正常。
七、踩坑清单(血泪总结)
-
不要用
watch -n 1 npu-smi info!
高频轮询会压垮驱动/监控通道,是"整机失联"的常见诱因。要用请 ≥60s 或脚本采样。 -
硬 panic(
not syncing)会把日志一起丢掉 —— 必须做"证据四件套":# ① RCU 卡死自动重启(避免只能去机房断电) sysctl -w kernel.panic_on_rcu_stall=1 kernel.panic=30 # ② journald 持久化(否则重启后查不到上次启动的日志) mkdir -p /var/log/journal sed -i 's/^#\?Storage=.*/Storage=persistent/' /etc/systemd/journald.conf systemctl restart systemd-journald # ③ 串口 Log Session 落盘 ④ dmesg -wT 持续落盘(单写者!) -
dmesg采集必须"单写者" —— 同一分钟起两条会撞同名文件、互相覆盖,最后只剩一条。 -
/tmp重启会清空 —— 脚本一律放持久目录(如/home/bench/)。 -
“敲了命令” ≠ “在跑” —— 采集类进程要验证
pgrep有 pid + 输出文件 mtime 在涨。 -
nohup setsid xxx &后 shell 报[1]+ 已完成是正常的(setsid 立刻 fork 退出)—— 验证永远看pgrep。 -
hccl_test在新版 CANN 里是源码包(只有.cc/.h+ Makefile),不是现成可执行文件 —— 不如直接用torch_npu写个all_reduce探针快。 -
HCCL 崩溃后可能残留 socket 占端口 → 可用
HCCL_NPU_SOCKET_PORT_RANGE=60000-60015换端口范围。 -
双实例并存时,
docker compose项目名必须不同(默认取目录名)—— 否则down会互删对方容器。显式写name:或用-p。 -
排查顺序建议:先做对照实验隔离变量,再写最小复现;不要一上来就调框架参数。
八、总结
- 一个看起来像硬件故障的整机挂死,通过对照实验 + 最小复现,最终定位成可配置绕过的通信库适配问题;
- 不用换硬件,加三个环境变量解决;
- 沉淀出一套可复用的平台问题排查方法:对照实验隔离变量 → 脱离框架的最小探针 → 证据保全四件套。
如果这篇帮你省了几个小时,欢迎点赞收藏 👍 有遇到类似问题的,评论区交流。
更多推荐


所有评论(0)