【昇腾踩坑】双卡 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
OSopenEuler 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(改善,未解决)
3PCIe 带宽不足lspci -vv 查链路x8(能力 x16)——是约束,不是根因
4ACS 阻断了 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 p50TPOT p50输出 tok/s总 tok/sfailed
b1512/128 @ 并发 11.1 s34.8 ms21.91110
b2512/128 @ 并发 3238.0 s41.0 ms92.04670
b3512/128 @ 并发 6448.3 s120.2 ms99.15040
b48192/512 @ 并发 3227.8 s193.0 ms107.618310
b5129024/512 @ 并发 4419 s156.0 ms4.110380
b6512/128 ×2000 @ 并发 6452.2 s240.6 ms93.14730

结论:

  • 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(实际链路宽度只有能力的一半),跨卡通信只能经主机内存中转。这是平台物理条件决定的,不是缺陷;短请求体验正常。


七、踩坑清单(血泪总结)

  1. 不要用 watch -n 1 npu-smi info!
    高频轮询会压垮驱动/监控通道,是"整机失联"的常见诱因。要用请 ≥60s 或脚本采样。

  2. 硬 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 持续落盘(单写者!)
    
  3. dmesg 采集必须"单写者" —— 同一分钟起两条会撞同名文件、互相覆盖,最后只剩一条。

  4. /tmp 重启会清空 —— 脚本一律放持久目录(如 /home/bench/)。

  5. “敲了命令” ≠ “在跑” —— 采集类进程要验证 pgrep 有 pid + 输出文件 mtime 在涨。

  6. nohup setsid xxx & 后 shell 报 [1]+ 已完成 是正常的(setsid 立刻 fork 退出)—— 验证永远看 pgrep。

  7. hccl_test 在新版 CANN 里是源码包(只有 .cc/.h + Makefile),不是现成可执行文件 —— 不如直接用 torch_npu 写个 all_reduce 探针快。

  8. HCCL 崩溃后可能残留 socket 占端口 → 可用 HCCL_NPU_SOCKET_PORT_RANGE=60000-60015 换端口范围。

  9. 双实例并存时,docker compose 项目名必须不同(默认取目录名)—— 否则 down 会互删对方容器。显式写 name: 或用 -p。

  10. 排查顺序建议:先做对照实验隔离变量,再写最小复现;不要一上来就调框架参数。


八、总结

  • 一个看起来像硬件故障的整机挂死,通过对照实验 + 最小复现,最终定位成可配置绕过的通信库适配问题;
  • 不用换硬件,加三个环境变量解决;
  • 沉淀出一套可复用的平台问题排查方法:对照实验隔离变量 → 脱离框架的最小探针 → 证据保全四件套。

如果这篇帮你省了几个小时,欢迎点赞收藏 👍 有遇到类似问题的,评论区交流。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