- added a the blas_library parameter to mamul1, for this:
  - added support for the `default-<packagetype>` keyword as package_id, which makes the parameter system to find the blas flavour of the default blas.
  - made the package default version retrieval more generic (replaces a gfortran specific code).

warning: these discovery mechanisms have only been implemented for debian hosts at the moment.

work related to [https://bugzilla.ipr.univ-rennes.fr/show_bug.cgi?id=3958]
This commit is contained in:
Guillaume Raffy 2024-11-21 08:29:45 +01:00
parent 7fd25890ec
commit 9d648b4fdc
5 changed files with 221 additions and 45 deletions

View File

@ -1,8 +1,20 @@
from ..core import IBenchmark, BenchParam, BenchmarkConfig, BenchmarkMeasurements, ITargetHost # , Package # PackageVariant
from ..core import IBenchmark, BenchParam, BenchmarkConfig, BenchmarkMeasurements, ITargetHost, PackageId
from pathlib import Path
import pandas as pd
import subprocess
from iprbench.util import extract_resource_dir
import logging
def get_bla_vendor(package_id: PackageId) -> str:
"""returns the bla vendor used by cmake's FindBLAS module
see https://cmake.org/cmake/help/latest/module/FindBLAS.html
"""
bla_vendor = {
'libopenblas-pthread': 'OpenBLAS',
'intelmkl': 'Intel10_64lp', # use 64 bits intel mkl with multithreading
}[package_id]
return bla_vendor
class MaMul1(IBenchmark):
@ -11,6 +23,7 @@ class MaMul1(IBenchmark):
def __init__(self):
bench_params = []
bench_params.append(BenchParam('fortran_compiler', BenchParam.Type.PARAM_TYPE_PACKAGE, 'the compiler used in the benchmark'))
bench_params.append(BenchParam('blas_library', BenchParam.Type.PARAM_TYPE_PACKAGE, 'the blas compatible linear algebra library used in the benchmark'))
bench_params.append(BenchParam('num_cores', BenchParam.Type.PARAM_TYPE_INT, 'the number of cores to use by this benchmark'))
bench_params.append(BenchParam('matrix_size', BenchParam.Type.PARAM_TYPE_INT, 'the size n of all the the n * n matrices'))
bench_params.append(BenchParam('num_loops', BenchParam.Type.PARAM_TYPE_INT, 'the number of identical multiplications performed in sequence'))
@ -32,6 +45,7 @@ class MaMul1(IBenchmark):
def execute(self, config: BenchmarkConfig, benchmark_output_dir: Path, target_host: ITargetHost) -> BenchmarkMeasurements:
fortran_compiler = config['fortran_compiler']
blas_library = config['blas_library']
num_cores = config['num_cores']
matrix_size = config['matrix_size']
num_loops = config['num_loops']
@ -49,14 +63,16 @@ class MaMul1(IBenchmark):
'-DCMAKE_BUILD_TYPE=Release', # build in release mode for highest performance
]
env_vars_bash_commands = target_host.get_package_activation_command(fortran_compiler.package_id, fortran_compiler.package_version)
package_activation_commands = []
for param in [fortran_compiler, blas_library]:
env_command = target_host.get_package_activation_command(param.package_id, param.package_version)
if env_command != '':
package_activation_commands.append(env_command)
env_vars_bash_commands = ' && '.join(package_activation_commands)
bla_vendor = get_bla_vendor(blas_library.package_id)
cmake_options.append(f'-DCMAKE_Fortran_COMPILER={fortran_compiler.package_id}')
if fortran_compiler.package_id == 'ifort':
cmake_options.append('-DBLA_VENDOR=Intel10_64lp') # use 64 bits intel mkl with multithreading
elif fortran_compiler.package_id == 'gfortran':
pass
else:
assert f'unhandled fortran_compiler_id : {fortran_compiler.package_id}'
cmake_options.append(f'-DBLA_VENDOR={bla_vendor}')
output_measurements_file_path = output_dir / "measurements.tsv"
@ -64,6 +80,7 @@ class MaMul1(IBenchmark):
if len(env_vars_bash_commands) > 0:
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')
measurements: BenchmarkMeasurements = {}
df = pd.read_csv(output_measurements_file_path, sep='\t')

