最近把 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 进程异常退出,或者笔记本休眠、网络切换、带到外面再回来,可能出现这样的状态:
- SSH 隧道进程已经没了;
/tmp/katago-tunnel.sock文件还在;- launchd 尝试重新启动 SSH;
- SSH 发现 socket 文件已存在;
- 绑定失败,隧道起不来;
- KaTrain bridge 连接失败;
- KaTrain 显示远程引擎崩溃。
这也是为什么这个问题很容易在移动笔记本、切换网络、休眠恢复之后出现。
修复:使用 StreamLocalBindUnlink 链接到标题
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 这类长期保活机制,就应该考虑加上它。
这类问题的排查顺序可以固定下来:
- 先确认远端 GPU、Docker、KataGo 模型是否正常;
- 再确认远端服务端口是否监听;
- 然后看本地 launchd 服务状态;
- 最后检查 Unix socket 是否是孤立残留。
对于经常带着笔记本移动、切换网络的人来说,这个小选项能避免很多“明明昨天还好好的”式问题。