Generate the calibration scripts instead of keeping one by hand - #38
Conversation
slurm/calibrate.sh was hardcoded to fever: the task, the twenty-task cap and the intrinsicmemory-fever arm were all written into the file. Calibrating any other dataset meant editing three places and remembering the fourth - pddl serves at --max-num-seqs 256 rather than 512, which the generator already knew and the hand-written script did not. So it goes the way the sweep scripts went in 8c232f6: `--calibrate` renders slurm/<task>_calibrate.sh for every task, or for the ones named by `--task`, and the file itself stops being tracked. The sweep scripts it generates are byte-identical to before. `#SBATCH --account` comes from SLURM_ACCOUNT at generation time rather than being committed, since it is one user's allocation. It was being added by hand to the working copy of every script, which is a local edit that survives no checkout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ault
Every generated script inherited --max_tokens 512 from the parser, and gpt-oss
reasons before it answers with both coming out of that one budget. Its reasoning
used all 512 and the answer never started, which the LLM layer reports as
"returned no answer after 5 attempts".
That did not fall evenly. It cost the arms that write memory with an LLM call,
because those make a summariser call whose prompt carries the whole trajectory,
and left the arms that write no memory alone. Over twenty tasks of the September
calibration, LLMCallFailed per arm:
empty, memorybank, generative, metagpt 0 0 0 0
chatdev, voyager 0 1 0 0-1
g-memory 3 5 10 3
intrinsicmemory-<task> 4 16 17 18
fever hqa sciw pddl
intrinsicmemory-sciworld scored 3 tasks of 20 against six baselines on 20, and
intrinsicmemory-llm-structured-template scored none at all. The comparison the
sweep exists to make was being decided by a token ceiling, in the direction of
the treatment arms.
2048 is the budget the next calibration measures; the number belongs here rather
than in a flag someone has to remember, and it is emitted for the experiment and
crosstask scripts too so a sweep runs at the budget it was sized at.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jericho runs 100 trials rather than the 30 every other task uses, and its prompt tokens grow with the square of the budget - every turn re-sends the trajectory. By the curve measured in data/data.md, 76*n^2 + 6789*n, that is about 1.44M prompt tokens for one task at 100 trials, so twenty tasks over ten arms is ~288M tokens. At the 260k tokens/min the four September calibrations sustained, that is an eighteen-hour job in a two-hour allocation. What that produces is worse than nothing: killed at the walltime it would have written rows for the arms that finished and none for the rest, and an arm with no row is indistinguishable in the results table from an arm that failed. The last two fixes were both about exactly that - a partial, survivorship-biased sample read as a result. So Jericho calibrates on five tasks at 20 trials, ~8M tokens and half an hour, which answers the question a calibration of Jericho can answer: whether it runs at all. Sizing its real job is a different question and needs a job of its own, because no two-hour run can measure an eighteen-hour one. The override is per task and applies to the calibration only; the experiment and crosstask scripts still run the full budget. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12yuens2
left a comment
There was a problem hiding this comment.
let's move the calibration to a separate python script instead, so its easier and we can also set different options for the calibration if needed
There was a problem hiding this comment.
We should separate the calibrate generation to a separate python file to keep the two clean
There was a problem hiding this comment.
Done — slurm/generate_calibration.py is its own script now.
It holds what a calibration is: one seed, the twenty-task cap, the 2-hour window, Jericho's smaller shakedown and the summary the job prints. generate_slurm.py keeps the cluster, the model and the arms, and exposes job_script(...) for the serve-then-run body, so its render() is back to experiment-or-crosstask with no variant branch. job_script takes the per-variant pieces as keyword arguments, so setting different options for the calibration means editing only the calibration file.
uv run slurm/generate_calibration.py # every dataset
uv run slurm/generate_calibration.py --task fever pddlAlso merged main, which had resized the sweep per dataset in the same three places. The calibrations now inherit its serve-block changes and take --max_tokens from MAX_TOKENS_OVERRIDES rather than a fixed 2048 — calibrating a budget the real job will not use measures the wrong thing. The sweep scripts remain byte-identical to the ones main generates.
main resized the sweep per dataset while this branch was open, and the two sides changed the same three places in generate_slurm.py: the constants block, the head of render() and the tail of the run command. Resolved by taking both. The calibration scripts now inherit everything main added to the serve block - the batched-token and model-length overrides, prefix caching, OMP_NUM_THREADS, `uv run --no-sync` - and take their --max_tokens from MAX_TOKENS_OVERRIDES rather than the branch's single MAX_TOKENS, which this drops: a calibration measuring a budget the real job will not use measures the wrong thing. babyai and pddl calibrate at 4096. The sweep scripts are byte-identical to the ones main generates.
render() had grown a three-way branch over a `variant` string, and every caller-facing constant for two unrelated jobs sat in one file: the review asked for the calibration to be its own script so its options can move without touching the sweep. slurm/generate_calibration.py now holds what a calibration is - one seed, the twenty-task cap, the 2-hour window, Jericho's smaller shakedown and the summary it prints - and calls generate_slurm.job_script() for the serve-then-run body. generate_slurm.py keeps the cluster, the model and the arms, and its render() is back to experiment-or-crosstask. job_script() takes the per-variant pieces as keyword arguments rather than deriving them from a variant name, so a job type the generator does not know about needs no branch here. `every_arm`/`intrinsic_arms` replace the two places that spelled out the same arm list. Both generators take --task. The scripts either one writes are byte-identical to those the merged single generator wrote.
main turned the six crosstask jobs into one and split the generator into preamble() / run_command() / CLEANUP, where this branch had a job_script() taking the whole job as keyword arguments. Both sides rewrote the same function, so the merge takes main's decomposition and drops job_script: generate_calibration.py composes preamble + run_command + its summary + CLEANUP, and the calibration no longer needs a variant name anywhere. Two of main's pieces grew a parameter for it, each defaulting to what the sweep already passed: preamble() takes time_limit and db_dir, run_command() takes seeds and scope. --max_tokens comes from MAX_TOKENS_OVERRIDES either way, so a calibration measures the budget its real job will use. The calibrations pick up main's single MAX_NUM_SEQS of 256, since the per-task override it replaced is gone. DB_DIR now defaults to $HOME/GMemory/.db-calibration. It was $HOME/GMemory/.db/sweep/calibration, which main's move of the experiment default to .db-experiment left as the last thing under .db/sweep; the two families of run now sit side by side, and neither appends to the other's overall_results.csv. --task stays on generate_calibration.py, where it selects the dataset to calibrate, and is gone from generate_slurm.py: one crosstask.sh covers every dataset, so a partial sweep generation is no longer a thing to ask for. The sweep scripts are byte-identical to the ones main generates.
| write_script, | ||
| ) | ||
|
|
||
| SEEDS_CALIBRATED = SEEDS[:1] |
There was a problem hiding this comment.
we should make all these args with defaults so they can be changed easily when running the script
There was a problem hiding this comment.
Done — --seed, --max_tasks, --max_trials, --time_limit and --db_dir, each defaulting to what the constant held:
uv run slurm/generate_calibration.py --max_tasks 5 --time_limit 00:30:00
uv run slurm/generate_calibration.py --task jericho --max_trials 10--max_trials is new — it was only reachable before through the per-dataset table. A flag given on the command line beats that table, so --max_tasks 30 means 30 for Jericho too, rather than its entry holding it to 5.
Generating with no flags writes byte-identical scripts to the ones the constants wrote.
| TIME_LIMIT = "02:00:00" | ||
| DB_DIR = "$HOME/GMemory/.db-calibration" | ||
|
|
||
| # Tasks whose full budget will not calibrate inside that window, and the smaller |
There was a problem hiding this comment.
this is way too much info, you should /caveman just 1 or 2 lines here
There was a problem hiding this comment.
Cut to two:
# Jericho's prompt tokens grow with the square of its 100-trial budget, so 20
# tasks would be ~288M tokens - an 18-hour job. Five at 20 trials is ~8M.
OVERRIDES = {"jericho": {"max_tasks": 5, "max_trials": 20}}The curve it was quoting is in data/data.md, and the rest was reasoning about the 2-hour window that nobody reading the constant needs.
…mment The seed, the task cap, the wall clock and the results directory were module constants, so resizing a calibration meant editing the generator and leaving the edit in the working copy. They are argparse defaults now, plus a --max_trials that had only ever been reachable through the per-dataset table. A flag given on the command line beats that table, so --max_tasks 30 means 30 for Jericho too, where its entry would otherwise hold it to 5. Generating with no flags writes byte-identical scripts to the ones the constants wrote. The nine-line note on Jericho's token arithmetic is two: the curve it cites lives in data/data.md, and the rest was reasoning about a window nobody reading the constant needs.
Why
slurm/calibrate.shwas hardcoded to fever — the task, the twenty-task cap and theintrinsicmemory-feverarm were all written into the file. Calibrating another dataset meant editing three places and remembering a fourth: pddl serves at--max-num-seqs 256, not 512, whichgenerate_slurm.pyalready knew and the hand-written script did not. Since thenmainhas grown more of the same per-dataset knowledge — the batched-token and model-length overrides, prefix caching,--max_tokensper task — none of which a hand-written script picks up.What
slurm/generate_calibration.pyrendersslurm/<task>_calibrate.sh, for every task or for the ones--tasknames, andslurm/calibrate.shstops being tracked:uv run slurm/generate_calibration.py # every dataset uv run slurm/generate_calibration.py --task fever pddlIt holds what a calibration is: one seed,
--max_tasks 20at the full trial budget, every arm, a 2-hour allocation, Jericho's smaller shakedown, and the result table / tokens-per-task / failed-tasks summary the job prints afterwards. They default to$HOME/GMemory/.db/sweep/calibration, so calibrating several datasets fills one table.The cluster, the model and the arms stay in
generate_slurm.py, which exposesjob_script(...)for the serve-then-run body. Itsrender()is experiment-or-crosstask again, and the per-variant pieces are keyword arguments rather than avariantstring a branch switches on — so the calibration's options move without touching the sweep. Both generators take--task.#SBATCH --accountnow comes fromSLURM_ACCOUNTat generation time rather than being committed, since it is one user's allocation — it was being added by hand to the working copy of every script, which is a local edit that survives no checkout.Unchanged
The sweep scripts this generates are byte-identical to the ones
maingenerates — verified by generating both sides and diffing all twelve.🤖 Generated with Claude Code