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 +1 -0
- gputaskman/__main__.py +3 -0
- gputaskman/adl.py +101 -0
- gputaskman/amdsmi.py +80 -0
- gputaskman/app.py +675 -0
- gputaskman/cli.py +87 -0
- gputaskman/collector.py +392 -0
- gputaskman/diff.py +254 -0
- gputaskman/drm.py +665 -0
- gputaskman/dxgi.py +125 -0
- gputaskman/gguf.py +254 -0
- gputaskman/handy.py +146 -0
- gputaskman/linux.py +356 -0
- gputaskman/llm.py +441 -0
- gputaskman/macos.py +407 -0
- gputaskman/nvml.py +105 -0
- gputaskman/pdh.py +225 -0
- gputaskman/procinfo.py +454 -0
- gputaskman/report.py +254 -0
- gputaskman/stores.py +87 -0
- gputaskman/telemetry.py +38 -0
- gputaskman/wslhost.py +503 -0
- gputaskman-0.3.2.dist-info/METADATA +803 -0
- gputaskman-0.3.2.dist-info/RECORD +27 -0
- gputaskman-0.3.2.dist-info/WHEEL +4 -0
- gputaskman-0.3.2.dist-info/entry_points.txt +4 -0
- gputaskman-0.3.2.dist-info/licenses/LICENSE +21 -0
gputaskman/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""gputaskman - an htop for what's *inside* your GPU."""
|
gputaskman/__main__.py
ADDED
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
|