最近把 KaTrain 配成了远程 KataGo:本地 Mac 只运行 KaTrain 界面,真正的 KataGo 分析放到远端 GPU 服务器上跑。整体体验很好,但这次遇到一个很典型的问题:KaTrain 里远程引擎显示“崩溃”或“无法使用”。

排查下来,服务器上的 GPU、Docker 镜像和模型文件都正常,真正的问题出在本地 SSH 隧道的 Unix socket 残留。

远程 KataGo 的整体结构 链接到标题

我的配置大致是这样:

KaTrain
  └─ engine.altcommand
       └─ katago-bridge-analysis
            └─ /tmp/katago-tunnel.sock
                 └─ launchd 维护的 SSH 隧道
                      └─ 远端服务器 localhost:5000
                           └─ katago-multiplexer.py
                                └─ Docker KataGo TensorRT 容器

这里有几个关键点:

  • KaTrain 不直接启动本地 KataGo,而是通过 engine.altcommand 调用一个 bridge 脚本。
  • bridge 脚本连接本地 Unix socket:/tmp/katago-tunnel.sock
  • 这个 socket 背后是 launchd 长期维护的 SSH 隧道。
  • 远端服务器上有一个 katago-multiplexer.py,负责把多个客户端请求转发给 KataGo analysis 进程。

也就是说,KaTrain App 关闭以后,SSH 隧道仍然可以由 launchd 继续维护。这样下一次打开 KaTrain 时,不需要重新建立完整链路。

故障现象 链接到标题

KaTrain 启动后,远程引擎状态显示崩溃或不可用。

一开始直觉怀疑是远端服务器出问题,比如:

  • GPU 掉卡;
  • Docker 无法使用 NVIDIA runtime;
  • KataGo 镜像损坏;
  • 模型文件路径错误;
  • 远端 KataGo 进程异常退出。

但逐项检查后发现,远端服务器的 GPU、Docker、KataGo 镜像、模型文件都正常。甚至直接在远端启动 KataGo analysis 也没有问题。

关键线索:本地 SSH 隧道反复退出 链接到标题

继续看本地 launchd 服务状态,发现 com.katago.tunnel 一直在反复启动和退出。

错误日志里最关键的一行是:

unix_listener: cannot bind to path /tmp/katago-tunnel.sock: Address already in use
Could not request local forwarding.

这说明 SSH 想创建 Unix socket 转发时,发现目标路径已经存在,于是绑定失败。

再检查 /tmp/katago-tunnel.sock,发现文件确实存在,但没有任何进程持有它。也就是说,它不是一个正在工作的 socket,而是一次异常退出后留下来的孤立文件。

根因:Unix socket 文件不会自动清理 链接到标题

普通 TCP 端口转发不会留下文件,但 Unix socket 转发会在文件系统里创建一个 socket 文件。

如果 SSH 进程异常退出,或者笔记本休眠、网络切换、带到外面再回来,可能出现这样的状态:

  1. SSH 隧道进程已经没了;
  2. /tmp/katago-tunnel.sock 文件还在;
  3. launchd 尝试重新启动 SSH;
  4. SSH 发现 socket 文件已存在;
  5. 绑定失败,隧道起不来;
  6. KaTrain bridge 连接失败;
  7. KaTrain 显示远程引擎崩溃。

这也是为什么这个问题很容易在移动笔记本、切换网络、休眠恢复之后出现。

SSH 本身提供了一个专门解决这个问题的选项:

StreamLocalBindUnlink=yes

它的作用是:在绑定 Unix socket 之前,如果目标路径已经存在,就先 unlink 掉旧文件。

也就是说,不需要额外写 wrapper 脚本去 rm -f /tmp/katago-tunnel.sock,直接让 SSH 自己处理即可。

launchd plist 中的 SSH 参数加入两行:

<string>-o</string>
<string>StreamLocalBindUnlink=yes</string>

对应的 SSH 命令语义类似:

ssh -NL /tmp/katago-tunnel.sock:localhost:5000 \
  -o ExitOnForwardFailure=yes \
  -o ServerAliveInterval=30 \
  -o ServerAliveCountMax=3 \
  -o ConnectTimeout=10 \
  -o StreamLocalBindUnlink=yes \
  user@remote-host

改完后重载 launchd 服务:

launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.katago.tunnel.plist 2>/dev/null || true
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.katago.tunnel.plist
launchctl kickstart -k gui/$(id -u)/com.katago.tunnel

再检查状态:

launchctl print gui/$(id -u)/com.katago.tunnel | grep -E "state =|active count|last exit code|runs"

如果看到 state = running,说明本地 SSH 隧道已经恢复。

远端顺手修复:Multiplexer 和后台容器残留 链接到标题

这次排查中还发现一个远端问题:katago-multiplexer.py 异常退出后,后台 Docker 容器仍然残留。再次启动 multiplexer 时,Docker 报容器名冲突:

The container name is already in use

处理分两层:

容器清理自动化 链接到标题

在 multiplexer 脚本的启动逻辑中加了一行,每次启动前先清理同名遗留容器:

subprocess.run(["docker", "rm", "-f", "katago-mux-backend"],
               capture_output=True)
self.katago = subprocess.Popen(KATAGO_CMD, ...)

这样无论之前容器是什么状态,multiplexer 都能顺利重建。

TensorRT 缓存持久化 链接到标题

但这样一来,每次 multiplexer 重启都会丢失 TensorRT 编译缓存——首次分析要等 60-110 秒,用户体验很差。

解决方案是在 KataGo 容器启动时,把宿主机的目录挂载到容器的默认缓存路径:

"-v", "/opt/katago/tensorrt-cache:/root/.katago",

KataGo TensorRT 后端默认将编译引擎缓存到 ~/.katago/trtcache/。挂载以后,即使容器被销毁重建,编译过一次的引擎文件仍然保留在宿主机上。

首次启动仍然需要 60-110 秒(加载模型 + 编译 TensorRT),但后续 multiplexer 重启后,首次分析就能秒回。

需要注意的是,目前这个版本的 KataGo(v1.16.2,TensorRT 后端)中 tensorrtCacheDir 配置项在 analysis 模式下不会生效,所以直接挂载 /root/.katago/ 比依赖配置项更可靠。

为什么不改成 TCP 本地端口转发? 链接到标题

另一个方案是把 Unix socket 改成本地 TCP 端口,比如:

127.0.0.1:15000 -> remote localhost:5000

这样确实不会有 socket 文件残留问题。但它也有几个缺点:

  • 需要额外占用一个本地 TCP 端口;
  • 可能和其他服务端口冲突;
  • bridge 脚本也要从 Unix socket 改成 TCP 连接;
  • 问题本质不是 Unix socket 不能用,而是旧 socket 文件没有自动清理。

因此保留 Unix socket,并加上 StreamLocalBindUnlink=yes,是改动最小也最干净的方案。

总结 链接到标题

这次问题表面上是 KaTrain 远程引擎崩溃,实际根因是本地 SSH Unix socket 残留。

最终修复点很小:

-o StreamLocalBindUnlink=yes

但这个选项非常关键。只要用 SSH 做 Unix socket 本地转发,尤其是配合 launchd、systemd 这类长期保活机制,就应该考虑加上它。

这类问题的排查顺序可以固定下来:

  1. 先确认远端 GPU、Docker、KataGo 模型是否正常;
  2. 再确认远端服务端口是否监听;
  3. 然后看本地 launchd 服务状态;
  4. 最后检查 Unix socket 是否是孤立残留。

对于经常带着笔记本移动、切换网络的人来说,这个小选项能避免很多“明明昨天还好好的”式问题。