View File

@ -5,17 +5,21 @@ from pathlib import Path
from datetime import datetime
from .util import Singleton
import json
import re
PackageVersion = str # a version string, such as 4.9.3
PackageId = str # a generic identifier of a package (eg libopenblas-pthread)
class ITargetHost(abc.ABC):
"""the host that runs the benchmark"""
@abc.abstractmethod
def get_package_default_version(self, package_id: str) -> str:
def get_package_default_version(self, package_id: PackageId) -> PackageVersion:
"""returns the latest installed version of the given package (eg '2021.1.2' for 'ifort')"""
@abc.abstractmethod
def get_package_activation_command(self, package_id: str, package_version: str) -> str:
def get_package_activation_command(self, package_id: PackageId, package_version: PackageVersion) -> str:
"""returns the bash command to activate the given package
eg for package_id=='ifort' and package_version=='2021.1.2' return 'module load compilers/ifort/2021.1.2'
@ -30,11 +34,29 @@ class Package():
def __init__(self, package_id: str, package_version: str, target_host: ITargetHost):
self.target_host = target_host
# resolve the package id, in case it contains keywords
resolved_package_id = ''
match = re.match(r'^<(?P<keyword>[a-z_]+)-(?P<arg1>[^>]+)>$', package_id)
if match:
keyword = match['keyword']
arg1 = match['arg1']
if keyword == 'default':
package_type = arg1 # eg 'libblas'
resolved_package_id = target_host.get_default_alternative(package_type)
else:
raise ValueError(f'unknown keyword {keyword}')
else:
if package_id.find('<') != -1 or package_id.find('>') != -1:
raise ValueError(f'unexpected syntax for package id {package_id}')
resolved_package_id = package_id
assert resolved_package_id != ''
if package_version == '<default>':
resolved_package_version = target_host.get_package_default_version(package_id)
resolved_package_version = target_host.get_package_default_version(resolved_package_id)
else:
resolved_package_version = package_version
self.package_id = package_id
self.package_id = resolved_package_id
self.package_version = resolved_package_version
def __repr__(self) -> str:

View File

@ -1,51 +1,187 @@
from typing import Set
from .core import ITargetHost
from typing import Set, Dict
from .core import ITargetHost, PackageId, PackageVersion
import subprocess
import re
import logging
from pathlib import Path
class GraffyWs2(ITargetHost):
DebianPackageVersion = str # a version string, as in debian package versions, eg 4:9.3.0-1ubuntu2
DebianPackageId = str # the identifier of a package in debian repositories (eg libopenblas0-pthread)
class DebianHost(ITargetHost):
def _get_debian_default(self, debian_generic_name: str) -> PackageVersion:
debian_default = None
completed_process = subprocess.run(f'update-alternatives --get-selections | grep "^{debian_generic_name} "', shell=True, check=False, capture_output=True)
if completed_process == 0:
raise ValueError(f'{debian_generic_name} is not a debian generic name listed by `update-alternatives --get-selections`')
else:
first_line = completed_process.stdout.decode('utf-8').split('\n')[0]
debian_default = first_line.split(' ')[-1]
return debian_default
@staticmethod
def debian_version_to_version(debian_version: DebianPackageVersion) -> PackageVersion:
"""
[https://serverfault.com/questions/604541/debian-packages-version-convention]
>The format is: [epoch:]upstream_version[-debian_revision]
"""
# expected to return '9.3.0' for '4:9.3.0-1ubuntu2'
match = re.match(r'^(?:(?P<epoch>[0-9]+):|)(?P<upstream_version>[0-9\.]+)(?P<debian_revision>.*)$', debian_version)
assert match, f'unexpected format for debian_version: "{debian_version}"'
version = PackageVersion(match['upstream_version'])
return version
@staticmethod
def which(executable: str) -> Path:
completed_process = subprocess.run(f'which {executable}', shell=True, check=True, capture_output=True)
first_line = completed_process.stdout.decode('utf-8').splitlines()[0]
return Path(first_line)
@staticmethod
def get_debian_package_providing_file(file_path: Path) -> DebianPackageId:
completed_process = subprocess.run(f'dpkg -S {file_path}', shell=True, check=True, capture_output=True)
first_line = completed_process.stdout.decode('utf-8').splitlines()[0]
match = re.match(r'^(?P<debian_package_id>[a-z0-9\-]+): (?P<file_path>[^$]+)$', first_line)
assert match, f'unexpected results for dpkg -S {file_path}'
return DebianPackageId(match['debian_package_id'])
@staticmethod
def package_id_to_debian_package_id(package_id: PackageId) -> DebianPackageId:
debian_package_id = ''
if package_id in ['gfortran']:
# in debian, the default gfortran compiler comes from the package `gfortran-9`, not `gfortran` (which is only a container package that depends on gfortran-9)
# so, the most reliable way to find that the package is gfortran-9 is to identify the package that provides the command gfortran
exec_path = DebianHost.which(package_id) # eg /usr/bin/gfortran
real_exec_path = exec_path.resolve()
debian_package_id = DebianHost.get_debian_package_providing_file(real_exec_path)
else:
gene_to_deb: Dict[PackageId, DebianPackageId] = {
'libopenblas-pthread': 'libopenblas0-pthread'
}
if package_id in gene_to_deb.keys():
debian_package_id = gene_to_deb[package_id]
else:
# we assume the ids are the same
debian_package_id = DebianPackageId(package_id)
return debian_package_id
PackageAttr = str # the name of a package attribute, as seen in dpkg -s <debian_package_id>
PackageValue = str # the name of a package value, as seen in dpkg -s <debian_package_id>
@staticmethod
def get_debian_package_status(debian_package_id: DebianPackageId) -> Dict[PackageAttr, PackageValue]:
package_status = {}
# 20241120-11:26:59 graffy@graffy-ws2:~/work/starbench/iprbench.git$ dpkg -s libopenblas0-pthread
# Package: libopenblas0-pthread
# Status: install ok installed
# Priority: optional
# Section: libs
# Installed-Size: 93686
# Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
# Architecture: amd64
# Multi-Arch: same
# Source: openblas
# Version: 0.3.8+ds-1ubuntu0.20.04.1
# Provides: libblas.so.3, liblapack.so.3
# Depends: libc6 (>= 2.29), libgfortran5 (>= 8)
# Breaks: libatlas3-base (<< 3.10.3-4~), libblas3 (<< 3.7.1-2~), liblapack3 (<< 3.7.1-2~), libopenblas-dev (<< 0.2.20+ds-3~)
# Description: Optimized BLAS (linear algebra) library (shared lib, pthread)
# OpenBLAS is an optimized BLAS library based on GotoBLAS2 1.13 BSD version.
# .
# Unlike Atlas, OpenBLAS provides a multiple architecture library.
# .
# All kernel will be included in the library and dynamically switched to the
# best architecture at run time (only on amd64, arm64, i386 and ppc64el).
# .
# For more information on how to rebuild locally OpenBLAS, see the section:
# "Building Optimized OpenBLAS Packages on your ARCH" in README.Debian
# .
# Configuration: USE_THREAD=1 USE_OPENMP=0 INTERFACE64=0
# Homepage: https://github.com/xianyi/OpenBLAS
# Original-Maintainer: Debian Science Team <debian-science-maintainers@lists.alioth.debian.org>
completed_process = subprocess.run(f'dpkg -s {debian_package_id}', check=True, shell=True, capture_output=True)
stdout = completed_process.stdout.decode('utf-8').splitlines()
for line in stdout:
match = re.match(r'^(?P<attr_name>[A-Za-z\-]+): (?P<attr_value>.*)$', line)
if match:
package_status[match['attr_name']] = match['attr_value']
return package_status
def is_installed_os_package(self, package_id: PackageId) -> bool:
debian_package_id = DebianHost.package_id_to_debian_package_id(package_id)
package_status = DebianHost.get_debian_package_status(debian_package_id)
return package_status['Status'] == 'install ok installed'
def get_installed_package_version(self, package_id: PackageId) -> PackageVersion:
debian_package_id = DebianHost.package_id_to_debian_package_id(package_id)
package_status = DebianHost.get_debian_package_status(debian_package_id)
assert package_status['Status'] == 'install ok installed'
debian_package_version = package_status['Version']
return DebianHost.debian_version_to_version(debian_package_version)
def get_default_alternative(self, package_type: str) -> str:
"""gets the installed variant for the given package type
eg returns 'openblas' for 'libblas'
"""
# https://wiki.debian.org/DebianScience/LinearAlgebraLibraries
debian_generic_name = {
'libblas': 'libblas.so-x86_64-linux-gnu'
}[package_type]
debian_default = self._get_debian_default(debian_generic_name)
package_id = {
'/usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so': 'libopenblas-pthread'
}[debian_default]
return package_id
class GraffyWs2(DebianHost):
host_name: str
available_packages: Set[str]
def __init__(self):
super().__init__()
self.host_name = 'graffy-ws2'
self.available_packages = {'gfortran'}
def get_package_default_version(self, package_id: str) -> str:
if package_id not in self.available_packages:
raise ValueError(f'ifort is not available on {self.host_name}')
elif package_id == 'gfortran':
completed_process = subprocess.run('gfortran --version', capture_output=True, check=False, shell=True)
if completed_process.returncode != 0:
raise ValueError(f'gfortran is not available on {self.host_name}')
else:
# GNU Fortran (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
# Copyright (C) 2019 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions. There is NO
# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
first_line = completed_process.stdout.decode('utf-8').split('\n')[0]
logging.debug('first line: %s', first_line)
gfortran_version = first_line.split(' ')[-1]
assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+', gfortran_version), f'unexpected format for gfortran version {gfortran_version}'
return gfortran_version
else:
assert False, f'unhandled package: {package_id}'
def get_package_default_version(self, package_id: PackageId) -> PackageVersion:
package_version = ''
if self.is_installed_os_package(package_id):
package_version = self.get_installed_package_version(package_id)
# if package_id not in self.available_packages:
# raise ValueError(f'{package_id} is not available on {self.host_name}')
# elif package_id == 'gfortran':
# completed_process = subprocess.run('gfortran --version', capture_output=True, check=False, shell=True)
# if completed_process.returncode != 0:
# raise ValueError(f'gfortran is not available on {self.host_name}')
# else:
# # GNU Fortran (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0
# # Copyright (C) 2019 Free Software Foundation, Inc.
# # This is free software; see the source for copying conditions. There is NO
# # warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# first_line = completed_process.stdout.decode('utf-8').split('\n')[0]
# logging.debug('first line: %s', first_line)
# gfortran_version = first_line.split(' ')[-1]
# assert re.match(r'[0-9]+\.[0-9]+\.[0-9]+', gfortran_version), f'unexpected format for gfortran version {gfortran_version}'
# return gfortran_version
# else:
# assert False, f'unhandled package: {package_id}'
return package_version
def get_package_activation_command(self, package_id: str, package_version: str) -> str:
if package_id not in self.available_packages:
raise ValueError(f'ifort is not available on {self.host_name}')
elif package_id == 'gfortran':
current_version = self.get_package_default_version(package_id)
if current_version != package_version:
raise ValueError(f'gfortran version {package_version} only gfortran version {current_version} is available on {self.host_name}')
return '' # no special instructions are required to activate the current gfortran version
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 {self.host_name}')
else:
assert False, f'unhandled package: {package_id}'
return '' # no special instructions are required to activate the current package version
class IprClusterNode(ITargetHost):
class IprClusterNode(DebianHost):
def get_latest_version_for_env_module(self, package_env_module: str):
# package_env_module: eg compilers/ifort

View File

@ -1 +1 @@
__version__ = '0.0.5'
__version__ = '0.0.6'

View File

@ -21,6 +21,7 @@ def test_resultsdb(resultsdb_params: ResultsDbParams, results_root_path: Path):
benchmark_id = 'mamul1'
benchmark_config = {
'fortran_compiler': 'gfortran:<default>',
'blas_library': '<default-libblas>:<default>',
'matrix_size': 1024,
'num_loops': 10,
'num_cores': 2