fix: switch GPU util fallback to typeperf, fix PDH wildcard aggregation

This commit is contained in:
2026-06-08 23:42:01 +02:00
parent 43f1e99f2b
commit 630fb54797
2 changed files with 53 additions and 34 deletions
+47 -28
View File
@@ -275,53 +275,53 @@ class _PdhQuery:
# ---------------------------------------------------------------------------
# PowerShell-based GPU utilisation fallback
# typeperf-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.
# Uses Windows built-in typeperf.exe (available since Vista) to query
# the \GPU Engine(*)\Utilization Percentage performance counter.
#
# Slower than PDH (~100-300ms per call vs <1ms) but works on any Windows
# version with WDDM drivers — no extra dependencies.
# typeperf avoids the quoting/escaping headaches of PowerShell -Command
# and is available on every Windows system with WDDM drivers.
#
# Output format (CSV):
# "(PDH-CSV 4.0) (...)", "\\COMPUTER\GPU Engine(*)\Utilization Percentage"
# "date time", "val1,val2,val3,..."
#
# We parse the second line, split the comma-separated values, and
# average them to get total GPU utilisation.
# ---------------------------------------------------------------------------
import csv as _csv
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."""
class _TypeperfGpuQuery:
"""GPU utilisation reader via typeperf (Windows built-in)."""
def __init__(self):
self._ok = False
self._counter_path = "\\GPU Engine(*)\\Utilization Percentage"
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"XPUSYSMonitor: typeperf GPU counters OK "
f"(test={val:.1f}%)."
)
else:
logger.warning(
"XPUSYSMonitor: PowerShell GPU counters unavailable."
"XPUSYSMonitor: typeperf GPU counters unavailable."
)
return self._ok
except Exception as exc:
logger.debug(f"XPUSYSMonitor: PowerShell GPU init error — {exc}")
logger.debug(f"XPUSYSMonitor: typeperf GPU init error — {exc}")
return False
def read_gpu_utilization(self) -> float:
"""Query total GPU utilisation % via PowerShell."""
"""Query total GPU utilisation % via typeperf."""
if not self._ok:
return 0.0
try:
@@ -330,19 +330,38 @@ class _PowerShellGpuQuery:
except Exception:
return 0.0
@staticmethod
def _run_query() -> float | None:
"""Run the PowerShell query and return the average, or None."""
def _run_query(self) -> float | None:
"""Run typeperf and parse the output. Returns average % or None."""
try:
r = _subprocess.run(
["powershell", "-NoProfile", "-Command", _POWERSHELL_GPU_CMD],
capture_output=True, text=True, timeout=5,
["typeperf", self._counter_path, "-sc", "1"],
capture_output=True, text=True, timeout=10,
creationflags=0x08000000, # CREATE_NO_WINDOW
)
if r.returncode != 0:
return None
val = r.stdout.strip()
return float(val) if val else None
# Parse CSV output
# Line 1: header
# Line 2: data e.g. "...", "0.000000,12.500000,5.200000"
lines = r.stdout.strip().splitlines()
if len(lines) < 2:
return None
# Second line: extract the quoted value column
row = list(_csv.reader([lines[1]]))[0]
if len(row) < 2:
return None
# The last column contains comma-separated per-engine values
raw = row[-1]
values = [float(v) for v in raw.split(",") if v.strip()]
if not values:
return None
# Average all engine values for total GPU utilisation
return sum(values) / len(values)
except Exception:
return None
@@ -353,5 +372,5 @@ __all__ = [
"_read_cpu_ram_stats",
"_read_commit_charge",
"_PdhQuery",
"_PowerShellGpuQuery",
"_TypeperfGpuQuery",
]
+6 -6
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, _PowerShellGpuQuery, _is_admin
from ._utils import _get_cpu_info, _read_cpu_ram_stats, _PdhQuery, _TypeperfGpuQuery, _is_admin
logger = logging.getLogger("XPUSYSMonitor")
@@ -52,11 +52,11 @@ class AMDProvider(BaseGPUProvider):
self._check_torch()
self._check_psutil()
# Windows GPU utilisation — chain: PDH (fast) -> PowerShell (fallback)
# Windows GPU utilisation — chain: PDH (fast) -> typeperf (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
self._tp_gpu = _TypeperfGpuQuery()
self._tp_gpu_ok = self._tp_gpu.init() if not self._pdh_ok else False
# BaseGPUProvider.__init__ starts the polling thread — call last
super().__init__(interval_ms=interval_ms)
@@ -191,8 +191,8 @@ class AMDProvider(BaseGPUProvider):
"""
if self._pdh_ok:
return self._pdh.read_gpu_utilization()
if self._ps_gpu_ok:
return self._ps_gpu.read_gpu_utilization()
if self._tp_gpu_ok:
return self._tp_gpu.read_gpu_utilization()
return 0.0
def _read_gpu_freq_mhz(self) -> float: