Source code for gpaw.benchmark.performance_index

import json
from collections import defaultdict
from pathlib import Path
from time import time

import numpy as np
from ase.geometry.cell import cell_to_cellpar

from gpaw.benchmark.systems import systems
from gpaw.calcinfo import get_calculation_info
from gpaw.dft import GPAW
from gpaw.mpi import normalize_communicator
from gpaw.utilities.memory import maxrss

PARAMS = dict(
    xc='PBE',
    mode={'name': 'pw', 'ecut': 800},
    kpts={'density': 5.0},
    setups={'Cr': '14'})


# Reference numbers:
#
# 1) energy
# 2) change in energy after first step
# 3) number of cores
# 4) time in seconds
#
# Initial set of 14 materials for the first bechmark-run
# with old GPAW (version 25.7.0):
REFERENCES0 = {
    'Bi2Se3-3': (-8.911, -0.00074, 24, 46.74),
    'C60-0': (-530.92535, -0.4486, 24, 204.56),
    'C72-2': (-663.739, 3.4112, 24, 389.18),
    'C2-3': (-18.19611, -0.00000, 24, 11.11),
    'Ga2F4N4H10-3': (-99.08900, 0.00013, 40, 80.28),
    'H2-0': (-6.77477, 0.1170, 24, 3.78),
    'LiC8-3': (-75.37653, 0.6415, 24, 32.01),
    'Fe8-3M': (-72.37710, -0.00713, 24, 114.55),
    'Al96-2': (-350.06299, -0.01145, 40, 1434.00),
    'Mo60S120-1': (-1291.31046, 7.2669, 56, 6239.00),
    'OPt24-2': (-153.25143, -1.6164, 40, 999.75),
    'CrSi2As4-2M': (-38.89434, -0.1714, 24, 100.10),
    'VI2-2M': (-12.965, -0.00017, 24, 31.65),
    'Ti2Br6-3': (-32.64699, -0.00286, 24, 155.44)}

RESCALE_FACTOR = 1.0

# New materials for second run
# (new GPAW, master branch Nov. 11 2025):
REFERENCES0 |= {
    'MnVS2-2M': (-29.11777, -0.00014, 24, 98.608),
    'PtLi2O6-2M': (-40.713, -2.06842, 24, 454.22),
    'V3Cl6-2N': (-51.117, -0.10189, 24, 3364.039)}

# Score for the 14 systems was 94.34.
# Rescaling to 17 systems:
old = 94.34
new = (old / 100 * 14 + 3) / 17 * 100
RESCALE_FACTOR *= old / new

# New initial magmoms for MnVS2-2M (new GPAW, master branch Nov 25 2025).
# Time for MnVS2-2M system changed from 98.608 to 68.767 seconds:
REFERENCES0['MnVS2-2M'] = (-29.11777, -0.00016, 24, 68.767)

# New score for 17 systems: 115.80
old = 115.80
# Adding two more converged systems:
REFERENCES0 |= {
    'ErGe-2M': (-5.557, -0.0578, 24, 72.937),
    'Fe8O8-3M': (-126.756, 0.000025, 40, 316.031)}
new = (old / 100 * 17 + 2) / 19 * 100
RESCALE_FACTOR *= old / new

# Not yet included in benchmark:
REFERENCES = REFERENCES0 | {
    'Mn2O2-3M': (-18.577, -0.01743, 24, 9999999)}

