# Copyright 2016-2021 Swiss National Supercomputing Centre (CSCS/ETH Zurich)
# ReFrame Project Developers. See the top-level LICENSE file for details.
#
# SPDX-License-Identifier: BSD-3-Clause
import json
import reframe.utility as utility
import reframe.utility.jsonext as jsonext
from reframe.core.backends import (getlauncher, getscheduler)
from reframe.core.logging import getlogger
from reframe.core.modules import ModulesSystem
from reframe.core.environments import (Environment, ProgEnvironment)
[docs]class ProcessorType(jsonext.JSONSerializable):
'''A representation of a processor inside ReFrame.
.. versionadded:: 3.5.0
.. warning::
Users may not create :class:`ProcessorType` objects directly.
'''
def __init__(self, processor_info):
self._arch = None
self._num_cpus = None
self._num_cpus_per_core = None
self._num_cpus_per_socket = None
self._num_sockets = None
self._topology = None
self._info = processor_info
if not processor_info:
return
for key, val in processor_info.items():
setattr(self, f'_{key}', val)
@property
def info(self):
'''All the available information from the configuration.
:type: :class:`dict`
'''
return self._info
@property
def arch(self):
'''The microarchitecture of the processor.
:type: :class:`str` or :class:`None`
'''
return self._arch
@property
def num_cpus(self):
'''Number of logical CPUs.
:type: integral or :class:`None`
'''
return self._num_cpus
@property
def num_cpus_per_core(self):
'''Number of logical CPUs per core.
:type: integral or :class:`None`
'''
return self._num_cpus_per_core
@property
def num_cpus_per_socket(self):
'''Number of logical CPUs per socket.
:type: integral or :class:`None`
'''
return self._num_cpus_per_socket
@property
def num_sockets(self):
'''Number of sockets.
:type: integral or :class:`None`
'''
return self._num_sockets
@property
def topology(self):
'''Processor topology.
:type: :class:`Dict[str, obj]` or :class:`None`
'''
return self._topology
@property
def num_cores(self):
'''Total number of cores.
:type: integral or :class:`None`
'''
if self._num_cpus and self._num_cpus_per_core:
return self._num_cpus // self._num_cpus_per_core
else:
return None
@property
def num_cores_per_socket(self):
'''Number of cores per socket.
:type: integral or :class:`None`
'''
if self.num_cores and self._num_sockets:
return self.num_cores // self._num_sockets
else:
return None
@property
def num_numa_nodes(self):
'''Number of NUMA nodes.
:type: integral or :class:`None`
'''
if self._topology and 'numa_nodes' in self._topology:
return len(self._topology['numa_nodes'])
else:
return None
@property
def num_cores_per_numa_node(self):
'''Number of cores per NUMA node.
:type: integral or :class:`None`
'''
if self.num_numa_nodes and self.num_cores:
return self.num_cores // self.num_numa_nodes
else:
return None
[docs]class DeviceType(jsonext.JSONSerializable):
'''A representation of a device inside ReFrame.
.. versionadded:: 3.5.0
.. warning::
Users may not create :class:`DeviceType` objects directly.
'''
def __init__(self, device_info):
self._type = None
self._arch = None
self._num_devices = 1
self._info = device_info
if not device_info:
return
for key, val in device_info.items():
setattr(self, f'_{key}', val)
@property
def num_devices(self):
'''Number of devices of this type.
It will return 1 if it wasn't set in the configuration.
:type: integral
'''
return self._num_devices
@property
def info(self):
'''All the available information from the configuration.
:type: :class:`dict`
'''
return self._info
@property
def arch(self):
'''The architecture of the device.
:type: :class:`str` or :class:`None`
'''
return self._arch
@property
def device_type(self):
'''The type of the device.
:type: :class:`str` or :class:`None`
'''
return self._type
[docs]class SystemPartition(jsonext.JSONSerializable):
'''A representation of a system partition inside ReFrame.
.. warning::
Users may not create :class:`SystemPartition` objects directly.
'''
def __init__(self, parent, name, sched_type, launcher_type,
descr, access, container_environs, resources,
local_env, environs, max_jobs, prepare_cmds,
processor, devices, extras):
getlogger().debug(f'Initializing system partition {name!r}')
self._parent_system = parent
self._name = name
self._sched_type = sched_type
self._scheduler = None
self._launcher_type = launcher_type
self._descr = descr
self._access = access
self._container_environs = container_environs
self._local_env = local_env
self._environs = environs
self._max_jobs = max_jobs
self._prepare_cmds = prepare_cmds
self._resources = {r['name']: r['options'] for r in resources}
self._processor = ProcessorType(processor)
self._devices = [DeviceType(d) for d in devices]
self._extras = extras
@property
def access(self):
'''The scheduler options for accessing this system partition.
:type: :class:`List[str]`
'''
return utility.SequenceView(self._access)
@property
def descr(self):
'''The description of this partition.
:type: :class:`str`
'''
return self._descr
@property
def environs(self):
'''The programming environments associated with this system partition.
:type: :class:`List[ProgEnvironment]`
'''
return utility.SequenceView(self._environs)
@property
def container_environs(self):
'''Environments associated with the different container platforms.
:type: :class:`Dict[str, Environment]`
'''
return utility.MappingView(self._container_environs)
@property
def fullname(self):
'''Return the fully-qualified name of this partition.
The fully-qualified name is of the form
``<parent-system-name>:<partition-name>``.
:type: :class:`str`
'''
return f'{self._parent_system}:{self._name}'
@property
def local_env(self):
'''The local environment associated with this partition.
:type: :class:`Environment`
'''
return self._local_env
@property
def max_jobs(self):
'''The maximum number of concurrent jobs allowed on this partition.
:type: integral
'''
return self._max_jobs
@property
def prepare_cmds(self):
'''Commands to be emitted before loading the modules.
:type: :class:`List[str]`
'''
return self._prepare_cmds
@property
def name(self):
'''The name of this partition.
:type: :class:`str`
'''
return self._name
@property
def resources(self):
'''The resources template strings associated with this partition.
This is a dictionary, where the key is the name of a resource and the
value is the scheduler options or directives associated with this
resource.
:type: :class:`Dict[str, List[str]]`
'''
return utility.MappingView(self._resources)
@property
def scheduler(self):
'''The backend scheduler of this partition.
:type: :class:`reframe.core.schedulers.JobScheduler`.
.. note::
.. versionchanged:: 2.8
Prior versions returned a string representing the scheduler and
job launcher combination.
.. versionchanged:: 3.2
The property now stores a :class:`JobScheduler` instance.
'''
if self._scheduler is None:
self._scheduler = self._sched_type()
return self._scheduler
@property
def launcher_type(self):
'''The type of the backend launcher of this partition.
.. versionadded:: 3.2
:type: a subclass of :class:`reframe.core.launchers.JobLauncher`.
'''
return self._launcher_type
@property
def launcher(self):
'''See :attr:`launcher_type`.
.. deprecated:: 3.2
Please use :attr:`launcher_type` instead.
'''
from reframe.core.warnings import user_deprecation_warning
user_deprecation_warning("the 'launcher' attribute is deprecated; "
"please use 'launcher_type' instead")
return self.launcher_type
def get_resource(self, name, **values):
'''Instantiate managed resource ``name`` with ``value``.
:meta private:
'''
ret = []
for r in self._resources.get(name, []):
try:
ret.append(r.format(**values))
except KeyError:
pass
return ret
[docs] def environment(self, name):
'''Return the partition environment named ``name``.'''
for e in self.environs:
if e.name == name:
return e
return None
@property
def processor(self):
'''Processor information for the current partition.
.. versionadded:: 3.5.0
:type: :class:`reframe.core.systems.ProcessorType`
'''
return self._processor
@property
def devices(self):
'''A list of devices in the current partition.
.. versionadded:: 3.5.0
:type: :class:`List[reframe.core.systems.DeviceType]`
'''
return self._devices
@property
def extras(self):
'''User defined attributes of the system.
By default, it is an empty dictionary.
.. versionadded:: 3.5.0
:type: :class:`object`
'''
return self._extras
def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented
return (self._name == other.name and
self._sched_type == other._sched_type and
self._launcher_type == other._launcher_type and
self._access == other._access and
self._environs == other._environs and
self._resources == other._resources and
self._local_env == other._local_env)
def __hash__(self):
return hash(self.fullname)
[docs] def json(self):
'''Return a JSON object representing this system partition.'''
return {
'name': self._name,
'descr': self._descr,
'scheduler': self._sched_type.registered_name,
'launcher': self._launcher_type.registered_name,
'access': self._access,
'container_platforms': [
{
'type': ctype,
'modules': [m for m in cpenv.modules],
'variables': [[n, v] for n, v in cpenv.variables.items()]
}
for ctype, cpenv in self._container_environs.items()
],
'modules': [m for m in self._local_env.modules],
'variables': [[n, v]
for n, v in self._local_env.variables.items()],
'environs': [e.name for e in self._environs],
'max_jobs': self._max_jobs,
'resources': [
{
'name': name,
'options': options
}
for name, options in self._resources.items()
]
}
def __str__(self):
return json.dumps(self.json(), indent=2)
[docs]class System(jsonext.JSONSerializable):
'''A representation of a system inside ReFrame.
.. warning::
Users may not create :class:`System` objects directly.
'''
def __init__(self, name, descr, hostnames, modules_system,
preload_env, prefix, outputdir,
resourcesdir, stagedir, partitions):
getlogger().debug(f'Initializing system {name!r}')
self._name = name
self._descr = descr
self._hostnames = hostnames
self._modules_system = ModulesSystem.create(modules_system)
self._preload_env = preload_env
self._prefix = prefix
self._outputdir = outputdir
self._resourcesdir = resourcesdir
self._stagedir = stagedir
self._partitions = partitions
@classmethod
def create(cls, site_config):
# Create the whole system hierarchy from bottom up
sysname = site_config.get('systems/0/name')
partitions = []
config_save = site_config.subconfig_system
for p in site_config.get('systems/0/partitions'):
site_config.select_subconfig(f'{sysname}:{p["name"]}')
partid = f"systems/0/partitions/@{p['name']}"
part_name = site_config.get(f'{partid}/name')
part_sched = getscheduler(site_config.get(f'{partid}/scheduler'))
part_launcher = getlauncher(site_config.get(f'{partid}/launcher'))
part_container_environs = {}
for i, p in enumerate(
site_config.get(f'{partid}/container_platforms')
):
ctype = p['type']
part_container_environs[ctype] = Environment(
name=f'__rfm_env_{ctype}',
modules=site_config.get(
f'{partid}/container_platforms/{i}/modules'
),
variables=site_config.get(
f'{partid}/container_platforms/{i}/variables'
)
)
part_environs = [
ProgEnvironment(
name=e,
modules=site_config.get(f'environments/@{e}/modules'),
variables=site_config.get(f'environments/@{e}/variables'),
cc=site_config.get(f'environments/@{e}/cc'),
cxx=site_config.get(f'environments/@{e}/cxx'),
ftn=site_config.get(f'environments/@{e}/ftn'),
cppflags=site_config.get(f'environments/@{e}/cppflags'),
cflags=site_config.get(f'environments/@{e}/cflags'),
cxxflags=site_config.get(f'environments/@{e}/cxxflags'),
fflags=site_config.get(f'environments/@{e}/fflags'),
ldflags=site_config.get(f'environments/@{e}/ldflags')
) for e in site_config.get(f'{partid}/environs')
]
partitions.append(
SystemPartition(
parent=site_config.get('systems/0/name'),
name=part_name,
sched_type=part_sched,
launcher_type=part_launcher,
descr=site_config.get(f'{partid}/descr'),
access=site_config.get(f'{partid}/access'),
resources=site_config.get(f'{partid}/resources'),
environs=part_environs,
container_environs=part_container_environs,
local_env=Environment(
name=f'__rfm_env_{part_name}',
modules=site_config.get(f'{partid}/modules'),
variables=site_config.get(f'{partid}/variables')
),
max_jobs=site_config.get(f'{partid}/max_jobs'),
prepare_cmds=site_config.get(f'{partid}/prepare_cmds'),
processor=site_config.get(f'{partid}/processor'),
devices=site_config.get(f'{partid}/devices'),
extras=site_config.get(f'{partid}/extras')
)
)
# Restore configuration, but ignore unresolved sections or
# configuration parameters at the system level; if we came up to this
# point, then all is good at the partition level, which is enough.
site_config.select_subconfig(config_save, ignore_resolve_errors=True)
return System(
name=sysname,
descr=site_config.get('systems/0/descr'),
hostnames=site_config.get('systems/0/hostnames'),
modules_system=site_config.get('systems/0/modules_system'),
preload_env=Environment(
name=f'__rfm_env_{sysname}',
modules=site_config.get('systems/0/modules'),
variables=site_config.get('systems/0/variables')
),
prefix=site_config.get('systems/0/prefix'),
outputdir=site_config.get('systems/0/outputdir'),
resourcesdir=site_config.get('systems/0/resourcesdir'),
stagedir=site_config.get('systems/0/stagedir'),
partitions=partitions
)
@property
def name(self):
'''The name of this system.
:type: :class:`str`
'''
return self._name
@property
def descr(self):
'''The description of this system.
:type: :class:`str`
'''
return self._descr
@property
def hostnames(self):
'''The hostname patterns associated with this system.
:type: :class:`List[str]`
'''
return self._hostnames
@property
def modules_system(self):
'''The modules system name associated with this system.
:type: :class:`reframe.core.modules.ModulesSystem`
'''
return self._modules_system
@property
def preload_environ(self):
'''The environment to load whenever ReFrame runs on this system.
.. versionadded:: 2.19
:type: :class:`reframe.core.environments.Environment`
'''
return self._preload_env
@property
def prefix(self):
'''The ReFrame prefix associated with this system.
:type: :class:`str`
'''
return self._prefix
@property
def stagedir(self):
'''The ReFrame stage directory prefix associated with this system.
:type: :class:`str`
'''
return self._stagedir
@property
def outputdir(self):
'''The ReFrame output directory prefix associated with this system.
:type: :class:`str`
'''
return self._outputdir
@property
def resourcesdir(self):
'''Global resources directory for this system.
This directory may be used for storing large files related to
regression tests. The value of this directory is controlled by the
`resourcesdir <config_reference.html#.systems[].resourcesdir>`__
configuration parameter.
:type: :class:`str`
'''
return self._resourcesdir
@property
def partitions(self):
'''The system partitions associated with this system.
:type: :class:`List[SystemPartition]`
'''
return utility.SequenceView(self._partitions)
def __eq__(self, other):
if not isinstance(other, type(self)):
return NotImplemented
return (self._name == other._name and
self._hostnames == other._hostnames and
self._partitions == other._partitions)
[docs] def json(self):
'''Return a JSON object representing this system.'''
return {
'name': self._name,
'descr': self._descr,
'hostnames': self._hostnames,
'modules_system': self._modules_system.name,
'modules': [m for m in self._preload_env.modules],
'variables': [
[name, value]
for name, value in self._preload_env.variables.items()
],
'prefix': self._prefix,
'outputdir': self._outputdir,
'stagedir': self._stagedir,
'resourcesdir': self._resourcesdir,
'partitions': [p.json() for p in self._partitions]
}
def __str__(self):
return json.dumps(self.json(), indent=2)
def __repr__(self):
return (
f'{type(self).__name__}( '
f'name={self._name!r}, descr={self._descr!r}, '
f'hostnames={self._hostnames!r}, '
f'modules_system={self.modules_system.name!r}, '
f'preload_env={self._preload_env!r}, prefix={self._prefix!r}, '
f'outputdir={self._outputdir!r}, '
f'resourcesdir={self._resourcesdir!r}, '
f'stagedir={self._stagedir!r}, partitions={self._partitions!r})'
)