Skip to content

Commit 79bedf8

Browse files
author
Vasileios Karakasis
authored
Merge pull request #2168 from ekouts/bugfix/autodetect_processor_info
[bugfix] Fix incomplete processor info object when processor topology is auto-detected
2 parents 7d6b831 + cac4606 commit 79bedf8

File tree

5 files changed

+123
-111
lines changed

5 files changed

+123
-111
lines changed

reframe/core/systems.py

Lines changed: 60 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -13,30 +13,50 @@
1313
from reframe.core.modules import ModulesSystem
1414

1515

16-
class ProcessorType(jsonext.JSONSerializable):
16+
class _ReadOnlyInfo:
17+
__slots__ = ('_info',)
18+
_known_attrs = ()
19+
20+
def __init__(self, info):
21+
self._info = info
22+
23+
def __deepcopy__(self, memo):
24+
# This is a read-only object; simply return ourself
25+
return self
26+
27+
def __getattr__(self, name):
28+
if name in self._known_attrs:
29+
return self._info.get(name, None)
30+
else:
31+
raise AttributeError(
32+
f'{type(self).__qualname__!r} object has no attribute {name!r}'
33+
)
34+
35+
def __setattr__(self, name, value):
36+
if name in self._known_attrs:
37+
raise AttributeError(f'attribute {name!r} is not writeable')
38+
else:
39+
super().__setattr__(name, value)
40+
41+
42+
class ProcessorInfo(_ReadOnlyInfo, jsonext.JSONSerializable):
1743
'''A representation of a processor inside ReFrame.
1844
45+
You can access all the keys of the `processor configuration object
46+
<config_reference.html#processor-info>`__.
47+
1948
.. versionadded:: 3.5.0
2049
2150
.. warning::
22-
Users may not create :class:`ProcessorType` objects directly.
51+
Users may not create :class:`ProcessorInfo` objects directly.
2352
2453
'''
2554

26-
def __init__(self, processor_info):
27-
self._arch = None
28-
self._num_cpus = None
29-
self._num_cpus_per_core = None
30-
self._num_cpus_per_socket = None
31-
self._num_sockets = None
32-
self._topology = None
33-
self._info = processor_info
34-
35-
if not processor_info:
36-
return
37-
38-
for key, val in processor_info.items():
39-
setattr(self, f'_{key}', val)
55+
__slots__ = ()
56+
_known_attrs = (
57+
'arch', 'num_cpus', 'num_cpus_per_core',
58+
'num_cpus_per_socket', 'num_sockets', 'topology'
59+
)
4060

4161
@property
4262
def info(self):
@@ -46,62 +66,14 @@ def info(self):
4666
'''
4767
return self._info
4868

49-
@property
50-
def arch(self):
51-
'''The microarchitecture of the processor.
52-
53-
:type: :class:`str` or :class:`None`
54-
'''
55-
return self._arch
56-
57-
@property
58-
def num_cpus(self):
59-
'''Number of logical CPUs.
60-
61-
:type: integral or :class:`None`
62-
'''
63-
return self._num_cpus
64-
65-
@property
66-
def num_cpus_per_core(self):
67-
'''Number of logical CPUs per core.
68-
69-
:type: integral or :class:`None`
70-
'''
71-
return self._num_cpus_per_core
72-
73-
@property
74-
def num_cpus_per_socket(self):
75-
'''Number of logical CPUs per socket.
76-
77-
:type: integral or :class:`None`
78-
'''
79-
return self._num_cpus_per_socket
80-
81-
@property
82-
def num_sockets(self):
83-
'''Number of sockets.
84-
85-
:type: integral or :class:`None`
86-
'''
87-
return self._num_sockets
88-
89-
@property
90-
def topology(self):
91-
'''Processor topology.
92-
93-
:type: :class:`Dict[str, obj]` or :class:`None`
94-
'''
95-
return self._topology
96-
9769
@property
9870
def num_cores(self):
9971
'''Total number of cores.
10072
10173
:type: integral or :class:`None`
10274
'''
103-
if self._num_cpus and self._num_cpus_per_core:
104-
return self._num_cpus // self._num_cpus_per_core
75+
if self.num_cpus and self.num_cpus_per_core:
76+
return self.num_cpus // self.num_cpus_per_core
10577
else:
10678
return None
10779

@@ -111,8 +83,8 @@ def num_cores_per_socket(self):
11183
11284
:type: integral or :class:`None`
11385
'''
114-
if self.num_cores and self._num_sockets:
115-
return self.num_cores // self._num_sockets
86+
if self.num_cores and self.num_sockets:
87+
return self.num_cores // self.num_sockets
11688
else:
11789
return None
11890