# Updated references for ALL systems, s.t. improvements are weighted equally.
# Additionally, Initial magnetic moments have been changed to yield the correct
# ground state for all systems.
REFERENCES = {
    'Bi2Se3-3': (-8.911, -0.00074, 24, 62.7),
    'C60-0': (-530.92535, -0.4486, 24, 174.3),
    'C72-2': (-663.739, 3.4112, 24, 343.1),
    'C2-3': (-18.19611, -0.00000, 24, 8.2),
    'Ga2F4N4H10-3': (-99.08900, 0.00013, 40, 79.7),
    'H2-0': (-6.77477, 0.1170, 24, 3.0),
    'LiC8-3': (-75.37653, 0.6415, 24, 36.0),
    'Fe8-3M': (-72.37710, -0.00713, 24, 97.0),
    'Al96-2': (-350.06299, -0.01145, 40, 910.6),
    'Mo60S120-1': (-1291.31046, 7.2669, 56, 4289),
    'OPt24-2': (-153.25143, -1.6164, 40, 850.0),
    'CrSi2As4-2M': (-38.89434, -0.1714, 24, 125.1),
    'VI2-2M': (-12.965, -0.00017, 24, 35.1),
    'Ti2Br6-3': (-32.64699, -0.00286, 24, 169.0),
    'MnVS2-2M': (-29.11777, -0.00014, 24, 53.3),
    'PtLi2O6-2M': (-41.787, -1.6355, 24, 381.3),
    'V3Cl6-2N': (-51.117, -0.10189, 24, 2328.8),
    'ErGe-2M': (-6.369, -0.0601, 24, 59.9),
    'Fe8O8-3M': (-126.756, 0.000025, 40, 311.9),
    'Mn2O2-3M': (-19.123, -0.006, 24, 108.8)}

RESCALE_FACTOR = 1.125  # Match previous PW score

NAMES = sorted(REFERENCES, key=lambda name: name.split('-')[::-1])


