From 7d019f74dd7e437e38c3a3721a61be7ea92b7a05 Mon Sep 17 00:00:00 2001 From: Guillaume Raffy Date: Tue, 26 Nov 2024 13:33:12 +0100 Subject: [PATCH] v0.0.11 - made IprCluster target host work. This target host provides access to intel ifort and intel mkl packages, which are available as environment modules. - added a unit test that validates the IprCluster (only works on ipr cluster node) - also improved unit tests to use TMPDIR environment variable if present, in order to avoid permission issues in case the test is run by different users on the same system. - made the benchmark display an error in case starbench fails work related to [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=3958] --- ci/ipr.jenkins | 11 ++++- ci/test_iprcluster.py | 67 ++++++++++++++++++++++++++++++ iprbench/benchmarks/hibench.py | 4 +- iprbench/benchmarks/mamul1.py | 4 +- iprbench/benchmarks/showresults.py | 2 +- iprbench/envmodules.py | 47 +++++++++++++++++++++ iprbench/targethosts.py | 53 ++++++++++++++++++----- iprbench/version.py | 2 +- test/test_resultsdb.py | 3 +- 9 files changed, 176 insertions(+), 17 deletions(-) create mode 100644 ci/test_iprcluster.py create mode 100644 iprbench/envmodules.py diff --git a/ci/ipr.jenkins b/ci/ipr.jenkins index 625f76d..e202737 100644 --- a/ci/ipr.jenkins +++ b/ci/ipr.jenkins @@ -22,7 +22,16 @@ pipeline { sh """#!/bin/bash set -o errexit source ${IPRBENCH_VENV_PATH}/bin/activate && - python -m unittest + python -m unittest ./test + """ + } + } + stage('testing (tests that only work on ipr cluster nodes)') { + steps { + sh """#!/bin/bash + set -o errexit + source ${IPRBENCH_VENV_PATH}/bin/activate && + python -m unittest ./ci/test_iprcluster.py """ } } diff --git a/ci/test_iprcluster.py b/ci/test_iprcluster.py new file mode 100644 index 0000000..79bb933 --- /dev/null +++ b/ci/test_iprcluster.py @@ -0,0 +1,67 @@ +"""tests specific for IPR cluster +""" +import unittest +import logging +import subprocess +import json +from pathlib import Path +from shutil import rmtree +from os import getenv +from iprbench.core import ResultsDbParams, HostTypeId +from cocluto.SimpaDbUtil import SshAccessedMysqlDb + + +def test_resultsdb(resultsdb_params: ResultsDbParams, results_root_path: Path): + results_db_type = resultsdb_params['type'] + logging.info('testing resultsdb %s', results_db_type) + logging.info('resultsdb_params : %s', json.dumps(resultsdb_params)) + results_dir = results_root_path / results_db_type + if results_dir.exists(): + rmtree(results_dir) + results_dir.mkdir(parents=True) + benchmark_id = 'mamul1' + benchmark_config = { + 'fortran_compiler': 'ifort:', + 'blas_library': 'intelmkl:', + 'matrix_size': 1024, + 'num_loops': 10, + 'num_cores': 2, + 'launcher': 'iprbench.unittest.iprcluster', + } + target_system_type_id = HostTypeId('fr.univ-rennes.ipr.cluster-node') + command = f'iprbench-run --benchmark-id \'{benchmark_id}\' --config \'{json.dumps(benchmark_config)}\' --results-dir {results_dir} --resultsdb-params \'{json.dumps(resultsdb_params)}\' --target-system-type-id "{target_system_type_id}"' + subprocess.run(command, shell=True, check=True, executable='/bin/bash') + + +class IprClusterTestCase(unittest.TestCase): + + results_root_dir: Path + + def setUp(self) -> None: # pylint: disable=useless-parent-delegation + self.results_root_dir = Path(f'{getenv("TMPDIR", default="/tmp")}/iprbenchs/test_results/resultsdb') + if self.results_root_dir.exists(): + rmtree(self.results_root_dir) + self.results_root_dir.mkdir(parents=True) + return super().setUp() + + def test_sqlserver(self): + db_server_fqdn = 'iprbenchsdb.ipr.univ-rennes1.fr' + db_user = 'test_iprbenchw' + db_name = 'test_iprbenchs' + ssh_user = 'test_iprbenchw' + sql_backend = SshAccessedMysqlDb(db_server_fqdn, db_user, db_name, ssh_user) + if sql_backend.table_exists('mamul1'): + sql_backend.delete_table('mamul1') + resultsdb_params = { + 'type': 'sqlserver-viassh-database', + 'db_server_fqdn': db_server_fqdn, + 'db_user': db_user, + 'db_name': db_name, + 'ssh_user': ssh_user + } + test_resultsdb(resultsdb_params, self.results_root_dir) + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) + unittest.main() diff --git a/iprbench/benchmarks/hibench.py b/iprbench/benchmarks/hibench.py index c78c9fd..be4c31b 100644 --- a/iprbench/benchmarks/hibench.py +++ b/iprbench/benchmarks/hibench.py @@ -85,7 +85,9 @@ class HiBench(IBenchmark): if len(env_vars_bash_commands) > 0: shell_command += f'{env_vars_bash_commands} && ' shell_command += f'{get_proxy_env_vars()} starbench --source-tree-provider \'{source_tree_provider}\' --num-cores {num_cores} --output-dir={output_dir} --cmake-path={cmake_path} {" ".join([f"--cmake-option={option}" for option in cmake_options])} --benchmark-command=\'{benchmark_command}\' --output-measurements={output_measurements_file_path}' - subprocess.run(shell_command, shell=True, check=True, executable='/bin/bash') + completed_process = subprocess.run(shell_command, shell=True, check=False, executable='/bin/bash') # for some reason, module is only found when executable is /bin/bash + if completed_process.returncode != 0: + assert False, f'the shell command "{shell_command}" failed. See details in {output_dir}' measurements: BenchmarkMeasurements = {} starbench_results = StarbenchResults(output_measurements_file_path) diff --git a/iprbench/benchmarks/mamul1.py b/iprbench/benchmarks/mamul1.py index f7d2ea0..a6eed46 100644 --- a/iprbench/benchmarks/mamul1.py +++ b/iprbench/benchmarks/mamul1.py @@ -76,7 +76,9 @@ class MaMul1(IBenchmark): shell_command += f'{env_vars_bash_commands} && ' shell_command += f'starbench --source-tree-provider \'{source_tree_provider}\' --num-cores {num_cores} --output-dir={output_dir} --cmake-path=/usr/bin/cmake {" ".join([f"--cmake-option={option}" for option in cmake_options])} --benchmark-command=\'{" ".join(benchmark_command)}\' --output-measurements={output_measurements_file_path}' logging.debug('shell_command = "%s"', shell_command) - subprocess.run(shell_command, shell=True, check=True, encoding='/bin/bash') + completed_process = subprocess.run(shell_command, shell=True, check=False, executable='/bin/bash') # for some reason, module is only found when executable is /bin/bash + if completed_process.returncode != 0: + assert False, f'the shell command "{shell_command}" failed. See details in {output_dir}' measurements: BenchmarkMeasurements = {} starbench_results = StarbenchResults(output_measurements_file_path) measurements['duration_avg'] = starbench_results.get_average_duration() diff --git a/iprbench/benchmarks/showresults.py b/iprbench/benchmarks/showresults.py index 5880662..48feda7 100755 --- a/iprbench/benchmarks/showresults.py +++ b/iprbench/benchmarks/showresults.py @@ -40,7 +40,7 @@ class HibenchResultsParser(): # module-whatis Sets the Environment to use the Intel Math Kernel Libraries version 2020.0.1 # setenv MKLROOT /opt/intel/compilers_and_libraries_2020.1.217/linux/mkl - lib_info['lib_name'] = 'mkl' + lib_info['lib_name'] = 'intelmkl' lib_info['lib_version'] = '2020.0.1' break elif path.find('/usr/lib/libblas.so') != -1: diff --git a/iprbench/envmodules.py b/iprbench/envmodules.py new file mode 100644 index 0000000..a20b60d --- /dev/null +++ b/iprbench/envmodules.py @@ -0,0 +1,47 @@ +from typing import Set +from pathlib import Path +import subprocess +import re + +EnvModule = str # eg compilers/ifort/latest + + +def get_available_modules() -> Set[EnvModule]: + available_modules = set() + completed_process = subprocess.run('module avail --terse', executable='/bin/bash', shell=True, capture_output=True, check=True) # for some reason, module is only found when executable is /bin/bash + # (iprbench.venv) graffy@alambix50:/opt/ipr/cluster/work.local/graffy/bug3958/iprbench.git$ module avail --terse + # /usr/share/modules/modulefiles: + # compilers/ifort/15.0.2 + # compilers/ifort/17.0.1 + # ... + # lib/mpi/intelmpi/2021.13.0 + # lib/mpi/intelmpi/latest + # module-git + # module-info + # modules + # null + stdout = completed_process.stdout.decode('utf-8') + for line in stdout.splitlines(): + if not re.search(r'\:$', line): # ignore the directories such as '/usr/share/modules/modulefiles:' + available_modules.add(EnvModule(line)) + return available_modules + + +def get_module_file_path(env_module: EnvModule) -> Path: + # graffy@alambix-frontal:~$ module help compilers/ifort/latest + # ------------------------------------------------------------------- + # Module Specific Help for /usr/share/modules/modulefiles/compilers/ifort/latest: + + # Provides the same functionality as the command '/opt/intel/oneapi-2024.2.1/compiler/latest/env/vars.sh intel64' + # ------------------------------------------------------------------- + module_file_path = None + completed_process = subprocess.run(f'module help {env_module}', executable='/bin/bash', shell=True, capture_output=True, check=True) # for some reason, module is only found when executable is /bin/bash + stdout = completed_process.stdout.decode('utf-8') + for line in stdout.splitlines(): + match = re.match(r'^Module Specific Help for (?P[^:]+):', line) + if match: + module_file_path = Path(match['module_file_path']) + break + if module_file_path is None: + raise ValueError(f'failed to find the file path of the environment module {env_module}') + return module_file_path diff --git a/iprbench/targethosts.py b/iprbench/targethosts.py index f4d2962..ec193d3 100644 --- a/iprbench/targethosts.py +++ b/iprbench/targethosts.py @@ -3,6 +3,8 @@ import subprocess import re from pathlib import Path from .core import ITargetHost, PackageId, PackageVersion, HostTypeId +from .envmodules import EnvModule, get_available_modules, get_module_file_path +# import logging DebianPackageVersion = str # a version string, as in debian package versions, eg 4:9.3.0-1ubuntu2 @@ -20,7 +22,7 @@ class DebianHost(ITargetHost): package_version = self.get_installed_package_version(package_id) return package_version - def get_package_activation_command(self, package_id: str, package_version: str) -> str: + def get_package_activation_command(self, package_id: PackageId, package_version: str) -> str: current_version = self.get_package_default_version(package_id) if current_version != package_version: raise ValueError(f'{package_id} version {package_version} not available: only {package_id} version {current_version} is available on this host') @@ -161,20 +163,49 @@ class IprClusterNode(DebianHost): def get_host_type_id(self) -> HostTypeId: return HostTypeId('fr.univ-rennes.ipr.cluster-node') - def get_latest_version_for_env_module(self, package_env_module: str): + def get_latest_version_for_env_module(self, package_env_module: str) -> PackageVersion: # package_env_module: eg compilers/ifort - # graffy@alambix-frontal:~$ module help compilers/ifort/latest - # ------------------------------------------------------------------- - # Module Specific Help for /usr/share/modules/modulefiles/compilers/ifort/latest: - # Provides the same functionality as the command '/opt/intel/oneapi-2024.2.1/compiler/latest/env/vars.sh intel64' - # ------------------------------------------------------------------- # graffy@alambix-frontal:~$ ls -l /usr/share/modules/modulefiles/compilers/ifort/latest # lrwxrwxrwx 1 root root 9 18 nov. 02:11 /usr/share/modules/modulefiles/compilers/ifort/latest -> 2021.13.1 - raise NotImplementedError() - def get_package_default_version(self, package_id: str) -> str: - if package_id == 'ifort': - return self.get_latest_version_for_env_module('compilers/ifort') + available_modules = get_available_modules() + shortcut_env_module = EnvModule(f'{package_env_module}/latest') + if shortcut_env_module not in available_modules: + raise ValueError(f'failed to find {shortcut_env_module} amongst available environment modules') + # logging.debug('available_modules = %s', available_modules) + shortcut_file_path = get_module_file_path(shortcut_env_module) + # logging.debug('shortcut_file_path = %s', shortcut_file_path) + assert shortcut_file_path.is_symlink(), f'unexpected case: {shortcut_file_path} is expected to be a symbolic link to an actual version of {package_env_module}' + real_file_path = shortcut_file_path.resolve() # eg /usr/share/modules/modulefiles/compilers/ifort/2021.13.1 + # logging.debug('real_file_path = %s', real_file_path) + version = real_file_path.name + match = re.match(r'^[0-9]+\.[0-9]+\.[0-9]$', version) + assert match, f'unexpected format for version: {version} for module file path {real_file_path}' + return PackageVersion(version) + + def get_package_default_version(self, package_id: PackageId) -> PackageVersion: + env_modules_packages = self._get_env_modules_packages() + if package_id in env_modules_packages.keys(): + return self.get_latest_version_for_env_module(env_modules_packages[package_id]) else: return super().get_package_default_version(package_id) + + def get_package_activation_command(self, package_id: PackageId, package_version: str) -> str: + env_modules_packages = self._get_env_modules_packages() + if package_id in env_modules_packages.keys(): + env_module = EnvModule(f'{env_modules_packages[package_id]}/{package_version}') + assert env_module in get_available_modules(), f'environment module "{env_module}" is not one of the available modules on this system' + return f'module load {env_module}' + else: + return super().get_package_activation_command(package_id, package_version) + + def _get_env_modules_packages(self) -> Dict[PackageId, str]: + """ gets list packages available as environment modules""" + return { + PackageId('ifort'): 'compilers/ifort', + PackageId('intelmkl'): 'lib/mkl', + } + + def _get_package_env_module_root(self, package_id: PackageId) -> str: + return self._get_env_modules_packages()[package_id] diff --git a/iprbench/version.py b/iprbench/version.py index 6820f36..ad3cf1d 100644 --- a/iprbench/version.py +++ b/iprbench/version.py @@ -1 +1 @@ -__version__ = '0.0.10' +__version__ = '0.0.11' diff --git a/test/test_resultsdb.py b/test/test_resultsdb.py index 56e6f2b..7ab7530 100644 --- a/test/test_resultsdb.py +++ b/test/test_resultsdb.py @@ -6,6 +6,7 @@ import subprocess import json from pathlib import Path from shutil import rmtree +from os import getenv from iprbench.core import ResultsDbParams, HostTypeId from cocluto.SimpaDbUtil import SshAccessedMysqlDb @@ -37,7 +38,7 @@ class ResultsDbTestCase(unittest.TestCase): results_root_dir: Path def setUp(self) -> None: # pylint: disable=useless-parent-delegation - self.results_root_dir = Path('/tmp/iprbenchs/test_results/resultsdb') + self.results_root_dir = Path(f'{getenv("TMPDIR", default="/tmp")}/iprbenchs/test_results/resultsdb') if self.results_root_dir.exists(): rmtree(self.results_root_dir) self.results_root_dir.mkdir(parents=True)