@@ -122,8 +94,8 @@ def num_numa_nodes(self):
12294
12395
:type: integral or :class:`None`
12496
'''
125-
if self._topology and 'numa_nodes' in self._topology:
126-
return len(self._topology['numa_nodes'])
97+
if self.topology and 'numa_nodes' in self.topology:
98+
return len(self.topology['numa_nodes'])
12799
else:
128100
return None
129101

@@ -140,37 +112,21 @@ def num_cores_per_numa_node(self):
140112
return None
141113

142114

143-
class DeviceType(jsonext.JSONSerializable):
115+
class DeviceInfo(_ReadOnlyInfo, jsonext.JSONSerializable):
144116
'''A representation of a device inside ReFrame.
145117
118+
You can access all the keys of the `device configuration object
119+
<config_reference.html#device-info>`__.
120+
146121
.. versionadded:: 3.5.0
147122
148123
.. warning::
149-
Users may not create :class:`DeviceType` objects directly.
124+
Users may not create :class:`DeviceInfo` objects directly.
150125
151126
'''
152127

153-
def __init__(self, device_info):
154-
self._type = None
155-
self._arch = None
156-
self._num_devices = 1
157-
self._info = device_info
158-
159-
if not device_info:
160-
return
161-
162-
for key, val in device_info.items():
163-
setattr(self, f'_{key}', val)
164-
165-
@property
166-
def num_devices(self):
167-
'''Number of devices of this type.
168-
169-
It will return 1 if it wasn't set in the configuration.
170-
171-
:type: integral
172-
'''
173-
return self._num_devices
128+
__slots__ = ()
129+
_known_attrs = ('type', 'arch')
174130

175131
@property
176132
def info(self):
@@ -181,20 +137,22 @@ def info(self):
181137
return self._info
182138

183139
@property
184-
def arch(self):
185-
'''The architecture of the device.
140+
def num_devices(self):
141+
'''Number of devices of this type.
186142
187-
:type: :class:`str` or :class:`None`
143+
It will return 1 if it wasn't set in the configuration.
144+
145+
:type: integral
188146
'''
189-
return self._arch
147+
return self._info.get('num_devices', 1)
190148

191149
@property
192150
def device_type(self):
193151
'''The type of the device.
194152
195153
:type: :class:`str` or :class:`None`
196154
'''
197-
return self._type
155+
return self.type
198156

199157

