作者为共用一张 RTX 3090 和本地 Qwen 后端的多个智能体会话增加跨进程 flock,按后端地址区分锁,并让未获锁的任务转用云端。锁按每轮生成持有和释放,避免长期会话独占资源;首次并发实测还暴露了锁文件路径错误。
我的 Agent 会同时运行多个 session,共用一个本地模型。聊天轮次、cron 任务、扫描到一半的 subagent,全都指向同一个 Qwen backend:它位于 :8080 端口的 llama-swap 后面,一张 3090 上只加载一个模型。两个轮次同时向这个引擎发起生成,就会扰乱先发起那个轮次正在使用的 KV cache。于是,规则变成了:谁正在生成,谁就独占这条通道,其他请求一律走云端 fallback。这个独占机制必须跨进程生效,因为我的这些 session 并不运行在同一个进程里。聊天通道、cron runner 和 subagent 工具各自都会启动进程。如果问题出在三个 Python 进程同时争抢资源,那么只存在于其中一个进程内部的锁,什么也保护不了。
这道门使用跨进程的 flock,每个 backend 对应一把锁,以 host 和 port 为键。第一个关键设计决定,是锁应该持有多久。如果按 session 持锁,一个长期存活的聊天 session 就会永远霸占 3090,让所有 cron 任务都得不到执行机会,所以我选择按轮次持锁。轮次开始时获取锁,结束时释放。这样,在两个轮次之间,通道就是空闲的,下一个要生成的请求可以直接进入。安静的 session 就只是安静地待着,只有正在生成时才会占用通道。
第二个决定是如何清理。flock 的锁由文件描述符持有,进程死亡时,内核会自动释放锁,这让我省掉了一整类代码。不需要清理过期锁的程序,不需要持续更新心跳文件,也不需要猜测一把已经存在 20 分钟的锁是不是失效了。释放锁的代码路径就算彻底崩溃,这道门依然会重新打开。锁文件旁边还有一个小型 sidecar 文件,记录获取锁时的 pid、session、model 和 timestamp。遇到疑似卡住的情况,就靠它查看这道门的状态。
这道门在 Agent 循环的三个位置接入。第一个是在构建轮次时,紧接着 runtime 恢复 primary model 之后,当前轮次尝试获取锁。如果门正忙,就进入实际的 fallback 链,而链上的第一个选项是云端模型。第二个是在遍历 fallback 链时,跳过指向繁忙本地 backend 的条目。这样,即使某个 session 使用的是以云端模型为 primary 的 profile,也不会 fallback 到一个已经饱和的引擎。第三个是释放锁的位置:放在 turn runner 最外层的 finally 中。无论轮次正常完成、报错,还是被取消,都在同一个地方释放锁。此外,我还新增了一个 failover 原因 local_backend_busy,显示为 "local model in use by another session",让用户一眼就能看出这是请求被分流,而不是 provider 出了故障。
fail-open 是不可妥协的要求。这道门只是性能保护措施,Agent 的可用性绝不能依赖它。门控出错不能阻塞轮次;没有配置时,行为必须与以前完全一致;任何非本地 backend 都直接跳过门控。这道门自身出故障,最坏的结果也不过是两个轮次共用显卡,也就是加门之前的状态。
测试套件第一次运行就全部通过了:17 个门控测试,覆盖互斥、繁忙时跳过、fail-open,以及一个真正跨进程的 flock 用例;另外还有 74 个覆盖 fallback 和轮次生命周期代码的回归测试。接着,我跑了真正能说明问题的版本:两个实际运行的 CLI session,再加一个 cron 轮次。第一次对决就抓到了一个测试套件在结构上根本无法发现的 bug。flock 只能让计算出相同锁路径的进程互斥,而单元测试只运行一个进程,home 目录也是我手动指定的,所以只要路径在内部保持一致,测试就能通过。我在剥离 profile home 路径时解析错了根目录,导致锁文件及其 sidecar 落到了各 profile 自己的目录树里,原本指定的共享目录却空空如也。我能发现这一点,全靠对决测试会打印锁文件的实际位置。两个 CLI session 之间的分流仍然有效,因为它们属于同一个 profile,认定的是同一条错误路径。这才是最隐蔽的地方。机器上的 session 可能来自任何一个 profile,共享根目录存在的意义,就是让它们最终使用同一把锁。如果一道门只有部分进程能看见,那实际上就成了两道门。
提交一次根目录修复后,第二次对决中,锁文件和 sidecar 都出现在了指定的标准路径上。B 被分流到云端模型,A 正常释放锁,测试通过。第三次对决又加上了我真正想要的探测:A 宣布完成后,确认锁文件已经没有被锁定,再重新获取一次锁,证明本地模型可以立即被再次获取。
如果 fallback 之后再也不回来,那运行这一个模型的成本可就高了,所以我也专门检查了回程。每个轮次开始时,primary runtime 都会恢复,因此这道门只会分流发生冲突的轮次,下一个计划执行的轮次仍然会优先尝试本地模型。实际验证来自同一个 cron 任务在一晚中的运行记录:00:05 使用本地模型;00:11 因为一场对决持有门锁,被分流到云端;00:17 又回到了本地模型。整个过程没有任何人工干预。
我的两个 Agent profile 都已打开 model.single_session_lock: true。那天晚上,gateway 使用修复后的代码重新启动了两次,每个通道都顺利重连。门控以 backend 而不是模型为键,因为这张显卡本来就一次只加载一个模型。等到有了第二条 GPU 通道,再把这个键改成按模型划分。
如果你的模型服务器前面接着任何会启动并发 session 的系统,你需要的就是这种机制。把精力花在让两个真实进程执行真实轮次上,因为无论锁文件落在哪里,单进程测试套件都可能通过。还要坚持 fail-open,因为一道可能让 Agent 死锁的门,比它原本要解决的资源争抢更糟。
一张 3090,一次一个模型,2026 年 9 月。
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。