今天在组里的某个 GPU 集群上部署了一套 SLURM,方便多人使用。我本以为只是从已有集群复制配置、启动服务,十分钟之内就能搞定,没想到因为一个玄学的问题整整卡了一个多小时。
问题现象:各个节点上的 slurmctld、slurmd、slurmdbd 都能正常启动,然而只要试图提交任务,机器上的 slurmd 和派生的 slurmstepd 就会卡死(任务并没有开始运行),不再有任何响应(slurmctld 也无法联系它),只能靠 SIGKILL 杀死后重新启动。各个进程的日志中都看不出什么异常;能看到的只有超过一定时间后,slurmctld 认为 slurmd 超时没有响应而产生的抱怨。
考虑到我也在 SLURM 上踩过各种各样的坑,我花了半个多小时,排除了以下问题:
- 文件系统问题:就算没有共享文件系统,
whoami之类的命令还是会工作的。并且我们的 NFS 看起来非常正常。 - hostname 问题:如果机器名称和 IP 的解析/反向解析有误,会导致各个 daemon 之间通信异常:机器上都有正确的 hosts,并且我额外还配置了
NodeAddr。而且只要不提交任务,其他的一切都正常。 slurmdbd/ 数据库问题:注释掉相应部分,故障依旧。- cgroups 问题:都 6202 年了,不可能还有 cgroups v1 的问题了;并且关闭相应部分,故障依旧。
并且最关键的一点是,相同的配置文件,在同样的系统、硬件上(我也是从这里复制的),是完全能工作的。那到底是怎么回事呢?
在 Claude 的提示下,我用 gdb 对卡死的 slurmstepd 进程打印了调用栈,结果还真有惊奇的发现:
$ gdb -p $(pgrep slurmstepd) -batch \
-ex "set pagination 0" \
-ex "thread apply all bt"
Thread 1 (Thread 0x151e80a3fc00 (LWP 261104) "slurmstepd"):
#0 0x0000151e806a59ee in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#1 0x0000151e8069a668 in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#2 0x0000151e8069a6ad in ?? () from /lib/x86_64-linux-gnu/libc.so.6
#3 0x0000151e8070e9c6 in poll () from /lib/x86_64-linux-gnu/libc.so.6
#4 0x0000151e7fa7928b in ?? () from /lib/x86_64-linux-gnu/libxcb.so.1
#5 0x0000151e7fa76b8e in xcb_connect_to_fd () from /lib/x86_64-linux-gnu/libxcb.so.1
#6 0x0000151e7fa7b279 in xcb_connect_to_display_with_auth_info () from /lib/x86_64-linux-gnu/libxcb.so.1
#7 0x0000151e7eaf20c2 in _XConnectXCB () from /lib/x86_64-linux-gnu/libX11.so.6
#8 0x0000151e7eae1b45 in XOpenDisplay () from /lib/x86_64-linux-gnu/libX11.so.6
#9 0x0000151e8036437b in ?? () from /usr/lib/x86_64-linux-gnu/hwloc/hwloc_gl.so
#10 0x0000151e80bba58e in ?? () from /lib/x86_64-linux-gnu/libhwloc.so.15
#11 0x0000151e80bc1c31 in hwloc_topology_load () from /lib/x86_64-linux-gnu/libhwloc.so.15
#12 0x0000151e80379047 in pmixp_libpmix_job_set () from /usr/lib/x86_64-linux-gnu/slurm-wlm/mpi_pmix_v5.so
#13 0x0000151e8037d3d9 in pmixp_stepd_init () from /usr/lib/x86_64-linux-gnu/slurm-wlm/mpi_pmix_v5.so
#14 0x0000151e80375dbb in mpi_p_slurmstepd_prefork () from /usr/lib/x86_64-linux-gnu/slurm-wlm/mpi_pmix_v5.so
#15 0x0000561857a128df in job_manager ()
#16 0x0000561857a06eb9 in main ()
这 poll 是在干啥?为啥会有 xcb_connect_to_fd 这种奇怪的函数?我又不是在图形界面上运行 SLURM,怎么会有 X11 的调用?
又经过一番调研,原来是 SLURM 的任务管理插件 PMIx 会使用 libhwloc 来探测硬件相关信息,而其中一个插件是 hwloc_gl,它会尝试连接 X11 来获取 GPU 的信息。在我的机器上,这个连接不知道为什么就卡住了,陷入了无限的等待(poll)。在发现是 hwloc 问题后,我意识到完全不需要 SLURM 这么复杂,直接使用 lstopo 工具就能复现问题。
hwloc 的问题告诉我们,用 HWLOC_COMPONENTS=-gl 环境变量可以禁用掉这个插件。果然,加上之后 lstopo 就能工作了,而将它写入 /etc/default/slurmd 之后,重启 slurmd,任务就可以正常提交了。
但是,巧夺麻袋,这确实能解决问题,然而我还是没搞明白,为什么别的机器上没有这个问题?我给 lstopo 挂上了 strace,发现它做了如下的尝试:
[pid 266805] connect(3<UNIX-STREAM:[264153037]>, {sa_family=AF_UNIX, sun_path=@"/tmp/.X11-unix/X0"}, 20) = -1 ECONNREFUSED (Connection refused)
[pid 266805] connect(3<UNIX-STREAM:[264153038]>, {sa_family=AF_UNIX, sun_path="/tmp/.X11-unix/X0"}, 110) = -1 ENOENT (No such file or directory)
[pid 266805] connect(3<UNIX-STREAM:[264153039]>, {sa_family=AF_UNIX, sun_path="/var/run/nscd/socket"}, 110) = 0
[pid 266805] poll([{fd=3<UNIX-STREAM:[264153039->264130315]>, events=POLLIN|POLLERR|POLLHUP}], 1, 5000) = 1 ([{fd=3, revents=POLLIN|POLLHUP}])
[pid 266805] connect(3<UDP:[264202942]>, {sa_family=AF_INET, sin_port=htons(6000), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
[pid 266805] connect(3<UDPv6:[264202943]>, {sa_family=AF_INET6, sin6_port=htons(6000), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, 28) = 0
[pid 266805] connect(3<TCPv6:[264202944]>, {sa_family=AF_INET6, sin6_port=htons(6000), sin6_flowinfo=htonl(0), inet_pton(AF_INET6, "::1", &sin6_addr), sin6_scope_id=0}, 28) = 0
[pid 266805] poll([{fd=3<TCPv6:[[::1]:39034->[::1]:6000]>, events=POLLIN|POLLOUT}], 1, -1) = 1 ([{fd=3, revents=POLLOUT}])
[pid 266805] poll([{fd=3<TCPv6:[[::1]:39034->[::1]:6000]>, events=POLLIN}], 1, -1 <unfinished ...>
[pid 266805] <... poll resumed>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
看起来是非常标准的流程,先尝试连接 abstract socket,再尝试普通 socket,最后才是 TCP 连接。前两者都失败了,可在一个 headless 服务器上,6000 端口上怎么会有程序在监听?我到处 ss 了一番,发现居然是组里其他同学在上面跑的 Megatron-LM 训练脚本把它作为 MASTER_PORT 了。于是,整个流程就非常明确了(这一段由 Claude 生成):
Megatron 用 MASTER_PORT=6000 起 TCPStore,rank0 监听 [::]:6000
↓
这台是无头节点,没有 /tmp/.X11-unix/X0
↓
slurmstepd 起新 PMIx 任务 → PMIx 调 hwloc_topology_load → hwloc_gl 探测 :0
↓
:0 的 Unix socket 不存在 → xcb fallback 到 TCP localhost:6000
↓
connect([::1]:6000) 成功 —— 连进了 TCPStore!
↓
xcb 发 12 字节 X11 握手,TCPStore 看不懂 X 协议、不回应
↓
hwloc 永久卡在 poll(POLLIN)
这下终于明白了!在对相关同学进行殴打之后,我了解到这个端口来自 Megatron-LM 的样例脚本。在相关仓库中搜索 6000,能得到非常惊人的结果:几乎所有脚本都默认使用 6000 作为 MASTER_PORT;如果把搜索范围扩大到整个 GitHub,能看到无数的脚本已经被传染了。老黄,真有你的。我去给 Megatron 开了个 issue #5232,建议他们还是换个默认端口。
那为什么在其他机器上没有这个问题呢?因为:
- 我并没有在集群被已有作业占用的时候部署 SLURM,也没有在这个情况下测试提交。
- 同学往现有的 SLURM 集群提交使用 6000 端口的任务时,不会影响到
slurmstepd的启动流程。 - 我们通常占用全机训练,所以也不会出现这个端口正在被占用时,
slurmstepd又开始启动的情况。