diff --git a/easybuild/easyblocks/c/cp2k.py b/easybuild/easyblocks/c/cp2k.py index f754ddf5b16..145f283cc30 100644 --- a/easybuild/easyblocks/c/cp2k.py +++ b/easybuild/easyblocks/c/cp2k.py @@ -35,6 +35,9 @@ @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) +@author: Pavel Tomanek (Inuits) """ import fileinput @@ -42,9 +45,8 @@ import re import os import sys -from easybuild.tools import LooseVersion - import easybuild.tools.toolchain as toolchain +from easybuild.tools import LooseVersion from easybuild.framework.easyblock import EasyBlock from easybuild.framework.easyconfig import CUSTOM from easybuild.tools.build_log import EasyBuildError @@ -80,6 +82,7 @@ def __init__(self, *args, **kwargs): # used for both libsmm and libxsmm self.libsmm = '' + self.libsmm_path = '' self.modincpath = '' self.openmp = '' @@ -90,19 +93,19 @@ def extra_options(): extra_vars = { 'extracflags': ['', "Extra CFLAGS to be added", CUSTOM], 'extradflags': ['', "Extra DFLAGS to be added", CUSTOM], - 'ignore_regtest_fails': [False, ("Ignore failures in regression test " - "(should be used with care)"), CUSTOM], + 'ignore_regtest_fails': [False, "Ignore failures in regression test", 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], + '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], + 'omp_num_threads': [None, "Value to set $OMP_NUM_THREADS to during testing, type=int", CUSTOM], 'plumed': [None, "Enable PLUMED support", CUSTOM], 'type': ['popt', "Type of build ('popt' or 'psmp')", CUSTOM], 'typeopt': [True, "Enable optimization", CUSTOM], + 'tests_maxtasks': [None, "--maxtasks in regtest command, type=int", CUSTOM], + 'tests_mpiranks': [None, "--mpiranks in regtest command, type=int", CUSTOM], + 'tests_maxerrors': [10000, "--maxerrors in regtest command, type=int", CUSTOM], + 'tests_timeout': [10000, "--timeout in regtest command, type=int", CUSTOM], } return EasyBlock.extra_options(extra_vars) @@ -110,7 +113,7 @@ 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()): + for key, value in options.items(): text += "%s = %s\n" % (key, value) return text + self.make_instructions @@ -120,10 +123,16 @@ def configure_step(self): - 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) + 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 2024+ popt maps to psmp with OMP_NUM_THREADS=1') # correct start dir, if needed # recent CP2K versions have a 'cp2k' dir in the unpacked 'cp2k' dir @@ -154,6 +163,7 @@ def configure_step(self): 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')) @@ -161,6 +171,7 @@ def configure_step(self): 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 @@ -193,7 +204,8 @@ def configure_step(self): options = self.configure_BLAS_lib(options) # FFTW (no MKL involved) - if 'fftw3' in os.getenv('LIBFFT', ''): + fftw_loaded = get_software_root('FFTW') or 'fftw3' in os.getenv('LIBFFT', '') + if fftw_loaded: options = self.configure_FFTW3(options) # LAPACK @@ -214,26 +226,72 @@ def configure_step(self): options['LIBS'] += ' -lplumed' options['DFLAGS'] += ' -D__PLUMED2' + # SIRIUS + sirius = get_software_root('SIRIUS') + if sirius: + options['DFLAGS'] += ' -D__SIRIUS' + + # includes for sirius.mod + include_dir = os.path.join(sirius, 'include', 'sirius') + if os.path.isdir(include_dir): + options['INCS'] += f' -I{include_dir}' + else: + options['INCS'] += f" -I{os.path.join(sirius, 'include')}" + + # ensure C++ linker flags are OK + if not options.get('CXX'): + options['CXX'] = os.getenv('MPICXX') or 'mpicxx' + + # library search paths + for libdir in (os.path.join(sirius, 'lib'), os.path.join(sirius, 'lib64')): + if os.path.isdir(libdir): + options['LDFLAGS'] += f' -L{libdir}' + + options['LIBS'] += ' -lsirius -lstdc++ -ldl' + # 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 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 cuda = get_software_root('CUDA') if cuda: - options['DFLAGS'] += ' -D__ACC -D__DBCSR_ACC' - options['LIBS'] += ' -lcudart -lcublas -lcufft -lrt' - options['NVCC'] = ' nvcc' + if cp2k_version >= LooseVersion('2024'): + # find cuda compute capabilities and pass it to ARCH_NUMBER + # from https://github.com/cp2k/cp2k/blob/v2025.2/exts/build_dbcsr/Makefile#L78 + get_gpu_cc = run_shell_cmd('nvidia-smi --query-gpu=compute_cap', hidden=True) + gpu_arch = get_gpu_cc.output.splitlines()[-1].strip().replace('.', '') + + 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' " + options['OFFLOAD_TARGET'] = 'cuda' + options['ARCH_NUMBER'] = gpu_arch + options['CXX'] = 'mpicxx' + options['CXXFLAGS'] = '-O3 -fopenmp -g -w --std=c++14 -fPIC $(DFLAGS) $(INCS)' + else: + 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', '') @@ -274,8 +332,10 @@ def prepmodinc(self): 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"]) + 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: @@ -299,10 +359,12 @@ def prepmodinc(self): 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 @@ -316,8 +378,13 @@ def configure_common(self): # 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] + 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 @@ -330,42 +397,50 @@ def configure_common(self): for mpi2lib in mpi2libs: if get_software_root(mpi2lib): mpi2 = True - self.log.debug("Determined MPI2 compatibility based on loaded MPI module: %s") + self.log.debug("Determined MPI2 compatibility based on loaded MPI module: %s" % mpi2lib) else: - self.log.debug("MPI-2 supporting MPI library %s not loaded.") + self.log.debug("MPI-2 supporting MPI library %s not loaded." % mpi2lib) 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') + ldflags = os.getenv('LDFLAGS') or '' + fflags = os.getenv('FFLAGS') or '' + mpicc = os.getenv('MPICC') or getattr(self.toolchain, 'cc', 'mpicc') + mpif90 = os.getenv('MPIF90') or getattr(self.toolchain, 'fc', 'mpif90') 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), + 'CC': mpicc, 'AR': 'ar -r', - 'CPPFLAGS': '', - + 'FC': mpif90, + 'LD': 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 -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', ''), } + # Make LIBXSMM visible for all CP2K versions (2023.x and 2024+) + if self.libsmm_path: + # headers / .mod + options['INCS'] += f' -I{self.libsmm_path}/include' + # library search path (keep both lib and lib64 in case) + options['LDFLAGS'] += f' -L{self.libsmm_path}/lib' + lib64 = os.path.join(self.libsmm_path, 'lib64') + if os.path.isdir(lib64): + options['LDFLAGS'] += f' -L{lib64}' + + # LIBINT libint = get_software_root('LibInt') if libint: options['DFLAGS'] += ' -D__LIBINT' @@ -411,12 +486,16 @@ def configure_common(self): 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 self.log.warning("Libint module not loaded, so building without Libint support") + # LIBXC libxc = get_software_root('libxc') if libxc: cur_libxc_version = get_software_version('libxc') @@ -431,25 +510,21 @@ def configure_common(self): 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") - libvori = get_software_root('libvori') - if libvori: - if LooseVersion(self.version) >= LooseVersion('8.1'): - options['LIBS'] += ' -lvori' - options['DFLAGS'] += ' -D__LIBVORI' - else: - raise EasyBuildError("This version of CP2K does not suppport libvori") + # only add existing include dirs + inc_main = os.path.join(libxc, 'include') + inc_sub = os.path.join(libxc, 'include', 'libxc') + if os.path.isdir(inc_main): + options['INCS'] += f' -I{inc_main}' + if os.path.isdir(inc_sub): + options['INCS'] += f' -I{inc_sub}' else: - self.log.info("libvori module not loaded, so building without support for Voronoi integration") + self.log.info("libxc module not loaded, so building without libxc support") return options @@ -458,8 +533,12 @@ def configure_intel_based(self): # 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/"]) + intelurl = ''.join( + [ + "http://software.intel.com/en-us/articles/", + "build-cp2k-using-intel-fortran-compiler-professional-edition/", + ] + ) options = self.configure_common() @@ -467,18 +546,17 @@ def configure_intel_based(self): 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.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': options['LDFLAGS'] + ' $(INCFLAGS) ', + 'OBJECTS_ARCHITECTURE': 'machine_intel.o', + } + ) options['DFLAGS'] += ' -D__INTEL' @@ -498,8 +576,9 @@ def configure_intel_based(self): # 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.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) @@ -544,21 +623,34 @@ def configure_intel_based(self): 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', - }) + cp2k_version = LooseVersion(self.version) + 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)' + ) + options['FCFLAGSOPT2'] = options['FCFLAGSOPT'].replace('-O3', '-O2') + else: + options.update( + { + # need this to prevent "Unterminated character constant beginning" errors + 'FREE': '-ffree-form -ffree-line-length-none ', + '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, @@ -573,6 +665,8 @@ def configure_GCC_based(self): 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' @@ -584,11 +678,19 @@ def configure_ACML(self, options): 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) + if cp2k_version >= LooseVersion('2024'): + options['LDFLAGS'] += ' -L%s/lib ' % self.libsmm_path + options['INCS'] += ' -I%s/include ' % self.libsmm_path options['LIBS'] += ' %s %s' % (self.libsmm, os.getenv('LIBBLAS', '')) return options @@ -628,21 +730,26 @@ def configure_MKL(self, options): 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', '') + cp2k_version = LooseVersion(self.version) + if cp2k_version >= LooseVersion('2024'): + options['LIBS'] += ' -lfftw3_omp -lfftw3' + libdir = os.getenv('FFT_LIB_DIR') + incdir = os.getenv('FFT_INC_DIR') + if libdir: + options['LDFLAGS'] += f' -L{libdir}' + if incdir: + options['INCS'] += f' -I{incdir}' 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', ''), + 'FFTW3INC': os.getenv('FFT_INC_DIR', ''), + 'FFTW3LIB': os.getenv('FFT_LIB_DIR', ''), + } + ) + libfft = os.getenv('LIBFFT_MT', '') if self.cfg['type'] == 'psmp' else 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): @@ -713,86 +820,122 @@ def test_step(self): return if self.cfg['omp_num_threads']: - setvar('OMP_NUM_THREADS', self.cfg['omp_num_threads']) + setvar('OMP_NUM_THREADS', str(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] + # set maxtasks to parallel if not specified in easyconfig file + tests_maxtasks = self.cfg['tests_maxtasks'] or self.cfg.parallel - regtest_cmd = ' '.join(regtest_cmd) + if LooseVersion(self.version) >= LooseVersion('2025'): + exedir = os.path.join(self.cfg['start_dir'], 'exe', self.typearch) + else: + exedir = self.typearch - # 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) + if LooseVersion(self.version) >= LooseVersion('2023.1'): + if LooseVersion(self.version) > LooseVersion('2024.0'): + regtest_script = os.path.join(self.cfg['start_dir'], 'tests', 'do_regtest.py') + else: + # only v2023.2 has test script in different directory + regtest_script = os.path.join(self.cfg['start_dir'], 'tools', 'regtesting', 'do_regtest.py') + regtest_cmd = [ + regtest_script, + f"--maxtasks {tests_maxtasks}", # parallel by default + f"--maxerrors {self.cfg['tests_maxerrors']}", # 10 000 by default + f"--timeout {self.cfg['tests_timeout']}", # 10 000 by default + "--debug", + # set --mpiranks test flag only when tests_mpiranks is set, 2 by default + *([f"--mpiranks {self.cfg['tests_mpiranks']}"] if self.cfg['tests_mpiranks'] else []), + # set --ompthreads test flag only when omp_num_threads is set, 2 by default + *([f"--ompthreads {self.cfg['omp_num_threads']}"] if self.cfg['omp_num_threads'] else []), + exedir, + self.cfg['type'], + ] + + else: # older versions up to v2023.1 + # configuration file for CP2K's test suite + 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] + + # patch do_regtest so that reference output is used + # 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 - else: - self.log.info("No reference output found for regression test, just continuing without it...") + 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) - # 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), - } + else: + self.log.info("No reference output found for regression test, just continuing without it...") - write_file(cfg_fn, cfg_txt) - self.log.debug("Contents of %s: %s" % (cfg_fn, cfg_txt)) + # 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': tests_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_cmd = ' '.join(regtest_cmd) regtest = run_shell_cmd(regtest_cmd, fail_on_error=False) - - if regtest.exit_code == 0: - self.log.info("Regression test output:\n%s" % regtest.output) + if regtest.exit_code == 0 and regtest.output: + self.log.info(f"Regression test output:\n{regtest.output}") + elif regtest.exit_code == 0: + raise EasyBuildError("Regression test failed: there is no output, tests probably did not run.") + elif regtest.exit_code != 0 and self.cfg['ignore_regtest_fails']: + self.log.info( + f"Regression test failed (non-zero exit code), but you set ignore_regtest_fails:\n{regtest.output}" + ) else: - raise EasyBuildError("Regression test failed (non-zero exit code): %s", regtest.output) + raise EasyBuildError(f"Regression test failed (non-zero exit code):\n{regtest.output}") # pattern to search for regression test summary re_pattern = r"number\s+of\s+%s\s+tests\s+(?P[0-9]+)" @@ -818,8 +961,9 @@ def test_report(test_result): 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()) + raise EasyBuildError( + "Finding number of %s tests in regression test summary failed", test_result.lower() + ) else: cnt = int(res.group('cnt')) @@ -918,10 +1062,7 @@ 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"] - } + 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'))