Skip to content

Generate the calibration scripts instead of keeping one by hand - #38

Merged
12yuens2 merged 7 commits into
mainfrom
calibration-slurm-scripts
Sep 7, 2026
Merged

Generate the calibration scripts instead of keeping one by hand#38
12yuens2 merged 7 commits into
mainfrom
calibration-slurm-scripts

Conversation

@12yuens2

@12yuens2 12yuens2 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Why

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 another dataset meant editing three places and remembering a fourth: pddl serves at --max-num-seqs 256, not 512, which generate_slurm.py already knew and the hand-written script did not. Since then main has grown more of the same per-dataset knowledge — the batched-token and model-length overrides, prefix caching, --max_tokens per task — none of which a hand-written script picks up.

What

slurm/generate_calibration.py renders slurm/<task>_calibrate.sh, for every task or for the ones --task names, and slurm/calibrate.sh stops being tracked:

uv run slurm/generate_calibration.py                  # every dataset
uv run slurm/generate_calibration.py --task fever pddl

It holds what a calibration is: one seed, --max_tasks 20 at 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 exposes job_script(...) for the serve-then-run body. Its render() is experiment-or-crosstask again, and the per-variant pieces are keyword arguments rather than a variant string a branch switches on — so the calibration's options move without touching the sweep. Both generators take --task.

#SBATCH --account now 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.

Unchanged

The sweep scripts this generates are byte-identical to the ones main generates — verified by generating both sides and diffing all twelve.

🤖 Generated with Claude Code

12yuens2 and others added 3 commits September 3, 2026 19:17
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 12yuens2 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread slurm/generate_slurm.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should separate the calibrate generation to a separate python file to keep the two clean

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 pddl

Also 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.
Comment thread slurm/generate_calibration.py Outdated
write_script,
)

SEEDS_CALIBRATED = SEEDS[:1]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make all these args with defaults so they can be changed easily when running the script

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread slurm/generate_calibration.py Outdated
TIME_LIMIT = "02:00:00"
DB_DIR = "$HOME/GMemory/.db-calibration"

# Tasks whose full budget will not calibrate inside that window, and the smaller

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is way too much info, you should /caveman just 1 or 2 lines here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@12yuens2
12yuens2 merged commit e0d02f8 into main Sep 7, 2026
1 check passed
@12yuens2
12yuens2 deleted the calibration-slurm-scripts branch September 7, 2026 11:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant