Skip to content

Commit 48d62e7

Browse files
Merge pull request #300 from jngrad/espresso-lb
ESPResSo 5.0-dev test cases
2 parents 8467774 + c298eef commit 48d62e7

File tree

5 files changed

+342
-39
lines changed

5 files changed

+342
-39
lines changed

eessi/testsuite/tests/apps/espresso/espresso.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
"""
2-
This module tests Espresso in available modules containing substring 'ESPResSo' which is different from Quantum
3-
Espresso. Tests included:
2+
This module tests ESPResSo in available modules containing substring 'ESPResSo'
3+
which is different from Quantum Espresso.
4+
Tests included:
45
- P3M benchmark - Ionic crystals
56
- Weak scaling
67
- Strong scaling Weak and strong scaling are options that are needed to be provided to the script and the system is
78
either scaled based on number of cores or kept constant.
9+
- LJ benchmark
10+
- LB benchmark
811
"""
912

1013
import reframe as rfm
1114
import reframe.utility.sanity as sn
15+
import re
1216

1317
from reframe.core.builtins import deferrable, parameter, performance_function, run_after, sanity_function
1418
from reframe.utility import reframe
1519

1620
from eessi.testsuite.constants import DEVICE_TYPES, SCALES, COMPUTE_UNITS
1721
from eessi.testsuite.eessi_mixin import EESSI_Mixin
18-
from eessi.testsuite.utils import find_modules, log
22+
from eessi.testsuite.utils import find_modules, log, split_module
1923

2024

