From 89b3acb8189bd6fc97ca1190755abd3b63f70717 Mon Sep 17 00:00:00 2001 From: Jack Betteridge Date: Wed, 11 Feb 2026 17:11:42 +0000 Subject: [PATCH 1/6] arch: Refactor of get_cpu_info --- devito/arch/archinfo.py | 167 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/devito/arch/archinfo.py b/devito/arch/archinfo.py index ea4025a3ab..85a2202adb 100644 --- a/devito/arch/archinfo.py +++ b/devito/arch/archinfo.py @@ -5,6 +5,7 @@ import os import re import sys +from collections import defaultdict from contextlib import suppress from functools import cached_property from pathlib import Path @@ -17,6 +18,7 @@ from devito.logger import warning from devito.tools import all_equal, as_tuple, memoized_func +from devito.warnings import warn __all__ = [ # noqa: RUF022 'platform_registry', 'get_cpu_info', 'get_gpu_info', 'get_visible_devices', @@ -25,6 +27,7 @@ 'Platform', 'Cpu64', 'Intel64', 'IntelSkylake', 'Amd', 'Arm', 'Power', 'Device', 'NvidiaDevice', 'AmdDevice', 'IntelDevice', + 'old_get_cpu_info', # Brand-agnostic 'ANYCPU', 'ANYGPU', # Intel CPUs @@ -48,7 +51,7 @@ @memoized_func -def get_cpu_info(): +def old_get_cpu_info(): """Attempt CPU info autodetection.""" # Obtain textual cpu info @@ -168,6 +171,166 @@ def get_cpu_brand(): cpu_info['physical'] = physical return cpu_info +def text2dict(text): + return { + line.split(':', 1)[0].strip(): line.split(':', 1)[1].strip() + for line in text.splitlines() + } + +def cast2numeric(adict): + # Try and convert numeric values + for k, v in adict.items(): + if not v: + adict[k] = None + continue + for cast in [int, float]: + with suppress(ValueError): + adict[k] = cast(v) + break + return adict + +def set2range(aset): + if len(aset) == 1: + r = aset.pop() + else: + r = aset + return r + +@memoized_func +def proc_cpuinfo(): + """ Creates a `dict` containing the information in `/proc/cpuinfo` + """ + # Obtain CPU info as text + try: + with open('/proc/cpuinfo') as f: + lines = f.read() + command = 'cat /proc/cpuinfo' + except FileNotFoundError: + lines = '' + command = '`/proc/cpuinfo` not found' + warn(f'File {command}') + + hwthreads = lines.strip().split('\n\n') + logical = len(hwthreads) + + info = [] + for hwt in hwthreads: + info.append(cast2numeric(text2dict(hwt))) + + # Nightmare + variations = defaultdict(set) + for hwt in info: + for k, v in hwt.items(): + variations[k].add(v) + + final = {} + for k, v in variations.items(): + final[k] = set2range(v) + + # `cpu MHz` is a "live" value so ignore it + with suppress(KeyError): + del final['cpu MHz'] + + final['command'] = command + + return final + +@memoized_func +def lscpu(): + """ Creates a `dict` containing the information from `lscpu` + """ + # Use `lscpu -J` if available (not available prior to v2.30, cerca 2017) + try: + ret = run(['lscpu', '-J'], capture_output=True, text=True) + info = { + x['field'].rstrip(':'): x['data'] + for x in json.loads(ret.stdout)['lscpu'] + } + info['command'] = 'lscpu -J' + except Exception as e: + info = None + + # Use `lscpu` if `-J` argument is not available + if info is None: + try: + ret = run(['lscpu'], capture_output=True, text=True) + info = text2dict(ret.stdout) + info['command'] = 'lscpu' + except Exception as e: + msg = '`lscpu` not found' + warn(f'Command {msg}') + info = {'command': msg} + + return cast2numeric(info) + +@memoized_func +def get_cpu_info(): + """Collect CPU information + + This function enriches the output of the `get_cpu_info` function provided by + `cpuinfo` by adding the number of physical and logical cores. + + There are numerous workarounds for different platforms where issues have + been encountered previously. + + Returns a dictionary with at least the following keys populated: + - brand: str, fallback '' + - flags: list[str], fallback [] + - logical: int, fallback 1 + - physical: int, fallback 1 + """ + try: + cpu_info = cpuinfo.get_cpu_info() + except Exception as e: + warn(f'Calling `cpuinfo.get_cpu_info()` faised an exception\n {e !s}') + cpu_info = {} + + cpu_info['logical'] = psutil.cpu_count(logical=True) + cpu_info['physical'] = psutil.cpu_count(logical=False) + + # At this point on a well behaved system we have everything that we need. + # If only life were that simple! We now check what we obtained + + procinfo = proc_cpuinfo() + lscpuinfo = lscpu() + # brand: + if cpu_info.get('brand') is None: + for source, key in ( + (procinfo, 'model name'), + (procinfo, 'cpu'), + (cpuinfo, 'arch'), + (cpuinfo, 'brand_raw'), + ): + cpu_info['brand'] = source.get(key) + if cpu_info.get('brand'): + break + else: + cpu_info['brand'] = '' + + # flags: + if cpu_info.get('flags') is None: + for source, key in ( + (procinfo, 'features'), + (procinfo, 'flags'), + ): + cpu_info['flags'] = source.get(key) + if cpu_info.get('flags'): + break + else: + cpu_info['flags'] = [] + + # logical + if cpu_info.get('logical') is None: + cpu_info['logical'] = lscpuinfo.get('CPU(s)', 1) + + # physical + if cpu_info.get('physical') is None: + cpu_info['physical'] = lscpuinfo.get('Core(s) per socket', 1) * lscpuinfo.get('Socket(s)', 1) + + cpu_info[procinfo.pop('command')] = procinfo + cpu_info[lscpuinfo.pop('command')] = lscpuinfo + + return cpu_info @memoized_func def get_gpu_info(): @@ -695,7 +858,7 @@ def check_cuda_runtime(): @memoized_func -def lscpu(): +def old_lscpu(): try: p1 = Popen(['lscpu'], stdout=PIPE, stderr=PIPE) except OSError: From d99468f1b12f6edf17038c182b7d4dc640adf233 Mon Sep 17 00:00:00 2001 From: Jack Betteridge Date: Wed, 11 Feb 2026 22:38:11 +0000 Subject: [PATCH 2/6] arch: Refactor get_gpu_info --- devito/arch/archinfo.py | 172 +++++++++---------- devito/arch/commands.py | 365 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 443 insertions(+), 94 deletions(-) create mode 100644 devito/arch/commands.py diff --git a/devito/arch/archinfo.py b/devito/arch/archinfo.py index 85a2202adb..83be120a1c 100644 --- a/devito/arch/archinfo.py +++ b/devito/arch/archinfo.py @@ -5,7 +5,6 @@ import os import re import sys -from collections import defaultdict from contextlib import suppress from functools import cached_property from pathlib import Path @@ -20,6 +19,9 @@ from devito.tools import all_equal, as_tuple, memoized_func from devito.warnings import warn +from .commands import lscpu, lshw, lspci, nvidia_smi, proc_cpuinfo, rocm_smi, sycl_ls + + __all__ = [ # noqa: RUF022 'platform_registry', 'get_cpu_info', 'get_gpu_info', 'get_visible_devices', 'get_nvidia_cc', 'get_cuda_path', 'get_cuda_version', 'get_hip_path', @@ -27,7 +29,7 @@ 'Platform', 'Cpu64', 'Intel64', 'IntelSkylake', 'Amd', 'Arm', 'Power', 'Device', 'NvidiaDevice', 'AmdDevice', 'IntelDevice', - 'old_get_cpu_info', + 'old_get_cpu_info', 'old_get_gpu_info', # Brand-agnostic 'ANYCPU', 'ANYGPU', # Intel CPUs @@ -171,97 +173,6 @@ def get_cpu_brand(): cpu_info['physical'] = physical return cpu_info -def text2dict(text): - return { - line.split(':', 1)[0].strip(): line.split(':', 1)[1].strip() - for line in text.splitlines() - } - -def cast2numeric(adict): - # Try and convert numeric values - for k, v in adict.items(): - if not v: - adict[k] = None - continue - for cast in [int, float]: - with suppress(ValueError): - adict[k] = cast(v) - break - return adict - -def set2range(aset): - if len(aset) == 1: - r = aset.pop() - else: - r = aset - return r - -@memoized_func -def proc_cpuinfo(): - """ Creates a `dict` containing the information in `/proc/cpuinfo` - """ - # Obtain CPU info as text - try: - with open('/proc/cpuinfo') as f: - lines = f.read() - command = 'cat /proc/cpuinfo' - except FileNotFoundError: - lines = '' - command = '`/proc/cpuinfo` not found' - warn(f'File {command}') - - hwthreads = lines.strip().split('\n\n') - logical = len(hwthreads) - - info = [] - for hwt in hwthreads: - info.append(cast2numeric(text2dict(hwt))) - - # Nightmare - variations = defaultdict(set) - for hwt in info: - for k, v in hwt.items(): - variations[k].add(v) - - final = {} - for k, v in variations.items(): - final[k] = set2range(v) - - # `cpu MHz` is a "live" value so ignore it - with suppress(KeyError): - del final['cpu MHz'] - - final['command'] = command - - return final - -@memoized_func -def lscpu(): - """ Creates a `dict` containing the information from `lscpu` - """ - # Use `lscpu -J` if available (not available prior to v2.30, cerca 2017) - try: - ret = run(['lscpu', '-J'], capture_output=True, text=True) - info = { - x['field'].rstrip(':'): x['data'] - for x in json.loads(ret.stdout)['lscpu'] - } - info['command'] = 'lscpu -J' - except Exception as e: - info = None - - # Use `lscpu` if `-J` argument is not available - if info is None: - try: - ret = run(['lscpu'], capture_output=True, text=True) - info = text2dict(ret.stdout) - info['command'] = 'lscpu' - except Exception as e: - msg = '`lscpu` not found' - warn(f'Command {msg}') - info = {'command': msg} - - return cast2numeric(info) @memoized_func def get_cpu_info(): @@ -330,10 +241,19 @@ def get_cpu_info(): cpu_info[procinfo.pop('command')] = procinfo cpu_info[lscpuinfo.pop('command')] = lscpuinfo + # This might actually be a bad idea... + # ~ # Like gpu_info attach callbacks for memory status + # ~ # NOTE: from the psutil docs: + # ~ # - The sum of used and available does not necessarily equal total. + # ~ # - free doesn’t reflect the actual memory available (use available instead) + # ~ cpu_info['mem.free'] = lambda: psutil.virtual_memory().available + # ~ cpu_info['mem.used'] = lambda: psutil.virtual_memory().used + # ~ cpu_info['mem.total'] = psutil.virtual_memory().total return cpu_info + @memoized_func -def get_gpu_info(): +def old_get_gpu_info(): """Attempt GPU info autodetection.""" # Filter out virtual GPUs from a list of GPU dictionaries @@ -657,6 +577,70 @@ def parse_product_arch(): return None +def homogenise_gpus(gpu_infos): + """Parse textual gpu info into a dict + + Run homogeneity checks on a list of GPUs, return GPU with count if + homogeneous, otherwise None. + """ + if gpu_infos == []: + homogenous = {} + else: + # Check must ignore physical IDs as they may differ + for gpu_info in gpu_infos: + gpu_info.pop('physicalid', None) + + if all_equal(gpu_infos): + gpu_infos[0]['ncards'] = len(gpu_infos) + homogenous = gpu_infos[0] + else: + warning('Different models of graphics cards detected') + homogenous = {'ncards': len(gpu_infos)} + + return homogenous + + +@memoized_func +def get_gpu_info(): + """Attempt GPU info autodetection. + + Probe for GPU information in the following order: + 1. `nvidia-smi`, nvidia cards only + 2. `rocm-smi`, AMD cards only + 3. `sycl-ls`, Intel cards only + 4. `lshw` + 5. `lspci`, more readable but less detailed than `lshw` + + nvidia and AMD cards allow polling the used and available memory + + Returns a dictionary which is empty or has at least the following keys populated: + - physicalid: str + - product: str + - vendor: str + - architecture: str, fallback 'unspecified' + """ + + for call in [nvidia_smi, rocm_smi, sycl_ls, lshw, lspci]: + try: + gpu_info = homogenise_gpus(call()) + break + except (OSError): + gpu_info = {} + + # Attach callbacks to retrieve instantaneous memory info + # Unsure whether this is used or even used to work! + if gpu_info['vendor'] == 'NVIDIA': + pass + + if gpu_info['vendor'] == 'AMD': + pass + + if gpu_info['vendor'] == 'INTEL': + pass + + return gpu_info + + def get_visible_devices(): device_vars = ( 'CUDA_VISIBLE_DEVICES', diff --git a/devito/arch/commands.py b/devito/arch/commands.py new file mode 100644 index 0000000000..8b2bf6442f --- /dev/null +++ b/devito/arch/commands.py @@ -0,0 +1,365 @@ +import json +import re +from collections import defaultdict +from contextlib import suppress +from dataclasses import dataclass +from subprocess import run + +from devito.warnings import warn + + +__all__ = [ + 'lscpu', + 'lshw', + 'lspci', + 'nvidia_smi', + 'nvidia_smi_memory', + 'proc_cpuinfo', + 'rocm_smi', + 'rocm_smi_memory', + 'sycl_ls', + 'xpu_smi_memory' +] + + +# Unified dataclass for describing RAM, SWAP and GPU RAM (maybe disk too?) +@dataclass +class Memory: + total: int + free: int + used: int + + +def text2dict(text): + return { + line.split(':', 1)[0].strip(): line.split(':', 1)[1].strip() + for line in text.splitlines() + } + + +def cast2numeric(adict): + # Try and convert numeric values + for k, v in adict.items(): + if not v: + adict[k] = None + continue + for cast in [int, float]: + with suppress(ValueError): + adict[k] = cast(v) + break + return adict + + +def set2range(aset): + if len(aset) == 1: + r = aset.pop() + else: + r = aset + return r + + +def proc_cpuinfo(): + """ Creates a `dict` containing the information in `/proc/cpuinfo` + """ + # Obtain CPU info as text + try: + with open('/proc/cpuinfo') as f: + lines = f.read() + command = 'cat /proc/cpuinfo' + except FileNotFoundError: + lines = '' + command = '`/proc/cpuinfo` not found' + warn(f'File {command}') + + hwthreads = lines.strip().split('\n\n') + logical = len(hwthreads) + + info = [] + for hwt in hwthreads: + info.append(cast2numeric(text2dict(hwt))) + + # Nightmare + variations = defaultdict(set) + for hwt in info: + for k, v in hwt.items(): + variations[k].add(v) + + final = {} + for k, v in variations.items(): + final[k] = set2range(v) + + # `cpu MHz` is a "live" value so ignore it + with suppress(KeyError): + del final['cpu MHz'] + + final['command'] = command + + return final + + +def lscpu(): + """ Creates a `dict` containing the information from `lscpu` + """ + # Use `lscpu -J` if available (not available prior to v2.30, cerca 2017) + try: + ret = run(['lscpu', '-J'], capture_output=True, text=True) + info = { + x['field'].rstrip(':'): x['data'] + for x in json.loads(ret.stdout)['lscpu'] + } + info['command'] = 'lscpu -J' + except Exception: + info = None + + # Use `lscpu` if `-J` argument is not available + if info is None: + try: + ret = run(['lscpu'], capture_output=True, text=True) + info = text2dict(ret.stdout) + info['command'] = 'lscpu' + except Exception: + msg = '`lscpu` not found' + warn(f'Command {msg}') + info = {'command': msg} + + return cast2numeric(info) + + +def nvidia_smi(): + """ Creates a `list[dict]` containing the information from `nvidia-smi` + """ + gpu_infos = [] + + ret = run(['nvidia-smi', '-L'], capture_output=True, text=True) + lines = ret.stdout.splitlines() + + for line in lines: + gpu_info = {} + if 'GPU' in line: + gpu_info = {} + match = re.match(r'GPU *[0-9]*\: ([\w]*) (.*) \(', line) + if match: + if match.group(1) == 'Graphics': + gpu_info['architecture'] = 'unspecified' + else: + gpu_info['architecture'] = match.group(1) + if match.group(2) == 'Device': + gpu_info['product'] = 'unspecified' + else: + gpu_info['product'] = match.group(2) + gpu_info['vendor'] = 'NVIDIA' + gpu_infos.append(gpu_info) + + return gpu_infos + + +def nvidia_smi_memory(): + ret = run( + [ + 'nvidia-smi', + '--query-gpu=--query-gpu=memory.total,memory.free,memory.used', + '--format=csv' + ], + capture_output=True, + text=True + ) + lines = ret.stdout.splitlines()[1:-1] + mem = [] + for line in lines: + with suppress(Exception): + # This should not fail, unless `nvidia-smi` changes + # the output format (though we still have tests in place that + # will catch this) + vals = [] + for value in line.split(', ') + _, v, unit = re.split(r'([0-9]+)\s', line) + assert unit == 'MiB' + vals.append(int(v)*10**6) + mem.append(Memory(*vals)) + return mem + + +def rocm_smi(): + gpu_infos = {} + + ret = run(['rocm-smi', '--showproductname'], capture_output=True, text=True) + lines = ret.stdout.splitlines() + + for line in lines: + if 'GPU' in line: + # Product + pattern = r'GPU\[(\d+)\].*?Card [sS]eries:\s*(.*?)\s*$' + match1 = re.match(pattern, line) + + if match1: + gid = match1.group(1) + gpu_infos.setdefault(gid, dict()) + gpu_infos[gid]['physicalid'] = gid + gpu_infos[gid]['product'] = match1.group(2) + + # Model + pattern = r'GPU\[(\d+)\].*?Card [mM]odel:\s*(.*?)\s*$' + match2 = re.match(pattern, line) + + if match2: + gid = match2.group(1) + gpu_infos.setdefault(gid, dict()) + gpu_infos[gid]['physicalid'] = match2.group(1) + gpu_infos[gid]['model'] = match2.group(2) + gpu_infos[gid]['vendor'] = 'AMD' + gpu_infos[gid]['architecture'] = 'unspecified' + + return list(gpu_infos.values()) + + +def rocm_smi_memory(): + ret = run( + ['rocm-smi', '--showmeminfo', 'vram', '--json'], + capture_output=True, + text=True + ) + json_data = json.loads(ret.stdout) + + mem = [] + for card in json_data: + assert len(json_data[k]) == 2 + info = cast2numeric(json_data[k]) + vals = [0]*3 + for k, v in info: + if 'Total' in k: + vals[0] = v + if 'Used' in k: + vals[2] = v + vals[1] = vals[0] - vals[2] + mem.append(Memory(*vals)) + return mem + + +def sycl_ls(): + gpu_infos = {} + + # sycl-ls sometimes finds gpu twice with opencl and without so + # we need to make sure we don't get duplicates + selected_platform = None + platform_block = "" + + ret = run(["sycl-ls", "--verbose"], capture_output=True, text=True) + sycl_output = ret.stdout + + # Extract platform blocks + platforms = re.findall( + r"Platform \[#(\d+)\]:([\s\S]*?)(?=Platform \[#\d+\]:|$)", + sycl_output + ) + + # Select Level-Zero if available, otherwise use OpenCL + for platform_id, platform_content in platforms: + if "Intel(R) Level-Zero" in platform_content: + selected_platform = platform_id + platform_block = platform_content + break + elif "Intel(R) OpenCL Graphics" in platform_content and \ + selected_platform is None: + selected_platform = platform_id + platform_block = platform_content + + # Extract GPU devices from the selected platform + devices = re.findall( + r"Device \[#(\d+)\]:([\s\S]*?)(?=Device \[#\d+\]:|$)", + platform_block + ) + + for device_id, device_block in devices: + if re.search(r"^\s*Type\s*:\s*gpu", device_block, re.MULTILINE): + name_match = re.search(r"^\s*Name\s*:\s*(.+)", device_block, re.MULTILINE) + + if name_match: + name = name_match.group(1).strip() + + # Store GPU info with correct physical ID + gpu_infos[device_id] = { + "physicalid": device_id, + "product": name, + 'vendor': 'INTEL', + 'architecture': 'unspecified' + } + + return list(gpu_infos.values()) + + +def xpu_smi_memory(): + raise NotImplementedError + + +def filter_real_gpus(gpus): + """ Filter out virtual GPUs from a list of GPU dictionaries + """ + return list(filter(lambda g: 'virtual' not in g['product'].lower(), gpus)) + + +def lshw(): + ret = run(['lshw', '-C', 'video'], capture_output=True, text=True) + raw_info = ret.stdout + + # Parse the information for all the devices listed with lshw + gpu_infos = [] + for block in raw_info.split('display')[1:]: + # Separate the output block into lines for processing + lines = block.splitlines() + + # Define the processing functions + if lines: + gpu_info = {} + for line in lines: + # Architecture + if line.lstrip().startswith('product') and '[' in line: + arch_match = re.search(r'\[([\w\s]+)\]', line) + if arch_match: + gpu_info['architecture'] = arch_match.group(1) + + for keyword in ['product', 'vendor', 'physical id']: + if line.lstrip().startswith(keyword): + gpu_info[keyword] = line.split(':')[1].lstrip() + + gpu_info.set_default('architecture', 'unspecified') + + if 'physical id' in gpu_info: + gpu_info['physicalid'] = gpu_info.pop('physical id') + + gpu_infos.append(gpu_info) + + return filter_real_gpus(gpu_infos) + + +def lspci(): + ret = run(['lspci'], capture_output=True, text=True) + lines = ret.stdout.splitlines() + + # Note: due to the single line descriptive format of lspci, 'vendor' + # and 'physicalid' elements cannot be reliably extracted so are left None + + gpu_infos = [] + for line in lines: + # Graphics cards are listed as VGA or 3D controllers in lspci + if any(i in line for i in ('VGA', '3D', 'Display')): + gpu_info = {} + # Lines produced by lspci command are of the form: + # xxxx:xx:xx.x Device Type: Name + # eg: + # 0001:00:00.0 3D controller: NVIDIA Corp... [Tesla K80] (rev a1) + name_match = re.match( + r'\d\d\d\d:\d\d:\d\d\.\d [\w\s]+: ([\w\s\(\)\[\]]*)', line + ) + if name_match: + gpu_info['product'] = name_match.group(1) + arch_match = re.search(r'\[([\w\s]+)\]', line) + if arch_match: + gpu_info['architecture'] = arch_match.group(1) + else: + gpu_info['architecture'] = 'unspecified' + else: + continue + + gpu_infos.append(gpu_info) + + return filter_real_gpus(gpu_infos) From 2c1b08997fa9795cc11b27d47fcdeb779f1bb624 Mon Sep 17 00:00:00 2001 From: Jack Betteridge Date: Fri, 13 Feb 2026 18:50:16 +0000 Subject: [PATCH 3/6] arch: Additional tweaks --- devito/arch/archinfo.py | 10 ++++++---- devito/arch/commands.py | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/devito/arch/archinfo.py b/devito/arch/archinfo.py index 83be120a1c..a95581c52d 100644 --- a/devito/arch/archinfo.py +++ b/devito/arch/archinfo.py @@ -19,7 +19,9 @@ from devito.tools import all_equal, as_tuple, memoized_func from devito.warnings import warn -from .commands import lscpu, lshw, lspci, nvidia_smi, proc_cpuinfo, rocm_smi, sycl_ls +from .commands import ( + lscpu, lshw, lspci_gpu, nvidia_smi, proc_cpuinfo, rocm_smi, sycl_ls +) __all__ = [ # noqa: RUF022 @@ -193,7 +195,7 @@ def get_cpu_info(): try: cpu_info = cpuinfo.get_cpu_info() except Exception as e: - warn(f'Calling `cpuinfo.get_cpu_info()` faised an exception\n {e !s}') + warn(f'Calling `cpuinfo.get_cpu_info()` raised an exception\n {e !s}') cpu_info = {} cpu_info['logical'] = psutil.cpu_count(logical=True) @@ -620,11 +622,11 @@ def get_gpu_info(): - architecture: str, fallback 'unspecified' """ - for call in [nvidia_smi, rocm_smi, sycl_ls, lshw, lspci]: + for call in [nvidia_smi, rocm_smi, sycl_ls, lshw, lspci_gpu]: try: gpu_info = homogenise_gpus(call()) break - except (OSError): + except (OSError, FileNotFoundError, CalledProcessError): gpu_info = {} # Attach callbacks to retrieve instantaneous memory info diff --git a/devito/arch/commands.py b/devito/arch/commands.py index 8b2bf6442f..c9ad5c47ae 100644 --- a/devito/arch/commands.py +++ b/devito/arch/commands.py @@ -12,6 +12,7 @@ 'lscpu', 'lshw', 'lspci', + 'lspci_gpu', 'nvidia_smi', 'nvidia_smi_memory', 'proc_cpuinfo', @@ -171,7 +172,7 @@ def nvidia_smi_memory(): # the output format (though we still have tests in place that # will catch this) vals = [] - for value in line.split(', ') + for value in line.split(', '): _, v, unit = re.split(r'([0-9]+)\s', line) assert unit == 'MiB' vals.append(int(v)*10**6) @@ -331,7 +332,7 @@ def lshw(): return filter_real_gpus(gpu_infos) -def lspci(): +def old_lspci(): ret = run(['lspci'], capture_output=True, text=True) lines = ret.stdout.splitlines() @@ -363,3 +364,38 @@ def lspci(): gpu_infos.append(gpu_info) return filter_real_gpus(gpu_infos) + + +def lspci(): + ret = run(['lspci', '-mm', '-v'], capture_output=True, text=True) + blocks = ret.stdout.strip().split('\n\n') + + pci_info = {} + for device in blocks: + line = device.splitlines() + slot = line[0].split(':', 1)[1].strip() + info = { + (part:=l.split(':', 1))[0]: part[1].strip() + for l in line[1:] + } + pci_info[slot] = info + return pci_info + + +def lspci_gpu(): + devices = lspci() + + gpu_info = [] + for dev in devices.values(): + # Graphics cards are listed as VGA, 3D controllers or Displays in lspci + if any(sub in dev['Class'] for sub in ('VGA', '3D', 'Display')): + info = {} + info['class'] = dev['Class'] + info['vendor'] = dev['Vendor'] + info['device'] = dev['Device'] + info['product'] = dev['Device'] + info['architecture'] = 'unspecified' + info['physicalid'] = 'unknown' + gpu_info.append(info) + + return filter_real_gpus(gpu_info) From cd37bb391fbd30b4119d2e48118c7b472fb1acf9 Mon Sep 17 00:00:00 2001 From: JDBetteridge Date: Mon, 23 Feb 2026 22:22:02 +0000 Subject: [PATCH 4/6] misc: Fix some reprs --- devito/operations/interpolators.py | 6 ++++++ devito/tools/abc.py | 2 +- devito/types/utils.py | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/devito/operations/interpolators.py b/devito/operations/interpolators.py index 6467e65df4..119fe64785 100644 --- a/devito/operations/interpolators.py +++ b/devito/operations/interpolators.py @@ -220,6 +220,12 @@ class WeightedInterpolator(GenericInterpolator): def __init__(self, sfunction): self.sfunction = sfunction + def __str__(self): + return f'{self.__class__.__name__}({self.sfunction})' + + def __repr__(self): + return f'{self.__class__.__name__}({self.sfunction.name})' + @property def grid(self): return self.sfunction.grid diff --git a/devito/tools/abc.py b/devito/tools/abc.py index 2e489ac3c0..6bf64db7af 100644 --- a/devito/tools/abc.py +++ b/devito/tools/abc.py @@ -38,7 +38,7 @@ def __hash__(self): return hash((self.name, self.val)) def __str__(self): - ret = self.name if self.val is None else f"{self.name}[{str(self.val)}]" + ret = self.name if self.val is None else f"{self.name}({self.val!r})" return ret __repr__ = __str__ diff --git a/devito/types/utils.py b/devito/types/utils.py index 0593c78575..c7b3578d47 100644 --- a/devito/types/utils.py +++ b/devito/types/utils.py @@ -107,7 +107,7 @@ def __init__(self, suffix=''): self.suffix = suffix def __repr__(self): - return f"Layer<{self.suffix}>" + return f"HierarchyLayer({self.suffix!r})" def __eq__(self, other): return (isinstance(other, HierarchyLayer) and From 57ee2b6303ef2982e1842aa42f87e57dbb135498 Mon Sep 17 00:00:00 2001 From: JDBetteridge Date: Tue, 24 Feb 2026 00:21:44 +0000 Subject: [PATCH 5/6] misc: Update frozendict repr --- devito/tools/data_structures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/tools/data_structures.py b/devito/tools/data_structures.py index d875878d02..945d93ca22 100644 --- a/devito/tools/data_structures.py +++ b/devito/tools/data_structures.py @@ -661,7 +661,7 @@ def __len__(self): return len(self._dict) def __repr__(self): - return f'<{self.__class__.__name__} {self._dict!r}>' + return f'{self.__class__.__name__}({self._dict!r})' def __hash__(self): if self._hash is None: From 30227906dda1a08579b0bdd260a16bf057e608c0 Mon Sep 17 00:00:00 2001 From: Jack Betteridge Date: Thu, 30 Apr 2026 19:52:24 +0100 Subject: [PATCH 6/6] dsl: Update repr for HierarchyLayer --- devito/types/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devito/types/utils.py b/devito/types/utils.py index c7b3578d47..e35b732eef 100644 --- a/devito/types/utils.py +++ b/devito/types/utils.py @@ -107,7 +107,7 @@ def __init__(self, suffix=''): self.suffix = suffix def __repr__(self): - return f"HierarchyLayer({self.suffix!r})" + return f"{self.__class__.__name__}({self.suffix!r})" def __eq__(self, other): return (isinstance(other, HierarchyLayer) and