文档导航

运行一次无人值守的研究会话

这是一份无人值守研究会话的运行程序:一个过夜会话,或白天的一次长时间交接。它取代了逐夜提示词(round-6 和 night-3 程序,保留在 git tag research-archive-2026-09-24 下的 docs/prompts/),并汇入了那些夜晚出的问题。它应用的规则在 PLAN.md 的「Standing rules for every round」里;命令在 AGENTS.md 里。

一次会话在注册的规则下爬山并确认,留下 Jared 可以据以行动的记录。它不发布。

1. 开始

头 30 分钟用来阅读,不是启动。

  1. AGENTS.md,全部(命令、冻结套件、规范主页、Modal 设置)。
  2. PLAN.md:我们处在什么位置、学到了什么、常设规则、数据政策和 Next。对你想据以构建的发现,读它在归档里的证据:git show research-archive-2026-09-24:PLAN.md。
  3. .agents/skills/ 里的技能:kev-modal-study(启动、观察和拉取 GPU 工作;读它的 Gotchas)、kev-verify(证明一次代码改动没有回归)、kev-pr-description(任何 PR 之前)、thermonuclear-code-review。
  4. kev/rounds.py(其 docstring 就是 spec schema)、experiments/rounds/ 里最接近的过往 spec,以及 kev/autoresearch.py(session)。
  5. 交接本身:授权(Modal 美元、AI Gateway 美元)、范围内是什么、什么需要 Jared。

然后设置:

  • 在研究分支上的一个 worktree 里工作(git worktree add -b research/<session> /tmp/kev-<session> origin/main)。每次提交后都推,这样机器休眠也不会丢东西。要进 main 的代码走它自己经过评审的 PR。
  • 读 uv run modal billing summary --json,并把 metered_cost 记为状态文件里的基线(第 6 节)。
  • 如果上一次会话留下了状态文件,先读它并从它继续;分离的 Modal 任务会在你不在时继续跑。

2. 预算与花费规则

  • 授权是这次会话的总量,把仍在运行的一切都算进去。每次启动之前,再读一次计费成本,如果 (metered_now - baseline) + sum(admission bounds of everything still running) >= authorization 就不要启动。
  • 一次 study 的准入上界在启动时打印,并保存在 runs/<study>.spawn.json;一次 benchmark 调用的上界是 compute_bound(gpu, timeout, trials)(kev/budget.py)。spec 的 study budget 必须至少是其上界(kev.rounds validate 会检查;modal_app.admit_study 在任何东西运行之前就拒绝超预算的 study,且一个 study 上限为 $250 和 28,800 s)。
  • 留一份储备(约授权的 10 %),任何阶段都不计划动它:计费读数滞后且会被修订,而准入上界严重高估读数(一个读取批次携带其最慢任务的超时)。
  • 把每次读数连同其 UTC 时间记进状态文件。AI Gateway 花费(Jev 参考读数、标签评委)有自己的上限,由花它的脚本强制执行,并记在 runs/<name>/usage.json。
  • Modal 工作区花费上限只能从 dashboard 提高;撞上它会在训练中途杀掉运行中的容器。

3. 注册一轮