2125
def filter_scales():
@@ -119,3 +123,50 @@ def assert_convergence(self):
119123
check_string = sn.assert_found(r'Final convergence met with relative tolerances:', self.stdout)
120124
energy = sn.extractsingle(r'^\s+sim_energy:\s+(?P<energy>\S+)', self.stdout, 'energy', float)
121125
return (check_string and (energy != 0.0))
126+
127+
128+
@rfm.simple_test
129+
class EESSI_ESPRESSO_LB(EESSI_ESPRESSO_base, EESSI_Mixin):
130+
executable = 'python3 lb.py'
131+
sourcesdir = 'src/lb'
132+
readonly_files = ['lb.py']
133+
bench_name = 'lb_without_particles'
134+
135+
def required_mem_per_node(self):
136+
"LB requires 250 MB per core"
137+
return (self.num_tasks_per_node * 0.25) * 1024
138+
139+
@run_after('init')
140+
def skip_tests_module_version_LB(self):
141+
"""
142+
The LB module versions need to be >= 5.0.0 or a expermental release version which includes walberla. The earlier
143+
lb method does not scale beyond 16 MPI tasks and is extremely slow in terms of case setup.
144+
Assumption:
145+
1. Versions with commit hashes have walberla in them. If not then they will not be filtered here and will
146+
run.
147+
"""
148+
module_version = split_module(self.module_name)[1]
149+
if re.match(r"\d+\.\d+\.\d+", module_version):
150+
major_version = re.search(r"\d+", module_version)
151+
major_version = int(major_version.group()) if major_version is not None else -1
152+
self.skip_if(0 <= major_version < 5, msg="LB tests scale only with walberla modules introduced in version"
153+
" 5.0.0 and above otherwise setup phase takes way too long even for simple cases.")
154+
155+
@run_after('init')
156+
def set_executable_opts(self):
157+
"""Set executable opts based on device_type parameter"""
158+
# Weak scaling (Gustafson's law: constant work per core): size scales with number of cores
159+
self.executable_opts += ['--kT', '1.2', '--weak-scaling']
160+
log(f'executable_opts set to {self.executable_opts}')
161+
162+
@deferrable
163+
def assert_completion(self):
164+
'''Check completion'''
165+
return sn.assert_found(r'^Algorithm executed.', self.stdout)
166+
167+
@deferrable
168+
def assert_convergence(self):
169+
'''Check convergence'''
170+
check_string = sn.assert_found(r'Final convergence met with tolerances:', self.stdout)
171+
energy = sn.extractsingle(r'^\s+energy:\s+(?P<energy>\S+)', self.stdout, 'energy', float)
172+
return (check_string and (energy != 0.0))
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# WARNING: do not remove this file.
2+
# It is needed to autogenerate documentation from this repo.
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#
2+
# Copyright (C) 2013-2025 The ESPResSo project
3+
#
4+
# This file is part of ESPResSo.
5+
#
6+
# ESPResSo is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU General Public License as published by
8+
# the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# ESPResSo is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
18+
#
19+
20+
import os
21+
import time
22+
import argparse
23+
import numpy as np
24+
import espressomd
25+
import espressomd.lb
26+
import espressomd.version
27+
28+
parser = argparse.ArgumentParser(description="Benchmark LB simulations.")
29+
parser.add_argument("--gpu", action=argparse.BooleanOptionalAction,
30+
default=False, required=False, help="Use GPU implementation")
31+
parser.add_argument("--topology", metavar=("X", "Y", "Z"), nargs=3, action="store",
32+
default=None, required=False, type=int, help="Cartesian topology")
33+
parser.add_argument("--unit-cell", action="store", nargs="+",
34+
type=int, default=[64, 64, 64], required=False,
35+
help="unit cell size")
36+
parser.add_argument("--single-precision", action="store_true", required=False,
37+
help="Using single-precision floating point accuracy")
38+
parser.add_argument("--kT", metavar="kT", action="store",
39+
type=float, default=0., required=False,
40+
help="Thermostat heat bath")
41+
group = parser.add_mutually_exclusive_group()
42+
group.add_argument("--weak-scaling", action="store_true",
43+
help="Weak scaling benchmark (Gustafson's law: constant work per core)")
44+
group.add_argument("--strong-scaling", action="store_true",
45+
help="Strong scaling benchmark (Amdahl's law: constant total work)")
46+
group = parser.add_mutually_exclusive_group()
47+
group.add_argument("--particles-per-rank", metavar="N", action="store",
48+
type=int, default=0, required=False,
49+
help="Number of particles per MPI rank")
50+
group.add_argument("--particles-total", metavar="N", action="store",
51+
type=int, default=0, required=False,
52+
help="Number of particles in total")
53+
args = parser.parse_args()
54+
55+
56+
required_features = []
57+
if args.particles_per_rank or args.particles_total:
58+
required_features.append("LENNARD_JONES")
59+
if args.gpu:
60+
required_features.append("CUDA")
61+
espressomd.assert_features(required_features)
62+
espresso_version = (espressomd.version.major(), espressomd.version.minor())
63+
64+
# initialize system
65+
system = espressomd.System(box_l=[100., 100., 100.])
66+
system.time_step = 0.01
67+
system.cell_system.skin = 0.4
68+
69+
# set MPI Cartesian topology
70+
node_grid = np.array(system.cell_system.node_grid)
71+
n_mpi_ranks = int(np.prod(node_grid))
72+
n_omp_threads = int(os.environ.get("OMP_NUM_THREADS", 1))
73+
if args.topology:
74+
system.cell_system.node_grid = node_grid = args.topology
75+
76+
if args.weak_scaling:
77+
system.box_l = np.multiply(np.array(args.unit_cell), node_grid)
78+
else:
79+
system.box_l = args.unit_cell
80+
81+
if args.particles_total:
82+
n_part = args.particles_total
83+
else:
84+
n_part = n_mpi_ranks * args.particles_per_rank
85+
86+
# set CUDA topology
87+
devices = {}
88+
if args.gpu:
89+
devices = espressomd.cuda_init.CudaInitHandle().list_devices()
90+
if len(devices) > 1 and espresso_version >= (5, 0):
91+
system.cuda_init_handle.call_method("set_device_id_per_rank")
92+
93+
# place particles at random
94+
if n_part:
95+
# volume of N spheres with radius r: N * (4/3*pi*r^3)
96+
lj_sig = 1.
97+
lj_eps = 1.
98+
lj_cut = lj_sig * 2**(1. / 6.)
99+
volume = float(np.prod(system.box_l))
100+
vfrac = n_part * 4. / 3. * np.pi * (lj_sig / 2.)**3 / volume
101+
print(f"volume fraction: {100.*vfrac:.2f}%")
102+
assert vfrac < 0.74, "invalid volume fraction"
103+
system.non_bonded_inter[0, 0].lennard_jones.set_params(
104+
epsilon=lj_eps, sigma=lj_sig, cutoff=lj_cut, shift="auto")
105+
system.part.add(pos=np.random.random((n_part, 3)) * system.box_l)
106+
print("minimize")
107+
energy_target = n_part / 10.
108+
system.integrator.set_steepest_descent(
109+
f_max=0., gamma=0.001, max_displacement=0.01)
110+
system.integrator.run(200)
111+
energy = system.analysis.energy()["total"]
112+
assert energy < energy_target, f"Minimization failed to converge to {energy_target:.1f}"
113+
print("set Langevin")
114+
system.integrator.set_vv()
115+
system.thermostat.set_langevin(kT=1., gamma=1., seed=42)
116+
min_skin = 0.2
117+
max_skin = 1.0
118+
print("Tune skin: {:.3f}".format(system.cell_system.tune_skin(
119+
min_skin=min_skin, max_skin=max_skin, tol=0.05, int_steps=10)))
120+
print("MD equilibration")
121+
system.integrator.run(500)
122+
print("Tune skin: {:.3f}".format(system.cell_system.tune_skin(
123+
min_skin=min_skin, max_skin=max_skin, tol=0.05, int_steps=10)))
124+
print("MD equilibration")
125+
system.integrator.run(500)
126+
system.thermostat.turn_off()
127+
128+
# setup LB solver
129+
lb_kwargs = {"agrid": 1., "tau": system.time_step, "kT": args.kT}
130+
if espresso_version == (4, 2):
131+
assert n_omp_threads == 1, "ESPResSo 4.2 doesn't support OpenMP"
132+
if args.gpu:
133+
lb_class = espressomd.lb.LBFluidGPU
134+
assert args.single_precision, "ESPResSo 4.2 LB GPU only available in single-precision"
135+
assert len(devices) == 1, "ESPResSo 4.2 LB GPU only supports 1 GPU accelerator"
136+
else:
137+
lb_class = espressomd.lb.LBFluid
138+
assert not args.single_precision, "ESPResSo 4.2 LB CPU only available in double-precision"
139+
lbf = lb_class(dens=1., visc=1., seed=42, **lb_kwargs)
140+
system.actors.add(lbf)
141+
else:
142+
if args.gpu:
143+
lb_class = espressomd.lb.LBFluidWalberlaGPU
144+
else:
145+
lb_class = espressomd.lb.LBFluidWalberla
146+
lbf = lb_class(density=1., kinematic_viscosity=1.,
147+
single_precision=args.single_precision, **lb_kwargs)
148+
system.lb = lbf
149+
150+
if n_part:
151+
system.thermostat.set_lb(LB_fluid=lbf, seed=42, gamma=1.)
152+
153+
print("LB equilibration")
154+
system.integrator.run(100)
155+
156+
157+
def get_lb_kT(lbf):
158+
nodes_mass = lbf[:, :, :].density * lbf.agrid**3
159+
nodes_vel_sq = np.sum(np.square(lbf[:, :, :].velocity), axis=3)
160+
return np.mean(nodes_mass * nodes_vel_sq) / 3.
161+
162+
163+
def get_md_kT(part):
164+
return np.mean(np.linalg.norm(part.all().v, axis=1)**2 * part.all().mass) / 3.
165+
166+
167+
print("Sanity checks")
168+
rtol_energy = 0.05
169+
fluid_kTs = []
170+
parts_kTs = []
171+
for _ in range(30):
172+
fluid_kTs.append(get_lb_kT(lbf))
173+
if n_part:
174+
parts_kTs.append(get_md_kT(system.part))
175+
system.integrator.run(10)
176+
if args.kT == 0.:
177+
np.testing.assert_almost_equal(np.mean(fluid_kTs), args.kT, decimal=3)
178+
else:
179+
np.testing.assert_allclose(np.mean(fluid_kTs), args.kT, rtol=rtol_energy)
180+
if n_part:
181+
np.testing.assert_allclose(np.mean(parts_kTs), args.kT, rtol=rtol_energy)
182+
183+
print("Final convergence met with tolerances: \n\
184+
energy: ", rtol_energy, "\n")
185+
186+
print("Benchmark")
187+
n_steps = 40
188+
n_loops = 15
189+
timings = []
190+
for _ in range(n_loops):
191+
tick = time.time()
192+
system.integrator.run(n_steps)
193+
tock = time.time()
194+
timings.append((tock - tick) / n_steps)
195+
196+
print(f"{n_loops * n_steps} steps executed...")
197+
print("Algorithm executed.")
198+
# write results to file
199+
header = '"mode","cores","mpi.x","mpi.y","mpi.z","omp.threads","gpus",\
200+
"particles","mean","std","box.x","box.y","box.z","precision","hardware"'
201+
report = f'''"{"weak scaling" if args.weak_scaling else "strong scaling"}",\
202+
{n_mpi_ranks * n_omp_threads},{node_grid[0]},{node_grid[1]},{node_grid[2]},\
203+
{n_omp_threads},{len(devices)},{len(system.part)},\
204+
{np.mean(timings):.3e},{np.std(timings,ddof=1):.3e},\
205+
{system.box_l[0]:.2f},{system.box_l[1]:.2f},{system.box_l[2]:.2f},\
206+
"{'sp' if args.single_precision else 'dp'}",\
207+
"{'gpu' if args.gpu else 'cpu'}"'''
208+
print(header)
209+
print(report)
210+
211+
print(f"Performance: {np.mean(timings):.3e} s/step")

eessi/testsuite/tests/apps/espresso/src/lj/lj.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,17 @@
1717
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1818
#
1919

20-
import argparse
20+
import os
2121
import time
22-
import espressomd
22+
import argparse
2323
import numpy as np
24-
25-
required_features = ["LENNARD_JONES"]
26-
espressomd.assert_features(required_features)
24+
import espressomd
25+
import espressomd.version
2726

2827
parser = argparse.ArgumentParser(description="Benchmark LJ simulations.")
2928
parser.add_argument("--particles-per-core", metavar="N", action="store",
3029
type=int, default=2000, required=False,
31-
help="Number of particles in the simulation box")
30+
help="Number of particles per MPI rank")
3231
parser.add_argument("--sample-size", metavar="S", action="store",
3332
type=int, default=30, required=False,
3433
help="Sample size")
@@ -38,6 +37,10 @@
3837
"particles (range: [0.01-0.74], default: 0.50)")
3938
args = parser.parse_args()
4039

