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

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

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

小夜