Repository navigation
feat(sleep): reject candidates that regress on tonight's training tasks (gate_train_check) - #410
Open
Bogdan (Dan) Baciu (bogdanbaciu21) wants to merge 2 commits into
Conversation
…ks (gate_train_check) The nightly gate accepts a candidate when it scores higher than the current documents on the validation slice. That slice is a fraction of one night's tasks and can be a handful, so a rule that damages the agent can still come out ahead on it and be accepted. gate_train_check (default false) adds a condition to an accept. A candidate the validation gate would accept is also replayed on tonight's real training tasks (recalled tasks included, dream variants not). It is rejected when its mean gate score there is lower than the current documents' by more than gate_train_margin (default 0.1) and at least two more of those tasks got worse than got better, so one task flipping can never decide a night. These tasks are not held out: reflection drew on tonight's failures, so a higher score on them cannot rule out overfitting and is never used to accept anything. A clearly lower score, in particular breaking tasks the current documents already handle, is not explained by overfitting, so the comparison is used only to reject. Each candidate is compared with the documents it would replace: a skill candidate with the night's training replay, a memory candidate with the replay that follows an accepted skill. A task with a missing or malformed score counts as worse and a non-finite mean rejects. The check is skipped when the gate is off and when the validation slice is not disjoint from training. The added cost is one replay of the training tasks per candidate that passes validation. Both arms and every task-level change are kept in the gate records and a gate/train_check evidence event, and report.md shows the two means and the counts. These records exist only when the check is enabled, so a night without it writes what it always wrote. The flag must be a boolean and an invalid margin raises before consolidation makes any model call. The settings are forwarded at both nightly call sites, including per-skill fan-out groups.
skillopt_sleep/experiments/gate_study.py measures the nightly gate through the shipped dream_consolidate on a public benchmark (SearchQA or LiveMathematicianBench): fault injection of fixed rules, and the multi-night recipe with held-out scoring. Arms share model draws through a response store. A committed fixture and a deterministic stand-in model give an offline mode with no key and no network; tests/test_sleep_gate_study.py pins its results and the report arithmetic. Live runs need --live and print their expected call count first.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What Problem This Solves
The nightly gate accepts a candidate when it scores strictly higher than the current skill on tonight's validation tasks. In the documented deployment recipe that slice is one to seven tasks (mean 3.3) with one replay each, so a rule that damages the agent can still come out ahead on it.
I measured how often. Six fixed rules, each with a measured held-out effect, were proposed to the shipped nightly step in 100 nightly contexts (20 seeds, 5 nights) of the SearchQA recipe from
docs/sleep/RESULTS.md. Three of the rules cost between 16 and 49 held-out points. The shipped gate accepted them 24 times out of 300.Proof
All runs use public data and the shipped code path: the benchmark split materialized from the released ID manifest, the in-tree evaluator, and
dream_consolidatecalled as the nightly cycle calls it. The harness is in this PR asskillopt_sleep/experiments/gate_study.py; Reproduce It Yourself gives one command per result. Two scenarios were measured live: SearchQA with gpt-5.4-nano (sections 1 to 3) and the checks in section 4 on a second target model and a second benchmark. The feature-on arms of sections 1 and 2 ran on this branch before its rebase (tree84765e6; the rebase moved one CHANGELOG entry and changed nothing else). The feature-off arm of section 1 ran on an earlier head of this branch and the feature-off arm of section 2 on upstream mainccfdcdf; with the key unset both take the shipped path.Target gpt-5.4-nano, optimizer gpt-5.5, gate metric
mixed, seeds 101 to 120, held-out exact match on the first 400 test items. Arms share every model draw they have in common. Intervals are percentile bootstraps over seeds (10,000 resamples); the paired test is frompython -m skillopt_sleep.evalkit.1. Fault injection through the nightly step, feature off versus feature on. Each rule is proposed in place of the optimizer's reflection; the starting skill is the shipped SearchQA initial skill (held-out exact match 50.2); one rollout,
recall_k=10.Target calls per decision: 20.6 off, 21.8 on for the costly rules.
2. Paired end-to-end run with real evolution, feature off versus feature on. The full recipe (
nights=5,per_night=10,dream_rollouts=5,dream_factor=2,recall_k=10), optimizer proposals untouched.On the one seed that differs, the check blocked a first-night candidate that an audit scored at +13.5 held-out points. That seed accepted edits on nights 4 and 5 instead and finished at 66.8 against 63.8. This is the check's measured false-veto cost on real proposals: 1 of 33.
3. Supplementary: the decision recomputed on stored samples (independent draws, 18 seeds). This table comes from stored per-task scores of the same study and is not reproducible from this PR alone; it is included because it separates real optimizer candidates from constructed rules. For 89 of the 90 audited optimizer candidates (the remaining one, a first-night candidate that measured -6.2, fits neither real row) and for the six rules in 90 contexts, fresh validation and training samples were drawn under the current skill and the candidate, and the decision was computed with and without the check.
4. A second target model and a second benchmark (run on this head,
20d8114). The design for the second benchmark was fixed before its first call. The second-model run was added after the second benchmark's rule effects were seen, for the reason given below, and is labelled as such. Both are smaller than section 1.4a. Second target model, gpt-5.4-mini on SearchQA, fault injection with the same six rules. Rule effects on this model (starting skill 72.8 held out; H1 costs 26.2 points and H4 72.8) were measured earlier on the first 400 test items and are reused, not re-measured. I first ran 6 seeds (30 contexts). The shipped gate accepted only 2 costly proposals there, too few to tell anything, so I extended to 20 seeds (100 contexts). The extension, its size and its stopping point were written down before the added runs, but it was decided after looking at the 6-seed result, so the 20-seed column is not a pre-planned test and both are shown.
At 20 seeds the check removed all 8 costly accepts (4 of H1, 4 of H4; 6 of the 8 fell in the 14 added seeds) and the interval excludes zero. At the 6 seeds first run it did not. It also removed 8 of 45 near-null accepts, which is its cost on this model. Target calls per decision at 20 seeds: 20.6 off, 21.2 on for the costly rules.
4b. Second benchmark, LiveMathematicianBench with gpt-5.4-nano. Nightly pool: the 52 items of the released train and val manifests, five nights of ten; held out: the 125 test items; choices shuffled per item with the dataloader's rule. Six rules written before any call were measured first (starting skill 26.4 held out):
No rule costs more than 15 points here, so this benchmark cannot test whether the check rejects costly rules; that was the outcome I had registered as uninformative for the main claim. The target answers 26 percent on five-option questions, so a rule cannot both cost 15 points and beat the current skill on a validation slice, and I did not add stronger rules after seeing these effects. What the run does show is a cost: over the five near-null rules the check lowered acceptance from 65 to 54 of 300 (-3.7 points, -6.3 to -1.3; 11 versus 0 discordant), and it left the one moderately harmful rule untouched (13 to 13). Target calls per decision: 21.0 off, 23.8 on.
4c. Paired multi-night run with the optimizer proposing, LiveMathematicianBench (gpt-5.5 optimizer, 5 seeds, 5 nights, full recipe).
The check ran on every accepted candidate and blocked none, so the two arms are the same run. Three of five seeds accepted nothing. A counter-example belongs here: on seed 101, night 4, the gate accepted an edit after which held-out accuracy fell from 29.6 to 20.0. The check did not stop it. On that night the current skill solved 1 of the 18 training tasks and the candidate solved the same 1 (no task worse, none better), so there was no drop to see. The check detects damage as tasks the current skill handles and the candidate breaks; when the current skill handles almost none, it has nothing to measure. Seed 105 shows the two-condition rule from the other side: on night 1 the candidate's training mean fell from 0.14 to 0.00, beyond the margin, but only one task got worse, so it was not blocked, and held-out accuracy moved from 26.4 to 25.6.
Why This Change Was Made
gate_train_check(defaultfalse) adds a condition to an accept. A candidate the validation gate would accept is also replayed on tonight's real training tasks (recalled tasks included, dream variants not). It is rejected when both hold: its mean gate score there is lower than the current documents' by more thangate_train_margin(default0.1), and at least two more of those tasks got worse than got better.Design choices that matter for review:
report.mdare what they were: the new record keys are written only when the check is enabled.Non-goals: this does not make the gate accept more, does not replace held-out validation, and does not detect harm that tonight's training tasks cannot show.
Project Fit
User Impact
Opt-in and default-unchanged. A user who sets
"gate_train_check": truegets a nightly gate that, on the measured recipe, stopped accepting rules that cost 16 to 49 held-out points (24 of 300 down to 1 of 300) while leaving the real learning path unchanged on 19 of 20 seeds.report.mdshows the two means, the counts of tasks that got worse and better, and whether the candidate was rejected for it; the JSON outputs and the evidence log keep the per-task comparison.Academic Support
Testing
New tests in
tests/test_sleep_gate_train_check.py(62 cases):gate_no_regressionis not replayed.The existing fan-out forwarding test in
tests/test_sleep_engine.pyalso asserts the two settings.tests/test_sleep_gate_study.py(6 cases) runs the study harness offline on the committed fixture: the fixture's rule effects, the shipped gate accepting the harmful fixture rule in 1 of 16 contexts and the check in 0 of 16, the helpful rule accepted equally in both arms, the cost of the extra training replay, the refusal to call a model without--liveor--offline, and the report arithmetic on known rows.Line and branch coverage of the source lines this PR adds in
skillopt_sleep/consolidate.pyandskillopt_sleep/cycle.py, measured withpytest --cov --cov-branchon the two test files above plustests/test_sleep_engine.py: 85 of 85 lines and 38 of 38 branches.Complete suite, this branch at
20d81145f10d44c284e7889042a60502faf5ac6eand unmodified upstream main45f65caon the same runners. Each job checked out the exact commit and asserted its SHA. These are contributor-run results, not official CI.The branch passes exactly 68 more tests than main on every runner, which is the 62 and 6 new cases. The failing test ids are identical on the branch and on main: 2 on macOS (
test_concurrent_adoption_cleanly_refuses_one_writerandtest_separate_nights_share_the_same_live_target_lockintests/test_sleep_adopt_skill_subset.py) and 19 ids on Windows (24 failures counting subtests). None is unique to this branch and none touches code this change modifies; they are reported, not fixed, to keep the PR scoped.On Linux 3.12 locally:
mkdocs build --strictexits 0,ruff checkon the changed source and test files is clean, andgit diff --checkis clean. The live runs in Proof sections 1 to 3 were made before the rebase fromccfdcdfonto45f65ca; section 4 ran on the head above.Limitations & Negative Results
report.md.Reproduce It Yourself
Deterministic tests, and the study harness offline (no key, no network):
python -m pytest tests/test_sleep_gate_train_check.py tests/test_sleep_gate_study.py tests/test_sleep_engine.py -q python -m pytest -q python -m skillopt_sleep.experiments.gate_study effects --offline python -m skillopt_sleep.experiments.gate_study faults --offline --arm off python -m skillopt_sleep.experiments.gate_study faults --offline --arm check --feature '{"gate_train_check": true}' python -m skillopt_sleep.experiments.gate_study report --offlineLive numbers. They need the
searchqaextra, theopenaipackage and any OpenAI-compatible endpoint (OPENAI_API_KEY, optionalOPENAI_BASE_URL). Each command needs--liveand prints the number of model calls it expects before the first one. Every model call is kept in a local SQLite store, so runs resume and the second arm reuses the first arm's draws.Proof section 1 (fault injection, SearchQA):
Proof section 2 (multi-night run with the optimizer proposing):
python -m skillopt_sleep.experiments.gate_study nights --live --arm base python -m skillopt_sleep.experiments.gate_study nights --live --arm check --feature '{"gate_train_check": true}' python -m skillopt_sleep.experiments.gate_study reportProof section 4 (second target model, second benchmark):
On SearchQA,
effectsis 2,800 target calls andfaults --arm offabout 4,000 per 20 seeds, of about 1,400 input tokens each; the feature-on arm adds a few hundred. A provider that names models with a prefix takes--modeland--optimizeraccordingly.