40+
required_features = ["LENNARD_JONES"]
41+
espressomd.assert_features(required_features)
42+
espresso_version = (espressomd.version.major(), espressomd.version.minor())
43+
4144
# process and check arguments
4245
measurement_steps = 100
4346
if args.particles_per_core < 16000:
@@ -83,9 +86,13 @@ def get_normalized_values_per_atom(system):
8386
lj_sig = 1.0 # particle diameter
8487
lj_cut = lj_sig * 2**(1. / 6.) # cutoff distance
8588

86-
n_proc = system.cell_system.get_state()["n_nodes"]
87-
n_part = n_proc * args.particles_per_core
8889
node_grid = np.array(system.cell_system.node_grid)
90+
n_mpi_ranks = int(np.prod(node_grid))
91+
n_omp_threads = int(os.environ.get("OMP_NUM_THREADS", 1))
92+
if espresso_version == (4, 2):
93+
assert n_omp_threads == 1, "ESPResSo 4.2 doesn't support OpenMP"
94+
95+
n_part = n_mpi_ranks * args.particles_per_core
8996
# volume of N spheres with radius r: N * (4/3*pi*r^3)
9097
box_v = args.particles_per_core * 4. / 3. * \
9198
np.pi * (lj_sig / 2.)**3 / args.volume_fraction
@@ -126,7 +133,7 @@ def get_normalized_values_per_atom(system):
126133
print("Equilibration")
127134
system.integrator.run(min(10 * measurement_steps, 60000))
128135

