Ground State Training with wandb#
The function train, in tachys.ground_state_training, runs a whole
ground-state optimization: for a given number of steps, it repeats the step of
the loop of the Quickstart, which advances the Markov chains, estimates
the energy, and computes and applies the parameter update. It prints the energy
and the timings of every step, and returns the optimized wavefunction. Given a
run of Weights & Biases (W&B), a web service that
records the metrics of training runs and plots them, it also logs these
quantities there and saves checkpoints.
The project#
The wandb extra of tachys installs W&B, with
jsonargparse, which reads the
configuration file below. This tutorial needs a W&B account, see the
W&B quickstart.
pip install "tachys[wandb]" # tachys[cuda13,wandb] or tachys[cuda12,wandb] on GPUs
wandb login
The project consists of four files, and the two directories that the runs fill:
j1j2_square/
├── vmc_config.yaml # the parameters
├── config.py # reads vmc_config.yaml
├── main.py # builds the model and calls train
├── job.sh # the Slurm job
├── log_output/ # the output of the jobs
└── wandb/ # the W&B runs
vmc_config.yaml holds the parameters:
seed: 22
wandb:
log: True
entity: 'my-team' # a W&B user or team
project: 'j1j2_square'
name: '' # appended to the name of the run
checkpoint:
input: '' # checkpoint directory to resume from
every: 1000
keep: 1
lattice:
Lx: 6
Ly: 6
hamiltonian:
J1: 1.0
J2: 0.5
transformer:
num_layers: 8
heads: 12
f: 6 # features per head: d_model = heads * f
b: 1 # patch side
optimizer:
mode: 'complex'
diag_shift: 1e-4
eta0: 0.005
eta_final: 0.001
N_steps: 5000
vmc:
N_mc: 8192
max_dist: 2
nsweeps: 1
config.py reads them into cfg, whose fields follow the structure of the
file: cfg.lattice.Lx, cfg.optimizer.N_steps. An argument on the command
line, such as --hamiltonian.J2 0.6, replaces the value of the file.
config.py also computes d_model and the name of the W&B run.
from jsonargparse import ArgumentParser
parser = ArgumentParser(default_config_files=["vmc_config.yaml"])
parser.add_argument("--seed", type=int)
parser.add_argument("--wandb.log", type=bool)
parser.add_argument("--wandb.entity", type=str)
parser.add_argument("--wandb.project", type=str)
parser.add_argument("--wandb.name", type=str)
parser.add_argument("--checkpoint.input", type=str)
parser.add_argument("--checkpoint.every", type=int)
parser.add_argument("--checkpoint.keep", type=int)
parser.add_argument("--lattice.Lx", type=int)
parser.add_argument("--lattice.Ly", type=int)
parser.add_argument("--hamiltonian.J1", type=float)
parser.add_argument("--hamiltonian.J2", type=float)
parser.add_argument("--transformer.num_layers", type=int)
parser.add_argument("--transformer.heads", type=int)
parser.add_argument("--transformer.f", type=int)
parser.add_argument("--transformer.b", type=int)
parser.add_argument("--optimizer.mode", type=str)
parser.add_argument("--optimizer.diag_shift", type=float)
parser.add_argument("--optimizer.eta0", type=float)
parser.add_argument("--optimizer.eta_final", type=float)
parser.add_argument("--optimizer.N_steps", type=int)
parser.add_argument("--vmc.N_mc", type=int)
parser.add_argument("--vmc.max_dist", type=int)
parser.add_argument("--vmc.nsweeps", type=int)
cfg = parser.parse_args()
cfg.transformer.d_model = cfg.transformer.heads * cfg.transformer.f
cfg.wandb.run_name = (f"{cfg.lattice.Lx}x{cfg.lattice.Ly}_J2={cfg.hamiltonian.J2}"
f"_h={cfg.transformer.heads}_f={cfg.transformer.f}")
if cfg.wandb.name:
cfg.wandb.run_name += f"_{cfg.wandb.name}"
The script#
main.py sets up the optimization with the parameters in cfg and calls
train:
from config import cfg
import jax
from tachys.lattice.lattice_database import square
from tachys.lattice.spins.spin_state import SpinState, init_config_fixed_magn
from tachys.lattice.spins.hamiltonians.heisenberg import heisenberg_hamiltonian
from tachys.lattice.spins.sign_rules import MSR_log_phase_square, add_sign_rule
from tachys.lattice.ansatz.spin_vit import SpinViT
from tachys.lattice.bond_exchange import BondExchange
from tachys.wavefunction import WaveFunction
from tachys.optimizer import MARCH, linear_decay
from tachys.ground_state_training import train
from tachys.checkpoint import load_checkpoint, get_last_step
from tachys.parallel import rank, MASTER
key = jax.random.key(cfg.seed)
lattice = square(shape=(cfg.lattice.Lx, cfg.lattice.Ly))
N = lattice.Ns
J1, J2 = cfg.hamiltonian.J1, cfg.hamiltonian.J2
H = heisenberg_hamiltonian(lattice, nn=[
((1, 0), J1), ((0, 1), J1), # nearest neighbours
((1, 1), J2), ((1, -1), J2), # next-nearest neighbours
])
key, subkey = jax.random.split(key)
spins = init_config_fixed_magn(subkey, N, N_mc=cfg.vmc.N_mc)
state = SpinState(spins=spins, lattice=lattice)
b = cfg.transformer.b
model = SpinViT(num_layers=cfg.transformer.num_layers, d_model=cfg.transformer.d_model,
num_heads=cfg.transformer.heads, seq_len=N // b**2, b=b,
transl_invariant=True, two_dimensional=True)
key, subkey = jax.random.split(key)
params = model.init(subkey, state)
# Marshall sign rule
apply_fn = add_sign_rule(MSR_log_phase_square, model.apply, lattice.L)
wf = WaveFunction(params=params, apply_fn=apply_fn)
action = BondExchange.create(lattice, max_dist=cfg.vmc.max_dist)
optimizer = MARCH(diag_shift=cfg.optimizer.diag_shift, mode=cfg.optimizer.mode)
lr_schedule = linear_decay(cfg.optimizer.eta0, cfg.optimizer.eta_final, cfg.optimizer.N_steps)
# resume from a checkpoint
opt_state, start_step = None, 0
if cfg.checkpoint.input:
params, opt_state, state, key = load_checkpoint(
cfg.checkpoint.input, state, optimizer.init(wf.params), params_template=wf.params,
)
wf = wf.replace(params=params)
start_step = get_last_step(cfg.checkpoint.input)
# the W&B run, on the master process only
wandb_run = None
if cfg.wandb.log and rank == MASTER:
import wandb
wandb_run = wandb.init(
entity=cfg.wandb.entity, project=cfg.wandb.project, name=cfg.wandb.run_name,
config={**cfg.as_dict(),
"checkpoint_every": cfg.checkpoint.every,
"checkpoint_keep": cfg.checkpoint.keep},
)
key, state, wf, opt_state, history = train(
key, H, state, wf, optimizer, action, cfg.optimizer.N_steps - start_step,
lr_schedule, cfg.vmc.N_mc, wandb_run=wandb_run, nsweeps=cfg.vmc.nsweeps,
opt_state=opt_state, start_step=start_step,
)
if wandb_run is not None:
wandb_run.finish()
The wavefunction is SpinViT, whose output is complex, with the Marshall sign
rule (Wavefunction). The Monte Carlo move, BondExchange with
max_dist=2, exchanges the spins of two nearest or next-nearest neighbours
(Monte Carlo Sampling). The optimizer is MARCH, a variant of stochastic
reconfiguration with momentum; the phase of the network depends on the
parameters, so it runs in complex mode (Optimization).
linear_decay lowers the learning rate from eta0 to eta_final over
N_steps steps.
wandb.init creates the run in the project project of the entity entity,
with a name and a config: the parameters of the run, here all of cfg, which
W&B shows with the run and uses to filter and group runs. Only the master
process creates the run; on the other processes wandb_run is None, but
train is called by all of them (Parallel Jobs).
train runs N_steps steps of the loop of the Quickstart, here
cfg.optimizer.N_steps - start_step, with nsweeps sweeps per step and the
learning rate lr_schedule(step) at step step. Its other keyword arguments
are
wandb_run, the W&B run, orNone;opt_stateandstart_step, the optimizer state and the number of the first step, to resume a run (Checkpoints);log_callback_fn, a function(state, wf, step) -> dictcalled at every step, whose values are logged with the others;skip_optimization, to sample and measure the energy without updating the parameters.
The API Reference describes all of them.
The output of train#
A short test on one GPU, an NVIDIA RTX A6000, sets a smaller lattice and network, fewer chains and fewer steps on the command line:
python main.py --lattice.Lx 4 --lattice.Ly 4 --transformer.num_layers 2 \
--vmc.N_mc 512 --optimizer.N_steps 250
train prints the setup, one line per step, and a summary:
--- Simulation setup ---
Lattice : Lattice Ns=16 L=(4, 4) nb=1 pbc=(True, True)
Hamiltonian : _OperatorSum (2 terms)
: 116,736 parameters
Optimizer : MARCH(mode=complex, nbatches=1, dtype=None)
MC action : BondExchange(max_dist=2, bonds=((0, 1), (0, 3), (0, 4), (0, 12), ...), Nbands=1)
N_mc : 512 N_steps: 250
lr schedule : 5.00e-03 -> 1.02e-03
------------------------------------------------------------
--- START TRAINING ---
step │ E/N │ var/N │ vscore │ accept │ lr │ t_mc │ t_exp │ t_opt │ t_tot │ ETA (h)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0 │ -0.085631646915 │ 7.99e+00 │ 1089.2874 │ 0.278 │ 5.00e-03 │ 4.06 │ 2.20 │ 9.30 │ 15.68 (s) │ 1.08 (h)
1 │ -0.074122214540 │ 1.01e+00 │ 183.8694 │ 0.158 │ 4.98e-03 │ 3.55 │ 2.35 │ 9.26 │ 15.16 (s) │ 1.04 (h)
2 │ -0.088764702597 │ 4.73e-01 │ 60.0299 │ 0.129 │ 4.97e-03 │ 0.15 │ 0.21 │ 0.56 │ 0.92 (s) │ 0.06 (h)
3 │ -0.095475820411 │ 2.67e-01 │ 29.3370 │ 0.105 │ 4.95e-03 │ 0.14 │ 0.20 │ 0.56 │ 0.90 (s) │ 0.06 (h)
4 │ -0.096146549477 │ 2.27e-01 │ 24.6006 │ 0.104 │ 4.94e-03 │ 0.14 │ 0.21 │ 0.55 │ 0.90 (s) │ 0.06 (h)
...
248 │ -0.435008411931 │ 8.98e-03 │ 0.0474 │ 0.124 │ 1.03e-03 │ 0.14 │ 0.21 │ 0.57 │ 0.92 (s) │ 0.00 (h)
249 │ -0.435610718530 │ 5.72e-02 │ 0.3013 │ 0.119 │ 1.02e-03 │ 0.14 │ 0.21 │ 0.57 │ 1.11 (s) │ 0.00 (h)
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Done. 250 steps in 258.4s (1.033s/step avg)
E/N over last 100 steps = -0.428942 ± 5.12e-03 (var = 2.62e-05)
Column |
Description |
|---|---|
|
The energy per site: the mean of the local energies, divided by \(N\). |
|
The variance of the local energies, divided by \(N\). |
|
The V-score \(N\sigma^2/E^2\) (Optimization). |
|
The acceptance rate of the moves, averaged over the chains. |
|
The learning rate. |
|
The seconds spent sampling, computing the local energies, and computing and applying the update. |
|
The seconds of the whole step. |
|
The hours left, estimated from the duration of the last step. |
The first two steps include the compilation by JAX, so the ETA is meaningful
from the third one. The last line gives the mean of E/N over the last 100
steps, or over all of them if there are fewer, with its standard deviation and
variance. If E/N becomes NaN or leaves the interval \([-100, 100]\), train
stops the program.
train returns (key, state, wf, opt_state, history). The first four, the
PRNG key, the last configurations of the chains, the optimized wavefunction and
the optimizer state, are what another call of train needs to continue, or
compute_observables to measure operators with the optimized wavefunction
(API Reference). history is a dict of lists, with one entry per step, for the
keys "energy" (per site), "variance_per_site", "vscore", "acceptance"
and "lr".
Weights & Biases#
At every step, train logs the quantities of history to the W&B run, under
the same names, against the step. At the end it writes the mean and
the variance of E/N over the last 100 steps to the summary of the run, as
mean_energy and var_energy; a run that diverges gets the tag divergence.
Online, wandb.init prints the address of the run page, which W&B updates as
the run proceeds. For the test above:
The test run on W&B: one chart per quantity logged by train.#
The overview of the run lists its config, with the values set on the command line, and its summary:
The config and the summary of the test run.#
On a cluster#
Compute nodes often have no internet access. With the environment variable
WANDB_MODE=offline, W&B writes the run to the directory
wandb/offline-run-<date>_<time>-<id> only, and wandb sync uploads it later
from a machine with internet access, such as the login node. job.sh runs the
parameters of vmc_config.yaml on the 8 GPUs of one node (Parallel Jobs):
#!/bin/bash
#SBATCH --job-name=j1j2
#SBATCH --partition=gpu
#SBATCH --nodes=1
#SBATCH --ntasks-per-node=8
#SBATCH --gres=gpu:8
#SBATCH --time=24:00:00
#SBATCH --output=log_output/%x.o%j
source $HOME/venvs/tachys/bin/activate # Python environment with tachys[cuda13,wandb]
export WANDB_MODE=offline
srun python main.py
The test run above was saved in this way, and uploaded with
wandb sync wandb/offline-run-20261001_192632-rszl7pcn
Checkpoints#
With a W&B run, train saves checkpoints in the directory of the run,
wandb_run.dir/checkpoints, which W&B uploads with the run: one every
checkpoint_every steps and one at the last step, keeping the last
checkpoint_keep. It reads both from the config of the run, where main.py
copies them from the checkpoint section of vmc_config.yaml. A checkpoint
holds the parameters, the optimizer state, the configurations of the chains
and the PRNG key.
A run that stopped before N_steps steps, at the time limit of its job for
instance, resumes from its last checkpoint, with the same parameters and
checkpoint.input set to the checkpoint directory:
python main.py --checkpoint.input wandb/offline-run-<date>_<time>-<id>/files/checkpoints
main.py restores the state with load_checkpoint, and passes the optimizer
state and the number of steps done, start_step, to train, which runs the
remaining N_steps - start_step steps. The learning-rate schedule, the printed
steps and the steps on W&B continue from start_step, in a new W&B run.