一轮就是一个 PLAN.md 小节加一个 spec,在任何训练或读取之前一起提交。

  1. 写 PLAN.md 小节:为什么(测得的差距及其证据)、数据(先冻结,带 manifest)、各臂、规则(主判据、按每个套件设定阈值的护栏、排名)、确认阶段,以及预算。用常设规则;不要为一轮发明一个新统计量。
  2. 通过复制最接近的过往 spec 来写 experiments/rounds/r<N>.json(联合增量用 r15,27B 用 r17,技能轮用 r10,无训练的事后臂用 r20:一个温度池、插值 checkpoint;向另一个 checkpoint 混合用 r23,其臂为两个端点的训练都命名 trained_on)。省掉 "archive":那个键标记已记录的 5–18 轮。spec 命名的每个 plan 文件、以及其规则需要的每个父读数,都必须存在于这个 checkout 里;如果某个父读数缺失,launch-reads <spec> --parents 会生成它。删掉被移除套件(kev.suite.REMOVED_SUITES,附原因)的每个读数:evals/external/scienthoon-v1 已在 2026-09-27 移除,所以从第 23 轮起 scienthoon 读数、面板和护栏都没了;evals/external/wanli-v2 和 typesafe-v1 已在 2026-09-30 移除,所以从第 27 轮起它们的读数也没了,SemIf 是剩下的唯一外部读数(仅报告)。合并的外部读数不是关卡:第 24 轮审计过的规则(第 23–26 轮遵循)把 SemIf、WANLI-v2 和 TypeSafe 报为可选面板。validate 和 launch 在该套件最后一个仍命名它的轮次之后拒绝一轮。
  3. 新数据是 evals/ 下的一个新目录,带一个 manifest.json(每个文件的 sha256、输入的 hash)。在 SFT 数据政策(PLAN.md)下,私有语料在 git 里只保留 manifest,用一个 "mirror" 条目指向私有数据集。
  4. 必须:每个被服务或发布的温度都来自一个留出数据集池,绝不来自训练语料的某个划分。 一轮若读取校准(ECE、Brier、自信错误、覆盖率),就为其臂注册一个 temperature 池(照抄 r20:transfer-r3 校准划分的八个留出公开来源 + transfer-v9 MMLU-Pro),而一次发布提供 scripts/calibrate_checkpoint.py 在同一池上拟合的温度。训练来源的留出条目(一个训练套件的 calibration / development 划分)是分布内的:第 19 轮用 T 0.955 服务其 SFT 臂,那是在 sft-v1 开发行上拟合的,结果所有校准判据都没过(breadth-v1 ECE 0.059);第 20 轮的留出数据集池在同一 checkpoint 上给出 0.0085。什么在强制它:
    • 从第 21 轮起,kev.rounds validate 和 launch 拒绝这样的轮:其规则或确认有一个会被温度移动的判据(ECE、Brier、NLL、自信错误、覆盖率;除准确率外的一切),却没有 temperature 池。Rounds <= 20 只对每个在训练语料上训练的臂打印一条 !!! warning,所以它们已记录的 spec 仍然能通过验证。
    • kev.rounds validate 拒绝这样的池读数:(a) 是某个臂的训练套件、其组成部分(sft-v1 的 inputs.components)或其 plan 的 data 套件,(b) 汇集了任何臂训练过的来源,或 (c) 读取任何训练语料的 calibration 或 development 划分;它也拒绝无法检查的池(训练未知的臂、没有 manifest 或未列来源的套件)。没有试验的 checkpoint 臂可以命名 trained_on。
    • 读数记录每个臂的 temperature_source;表格对在训练语料开发行上服务其试验的臂打印 !!!(第 5–19 轮全是如此;从现在起这样的温度仅供筛选)。
    • scripts/calibrate_checkpoint.py 拒绝同样的拟合集(对照 head.pt 的训练套件检查);--allow-in-distribution 只用于复现旧的拟合,并记录在 head.pt["temperature_fit"] 里。
    • 试验内的温度(result.json 的 calibration_fit)写着 role: in-trial screening ... not a served or shipped temperature。
    • 父模型在其试验开发行上拟合的温度下服务(对 Kev-27B 就是它发布的 1.38,在同一批行上拟合);读数记录这一点及其发布的 head.pt T(parent_temperature_source),当两者在一个训练语料的行上相差超过 0.05 时 validate 会警告。
    • 不相交检查按来源名称(名义,而非语义):两个套件以不同名字携带同一数据集也能通过。所以池必须使用在构造上于 Kev 中仅用于评测的来源,比如 transfer-r3 的八个留出公开来源和 transfer-v9 的 MMLU-Pro。池读数的 sources 白名单必须命名其套件列出的来源(打错字是个问题),而检查器列不出的训练(evals/ 之外的 data 文件、没有来源的 manifest)对新的一轮是个问题。
    • calibrate_checkpoint.py --temperature T(一个手填值,什么都没拟合)需要 --reason,记在 head.pt["temperature_fit"](例如「copied from the pool fit of runs/r20-readout」)。
  5. uv run python -m kev.rounds validate experiments/rounds/r<N>.json(加 --partitions 以校验划分)直到它打印 ok。把 PLAN 小节和 spec 提交在一个 commit 里,推。那个 commit 时间就是注册时间。

4. 端到端跑它