129-
print("Sampling runtime...")
136+
print("Benchmark")
130137
timings = []
131138
energies = []
132139
pressures = []
@@ -144,18 +151,19 @@ def get_normalized_values_per_atom(system):
144151
sim_pressure = np.mean(pressures)
145152
ref_energy, ref_pressure = get_reference_values_per_atom(args.volume_fraction)
146153

147-
print("Algorithm executed. \n")
154+
print("Algorithm executed.")
148155
np.testing.assert_allclose(sim_energy, ref_energy, atol=0., rtol=0.1)
149156
np.testing.assert_allclose(sim_pressure, ref_pressure, atol=0., rtol=0.1)
150157

151158
print("Final convergence met with relative tolerances: \n\
152159
sim_energy: ", 0.1, "\n\
153160
sim_pressure: ", 0.1, "\n")
154161

155-
header = '"mode","cores","mpi.x","mpi.y","mpi.z","particles","volume_fraction","mean","std"'
156-
report = f'''"weak scaling",{n_proc},{node_grid[0]},{node_grid[1]},\
157-
{node_grid[2]},{len(system.part)},{args.volume_fraction:.4f},\
162+
header = '"mode","cores","mpi.x","mpi.y","mpi.z","omp.threads","particles","volume_fraction","mean","std"'
163+
report = f'''"weak scaling",{n_mpi_ranks * n_omp_threads},{node_grid[0]},{node_grid[1]},\
164+
{node_grid[2]},{n_omp_threads},{len(system.part)},{args.volume_fraction:.4f},\
158165
{np.mean(timings):.3e},{np.std(timings,ddof=1):.3e}'''
159166
print(header)
160167
print(report)
161-
print(f"Performance: {np.mean(timings):.3e}")
168+
169+
print(f"Performance: {np.mean(timings):.3e} s/step")

0 commit comments

Comments
 (0)