AI News HubLIVE
站内改写2 分钟阅读

VRAM幽灵猎手:你该关闭哪个()?

H公司团队在GPU集群中发现了一个幽灵进程漏洞:作业崩溃后,GPU显存仍被占用且无归属进程。深入排查发现,这是由于FUSE挂载的S3存储路径中,用户态守护进程(rclone mount)因OOM被杀后,内核未能正确中止FUSE连接,导致训练进程卡在不可中断睡眠状态,从而持有CUDA上下文。最终通过强制中止FUSE连接解决了问题,但根本原因在于文件描述符泄露。

来源Hacker News AI作者: zhwu

在H公司,训练工作负载(尤其是在线强化学习和大型监督微调任务)运行在由SkyPilot管理的GPU集群上,后端包括Kubernetes和Slurm。典型作业使用8×H100节点进行多节点分布式训练,检查点和数据集分片存储在S3中,需要高吞吐量流式传输。

SkyPilot的MOUNT_CACHED功能简化了这一过程:它将S3桶挂载为Pod内的本地目录,使用本地磁盘回写缓存和rclone挂载驱动与S3异步同步。训练代码看到的是普通文件系统,但实际上它是FUSE挂载,内核将所有文件系统系统调用传递给用户态守护进程。

问题最初出现在Kubernetes后端,研究者新预订的8×H100节点在加载模型权重时出现OOM。使用nvidia-smi检查发现,GPU 0占用了80 GiB显存,其他GPU也有数百MiB到1 GiB不等的占用,但没有关联的PID。这些显存似乎被幽灵进程占用。

调查过程中,团队使用了containerd CLI列出仍在运行的容器,发现多个容器在Pod被删除后仍处于RUNNING状态。进一步检查显示,这些容器内的线程处于D状态(不可中断睡眠),其等待通道是request_wait_answer,这是FUSE设备驱动中用于等待用户态守护进程响应的函数。这意味着训练进程被阻塞在FUSE挂载上的read()调用,而响应永远不会到来。

通过检查/sys/fs/fuse/connections/目录,团队发现多个FUSE连接有等待者但没有对应的用户态守护进程。正常来说,守护进程死亡后内核应调用fuse_abort_conn唤醒所有阻塞的读取并返回-ECONNABORTED,但这里并未发生。

强制中止连接后(通过向/sys/fs/fuse/connections/*/abort写入1),被阻塞的线程立即退出,显存被释放。这证实了机制:卡住的FUSE连接导致训练进程停滞,进而持有CUDA上下文,使VRAM无法回收。

进一步分析发现,用户态守护进程rclone mount因OOM被杀死,但内核未能中止连接,因为fusermount-server(SkyPilot中用于非特权Pod的FUSE挂载代理)仍持有/dev/fuse的文件描述符。这阻止了内核检测到守护进程已死。团队还注意到fusermount-server中累积了大量/dev/fuse文件描述符,表明存在文件描述符泄漏。

作为临时热修复,团队编写了脚本检测空闲GPU节点上的显存占用,并通过AWS SSM执行fusermount -u --abort强制中止僵尸FUSE连接。大多数节点立即恢复正常,但少数顽固节点需要重启。

该漏洞仅在Kubernetes后端出现,因为SkyPilot使用了额外的特权辅助容器来为非特权用户Pod代理FUSE挂载。然而,类似的模式可能出现在任何具有文件描述符代理的环境中。团队提供了检测清单,包括检查显存占用、卡住的容器、D状态线程和FUSE连接等待者。

此案例强调了在分布式训练基础设施中,FUSE文件系统路径的稳定性至关重要,尤其是在守护进程可能因内存压力被杀的情况下。未来的工作包括改进MOUNT_CACHED的OOM处理,以及修复fusermount-server中的文件描述符泄漏。