> 一个"不覆盖已有结果"的守卫,会把可重试的失败变成不可重试的
一个跑 GPU 实验的 runner 加了两个都很合理的机制,合起来把自己锁死了:
结论先行¶
一个跑 GPU 实验的 runner 加了两个都很合理的机制,合起来把自己锁死了:
| 机制 | 理由 | 位置 |
|---|---|---|
启动时写 command.json |
记下这次跑的到底是什么参数 | 加载模型之前 |
| 拒绝任何非空的输出目录 | 不要覆盖一次已有的结果 | 同一处 |
| launcher 在可重试错误上 sleep 后重试 | 显存不够 / 别的租户占着卡,等一会儿就好 | launcher |
于是:显存不够导致的失败,如果发生在目录创建之后,
目录里就留下了一个 command.json;下一次重试立刻 FileExistsError 退出。
那条"等一会儿就好"的路径一次也走不通。
后来的 v2 launcher 改成"目录存在、且里面没有 result.json 时才删掉重排队"。
这保住了已完成的运行,但把判据换成了另一个代理:
result.json 存在就算完成——而写这个文件的过程被打断、或者文件本身残缺,
一样算完成。
同一轮审查还抓到一处纯粹的顺序问题:
--lora 和 --save-fork-checkpoint 这两个参数不兼容,而这个拒绝写在训练循环之后。
一条只看命令行参数就能判定非法的组合,会先花掉整整一次训练,
然后退出,既没有 result.json 也没有 checkpoint。
为什么¶
这三件事是同一个问题的三张脸:失败处理是一个顺序问题, 而"完成"必须是一个被明确定义、且各方共用的谓词。
① 守卫和重试必须共用同一个"完成"定义。 "拒绝非空目录"隐含的定义是"目录非空 = 这里跑过了"; 重试隐含的定义是"没有产出 = 可以再来一次"。 两个定义在"创建了目录但没跑完"这个状态上正好相反,而那个状态恰恰是失败后的常态。 正确做法是定义一个终止标记——最后一步原子写入的那个文件—— 然后让守卫和重试都读它:有标记就不许覆盖,没标记就可以清掉重来。 注意这和"存在即完成"不是一回事:终止标记要最后、原子地写 (写临时文件再 rename),这样它的存在本身就意味着前面都做完了。
② 校验按代价排序。 一条检查只依赖命令行参数,它就该在 argparse 阶段跑。 修补之后的代码把这件事和理由一起写下来了:
if args.lora and args.save_fork_checkpoint:
# validate BEFORE model load/training: the late check alone wasted a
# full run (Codex T2 review, 2026-10-08)
ap.error("--save-fork-checkpoint is full-FT only (...)")
注意那条晚的检查留着没删,这是对的:它不花钱,而且它防的是 "参数以外的状态也可能导致同一种不兼容"。 早的检查省钱,晚的检查兜底,两个都要。
排序的通用形态是按代价分层:
只看参数的 → argparse 阶段
只看文件系统的 → 加载模型之前
需要模型/数据的 → 加载之后、训练之前
只有跑完才知道的 → 训练之后(这一层才允许昂贵地失败)
③ 一个"可重试"的错误,要确认它真的可重试。 显存不够是典型的可重试错误——前提是重试的路径是通的。 写了重试而路径被自己的守卫堵住,比没写重试更糟: 它让日志里出现一串"retrying…",而每一次都在同一个地方立刻失败。
怎么用¶
- 定义一个终止标记,最后一步用"临时文件 + rename"原子写入。 守卫(不覆盖)和重试(可清理)都读同一个标记。
- 不要用"目录非空"或"某个文件存在"当完成判据。 前者在失败后一定为真,后者在写入被打断时也为真。
- 按代价给校验分层,只依赖参数的放在参数解析里。 早的检查省钱,晚的检查兜底;加了早的不要删晚的。
- 写完重试之后真的触发一次那个错误,确认重试路径能走通。 这类死锁在代码里看不出来,因为两个机制单独看都对。
- 修补时把理由和触发它的那次事故写进注释。上面那段注释里的 "the late check alone wasted a full run" 就是下一个读代码的人需要的全部上下文。
- 相关:断言的是拒绝本身而不是非零退出,见 断言"它拒绝了"不能用非零退出码——任何东西都会非零退出; "存在即完成"的同类坑见 0 FAIL 不等于那些条件都成立——一份严格审计里八处用廉价代理冒充实质检查; 产物落进自己的输入命名空间见 派生产物写回输入的命名空间,这一步就不再幂等——而删掉产物不是修法。
出处¶
自己的 run_llm_collapse_experiment.py 与三个 launcher 脚本,一次只读代码审查
(2026-10-08):command.json 写在模型加载之前且拒绝非空目录,
两个 launcher 的可重试分支都没有清理目录,v2 改用 result.json 存在性当完成判据;
LoRA 与 --save-fork-checkpoint 的不兼容原本在训练循环之后才拒绝。
参数级拒绝已前移到 argparse 阶段(晚的那条保留),上面那段注释是修补时写的原文。