200158
class SystemPartition(jsonext.JSONSerializable):
@@ -222,8 +180,8 @@ def __init__(self, parent, name, sched_type, launcher_type,
222180
self._max_jobs = max_jobs
223181
self._prepare_cmds = prepare_cmds
224182
self._resources = {r['name']: r['options'] for r in resources}
225-
self._processor = ProcessorType(processor)
226-
self._devices = [DeviceType(d) for d in devices]
183+
self._processor = ProcessorInfo(processor)
184+
self._devices = [DeviceInfo(d) for d in devices]
227185
self._extras = extras
228186

229187
@property
@@ -390,7 +348,7 @@ def processor(self):
390348
391349
.. versionadded:: 3.5.0
392350
393-
:type: :class:`reframe.core.systems.ProcessorType`
351+
:type: :class:`reframe.core.systems.ProcessorInfo`
394352
'''
395353
return self._processor
396354

@@ -400,7 +358,7 @@ def devices(self):
400358
401359
.. versionadded:: 3.5.0
402360
403-
:type: :class:`List[reframe.core.systems.DeviceType]`
361+
:type: :class:`List[reframe.core.systems.DeviceInfo]`
404362
'''
405363
return self._devices
406364

reframe/frontend/autodetect.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from reframe.core.exceptions import ConfigError
1616
from reframe.core.logging import getlogger
1717
from reframe.core.schedulers import Job
18+
from reframe.core.systems import DeviceInfo, ProcessorInfo
1819
from reframe.utility.cpuinfo import cpuinfo
1920

2021

@@ -183,8 +184,8 @@ def detect_topology():
183184
f'> found topology file {topo_file!r}; loading...'
184185
)
185186
try:
186-
part.processor._info = _load_info(
187-
topo_file, _subschema('#/defs/processor_info')
187+
part._processor = ProcessorInfo(
188+
_load_info(topo_file, _subschema('#/defs/processor_info'))
188189
)
189190
found_procinfo = True
190191
except json.decoder.JSONDecodeError as e:
@@ -197,9 +198,10 @@ def detect_topology():
197198
f'> found devices file {dev_file!r}; loading...'
198199
)
199200
try:
200-
part._devices = _load_info(
201+
devices_info = _load_info(
201202
dev_file, _subschema('#/defs/devices')
202203
)
204+
part._devices = [DeviceInfo(d) for d in devices_info]
203205
found_devinfo = True
204206
except json.decoder.JSONDecodeError as e:
205207
getlogger().debug(
@@ -220,13 +222,13 @@ def detect_topology():
220222

221223
# Unconditionally detect the system for fully local partitions
222224
with runtime.temp_environment(modules=modules, variables=vars):
223-
part.processor._info = cpuinfo()
225+
part._processor = ProcessorInfo(cpuinfo())
224226

225227
_save_info(topo_file, part.processor.info)
226228
elif detect_remote_systems:
227229
with runtime.temp_environment(modules=temp_modules,
228230
variables=temp_vars):
229-
part.processor._info = _remote_detect(part)
231+
part._processor = ProcessorInfo(_remote_detect(part))
230232

231233
if part.processor.info:
232234
_save_info(topo_file, part.processor.info)

reframe/utility/jsonext.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,20 @@
1212

1313

1414
class JSONSerializable:
15+
__slots__ = ()
16+
1517
def __rfm_json_encode__(self):
1618
ret = {
1719
'__rfm_class__': type(self).__qualname__,
1820
'__rfm_file__': inspect.getfile(type(self))
1921
}
20-
ret.update(self.__dict__)
22+
if hasattr(self, '__dict__'):
23+
ret.update(self.__dict__)
24+
25+
# Set the slots attribute
26+
for attr in self.__slots__:
27+
ret[attr] = getattr(self, attr)
28+
2129
encoded_ret = encode_dict(ret, recursive=True)
2230
return encoded_ret if encoded_ret else ret
2331

@@ -90,7 +98,12 @@ def _object_hook(json):
9098
mod = util.import_module_from_file(filename)
9199
cls = getattr(mod, typename)
92100
obj = cls.__new__(cls)
93-
obj.__dict__.update(json)
101+
if hasattr(obj, '__dict__'):
102+
obj.__dict__.update(json)
103+
else:
104+
for attr, value in json.items():
105+
setattr(obj, attr, value)
106+
94107
if hasattr(obj, '__rfm_json_decode__'):
95108
obj.__rfm_json_decode__(json)
96109

unittests/test_autodetect.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,29 @@ def test_autotect(exec_ctx):
5454
detect_topology()
5555
part = runtime().system.partitions[0]
5656
assert part.processor.info == cpuinfo()
57-
assert part.devices == [{'type': 'gpu', 'arch': 'a100', 'num_devices': 8}]
57+
if part.processor.info:
58+
assert part.processor.num_cpus == part.processor.info['num_cpus']
59+
60+
assert len(part.devices) == 1
61+
assert part.devices[0].info == {
62+
'type': 'gpu',
63+
'arch': 'a100',
64+
'num_devices': 8
65+
}
66+
assert part.devices[0].device_type == 'gpu'
67+
68+
# Test immutability of ProcessorInfo and DeviceInfo
69+
with pytest.raises(AttributeError):
70+
part.processor.num_cpus = 3
71+
72+
with pytest.raises(AttributeError):
73+
part.processor.foo = 10
74+
75+
with pytest.raises(AttributeError):
76+
part.devices[0].arch = 'foo'
77+
78+
with pytest.raises(AttributeError):
79+
part.devices[0].foo = 10
5880

5981

6082
def test_autotect_with_invalid_files(invalid_topo_exec_ctx):

0 commit comments

Comments
 (0)