fix: reuse the auto-build forward pass in evaluate - #96
Merged
Conversation
`evaluate()` builds an unbuilt program by calling it on the first batch, then drops those predictions and has `test_on_batch` compute them again. Every sample in that batch is therefore predicted twice. For a deterministic program this is a wasted pass. For an agent program it is a second full agent run — every LM call, tool call and sandbox turn paid twice for a result that is discarded. It is also permanent rather than a first-call toll. `built` is only set by calling a module, so a module on a path the program does not take (an agent's fallback generator that only runs when the loop ends without submitting, a tool the model happens not to call) never becomes built, `all(module.built ...)` never becomes true, and *every* `evaluate` pays it. Measured on a 30-task ARC-AGI-2 benchmark: program invocations came to `test_items + 1` for every task, roughly doubling wall-clock and tokens on the single-test-item tasks that are the majority. `_auto_build` now returns the predictions it computed, and `evaluate` hands them to the first `test_on_batch` instead of recomputing them. The build batch is the first batch scored — the iterator is unshuffled and rewound — so they are the same predictions either way. `test_on_batch` takes an optional `y_pred` for this; nothing else changes, and no path skips prediction that did not already have one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The problem
evaluate()builds an unbuilt program by calling it on the first batch, then throws those predictions away and letstest_on_batchcompute them again:Every sample in that first batch is predicted twice. For a deterministic program that's a wasted pass; for an agent program it's a second full agent run — every LM call, tool call and sandbox turn paid twice for a discarded result.
Why it's permanent, not a first-call toll
builtis only ever set byModule.__call__. A module on a path the program doesn't take never gets called, so it never becomes built, soall(module.built ...)never becomes true — and everyevaluatepays the extra pass, forever.Agent programs are full of such modules: a fallback generator that only runs when the loop ends without submitting, a tool the model happens not to call. The better the agent behaves, the more reliably the harness pays double.
Measured on a 30-task ARC-AGI-2 benchmark, top-level program invocations came to exactly
test_items + 1for every task:Most tasks have a single test item, so that is close to a 2x on wall-clock and tokens. In the traces you can watch one agent submit, the program return, and a fresh top-level run start 17ms later.
The fix
_auto_buildreturns the predictions it computed;evaluatehands them to the firsttest_on_batchinstead of recomputing them. The build batch is the first batch scored — the iterator is unshuffled and rewound — so they are the same predictions either way.test_on_batchgains an optionaly_predfor this. Nothing else changes, and no path that previously predicted now skips prediction.Test
test_evaluate_predicts_each_sample_once_when_unbuiltbuilds a program holding a module itscall()never reaches, then asserts each sample is predicted once. Forward passes for 10 samples:evaluate, 10 on every one afterIt also asserts the second
evaluatecosts exactly one pass per sample while the program is still unbuilt, which is the part that makes the waste recurring.The residual
+1on the first evaluate is_auto_build's own spec pass for metric and reward state. It is paid once per program rather than once per sample, and is left alone here.Verification
Full suite: 2171 passed, 36 skipped, 69 subtests (
synalinks/src, sandboxes included).🤖 Generated with Claude Code