無人で走る研究セッションの運用
これは、人が見ていない間に走る研究セッション(夜通しのセッション、または日中のある長い引き継ぎ)のための運用プログラムです。夜ごとのプロンプト(ラウンド 6 と night-3 のプログラム、git タグ research-archive-2026-09-24 の docs/prompts/ に保持)を置き換え、それらの夜にうまくいかなかったことを取り込みます。適用する研究ルールは PLAN.md の「Standing rules for every round」にあり、コマンドは AGENTS.md にあります。
セッションは登録済みルールの下で山登りと確認を行い、Jared が行動できる記録を残します。公開はしません。
1. 開始
最初の 30 分は、起動ではなく読むことに使ってください。
AGENTS.mdのすべて(コマンド、凍結スイート、正典の置き場所、Modal 設定)。PLAN.md: 現在地、学んだこと、常設ルール、データポリシー、Next。土台にしたい発見については、アーカイブでその根拠を読んでください:git show research-archive-2026-09-24:PLAN.md。.agents/skills/のスキル:kev-modal-study(GPU 作業の起動、監視、取り込み。その Gotchas を読む)、kev-verify(コード変更に回帰がないことを証明する)、kev-pr-description(PR の前に)、thermonuclear-code-review。kev/rounds.py(その docstring が spec スキーマです)、experiments/rounds/の最も近い過去の spec、そしてkev/autoresearch.py(session)。- 引き継ぎそのもの: 認可(Modal のドル、AI Gateway のドル)、範囲に入るもの、Jared が必要なもの。
そしてセットアップします:
- 研究ブランチの worktree で作業します(
git worktree add -b research/<session> /tmp/kev-<session> origin/main)。コミットごとに push して、マシンがスリープしても何も失わないようにしてください。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 の admission bound は起動時に表示され、
runs/<study>.spawn.jsonに保存されます。benchmark 呼び出しの bound はcompute_bound(gpu, timeout, trials)(kev/budget.py)です。spec の studybudgetは少なくともその bound でなければなりません(kev.rounds validateが確認します。modal_app.admit_studyは予算を超えた study を、何かが走る前に拒否し、study は $250 と 28,800 秒で上限されます)。 - どのフェーズも計画に入れない予備(認可額の約 10 %)を取っておいてください: 請求の読みは遅れて改訂され、admission bound は読みをひどく過大評価します(読みのバッチは最も遅いジョブの timeout を運びます)。
- すべての読みをその UTC 時刻とともに状態ファイルに記録してください。AI Gateway の支出(Jev 参照読み、ラベル判定)は独自の上限を持ち、それを支出するスクリプトが強制し、
runs/<name>/usage.jsonに記録されます。 - Modal ワークスペースの支出上限はダッシュボードからしか上げられません。それに当たると、訓練中に走っているコンテナが殺されます。
3. ラウンドを登録する
ラウンドは PLAN.md の節と spec で、訓練や読みの前に一緒にコミットします。
- PLAN.md の節を書きます: なぜ(測ったギャップとその根拠)、データ(先に凍結、manifest 付き)、アーム、ルール(primary、各スイートに合わせたしきい値の guard、rank)、確認段階、予算。常設ルールを使ってください。1 つのラウンドのために新しい統計を発明しないでください。
- 最も近い過去の spec をコピーして
experiments/rounds/r<N>.jsonを書きます(joint delta なら r15、27B なら r17、スキルのラウンドなら r10、訓練を伴わない事後アーム〔温度プール、補間チェックポイント〕なら r20、別のチェックポイントへ寄せるブレンドなら r23。そのアームは両端の訓練についてtrained_onを名指しします)。"archive"は除いてください。そのキーは記録済みのラウンド 5-18 を印付けます。spec が名指しするすべての計画ファイルと、そのルールが必要とするすべての親読みは、このチェックアウトに存在しなければなりません。親に読みがなければ、launch-reads <spec> --parentsが作ります。削除されたスイート(kev.suite.REMOVED_SUITES、理由付き)の読みはすべて外してください:evals/external/scienthoon-v1は 2026-09-27 に削除されたので、ラウンド 23 以降は scienthoon の読み、パネル、guard が外れます。evals/external/wanli-v2とtypesafe-v1は 2026-09-30 に削除されたので、ラウンド 27 以降はそれらの読みも外れ、SemIf が残る唯一の外部読みです(報告のみ)。プールされた外部群はゲートではありません: ラウンド 23-26 が従うラウンド 24 の監査済みルールは、SemIf、WANLI-v2、TypeSafe を任意パネルとして報告しました。validateとlaunchは、スイートをなお名指しする最後のラウンドより後のラウンドを拒否します。 - 新しいデータは
evals/の下の新しいディレクトリで、manifest.json(ファイルごとの sha256、入力のハッシュ)を伴います。SFT データポリシー(PLAN.md)の下では、非公開コーパスは manifest だけを git に保ち、非公開データセットを指す"mirror"エントリを付けます。 - 必須: 提供または出荷される温度は、すべてホールドアウトデータセットのプールから来なければならず、決して訓練コーパスの分割から来てはなりません。 較正(ECE、Brier、確信ある誤り、被覆)を読むラウンドは、そのアームに
temperatureプールを登録し(r20 をコピー: transfer-r3 較正分割の 8 つのホールドアウト公開ソース + transfer-v9 MMLU-Pro)、リリースはscripts/calibrate_checkpoint.pyが同じプールでフィットした温度を出荷します。訓練ソースのホールドアウト項目(訓練スイートのcalibration/development分割)は分布内です: ラウンド 19 は SFT アームをsft-v1開発行でフィットした T 0.955 で提供し、すべての較正基準を落としました(breadth-v1 ECE 0.059)。ラウンド 20 のホールドアウトデータセットのプールは同じチェックポイントで 0.0085 を与えました。何が強制するか:- ラウンド 21 以降、
kev.rounds validateとlaunchは、ルールまたは確認に温度が動かす基準(ECE、Brier、NLL、確信ある誤り、被覆。accuracy 以外)があり、temperatureプールがないラウンドを拒否します。ラウンド <= 20 は、訓練コーパスで訓練したアームごとに!!! warningを 1 つ出すだけなので、記録済み spec はなお検証を通ります。 kev.rounds validateは、次のプール読みを拒否します: (a) アームの訓練スイート、その構成要素(sft-v1 のinputs.components)またはその計画のdataスイートである、(b) いずれかのアームが訓練したソースをプールする、(c) いずれかの訓練コーパスのcalibrationまたはdevelopment分割を読む。また、確認できないプール(訓練が不明なアーム、manifest または列挙ソースのないスイート)も拒否します。トライアルのないチェックポイントアームは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)、validateは、訓練コーパスの行で両者が 0.05 超異なるときに警告します。 - 非交差性のチェックはソース名前(名目的で、意味的ではない)で行います: 同じデータセットを別の名前で運ぶ 2 つのスイートは通ります。したがってプールは、構成上 Kev で eval 専用であるソースを使わなければなりません。transfer-r3 の 8 つのホールドアウト公開ソースや 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」)。
- ラウンド 21 以降、
uv run python -m kev.rounds validate experiments/rounds/r<N>.json(分割を検証するには--partitionsを追加)がokを印字するまで。PLAN の節と spec を 1 つのコミットでコミットし、push します。そのコミット時刻が登録時刻です。
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 の最初の 5 分で、
modal container logs <id>で 1 分あたりのオプティマイザステップを数え、timeout に対して実時間を見積もってください(ep0 step N/M: M は全エポックにわたります)。タイムアウトしたコンテナは何も保存しません。キャンセルし(FunctionCall.from_id(cid).cancel())、より少ないレコードかより長い timeout で新しい study 名の下に再起動してください。 watchは生成されたトライアルをポーリングし、終わった study をそれぞれ取り込み(同時に study あたり 1 取り込み)、そのアームの読みを一度起動し(アームあたり 1 バッチの::benchmarks呼び出し、60 秒間隔)、それらを待ち、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)。ロック済み読みの前にテストパネルを。読みは各 1 回、例外なし。 - 上限の下で複数の登録済みラウンドを順に:
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はトライアルを transfer 精度で参照とペアにし、release-check --study <name>はその study のすべての config を審査します(各 config は、そのすべてのシードがゲートを通ったときのみ通ります)。
5. セッションが触れてよいものとよくないもの
よい: spec、計画、PLAN.md の節を書く。新しいディレクトリの下に新しい凍結データを作る。modal_app.py を通じて study と読みを起動する。スクリプトと modal_app.py のインフラ定数を変える。main に属するコードの PR を開く。
Jared の明示的な OK なしには、よくない:
- Hub 上の何かを公開または変更する(
kev.publish、hf upload、hf repos tag、scripts/publish_space.sh、リリース済みのhead.pt)、非公開リポジトリを公開にする、公開エンドポイントをデプロイする。 - main にコミットする、force-push する、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 はこれらのモデルを保てません)、または 1 つのマシンで 2 つの訓練プロセスを走らせる。
- runs ボリュームからチェックポイントまたはスナップショットを削除する(
modal volume rm、コンテナ内のshutil.rmtree)、または登録済み spec で完全重みトライアルのスナップショットを切る("snapshot_fractions": "none")。完全重みトライアルは、ステップの 0.25、0.5、0.75 でスナップショットを保つ(kev.experiment.SNAPSHOT_FRACTIONS)ので、読みは実行が終わった後でその最良点を見つけられます。ラウンド 19 はそれができませんでした。唯一の実行中状態が再開点で、実行が終わると削除され、AutoJev の最良チェックポイントは 0.7 エポックにあったからです。27B のスナップショットは 1 トライアルあたり約 154 GB のボリュームです。容量はセッションではなく Jared の判断です。スナップショットは runs ボリューム(primary)に住み、非公開 Hub ミラー(計画のsnapshot_hub_repo、またはmodal_app.py::mirror_snapshots)は、保つ価値のあるチェックポイントの長期保管であり、置き換えではありません: 27B チェックポイントのミラー(各約 51 GB、jaredpalmer/kev-snapshotsのような非公開リポジトリへ)も Jared の判断で、決して公開リポジトリへは行いません。
アームがブロックされたら(認証、支出上限、30 分で動かないデプロイ)、何が起きたかを書き、次のアームに移ってください。人を待たないでください。
6. 回復力
- 状態ファイル
runs/<session>-state.json(runs/ は gitignore されています。研究ブランチでgit add -fしてください): ベースラインと認可額、UTC 時刻付きの支出読み、spawn id・bound・状態付きのすべての study、起動して取り込んだ読み、候補、PR、保留中の決定。起動、取り込み、読みのたびに更新し、PLAN の節と一緒にコミットしてください。 - 切り離されたジョブ。 study はデプロイ済みアプリ上で spawn し、ローカルクライアントを超えて生き残ります。
studyの後のローカルエラーでもトライアルは spawn されたかもしれません。だから再起動の前にmodal container listを実行し、同じ study 名の下で決して再起動しないでください。probe と benchmark は--detachで走ります。 - watcher はローカルプロセスで、マシンかネットワークとともに死にます。
nohupとcaffeinateの下で走らせてください。中断後はwatchを再起動してください(状態から再開します)。DNS と接続エラーは自分で再試行します。トライアル自身の例外は失敗で、報告されます。 - タイムアウトした完全重みトライアルは、Modal ではなく watcher が継続します。 トライアルは Modal の retry を切って spawn します。完全重みトライアルの呼び出しがその timeout で終わると、
watchがmodal_app.py::resume --trial <label>を実行し、それが次の試行を spawn します(最後にコミットされた再開点から継続します)。GPU と timeout は study が認められたもので、runs/<study>.spawn.jsonに記録されます(attempts、トライアルあたり最大 1 +kev.budget.FULL_FT_RETRIES、admission bound が計算されたときの数)。現在の呼び出しがまだ走っているトライアルは決して継続されません。watcher が止まっている間は何も継続されません: 再起動すれば timeout を拾い上げます。なぜか: Modal はタイムアウトした各試行に二重に課金しました(timeout、そして 30 秒後に殺したタスク)。だからRetries(2)はラウンド 22 のトライアルに 3 回の試行のうち 2 回を与え、殺しの retry が走っている試行の隣で始まりえます(scripts/modal_retry_probe.py)。ledger より前に spawn された study は数を持ちません:resume --trial <label> --beyond-boundが手作業で、bound の外で継続し、そう言います。2 つの試行は決してトライアルを共有しません: それぞれが spawn 前に pending として記録され、kev-leasesボリュームにリースを保ちます(毎分ハートビート)。新しい試行は、他のリースが新しい間は拒否し、継続は、殺された試行の最後のハートビートの後、kev.budget.LEASE_STALE(15 分)まで待ってから spawn します。 - ネットワーク断 はローカルクライアントを殺し、リモート作業は殺しません: クライアントが死んだ読みはたいてい Modal 上で終わっています。再起動する代わりに、そのディレクトリをボリュームから取り込んでください(
modal volume get kev-runs /<name> runs/<name>)。 - 失敗した benchmark または probe はそのディレクトリをボリュームに残します。新しい名前で再試行してください。
7. 報告
セッションの終わりに(そして進行に応じて状態ファイルにも):
- 各ラウンドの PLAN.md 節は、その登録、読み出し表、確認結果、判定を、否定的であっても、レポートパスとともに運びます。
- PLAN.md の「Where we stand」(リリース済み・確認済みの候補、走っているジョブ、支出)と、発見が変わったなら「What we have learned」を更新し、各ラウンドを Record 表に加えます。
- PLAN.md にセッション要約: 支出(ベースライン、最終読み、走っている bound)、Modal 上で保留中のものとそれを終える正確なコマンド、インシデント、そして最大 3 つの次のステップとその根拠。
- すべての数値はチェックポイント、スイート、分割、n、レポートパスを運びます。数値の出所である読み出しと判定をコミットしてください(
.gitignoreはレポートを保ち、予測ダンプは保ちません。新しい読み出しディレクトリにはルールを追加してください)。 - 時刻スタンプ: 登録時刻と結果時刻はコミット時刻です。起きる前に時刻を見出しに書かないでください。night 3 のスクラッチパッドはそうしてしまい、そのスタンプは使えませんでした。
8. 既知の落とし穴
- Modal の app-create レート制限。 1 分以内に約 3 つを超える切り離された
modal runは「App create rate limit exceeded」で失敗し、何も走りません。kev.roundsは起動を 60 秒間隔にずらし、アームの読みを 1 呼び出しにまとめます。手作業でも同じにしてください。 - benchmark ジョブの
repo@shaは以前、run@suite@name@flagsのすべてのフィールドをずらしていました。modal_app.parse_jobsは今、右から解析するので、固定した Hub リビジョンは安全です。スイートと名前は@や,を含んではなりません。 - study あたり 1 取り込み。 同じ study の同時取り込みは互いのトライアルディレクトリを削除していました。
pull_studyは今、study ごとのロックを保ちます。トライアルがまだ走っている間の取り込みは安全で、未完了のトライアルだけを更新します。 - 取り込みは完全重みをボリュームに残します。
::pull(とwatch)は完全重みシャード(model*.safetensors、27B チェックポイントまたはスナップショットあたり約 51 GB)と再開点をスキップします。それ以外はすべて降りてきます(結果、行、head.pt、config)。ボリューム上のチェックポイントまたはスナップショットを読むには:::benchmarks --jobs "/runs/<study>/<trial>/snapshots/step-<N>/checkpoint@<suite>@<name>"。ローカルで本当に必要なときは::pull --weightsがシャードをコピーします。 - データの後にデプロイ。 イメージは
evals/をコピーします。ランチャーはkev/*.pyのハッシュしか確認しないので、デプロイ後にデータファイルが追加されたトライアルはコンテナ内で失敗します。studyの--gpu H200にはKEV_GPU=H200でデプロイされたアプリが必要です。 - 27B。 H200 のみ(bf16 バックボーン、常駐 55 GB)。study の timeout は最大 28,800 秒(lr 2e-5 の 1 エポックのスキル delta は 1 オプティマイザステップあたり約 8.8 秒で走りました)。fp32 の読みは 9B の約 3 倍です(spec
read_timeout: {"27b": 14400})。ロック済み読みは H200 で--timeout 14400 --memory-mb 131072(speclocked_args)が必要です(GPU は spec のgpu/ デプロイ済みアプリから来るか、手作業で--gpu H200)。bf16 重みのすべてのトライアルはトライアル内のisolation_and_packingゲート(fp32 チェック)を落とします。結果は行から読み、bf16 での提供時の独立性は別に測ってください。 locked_testの命名。 トライアル内のスクリーニングゲートが失敗したとき、ツールは-ungated接尾辞を要求します(kev-4b-r8-ungated)。判定はなお登録済みルールに従います。- スイートごとの読みの timeout。
modal_app.READ_TIMEOUTSは長い状態のパネルを 7,200 秒、文書を 5,400 秒、transfer-v9 を 3,600 秒、それ以外を 1,800 秒に設定します。1 つのグローバルな--timeoutはバッチ内のすべてのジョブの admission bound を膨らませます。 - 予算 admission。
--budgetを超える起動は、何かが走る前に終了します。印字された bound 以上の予算で再起動してください。 - 外部サーバーは single-flight です。 AutoJev のサーバーは一度に 1 リクエストに答えました(忙しいときは HTTP 529)。長い
kev.benchmark --remoteの前に外部エンドポイントを probe し、--remote-concurrencyをそれが耐えられる値に設定してください。それが拒否するリクエスト(例えば文脈を超えた 422)を被覆として数え、決して無言で捨てないでください。 - 温度: 出荷済みかトライアル内か。 トライアルの
result.jsonとそのロック済み要約はトライアル内のフィットで採点されます。リリースはscripts/calibrate_checkpoint.pyがhead.ptに書いた T を出荷します。最初の AutoJev の直接対決は Kev-27B を出荷済みの 1.38 ではなくトライアル内の 1.19 で提供し、訂正が必要でした。すべての数値がどの T を使い、どこでフィットされたかを言ってください(セクション 3、ルール 4: ホールドアウトデータセット、決して訓練コーパス自身の分割ではない)。 - 長文脈の較正。
"by_length": trueのパネルは、状態トークンのバケットごと(8k 未満から 64k+ まで、そして 8k+/16k+/32k+ の裾)に精度、ECE、Brier、確信ある誤りを報告し、基準は 1 つをゲートできます(long.ece_16k_plus.candidate <= 0.05)。トークンは読みのスイートレコードから数えられるので、両側がバケットを共有します。 - 新しいスイートバージョンなしのスイート修正。 パネルはソース、タスク、または(非公開の、ハッシュ登録済みの)id かソースのリストを両側で外せます(
exclude_sources、exclude_tasks、exclude_file)。報告専用のパネルは"optional": trueと印付けられるので、欠けた報告読みが候補を不完全にすることは決してありません。除外はその下で読み出す前にラベルの妥当性で選び(ラウンド 24 は 2026-09-27 の監査から取りました)、非公開リストを決してコミットしないでください。 - ワークスペース容量。 ワークスペースは最大で約 10 の GPU コンテナを同時に走らせたことがあります。pending のコンテナは容量であってバグではないので、再起動しないでください。
- 小さいスイート。 89 または 144 問の guard は 2-3 pp の床を解像できません。プールされたパネルを通じてゲートしてください。
- Jev の読みは実行中に失敗します(ゲートウェイの 503)。部分的な行をつなぎ合わせるのではなく、読み全体を新しい名前で再実行してください。
- ソフトターゲットデータ。 builder を書くときは、いくつかのレコードを目で確認してください:
targetは 1 に合計し、レコードが知りようのないものでない限りラベルの質量は少なくとも 0.5 です(kev.data.none_pairはかつてソフトターゲットでゼロ質量を訓練しました。これは #60 で修正)。 modal run ...::studyの出力はログファイルにリダイレクトしてください。フィルタが、なぜ何も起動しなかったかを説明するSystemExitを隠しえます。