dsh-gpu-guard:清理 Agent 停止後遺留的 GPU 孤兒進程樹

前言

在 Windows 上用 DSH 跑訓練類任務時經常會遇到一種情況:你已經按了停止,agent 也確實退出了,但任務管理器裏某個 python.exe 還在佔着顯存,過一會兒 GPU 又被點滿。看起來像“停了又自己跑起來”,其實是兩件事疊在一起:Windows 不會級聯殺進程,停掉 subagent 只殺掉它直接 spawn 的那個 shell,shell 下面的 bash -> conda run -> python -> worker 全部變成孤兒,繼續持有 CUDA 上下文;而訓練腳本自帶的 for 循環或 --retries N 又會讓外層腳本再起一輪,於是顯存被反覆佔滿。

lvyue1314/dsh-gpu-guard 就是爲這個問題寫的 DSH 插件。它做的是兩件事:把“誰是孤兒根”算出來,並可選地讓一個看門狗一直盯着,在孤兒樹存活超過寬限期後從根上整樹結束。

下面介紹它的工作方式、安裝方法和典型用法。

這是什麼

一個社區維護的 DSH 插件,維護者是 lvyue1314,分類爲 admin-security,採用 MIT 許可證。它面向 Windows + Node >= 22 + DSH >= 0.2.1-rc.5 的環境。

定位一句話:掃描持有 GPU 計算上下文的進程,沿着祖先鏈找到那個“發起它的進程已經不存在”的孤兒根,然後從這個根開始用 taskkill /T /F 整樹結束。默認 dry-run,需要顯式確認或開啓看門狗才會真殺。

一個典型的泄漏鏈如下,進程名和層級是插件作者實測的形狀:

<launcher shell>            <- agent 停止時被殺掉的就是這一層
 └ bash run_train.sh        <- 從這裏開始全部成爲孤兒
   └ conda run -n myenv python -m pkg.train --retries 2
     └ cmd.exe /d /c %TEMP%\tmpXXXXXXXX.bat
       └ python -m pkg.train --worker
         └ python train.py  <- 持有 CUDA 上下文,顯存與利用率都被它佔着

命令行層面本來就能用 taskkill /T /F /PID <根> 解決,難點是你得先知道根是誰,還得在腳本拉起下一輪之前動手。這個插件把這兩步自動化了。

判定規則

對每個持有 GPU 計算上下文的 PID,插件會向上走一遍祖先鏈,按下面幾條規則歸類。

1、鏈上某個父 PID 已經不在進程表裏 → 判爲孤兒,根取鏈上最上面那個還活着的進程。
2、父進程存在,但它的啓動時間比子進程還晚 → 說明 PID 被複用了,原父進程實際已死 → 同樣判孤兒。
3、走到 0 或自身(System、服務宿主)→ 視爲正常,不動。
4、鏈上有環,或者長度超出合理深度 → 歸屬不明,一律不殺。
5、鏈上任意一環命中 protect 正則(默認 jupyter、nohup、code.exe、explorer.exe、services.exe、svchost.exe,大小寫不敏感,匹配進程名和完整命令行)→ 跳過。

真正依據的信號是“發起它的那個進程沒了”,不是“它跑了很久”。所以用 nohup 或計劃任務拉起來的常駐任務不受影響,除非把 nohup 從保護列表裏刪掉。

殺的時候殺的是整棵樹最上面的孤兒根,不是最深的那個 worker。這樣腳本里的重試循環和 supervisor 會被一起端掉,不會出現“殺了 worker、父進程又起一個”的情況。

核心功能

插件暴露三種使用面,共用同一套掃描和判定實現(lib/core.js 是純函數層,插件和命令行共用,沒有第二套邏輯)。

命令行工具 gpu-guard 提供幾個子命令:

node bin/gpu-guard.mjs status              # 誰在佔 GPU、誰是孤兒
node bin/gpu-guard.mjs reap                # 預覽:會殺哪些樹
node bin/gpu-guard.mjs reap --apply        # 真殺
node bin/gpu-guard.mjs watch --apply       # 常駐看門狗(默認 60s 掃一次、孤兒滿 60s 才動手)
node bin/gpu-guard.mjs status --json       # 給腳本用

JSON 輸出結構爲 { at, gpu: { gpus, holders }, holders, roots, warnings }。命令行這份不依賴 DSH 運行,裝完隨時可用。

插件工具在對話裏提供三個:

  • gpu_status:每張卡的顯存/利用率/溫度 + 持有 GPU 的進程 + 祖先鏈 + 哪些是孤兒。
  • gpu_reap:結束孤兒樹,默認 dry-run,傳 confirm: true 才真殺;可以只處理指定的 pids。
  • gpu_guard:看門狗狀態、開啓、關閉、改配置(間隔、寬限期、保護正則)。