KEV_GPU=H200 uv run modal deploy modal_app.py                     # after any change to kev/*.py or any new file under evals/
uv run python -m kev.rounds launch experiments/rounds/r<N>.json   # one ::study per study, 60 s apart, logs in runs/<study>.log
caffeinate -i nohup uv run python -m kev.rounds watch experiments/rounds/r<N>.json > runs/r<N>.watch.log 2>&1 &
  • 每次 study 的头五分钟里,在 modal container logs <id> 里数每分钟的优化器步数,并对照超时推算墙钟时间(ep0 step N/M:M 是全部 epoch)。超时的容器什么都存不下;取消(FunctionCall.from_id(cid).cancel())并以更少的记录或更长的超时在新 study 名下重新启动。
  • watch 轮询已生成的试验,拉取每个完成的 study(同一时间一个 study 一次拉取),在该臂的读数就绪时立即启动(每个臂一次批量的 ::benchmarks 调用,间隔 60 s),等它们完成并写出 runs/r<N>-readout/round<N>.json 和一张表。它可重启:状态在 runs/<study>.watch.json,启动意图在 runs/r<N>-reads-<arm>.json。手工:launch-reads <spec> [--arms a,b] [--parents] [--dry-run]、readout <spec>。
  • 把读数写进 PLAN 小节:每个臂、每条判据及其区间、判决和什么失败了。
  • 确认是刻意的,从不自动。 对读数点名的候选,把选择写进 PLAN.md 并提交,然后按阶段:launch-reads <spec> --stage <stage> --arm <arm>,再 confirm <spec> --stage <stage> --arm <arm>(→ runs/r<N>-verdict/<size>-<stage>.json)。在锁定读数之前测试面板。每次读一次,没有例外。
  • 在上限下依次跑几个已注册的轮:uv run python -m kev.autoresearch session experiments/rounds/r19.json [...] --spend-start <baseline> --spend-cap <authorization>。它校验、启动并观察每一轮到其读数结束,在某轮预算会越过上限之前停下,追加到 runs/autoresearch-sessions.jsonl,并打印确认命令;它从不运行它们。kev.autoresearch leaderboard 刷新 runs/leaderboard.{jsonl,md}(不提交),compare 在迁移准确率上把试验与参考配对,release-check --study <name> 筛选该 study 里的每个配置(每个配置只有在它的所有 seed 都通过各自的关卡时才通过)。

5. 一次会话可以碰和不可以碰什么

可以:写 spec、plan 和 PLAN.md 小节;在新目录下构建新的冻结数据;通过 modal_app.py 启动 study 和读数;改脚本和 modal_app.py 基础设施常量;为属于 main 的代码开 PR。

不可以,未经 Jared 明确同意:

  • 发布或改动 Hub 上的任何东西(kev.publish、hf upload、hf repos tag、scripts/publish_space.sh、一个已发布的 head.pt),把私有仓库变公开,或部署公开端点;
  • 提交到 main、强推或合并 PR(代码通过经过评审、squash 合并、CI 全绿的 PR 到达 main);
  • 编辑 evals/ 下已存在(冻结)的任何东西,或评测器:kev/experiment.py: EVALUATOR_FILES、各关卡、kev/metrics.py、kev/rounds.py 的配对读数。需要的评测器改动是它自己的 PR,在任何轮依赖它之前用 kev-verify 和 tests/test_rounds.py 验证;
  • 在注册的确认阶段之外传 --allow-test 或运行 locked_test;
  • 把任何 Jev 输出,或任何闭源模型的生成,放进训练数据;
  • 本地训练(32 GB Mac 装不下这些模型)或在一台机器上跑两个训练进程;
  • 从 runs 卷上删除一个 checkpoint 或快照(容器里的 modal volume rm、shutil.rmtree),或在注册的 spec 里关掉一个完整权重试验的快照("snapshot_fractions": "none")。完整权重试验在其步数的 0.25、0.5 和 0.75 处保留快照(kev.experiment.SNAPSHOT_FRACTIONS),这样读数能在一次运行结束后找到它的最佳点:第 19 轮找不到,因为唯一的运行中状态是一个 resume 点,运行结束就被删了,而 AutoJev 的最佳 checkpoint 在 0.7 epoch。一个 27B 的快照每个试验约占 154 GB 卷;这个空间由 Jared 决定,不是会话。快照存在 runs 卷上(主);一个私有 Hub 镜像(plan 里的 snapshot_hub_repo,或 modal_app.py::mirror_snapshots)是值得保留的 checkpoint 的长期存储,不是替代品:镜像 27B checkpoint(每个约 51 GB,进一个私有仓库如 jaredpalmer/kev-snapshots)同样由 Jared 决定,且绝不进公开仓库。

