Skip to content

Commit 89b3acb

Browse files
committed
arch: Refactor of get_cpu_info
1 parent 870a160 commit 89b3acb

1 file changed

Lines changed: 165 additions & 2 deletions

File tree

devito/arch/archinfo.py

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import os
66
import re
77
import sys
8+
from collections import defaultdict
89
from contextlib import suppress
910
from functools import cached_property
1011
from pathlib import Path
@@ -17,6 +18,7 @@
1718

1819
from devito.logger import warning
1920
from devito.tools import all_equal, as_tuple, memoized_func
21+
from devito.warnings import warn
2022

2123
__all__ = [ # noqa: RUF022
2224
'platform_registry', 'get_cpu_info', 'get_gpu_info', 'get_visible_devices',
@@ -25,6 +27,7 @@
2527
'Platform', 'Cpu64', 'Intel64', 'IntelSkylake', 'Amd', 'Arm', 'Power',
2628
'Device',
2729
'NvidiaDevice', 'AmdDevice', 'IntelDevice',
30+
'old_get_cpu_info',
2831
# Brand-agnostic
2932
'ANYCPU', 'ANYGPU',
3033
# Intel CPUs
@@ -48,7 +51,7 @@
4851

4952

5053
@memoized_func
51-
def get_cpu_info():
54+
def old_get_cpu_info():
5255
"""Attempt CPU info autodetection."""
5356

5457
# Obtain textual cpu info
@@ -168,6 +171,166 @@ def get_cpu_brand():
168171
cpu_info['physical'] = physical
169172
return cpu_info
170173

174+
def text2dict(text):
175+
return {
176+
line.split(':', 1)[0].strip(): line.split(':', 1)[1].strip()
177+
for line in text.splitlines()
178+
}
179+
180+
def cast2numeric(adict):
181+
# Try and convert numeric values
182+
for k, v in adict.items():
183+
if not v:
184+
adict[k] = None
185+
continue
186+
for cast in [int, float]:
187+
with suppress(ValueError):
188+
adict[k] = cast(v)
189+
break
190+
return adict
191+
192+
def set2range(aset):
193+
if len(aset) == 1:
194+
r = aset.pop()
195+
else:
196+
r = aset
197+
return r
198+
199+
@memoized_func
200+
def proc_cpuinfo():
201+
""" Creates a `dict` containing the information in `/proc/cpuinfo`
202+
"""
203+
# Obtain CPU info as text
204+
try:
205+
with open('/proc/cpuinfo') as f:
206+
lines = f.read()
207+
command = 'cat /proc/cpuinfo'
208+
except FileNotFoundError:
209+
lines = ''
210+
command = '`/proc/cpuinfo` not found'
211+
warn(f'File {command}')
212+
213+
hwthreads = lines.strip().split('\n\n')
214+
logical = len(hwthreads)
215+
216+
info = []
217+
for hwt in hwthreads:
218+
info.append(cast2numeric(text2dict(hwt)))
219+
220+
# Nightmare
221+
variations = defaultdict(set)
222+
for hwt in info:
223+
for k, v in hwt.items():
224+
variations[k].add(v)
225+
226+
final = {}
227+
for k, v in variations.items():
228+
final[k] = set2range(v)
229+
230+
# `cpu MHz` is a "live" value so ignore it
231+
with suppress(KeyError):
232+
del final['cpu MHz']
233+
234+
final['command'] = command
235+
236+
return final
237+
238+
@memoized_func
239+
def lscpu():
240+
""" Creates a `dict` containing the information from `lscpu`
241+
"""
242+
# Use `lscpu -J` if available (not available prior to v2.30, cerca 2017)
243+
try:
244+
ret = run(['lscpu', '-J'], capture_output=True, text=True)
245+
info = {
246+
x['field'].rstrip(':'): x['data']
247+
for x in json.loads(ret.stdout)['lscpu']
248+
}
249+
info['command'] = 'lscpu -J'
250+
except Exception as e:
251+
info = None
252+
253+
# Use `lscpu` if `-J` argument is not available
254+
if info is None:
255+
try:
256+
ret = run(['lscpu'], capture_output=True, text=True)
257+
info = text2dict(ret.stdout)
258+
info['command'] = 'lscpu'
259+
except Exception as e:
260+
msg = '`lscpu` not found'
261+
warn(f'Command {msg}')
262+
info = {'command': msg}
263+
264+
return cast2numeric(info)
265+
266+
@memoized_func
267+
def get_cpu_info():
268+
"""Collect CPU information
269+
270+
This function enriches the output of the `get_cpu_info` function provided by
271+
`cpuinfo` by adding the number of physical and logical cores.
272+
273+
There are numerous workarounds for different platforms where issues have
274+
been encountered previously.
275+
276+
Returns a dictionary with at least the following keys populated:
277+
- brand: str, fallback ''
278+
- flags: list[str], fallback []
279+
- logical: int, fallback 1
280+
- physical: int, fallback 1
281+
"""
282+
try:
283+
cpu_info = cpuinfo.get_cpu_info()
284+
except Exception as e:
285+
warn(f'Calling `cpuinfo.get_cpu_info()` faised an exception\n {e !s}')
286+
cpu_info = {}
287+
288+
cpu_info['logical'] = psutil.cpu_count(logical=True)
289+
cpu_info['physical'] = psutil.cpu_count(logical=False)
290+
291+
# At this point on a well behaved system we have everything that we need.
292+
# If only life were that simple! We now check what we obtained
293+
294+
procinfo = proc_cpuinfo()
295+
lscpuinfo = lscpu()
296+
# brand:
297+
if cpu_info.get('brand') is None:
298+
for source, key in (
299+
(procinfo, 'model name'),
300+
(procinfo, 'cpu'),
301+
(cpuinfo, 'arch'),
302+
(cpuinfo, 'brand_raw'),
303+
):
304+
cpu_info['brand'] = source.get(key)
305+
if cpu_info.get('brand'):
306+
break
307+
else:
308+
cpu_info['brand'] = ''
309+
310+
# flags:
311+
if cpu_info.get('flags') is None:
312+
for source, key in (
313+
(procinfo, 'features'),
314+
(procinfo, 'flags'),
315+
):
316+
cpu_info['flags'] = source.get(key)
317+
if cpu_info.get('flags'):
318+
break
319+
else:
320+
cpu_info['flags'] = []
321+
322+
# logical
323+
if cpu_info.get('logical') is None:
324+
cpu_info['logical'] = lscpuinfo.get('CPU(s)', 1)
325+
326+
# physical
327+
if cpu_info.get('physical') is None:
328+
cpu_info['physical'] = lscpuinfo.get('Core(s) per socket', 1) * lscpuinfo.get('Socket(s)', 1)
329+
330+
cpu_info[procinfo.pop('command')] = procinfo
331+
cpu_info[lscpuinfo.pop('command')] = lscpuinfo
332+
333+
return cpu_info
171334

172335
@memoized_func
173336
def get_gpu_info():
@@ -695,7 +858,7 @@ def check_cuda_runtime():
695858

696859

697860
@memoized_func
698-
def lscpu():
861+
def old_lscpu():
699862
try:
700863
p1 = Popen(['lscpu'], stdout=PIPE, stderr=PIPE)
701864
except OSError:

0 commit comments

Comments
 (0)