看門狗通過配置文件開啓:

node bin/gpu-guard.mjs config --auto     # 寫 ~/.dsh/gpu-guard.json: { "autoReap": true }

插件加載時就會啓動定時器。autoReap=false 時看門狗只把孤兒樹打到日誌和 stderr,不動手。孤兒樹要連續存活超過 graceSeconds(默認 60s)纔會被處理,短暫出現的孤兒(比如一個命令剛好結束)不會被誤殺。

安裝與啓用

把插件鏈接進 profile:

cd ~/.dsh/profiles/desktop
pnpm add link:/path/to/dsh-gpu-guard

然後二選一掛載。

方式 A,寫進 profile 的 package.json:

{
  "dependencies": { "dsh-gpu-guard": "link:/path/to/dsh-gpu-guard" },
  "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-gpu-guard"] } }
}

方式 B,寫進 profile 的 cordis.patch.yml:

- insert:
    - id: dsh-gpu-guard
      name: file:///path/to/dsh-gpu-guard/lib/entry.js

如果 profile 配置了 patchReload: live,下一次模型調用就能看到三個工具;否則重啓 DSH。README 裏還給了一個用計劃任務替代常駐進程跑看門狗的示例 examples/scheduled-task.ps1。

配置

配置文件位於 ~/.dsh/gpu-guard.json,可以用環境變量 DSH_GPU_GUARD_CONFIG 改路徑:

鍵 默認 說明
autoReap false 看門狗是否自動結束孤兒樹。默認關,因爲這是殺進程
intervalSeconds 60 掃描間隔(一次完整掃描約 1.5s:nvidia-smi + WMI 進程表)
graceSeconds 60 孤兒樹要存活多久才動手
protect 見上文 命中即跳過的正則

日誌追加到 ~/.dsh/gpu-guard.log(環境變量 DSH_GPU_GUARD_LOG)。插件狀態也可以直接查:GET http://127.0.0.1:<port>/dsh-gpu-guard/state。

適用場景與注意

適合使用場景:Windows 上通過 DSH 跑 CUDA 訓練或推理任務,曾經遇到過 agent 停止後顯存仍被佔用、或每隔幾分鐘 GPU 又被點滿的情況。插件只處理持有 GPU 計算上下文的孤兒樹,節點是 Windows,命令行版本不依賴 DSH 也能用。

幾個使用上需要注意的點:

1、插件以當前 DSH 進程的權限運行,殺進程用的是 taskkill /T /F,安裝前應當先讀一遍源碼確認行爲,並確認許可證爲 MIT。
2、接口層面默認不殺,gpu_reap 需要顯式 confirm: true,命令行 reap 需要 --apply。
3、WDDM 下 nvidia-smi 的 per-process 顯存顯示爲 N/A,而且只列計算上下文。插件改從 Windows 性能計數器讀(\GPU Process Memory(*)\Dedicated Usage),把圖形上下文也算上;極少數服務器 SKU 沒有這組計數器時會給出警告並退回 N/A。
4、圖形上下文(瀏覽器/聊天軟件的硬件加速、桌面合成)會被單獨標註爲 G,計算上下文標註爲 C,兩者都有標註 C+G。把性能計數器加總能和 nvidia-smi 報的總佔用對上,就說明沒有看不見的殘骸,顯存剩餘通常是別的常駐程序佔的。
5、PID 複用已用啓動時間校驗擋掉;極端情況下(同一秒內 PID 被複用)仍有窗口,killTree 會在殺之前再讀一次進程表確認。
6、不處理 DSH 的 background job:如果進程是以 run_in_background: true 啓動的,它本來就應該活過 subagent。插件不替你判斷語義,只按“祖先鏈斷沒斷”這條硬事實動手。

作者在 README 裏也說明這是止血手段,根治方案是在 dsh-subprocess-local 那一層用 Job Object(JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE)讓父進程死亡時整棵樹被內核帶走,相關 issue 指向 @deepseek-ai/dsh-win32-process 中已存在的 spawnCurrentTokenJobProcess 原語。

結語

停下來別不管的 GPU 守護進程問題,在 Windows + DSH 的組合裏很常見。dsh-gpu-guard 的價值在於把“找孤兒根”這件事從手工排查變成一條命令或一個工具調用,並且在默認配置下不會誤殺。對這類任務,它值得先試用 dry-run 模式看看判定結果是否準確,再決定是否開啓看門狗。

插件目錄頁:https://www.skillhub.cn/plugins/lvyue1314/dsh-gpu-guard
GitHub 倉庫:https://github.com/lvyue1314/dsh-gpu-guard

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