diff --git a/easybuild/easyblocks/g/gromacs.py b/easybuild/easyblocks/g/gromacs.py index f5f0f5ac7a4..7a5d195dd72 100644 --- a/easybuild/easyblocks/g/gromacs.py +++ b/easybuild/easyblocks/g/gromacs.py @@ -70,10 +70,15 @@ def extra_options(): 'mpisuffix': ['_mpi', "Suffix to append to MPI-enabled executables (only for GROMACS < 4.6)", CUSTOM], 'mpiexec': ['mpirun', "MPI executable to use when running tests", CUSTOM], 'mpiexec_numproc_flag': ['-np', "Flag to introduce the number of MPI tasks when running tests", CUSTOM], + 'mpi_only': [False, "Only build for MPI and skip nompi.", CUSTOM], 'mpi_numprocs': [0, "Number of MPI tasks to use when running tests", CUSTOM], + 'python_pkg': [None, "Build gmxapi Python package. None (default) is auto-detect." + + "True or False forces behaviour.", CUSTOM], 'ignore_plumed_version_check': [False, "Ignore the version compatibility check for PLUMED", CUSTOM], - 'plumed': [None, "Try to apply PLUMED patches. None (default) is auto-detect. " + - "True or False forces behaviour.", CUSTOM], + 'plumed': [None, "Try to enable PLUMED support. None (default) is auto-detect. " + + "'native' enables native PLUMED support for GROMACS 2025 and newer." + + "'patch' (or True) applies PLUMED patches." + + "False disables PLUMED support.", CUSTOM], }) return extra_vars @@ -117,13 +122,13 @@ def get_gromacs_arch(self): # http://manual.gromacs.org/documentation/2018/install-guide/index.html#simd-support if 'MIC-AVX512' in optarch and LooseVersion(self.version) >= LooseVersion('2016'): res = 'AVX_512_KNL' - elif 'AVX512' in optarch and LooseVersion(self.version) >= LooseVersion('2016'): + elif ('AVX512' in optarch or 'X86-64-V4' in optarch) and LooseVersion(self.version) >= LooseVersion('2016'): res = 'AVX_512' - elif 'AVX2' in optarch and LooseVersion(self.version) >= LooseVersion('5.0'): + elif ('AVX2' in optarch or 'X86-64-V3' in optarch) and LooseVersion(self.version) >= LooseVersion('5.0'): res = 'AVX2_256' elif 'AVX' in optarch: res = 'AVX_256' - elif 'SSE3' in optarch or 'SSE2' in optarch or 'MARCH=NOCONA' in optarch: + elif 'SSE3' in optarch or 'SSE2' in optarch or 'MARCH=NOCONA' in optarch or 'X86-64-V2' in optarch: # Gromacs doesn't have any GMX_SIMD=SSE3 but only SSE2 and SSE4.1 [1]. # According to [2] the performance difference between SSE2 and SSE4.1 is minor on x86 # and SSE4.1 is not supported by AMD Magny-Cours[1]. @@ -215,35 +220,73 @@ def configure_step(self): self.cfg.update('configopts', "-DGMX_GPU=OFF") # PLUMED detection - # enable PLUMED support if PLUMED is listed as a dependency - # and PLUMED support is either explicitly enabled (plumed = True) or unspecified ('plumed' not defined) + # enable PLUMED support if PLUMED is listed as a dependency. + # plumed = 'native' will enable GROMACS' native PLUMED support ('-DGMX_USE_PLUMED=ON') + # for GROMACS 2025 and newer. plumed = 'patch' specifically requests PLUMED patches. + # In auto-detect ('plumed = None' or not defined) will prefer native support for 2026 + # and newer. Older versions of GROMACS patches are applied to enable PLUMED support. + # plumed = True behaves like plumed = 'patch' for backwards compatibility. plumed_root = get_software_root('PLUMED') + plumed_patches = False if self.cfg['plumed'] and not plumed_root: msg = "PLUMED support has been requested but PLUMED is not listed as a dependency." raise EasyBuildError(msg) elif plumed_root and self.cfg['plumed'] is False: self.log.info('PLUMED was found, but compilation without PLUMED has been requested.') plumed_root = None + elif plumed_root and self.cfg['plumed'] == 'patch': + self.log.info('PLUMED was found, and PLUMED patching has been requested.') + plumed_patches = True + elif plumed_root and self.cfg['plumed'] == 'native': + msg = 'PLUMED was found, and native PLUMED support has been requested.' + if gromacs_version >= '2025': + msg += ' Will use native PLUMED support.' + plumed_patches = False + self.log.info(msg) + else: + msg += " Native PLUMED support is only available with GROMACS 2025 and newer." + raise EasyBuildError(msg) + elif plumed_root and self.cfg['plumed'] is True: + msg = 'PLUMED was found, and PLUMED support has been requested.' + msg += ' Will apply PLUMED patches.' + plumed_patches = True + self.log.info(msg) + elif plumed_root and self.cfg['plumed'] is None: + msg = 'PLUMED was found.' + if gromacs_version >= '2026': + # Even though native support is available since GROMACS 2025, we'll only use + # it as default with 2026 and newer to avoid a sudden change in behaviour. + msg += ' Will use native PLUMED support.' + plumed_patches = False + else: + msg += ' Will apply PLUMED patches.' + plumed_patches = True + self.log.info(msg) if plumed_root: self.log.info('PLUMED support has been enabled.') - # Need to check if PLUMED has an engine for this version - engine = 'gromacs-%s' % self.version + if gromacs_version >= '2025' and plumed_patches is False: + self.log.info('Native PLUMED support has been enabled.') + self.cfg.update('configopts', '-DGMX_USE_PLUMED=ON') - res = run_shell_cmd("plumed-patch -l") - if not re.search(engine, res.output): - plumed_ver = get_software_version('PLUMED') - msg = "There is no support in PLUMED version %s for GROMACS %s: %s" % (plumed_ver, self.version, - res.output) - if self.cfg['ignore_plumed_version_check']: - self.log.warning(msg) - else: - raise EasyBuildError(msg) + else: + # Need to check if PLUMED has an engine for this version + engine = 'gromacs-%s' % self.version + + res = run_shell_cmd("plumed-patch -l") + if not re.search(engine, res.output): + plumed_ver = get_software_version('PLUMED') + msg = "There is no support in PLUMED version %s for GROMACS %s: %s" % (plumed_ver, self.version, + res.output) + if self.cfg['ignore_plumed_version_check']: + self.log.warning(msg) + else: + raise EasyBuildError(msg) - # PLUMED patching must be done at different stages depending on - # version of GROMACS. Just prepare first part of cmd here - plumed_cmd = "plumed-patch -p -e %s" % engine + # PLUMED patching must be done at different stages depending on + # version of GROMACS. Just prepare first part of cmd here + plumed_cmd = "plumed-patch -p -e %s" % engine # Ensure that the GROMACS log files report how the code was patched # during the build, so that any problems are easier to diagnose. @@ -251,7 +294,7 @@ def configure_step(self): if (gromacs_version >= '2020' and '-DGMX_VERSION_STRING_OF_FORK=' not in self.cfg['configopts']): gromacs_version_string_suffix = 'EasyBuild-%s' % EASYBUILD_VERSION - if plumed_root: + if plumed_patches: gromacs_version_string_suffix += '-PLUMED-%s' % get_software_version('PLUMED') self.cfg.update('configopts', '-DGMX_VERSION_STRING_OF_FORK=%s' % gromacs_version_string_suffix) @@ -291,7 +334,8 @@ def configure_step(self): ConfigureMake.configure_step(self) # Now patch GROMACS for PLUMED between configure and build - if plumed_root: + if plumed_root and plumed_patches: + self.log.info('Applying PLUMED patches to GROMACS.') run_shell_cmd(plumed_cmd) else: @@ -330,7 +374,14 @@ def configure_step(self): if gromacs_version >= '2020': # build Python bindings if Python is loaded as a dependency python_root = get_software_root('Python') - if python_root: + if self.cfg['python_pkg'] is True and not python_root: + msg = "Building Python gmxapi has been requested but Python is not listed as a dependency." + raise EasyBuildError(msg) + elif python_root and self.cfg['python_pkg'] is False: + msg = "Python was found, but compilation without Python gmxapi has been requested." + self.log.info(msg) + self.cfg.update('configopts', "-DGMX_PYTHON_PACKAGE=OFF") + elif python_root: self.cfg.update('configopts', "-DGMX_PYTHON_PACKAGE=ON") bin_python = os.path.join(python_root, 'bin', 'python') # For find_package(PythonInterp) @@ -340,7 +391,7 @@ def configure_step(self): self.cfg.update('configopts', "-DPython3_FIND_VIRTUALENV=STANDARD") # Now patch GROMACS for PLUMED before cmake - if plumed_root: + if plumed_root and plumed_patches: if gromacs_version >= '5.1': # Use shared or static patch depending on # setting of self.cfg['build_shared_libs'] @@ -655,8 +706,12 @@ def sanity_check_step(self): else: mpisuff = '_mpi' - mpi_bins.extend([binary + mpisuff for binary in mpi_bins]) - mpi_libnames.extend([libname + mpisuff for libname in mpi_libnames]) + if self.cfg['mpi_only']: + mpi_bins = [binary + mpisuff for binary in mpi_bins] + mpi_libnames = [libname + mpisuff for libname in mpi_libnames] + else: + mpi_bins.extend([binary + mpisuff for binary in mpi_bins]) + mpi_libnames.extend([libname + mpisuff for libname in mpi_libnames]) suffixes = [''] @@ -769,9 +824,12 @@ def run_all_steps(self, *args, **kwargs): if precisions == []: raise EasyBuildError("No precision selected. At least one of single/double_precision must be unset or True") - mpitypes = ['nompi'] - if self.toolchain.options.get('usempi', None): - mpitypes.append('mpi') + if self.cfg['mpi_only']: + mpitypes = ['mpi'] + else: + mpitypes = ['nompi'] + if self.toolchain.options.get('usempi', None): + mpitypes.append('mpi') # We need to count the number of variations to build. versions_built = []