[docs] def score(data: dict[str, float]) -> tuple[float, int]: """GPAW's PW-index (or score). With `N` materials and times for completion of each material `t_i`, we get this index (normalized to 100 for the first run with reference times `t_i^0`)::: 0 N t 100 α --- i ----- > ----. N --- t i=1 i The rescaling factor α is used for rescaling the index when new materials are added or hardware is updated. """ s = 0.0 n = 0 for name, (_, _, _, tref) in REFERENCES.items(): if name in data: s += tref / data[name] n += 1 return 100 * RESCALE_FACTOR * s / len(REFERENCES), n
def workflow(skip: list[str] | None = None) -> list: """MyQueue workflow.""" from myqueue.workflow import run handles = [] for name, (_, _, cores, est_time) in REFERENCES.items(): if skip and name in skip: continue tmax = '2h' if cores == 24: if est_time > 180: nodename = 'xeon24el9' else: nodename = 'xeon24el9_test' tmax = '10m' elif cores == 40: nodename = 'xeon40el9' tmax = '3h' elif cores == 56: nodename = 'xeon56el9' tmax = '7h' handle = run(function=work, args=[name], cores=cores, tmax=tmax, nodename=nodename, name=name, creates=[f'{name}.json']) handles.append(handle) return handles def work(name: str, params: dict | None = None, world=None) -> None: """Do two steps.""" world = normalize_communicator(world) params = params or PARAMS.copy() extra = Path('params.json') if extra.is_file(): params |= json.loads(extra.read_text()) atoms = systems[name]() # Do a non-colinear calculation? if hasattr(atoms, '_magmoms'): params |= dict( magmoms=atoms._magmoms, symmetry='off', xc='LDA') # Warmup: atoms.calc = GPAW( txt=None, convergence={'maximum iterations': 3}, communicator=world, **params) atoms.get_potential_energy() atoms.calc = GPAW( txt=f'{name}.txt', convergence={'density': 1e-4}, # keep old default for now ... communicator=world, **params) # First step: t1 = time() e1 = atoms.get_potential_energy() f1 = atoms.get_forces() i1 = atoms.calc.dft.scf_loop.niter if name in {'C2-3', 'Fe8-3M', 'Mn2O2-3M'}: # These systems have zero forces by symmetry assert abs(f1).max() < 0.0001 if atoms.calc.params.mode.name == 'pw': stress = atoms.get_stress(voigt=False) else: # LCAO and FD-mode does not do stress s = {'C2-3': -0.0014, 'Fe8-3M': 0.0364, 'Mn2O2-3M': 0.0382}[name] stress = np.diag([s, s, s]) atoms.set_cell(atoms.cell @ (np.eye(3) - 0.02 * stress), scale_atoms=True) else: atoms.positions += 0.1 * f1 t1 = time() - t1 m1 = maxrss() # Second step: t2 = time() e2 = atoms.get_potential_energy() atoms.get_forces() i2 = atoms.calc.dft.scf_loop.niter t2 = time() - t2 m2 = maxrss() atoms.calc.__del__() # make sure we get timing info in log-file if world.rank == 0: Path(f'{name}.json').write_text(json.dumps([e1, t1, i1, m1, e2, t2, i2, m2])) def read(folder: Path, mode: int, eps: float = 0.001) -> dict[str, tuple[float, int]]: """Read <name>.json files.""" data = {} for name, (e0, de0, _, _) in REFERENCES.items(): path = folder / f'{name}.json' if path.is_file(): x = json.loads(path.read_text()) if abs(x[0] - e0) > eps: print(path, x[0], x[0] - e0) if abs((x[4] - x[0] - de0) / e0) > eps: print(path, 'D', x[4] - x[0], x[4] - (e0 + de0)) if mode == 1: t = x[1] i = x[2] elif mode == 2: t = x[5] i = x[6] else: t = x[1] + x[5] i = x[2] + x[6] else: t = np.inf i = 999 data[name] = (t, i) return data def summary(folders: list[Path], mode: int) -> None: from gpaw.new.logger import GREEN, RED, RESET alldata = [read(folder, mode) for folder in folders] for i, folder in enumerate(folders): print(i + 1, folder) print('-----------------' + '+-----------------------' * len(folders)) scores = [0.0] * len(folders) N = 0 for r, name in enumerate(REFERENCES): print(f'{r + 1:2} {name:12} ', end='') times = [data[name][0] for data in alldata] iters = [data[name][1] for data in alldata] t0 = times[0] if max(times) < np.inf: N += 1 for n, (t, i) in enumerate(zip(times, iters)): if t == np.inf: line = ' | -------(---) -------%' else: percent = f'{(t / t0 - 1) * 100:+7.1f}%' if t < (0.98 * t0): percent = GREEN + percent + RESET elif t > (1.02 * t0): percent = RED + percent + RESET line = f' | {t:7.1f}({i:3}) {percent}' print(line, end='') if max(times) < np.inf: scores[n] += t / t0 print() print('-----------------' + '+-----------------------' * len(folders) + '\n ', end='') for s in scores: print(f'{(s / N - 1) * 100:+21.1f}%', end='') if mode == 3: print('\n ', end='') for data in alldata: s, _ = score({name: t for name, (t, i) in data.items()}) print(f'{s:22.2f}', end='') print() def average(folders: list[Path]) -> None: data: dict[str, np.ndarray] = defaultdict(lambda: np.zeros(8)) for folder in folders: for path in folder.glob('*-*.json'): x = json.loads(path.read_text()) data[path.stem] += np.array(x) for name in NAMES: if name not in data: continue x = data[name] e1, t1, i1, m1, e2, t2, i2, m2 = x / len(folders) print( f' "{name}": [' f'{e1:.6f}, {t1:.3f}, {round(i1):.0f}, {int(m1)}, ' f'{e2:.6f}, {t2:.3f}, {round(i2):.0f}, {int(m2)}],') def main(arguments: list[str] | None = None, world=None): from argparse import ArgumentParser world = normalize_communicator(world) parser = ArgumentParser() parser.add_argument( '-m', '--mode', type=int, default=3, help='1: first step, 2: second step, 3: both (default).') parser.add_argument( 'folder', nargs='*', help='Folder with <name>.json files.') parser.add_argument( '-a', '--average', action='store_true', help='Write average.') args = parser.parse_args(arguments) if args.folder: folders = [Path(folder) for folder in args.folder] summary(folders=folders, mode=args.mode) if args.average: average(folders) return print('name natoms ndim IBZ spin bands cores vol ' '(lengths) (angles)') for name, (e, de, cores, t) in REFERENCES.items(): atoms = systems[name]() info = get_calculation_info(atoms, comm=world, **PARAMS) print(f'{name:12} {len(atoms):4} {atoms.pbc.sum()}', end=' ') print(f'{len(info.ibz):3} {info.ncomponents} {info.nbands:3}', end='') print(f' {cores} {atoms.cell.volume:7.1f}', end=' ') a, b, c, A, B, C = cell_to_cellpar(atoms.cell) print(f'({a:5.1f},{b:5.1f}{c:5.1f}) ({A:5.1f},{B:5.1f},{C:5.1f})') if __name__ == '__main__': main()