A1 Batched relaxation for screening¶
Objectives
Explain why batched GPU relaxation speeds up screening.
Relax one set serially with ASE and in one TorchSim batch.
Compare wall time and energy agreement.
Serial relaxation leaves the GPU idle; batching fills it (GPU engines).
One MACE-MP model, two routes: serial ASE and one batched TorchSim call on one GPU.
Part A: own pixi environment, MACE-MP-0b small (not Part B’s MACE-MP-0a).
Workload: rattled copies of Cu fcc, Si diamond, Fe bcc and Al fcc (8 to 32 atoms); each copy needs its own relaxation.
workload.py
7FAMILIES = {
8 "Cu-fcc": lambda: bulk("Cu", "fcc", a=3.61, cubic=True).repeat((2, 2, 2)),
9 "Si-diamond": lambda: bulk("Si", "diamond", a=5.43, cubic=True),
10 "Fe-bcc": lambda: bulk("Fe", "bcc", a=2.87, cubic=True).repeat((2, 2, 2)),
11 "Al-fcc": lambda: bulk("Al", "fcc", a=4.05, cubic=True).repeat((2, 2, 2)),
12}
13
14
15def build_workload(n_variants: int, seed: int = 42) -> tuple[list[Atoms], list[str]]:
16 rng = np.random.default_rng(seed)
17 structures, labels = [], []
18 for name, make in FAMILIES.items():
19 for i in range(n_variants):
20 atoms = make()
21 atoms.rattle(stdev=rng.uniform(0.03, 0.12), seed=int(rng.integers(1e6)))
22 structures.append(atoms)
23 labels.append(f"{name}-{i:02d}")
24 return structures, labels