如果一个臂被阻塞(认证、花费上限、30 分钟内做不成的部署),写下发生了什么,转到下一个臂。不要等人。

6. 韧性

  • 状态文件 runs/<session>-state.json(runs/ 被 gitignore;在研究分支上 git add -f 它):基线与授权、带 UTC 时间的花费读数、每个 study 及其 spawn id、上界与状态、已启动和已拉取的读数、候选、PR、待定决定。每次启动、拉取和读取后都更新它,并随 PLAN 小节一起提交。
  • 分离的任务。 Study 在已部署的应用上生成,能在本地客户端消失后存活;study 之后的一个本地错误可能仍生成了试验,所以重新启动前先跑 modal container list,绝不在同一 study 名下重启。探测和 benchmark 用 --detach 跑。
  • 观察者是本地进程,随机器或网络一起死。在 nohup 和 caffeinate 下跑它们;任何中断后都重启 watch(它从状态恢复)。它自己重试 DNS 和连接错误;试验自身的异常是失败并被报告。
  • 超时的完整权重试验由观察者续跑,不由 Modal。 试验生成时关闭 Modal 的重试;当一个完整权重试验的调用因超时结束时,watch 跑 modal_app.py::resume --trial <label>,它会生成下一次尝试(从最后提交的 resume 点继续),用该 study 被准入时所用的 GPU 和超时,并记进 runs/<study>.spawn.json(attempts,每个试验最多 1 + kev.budget.FULL_FT_RETRIES,即准入上界计算时用的计数;当前调用仍在运行的试验绝不被续跑)。观察者停着的时候什么都不续跑:重启它,它就会捡起超时。原因:Modal 对每次超时的尝试收两次费(超时一次,然后 30 s 后它杀掉的任务再一次),所以 Retries(2) 给了第 22 轮试验三次尝试中的两次,而那次杀掉的 retry 可能在一个正在运行的尝试旁边启动(scripts/modal_retry_probe.py)。在 ledger 之前生成的 study 没有计数:resume --trial <label> --beyond-bound 手工续跑它,在任何上界之外,并说明这一点。两次尝试绝不共享一个试验:每次在其生成前就记为 pending,各自在 kev-leases 卷上持有一个租约(每分钟心跳);在新尝试的租约新鲜时它会拒绝,而一次续跑在被杀尝试最后一次心跳后最多等 kev.budget.LEASE_STALE(15 分钟)才生成。
  • 网络中断杀死本地客户端,不杀远程工作:客户端死掉的读数通常已在 Modal 上完成;从卷上拉它的目录(modal volume get kev-runs /<name> runs/<name>),而不是重启它。
  • 失败的 benchmark 或探测会在卷上留下它的目录;用新名字重试。

7. 汇报

会话结束时(并在进行中写入状态文件):

  • 每轮的 PLAN.md 小节带着它的注册、读数表、确认结果和判决,无论正负,附报告路径。
  • 更新 PLAN.md 的「Where we stand」(已发布和已确认的候选、运行中的任务、花费)和「What we have learned」(若有发现改变);把每一轮加进 Record 表。
  • PLAN.md 里的一份会话总结:花费(基线、最终读数、运行中的上界)、Modal 上待处理的内容及完成它的确切命令、事故,以及至多三步下一步及其证据。
  • 每个数字都带 checkpoint、套件和划分、n 和报告路径;提交数字来源的读数和判决(.gitignore 保留报告,不保留预测转储;为新读数目录加一条规则)。
  • 时钟戳:注册和结果时间就是提交时间。不要在事情发生之前把时间写进标题;night 3 的草稿本这么做了,它的戳就没法用。

