feat: add PowerShell GPU util fallback when PDH unavailable

This commit is contained in:
2026-06-08 23:36:15 +02:00
parent 24c6c88504
commit 43f1e99f2b
2 changed files with 84 additions and 5 deletions
+74
View File
@@ -274,10 +274,84 @@ class _PdhQuery:
self._ok = False
# ---------------------------------------------------------------------------
# PowerShell-based GPU utilisation fallback
#
# Used when PDH counters are unavailable. Calls PowerShell's Get-Counter
# cmdlet via subprocess to read the same WDDM GPU engine counters.
#
# Slower than PDH (~100-300ms per call vs <1ms) but works on any Windows
# version with WDDM drivers — no extra dependencies.
# ---------------------------------------------------------------------------
import subprocess as _subprocess
_POWERSHELL_GPU_CMD = (
'try{(Get-Counter \\"\\GPU Engine(*)\\Utilization Percentage\\" '
'-MaxSamples 1 -ErrorAction Stop).CounterSamples|'
'?{$_.Status -eq 0}|'
'Measure-Object CookedValue -Average|'
'%{$_.Average}}catch{0}'
)
class _PowerShellGpuQuery:
"""Fallback GPU utilisation reader via PowerShell Get-Counter."""
def __init__(self):
self._ok = False
def init(self) -> bool:
# Quick self-test: can we launch PowerShell and get a number back?
try:
val = self._run_query()
self._ok = val is not None
if self._ok:
logger.info(
f"XPUSYSMonitor: PowerShell GPU counters OK "
f"(test={val:.1f}%)."
)
else:
logger.warning(
"XPUSYSMonitor: PowerShell GPU counters unavailable."
)
return self._ok
except Exception as exc:
logger.debug(f"XPUSYSMonitor: PowerShell GPU init error — {exc}")
return False
def read_gpu_utilization(self) -> float:
"""Query total GPU utilisation % via PowerShell."""
if not self._ok:
return 0.0
try:
val = self._run_query()
return min(val, 100.0) if val is not None else 0.0
except Exception:
return 0.0
@staticmethod
def _run_query() -> float | None:
"""Run the PowerShell query and return the average, or None."""
try:
r = _subprocess.run(
["powershell", "-NoProfile", "-Command", _POWERSHELL_GPU_CMD],
capture_output=True, text=True, timeout=5,
creationflags=0x08000000, # CREATE_NO_WINDOW
)
if r.returncode != 0:
return None
val = r.stdout.strip()
return float(val) if val else None
except Exception:
return None
__all__ = [
"_is_admin",
"_get_cpu_info",
"_read_cpu_ram_stats",
"_read_commit_charge",
"_PdhQuery",
"_PowerShellGpuQuery",
]
+10 -5
View File
@@ -18,7 +18,7 @@ import sys
from typing import Tuple
from .base import BaseGPUProvider, GPUSnapshot
from ._utils import _get_cpu_info, _read_cpu_ram_stats, _PdhQuery, _is_admin
from ._utils import _get_cpu_info, _read_cpu_ram_stats, _PdhQuery, _PowerShellGpuQuery, _is_admin
logger = logging.getLogger("XPUSYSMonitor")
@@ -52,9 +52,11 @@ class AMDProvider(BaseGPUProvider):
self._check_torch()
self._check_psutil()
# Windows PDH — GPU engine utilisation (graceful if unavailable)
# Windows GPU utilisation — chain: PDH (fast) -> PowerShell (fallback)
self._pdh = _PdhQuery()
self._pdh_ok = self._pdh.init()
self._ps_gpu = _PowerShellGpuQuery()
self._ps_gpu_ok = self._ps_gpu.init() if not self._pdh_ok else False
# BaseGPUProvider.__init__ starts the polling thread — call last
super().__init__(interval_ms=interval_ms)
@@ -181,13 +183,16 @@ class AMDProvider(BaseGPUProvider):
def _read_gpu_load(self) -> float:
"""
Return GPU utilisation % via Windows PDH API.
Return GPU utilisation %.
Falls back to 0 if PDH is unavailable (non-Windows, or
counters not installed by the AMD driver).
Tries PDH API first (sub-millisecond, ctypes).
Falls back to PowerShell Get-Counter if PDH is unavailable
(slower ~100-300ms but works on any Windows WDDM driver).
"""
if self._pdh_ok:
return self._pdh.read_gpu_utilization()
if self._ps_gpu_ok:
return self._ps_gpu.read_gpu_utilization()
return 0.0
def _read_gpu_freq_mhz(self) -> float: