VRAM幽靈獵手:你該關閉哪個()?
H公司團隊在GPU集羣中發現了一個幽靈進程漏洞:作業崩潰後,GPU顯存仍被佔用且無歸屬進程。深入排查發現,這是由於FUSE掛載的S3存儲路徑中,用户態守護進程(rclone mount)因OOM被殺後,內核未能正確中止FUSE連接,導致訓練進程卡在不可中斷睡眠狀態,從而持有CUDA上下文。最終通過強制中止FUSE連接解決了問題,但根本原因在於文件描述符泄露。
在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中的文件描述符泄漏。