diff --git a/easyblocks/c/cp2k-cuda.py b/easyblocks/c/cp2k-cuda.py new file mode 100644 index 0000000000000000000000000000000000000000..1adfd2f7cc506f57e58921d430ef657b5a2b7a8f --- /dev/null +++ b/easyblocks/c/cp2k-cuda.py @@ -0,0 +1,970 @@ +# IT4Innovations 2022 +# JK +# EasyBlock for CP2K CUDA support +## +# Copyright 2009-2021 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 CP2K, implemented as an easyblock + +@author: Stijn De Weirdt (Ghent University) +@author: Dries Verdegem (Ghent University) +@author: Kenneth Hoste (Ghent University) +@author: Pieter De Baets (Ghent University) +@author: Jens Timmerman (Ghent University) +@author: Ward Poelmans (Ghent University) +@author: Luca Marsella (CSCS) +@author: Damian Alvarez (Forschungszentrum Juelich GmbH) +@author: Alan O'Cais (Forschungszentrum Juelich GmbH) +@author: Balazs Hajgato (Free University Brussels (VUB)) +""" + +import fileinput +import glob +import re +import os +import sys +from distutils.version import LooseVersion + +import easybuild.tools.toolchain as toolchain +from easybuild.framework.easyblock import EasyBlock +from easybuild.framework.easyconfig import CUSTOM +from easybuild.tools.build_log import EasyBuildError +from easybuild.tools.environment import setvar +from easybuild.tools.filetools import change_dir, copy_dir, copy_file, mkdir, write_file +from easybuild.tools.config import build_option +from easybuild.tools.modules import get_software_root, get_software_version +from easybuild.tools.run import run_cmd +from easybuild.tools.systemtools import get_avail_core_count + + +class EB_CP2K(EasyBlock): + """ + Support for building CP2K + - prepare module include files if required + - generate custom config file in 'arch' directory + - build CP2K + - run regression test if desired + - install by copying binary executables + """ + + def __init__(self, *args, **kwargs): + super(EB_CP2K, self).__init__(*args, **kwargs) + + self.typearch = None + + # this should be set to False for old versions of GCC (e.g. v4.1) + self.compilerISO_C_BINDING = True + + # compiler options that need to be set in Makefile + self.debug = '' + self.fpic = '' + + # used for both libsmm and libxsmm + self.libsmm = '' + self.modincpath = '' + self.openmp = '' + + self.make_instructions = '' + + @staticmethod + def extra_options(): + extra_vars = { + 'extracflags': ['', "Extra CFLAGS to be added", CUSTOM], + 'extradflags': ['', "Extra DFLAGS to be added", CUSTOM], + 'gpuver': [None, "Value for GPUVER configuration setting, specifies type of GPU to build for", CUSTOM], + 'ignore_regtest_fails': [False, ("Ignore failures in regression test " + "(should be used with care)"), CUSTOM], + 'library': [False, "Also build CP2K as a library", CUSTOM], + 'maxtasks': [4, ("Maximum number of CP2K instances run at " + "the same time during testing"), CUSTOM], + 'modinc': [[], ("List of modinc's to use (*.f90], or 'True' to use " + "all found at given prefix"), CUSTOM], + 'modincprefix': ['', "Intel MKL prefix for modinc include dir", CUSTOM], + 'omp_num_threads': [None, "Value to set $OMP_NUM_THREADS to during testing", CUSTOM], + 'plumed': [None, "Enable PLUMED support", CUSTOM], + 'runtest': [True, "Build and run CP2K tests", CUSTOM], + 'type': ['popt', "Type of build ('popt' or 'psmp')", CUSTOM], + 'typeopt': [True, "Enable optimization", CUSTOM], + } + return EasyBlock.extra_options(extra_vars) + + def _generate_makefile(self, options): + """Generate Makefile based on options dictionary and optional make instructions""" + + text = "# Makefile generated by CP2K easyblock in EasyBuild\n" + for key, value in sorted(options.items()): + text += "%s = %s\n" % (key, value) + return text + self.make_instructions + + def configure_step(self): + """Configure build + - build Libint wrapper + - generate Makefile + """ + + known_types = ['popt', 'psmp'] + if self.cfg['type'] not in known_types: + raise EasyBuildError("Unknown build type specified: '%s', known types are %s", + self.cfg['type'], known_types) + + # correct start dir, if needed + # recent CP2K versions have a 'cp2k' dir in the unpacked 'cp2k' dir + cp2k_path = os.path.join(self.cfg['start_dir'], 'cp2k') + if os.path.exists(cp2k_path): + self.cfg['start_dir'] = cp2k_path + self.log.info("Corrected start_dir to %s" % self.cfg['start_dir']) + + # set compilers options according to toolchain config + # full debug: -g -traceback -check all -fp-stack-check + # -g links to mpi debug libs + if self.toolchain.options['debug']: + self.debug = '-g' + self.log.info("Debug build") + if self.toolchain.options['pic']: + self.fpic = "-fPIC" + self.log.info("Using fPIC") + + # report on extra flags being used + if self.cfg['extracflags']: + self.log.info("Using extra CFLAGS: %s" % self.cfg['extracflags']) + if self.cfg['extradflags']: + self.log.info("Using extra DFLAGS: %s" % self.cfg['extradflags']) + + # lib(x)smm support + libsmm = get_software_root('libsmm') + libxsmm = get_software_root('libxsmm') + if libxsmm: + self.cfg.update('extradflags', '-D__LIBXSMM') + self.libsmm = '-lxsmm -lxsmmf' + self.log.debug('Using libxsmm %s' % libxsmm) + elif libsmm: + libsmms = glob.glob(os.path.join(libsmm, 'lib', 'libsmm_*nn.a')) + dfs = [os.path.basename(os.path.splitext(x)[0]).replace('lib', '-D__HAS_') for x in libsmms] + moredflags = ' ' + ' '.join(dfs) + self.cfg.update('extradflags', moredflags) + self.libsmm = ' '.join(libsmms) + self.log.debug('Using libsmm %s (extradflags %s)' % (self.libsmm, moredflags)) + + # obtain list of modinc's to use + if self.cfg["modinc"]: + self.modincpath = self.prepmodinc() + + # set typearch + self.typearch = "Linux-x86-64-%s" % self.toolchain.name + + # extra make instructions + self.make_instructions = '' # "graphcon.o: graphcon.F\n\t$(FC) -c $(FCFLAGS2) $<\n" + + # compiler toolchain specific configuration + comp_fam = self.toolchain.comp_family() + if comp_fam == toolchain.INTELCOMP: + options = self.configure_intel_based() + elif comp_fam == toolchain.GCC: + options = self.configure_GCC_based() + else: + raise EasyBuildError("Don't know how to tweak configuration for compiler family %s" % comp_fam) + + # BLAS/LAPACK/FFTW + if get_software_root('imkl'): + options = self.configure_MKL(options) + else: + # BLAS + if get_software_root('ACML'): + options = self.configure_ACML(options) + else: + options = self.configure_BLAS_lib(options) + + # FFTW (no MKL involved) + if 'fftw3' in os.getenv('LIBFFT', ''): + options = self.configure_FFTW3(options) + + # LAPACK + if os.getenv('LIBLAPACK_MT', None) is not None: + options = self.configure_LAPACK(options) + + if os.getenv('LIBSCALAPACK', None) is not None: + options = self.configure_ScaLAPACK(options) + + # PLUMED + plumed = get_software_root('PLUMED') + if self.cfg['plumed'] and not plumed: + raise EasyBuildError("The PLUMED module needs to be loaded to build CP2K with PLUMED support") + + # enable PLUMED support if PLUMED is listed as a dependency + # and PLUMED support is either explicitly enabled (plumed = True) or unspecified ('plumed' not defined) + if plumed and (self.cfg['plumed'] or self.cfg['plumed'] is None): + options['LIBS'] += ' -lplumed' + options['DFLAGS'] += ' -D__PLUMED2' + + # ELPA + elpa = get_software_root('ELPA') + if elpa: + options['LIBS'] += ' -lelpa' + elpa_inc_dir = os.path.join(elpa, 'include', 'elpa-%s' % get_software_version('ELPA'), 'modules') + options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir + if LooseVersion(self.version) >= LooseVersion('6.1'): + elpa_ver = ''.join(get_software_version('ELPA').split('.')[:2]) + options['DFLAGS'] += ' -D__ELPA=%s' % elpa_ver + elpa_inc_dir = os.path.join(elpa, 'include', 'elpa-%s' % get_software_version('ELPA'), 'elpa') + options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir + else: + options['DFLAGS'] += ' -D__ELPA3' + + # CUDA support + # see https://github.com/cp2k/cp2k/blob/master/INSTALL.md#2j-cuda-optional-improved-performance-on-gpu-systems + cuda = get_software_root('CUDA') + if cuda: + # determine CUDA compute capability to use based on --cuda-compute-capabilities in EasyBuild configuration, + # or cuda_compute_capabilities easyconfig parameter (fallback); + # must be a single value to build CP2K with CUDA support! + cuda_cc = build_option('cuda_compute_capabilities') or self.cfg.get('cuda_compute_capabilities') + if len(cuda_cc) == 1: + cuda_cc = cuda_cc[0] + elif cuda_cc: + error_msg = "Exactly one CUDA compute capability must be specified, found %d: %s" + raise EasyBuildError(error_msg, len(cuda_cc), ', '.join(cuda_cc)) + else: + error_msg = "Exactly one CUDA compute capability must be specified via " + error_msg += "--cuda-compute-capabilities or the cuda_compute_capabilities easyconfig parameter." + raise EasyBuildError(error_msg) + + # GPUVER must be set, required by the DBCSR component, + # see exts/dbcsr/Makefile and the parameters_*.json in src/acc/libsmm_acc/libcusmm/; + # determine string value to use based on select CUDA compute capability, unless specified explicitly via + # custom 'gpuver' easyconfig parameter + gpuver = self.cfg['gpuver'] + + if gpuver is None: + cuda_cc_lv = LooseVersion(cuda_cc) + known_gpuver = [ + ('7.0', 'V100'), + ('6.0', 'P100'), + ('3.7', 'K80'), + ('3.5', 'K40'), + ] + for min_cuda_cc, val in known_gpuver: + if cuda_cc_lv >= LooseVersion(min_cuda_cc): + gpuver = val + break + + if gpuver is None: + raise EasyBuildError("Failed to determine value for required GPUVER setting!") + else: + options['GPUVER'] = gpuver + + options['DFLAGS'] += ' -D__ACC -D__DBCSR_ACC -D__PW_CUDA -D__GRID_CUDA' + options['LIBS'] += ' -lcudart -lnvrtc -lcuda -lcublas -lcufft -lrt' + options['NVCC'] = 'nvcc' + options['NVFLAGS'] = ' '.join([ + options['DFLAGS'], + '-O3', + '--std=c++11', + '-arch sm_%s' % cuda_cc.replace('.', ''), + # control host compilers + options + "-ccbin='%s'" % os.getenv('CXX'), + "-Xcompiler='%s'" % os.getenv('CXXFLAGS'), + ]) + + # avoid group nesting + options['LIBS'] = options['LIBS'].replace('-Wl,--start-group', '').replace('-Wl,--end-group', '') + + options['LIBS'] = "-Wl,--start-group %s -Wl,--end-group" % options['LIBS'] + + # specify correct location for 'data' directory in final installation + options['DATA_DIR'] = os.path.join(self.installdir, 'data') + + # create arch file using options set + archfile = os.path.join(self.cfg['start_dir'], 'arch', '%s.%s' % (self.typearch, self.cfg['type'])) + txt = self._generate_makefile(options) + write_file(archfile, txt) + self.log.info("Content of makefile (%s):\n%s" % (archfile, txt)) + + def prepmodinc(self): + """Prepare list of module files""" + + self.log.debug("Preparing module files") + + imkl = get_software_root('imkl') + + if imkl: + + # prepare modinc target path + modincpath = os.path.join(os.path.dirname(os.path.normpath(self.cfg['start_dir'])), 'modinc') + self.log.debug("Preparing module files in %s" % modincpath) + + mkdir(modincpath, parents=True) + + # get list of modinc source files + modincdir = os.path.join(imkl, self.cfg["modincprefix"], 'include') + + if isinstance(self.cfg["modinc"], list): + modfiles = [os.path.join(modincdir, x) for x in self.cfg["modinc"]] + + elif isinstance(self.cfg["modinc"], bool) and self.cfg["modinc"]: + modfiles = glob.glob(os.path.join(modincdir, '*.f90')) + + else: + raise EasyBuildError("prepmodinc: Please specify either a boolean value or a list of files in modinc " + "(found: %s).", self.cfg["modinc"]) + + f77 = os.getenv('F77') + if not f77: + raise EasyBuildError("F77 environment variable not set, can't continue.") + + # create modinc files + for f in modfiles: + if f77.endswith('ifort'): + cmd = "%s -module %s -c %s" % (f77, modincpath, f) + elif f77 in ['gfortran', 'mpif77']: + cmd = "%s -J%s -c %s" % (f77, modincpath, f) + else: + raise EasyBuildError("prepmodinc: Unknown value specified for F77 (%s)", f77) + + run_cmd(cmd, log_all=True, simple=True) + + return modincpath + else: + raise EasyBuildError("Don't know how to prepare modinc, imkl not found") + + def configure_common(self): + """Common configuration for all toolchains""" + + # openmp introduces 2 major differences + # -automatic is default: -noautomatic -auto-scalar + # some mem-bandwidth optimisation + if self.cfg['type'] == 'psmp': + self.openmp = self.toolchain.get_flag('openmp') + + # determine which opt flags to use + if self.cfg['typeopt']: + optflags = 'OPT' + regflags = 'OPT2' + else: + optflags = 'NOOPT' + regflags = 'NOOPT' + + # make sure a MPI-2 able MPI lib is used + mpi2 = False + if hasattr(self.toolchain, 'MPI_FAMILY') and self.toolchain.MPI_FAMILY is not None: + known_mpi2_fams = [toolchain.MPICH, toolchain.MPICH2, toolchain.MVAPICH2, toolchain.OPENMPI, + toolchain.INTELMPI] + mpi_fam = self.toolchain.mpi_family() + if mpi_fam in known_mpi2_fams: + mpi2 = True + self.log.debug("Determined MPI2 compatibility based on MPI toolchain component: %s" % mpi_fam) + else: + self.log.debug("Cannot determine MPI2 compatibility based on MPI toolchain component: %s" % mpi_fam) + else: + # can't use toolchain.mpi_family, because of system toolchain + mpi2libs = ['impi', 'MVAPICH2', 'OpenMPI', 'MPICH2', 'MPICH'] + for mpi2lib in mpi2libs: + if get_software_root(mpi2lib): + mpi2 = True + self.log.debug("Determined MPI2 compatibility based on loaded MPI module: %s") + else: + self.log.debug("MPI-2 supporting MPI library %s not loaded.") + + if not mpi2: + raise EasyBuildError("CP2K needs MPI-2, no known MPI-2 supporting library loaded?") + + cppflags = os.getenv('CPPFLAGS') + ldflags = os.getenv('LDFLAGS') + cflags = os.getenv('CFLAGS') + fflags = os.getenv('FFLAGS') + fflags_lowopt = re.sub('-O[0-9]', '-O1', fflags) + options = { + 'CC': os.getenv('MPICC'), + 'CPP': '', + 'FC': '%s %s' % (os.getenv('MPIF90'), self.openmp), + 'LD': '%s %s' % (os.getenv('MPIF90'), self.openmp), + 'AR': 'ar -r', + 'CPPFLAGS': '', + + 'FPIC': self.fpic, + 'DEBUG': self.debug, + + 'FCFLAGS': '$(FCFLAGS%s)' % optflags, + 'FCFLAGS2': '$(FCFLAGS%s)' % regflags, + + 'CFLAGS': ' %s %s %s $(FPIC) $(DEBUG) %s ' % (cflags, cppflags, ldflags, self.cfg['extracflags']), + 'DFLAGS': ' -D__parallel -D__BLACS -D__SCALAPACK -D__FFTSG %s' % self.cfg['extradflags'], + + 'LIBS': os.getenv('LIBS', ''), + + 'FCFLAGSNOOPT': '$(DFLAGS) $(CFLAGS) -O0 $(FREE) $(FPIC) $(DEBUG)', + 'FCFLAGSOPT': '%s $(FREE) $(SAFE) $(FPIC) $(DEBUG)' % fflags, + 'FCFLAGSOPT2': '%s $(FREE) $(SAFE) $(FPIC) $(DEBUG)' % fflags_lowopt, + } + + libint = get_software_root('LibInt') + if libint: + options['DFLAGS'] += ' -D__LIBINT' + + libintcompiler = "%s %s" % (os.getenv('CC'), os.getenv('CFLAGS')) + + # Build libint-wrapper, if required + libint_wrapper = '' + + # required for old versions of GCC + if not self.compilerISO_C_BINDING: + options['DFLAGS'] += ' -D__HAS_NO_ISO_C_BINDING' + + # determine path for libint_tools dir + libinttools_paths = ['libint_tools', 'tools/hfx_tools/libint_tools'] + libinttools_path = None + for path in libinttools_paths: + path = os.path.join(self.cfg['start_dir'], path) + if os.path.isdir(path): + libinttools_path = path + change_dir(libinttools_path) + if not libinttools_path: + raise EasyBuildError("No libinttools dir found") + + # build libint wrapper + cmd = "%s -c libint_cpp_wrapper.cpp -I%s/include" % (libintcompiler, libint) + if not run_cmd(cmd, log_all=True, simple=True): + raise EasyBuildError("Building the libint wrapper failed") + libint_wrapper = '%s/libint_cpp_wrapper.o' % libinttools_path + + # determine Libint libraries based on major version number + libint_maj_ver = get_software_version('Libint').split('.')[0] + if libint_maj_ver == '1': + libint_libs = "$(LIBINTLIB)/libderiv.a $(LIBINTLIB)/libint.a $(LIBINTLIB)/libr12.a" + elif libint_maj_ver == '2': + libint_libs = "$(LIBINTLIB)/libint2.a" + else: + raise EasyBuildError("Don't know how to handle libint version %s", libint_maj_ver) + self.log.info("Using Libint version %s" % (libint_maj_ver)) + + options['LIBINTLIB'] = '%s/lib' % libint + options['LIBS'] += ' %s -lstdc++ %s' % (libint_libs, libint_wrapper) + + # add Libint include dir to $FCFLAGS + options['FCFLAGS'] += ' -I' + os.path.join(libint, 'include') + + else: + # throw a warning, since CP2K without Libint doesn't make much sense + self.log.warning("Libint module not loaded, so building without Libint support") + + libxc = get_software_root('libxc') + if libxc: + cur_libxc_version = get_software_version('libxc') + if LooseVersion(self.version) >= LooseVersion('6.1'): + libxc_min_version = '4.0.3' + options['DFLAGS'] += ' -D__LIBXC' + else: + libxc_min_version = '2.0.1' + options['DFLAGS'] += ' -D__LIBXC2' + + if LooseVersion(cur_libxc_version) < LooseVersion(libxc_min_version): + raise EasyBuildError("This version of CP2K is not compatible with libxc < %s" % libxc_min_version) + + if LooseVersion(cur_libxc_version) >= LooseVersion('4.0.3'): + # cfr. https://www.cp2k.org/howto:compile#k_libxc_optional_wider_choice_of_xc_functionals + options['LIBS'] += ' -L%s/lib -lxcf03 -lxc' % libxc + elif LooseVersion(cur_libxc_version) >= LooseVersion('2.2'): + options['LIBS'] += ' -L%s/lib -lxcf90 -lxc' % libxc + else: + options['LIBS'] += ' -L%s/lib -lxc' % libxc + self.log.info("Using Libxc-%s" % cur_libxc_version) + else: + self.log.info("libxc module not loaded, so building without libxc support") + + return options + + def configure_intel_based(self): + """Configure for Intel based toolchains""" + + # based on guidelines available at + # http://software.intel.com/en-us/articles/build-cp2k-using-intel-fortran-compiler-professional-edition/ + intelurl = ''.join(["http://software.intel.com/en-us/articles/", + "build-cp2k-using-intel-fortran-compiler-professional-edition/"]) + + options = self.configure_common() + + extrainc = '' + if self.modincpath: + extrainc = '-I%s' % self.modincpath + + options.update({ + # -Vaxlib : older options + 'FREE': '-fpp -free', + + # SAFE = -assume protect_parens -fp-model precise -ftz # causes problems, so don't use this + 'SAFE': '-assume protect_parens -no-unroll-aggressive', + + 'INCFLAGS': '$(DFLAGS) -I$(INTEL_INC) -I$(INTEL_INCF) %s' % extrainc, + + 'LDFLAGS': '$(INCFLAGS) ', + 'OBJECTS_ARCHITECTURE': 'machine_intel.o', + }) + + options['DFLAGS'] += ' -D__INTEL' + + options['FCFLAGSOPT'] += ' $(INCFLAGS) -heap-arrays 64' + options['FCFLAGSOPT2'] += ' $(INCFLAGS) -heap-arrays 64' + + ifortver = LooseVersion(get_software_version('ifort')) + + # Required due to memory leak that occurs if high optimizations are used (from CP2K 7.1 intel-popt-makefile) + if ifortver >= LooseVersion("2018.5"): + self.make_instructions += "mp2_optimize_ri_basis.o: mp2_optimize_ri_basis.F\n" \ + "\t$(FC) -c $(subst O2,O0,$(FCFLAGSOPT)) $<\n" + self.log.info("Optimization level of mp2_optimize_ri_basis.F was decreased to '-O0'") + + # RHEL8 intel/2020a lots of CPASSERT failed (due to high optimization in cholesky decomposition) + if ifortver >= LooseVersion("2019"): + self.make_instructions += "cp_fm_cholesky.o: cp_fm_cholesky.F\n\t$(FC) -c $(FCFLAGS2) $<\n" + self.log.info("Optimization flags for cp_fm_cholesky.F is set to '%s'", options['FCFLAGSOPT2']) + + # -i-static has been deprecated prior to 2013, but was still usable. From 2015 it is not. + if ifortver < LooseVersion("2013"): + options['LDFLAGS'] += ' -i-static ' + else: + options['LDFLAGS'] += ' -static-intel ' + + # Otherwise it fails on linking, since there are 2 definitions of main + if LooseVersion(self.version) >= LooseVersion('4.1'): + options['LDFLAGS'] += ' -nofor-main ' + + failmsg = "CP2K won't build correctly with the Intel %%s compilers prior to %%s, see %s" % intelurl + + if ifortver >= LooseVersion("2011") and ifortver < LooseVersion("2012"): + + # don't allow using Intel compiler 2011 prior to release 8, because of known issue (see Intel URL) + if ifortver >= LooseVersion("2011.8"): + # add additional make instructions to Makefile + self.make_instructions += "et_coupling.o: et_coupling.F\n\t$(FC) -c $(FCFLAGS2) $<\n" + self.make_instructions += "qs_vxc_atom.o: qs_vxc_atom.F\n\t$(FC) -c $(FCFLAGS2) $<\n" + + else: + raise EasyBuildError(failmsg, "v12", "v2011.8") + + elif ifortver >= LooseVersion("11"): + if LooseVersion(get_software_version('ifort')) >= LooseVersion("11.1.072"): + self.make_instructions += "qs_vxc_atom.o: qs_vxc_atom.F\n\t$(FC) -c $(FCFLAGS2) $<\n" + + else: + raise EasyBuildError(failmsg, "v11", "v11.1.072") + + else: + raise EasyBuildError("Intel compilers version %s not supported yet.", ifortver) + + return options + + def configure_GCC_based(self): + """Configure for GCC based toolchains""" + options = self.configure_common() + + options.update({ + # need this to prevent "Unterminated character constant beginning" errors + 'FREE': '-ffree-form -ffree-line-length-none', + + 'LDFLAGS': '$(FCFLAGS)', + 'OBJECTS_ARCHITECTURE': 'machine_gfortran.o', + }) + + options['DFLAGS'] += ' -D__GFORTRAN' + + options['FCFLAGSOPT'] += ' $(DFLAGS) $(CFLAGS) -fmax-stack-var-size=32768' + options['FCFLAGSOPT2'] += ' $(DFLAGS) $(CFLAGS)' + + gcc_version = get_software_version('GCCcore') or get_software_version('GCC') + if LooseVersion(gcc_version) >= LooseVersion('10.0') and LooseVersion(self.version) <= LooseVersion('7.1'): + # -fallow-argument-mismatch is required for CP2K 7.1 (and older) when compiling with GCC 10.x & more recent, + # see https://github.com/cp2k/cp2k/issues/1157, https://github.com/cp2k/dbcsr/issues/351, + # https://github.com/cp2k/dbcsr/commit/58ee9709545deda8524cab804bf1f88a61a864ac and + # https://gcc.gnu.org/legacy-ml/gcc-patches/2019-10/msg01861.html + options['FCFLAGSOPT'] += ' -fallow-argument-mismatch' + options['FCFLAGSOPT2'] += ' -fallow-argument-mismatch' + + return options + + def configure_ACML(self, options): + """Configure for AMD Math Core Library (ACML)""" + + openmp_suffix = '' + if self.openmp: + openmp_suffix = '_mp' + + options['ACML_INC'] = '%s/gfortran64%s/include' % (get_software_root('ACML'), openmp_suffix) + options['CFLAGS'] += ' -I$(ACML_INC) -I$(FFTW_INC)' + options['DFLAGS'] += ' -D__FFTACML' + + blas = os.getenv('LIBBLAS', '') + blas = blas.replace('gfortran64', 'gfortran64%s' % openmp_suffix) + options['LIBS'] += ' %s %s %s' % (self.libsmm, os.getenv('LIBSCALAPACK', ''), blas) + + return options + + def configure_BLAS_lib(self, options): + """Configure for BLAS library.""" + options['LIBS'] += ' %s %s' % (self.libsmm, os.getenv('LIBBLAS', '')) + return options + + def configure_MKL(self, options): + """Configure for Intel Math Kernel Library (MKL)""" + + options['INTEL_INC'] = '$(MKLROOT)/include' + options['DFLAGS'] += ' -D__FFTW3' + + extra = '' + if self.modincpath: + extra = '-I%s' % self.modincpath + options['CFLAGS'] += ' -I$(INTEL_INC) %s $(FPIC) $(DEBUG)' % extra + + options['LIBS'] += ' %s %s' % (self.libsmm, os.getenv('LIBSCALAPACK', '')) + + fftw_root = get_software_root('FFTW') + if fftw_root: + libfft = '-lfftw3' + if self.cfg['type'] == 'psmp': + libfft += ' -lfftw3_omp' + + options['CFLAGS'] += ' -I$(INTEL_INCF)' + options['INTEL_INCF'] = os.path.join(fftw_root, 'include') + options['LIBS'] += ' -L%s %s' % (os.path.join(fftw_root, 'lib'), libfft) + + else: + # only use Intel FFTW wrappers if FFTW is not loaded + options['CFLAGS'] += ' -I$(INTEL_INCF)' + options['DFLAGS'] += ' -D__FFTMKL' + options['INTEL_INCF'] = '$(INTEL_INC)/fftw' + options['LIBS'] = '%s %s' % (os.getenv('LIBFFT', ''), options['LIBS']) + + return options + + def configure_FFTW3(self, options): + """Configure for FFTW3""" + + options.update({ + 'FFTW_INC': os.getenv('FFT_INC_DIR', ''), # GCC + 'FFTW3INC': os.getenv('FFT_INC_DIR', ''), # Intel + 'FFTW3LIB': os.getenv('FFT_LIB_DIR', ''), # Intel + }) + + options['DFLAGS'] += ' -D__FFTW3' + if self.cfg['type'] == 'psmp': + libfft = os.getenv('LIBFFT_MT', '') + else: + libfft = os.getenv('LIBFFT', '') + options['LIBS'] += ' -L%s %s' % (os.getenv('FFT_LIB_DIR', '.'), libfft) + + return options + + def configure_LAPACK(self, options): + """Configure for LAPACK library""" + options['LIBS'] += ' %s' % os.getenv('LIBLAPACK_MT', '') + return options + + def configure_ScaLAPACK(self, options): + """Configure for ScaLAPACK library""" + + options['LIBS'] += ' %s' % os.getenv('LIBSCALAPACK', '') + + return options + + def build_step(self): + """Start the actual build + - go into makefiles dir + - patch Makefile + -build_and_install + """ + + if LooseVersion(self.version) < LooseVersion('7.0'): + makefiles = os.path.join(self.cfg['start_dir'], 'makefiles') + change_dir(makefiles) + + # modify makefile for parallel build + parallel = self.cfg['parallel'] + if parallel: + + try: + for line in fileinput.input('Makefile', inplace=1, backup='.orig.patchictce'): + line = re.sub(r"^PMAKE\s*=.*$", "PMAKE\t= $(SMAKE) -j %s" % parallel, line) + sys.stdout.write(line) + except IOError as err: + raise EasyBuildError("Can't modify/write Makefile in %s: %s", makefiles, err) + + # update make options with MAKE + self.cfg.update('buildopts', 'MAKE="make -j %s"' % self.cfg['parallel']) + + # update make options with ARCH and VERSION + self.cfg.update('buildopts', 'ARCH=%s VERSION=%s' % (self.typearch, self.cfg['type'])) + + cmd = "make %s" % self.cfg['buildopts'] + + # clean first + run_cmd(cmd + " clean", log_all=True, simple=True, log_output=True) + + # build and install + if self.cfg['library']: + cmd += ' libcp2k' + run_cmd(cmd + " all", log_all=True, simple=True, log_output=True) + + def test_step(self): + """Run regression test.""" + + if self.cfg['runtest']: + + # we need to specify location of 'data' directory in *build* dir, + # since we've configured CP2K to look into the installation directory + # (where 'data' will be copied to in install step) + setvar('CP2K_DATA_DIR', os.path.join(self.cfg['start_dir'], 'data')) + + if not build_option('mpi_tests'): + self.log.info("Skipping testing of CP2K since MPI testing is disabled") + return + + if self.cfg['omp_num_threads']: + setvar('OMP_NUM_THREADS', self.cfg['omp_num_threads']) + + # change to root of build dir + change_dir(self.builddir) + + # use regression test reference output if available + # try and find an unpacked directory that starts with 'LAST-' + regtest_refdir = None + for d in os.listdir(self.builddir): + if d.startswith("LAST-"): + regtest_refdir = d + break + + # location of do_regtest script + cfg_fn = 'cp2k_regtest.cfg' + + regtest_script = os.path.join(self.cfg['start_dir'], 'tools', 'regtesting', 'do_regtest') + regtest_cmd = [regtest_script, '-nobuild', '-config', cfg_fn] + if LooseVersion(self.version) < LooseVersion('7.1'): + # -nosvn option was removed in CP2K 7.1 + regtest_cmd.insert(1, '-nosvn') + + # older version of CP2K + if not os.path.exists(regtest_script): + regtest_script = os.path.join(self.cfg['start_dir'], 'tools', 'do_regtest') + regtest_cmd = [regtest_script, '-nocvs', '-quick', '-nocompile', '-config', cfg_fn] + + regtest_cmd = ' '.join(regtest_cmd) + + # patch do_regtest so that reference output is used + if regtest_refdir: + self.log.info("Using reference output available in %s" % regtest_refdir) + try: + for line in fileinput.input(regtest_script, inplace=1, backup='.orig.refout'): + line = re.sub(r"^(dir_last\s*=\${dir_base})/.*$", r"\1/%s" % regtest_refdir, line) + sys.stdout.write(line) + except IOError as err: + raise EasyBuildError("Failed to modify '%s': %s", regtest_script, err) + + else: + self.log.info("No reference output found for regression test, just continuing without it...") + + # prefer using 4 cores, since some tests require/prefer square (n^2) numbers or powers of 2 (2^n) + test_core_cnt = min(self.cfg['parallel'], 4) + if get_avail_core_count() < test_core_cnt: + raise EasyBuildError("Cannot run MPI tests as not enough cores (< %s) are available", test_core_cnt) + else: + self.log.info("Using %s cores for the MPI tests" % test_core_cnt) + + # configure regression test + cfg_txt = '\n'.join([ + 'FORT_C_NAME="%(f90)s"', + 'dir_base=%(base)s', + 'cp2k_version=%(cp2k_version)s', + 'dir_triplet=%(triplet)s', + 'export ARCH=${dir_triplet}', + 'cp2k_dir=%(cp2k_dir)s', + 'leakcheck="YES"', + 'maxtasks=%(maxtasks)s', + 'cp2k_run_prefix="%(mpicmd_prefix)s"', + ]) % { + 'f90': os.getenv('F90'), + 'base': os.path.dirname(os.path.normpath(self.cfg['start_dir'])), + 'cp2k_version': self.cfg['type'], + 'triplet': self.typearch, + 'cp2k_dir': os.path.basename(os.path.normpath(self.cfg['start_dir'])), + 'maxtasks': self.cfg['maxtasks'], + 'mpicmd_prefix': self.toolchain.mpi_cmd_for('', test_core_cnt), + } + + write_file(cfg_fn, cfg_txt) + self.log.debug("Contents of %s: %s" % (cfg_fn, cfg_txt)) + + # run regression test + (regtest_output, ec) = run_cmd(regtest_cmd, log_all=True, simple=False, log_output=True) + + if ec == 0: + self.log.info("Regression test output:\n%s" % regtest_output) + else: + raise EasyBuildError("Regression test failed (non-zero exit code): %s", regtest_output) + + # pattern to search for regression test summary + re_pattern = r"number\s+of\s+%s\s+tests\s+(?P<cnt>[0-9]+)" + + # find total number of tests + regexp = re.compile(re_pattern % "", re.M | re.I) + res = regexp.search(regtest_output) + tot_cnt = None + if res: + tot_cnt = int(res.group('cnt')) + else: + raise EasyBuildError("Finding total number of tests in regression test summary failed") + + # function to report on regtest results + def test_report(test_result): + """Report on tests with given result.""" + + postmsg = '' + + test_result = test_result.upper() + regexp = re.compile(re_pattern % test_result, re.M | re.I) + + cnt = None + res = regexp.search(regtest_output) + if not res: + raise EasyBuildError("Finding number of %s tests in regression test summary failed", + test_result.lower()) + else: + cnt = int(res.group('cnt')) + + logmsg = "Regression test reported %s / %s %s tests" + logmsg_values = (cnt, tot_cnt, test_result.lower()) + + # failed tests indicate problem with installation + # wrong tests are only an issue when there are excessively many + if (test_result == "FAILED" and cnt > 0) or (test_result == "WRONG" and (cnt / tot_cnt) > 0.1): + if self.cfg['ignore_regtest_fails']: + self.log.warning(logmsg, *logmsg_values) + self.log.info("Ignoring failures in regression test, as requested.") + else: + raise EasyBuildError(logmsg, *logmsg_values) + elif test_result == "CORRECT" or cnt == 0: + self.log.info(logmsg, *logmsg_values) + else: + self.log.warning(logmsg, *logmsg_values) + + return postmsg + + # number of failed/wrong tests, will report error if count is positive + self.postmsg += test_report("FAILED") + self.postmsg += test_report("WRONG") + + # there are no more 'new' tests from CP2K 8.1 onwards + if LooseVersion(self.version) < LooseVersion('8.0'): + # number of new tests, will be high if a non-suitable regtest reference was used + # will report error if count is positive (is that what we want?) + self.postmsg += test_report("NEW") + + # number of correct tests: just report + test_report("CORRECT") + + def install_step(self): + """Install built CP2K + - copy from exe to bin + - copy data dir (if exists) + - copy tests + """ + + # copy executables + exedir = os.path.join(self.cfg['start_dir'], 'exe', self.typearch) + targetdir = os.path.join(self.installdir, 'bin') + copy_dir(exedir, targetdir) + + # copy libraries and include files, not sure what is strictly required so we take everything + if self.cfg['library']: + libdir = os.path.join(self.cfg['start_dir'], 'lib', self.typearch, self.cfg['type']) + targetdir = os.path.join(self.installdir, 'lib') + copy_dir(libdir, targetdir) + # Also need to populate the include directory + targetdir = os.path.join(self.installdir, 'include') + libcp2k_header = os.path.join(self.cfg['start_dir'], 'src', 'start', 'libcp2k.h') + target_header = os.path.join(targetdir, os.path.basename(libcp2k_header)) + copy_file(libcp2k_header, target_header) + # include all .mod files for fortran users (don't know the exact list so take everything) + mod_path = os.path.join(self.cfg['start_dir'], 'obj', self.typearch, self.cfg['type']) + for mod_file in glob.glob(os.path.join(mod_path, '*.mod')): + target_mod = os.path.join(targetdir, os.path.basename(mod_file)) + copy_file(mod_file, target_mod) + + # copy data dir + datadir = os.path.join(self.cfg['start_dir'], 'data') + targetdir = os.path.join(self.installdir, 'data') + if os.path.exists(targetdir): + self.log.info("Won't copy data dir. Destination directory %s already exists" % targetdir) + elif os.path.exists(datadir): + copy_dir(datadir, targetdir) + else: + self.log.info("Won't copy data dir. Source directory %s does not exist" % datadir) + + # copy tests + srctests = os.path.join(self.cfg['start_dir'], 'tests') + targetdir = os.path.join(self.installdir, 'tests') + if os.path.exists(targetdir): + self.log.info("Won't copy tests. Destination directory %s already exists" % targetdir) + else: + copy_dir(srctests, targetdir) + + # copy regression test results + if self.cfg['runtest']: + try: + testdir = os.path.dirname(os.path.normpath(self.cfg['start_dir'])) + for d in os.listdir(testdir): + if d.startswith('TEST-%s-%s' % (self.typearch, self.cfg['type'])): + path = os.path.join(testdir, d) + target = os.path.join(self.installdir, d) + copy_dir(path, target) + self.log.info("Regression test results dir %s copied to %s" % (d, self.installdir)) + break + except (OSError, IOError) as err: + raise EasyBuildError("Failed to copy regression test results dir: %s", err) + + def sanity_check_step(self): + """Custom sanity check for CP2K""" + + cp2k_type = self.cfg['type'] + custom_paths = { + 'files': ["bin/%s.%s" % (x, cp2k_type) for x in ["cp2k", "cp2k_shell"]], + 'dirs': ["tests"] + } + if self.cfg['library']: + custom_paths['files'].append(os.path.join('lib', 'libcp2k.a')) + custom_paths['files'].append(os.path.join('include', 'libcp2k.h')) + custom_paths['files'].append(os.path.join('include', 'libcp2k.mod')) + super(EB_CP2K, self).sanity_check_step(custom_paths=custom_paths) + + def make_module_extra(self): + """Set up a CP2K_DATA_DIR environment variable to find CP2K provided basis sets""" + + txt = super(EB_CP2K, self).make_module_extra() + + # also define $CP2K_DATA_DIR in module, + # even though CP2K was already configured to pick up 'data' from install dir + # this could be useful for users to access the 'data' dir in a documented way (and it doesn't hurt) + datadir = os.path.join(self.installdir, 'data') + if os.path.exists(datadir): + txt += self.module_generator.set_environment('CP2K_DATA_DIR', datadir) + + return txt + diff --git a/easyblocks/c/cp2k.py b/easyblocks/c/cp2k.py index 1adfd2f7cc506f57e58921d430ef657b5a2b7a8f..27b249418f4b631677ea98860c983bf5abc70d45 100644 --- a/easyblocks/c/cp2k.py +++ b/easyblocks/c/cp2k.py @@ -1,8 +1,5 @@ -# IT4Innovations 2022 -# JK -# EasyBlock for CP2K CUDA support ## -# Copyright 2009-2021 Ghent University +# Copyright 2009-2024 Ghent University # # This file is part of EasyBuild, # originally created by the HPC team of Ghent University (http://ugent.be/hpc/en), @@ -38,6 +35,8 @@ EasyBuild support for building and installing CP2K, implemented as an easyblock @author: Damian Alvarez (Forschungszentrum Juelich GmbH) @author: Alan O'Cais (Forschungszentrum Juelich GmbH) @author: Balazs Hajgato (Free University Brussels (VUB)) +@author: O. Baris Malcioglu (Middle East Technical University) +@author: Bibek Chapagain(Barcelona Supercomputing Center) """ import fileinput @@ -45,7 +44,7 @@ import glob import re import os import sys -from distutils.version import LooseVersion +from easybuild.tools import LooseVersion import easybuild.tools.toolchain as toolchain from easybuild.framework.easyblock import EasyBlock @@ -83,6 +82,7 @@ class EB_CP2K(EasyBlock): # used for both libsmm and libxsmm self.libsmm = '' + self.libsmm_path = '' self.modincpath = '' self.openmp = '' @@ -93,7 +93,6 @@ class EB_CP2K(EasyBlock): extra_vars = { 'extracflags': ['', "Extra CFLAGS to be added", CUSTOM], 'extradflags': ['', "Extra DFLAGS to be added", CUSTOM], - 'gpuver': [None, "Value for GPUVER configuration setting, specifies type of GPU to build for", CUSTOM], 'ignore_regtest_fails': [False, ("Ignore failures in regression test " "(should be used with care)"), CUSTOM], 'library': [False, "Also build CP2K as a library", CUSTOM], @@ -102,11 +101,13 @@ class EB_CP2K(EasyBlock): 'modinc': [[], ("List of modinc's to use (*.f90], or 'True' to use " "all found at given prefix"), CUSTOM], 'modincprefix': ['', "Intel MKL prefix for modinc include dir", CUSTOM], + 'runtest': [True, "Build and run CP2K tests", CUSTOM], 'omp_num_threads': [None, "Value to set $OMP_NUM_THREADS to during testing", CUSTOM], 'plumed': [None, "Enable PLUMED support", CUSTOM], - 'runtest': [True, "Build and run CP2K tests", CUSTOM], + 'spglib': [None, "Enable SPGLIB support", CUSTOM], 'type': ['popt', "Type of build ('popt' or 'psmp')", CUSTOM], 'typeopt': [True, "Enable optimization", CUSTOM], + 'gpuver': ['H100', "CUDA gpu version", CUSTOM], } return EasyBlock.extra_options(extra_vars) @@ -114,7 +115,7 @@ class EB_CP2K(EasyBlock): """Generate Makefile based on options dictionary and optional make instructions""" text = "# Makefile generated by CP2K easyblock in EasyBuild\n" - for key, value in sorted(options.items()): + for key, value in options.items(): text += "%s = %s\n" % (key, value) return text + self.make_instructions @@ -123,11 +124,16 @@ class EB_CP2K(EasyBlock): - build Libint wrapper - generate Makefile """ + cp2k_version = LooseVersion(self.version) known_types = ['popt', 'psmp'] if self.cfg['type'] not in known_types: raise EasyBuildError("Unknown build type specified: '%s', known types are %s", self.cfg['type'], known_types) + if cp2k_version >= LooseVersion('2024') and self.cfg['type'] == 'popt': + self.cfg['type'] = 'psmp' + setvar('OMP_NUM_THREADS', '1') + self.log.debug('In 2023 version popt is psmp with OMP_NUM_THREADS set to 1') # correct start dir, if needed # recent CP2K versions have a 'cp2k' dir in the unpacked 'cp2k' dir @@ -158,6 +164,7 @@ class EB_CP2K(EasyBlock): if libxsmm: self.cfg.update('extradflags', '-D__LIBXSMM') self.libsmm = '-lxsmm -lxsmmf' + self.libsmm_path = libxsmm self.log.debug('Using libxsmm %s' % libxsmm) elif libsmm: libsmms = glob.glob(os.path.join(libsmm, 'lib', 'libsmm_*nn.a')) @@ -165,6 +172,7 @@ class EB_CP2K(EasyBlock): moredflags = ' ' + ' '.join(dfs) self.cfg.update('extradflags', moredflags) self.libsmm = ' '.join(libsmms) + self.libsmm_path = libsmm self.log.debug('Using libsmm %s (extradflags %s)' % (self.libsmm, moredflags)) # obtain list of modinc's to use @@ -207,6 +215,17 @@ class EB_CP2K(EasyBlock): if os.getenv('LIBSCALAPACK', None) is not None: options = self.configure_ScaLAPACK(options) + # SPGLIB + spglib = get_software_root('SPGLIB') + if self.cfg['spglib'] and not spglib: + raise EasyBuildError("The SPGLIB module needs to be loaded to build CP2K with SPGLIB support") + + # enable SPGLIB support if SPGLIB is listed as a dependency + # and SPGLIB support is either explicitly enabled (spglib = True) or unspecified ('spglib' not defined) + if spglib and (self.cfg['spglib'] or self.cfg['spglib'] is None): + options['LIBS'] += ' -lsymspg' + options['DFLAGS'] += ' -D__SPGLIB' + # PLUMED plumed = get_software_root('PLUMED') if self.cfg['plumed'] and not plumed: @@ -223,70 +242,39 @@ class EB_CP2K(EasyBlock): if elpa: options['LIBS'] += ' -lelpa' elpa_inc_dir = os.path.join(elpa, 'include', 'elpa-%s' % get_software_version('ELPA'), 'modules') - options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir + if cp2k_version >= LooseVersion('2024'): + options['INCS'] += ' -I%s ' % elpa_inc_dir + else: + options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir if LooseVersion(self.version) >= LooseVersion('6.1'): elpa_ver = ''.join(get_software_version('ELPA').split('.')[:2]) options['DFLAGS'] += ' -D__ELPA=%s' % elpa_ver elpa_inc_dir = os.path.join(elpa, 'include', 'elpa-%s' % get_software_version('ELPA'), 'elpa') - options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir + if cp2k_version >= LooseVersion('2024'): + options['INCS'] += ' -I%s ' % elpa_inc_dir + else: + options['FCFLAGSOPT'] += ' -I%s ' % elpa_inc_dir else: options['DFLAGS'] += ' -D__ELPA3' - # CUDA support - # see https://github.com/cp2k/cp2k/blob/master/INSTALL.md#2j-cuda-optional-improved-performance-on-gpu-systems + # CUDA cuda = get_software_root('CUDA') if cuda: - # determine CUDA compute capability to use based on --cuda-compute-capabilities in EasyBuild configuration, - # or cuda_compute_capabilities easyconfig parameter (fallback); - # must be a single value to build CP2K with CUDA support! - cuda_cc = build_option('cuda_compute_capabilities') or self.cfg.get('cuda_compute_capabilities') - if len(cuda_cc) == 1: - cuda_cc = cuda_cc[0] - elif cuda_cc: - error_msg = "Exactly one CUDA compute capability must be specified, found %d: %s" - raise EasyBuildError(error_msg, len(cuda_cc), ', '.join(cuda_cc)) - else: - error_msg = "Exactly one CUDA compute capability must be specified via " - error_msg += "--cuda-compute-capabilities or the cuda_compute_capabilities easyconfig parameter." - raise EasyBuildError(error_msg) - - # GPUVER must be set, required by the DBCSR component, - # see exts/dbcsr/Makefile and the parameters_*.json in src/acc/libsmm_acc/libcusmm/; - # determine string value to use based on select CUDA compute capability, unless specified explicitly via - # custom 'gpuver' easyconfig parameter - gpuver = self.cfg['gpuver'] - - if gpuver is None: - cuda_cc_lv = LooseVersion(cuda_cc) - known_gpuver = [ - ('7.0', 'V100'), - ('6.0', 'P100'), - ('3.7', 'K80'), - ('3.5', 'K40'), - ] - for min_cuda_cc, val in known_gpuver: - if cuda_cc_lv >= LooseVersion(min_cuda_cc): - gpuver = val - break - - if gpuver is None: - raise EasyBuildError("Failed to determine value for required GPUVER setting!") + if cp2k_version >= LooseVersion('2024'): + options['DFLAGS'] += ' -D__OFFLOAD_CUDA -D__DBCSR_ACC ' + options['LIBS'] += ' -lcufft -lcudart -lnvrtc -lcuda -lcublas' + options['OFFLOAD_CC'] = 'nvcc' + options['OFFLOAD_FLAGS'] = "-O3 -g -w --std=c++11 $(DFLAGS) -Xcompiler='-fopenmp -Wall -Wextra -Werror'" + options['OFFLOAD_TARGET'] = 'cuda' + options['GPUVER'] = self.cfg['gpuver'] + options['CXX'] = 'mpicxx' + options['CXXFLAGS'] = '-O3 -fopenmp -g -w --std=c++14 -fPIC $(DFLAGS) $(INCS)' else: - options['GPUVER'] = gpuver - - options['DFLAGS'] += ' -D__ACC -D__DBCSR_ACC -D__PW_CUDA -D__GRID_CUDA' - options['LIBS'] += ' -lcudart -lnvrtc -lcuda -lcublas -lcufft -lrt' - options['NVCC'] = 'nvcc' - options['NVFLAGS'] = ' '.join([ - options['DFLAGS'], - '-O3', - '--std=c++11', - '-arch sm_%s' % cuda_cc.replace('.', ''), - # control host compilers + options - "-ccbin='%s'" % os.getenv('CXX'), - "-Xcompiler='%s'" % os.getenv('CXXFLAGS'), - ]) - + options['DFLAGS'] += ' -D__ACC -D__DBCSR_ACC' + options['LIBS'] += ' -lcudart -lcublas -lcufft -lrt' + options['NVCC'] = ' nvcc' + # reset typearch + self.typearch = "Linux-x86-64-%s-cuda" % self.toolchain.name # avoid group nesting options['LIBS'] = options['LIBS'].replace('-Wl,--start-group', '').replace('-Wl,--end-group', '') @@ -317,7 +305,7 @@ class EB_CP2K(EasyBlock): mkdir(modincpath, parents=True) # get list of modinc source files - modincdir = os.path.join(imkl, self.cfg["modincprefix"], 'include') + modincdir = os.path.join(os.getenv('MKLROOT'), self.cfg["modincprefix"], 'include') if isinstance(self.cfg["modinc"], list): modfiles = [os.path.join(modincdir, x) for x in self.cfg["modinc"]] @@ -351,10 +339,12 @@ class EB_CP2K(EasyBlock): def configure_common(self): """Common configuration for all toolchains""" + cp2k_version = LooseVersion(self.version) + # openmp introduces 2 major differences # -automatic is default: -noautomatic -auto-scalar # some mem-bandwidth optimisation - if self.cfg['type'] == 'psmp': + if self.cfg['type'] == 'psmp' or cp2k_version >= LooseVersion('2024'): self.openmp = self.toolchain.get_flag('openmp') # determine which opt flags to use @@ -389,33 +379,32 @@ class EB_CP2K(EasyBlock): if not mpi2: raise EasyBuildError("CP2K needs MPI-2, no known MPI-2 supporting library loaded?") - cppflags = os.getenv('CPPFLAGS') ldflags = os.getenv('LDFLAGS') - cflags = os.getenv('CFLAGS') fflags = os.getenv('FFLAGS') fflags_lowopt = re.sub('-O[0-9]', '-O1', fflags) + options = { 'CC': os.getenv('MPICC'), - 'CPP': '', - 'FC': '%s %s' % (os.getenv('MPIF90'), self.openmp), - 'LD': '%s %s' % (os.getenv('MPIF90'), self.openmp), 'AR': 'ar -r', - 'CPPFLAGS': '', - + 'FC': '%s' % (os.getenv('MPIF90')), + 'LD': '%s' % (os.getenv('MPIF90')), 'FPIC': self.fpic, + 'DFLAGS': ' -D__parallel -D__BLACS -D__SCALAPACK -D__FFTSG %s' % self.cfg['extradflags'], + 'INCS': '', + 'CFLAGS': '-O3 -fopenmp -ftree-vectorize -march=native -fno-math-errno -fopenmp -std=c11 $(FPIC) $(DEBUG) ' + '$(INCS) $(DFLAGS) %s' % + self.cfg['extracflags'], 'DEBUG': self.debug, - - 'FCFLAGS': '$(FCFLAGS%s)' % optflags, + 'FREE': '', + 'FCFLAGS': '$(FCFLAGS%s) $(INCS)' % optflags, 'FCFLAGS2': '$(FCFLAGS%s)' % regflags, - - 'CFLAGS': ' %s %s %s $(FPIC) $(DEBUG) %s ' % (cflags, cppflags, ldflags, self.cfg['extracflags']), - 'DFLAGS': ' -D__parallel -D__BLACS -D__SCALAPACK -D__FFTSG %s' % self.cfg['extradflags'], - - 'LIBS': os.getenv('LIBS', ''), - 'FCFLAGSNOOPT': '$(DFLAGS) $(CFLAGS) -O0 $(FREE) $(FPIC) $(DEBUG)', 'FCFLAGSOPT': '%s $(FREE) $(SAFE) $(FPIC) $(DEBUG)' % fflags, 'FCFLAGSOPT2': '%s $(FREE) $(SAFE) $(FPIC) $(DEBUG)' % fflags_lowopt, + + 'LDFLAGS': '$(FCFLAGS) %s ' % ldflags, + 'LIBS': os.getenv('LIBS', ''), + } libint = get_software_root('LibInt') @@ -462,7 +451,10 @@ class EB_CP2K(EasyBlock): options['LIBS'] += ' %s -lstdc++ %s' % (libint_libs, libint_wrapper) # add Libint include dir to $FCFLAGS - options['FCFLAGS'] += ' -I' + os.path.join(libint, 'include') + if cp2k_version >= LooseVersion('2024'): + options['INCS'] += ' -I' + os.path.join(libint, 'include') + else: + options['FCFLAGS'] += ' -I' + os.path.join(libint, 'include') else: # throw a warning, since CP2K without Libint doesn't make much sense @@ -488,6 +480,10 @@ class EB_CP2K(EasyBlock): options['LIBS'] += ' -L%s/lib -lxcf90 -lxc' % libxc else: options['LIBS'] += ' -L%s/lib -lxc' % libxc + + if cp2k_version >= LooseVersion('2024'): + options['INCS'] += ' -I%s/include ' % libxc + self.log.info("Using Libxc-%s" % cur_libxc_version) else: self.log.info("libxc module not loaded, so building without libxc support") @@ -526,12 +522,21 @@ class EB_CP2K(EasyBlock): options['FCFLAGSOPT'] += ' $(INCFLAGS) -heap-arrays 64' options['FCFLAGSOPT2'] += ' $(INCFLAGS) -heap-arrays 64' - ifortver = LooseVersion(get_software_version('ifort')) + # for recent intel toolchains (>= intel/2021a), intel-compilers is the toolchain component + ifortver = get_software_version('intel-compilers') + if ifortver is None: + # fall back to trying to determining Intel Fortran compiler version using 'ifort' as software name + ifortver = get_software_version('ifort') + + if ifortver: + ifortver = LooseVersion(ifortver) + else: + raise EasyBuildError("Failed to determine Intel Fortran compiler version!") # Required due to memory leak that occurs if high optimizations are used (from CP2K 7.1 intel-popt-makefile) if ifortver >= LooseVersion("2018.5"): self.make_instructions += "mp2_optimize_ri_basis.o: mp2_optimize_ri_basis.F\n" \ - "\t$(FC) -c $(subst O2,O0,$(FCFLAGSOPT)) $<\n" + "\t$(FC) -c $(subst O2,O0,$(FCFLAGSOPT)) $<\n" self.log.info("Optimization level of mp2_optimize_ri_basis.F was decreased to '-O0'") # RHEL8 intel/2020a lots of CPASSERT failed (due to high optimization in cholesky decomposition) @@ -563,7 +568,7 @@ class EB_CP2K(EasyBlock): raise EasyBuildError(failmsg, "v12", "v2011.8") elif ifortver >= LooseVersion("11"): - if LooseVersion(get_software_version('ifort')) >= LooseVersion("11.1.072"): + if ifortver >= LooseVersion("11.1.072"): self.make_instructions += "qs_vxc_atom.o: qs_vxc_atom.F\n\t$(FC) -c $(FCFLAGS2) $<\n" else: @@ -576,21 +581,29 @@ class EB_CP2K(EasyBlock): def configure_GCC_based(self): """Configure for GCC based toolchains""" - options = self.configure_common() + cp2k_version = LooseVersion(self.version) - options.update({ - # need this to prevent "Unterminated character constant beginning" errors - 'FREE': '-ffree-form -ffree-line-length-none', + options = self.configure_common() + if cp2k_version >= LooseVersion('2024'): + options.update({ + # need this to prevent "Unterminated character constant beginning" errors + 'FREE': '-ffree-form -ffree-line-length-none -std=f2008', + }) + options[ + 'FCFLAGSOPT'] = '-O3 -ftree-vectorize -march=native -fno-math-errno -fopenmp -fPIC $(FREE) $(DFLAGS)' - 'LDFLAGS': '$(FCFLAGS)', - 'OBJECTS_ARCHITECTURE': 'machine_gfortran.o', - }) + else: + options.update({ + # need this to prevent "Unterminated character constant beginning" errors + 'FREE': '-ffree-form -ffree-line-length-none ', + 'LDFLAGS': '$(FCFLAGS) ', + 'OBJECTS_ARCHITECTURE': 'machine_gfortran.o', + }) + options['FCFLAGSOPT'] += ' $(DFLAGS) $(CFLAGS) -fmax-stack-var-size=32768' + options['FCFLAGSOPT2'] += ' $(DFLAGS) $(CFLAGS)' options['DFLAGS'] += ' -D__GFORTRAN' - options['FCFLAGSOPT'] += ' $(DFLAGS) $(CFLAGS) -fmax-stack-var-size=32768' - options['FCFLAGSOPT2'] += ' $(DFLAGS) $(CFLAGS)' - gcc_version = get_software_version('GCCcore') or get_software_version('GCC') if LooseVersion(gcc_version) >= LooseVersion('10.0') and LooseVersion(self.version) <= LooseVersion('7.1'): # -fallow-argument-mismatch is required for CP2K 7.1 (and older) when compiling with GCC 10.x & more recent, @@ -605,6 +618,8 @@ class EB_CP2K(EasyBlock): def configure_ACML(self, options): """Configure for AMD Math Core Library (ACML)""" + cp2k_version = LooseVersion(self.version) + openmp_suffix = '' if self.openmp: openmp_suffix = '_mp' @@ -616,17 +631,28 @@ class EB_CP2K(EasyBlock): blas = os.getenv('LIBBLAS', '') blas = blas.replace('gfortran64', 'gfortran64%s' % openmp_suffix) options['LIBS'] += ' %s %s %s' % (self.libsmm, os.getenv('LIBSCALAPACK', ''), blas) + if cp2k_version >= LooseVersion('2024'): + options['LDFLAGS'] += ' -L%s/lib ' % self.libsmm_path + options['INCS'] += ' -I%s/include ' % self.libsmm_path return options def configure_BLAS_lib(self, options): """Configure for BLAS library.""" + + cp2k_version = LooseVersion(self.version) + options['LIBS'] += ' %s %s' % (self.libsmm, os.getenv('LIBBLAS', '')) + if cp2k_version >= LooseVersion('2024'): + options['LDFLAGS'] += ' -L%s/lib ' % self.libsmm_path + options['INCS'] += ' -I%s/include ' % self.libsmm_path return options def configure_MKL(self, options): """Configure for Intel Math Kernel Library (MKL)""" + cp2k_version = LooseVersion(self.version) + options['INTEL_INC'] = '$(MKLROOT)/include' options['DFLAGS'] += ' -D__FFTW3' @@ -650,28 +676,41 @@ class EB_CP2K(EasyBlock): else: # only use Intel FFTW wrappers if FFTW is not loaded options['CFLAGS'] += ' -I$(INTEL_INCF)' - options['DFLAGS'] += ' -D__FFTMKL' + if LooseVersion(self.version) > LooseVersion('2.3'): + options['DFLAGS'] += ' -D__MKL' + else: + options['DFLAGS'] += ' -D__FFTMKL' options['INTEL_INCF'] = '$(INTEL_INC)/fftw' options['LIBS'] = '%s %s' % (os.getenv('LIBFFT', ''), options['LIBS']) + if cp2k_version >= LooseVersion('2024'): + options['LDFLAGS'] += ' %s ' % self.libsmm + return options def configure_FFTW3(self, options): """Configure for FFTW3""" - options.update({ - 'FFTW_INC': os.getenv('FFT_INC_DIR', ''), # GCC - 'FFTW3INC': os.getenv('FFT_INC_DIR', ''), # Intel - 'FFTW3LIB': os.getenv('FFT_LIB_DIR', ''), # Intel - }) + cp2k_version = LooseVersion(self.version) - options['DFLAGS'] += ' -D__FFTW3' - if self.cfg['type'] == 'psmp': + if cp2k_version >= LooseVersion('2024'): libfft = os.getenv('LIBFFT_MT', '') + options['LIBS'] += ' -lfftw3_omp -lfftw3' + options['LDFLAGS'] += ' -L%s ' % os.getenv('FFT_LIB_DIR', '') + options['INCS'] += ' -I%s ' % os.getenv('FFT_INC_DIR', '') else: - libfft = os.getenv('LIBFFT', '') - options['LIBS'] += ' -L%s %s' % (os.getenv('FFT_LIB_DIR', '.'), libfft) + options.update({ + 'FFTW_INC': os.getenv('FFT_INC_DIR', ''), # GCC + 'FFTW3INC': os.getenv('FFT_INC_DIR', ''), # Intel + 'FFTW3LIB': os.getenv('FFT_LIB_DIR', ''), # Intel + }) + if self.cfg['type'] == 'psmp': + libfft = os.getenv('LIBFFT_MT', '') + else: + libfft = os.getenv('LIBFFT', '') + options['LIBS'] += ' -L%s %s' % (os.getenv('FFT_LIB_DIR', '.'), libfft) + options['DFLAGS'] += ' -D__FFTW3' return options def configure_LAPACK(self, options): @@ -720,9 +759,13 @@ class EB_CP2K(EasyBlock): run_cmd(cmd + " clean", log_all=True, simple=True, log_output=True) # build and install + # compile regularly first with the default make target + # and only then build the library + run_cmd(cmd + ' all', log_all=True, simple=True, log_output=True) + + # build as a library if self.cfg['library']: - cmd += ' libcp2k' - run_cmd(cmd + " all", log_all=True, simple=True, log_output=True) + run_cmd(cmd + 'libcp2k', log_all=True, simple=True, log_output=True) def test_step(self): """Run regression test.""" @@ -755,15 +798,19 @@ class EB_CP2K(EasyBlock): # location of do_regtest script cfg_fn = 'cp2k_regtest.cfg' - regtest_script = os.path.join(self.cfg['start_dir'], 'tools', 'regtesting', 'do_regtest') - regtest_cmd = [regtest_script, '-nobuild', '-config', cfg_fn] + regtest_script = os.path.join(self.cfg['start_dir'], 'tests', 'do_regtest.py') + if LooseVersion(self.version) >= LooseVersion('2025'): + exedir = os.path.join(self.cfg['start_dir'], 'exe', self.typearch) + else: + exedir = self.typearch + regtest_cmd = [regtest_script, exedir, self.cfg['type']] if LooseVersion(self.version) < LooseVersion('7.1'): # -nosvn option was removed in CP2K 7.1 regtest_cmd.insert(1, '-nosvn') # older version of CP2K if not os.path.exists(regtest_script): - regtest_script = os.path.join(self.cfg['start_dir'], 'tools', 'do_regtest') + regtest_script = os.path.join(self.cfg['start_dir'], 'tests', 'do_regtest.py') regtest_cmd = [regtest_script, '-nocvs', '-quick', '-nocompile', '-config', cfg_fn] regtest_cmd = ' '.join(regtest_cmd) @@ -807,7 +854,7 @@ class EB_CP2K(EasyBlock): 'cp2k_dir': os.path.basename(os.path.normpath(self.cfg['start_dir'])), 'maxtasks': self.cfg['maxtasks'], 'mpicmd_prefix': self.toolchain.mpi_cmd_for('', test_core_cnt), - } + } write_file(cfg_fn, cfg_txt) self.log.debug("Contents of %s: %s" % (cfg_fn, cfg_txt)) @@ -967,4 +1014,3 @@ class EB_CP2K(EasyBlock): txt += self.module_generator.set_environment('CP2K_DATA_DIR', datadir) return txt - diff --git a/easyblocks/w/wrf.py b/easyblocks/w/wrf.py new file mode 100644 index 0000000000000000000000000000000000000000..3c9c76f18e09dc38da9f3bf596719fc7399809e4 --- /dev/null +++ b/easyblocks/w/wrf.py @@ -0,0 +1,452 @@ +## +# Copyright 2009-2024 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 WRF, implemented as an easyblock + +@author: Stijn De Weirdt (Ghent University) +@author: Dries Verdegem (Ghent University) +@author: Kenneth Hoste (Ghent University) +@author: Pieter De Baets (Ghent University) +@author: Jens Timmerman (Ghent University) +@author: Andreas Hilboll (University of Bremen) +""" +import os +import re + +from easybuild.tools import LooseVersion + +import easybuild.tools.environment as env +import easybuild.tools.toolchain as toolchain +from easybuild.easyblocks.netcdf import set_netcdf_env_vars # @UnresolvedImport +from easybuild.framework.easyblock import EasyBlock +from easybuild.framework.easyconfig import CUSTOM, MANDATORY +from easybuild.tools.build_log import EasyBuildError +from easybuild.tools.config import build_option +from easybuild.tools.filetools import apply_regex_substitutions, change_dir +from easybuild.tools.filetools import patch_perl_script_autoflush, read_file, which +from easybuild.tools.filetools import remove_file, symlink +from easybuild.tools.modules import get_software_root +from easybuild.tools.run import run_cmd, run_cmd_qa + + +def det_wrf_subdir(wrf_version): + """Determine WRF subdirectory for given WRF version.""" + + if LooseVersion(wrf_version) < LooseVersion('4.0'): + wrf_subdir = 'WRFV%s' % wrf_version.split('.')[0] + elif LooseVersion(wrf_version) >= LooseVersion('4.5.1'): + wrf_subdir = 'WRFV%s' % wrf_version + else: + wrf_subdir = 'WRF-%s' % wrf_version + + return wrf_subdir + + +class EB_WRF(EasyBlock): + """Support for building/installing WRF.""" + + def __init__(self, *args, **kwargs): + """Add extra config options specific to WRF.""" + super(EB_WRF, self).__init__(*args, **kwargs) + + self.build_in_installdir = True + self.comp_fam = None + + self.wrfsubdir = det_wrf_subdir(self.version) + + @staticmethod + def extra_options(): + extra_vars = { + 'buildtype': [None, "Specify the type of build (serial, smpar (OpenMP), " + "dmpar (MPI), dm+sm (hybrid OpenMP/MPI)).", MANDATORY], + 'rewriteopts': [True, "Replace -O3 with CFLAGS/FFLAGS", CUSTOM], + 'runtest': [True, "Build and run WRF tests", CUSTOM], + } + return EasyBlock.extra_options(extra_vars) + + def configure_step(self): + """Configure build: + - set some magic environment variables + - run configure script + - adjust configure.wrf file if needed + """ + + wrfdir = os.path.join(self.builddir, self.wrfsubdir) + + # define $NETCDF* for netCDF dependency (used when creating WRF module file) + set_netcdf_env_vars(self.log) + + # HDF5 (optional) dependency + hdf5 = get_software_root('HDF5') + if hdf5: + env.setvar('HDF5', hdf5) + # check if this is parallel HDF5 + phdf5_bins = ['h5pcc', 'ph5diff'] + parallel_hdf5 = True + for f in phdf5_bins: + if not os.path.exists(os.path.join(hdf5, 'bin', f)): + parallel_hdf5 = False + break + if parallel_hdf5: + env.setvar('PHDF5', hdf5) + else: + self.log.info("Parallel HDF5 module not loaded, assuming that's OK...") + else: + self.log.info("HDF5 module not loaded, assuming that's OK...") + + # Parallel netCDF (optional) dependency + pnetcdf = get_software_root('PnetCDF') + if pnetcdf: + env.setvar('PNETCDF', pnetcdf) + + # JasPer dependency check + setting env vars + jasper = get_software_root('JasPer') + if jasper: + jasperlibdir = os.path.join(jasper, "lib") + env.setvar('JASPERINC', os.path.join(jasper, "include")) + env.setvar('JASPERLIB', jasperlibdir) + + else: + if os.getenv('JASPERINC') or os.getenv('JASPERLIB'): + raise EasyBuildError("JasPer module not loaded, but JASPERINC and/or JASPERLIB still set?") + else: + self.log.info("JasPer module not loaded, assuming that's OK...") + + # enable support for large file support in netCDF + env.setvar('WRFIO_NCD_LARGE_FILE_SUPPORT', '1') + + # patch arch/Config_new.pl script, so that run_cmd_qa receives all output to answer questions + if LooseVersion(self.version) < LooseVersion('4.0'): + patch_perl_script_autoflush(os.path.join(wrfdir, "arch", "Config_new.pl")) + + # determine build type option to look for + build_type_option = None + self.comp_fam = self.toolchain.comp_family() + if self.comp_fam == toolchain.INTELCOMP: # @UndefinedVariable + if LooseVersion(self.version) >= LooseVersion('3.7'): + build_type_option = r"INTEL\ \(ifort\/icc\)" + else: + build_type_option = "Linux x86_64 i486 i586 i686, ifort compiler with icc" + + elif self.comp_fam == toolchain.GCC: # @UndefinedVariable + if LooseVersion(self.version) >= LooseVersion('3.7'): + build_type_option = r"GNU\ \(gfortran\/gcc\)" + else: + build_type_option = "x86_64 Linux, gfortran compiler with gcc" + + else: + raise EasyBuildError("Don't know how to figure out build type to select.") + + # fetch selected build type (and make sure it makes sense) + known_build_types = ['serial', 'smpar', 'dmpar', 'dm+sm'] + self.parallel_build_types = ["dmpar", "dm+sm"] + bt = self.cfg['buildtype'] + + if bt not in known_build_types: + raise EasyBuildError("Unknown build type: '%s'. Supported build types: %s", bt, known_build_types) + + # Escape the "+" in "dm+sm" since it's being used in a regexp below. + bt = bt.replace('+', r'\+') + + # fetch option number based on build type option and selected build type + if LooseVersion(self.version) >= LooseVersion('3.7'): + # the two relevant lines in the configure output for WRF 3.8 are: + # 13. (serial) 14. (smpar) 15. (dmpar) 16. (dm+sm) INTEL (ifort/icc) + # 32. (serial) 33. (smpar) 34. (dmpar) 35. (dm+sm) GNU (gfortran/gcc) + build_type_question = r"\s*(?P<nr>[0-9]+)\.\ \(%s\).*%s" % (bt, build_type_option) + else: + # the relevant lines in the configure output for WRF 3.6 are: + # 13. Linux x86_64 i486 i586 i686, ifort compiler with icc (serial) + # 14. Linux x86_64 i486 i586 i686, ifort compiler with icc (smpar) + # 15. Linux x86_64 i486 i586 i686, ifort compiler with icc (dmpar) + # 16. Linux x86_64 i486 i586 i686, ifort compiler with icc (dm+sm) + # 32. x86_64 Linux, gfortran compiler with gcc (serial) + # 33. x86_64 Linux, gfortran compiler with gcc (smpar) + # 34. x86_64 Linux, gfortran compiler with gcc (dmpar) + # 35. x86_64 Linux, gfortran compiler with gcc (dm+sm) + build_type_question = r"\s*(?P<nr>[0-9]+).\s*%s\s*\(%s\)" % (build_type_option, bt) + + # run configure script + cmd = ' '.join([self.cfg['preconfigopts'], './configure', self.cfg['configopts']]) + qa = { + # named group in match will be used to construct answer + "Compile for nesting? (1=basic, 2=preset moves, 3=vortex following) [default 1]:": "1", + "Compile for nesting? (0=no nesting, 1=basic, 2=preset moves, 3=vortex following) [default 0]:": "0" + } + no_qa = [ + "testing for fseeko and fseeko64", + r"If you wish to change the default options, edit the file:[\s\n]*arch/configure_new.defaults" + ] + std_qa = { + # named group in match will be used to construct answer + r"%s.*\n(.*\n)*Enter selection\s*\[[0-9]+-[0-9]+\]\s*:" % build_type_question: "%(nr)s", + } + + run_cmd_qa(cmd, qa, no_qa=no_qa, std_qa=std_qa, log_all=True, simple=True, maxhits=200) + + cfgfile = 'configure.wrf' + + # make sure correct compilers are being used + comps = { + 'SCC': os.getenv('CC'), + 'SFC': os.getenv('F90'), + 'CCOMP': os.getenv('CC'), + 'DM_FC': os.getenv('MPIF90'), + 'DM_CC': "%s -DMPI2_SUPPORT" % os.getenv('MPICC'), + } + regex_subs = [(r"^(%s\s*=\s*).*$" % k, r"\1 %s" % v) for (k, v) in comps.items()] + # fix hardcoded preprocessor + regex_subs.append(('/lib/cpp', 'cpp')) + + apply_regex_substitutions(cfgfile, regex_subs) + + # rewrite optimization options if desired + if self.cfg['rewriteopts']: + + # replace default -O3 option in configure.wrf with CFLAGS/FFLAGS from environment + self.log.info("Rewriting optimization options in %s" % cfgfile) + + # set extra flags for Intel compilers + # see http://software.intel.com/en-us/forums/showthread.php?t=72109&p=1#146748 + if self.comp_fam == toolchain.INTELCOMP: # @UndefinedVariable + + # -O3 -heap-arrays is required to resolve compilation error + for envvar in ['CFLAGS', 'FFLAGS']: + val = os.getenv(envvar) + if '-O3' in val: + env.setvar(envvar, '%s -heap-arrays' % val) + self.log.info("Updated %s to '%s'" % (envvar, os.getenv(envvar))) + + # replace -O3 with desired optimization options + regex_subs = [ + (r"^(FCOPTIM.*)(\s-O3)(\s.*)$", r"\1 %s \3" % os.getenv('FFLAGS')), + (r"^(CFLAGS_LOCAL.*)(\s-O3)(\s.*)$", r"\1 %s \3" % os.getenv('CFLAGS')), + ] + apply_regex_substitutions(cfgfile, regex_subs) + + def build_step(self): + """Build and install WRF and testcases using provided compile script.""" + + # enable parallel build + par = self.cfg['parallel'] + self.par = '' + if par: + self.par = "-j %s" % par + + # fix compile script shebang to use provided tcsh + cmpscript = os.path.join(self.start_dir, 'compile') + tcsh_root = get_software_root('tcsh') + if tcsh_root: + tcsh_path = os.path.join(tcsh_root, 'bin', 'tcsh') + # avoid using full path to tcsh if possible, since it may be too long to be used as shebang line + which_tcsh = which('tcsh') + if which_tcsh and os.path.samefile(which_tcsh, tcsh_path): + env_path = os.path.join('/usr', 'bin', 'env') + # use env command from alternate sysroot, if available + sysroot = build_option('sysroot') + if sysroot: + sysroot_env_path = os.path.join(sysroot, 'usr', 'bin', 'env') + if os.path.exists(sysroot_env_path): + env_path = sysroot_env_path + new_shebang = env_path + ' tcsh' + else: + new_shebang = tcsh_path + + regex_subs = [('^#!/bin/csh.*', '#!' + new_shebang)] + apply_regex_substitutions(cmpscript, regex_subs) + + # build wrf + cmd = "%s %s wrf" % (cmpscript, self.par) + run_cmd(cmd, log_all=True, simple=True, log_output=True) + + # build two testcases to produce ideal.exe and real.exe + for test in ["em_real", "em_b_wave"]: + cmd = "%s %s %s" % (cmpscript, self.par, test) + run_cmd(cmd, log_all=True, simple=True, log_output=True) + + def test_step(self): + """Build and run tests included in the WRF distribution.""" + if self.cfg['runtest']: + + if self.cfg['buildtype'] in self.parallel_build_types and not build_option('mpi_tests'): + self.log.info("Skipping testing of WRF with build type '%s' since MPI testing is disabled", + self.cfg['buildtype']) + return + + # get list of WRF test cases + self.testcases = [] + if os.path.exists('test'): + self.testcases = os.listdir('test') + + elif not self.dry_run: + raise EasyBuildError("Test directory not found, failed to determine list of test cases") + + # exclude 2d testcases in parallel WRF builds + if self.cfg['buildtype'] in self.parallel_build_types: + self.testcases = [test for test in self.testcases if '2d_' not in test] + + # exclude real testcases + self.testcases = [test for test in self.testcases if not test.endswith("_real")] + + self.log.debug("intermediate list of testcases: %s" % self.testcases) + + # exclude tests that should not be run + for test in ["em_esmf_exp", "em_scm_xy", "nmm_tropical_cyclone"]: + if test in self.testcases: + self.testcases.remove(test) + + # some tests hang when WRF is built with Intel compilers + if self.comp_fam == toolchain.INTELCOMP: # @UndefinedVariable + for test in ["em_heldsuarez"]: + if test in self.testcases: + self.testcases.remove(test) + + # determine number of MPI ranks to use in tests (1/2 of available processors + 1); + # we need to limit max number of MPI ranks (8 is too high for some tests, 4 is OK), + # since otherwise run may fail because domain size is too small + n_mpi_ranks = min(self.cfg['parallel'] // 2 + 1, 4) + + # prepare run command + + # stack limit needs to be set to unlimited for WRF to work well + if self.cfg['buildtype'] in self.parallel_build_types: + test_cmd = "ulimit -s unlimited && %s && %s" % (self.toolchain.mpi_cmd_for("./ideal.exe", 1), + self.toolchain.mpi_cmd_for("./wrf.exe", n_mpi_ranks)) + else: + test_cmd = "ulimit -s unlimited && ./ideal.exe && ./wrf.exe >rsl.error.0000 2>&1" + + # regex to check for successful test run + re_success = re.compile("SUCCESS COMPLETE WRF") + + def run_test(): + """Run a single test and check for success.""" + + # run test + (_, ec) = run_cmd(test_cmd, log_all=False, log_ok=False, simple=False) + + # read output file + out_fn = 'rsl.error.0000' + if os.path.exists(out_fn): + out_txt = read_file(out_fn) + else: + out_txt = 'FILE NOT FOUND' + + if ec == 0: + # exit code zero suggests success, but let's make sure... + if re_success.search(out_txt): + self.log.info("Test %s ran successfully (found '%s' in %s)", test, re_success.pattern, out_fn) + else: + raise EasyBuildError("Test %s failed, pattern '%s' not found in %s: %s", + test, re_success.pattern, out_fn, out_txt) + else: + # non-zero exit code means trouble, show command output + raise EasyBuildError("Test %s failed with exit code %s, output: %s", test, ec, out_txt) + + # clean up stuff that gets in the way + fn_prefs = ["wrfinput_", "namelist.output", "wrfout_", "rsl.out.", "rsl.error."] + for filename in os.listdir('.'): + for pref in fn_prefs: + if filename.startswith(pref): + remove_file(filename) + self.log.debug("Cleaned up file %s", filename) + + # build and run each test case individually + for test in self.testcases: + + self.log.debug("Building and running test %s" % test) + + # build and install + cmd = "./compile %s %s" % (self.par, test) + run_cmd(cmd, log_all=True, simple=True) + + # run test + try: + prev_dir = change_dir('run') + + if test in ["em_fire"]: + + # handle tests with subtests seperately + testdir = os.path.join("..", "test", test) + + for subtest in [x for x in os.listdir(testdir) if os.path.isdir(x)]: + + subtestdir = os.path.join(testdir, subtest) + + # link required files + for filename in os.listdir(subtestdir): + if os.path.exists(filename): + remove_file(filename) + symlink(os.path.join(subtestdir, filename), filename) + + # run test + run_test() + + else: + + # run test + run_test() + + change_dir(prev_dir) + + except OSError as err: + raise EasyBuildError("An error occured when running test %s: %s", test, err) + + # building/installing is done in build_step, so we can run tests + def install_step(self): + """Building was done in install dir, so nothing to do in install_step.""" + pass + + def sanity_check_step(self): + """Custom sanity check for WRF.""" + + files = ['libwrflib.a', 'wrf.exe', 'ideal.exe', 'real.exe', 'ndown.exe', 'tc.exe'] + # nup.exe was 'temporarily removed' in WRF v3.7, at least until 3.8 + if LooseVersion(self.version) < LooseVersion('3.7'): + files.append('nup.exe') + + custom_paths = { + 'files': [os.path.join(self.wrfsubdir, 'main', f) for f in files], + 'dirs': [os.path.join(self.wrfsubdir, d) for d in ['main', 'run']], + } + + super(EB_WRF, self).sanity_check_step(custom_paths=custom_paths) + + def make_module_req_guess(self): + """Path-like environment variable updates specific to WRF.""" + + maindir = os.path.join(self.wrfsubdir, 'main') + return { + 'PATH': [maindir], + 'LD_LIBRARY_PATH': [maindir], + 'MANPATH': [], + } + + def make_module_extra(self): + """Add netCDF environment variables to module file.""" + txt = super(EB_WRF, self).make_module_extra() + for netcdf_var in ['NETCDF', 'NETCDFF']: + if os.getenv(netcdf_var) is not None: + txt += self.module_generator.set_environment(netcdf_var, os.getenv(netcdf_var)) + return txt