目录
- 【昇腾踩坑】双卡 TP=2 一跑就整机挂死?从 RCU Stall 一路查到 HCCL 链路选错
- 一、背景
- 二、环境信息
- 三、排查过程(逐层排除)
- 两个关键手段(强烈推荐)
- 探针第一次运行 → 卡死整机
- 四、根因
- 五、解决方案
- 六、验证结果
- 6.1 探针(加变量后)
- 6.2 稳定性阶梯(6 档,累计 1.5 小时)
- 6.3 性能说明
- 七、踩坑清单(血泪总结)
- 八、总结
【昇腾踩坑】双卡 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-ascendv0.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.pyimportosimporttorchimporttorch_npu# noqa: F401importtorch.distributedasdist 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 MiBforiinrange(20):dist.all_reduce(x,op=dist.ReduceOp.SUM)ifrank==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 环境保持一致):
dockerrun--rm--privileged--networkhost--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\-eASCEND_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 OK6.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-wkernel.panic_on_rcu_stall=1kernel.panic=30# ② journald 持久化(否则重启后查不到上次启动的日志)mkdir-p/var/log/journalsed-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。排查顺序建议:先做对照实验隔离变量,再写最小复现;不要一上来就调框架参数。
八、总结
- 一个看起来像硬件故障的整机挂死,通过对照实验 + 最小复现,最终定位成可配置绕过的通信库适配问题;
- 不用换硬件,加三个环境变量解决;
- 沉淀出一套可复用的平台问题排查方法:对照实验隔离变量 → 脱离框架的最小探针 → 证据保全四件套。
如果这篇帮你省了几个小时,欢迎点赞收藏 👍 有遇到类似问题的,评论区交流。