diff --git a/easybuild/easyblocks/l/lammps.py b/easybuild/easyblocks/l/lammps.py index 6f48322643..ff214df1c3 100644 --- a/easybuild/easyblocks/l/lammps.py +++ b/easybuild/easyblocks/l/lammps.py @@ -46,7 +46,7 @@ from easybuild.tools.filetools import copy_dir, copy_file, mkdir, read_file from easybuild.tools.modules import get_software_root, get_software_version from easybuild.tools.run import run_shell_cmd -from easybuild.tools.systemtools import AARCH64, get_cpu_architecture, get_shared_lib_ext +from easybuild.tools.systemtools import AARCH64, get_cpu_architecture, get_shared_lib_ext, get_avail_core_count from easybuild.tools.toolchain.compiler import OPTARCH_GENERIC from easybuild.easyblocks.generic.cmakemake import CMakeMake @@ -359,7 +359,7 @@ def prepare_step(self, *args, **kwargs): # version 1.3.2 is used in the test suite to check easyblock can be initialised if self.version != '1.3.2': # take into account that build directory may not be available (in case of --module-only) - if os.path.exists(self.start_dir) and os.listdir(self.start_dir): + if self.start_dir and os.path.exists(self.start_dir) and os.listdir(self.start_dir): self.cur_version = translate_lammps_version(self.version, path=self.start_dir) else: self.cur_version = translate_lammps_version(self.version, path=self.installdir) @@ -471,6 +471,7 @@ def configure_step(self, **kwargs): # https://docs.lammps.org/Build_basics.html # https://docs.lammps.org/Build_settings.html # https://docs.lammps.org/Build_package.html + # https://docs.lammps.org/Build_extras.html if self.cfg['general_packages']: for package in self.cfg['general_packages']: self.cfg.update('configopts', '-D%s%s=on' % (self.pkg_prefix, package)) @@ -478,23 +479,11 @@ def configure_step(self, **kwargs): if self.cfg['user_packages']: for package in self.cfg['user_packages']: self.cfg.update('configopts', '-D%s%s=on' % (self.pkg_user_prefix, package)) - - # Optimization settings + # OPT package pkg_opt = '-D%sOPT=' % self.pkg_prefix if pkg_opt not in self.cfg['configopts']: self.cfg.update('configopts', pkg_opt + 'on') - # grab the architecture so we can check if we have Intel hardware (also used for Kokkos below) - processor_arch, gpu_arch = self.get_kokkos_arch(cuda_cc, self.cfg['kokkos_arch']) - - if processor_arch in INTEL_PACKAGE_ARCH_LIST or \ - (processor_arch == 'NATIVE' and self.kokkos_cpu_mapping.get(get_cpu_arch()) in INTEL_PACKAGE_ARCH_LIST): - # USER-INTEL enables optimizations on Intel processors. GCC has also partial support for some of them. - pkg_user_intel = '-D%sINTEL=' % self.pkg_user_prefix - if pkg_user_intel not in self.cfg['configopts']: - if self.toolchain.comp_family() in [toolchain.GCC, toolchain.INTELCOMP]: - self.cfg.update('configopts', pkg_user_intel + 'on') - # MPI/OpenMP if self.toolchain.options.get('usempi', None): self.cfg.update('configopts', '-DBUILD_MPI=yes') @@ -517,11 +506,27 @@ def configure_step(self, **kwargs): if '-DFFT_PACK=' not in self.cfg['configopts']: self.cfg.update('configopts', '-DFFT_PACK=array') - # https://lammps.sandia.gov/doc/Build_extras.html - # KOKKOS + # detect the CPU and GPU architecture (used for Intel and Kokkos packages belos) + processor_arch, gpu_arch = self.get_kokkos_arch(cuda_cc, self.cfg['kokkos_arch']) + + # INTEL package + self.pkg_intel = False + if processor_arch in INTEL_PACKAGE_ARCH_LIST or \ + (processor_arch == 'NATIVE' and self.kokkos_cpu_mapping.get(get_cpu_arch()) in INTEL_PACKAGE_ARCH_LIST): + # USER-INTEL enables optimizations on Intel processors. GCC has also partial support for some of them. + pkg_user_intel = '-D%sINTEL=' % self.pkg_user_prefix + if pkg_user_intel not in self.cfg['configopts']: + if self.toolchain.comp_family() in [toolchain.GCC, toolchain.INTELCOMP]: + self.cfg.update('configopts', pkg_user_intel + 'on') + self.pkg_intel = True + else: + self.pkg_intel = True + + # KOKKOS package if self.cfg['kokkos']: print_msg("Using Kokkos package with arch: CPU - %s, GPU - %s" % (processor_arch, gpu_arch)) self.cfg.update('configopts', '-D%sKOKKOS=on' % self.pkg_prefix) + self.cfg.update('configopts', '-D%s_ENABLE_SERIAL=yes' % self.kokkos_prefix) if self.toolchain.options.get('openmp', None): self.cfg.update('configopts', '-D%s_ENABLE_OPENMP=yes' % self.kokkos_prefix) @@ -557,7 +562,7 @@ def configure_step(self, **kwargs): elif get_software_root("FFTW"): self.cfg.update('configopts', '-DFFT_KOKKOS=FFTW3') - # CUDA only + # GPU package (cannot be built with KOKKOS+CUDA) elif self.cuda: print_msg("Using GPU package (not Kokkos) with arch: CPU - %s, GPU - %s" % (processor_arch, gpu_arch)) self.cfg.update('configopts', '-D%sGPU=on' % self.pkg_prefix) @@ -568,6 +573,7 @@ def configure_step(self, **kwargs): # to lib64) self.cfg.update('configopts', '-DCMAKE_INSTALL_LIBDIR=lib') + # Python interface # avoid that pip (ab)uses $HOME/.cache/pip # cfr. https://pip.pypa.io/en/stable/reference/pip_install/#caching env.setvar('XDG_CACHE_HOME', tempfile.gettempdir()) @@ -602,6 +608,23 @@ def configure_step(self, **kwargs): else: raise EasyBuildError("Expected to find a Python dependency as sanity check commands rely on it!") + # Testing (PyYAML must be installed) + if self.cfg['runtest'] is None or self.cfg['runtest']: + if LooseVersion(self.cur_version) >= LooseVersion(translate_lammps_version('29Aug2024')): + # Testing of KOKKOS+CUDA builds does not work for version < 22Jul2025 + # See: https://github.com/lammps/lammps/issues/405 + if LooseVersion(self.cur_version) < LooseVersion(translate_lammps_version('22Jul2025')) \ + and self.cfg['kokkos'] and self.cuda: + print_warning("Skipping tests: KOKKOS+CUDA builds have broken testing in LAMMPS < 22Jul2025.") + self.cfg['runtest'] = False + else: + self.cfg['runtest'] = True + if 'PyYAML' not in (dep['name'] for dep in self.cfg.builddependencies()): + raise EasyBuildError("PyYAML not included as build dependency: cannot run tests.") + self.cfg.update('configopts', '-DENABLE_TESTING=on') + else: + self.cfg['runtest'] = False + return super().configure_step() def install_step(self): @@ -648,6 +671,19 @@ def install_step(self): run_shell_cmd(cmd) + def test_step(self): + """Filter the ctests that should be run""" + # add flags to test_cmd to ignore some tests + if self.cfg.get('runtest') is True and not self.cfg.get('test_cmd'): + test_cmd = 'ctest' + if LooseVersion(self.cmake_version) >= '3.17.0': + test_cmd += ' --no-tests=error' + test_cmd += ' -LE unstable -E "TestMliapPyUnified|AtomicPairStyle:meam_spline|KSpaceStyle:scafacos.*"' + self.log.debug(f"Running tests using test_cmd = '{test_cmd}' as test_cmd") + self.cfg['test_cmd'] = test_cmd + + super().test_step() + def sanity_check_step(self, *args, **kwargs): """Run custom sanity checks for LAMMPS files, dirs and commands.""" @@ -655,6 +691,7 @@ def sanity_check_step(self, *args, **kwargs): if self.cur_version is None: self.cur_version = translate_lammps_version(self.version, path=self.installdir) + # Test some LAMMPS examples # Output files need to go somewhere (and has to work for --module-only as well) execution_dir = tempfile.mkdtemp() @@ -676,15 +713,53 @@ def sanity_check_step(self, *args, **kwargs): for check_file in sanity_check_test_inputs ] + # add accelerator-specific tests + if self.pkg_intel: # INTEL package + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "intel"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + if self.cfg['kokkos']: # KOKKOS package + if self.cuda: # NOTE: requires a GPU to run + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "kk", "-k", "on", "g", "1"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + else: + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "kk", "-k", "on"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + elif self.cuda: # GPU package + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "gpu", "-pk", "gpu", "1"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + if self.toolchain.options.get('openmp', None): # OPENMP package + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "omp", "-pk", "omp", "2"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + # OPT package + custom_commands.append( + 'from lammps import lammps; l=lammps(cmdargs=["-sf", "opt"]); l.file("%s")' % + os.path.join(self.installdir, "examples", "msst", "in.msst") + ) + # mpirun command needs an l.finalize() in the sanity check from LAMMPS 29Sep2021 - if LooseVersion(self.cur_version) >= LooseVersion(translate_lammps_version('29Sep2021')): + # This is actually not needed if mpi4py is installed, and can cause a crash in version 2025+ + if LooseVersion(self.cur_version) >= LooseVersion(translate_lammps_version('29Sep2021')) and \ + LooseVersion(self.cur_version) < LooseVersion(translate_lammps_version('22Jul2025')): custom_commands = [cmd + '; l.finalize()' for cmd in custom_commands] custom_commands = ["""python -c '%s'""" % cmd for cmd in custom_commands] # Execute sanity check commands within an initialized MPI in MPI enabled toolchains if self.toolchain.options.get('usempi', None): - custom_commands = [self.toolchain.mpi_cmd_for(cmd, 1) for cmd in custom_commands] + # use up to 4 cores, to speed up tests + test_core_cnt = min(self.cfg.parallel, get_avail_core_count(), 4) + self.log.info("Using %s cores for the MPI tests" % test_core_cnt) + custom_commands = [self.toolchain.mpi_cmd_for(cmd, test_core_cnt) for cmd in custom_commands] custom_commands = ["cd %s && " % execution_dir + cmd for cmd in custom_commands]