Declarative experiment runner for Megatron-LM on SLURM.
Spellbook lets you define experiments as Python dataclasses, generate reproducible job scripts, and submit sweeps without hand-editing shell scripts.
If you are using coding agents in this repository, see AGENTS.md for repo-specific instructions and conventions.
See CHANGELOG.md for the current main-merge summary and operational notes.
uv sync
source .venv/bin/activateOptional quality checks:
uv run ruff check .
uv run ty checkpython main.py list experiments/big_moe_speed_ablations/experiment.py
python main.py render experiments/big_moe_speed_ablations/experiment.py
python main.py submit experiments/big_moe_speed_ablations/experiment.pyIf you prefer explicit uv execution:
uv run python main.py list experiments/big_moe_speed_ablations/experiment.pyOptions:
--only NAME: render/submit a single experiment from a sweep.--output-dir DIR: where generated scripts/CSVs are written (default:sbatch_scripts).list --all: show all experiment fields.list --columns a,b,c: show selected fields.
# All fields → <experiment_dir>/<sweep_name>.csv
python main.py csv experiments/big_moe_speed_ablations/experiment.py
# Only differing fields → <experiment_dir>/<ClassName>.changed.csv
python main.py csv experiments/big_moe_speed_ablations/experiment.py --changed
# Override output path
python main.py csv experiments/big_moe_speed_ablations/experiment.py --output my.csvEvery row includes computed size columns (total_params_B, active_params_B, activation_ratio)
and a parent_idx column — the 0-based row index of the experiment that .change() was called
on (empty for root/base experiments).
The following fields are always excluded from CSV output: wandb_project, wandb_exp_name,
tensorboard_dir, save, load.
Spellbook loads .env automatically in main.py startup using python-dotenv.
Behavior:
.envis optional.- Existing exported environment variables are not overwritten.
.envis gitignored in this repo.
Example:
WANDB_API_KEY=...
HF_TOKEN=...
MEGATRON_PATH=/path/to/Megatron-LM
DATA_PATH=/path/to/data
TOKENIZER_MODEL=/path/to/tokenizer.model
CHECKPOINT_SAVE=/path/to/checkpoints
SLURM_ACCOUNT=a139
SLURM_PARTITION=normal
CONTAINER_EDF=apertus2-alps4-temp
CONTAINER_MOUNTS=${SCRATCH}:${SCRATCH},${HOME}:${HOME},/capstor:/capstor,/iopsstor:/iopsstorRecommended starting .env for this repository:
SLURM_ACCOUNT=a139
SLURM_PARTITION=normal
CONTAINER_EDF=apertus2-alps4-temp
CONTAINER_MOUNTS=${SCRATCH}:${SCRATCH},${HOME}:${HOME},/capstor:/capstor,/iopsstor:/iopsstor
MEGATRON_PATH=$HOME/open_source/Megatron-LM-upstream
TOKENIZER_MODEL=swiss-ai/Apertus-70B-2509Experiment: base dataclass withchange(),sweep(),diff(),to_dict().MegatronExperiment: rich experiment schema plus Megatron CLI flag translation.Sweep: named collection of experiments rendered/submitted together.SlurmBackend: renders Jinja templates and submits viasbatchor runs viasrun.
An experiment module must expose a top-level sweep variable.
from spellbook.backends import SlurmBackend
from spellbook.core import Sweep
from spellbook.megatron import MegatronExperiment
BASE = MegatronExperiment(
name="my-base",
megatron_path="/path/to/megatron",
data_path="/path/to/data",
tokenizer_model="/path/to/tokenizer.model",
save="/path/to/checkpoints",
num_layers=24,
hidden_size=4096,
ffn_hidden_size=11008,
num_attention_heads=32,
seq_length=4096,
vocab_size=131072,
tp=2,
pp=2,
ep=1,
num_gpus=16,
mbs=1,
gbs=128,
train_tokens=10_000_000,
lr=3e-4,
min_lr=3e-5,
wandb_project="spellbook-demo",
install_commands="""
pip install --upgrade --no-deps "nvidia-cutlass-dsl==4.4.2"
pip install --upgrade --no-deps "quack-kernels[cu13]==0.4.1"
""".strip(),
)
backend = SlurmBackend(
account="a139",
partition="normal",
nodes=None, # auto-derive from experiment.num_gpus / gpus_per_node
gpus_per_node=4,
run_time="01:00:00",
extra={
"container_edf": "apertus2-alps4-temp",
"container_mounts": "${SCRATCH}:${SCRATCH},${HOME}:${HOME}",
},
)
sweep = Sweep(
name="demo-sweep",
experiments=[
BASE,
BASE.change("my-tp4", tp=4, pp=1),
],
backend=backend,
)Use convenience constructors from spellbook.core:
from spellbook.core import sweep_axis, sweep_grid
# one-axis
sweep = sweep_axis(BASE, "tp", [1, 2, 4], backend=backend, name="tp-sweep")
# grid
sweep = sweep_grid(
BASE,
{"tp": [1, 2, 4], "ep": [4, 8]},
backend=backend,
name="tp-ep-grid",
)SlurmBackend modes:
- default: render
slurm.sh.j2and submit withsbatch. launch_mode="tasks": renderslurm_tasks.sh.j2, requestntasks-per-node=gpus_per_node, and run the training script directly once per Slurm task instead of usingtorchrun.srun_job_idset: rendersrun.sh.j2and execute script withbashin an existing allocation.mem_estimator=True: rendermem_estimator.sh.j2.
Important fields:
MegatronExperiment.megatron_path: a local checkout or Git URL. URL caches are refreshed under${SCRATCH}/tmp/megatron_reposbefore each launch; unpinned URLs follow the remote default branch, whilemegatron_commitpins a commit or resolves a refreshed remote branch. Runs use source-specific locked worktrees under${SCRATCH}/tmp/megatron_worktrees. WithoutSCRATCH, the cache falls back below the user-namespaced${TMPDIR:-/tmp}/spellbook-$USERdirectory. Whencontainer_edfis configured, the selected checkout is copied into the container at/opt/megatronbefore Megatron starts; non-container launches use the source checkout directly.MegatronExperiment.megatron_container_path: writable destination for that container copy. It defaults to/opt/megatronand must be a shell-safe absolute path with at least two components.extra: generic Jinja template context (for examplecontainer_edf,container_mounts).- In
launch_mode="tasks",extramay also includecpus_per_task,mem,no_requeue, or rawsbatch_extra_lines. srun_extra_args: extra raw flags inserted into everysruncommand. If this includes--network=VALUE, Spellbook also exportsSLURM_NETWORK=VALUEbeforesrunso the step inherits the same network setting.reservation: added to sbatch header and sbatch invocation.auto_requeue=True: submit a singleton-dependent successor before training starts.auto_requeue_stop_regex: an extended regular expression checked against the completed job's Slurm stdout log. It defaults to Megatron's normal-completion marker,r"\[after training is done\] datetime:". When it matches, Spellbook cancels the successor submitted byauto_requeue, allowing an auto-requeue chain to stop after training finishes. Set it to""to disable the completion check or override it for another trainer.MegatronExperiment.pre_launch_commands: raw shell commands inserted intoslurm.sh.j2before the main trainingsrun. Use this for setup that needs to run once per job, including a separate one-task setupsrun.MegatronExperiment.install_commands: raw shell commands inserted insideslurm.sh.j2before data path setup and training launch. Use this for per-experiment package installation.MegatronExperiment.base_data_path: comma-separated dataset roots whose.binshards are discovered at render time. Saved renders write the sorted shard prefixes to<experiment>.data_args.txtnext to the job script and pass it with--data-args-path.MegatronExperiment.follow_symlinks: follow symlinked directories duringbase_data_pathdiscovery. It defaults toFalse; enable it for layouts whose shard directories are symlinks.MegatronExperiment.data_args_path: use an existing Megatron data-path manifest without discovery. Configuring it suppresses the missing-data warning. Explicitdata_pathtakes precedence overdata_args_path, anddata_args_pathtakes precedence overbase_data_path.MegatronEvalConfig.megatron_path: uses the same container-local/optcopy behavior for evaluation jobs.MegatronEvalConfig.install_commands: raw shell commands run once per node inside the evalsrunshell beforelm_evalstarts; sibling ranks wait for the local install to finish. Use this for per-eval package installation.
srun_extra_args is not the same as extra.
Once you are happy with a config, call .lock() to freeze it:
MY_EXP = BASE.change("MY_EXP", tp=4, lr=3e-4).lock()First call (no lock file yet): writes locks/MY_EXP.lock.yaml with the full config and a timestamp.
Subsequent calls (lock file exists): validates the current config against the file and raises an error if anything differs:
RuntimeError: Experiment 'MY_EXP' differs from its lock file (locks/MY_EXP.lock.yaml):
lr: locked=0.0003 current=0.001
To change a locked experiment: delete locks/MY_EXP.lock.yaml, update the config, and run the experiment file again — a fresh lock is written automatically.
Lock files live at locks/<name>.lock.yaml (same level as sbatch_scripts/). Commit them to git so drift is caught in code review.
render and submit create:
- one script per experiment at
sbatch_scripts/<sweep_name>/<experiment_name>.sh
Use csv to export a CSV alongside the experiment definition (see the csv section above).
# 1) Inspect sweep variants and changed fields
python main.py list experiments/big_moe_speed_ablations/experiment.py
# 2) Render scripts without submitting
python main.py render experiments/big_moe_speed_ablations/experiment.py
# 3) Submit one variant first (sanity check)
python main.py submit experiments/big_moe_speed_ablations/experiment.py --only DEEPSEEK_V3_BASE
# 4) Submit full sweep
python main.py submit experiments/big_moe_speed_ablations/experiment.pyRunning inside an existing allocation:
- Set
srun_job_idonSlurmBackend. - Render or submit as usual.
- Spellbook will use the
srun.sh.j2path and execute scripts withbash.
Most dataclass fields map automatically:
snake_casefield name ->--kebab-caseMegatron CLI flag.- Booleans:
Trueemits bare flag,Falseomitted. - Lists emit space-joined values, except
moe_layer_freq. moe_layer_freqemits compact bracketed form with no spaces (for example[1,1,1]).- Rendered scripts keep each flag and its value on the same line.
- Aliases:
tp,pp,ep,etp,cp,vpp,mbs,gbsmap to canonical Megatron flags. - Special:
train_tokensbecomes--train-samples train_tokens // seq_length.
spellbook/
backends/
slurm_megatron/
slurm.py
slurm.sh.j2
srun.sh.j2
mem_estimator.sh.j2
core/
experiment.py
sweep.py
megatron/
experiment.py
flags.py
experiments/
big_moe_speed_ablations/
big_moe_speed_ablations_example/
main.py
uv sync
uv run python -m unittest discover -v
uv run ruff check .
uv run ty checkThe optional Megatron-dependent memory-estimator subtree is excluded from local Ruff and ty checks because its imports are supplied only by the runtime Megatron environment.