diff --git a/easyblocks/g/gromacs.py b/easyblocks/g/gromacs.py new file mode 100644 index 0000000000000000000000000000000000000000..54780e7ec30b96b7d4a6c06f421b8bae93c140f4 --- /dev/null +++ b/easyblocks/g/gromacs.py @@ -0,0 +1,441 @@ +## +# Copyright 2013 Ghent University +# +# This file is part of EasyBuild, +# originally created by the HPC team of Ghent University (http://ugent.be/hpc/en), +# with support of Ghent University (http://ugent.be/hpc), +# the Flemish Supercomputer Centre (VSC) (https://www.vscentrum.be), +# Flemish Research Foundation (FWO) (http://www.fwo.be/en) +# and the Department of Economy, Science and Innovation (EWI) (http://www.ewi-vlaanderen.be/en). +# +# https://github.com/easybuilders/easybuild +# +# EasyBuild is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation v2. +# +# EasyBuild is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with EasyBuild. If not, see <http://www.gnu.org/licenses/>. +## +""" +EasyBuild support for building and installing GROMACS, implemented as an easyblock + +@author: Kenneth Hoste (Ghent University) +@author: Ward Poelmans (Ghent University) +@author: Benjamin Roberts (The University of Auckland) +@author: Luca Marsella (CSCS) +@author: Guilherme Peretti-Pezzi (CSCS) +@author: Oliver Stueker (Compute Canada/ACENET) +""" +import glob +import os +import re +import shutil +from distutils.version import LooseVersion + +import easybuild.tools.environment as env +import easybuild.tools.toolchain as toolchain +from easybuild.easyblocks.generic.configuremake import ConfigureMake +from easybuild.easyblocks.generic.cmakemake import CMakeMake +from easybuild.framework.easyconfig import CUSTOM +from easybuild.tools.build_log import EasyBuildError +from easybuild.tools.config import build_option +from easybuild.tools.filetools import download_file, extract_file, which +from easybuild.tools.modules import get_software_libdir, get_software_root, get_software_version +from easybuild.tools.run import run_cmd +from easybuild.tools.systemtools import get_platform_name , get_shared_lib_ext + + +class EB_GROMACS(CMakeMake): + """Support for building/installing GROMACS.""" + + @staticmethod + def extra_options(): + extra_vars = { + 'double_precision': [False, "Build with double precision enabled (-DGMX_DOUBLE=ON)", CUSTOM], + '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_numprocs': [0, "Number of MPI tasks to use when running tests", CUSTOM], + } + return CMakeMake.extra_options(extra_vars) + + def __init__(self, *args, **kwargs): + """Initialize GROMACS-specific variables.""" + super(EB_GROMACS, self).__init__(*args, **kwargs) + self.lib_subdir = '' + self.pre_env = '' + + def get_gromacs_arch(self): + """Determine value of GMX_SIMD CMake flag based on optarch string. + + Refs: + [0] http://manual.gromacs.org/documentation/2016.3/install-guide/index.html#typical-installation + [1] http://manual.gromacs.org/documentation/2016.3/install-guide/index.html#simd-support + [2] http://www.gromacs.org/Documentation/Acceleration_and_parallelization + """ + # default: fall back on autodetection + res = None + + optarch = build_option('optarch') or '' + # take into account that optarch value is a dictionary if it is specified by compiler family + if isinstance(optarch, dict): + comp_fam = self.toolchain.comp_family() + optarch = optarch.get(comp_fam, '') + optarch = optarch.upper() + + if 'AVX512' in optarch and LooseVersion(self.version) >= LooseVersion('2016'): + res = 'AVX_512' + elif 'AVX2' 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: + # 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]. + res = 'SSE2' + + if res: + self.log.info("Target architecture based on optarch configuration option ('%s'): %s", optarch, res) + else: + self.log.info("No target architecture specified based on optarch configuration option ('%s')", optarch) + + return res + + def configure_step(self): + """Custom configuration procedure for GROMACS: set configure options for configure or cmake.""" + + # check whether PLUMED is loaded as a dependency + plumed_root = get_software_root('PLUMED') + if plumed_root: + # Need to check if PLUMED has an engine for this version + engine = 'gromacs-%s' % self.version + + (out, _) = run_cmd("plumed-patch -l", log_all=True, simple=False) + if not re.search(engine, out): + raise EasyBuildError("There is no support in PLUMED version %s for GROMACS %s: %s", + get_software_version('PLUMED'), self.version, out) + + # 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 + + if LooseVersion(self.version) < LooseVersion('4.6'): + self.log.info("Using configure script for configuring GROMACS build.") + # Use static libraries if possible + self.cfg.update('configopts', "--enable-static") + + # Use external BLAS and LAPACK + self.cfg.update('configopts', "--with-external-blas --with-external-lapack") + env.setvar('LIBS', "%s %s" % (os.environ['LIBLAPACK'], os.environ['LIBS'])) + + # Don't use the X window system + self.cfg.update('configopts', "--without-x") + + # OpenMP is not supported for versions older than 4.5. + if LooseVersion(self.version) >= LooseVersion('4.5'): + # enable OpenMP support if desired + if self.toolchain.options.get('openmp', None): + self.cfg.update('configopts', "--enable-threads") + else: + self.cfg.update('configopts', "--disable-threads") + elif self.toolchain.options.get('openmp', None): + raise EasyBuildError("GROMACS version %s does not support OpenMP" % self.version) + + # GSL support + if get_software_root('GSL'): + self.cfg.update('configopts', "--with-gsl") + else: + self.cfg.update('configopts', "--without-gsl") + + # actually run configure via ancestor (not direct parent) + ConfigureMake.configure_step(self) + + # Now patch GROMACS for PLUMED between configure and build + if plumed_root: + run_cmd(plumed_cmd, log_all=True, simple=True) + + else: + # Now patch GROMACS for PLUMED before cmake + if plumed_root: + if LooseVersion(self.version) >= LooseVersion('5.1'): + # Use shared or static patch depending on + # setting of self.toolchain.options.get('dynamic') + # and adapt cmake flags accordingly as per instructions + # from "plumed patch -i" + if self.toolchain.options.get('dynamic', False): + mode = 'shared' + else: + mode = 'static' + plumed_cmd = plumed_cmd + ' -m %s' % mode + + run_cmd(plumed_cmd, log_all=True, simple=True) + + # build a release build + self.cfg.update('configopts', "-DCMAKE_BUILD_TYPE=Release") + + # prefer static libraries, if available + if self.toolchain.options.get('dynamic', False): + self.cfg.update('configopts', "-DGMX_PREFER_STATIC_LIBS=OFF") + else: + self.cfg.update('configopts', "-DGMX_PREFER_STATIC_LIBS=ON") + if plumed_root: + self.cfg.update('configopts', "-DBUILD_SHARED_LIBS=OFF") + + if self.cfg['double_precision']: + self.cfg.update('configopts', "-DGMX_DOUBLE=ON") + + # always specify to use external BLAS/LAPACK + self.cfg.update('configopts', "-DGMX_EXTERNAL_BLAS=ON -DGMX_EXTERNAL_LAPACK=ON") + + # disable GUI tools + self.cfg.update('configopts', "-DGMX_X11=OFF") + + # convince to build for an older architecture than present on the build node by setting GMX_SIMD CMake flag + gmx_simd = self.get_gromacs_arch() + if gmx_simd: + if LooseVersion(self.version) < LooseVersion('5.0'): + self.cfg.update('configopts', "-DGMX_CPU_ACCELERATION=%s" % gmx_simd) + else: + self.cfg.update('configopts', "-DGMX_SIMD=%s" % gmx_simd) + + # set regression test path + prefix = 'regressiontests' + if any([src['name'].startswith(prefix) for src in self.src]): + major_minor_version = '.'.join(self.version.split('.')[:2]) + self.cfg.update('configopts', "-DREGRESSIONTEST_PATH='%%(builddir)s/%s-%%(version)s' " % prefix) + + # enable OpenMP support if desired + if self.toolchain.options.get('openmp', None): + self.cfg.update('configopts', "-DGMX_OPENMP=ON") + else: + self.cfg.update('configopts', "-DGMX_OPENMP=OFF") + + # disable MPI support for initial, serial/SMP build; actual MPI build is done later + self.cfg.update('configopts', "-DGMX_MPI=OFF") + + # explicitly disable GPU support if CUDA is not available, + # to avoid that GROMACS find and uses a system-wide CUDA compiler + cuda = get_software_root('CUDA') + if cuda: + self.cfg.update('configopts', "-DGMX_GPU=ON -DCUDA_TOOLKIT_ROOT_DIR=%s" % cuda) + else: + self.cfg.update('configopts', "-DGMX_GPU=OFF") + + if get_software_root('imkl'): + # using MKL for FFT, so it will also be used for BLAS/LAPACK + self.cfg.update('configopts', '-DGMX_FFT_LIBRARY=mkl -DMKL_INCLUDE_DIR="$EBROOTMKL/mkl/include" ') + mkl_libs = [os.path.join(os.getenv('LAPACK_LIB_DIR'), lib) for lib in ['libmkl_lapack.a']] + self.cfg.update('configopts', '-DMKL_LIBRARIES="%s" ' % ';'.join(mkl_libs)) + else: + shlib_ext = get_shared_lib_ext() + for libname in ['BLAS', 'LAPACK']: + libdir = os.getenv('%s_LIB_DIR' % libname) + if self.toolchain.toolchain_family() == toolchain.CRAYPE: + libsci_mpi_mp_lib = glob.glob(os.path.join(libdir, 'libsci_*_mpi_mp.a')) + if libsci_mpi_mp_lib: + self.cfg.update('configopts', '-DGMX_%s_USER=%s' % (libname, libsci_mpi_mp_lib[0])) + else: + raise EasyBuildError("Failed to find libsci library to link with for %s", libname) + else: + # -DGMX_BLAS_USER & -DGMX_LAPACK_USER require full path to library + libs = os.getenv('%s_STATIC_LIBS' % libname).split(',') + libpaths = [os.path.join(libdir, lib) for lib in libs if lib != 'libgfortran.a'] + self.cfg.update('configopts', '-DGMX_%s_USER="%s"' % (libname, ';'.join(libpaths))) + # if libgfortran.a is listed, make sure it gets linked in too to avoiding linking issues + if 'libgfortran.a' in libs: + env.setvar('LDFLAGS', "%s -lgfortran -lm" % os.environ.get('LDFLAGS', '')) + + # no more GSL support in GROMACS 5.x, see http://redmine.gromacs.org/issues/1472 + if LooseVersion(self.version) < LooseVersion('5.0'): + # enable GSL when it's provided + if get_software_root('GSL'): + self.cfg.update('configopts', "-DGMX_GSL=ON") + else: + self.cfg.update('configopts', "-DGMX_GSL=OFF") + + # include flags for linking to zlib/XZ in $LDFLAGS if they're listed as a dep; + # this is important for the tests, to correctly link against libxml2 + for dep, link_flag in [('XZ', '-llzma'), ('zlib', '-lz')]: + root = get_software_root(dep) + if root: + libdir = get_software_libdir(dep) + ldflags = os.environ.get('LDFLAGS', '') + env.setvar('LDFLAGS', "%s -L%s %s" % (ldflags, os.path.join(root, libdir), link_flag)) + + # complete configuration with configure_method of parent + self.cfg['separate_build_dir'] = True + out = super(EB_GROMACS, self).configure_step() + + # for recent GROMACS versions, make very sure that a decent BLAS, LAPACK and FFT is found and used + if LooseVersion(self.version) >= LooseVersion('4.6.5'): + patterns = [ + r"Using external FFT library - \S*", + r"Looking for dgemm_ - found", + r"Looking for cheev_ - found", + ] + for pattern in patterns: + regex = re.compile(pattern, re.M) + if not regex.search(out): + raise EasyBuildError("Pattern '%s' not found in GROMACS configuration output.", pattern) + + def test_step(self): + """Run the basic tests (but not necessarily the full regression tests) using make check""" + # allow to escape testing by setting runtest to False + #if not self.cfg['runtest'] and not isinstance(self.cfg['runtest'], bool): + + # make very sure OMP_NUM_THREADS is set to 1, to avoid hanging GROMACS regression test + # env.setvar('OMP_NUM_THREADS', '1') + + # self.cfg['runtest'] = 'check' + # super(EB_GROMACS, self).test_step() + + def install_step(self): + """ + Custom install step for GROMACS; figure out where libraries were installed to. + Also, install the MPI version of the executable in a separate step. + """ + # run 'make install' in parallel since it involves more compilation + self.cfg.update('installopts', "-j %s" % self.cfg['parallel']) + super(EB_GROMACS, self).install_step() + + # the GROMACS libraries get installed in different locations (deeper subdirectory), depending on the platform; + # this is determined by the GNUInstallDirs CMake module; + # rather than trying to replicate the logic, we just figure out where the library was placed + + if self.toolchain.options.get('dynamic', False): + self.libext = get_shared_lib_ext() + else: + self.libext = 'a' + + if LooseVersion(self.version) < LooseVersion('5.0'): + libname = 'libgmx*.%s' % self.libext + else: + libname = 'libgromacs*.%s' % self.libext + + for libdir in ['lib', 'lib64']: + if os.path.exists(os.path.join(self.installdir, libdir)): + for subdir in [libdir, os.path.join(libdir, '*')]: + libpaths = glob.glob(os.path.join(self.installdir, subdir, libname)) + if libpaths: + self.lib_subdir = os.path.dirname(libpaths[0])[len(self.installdir)+1:] + self.log.info("Found lib subdirectory that contains %s: %s", libname, self.lib_subdir) + break + if not self.lib_subdir: + raise EasyBuildError("Failed to determine lib subdirectory in %s", self.installdir) + + # Install a version with the MPI suffix + if self.toolchain.options.get('usempi', None): + if LooseVersion(self.version) < LooseVersion('4.6'): + + cmd = "make distclean" + (out, _) = run_cmd(cmd, log_all=True, simple=False) + + self.cfg.update('configopts', "--enable-mpi --program-suffix={0}".format(self.cfg['mpisuffix'])) + ConfigureMake.configure_step(self) + + super(EB_GROMACS, self).build_step() + + super(EB_GROMACS, self).install_step() + + else: + self.cfg['configopts'] = re.sub(r'-DGMX_MPI=OFF', r'', self.cfg['configopts']) + + if self.cfg['mpi_numprocs'] == 0: + self.log.info("No number of test MPI tasks specified -- using default: %s" % self.cfg['parallel']) + self.cfg['mpi_numprocs'] = self.cfg['parallel'] + + elif self.cfg['mpi_numprocs'] > self.cfg['parallel']: + self.log.warning("Number of test MPI tasks (%s) is greater than value for 'parallel': %s", + self.cfg['mpi_numprocs'], self.cfg['parallel']) + + self.cfg.update('configopts', "-DGMX_MPI=ON -DGMX_THREAD_MPI=OFF") + + mpiexec = which(self.cfg['mpiexec']) + if mpiexec: + self.cfg.update('configopts', "-DMPIEXEC=%s" % mpiexec) + self.cfg.update('configopts', "-DMPIEXEC_NUMPROC_FLAG=%s" % self.cfg['mpiexec_numproc_flag']) + self.cfg.update('configopts', "-DNUMPROC=%s" % self.cfg['mpi_numprocs']) + elif self.cfg['runtest']: + raise EasyBuildError("'%s' not found in $PATH", self.cfg['mpiexec']) + + + self.log.info("Using %s as MPI executable when testing, with numprocs flag '%s' and %s tasks", + self.cfg['mpiexec'], self.cfg['mpiexec_numproc_flag'], self.cfg['mpi_numprocs']) + + # clean up obj dir before reconfiguring + shutil.rmtree(os.path.join(self.builddir, 'easybuild_obj')) + + # rebuild/test/install with MPI options + super(EB_GROMACS, self).configure_step() + super(EB_GROMACS, self).build_step() + super(EB_GROMACS, self).test_step() + super(EB_GROMACS, self).install_step() + + self.log.info("A full regression test suite is available from the GROMACS web site") + + def make_module_req_guess(self): + """Custom library subdirectories for GROMACS.""" + guesses = super(EB_GROMACS, self).make_module_req_guess() + guesses.update({ + 'LD_LIBRARY_PATH': [self.lib_subdir], + 'LIBRARY_PATH': [self.lib_subdir], + 'PKG_CONFIG_PATH': [os.path.join(self.lib_subdir, 'pkgconfig')], + }) + return guesses + + def sanity_check_step(self): + """Custom sanity check for GROMACS.""" + + dirs = [os.path.join('include', 'gromacs')] + + # in GROMACS v5.1, only 'gmx' binary is there + # (only) in GROMACS v5.0, other binaries are symlinks to 'gmx' + bins = [] + libnames = [] + if LooseVersion(self.version) < LooseVersion('5.1'): + bins.extend(['editconf', 'g_lie', 'genbox', 'genconf', 'mdrun']) + + if LooseVersion(self.version) >= LooseVersion('5.0'): + bins.append('gmx') + libnames.append('gromacs') + if LooseVersion(self.version) < LooseVersion('5.1') and self.toolchain.options.get('usempi', None): + bins.append('mdrun') + else: + libnames.extend(['gmxana', 'gmx', 'md']) + # note: gmxpreprocess may also already be there for earlier versions + if LooseVersion(self.version) > LooseVersion('4.6'): + libnames.append('gmxpreprocess') + + # also check for MPI-specific binaries/libraries + if self.toolchain.options.get('usempi', None): + if LooseVersion(self.version) < LooseVersion('4.6'): + mpisuff = self.cfg['mpisuffix'] + else: + mpisuff = '_mpi' + + bins.extend([binary + mpisuff for binary in bins]) + libnames.extend([libname + mpisuff for libname in libnames]) + + suff = '' + # add the _d suffix to the suffix, in case of the double precission + if re.search('DGMX_DOUBLE=(ON|YES|TRUE|Y|[1-9])', self.cfg['configopts'], re.I): + suff = '_d' + + libs = ['lib%s%s.%s' % (libname, suff, self.libext) for libname in libnames] + + # pkgconfig dir not available for earlier versions, exact version to use here is unclear + if LooseVersion(self.version) >= LooseVersion('4.6'): + dirs.append(os.path.join(self.lib_subdir, 'pkgconfig')) + + custom_paths = { + 'files': [os.path.join('bin', b + suff) for b in bins] + [os.path.join(self.lib_subdir, l) for l in libs], + 'dirs': dirs, + } + super(EB_GROMACS, self).sanity_check_step(custom_paths=custom_paths)