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中的檔案描述符洩漏。