Skip to content

Commit e0d02f8

Browse files
authored
Merge pull request #38 from alan-turing-institute/calibration-slurm-scripts
Generate the calibration scripts instead of keeping one by hand
2 parents 8b3a2dc + 3a8e00e commit e0d02f8

5 files changed

Lines changed: 209 additions & 122 deletions

File tree

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ data
2222
*.out
2323
tasks/tests/logs
2424

25-
# generated by slurm/generate_slurm.py - edit the generator, not these
25+
# generated by slurm/generate_slurm.py and slurm/generate_calibration.py -
26+
# edit the generators, not these
2627
slurm/*_experiment.sh
2728
slurm/crosstask.sh
29+
slurm/*_calibrate.sh

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,19 @@ TASK=alfworld VENV=~/alfworld-test-venv sbatch slurm/smoke_test.sh
8989
```
9090

9191
**Size the real jobs before submitting them**
92-
`slurm/calibrate.sh` sits between the smoke test and a 24-hour job: one dataset, every arm, one seed, twenty tasks at the full trial budget, in a 2-hour allocation. It prints the result table, the tokens per task and any failed tasks. That is what sizes the real jobs — every experiment in a job runs concurrently against one throughput-bound server, so the wall clock is total tokens divided by what the server sustains, and `tokens_per_task x episodes / throughput` is the estimate. Twenty tasks also takes `g-memory` past its twentieth, where `merge_insights` runs.
92+
`slurm/<task>_calibrate.sh` sits between the smoke test and a 24-hour job: one dataset, every arm, one seed, twenty tasks at the full trial budget, in a 2-hour allocation. `slurm/generate_calibration.py` generates them, out of the same cluster configuration and job pieces as the sweep:
93+
94+
```bash
95+
uv run slurm/generate_calibration.py # every dataset
96+
uv run slurm/generate_calibration.py --task fever pddl
97+
sbatch slurm/fever_calibrate.sh
98+
```
99+
100+
Every value a calibration is sized by is a flag with that default — `--seed`, `--max_tasks`, `--max_trials`, `--time_limit`, `--db_dir` — so resizing one takes no edit: `--max_tasks 5 --time_limit 00:30:00` for a quicker shakedown. A flag given on the command line also overrides the per-dataset `OVERRIDES` table, which is what holds Jericho to five tasks by default.
101+
102+
`SLURM_ACCOUNT=<account>` in front of either generator puts an `#SBATCH --account` line in whatever it writes; without it the scripts submit under your default account.
103+
104+
Calibrations default to `$HOME/GMemory/.db-calibration`, not the sweep's directory, so calibrating several datasets fills one table of its own. Each prints that table, the tokens per task and any failed tasks. That is what sizes the real jobs — every experiment in a job runs concurrently against one throughput-bound server, so the wall clock is total tokens divided by what the server sustains, and `tokens_per_task x episodes / throughput` is the estimate. Twenty tasks also takes `g-memory` past its twentieth, where `merge_insights` runs.
93105

94106
**Attach to an already-running vLLM/Ray cluster**
95107
`slurm/experiment.sh` doesn't start its own model server. It expects a vLLM/Ray serving job already running elsewhere on the cluster and resolves that job's head node from its Slurm job ID:

slurm/calibrate.sh

Lines changed: 0 additions & 99 deletions
This file was deleted.

slurm/generate_calibration.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
"""Generate the per-task slurm/*_calibrate.sh runs that size the sweep jobs.
3+
4+
One dataset, every arm, one seed, twenty tasks at the full trial budget, in a
5+
2-hour allocation. The result rows carry the token spend, so tokens / elapsed =
6+
throughput, and episodes x tokens-per-task / throughput = the wall clock a real
7+
job needs.
8+
9+
uv run slurm/generate_calibration.py # every dataset
10+
uv run slurm/generate_calibration.py --task fever pddl
11+
uv run slurm/generate_calibration.py --max_tasks 5 --time_limit 00:30:00
12+
13+
Every default below is a flag, so a calibration can be resized without editing
14+
the file. The cluster, the model and the arms are generate_slurm.py's.
15+
"""
16+
import argparse
17+
18+
from generate_slurm import (
19+
CLEANUP,
20+
SEEDS,
21+
TASKS,
22+
every_arm,
23+
preamble,
24+
run_command,
25+
write_script,
26+
)
27+
28+
DEFAULT_SEEDS = SEEDS[:1]
29+
DEFAULT_MAX_TASKS = 20
30+
DEFAULT_TIME_LIMIT = "02:00:00"
31+
DEFAULT_DB_DIR = "$HOME/GMemory/.db-calibration"
32+
33+
# Jericho's prompt tokens grow with the square of its 100-trial budget, so 20
34+
# tasks would be ~288M tokens - an 18-hour job. Five at 20 trials is ~8M.
35+
OVERRIDES = {"jericho": {"max_tasks": 5, "max_trials": 20}}
36+
37+
SUMMARY = """
38+
39+
echo "==== calibration ===="
40+
column -s, -t < ${DB_DIR}/overall_results.csv
41+
42+
python3 -c '
43+
import csv, sys
44+
rows = list(csv.DictReader(open(sys.argv[1])))
45+
tokens = sum(int(r["completion_tokens"]) + int(r["prompt_tokens"]) for r in rows)
46+
scored = sum(int(r["tasks_scored"]) for r in rows)
47+
print(f"{len(rows)} arms, {scored} tasks scored, {tokens:,} tokens")
48+
print(f"{tokens/max(scored, 1):,.0f} tokens per task")
49+
' ${DB_DIR}/overall_results.csv
50+
51+
cat ${DB_DIR}/*/*/*/*/failed_tasks.csv 2>/dev/null
52+
"""
53+
54+
55+
def scope_flags(task: str, max_tasks: int | None, max_trials: int | None) -> str:
56+
"""The --max_tasks/--max_trials a calibration of `task` runs at.
57+
58+
A flag given on the command line wins over OVERRIDES, for every dataset.
59+
"""
60+
overrides = OVERRIDES.get(task, {})
61+
if max_tasks is None:
62+
max_tasks = overrides.get("max_tasks", DEFAULT_MAX_TASKS)
63+
if max_trials is None:
64+
max_trials = overrides.get("max_trials")
65+
66+
flags = f"\n\t--max_tasks {max_tasks} \\"
67+
if max_trials is not None:
68+
flags += f"\n\t--max_trials {max_trials} \\"
69+
return flags
70+
71+
72+
def render(task: str, *, seeds: list[int], time_limit: str, db_dir: str,
73+
max_tasks: int | None, max_trials: int | None) -> str:
74+
return (
75+
preamble(
76+
f"vllm-{task}-calibrate",
77+
f"out/{task}-calibrate-%x.%j.%t.out",
78+
f"{task}_calibrate.sh",
79+
time_limit=time_limit,
80+
db_dir=db_dir,
81+
)
82+
+ "\n"
83+
+ run_command(
84+
task,
85+
every_arm(task),
86+
cross_task=False,
87+
seeds=seeds,
88+
scope=scope_flags(task, max_tasks, max_trials),
89+
)
90+
+ SUMMARY
91+
+ CLEANUP
92+
)
93+
94+
95+
def parse_args() -> argparse.Namespace:
96+
parser = argparse.ArgumentParser(description=__doc__)
97+
parser.add_argument(
98+
"--task",
99+
nargs="+",
100+
choices=TASKS,
101+
default=TASKS,
102+
help="the datasets to calibrate (default: all of them)",
103+
)
104+
parser.add_argument(
105+
"--seed",
106+
nargs="+",
107+
type=int,
108+
default=DEFAULT_SEEDS,
109+
help=f"the seeds each arm runs (default: {' '.join(str(s) for s in DEFAULT_SEEDS)})",
110+
)
111+
parser.add_argument(
112+
"--max_tasks",
113+
type=int,
114+
help=f"tasks of the dataset per arm (default: {DEFAULT_MAX_TASKS}, or the dataset's"
115+
" entry in OVERRIDES)",
116+
)
117+
parser.add_argument(
118+
"--max_trials",
119+
type=int,
120+
help="trials per task, overriding the dataset's own budget (default: the budget,"
121+
" or the dataset's entry in OVERRIDES)",
122+
)
123+
parser.add_argument(
124+
"--time_limit",
125+
default=DEFAULT_TIME_LIMIT,
126+
help=f"the #SBATCH --time each job asks for (default: {DEFAULT_TIME_LIMIT})",
127+
)
128+
parser.add_argument(
129+
"--db_dir",
130+
default=DEFAULT_DB_DIR,
131+
help=f"where the runs write, unless DB_DIR is set at submit time (default:"
132+
f" {DEFAULT_DB_DIR})",
133+
)
134+
return parser.parse_args()
135+
136+
137+
def main() -> None:
138+
args = parse_args()
139+
for task in args.task:
140+
write_script(
141+
f"{task}_calibrate.sh",
142+
render(
143+
task,
144+
seeds=args.seed,
145+
time_limit=args.time_limit,
146+
db_dir=args.db_dir,
147+
max_tasks=args.max_tasks,
148+
max_trials=args.max_trials,
149+
),
150+
)
151+
152+
153+
if __name__ == "__main__":
154+
main()

0 commit comments

Comments
 (0)