gputaskman 0.3.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gputaskman/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """gputaskman - an htop for what's *inside* your GPU."""
gputaskman/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
gputaskman/adl.py ADDED
@@ -0,0 +1,101 @@
1
+ """Read-only AMD Display Library telemetry supplied by the Windows driver.
2
+
3
+ ADL2_New_QueryPMLogData_Get is deprecated by AMD; failures leave data unavailable.
4
+ Definitions: https://github.com/GPUOpen-LibrariesAndSDKs/display-library/tree/master/include
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import ctypes
9
+ import os
10
+
11
+ from .dxgi import Adapter, VENDOR_AMD
12
+ from .telemetry import DeviceTelemetry
13
+
14
+
15
+ class _AdapterInfo(ctypes.Structure):
16
+ _fields_ = [("size", ctypes.c_int), ("index", ctypes.c_int), ("udid", ctypes.c_char * 256),
17
+ ("bus", ctypes.c_int), ("device", ctypes.c_int), ("function", ctypes.c_int),
18
+ ("vendor", ctypes.c_int), ("name", ctypes.c_char * 256),
19
+ ("display", ctypes.c_char * 256), ("present", ctypes.c_int), ("exists", ctypes.c_int),
20
+ ("driver", ctypes.c_char * 256), ("driver_ext", ctypes.c_char * 256),
21
+ ("pnp", ctypes.c_char * 256), ("os_index", ctypes.c_int)]
22
+
23
+
24
+ class _Sensor(ctypes.Structure):
25
+ _fields_ = [("supported", ctypes.c_int), ("value", ctypes.c_int)]
26
+
27
+
28
+ class _Metrics(ctypes.Structure):
29
+ _fields_ = [("size", ctypes.c_int), ("sensors", _Sensor * 256)]
30
+
31
+
32
+ class Adl:
33
+ def __init__(self, adapter: Adapter | None, adapters: list[Adapter]):
34
+ self.dll = None
35
+ self.context = ctypes.c_void_p()
36
+ self.index = None
37
+ self.driver = ""
38
+ if not adapter or adapter.vendor_id != VENDOR_AMD or adapter.pci_address is None:
39
+ return
40
+ if sum(a.pci_address == adapter.pci_address for a in adapters if not a.software) != 1:
41
+ return
42
+ try:
43
+ # System32 is the AMD driver's installation location, not the working directory.
44
+ self.dll = ctypes.CDLL(os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "atiadlxx.dll"))
45
+ crt = ctypes.CDLL("msvcrt")
46
+ crt.malloc.argtypes = [ctypes.c_size_t]
47
+ crt.malloc.restype = ctypes.c_void_p
48
+ self._malloc = ctypes.CFUNCTYPE(ctypes.c_void_p, ctypes.c_int)(crt.malloc)
49
+ self.dll.ADL2_Main_Control_Create.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(ctypes.c_void_p)]
50
+ self.dll.ADL2_Main_Control_Destroy.argtypes = [ctypes.c_void_p]
51
+ self.dll.ADL2_Adapter_NumberOfAdapters_Get.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_int)]
52
+ self.dll.ADL2_Adapter_AdapterInfo_Get.argtypes = [ctypes.c_void_p, ctypes.POINTER(_AdapterInfo), ctypes.c_int]
53
+ self.dll.ADL2_New_QueryPMLogData_Get.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.POINTER(_Metrics)]
54
+ if self.dll.ADL2_Main_Control_Create(self._malloc, 1, ctypes.byref(self.context)) != 0:
55
+ return
56
+ count = ctypes.c_int()
57
+ if self.dll.ADL2_Adapter_NumberOfAdapters_Get(self.context, ctypes.byref(count)) != 0 or not 0 < count.value <= 256:
58
+ return
59
+ entries = (_AdapterInfo * count.value)()
60
+ for entry in entries:
61
+ entry.size = ctypes.sizeof(_AdapterInfo)
62
+ if self.dll.ADL2_Adapter_AdapterInfo_Get(self.context, entries, ctypes.sizeof(entries)) != 0:
63
+ return
64
+ matches = {entry.index for entry in entries if (entry.bus, entry.device, entry.function) == adapter.pci_address}
65
+ # ADL may return several logical display handles for the same physical GPU.
66
+ if matches:
67
+ self.index = min(matches)
68
+ except (OSError, AttributeError, ValueError):
69
+ self.close()
70
+ finally:
71
+ if self.index is None:
72
+ self.close()
73
+
74
+ def sample(self) -> DeviceTelemetry | None:
75
+ if self.index is None:
76
+ return None
77
+ metrics = _Metrics()
78
+ metrics.size = ctypes.sizeof(metrics)
79
+ try:
80
+ status = self.dll.ADL2_New_QueryPMLogData_Get(self.context, self.index, ctypes.byref(metrics))
81
+ except (OSError, AttributeError):
82
+ return None
83
+ if status != 0 or not any(item.supported for item in metrics.sensors):
84
+ return None
85
+ def sensor(index):
86
+ item = metrics.sensors[index]
87
+ return item.value if item.supported and item.value >= 0 else None
88
+ board_power = sensor(73)
89
+ asic_power = sensor(23)
90
+ return DeviceTelemetry(index=self.index, source="AMD ADL", gfx_clock=sensor(1), mem_clock=sensor(2),
91
+ temp=sensor(8), fan=sensor(15), util_gpu=sensor(19),
92
+ power_w=board_power if board_power is not None else asic_power,
93
+ power_kind="board" if board_power is not None else "ASIC" if asic_power is not None else "",
94
+ pcie_width=sensor(41))
95
+
96
+ def close(self):
97
+ self.index = None
98
+ if self.dll is not None and self.context.value:
99
+ context = self.context
100
+ self.context = ctypes.c_void_p()
101
+ self.dll.ADL2_Main_Control_Destroy(context)
gputaskman/amdsmi.py ADDED
@@ -0,0 +1,80 @@
1
+ """Optional read-only AMD SMI telemetry. WDDM collection works without this module."""
2
+ from __future__ import annotations
3
+
4
+ import importlib
5
+ import math
6
+ import re
7
+
8
+ from .dxgi import Adapter, VENDOR_AMD
9
+ from .telemetry import DeviceTelemetry
10
+
11
+
12
+ def _number(value):
13
+ return value if isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) else None
14
+
15
+
16
+ def _bdf(value):
17
+ match = re.fullmatch(r"([0-9a-fA-F]+):([0-9a-fA-F]+):([0-9a-fA-F]+)\.([0-9a-fA-F]+)", str(value))
18
+ if not match:
19
+ return None
20
+ domain, bus, device, function = (int(part, 16) for part in match.groups())
21
+ # WDDM's ADAPTERADDRESS has no domain. Refuse nonzero domains rather than guess.
22
+ return (bus, device, function) if domain == 0 else None
23
+
24
+
25
+ class AmdSmi:
26
+ def __init__(self, adapter: Adapter | None, adapters: list[Adapter], api=None):
27
+ self.api = None
28
+ self.handle = None
29
+ self.driver = ""
30
+ self.initialized = False
31
+ if not adapter or adapter.vendor_id != VENDOR_AMD or adapter.pci_address is None:
32
+ return
33
+ if sum(a.pci_address == adapter.pci_address for a in adapters if not a.software) != 1:
34
+ return
35
+ try:
36
+ self.api = api or importlib.import_module("amdsmi")
37
+ self.api.amdsmi_init()
38
+ self.initialized = True
39
+ matches = [h for h in self.api.amdsmi_get_processor_handles()
40
+ if _bdf(self._call("amdsmi_get_gpu_device_bdf", h)) == adapter.pci_address]
41
+ if len(matches) == 1:
42
+ self.handle = matches[0]
43
+ info = self._call("amdsmi_get_gpu_driver_info", self.handle) or {}
44
+ if isinstance(info, dict):
45
+ self.driver = str(info.get("driver_version") or "")
46
+ except Exception:
47
+ self.close()
48
+
49
+ def _call(self, name, *args):
50
+ try:
51
+ return getattr(self.api, name)(*args)
52
+ except Exception:
53
+ return None
54
+
55
+ def sample(self) -> DeviceTelemetry | None:
56
+ if self.handle is None:
57
+ return None
58
+ d = DeviceTelemetry(index=0, source="AMD SMI")
59
+ activity = self._call("amdsmi_get_gpu_activity", self.handle)
60
+ if isinstance(activity, dict):
61
+ d.util_gpu = _number(activity.get("gfx_activity"))
62
+ power = self._call("amdsmi_get_power_info", self.handle)
63
+ if isinstance(power, dict):
64
+ d.power_w = _number(power.get("socket_power"))
65
+ d.power_kind = "socket" if d.power_w is not None else ""
66
+ d.power_limit_w = _number(power.get("power_limit"))
67
+ try:
68
+ temp = self._call("amdsmi_get_temp_metric", self.handle,
69
+ self.api.AmdSmiTemperatureType.EDGE, self.api.AmdSmiTemperatureMetric.CURRENT)
70
+ value = _number(temp)
71
+ d.temp = value # AMD SMI reports degrees Celsius
72
+ except AttributeError:
73
+ pass
74
+ return d
75
+
76
+ def close(self):
77
+ self.handle = None
78
+ if self.initialized:
79
+ self._call("amdsmi_shut_down")
80
+ self.initialized = False