8. 已知坑

  • Modal 应用创建速率限制。 一分钟内超过约三次分离的 modal run 会以「App create rate limit exceeded」失败,什么都不跑。kev.rounds 把启动错开 60 s,并把一个臂的读数批成一次调用;手工也照做。
  • benchmark 任务里的 repo@sha 过去会移动 run@suite@name@flags 的每个字段;modal_app.parse_jobs 现在从右边解析,所以钉住的 Hub revision 是安全的。套件名和名称不得包含 @ 或 ,。
  • 每个 study 一次拉取。 同一 study 的并发拉取会删掉彼此的试验目录;pull_study 现在持有每个 study 的锁。试验仍在跑时拉取是安全的,只刷新未完成的试验。
  • 拉取会把完整权重留在卷上。 ::pull(以及 watch)跳过完整权重分片(model*.safetensors,每个 27B checkpoint 或快照约 51 GB)和 resume 点;其余全都拉下来(结果、行、head.pt、配置)。在卷上读一个 checkpoint 或快照:::benchmarks --jobs "/runs/<study>/<trial>/snapshots/step-<N>/checkpoint@<suite>@<name>"。::pull --weights 在本地确实需要它们时复制分片。
  • 数据之后部署。 镜像复制 evals/;启动器只检查 kev/*.py 的 hash,所以数据文件在部署之后才加入的试验会在容器里失败。study 上的 --gpu H200 需要一个用 KEV_GPU=H200 部署的应用。
  • 27B。 仅 H200(bf16 主干,常驻 55 GB);study 超时最高 28,800 s(lr 2e-5 的 1-epoch 技能增量每个优化器步约 8.8 s);fp32 读取约为 9B 的三倍(spec read_timeout: {"27b": 14400});锁定读数在 H200 上需要 --timeout 14400 --memory-mb 131072(spec locked_args)(GPU 来自 spec 的 gpu / 已部署的应用,或手工 --gpu H200)。每个 bf16 权重试验都过不了试验内的 isolation_and_packing 关卡(一个 fp32 检查);从行里读结果,并单独在 bf16 下测量服务隔离。
  • locked_test 命名。 当试验内筛选关卡失败时,工具要求 -ungated 后缀(kev-4b-r8-ungated);判决仍遵循注册的规则。
  • 每个套件的读取超时。 modal_app.READ_TIMEOUTS 把长 state 面板设为 7,200 s、文档 5,400 s、transfer-v9 3,600 s,其余 1,800 s。一个全局 --timeout 会抬高批里每个任务的准入上界。
  • 预算准入。 超过其 --budget 的启动在任何东西运行之前就退出;用至少是打印出的上界的预算重启。
  • 外部服务器是单飞的。 AutoJev 的服务器一次只答一个请求(忙时 HTTP 529);在长 kev.benchmark --remote 之前先探测一个外部端点,并把 --remote-concurrency 设成它能承受的值。把它拒绝的请求(例如超出其上下文的 422)计为覆盖率,绝不静默丢弃。
  • 温度:发布的 vs 试验内的。 一次试验的 result.json 及其锁定摘要按试验内拟合打分;一次发布提供 scripts/calibrate_checkpoint.py 写进 head.pt 的那个 T。第一次 AutoJev 正面交锋用试验内的 1.19 而非发布的 1.38 服务 Kev-27B,不得不更正。说明每个数字用哪个 T,以及它在哪里拟合(第 3 节规则 4:留出数据集,绝不是训练语料自己的划分)。
  • 长上下文校准。 一个带 "by_length": true 的面板按 state-token 桶报告准确率、ECE、Brier 和自信错误(8k 以下到 64k+,以及 8k+/16k+/32k+ 尾部),一条判据可以卡其中一个(long.ece_16k_plus.candidate <= 0.05)。Tokens 从读数的套件记录里计,所以两边共享桶。
  • 不加新套件版本的套件修复。 一个面板可以在两边删掉来源、任务或一份(私有、按 hash 注册的)id 或来源列表(exclude_sources、exclude_tasks、exclude_file),而一个仅报告的面板标为 "optional": true,这样缺一份报告读数永远不会让候选不完整。在任何读数之前按标签有效性选择排除项(第 24 轮取自 2026-09-27 的审计),绝不提交私有列表。
  • 工作区容量。 工作区一次最多跑过约十个 GPU 容器;挂起的容器是容量,不是 bug,所以不要重启它们。
  • 小套件。 对 89 或 144 个问题的关卡无法分辨 2-3 pp 的下限;通过一个汇合面板来卡它们。
  • Jev 读数在运行中失败,遇到网关 503;在新名字下重跑整个读数,而不是拼接部分行。
  • 软目标数据。 写构建器时,用眼睛检查几行记录:target 之和为 1,且标签的质量至少 0.5,除非该记录不可知(kev.data.none_pair 曾在软目标上训练零质量,已在 #60 修复)。
  • 把 modal run ...::study 的输出重定向到日志文件;过滤器可能藏起解释为什么什么都没启动的 SystemExit。