Skip to content

Commit

Permalink
Merge pull request #3134 from Crivella/feature-qe_scaling_test
Browse files Browse the repository at this point in the history
[testlib] Added QuantumESPRESSO scalability test for `pw.x`
  • Loading branch information
victorusu authored Mar 20, 2024
2 parents f2419e8 + 091d253 commit 7e6514a
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 0 deletions.
3 changes: 3 additions & 0 deletions docs/hpctestlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ Scientific Applications
:members:
:show-inheritance:

.. automodule:: hpctestlib.sciapps.qespresso.benchmarks
:members:
:show-inheritance:

System
=======================
Expand Down
202 changes: 202 additions & 0 deletions hpctestlib/sciapps/qespresso/benchmarks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""ReFrame benchmark for QuantumESPRESSO"""
import os
from typing import TypeVar

import reframe as rfm
import reframe.utility.sanity as sn
from reframe.core.builtins import (performance_function, run_after, run_before,
sanity_function)
from reframe.core.exceptions import SanityError
from reframe.core.parameters import TestParam as parameter
from reframe.core.variables import TestVar as variable

R = TypeVar('R')

INPUT_TEMPLATE = """&CONTROL
calculation = "scf",
prefix = "Si",
pseudo_dir = ".",
outdir = "./out",
restart_mode = "from_scratch"
verbosity = 'high'
/
&SYSTEM
ibrav = 2,
celldm(1) = 10.2,
nat = 2,
ntyp = 1,
nbnd = {nbnd}
ecutwfc = {ecut}
/
&ELECTRONS
conv_thr = 1.D-8,
mixing_beta = 0.7D0,
/
ATOMIC_SPECIES
Si 28.086 {pseudo}
ATOMIC_POSITIONS
Si 0.00 0.00 0.00
Si 0.25 0.25 0.25
K_POINTS {{automatic}}
15 15 15 0 0 0
"""


@rfm.simple_test
class QEspressoPWCheck(rfm.RunOnlyRegressionTest):
"""QuantumESPRESSO benchmark test.
`QuantumESPRESSO <https://www.quantum-espresso.org/>`__ is an integrated
suite of Open-Source computer codes for electronic-structure calculations
and materials modeling at the nanoscale.
The benchmarks consist of one input file templated inside the code and
a pseudo-potential file that is downloaded from the official repository.
This tests aims at measuring the scalability of the pw.x executable, in
particular the FFT and diagonalization algorithms, by running a simple
silicon calculation with high `ecut` (increases size of FFTs) and `nbnd`
(increases size of matrices to diagonalize) values."""

#: Parametert to tests the performance of the FFTW algorithm,
#: higher `ecut` implicates more FFTs
#:
#: :type: :class:`int`
#: :values: ``[50, 150]``
ecut = parameter([50, 150], loggable=True)

#: Parameter to Tests the performance of the diagonalization algorithm,
#: higher `nbnd` implicates bigger matrices
#:
#: :type: :class:`int`
#: :values: ``[10, 200]``
nbnd = parameter([10, 200], loggable=True)

executable = 'pw.x'
tags = {'sciapp', 'chemistry'}

descr = 'QuantumESPRESSO pw.x benchmark'

#: The name of the input file used.
#:
#: :type: :class:`str`
#: :default: ``'Si.scf.in'``
input_name: str = variable(str, value='Si.scf.in')

#: The pseudo-potential file to be used check
#: https://www.quantum-espresso.org/pseudopotentials/ for more info
#:
#: :type: :class:`str`
#: :default: ``'Si.pbe-n-kjpaw_psl.1.0.0.UPF'``
pp_name: str = variable(str, value='Si.pbe-n-kjpaw_psl.1.0.0.UPF')

@run_after('init')
def prepare_test(self):
"""Hook to the set the downloading of the pseudo-potentials"""
self.prerun_cmds = [(
'wget -q http://pseudopotentials.quantum-espresso.org/upf_files/'
f'{self.pp_name}'
)]
self.executable_opts += [f'-in {self.input_name}']

@run_after('setup')
def write_input(self):
"""Write the input file for the calculation"""
inp_file = os.path.join(self.stagedir, self.input_name)
with open(inp_file, 'w', encoding='utf-8') as file:
file.write(
INPUT_TEMPLATE.format(
ecut=self.ecut,
nbnd=self.nbnd,
pseudo=self.pp_name,
))

@staticmethod
@sn.deferrable
def extractsingle_or_val(*args, on_except_value: str = '0s') -> str:
"""Wrap extractsingle_or_val to return a default value if the regex is
not found.
Returns:
str: The value of the regular expression
"""
try:
res = sn.extractsingle(*args).evaluate()
except SanityError:
res = on_except_value

return res

@staticmethod
@sn.deferrable
def convert_timings(timing: str) -> float:
"""Convert timings to seconds"""

if timing is None:
return 0

days, timing = (['0', '0'] + timing.split('d'))[-2:]
hours, timing = (['0', '0'] + timing.split('h'))[-2:]
minutes, timing = (['0', '0'] + timing.split('m'))[-2:]
seconds = timing.split('s')[0]

return (
float(days) * 86400 +
float(hours) * 3600 +
float(minutes) * 60 +
float(seconds)
)

@performance_function('s')
def extract_report_time(self, name: str = None, kind: str = None) -> float:
"""Extract timings from pw.x stdout
Args:
name (str, optional): Name of the timing to extract.
Defaults to None.
kind (str, optional): Kind ('cpu' or 'wall) of timing to extract.
Defaults to None.
Raises:
ValueError: If the kind is not 'cpu' or 'wall'
Returns:
float: The timing in seconds
"""
if kind is None:
return 0
kind = kind.lower()
if kind == 'cpu':
tag = 1
elif kind == 'wall':
tag = 2
else:
raise ValueError(f'unknown kind: {kind}')

# Possible formats
# PWSCF : 4d 6h19m CPU 10d14h38m WALL
# --> (Should also catch spaces)
return self.convert_timings(
self.extractsingle_or_val(
fr'{name}\s+:\s+(.+)\s+CPU\s+(.+)\s+WALL',
self.stdout, tag, str
))

@run_before('performance')
def set_perf_variables(self):
"""Build a dictionary of performance variables"""

timings = [
'PWSCF', 'electrons', 'c_bands', 'cegterg', 'calbec',
'fft', 'ffts', 'fftw'
]
for name in timings:
for kind in ['cpu', 'wall']:
res = self.extract_report_time(name, kind)
self.perf_variables[f'{name}_{kind}'] = res

@sanity_function
def assert_job_finished(self):
"""Check if the job finished successfully"""
return sn.assert_found(r'JOB DONE', self.stdout)

0 comments on commit 7e6514a

Please sign in to comment.