GPU VMs and SLURM¶
A fresh GPU VM¶
git clone https://github.com/lamng3/toolbox.git && cd toolbox
bash scripts/setup_vm.sh # uv venv + `.[llm,tracking]` + env report + 5-step smoke test
.venv/bin/toolbox train experiment=grpo_qwen_gsm8k
Multiple GPUs on one machine:
toolbox launch experiment=sft_qwen launcher=torchrun launcher.nproc_per_node=8
# runs: python -m torch.distributed.run --standalone --nproc-per-node=8 -m toolbox.cli train ... strategy=ddp
strategy=fsdp shards parameters, gradients and optimizer state for models that do not fit on
one GPU.
SLURM¶
toolbox launch turns the experiment config into an sbatch script and submits it. Always look
at the script first:
toolbox launch experiment=grpo_qwen_gsm8k launcher=slurm \
launcher.partition=gpu launcher.account=myproj \
launcher.nodes=2 launcher.gpus_per_node=4 launcher.time=24:00:00 --dry-run
Drop --dry-run to submit. The script is saved to <output_dir>/<job_name>.sbatch and logs
go to <output_dir>/logs/.
What the generated job does:
srun torchrunlaunchesnodes × gpus_per_nodeprocesses with a c10d rendezvous on the first node, and forcesstrategy=ddp.- It adds
trainer.resume_from=auto, so every (re)start continues fromcheckpoints/last.pt. - It traps
USR1, which SLURM sends 120 s before the time limit or on preemption, and requeues itself. Long runs survive wall-time limits without manual babysitting. - Checkpoints are written atomically (temp file + rename), so a killed job never leaves a
corrupt
last.pt.
Cluster presets¶
Keep cluster details (modules, accounts, scratch directories) in your own config directory instead of passing flags every time:
configs/launcher/mycluster.yaml
name: slurm
job_name: ${name}
partition: gpu
account: my_account
qos: normal
constraint: "a100|h100"
nodes: 1
gpus_per_node: 4
cpus_per_task: 32
mem: 256G
time: "24:00:00"
requeue: true
setup:
- "module load cuda/12.4"
- "cd ${oc.env:PWD}"
- "source .venv/bin/activate"
env:
HF_HOME: /scratch/${oc.env:USER}/hf
WANDB_MODE: offline
Sweeps¶
Each launch call is a Command object. A loop of submissions is a sweep, and
CommandInvoker.undo_all() cancels them all:
from toolbox.launch import CommandInvoker, SlurmSubmitCommand
invoker = CommandInvoker()
for lr in (1e-6, 3e-6, 1e-5):
for seed in (0, 1, 2):
invoker.run(SlurmSubmitCommand(
[f"experiment=grpo_qwen_gsm8k", f"optimizer.lr={lr}", f"seed={seed}",
f"output_dir=runs/sweep/lr{lr}_s{seed}"],
output_dir=f"runs/sweep/lr{lr}_s{seed}", job_name=f"grpo-{lr}-{seed}",
partition="gpu", gpus_per_node=1,
))
# changed your mind?
invoker.undo_all() # scancel every submitted job