Serial and batched runs¶
Both routes use FIRE with a fixed cell.
Stop at
--fmax(default 0.05 eV/Å) or--max-steps.Serial relaxes only the first
--baseline-nstructures and extrapolates to the full set; a full serial run would use most of the allocation.
runners.py
17class SerialRelaxer:
18 def __init__(self, make_calc, fmax: float, max_steps: int):
19 self.make_calc = make_calc
20 self.fmax = fmax
21 self.max_steps = max_steps
22
23 def run(self, structures: list[Atoms]) -> RelaxResult:
24 calc = self.make_calc()
25 energies, relaxed = [], []
26 start = time.perf_counter()
27 for original in structures:
28 atoms = original.copy()
29 atoms.calc = calc
30 FIRE(atoms, logfile=None).run(fmax=self.fmax, steps=self.max_steps)
31 energies.append(float(atoms.get_potential_energy()))
32 relaxed.append(atoms)
33 return RelaxResult(time.perf_counter() - start, energies, relaxed)
34
35
36class BatchedRelaxer:
37 def __init__(self, model, fmax: float, max_steps: int, autobatch: bool = False):
38 self.model = model
39 self.fmax = fmax
40 self.max_steps = max_steps
41 self.autobatch = autobatch
42
43 def run(self, structures: list[Atoms]) -> RelaxResult:
44 import torch_sim as ts
45
46 start = time.perf_counter()
47 state = ts.optimize(
48 system=structures,
49 model=self.model,
50 optimizer=ts.Optimizer.fire,
51 convergence_fn=ts.generate_force_convergence_fn(force_tol=self.fmax),
52 max_steps=self.max_steps,
53 autobatcher=self.autobatch,
54 pbar=True,
55 )
56 wall = time.perf_counter() - start
57 energies = [float(e) for e in state.energy]
58 return RelaxResult(wall, energies, ts.io.state_to_atoms(state))
TorchSim advances all ASE
Atomsas one batched state; converged structures leave the batch.--autobatchsplits the set to fit GPU memory.Same checkpoint for both; TorchSim needs the raw model (
return_raw_model=True):
models.py
52def load_mace(name: str, device: torch.device, dtype: torch.dtype,
53 checkpoint: str | None = None) -> ModelPair:
54 from mace.calculators.foundations_models import mace_mp
55 from torch_sim.models.mace import MaceModel, MaceUrls
56
57 source = checkpoint or getattr(MaceUrls, MACE_MODELS[name]).value
58 options = {"model": source, "device": str(device),
59 "default_dtype": str(dtype).removeprefix("torch.")}
60
61 def make_torchsim():
62 raw = mace_mp(return_raw_model=True, **options)
63 return MaceModel(model=raw, device=device, dtype=dtype,
64 compute_forces=True, compute_stress=False)
65 return ModelPair(keeps_dtype(make_torchsim),
66 keeps_dtype(lambda: mace_mp(**options)), checkpoint)
67
68
69def orb_ase_calc(orbff, adapter, device: torch.device, dtype: torch.dtype):
70 """ORBCalculator builds its input graph in the global default dtype."""
71 from orb_models.forcefield.inference.calculator import ORBCalculator
72
73 class Calculator(ORBCalculator):
74 def calculate(self, *args, **kwargs):
75 with default_dtype(dtype):
76 super().calculate(*args, **kwargs)
77 return Calculator(orbff, adapter, device=device)
78
79
80def load_orb(name: str, device: torch.device, dtype: torch.dtype,
81 checkpoint: str | None = None, d3: bool = False) -> ModelPair:
82 from orb_models.forcefield import pretrained
83 from orb_models.forcefield.inference.d3_model import AlchemiDFTD3, D3SumModel
84 from torch_sim.models.orb import OrbModel
85
86 loader = getattr(pretrained, ORB_MODELS[name])
87 options = {"device": device, "precision": ORB_PRECISION[dtype], "compile": False}
88 if checkpoint:
89 options["weights_path"] = checkpoint
90 with default_dtype():
91 orbff, adapter = loader(**options)
92 if d3:
93 orbff = D3SumModel(orbff, AlchemiDFTD3(functional="PBE", damping="BJ"))
94 return ModelPair(lambda: OrbModel(orbff, adapter, device=device, dtype=dtype),
95 lambda: orb_ase_calc(orbff, adapter, device, dtype), checkpoint)
96
97
98def load_matgl(name: str, device: torch.device, dtype: torch.dtype) -> ModelPair:
99 """ASE only: TorchSim has no MatGL model, and MatGL runs in float32."""
100 import matgl
101 from matgl.ext.ase import PESCalculator
102
103 if dtype != torch.float32:
104 raise ValueError("MatGL models run in float32; pass --dtype float32")
105 with default_dtype():
106 potential = matgl.load_model(MATGL_MODELS[name]).to(device)
107 return ModelPair(None, lambda: PESCalculator(potential, stress_unit="eV/A3"))
108
109
Outputs: ASE versus TorchSim energies on the shared structures,
summary.json and relaxed.extxyz. A local --checkpoint file also gets
its SHA-256 recorded.
Laptop run¶
Package
examples/torchsim/with its own pixi environment; entry pointexamples/torchsim/__main__.py.smokeuses a toy Lennard-Jones potential: no download, seconds to run, no physical meaning.
cd examples/torchsim
pixi run smoke
Output goes to examples/torchsim_smoke/. Existing output directories are
never overwritten: remove it or pass a new --outdir.
Expected output (numbers vary):
model=lj device=cpu dtype=float64 structures=8 serial subset=2
wall time s/structure
serial, measured 0.1 s 0.03 s (2 structures)
serial, estimated 0.2 s 0.03 s (8 structures)
batched, measured 5.4 s 0.68 s (8 structures)
estimated serial / batched: 0.04
max |E_ASE - E_TorchSim| on the serial subset: 2.03e-01 eV
Batching is slower on a CPU with a cheap potential: no GPU parallelism.
The energy gap comes from different Lennard-Jones cutoff and shift conventions in ASE and TorchSim. Only the MACE run is a consistency check (a few meV).
MACE on a CPU: the cpu task relaxes four structures for at most 20 steps
into examples/torchsim_cpu/. First run downloads MACE-MP-0b small (about
68 MB):
pixi run cpu
Leonardo run (one A100)¶
Compute nodes have no internet. On a login node, install the pixi environment in your lesson copy and download the checkpoint to a private location outside Git:
Commands
cd /leonardo_scratch/fast/<PROJECT>/<USER>/mlip-hands-on/examples/torchsim
pixi install
curl -L -o <SCRATCH>/models/mace-mp-0b-small.model \
https://github.com/ACEsuit/mace-mp/releases/download/mace_mp_0b/mace_agnesi_small.model
printf '%s %s\n' \
7e3a0abcaf41e03a80e69f778e1b11b29de1cca704783dc25917a736392f8cf0 \
<SCRATCH>/models/mace-mp-0b-small.model | sha256sum -c -
The job checks this SHA-256 and refuses any other file. Submit from the repository root; missing inputs or an existing output directory abort it:
Commands
export MLIP_LESSON_ROOT=/leonardo_scratch/fast/<PROJECT>/<USER>/mlip-hands-on
export MLIP_TORCHSIM_CHECKPOINT=<SCRATCH>/models/mace-mp-0b-small.model
export MLIP_RESULTS_DIR=/path/to/private/results
sbatch --account=<PROJECT> \
--export=ALL,MLIP_LESSON_ROOT,MLIP_TORCHSIM_CHECKPOINT,MLIP_RESULTS_DIR \
scripts/test-leonardo-torchsim.sbatch
test-leonardo-torchsim.sbatch
23outdir="$MLIP_RESULTS_DIR/leonardo-torchsim-${SLURM_JOB_ID}"
24test ! -e "$outdir"
25cd "$MLIP_LESSON_ROOT/examples"
26srun --nodes=1 --ntasks=1 --gpus=1 --cpu-bind=cores \
27 pixi run --frozen --manifest-path "$manifest" \
28 python -m torchsim --model mace-small --checkpoint "$MLIP_TORCHSIM_CHECKPOINT" \
29 --device cuda --dtype float64 --n-variants 16 --baseline-n 8 --outdir "$outdir"
30jq -e '.n_structures == 64 and .baseline_n == 8 and .batched_wall_s > 0' \
31 "$outdir/summary.json" >/dev/null
32printf 'Leonardo TorchSim batched relaxation passed: %s\n' "$outdir"
This script has not yet been qualified. Neither result row below comes from it: float64 is from an earlier script with the same workload and options; float32 is from a separate tuned run (options under the table).
Leonardo results¶
One A100, MACE-MP-0b small, no cuEquivariance. Serial measured 8 structures, scaled to the full set.
Precision and batching |
Structures |
Serial, 8 measured (s) |
Serial, estimated (s) |
Batched (s) |
Estimated serial / batched |
|---|---|---|---|---|---|
float64 |
64 |
37.4 |
299.5 |
59.3 |
5.05 |
float32, |
128 |
38.7 |
619.6 |
85.2 |
7.27 |
The float32 run used --dtype float32 --n-variants 32 --autobatch.
Warning
Single examples, not a benchmark.
One run per row: no repeats, medians or ranges.
Full-set serial time is extrapolated from 8 structures.
MACE-MP-0b small, not Part B’s MACE-MP-0a: energies and costs are not interchangeable.
No cuEquivariance kernels; they would change the timings.
Relaxation, not MD: do not compare with Part B timings.
LUMI (AMD MI250X) check¶
MACE runs on AMD via ROCm PyTorch (CSC pytorch module). Separate ASE
script, no TorchSim batching, one MI250X GCD:
Step |
System |
Time (s) |
|---|---|---|
Single point |
32 atoms |
1.6 |
FIRE relaxation, serial |
6 structures (Cu, Al, Fe, Si) |
17.0 |
MD, 600 K |
200 steps, 32 atoms |
3.9 |
One run, float64, MACE-MP-0a small.
Shows the model runs on AMD; not a speed comparison with the A100 table.
CUDA-only kernels (cuEquivariance, ALCHEMI) are not available on AMD.
Reading the results¶
One A100: batched about 5 times faster than serial estimate (float64), about 7 times (float32,
--autobatch).CPU with the toy potential: batching slower; the gain is GPU parallelism.
One run each, extrapolated serial time: ratios are indicative.
Keypoints
Batching runs many relaxations in one GPU call; 5 to 7 times faster on an A100.
On a CPU, batching gives no gain.
Check serial and batched energies agree before comparing timings.