From 43f1e99f2b30c34ac9d26b3e4ba5821603b42dcb Mon Sep 17 00:00:00 2001 From: forkless Date: Mon, 8 Jun 2026 23:36:15 +0200 Subject: [PATCH] feat: add PowerShell GPU util fallback when PDH unavailable --- providers/_utils.py | 74 +++++++++++++++++++++++++++++++++++++++++++++ providers/amd.py | 15 ++++++--- 2 files changed, 84 insertions(+), 5 deletions(-) diff --git a/providers/_utils.py b/providers/_utils.py index df782f3..f42a78f 100644 --- a/providers/_utils.py +++ b/providers/_utils.py @@ -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", ] diff --git a/providers/amd.py b/providers/amd.py index a78971b..67527eb 100644 --- a/providers/amd.py +++ b/providers/amd.py @@ -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: