|
1
|
|
|
# |
|
2
|
|
|
# This file is part of Glances. |
|
3
|
|
|
# |
|
4
|
|
|
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]> |
|
5
|
|
|
# |
|
6
|
|
|
# SPDX-License-Identifier: LGPL-3.0-only |
|
7
|
|
|
# |
|
8
|
|
|
|
|
9
|
|
|
"""AMD Extension unit for Glances' GPU plugin. |
|
10
|
|
|
|
|
11
|
|
|
The class grabs the stats from the /sys/class/drm/ directory. |
|
12
|
|
|
|
|
13
|
|
|
See: https://wiki.archlinux.org/title/AMDGPU#Manually |
|
14
|
|
|
""" |
|
15
|
|
|
|
|
16
|
|
|
# Example |
|
17
|
|
|
# tests-data/plugins/gpu/amd/ |
|
18
|
|
|
# └── sys |
|
19
|
|
|
# ├── class |
|
20
|
|
|
# │ └── drm |
|
21
|
|
|
# │ └── card0 |
|
22
|
|
|
# │ └── device |
|
23
|
|
|
# │ ├── device |
|
24
|
|
|
# │ ├── gpu_busy_percent |
|
25
|
|
|
# │ ├── hwmon |
|
26
|
|
|
# │ │ └── hwmon0 |
|
27
|
|
|
# │ │ └── temp1_input |
|
28
|
|
|
# │ ├── mem_info_vram_total |
|
29
|
|
|
# │ ├── mem_info_vram_used |
|
30
|
|
|
# │ ├── pp_dpm_mclk |
|
31
|
|
|
# │ ├── pp_dpm_sclk |
|
32
|
|
|
# │ └── revision |
|
33
|
|
|
# └── kernel |
|
34
|
|
|
# └── debug |
|
35
|
|
|
# └── dri |
|
36
|
|
|
# └── 0 |
|
37
|
|
|
# └── amdgpu_pm_info |
|
38
|
|
|
|
|
39
|
|
|
import os |
|
40
|
|
|
import re |
|
41
|
|
|
from typing import Optional |
|
42
|
|
|
import functools |
|
43
|
|
|
|
|
44
|
|
|
DRM_ROOT_FOLDER: str = '/sys/class/drm' |
|
45
|
|
|
CARD_REGEX: str = r"^card\d$" |
|
46
|
|
|
DEVICE_FOLDER: str = 'device' |
|
47
|
|
|
AMDGPU_IDS_FILE: str = '/usr/share/libdrm/amdgpu.ids' |
|
48
|
|
|
PCI_DEVICE_ID: str = 'device' |
|
49
|
|
|
PCI_REVISION_ID: str = 'revision' |
|
50
|
|
|
GPU_PROC_PERCENT: str = 'gpu_busy_percent' |
|
51
|
|
|
GPU_MEM_TOTAL: str = 'mem_info_vram_total' |
|
52
|
|
|
GPU_MEM_USED: str = 'mem_info_vram_used' |
|
53
|
|
|
HWMON_REGEXP: str = r"^hwmon\d$" |
|
54
|
|
|
GPU_TEMPERATURE_REGEXP: str = r"^temp\d_input" |
|
55
|
|
|
|
|
56
|
|
|
|
|
57
|
|
|
class AmdGPU: |
|
58
|
|
|
"""GPU card class.""" |
|
59
|
|
|
|
|
60
|
|
|
def __init__(self, drm_root_folder: str = DRM_ROOT_FOLDER): |
|
61
|
|
|
"""Init AMD GPU card class.""" |
|
62
|
|
|
self.drm_root_folder = drm_root_folder |
|
63
|
|
|
self.device_folders = get_device_list(drm_root_folder) |
|
64
|
|
|
|
|
65
|
|
|
def exit(self): |
|
66
|
|
|
"""Close AMD GPU class.""" |
|
67
|
|
|
|
|
68
|
|
|
def get_device_stats(self): |
|
69
|
|
|
"""Get AMD GPU stats.""" |
|
70
|
|
|
stats = [] |
|
71
|
|
|
|
|
72
|
|
|
for index, device in enumerate(self.device_folders): |
|
73
|
|
|
device_stats = {} |
|
74
|
|
|
# Dictionary key is the GPU_ID |
|
75
|
|
|
device_stats['key'] = 'gpu_id' |
|
76
|
|
|
# GPU id (for multiple GPU, start at 0) |
|
77
|
|
|
device_stats['gpu_id'] = f'amd{index}' |
|
78
|
|
|
# GPU name |
|
79
|
|
|
device_stats['name'] = get_device_name(device) |
|
80
|
|
|
# Memory consumption in % (not available on all GPU) |
|
81
|
|
|
device_stats['mem'] = get_mem(device) |
|
82
|
|
|
# Processor consumption in % |
|
83
|
|
|
device_stats['proc'] = get_proc(device) |
|
84
|
|
|
# Processor temperature in °C |
|
85
|
|
|
device_stats['temperature'] = get_temperature(device) |
|
86
|
|
|
# Fan speed in % |
|
87
|
|
|
device_stats['fan_speed'] = get_fan_speed(device) |
|
88
|
|
|
stats.append(device_stats) |
|
89
|
|
|
|
|
90
|
|
|
return stats |
|
91
|
|
|
|
|
92
|
|
|
|
|
93
|
|
|
def get_device_list(drm_root_folder: str) -> list: |
|
94
|
|
|
"""Return a list of path to the device stats.""" |
|
95
|
|
|
ret = [] |
|
96
|
|
|
for root, dirs, _ in os.walk(drm_root_folder): |
|
97
|
|
|
for d in dirs: |
|
98
|
|
|
if ( |
|
99
|
|
|
re.match(CARD_REGEX, d) |
|
100
|
|
|
and DEVICE_FOLDER in os.listdir(os.path.join(root, d)) |
|
101
|
|
|
and os.path.isfile(os.path.join(root, d, DEVICE_FOLDER, GPU_PROC_PERCENT)) |
|
102
|
|
|
): |
|
103
|
|
|
# If the GPU busy file is present then take the card into account |
|
104
|
|
|
ret.append(os.path.join(root, d, DEVICE_FOLDER)) |
|
105
|
|
|
return ret |
|
106
|
|
|
|
|
107
|
|
|
|
|
108
|
|
|
def read_file(*path_segments: str) -> Optional[str]: |
|
109
|
|
|
"""Return content of file.""" |
|
110
|
|
|
path = os.path.join(*path_segments) |
|
111
|
|
|
if os.path.isfile(path): |
|
112
|
|
|
with open(path) as f: |
|
113
|
|
|
try: |
|
114
|
|
|
return f.read().strip() |
|
115
|
|
|
except PermissionError: |
|
116
|
|
|
# Catch exception (see issue #3125) |
|
117
|
|
|
return None |
|
118
|
|
|
return None |
|
119
|
|
|
|
|
120
|
|
|
|
|
121
|
|
|
@functools.cache |
|
122
|
|
|
def get_device_name(device_folder: str) -> str: |
|
123
|
|
|
"""Return the GPU name.""" |
|
124
|
|
|
|
|
125
|
|
|
# Table source: https://cgit.freedesktop.org/drm/libdrm/tree/data/amdgpu.ids |
|
126
|
|
|
device_id = read_file(device_folder, PCI_DEVICE_ID) |
|
127
|
|
|
revision_id = read_file(device_folder, PCI_REVISION_ID) |
|
128
|
|
|
amdgpu_ids = read_file(AMDGPU_IDS_FILE) |
|
129
|
|
|
if device_id and revision_id and amdgpu_ids: |
|
130
|
|
|
# Strip leading "0x" and convert to uppercase hexadecimal |
|
131
|
|
|
device_id = device_id[2:].upper() |
|
132
|
|
|
revision_id = revision_id[2:].upper() |
|
133
|
|
|
# Syntax: |
|
134
|
|
|
# device_id, revision_id, product_name <-- single tab after comma |
|
135
|
|
|
pattern = re.compile(f'^{device_id},\\s{revision_id},\\s(?P<product_name>.+)$', re.MULTILINE) |
|
136
|
|
|
if match := pattern.search(amdgpu_ids): |
|
137
|
|
|
return match.group('product_name').removeprefix('AMD ').removesuffix(' Graphics') |
|
138
|
|
|
|
|
139
|
|
|
return 'AMD GPU' |
|
140
|
|
|
|
|
141
|
|
|
|
|
142
|
|
|
def get_mem(device_folder: str) -> Optional[int]: |
|
143
|
|
|
"""Return the memory consumption in %.""" |
|
144
|
|
|
mem_info_vram_total = read_file(device_folder, GPU_MEM_TOTAL) |
|
145
|
|
|
mem_info_vram_used = read_file(device_folder, GPU_MEM_USED) |
|
146
|
|
|
if mem_info_vram_total and mem_info_vram_used: |
|
147
|
|
|
mem_info_vram_total = int(mem_info_vram_total) |
|
148
|
|
|
mem_info_vram_used = int(mem_info_vram_used) |
|
149
|
|
|
if mem_info_vram_total > 0: |
|
150
|
|
|
return round(mem_info_vram_used / mem_info_vram_total * 100) |
|
151
|
|
|
return None |
|
152
|
|
|
|
|
153
|
|
|
|
|
154
|
|
|
def get_proc(device_folder: str) -> Optional[int]: |
|
155
|
|
|
"""Return the processor consumption in %.""" |
|
156
|
|
|
if gpu_busy_percent := read_file(device_folder, GPU_PROC_PERCENT): |
|
157
|
|
|
return int(gpu_busy_percent) |
|
158
|
|
|
return None |
|
159
|
|
|
|
|
160
|
|
|
|
|
161
|
|
|
def get_temperature(device_folder: str) -> Optional[int]: |
|
162
|
|
|
"""Return the processor temperature in °C (mean of all HWMON)""" |
|
163
|
|
|
temp_input = [] |
|
164
|
|
|
for root, dirs, _ in os.walk(device_folder): |
|
165
|
|
|
for d in dirs: |
|
166
|
|
|
if re.match(HWMON_REGEXP, d): |
|
167
|
|
|
for _, _, files in os.walk(os.path.join(root, d)): |
|
168
|
|
|
for f in files: |
|
169
|
|
|
if re.match(GPU_TEMPERATURE_REGEXP, f): |
|
170
|
|
|
if a_temp_input := read_file(root, d, f): |
|
171
|
|
|
temp_input.append(int(a_temp_input)) |
|
172
|
|
|
else: |
|
173
|
|
|
return None |
|
174
|
|
|
if temp_input: |
|
175
|
|
|
return round(sum(temp_input) / len(temp_input) / 1000) |
|
176
|
|
|
return None |
|
177
|
|
|
|
|
178
|
|
|
|
|
179
|
|
|
def get_fan_speed(device_folder: str) -> Optional[int]: |
|
180
|
|
|
"""Return the fan speed in %.""" |
|
181
|
|
|
return None |
|
182
|
|
|
|