diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1c980e6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,148 @@ +# Changelog — XPUSYS-Monitor-NG + +All notable changes for the Windows-native AMD ROCm port are documented here. + +This fork is based on [ComfyUI-XPUSYS-Monitor](https://github.com/allanmeng/ComfyUI-XPUSYS-Monitor) +v1.0.3 by allanmeng. The version below tracks deviations from that baseline. + +--- + +## v0.1.0 — 2026-06-08 + +### Intent + +The upstream AMD provider (`providers/amd.py`) relied on `rocm_smi_lib` — a +Linux-only Python package that wraps `librocm_smi64.so`. On Windows with AMD +ROCm, `pip install rocm_smi_lib` fails because the native `.so` library does +not exist. This port replaces every `rocm_smi` call with a Windows-native +alternative. + +### Detection: `providers/__init__.py` + +**Problem:** The upstream detection function `_is_amd_rocme()` accessed +`torch.version.roc` directly. On Windows AMD ROCm builds (tested with PyTorch +2.9.1+rocm7.2.1), the `roc` attribute **does not exist** on the +`torch.version` module — raising `AttributeError`. The outer `try/except` +caught it and returned `False`, causing the auto-detector to load +`NvidiaProvider` instead of `AMDProvider`. The GPU name fallback code after +the `roc` check was unreachable. + +**Fix:** Replaced bare attribute access with `getattr(torch.version, 'roc', +None)`. Added a secondary signal `getattr(torch.version, 'hip', None)` for +HIP-based detection. Added a tertiary fallback scanning the GPU device name +(via `torch.cuda.get_device_name(0)`) for the markers `"amd"`, `"radeon"`, +or `"advanced micro devices"`. + +### VRAM: `providers/amd.py` → `_read_vram()` + +**Problem:** The upstream AMD provider used `rocm_smi.getMemFreeVdev(0)`, +`rocm_smi.getMemSizeVdev(0)`, and `rocm_smi.getMemUsedVdev(0)` for +driver-level VRAM reads. Without `rocm_smi_lib`, the fallback returned only +total VRAM from `torch.cuda.get_device_properties(0).total_memory`, leaving +`free` and `driver_used` as `0.0` — making the PRED predictor and VRAM +capsule unusable. + +**Fix:** Replaced all three `rocm_smi` VRAM calls with +`torch.cuda.mem_get_info(device_index)`, which returns `(free_bytes, +total_bytes)` from the AMD driver on ROCm 6+ for Windows. Moved the call +inside a `try/except` with a fallback to `get_device_properties().total_memory` +if `mem_get_info` is unavailable. Added `torch.cuda.synchronize()` before +reads to force CUDA/HIP context creation (some ROCm builds defer context +init until the first GPU operation, returning zeros otherwise). + +### GPU Load: `providers/amd.py` → `_read_gpu_load()` + +**Problem:** The upstream used `rocm_smi.getGpuBusyVdev(0)`. No standard +Python-accessible equivalent exists on Windows AMD. + +**Solution attempted — PDH (ctypes):** Added `_PdhQuery` to +`providers/_utils.py` using `ctypes` wrappers around `pdh.dll` to query +`\GPU Engine(*)\Utilization Percentage`. The wildcard counter path does not +aggregate correctly with `PdhGetFormattedCounterValue` (returns only the +first matching instance). This approach was disabled for AMD in favour of +typeperf. + +**Solution adopted — typeperf:** Added `_TypeperfGpuQuery` to +`providers/_utils.py`. Uses Windows built-in `typeperf.exe` (available since +Vista) with the same counter path `\GPU Engine(*)\Utilization Percentage`. +Output is CSV; we parse columns after the timestamp and take `max()` across +all engine instances. Averaging would dilute the signal (hundreds of engine +columns including idle video/copy/timer). `max()` correctly reflects the +busiest engine (typically 3D or Compute during a ComfyUI workflow). At idle +all engines report ~0%, so the capsule drops cleanly. + +**Attempted — amdsmi:** Added `_AmdSmiGpuQuery` to `providers/_utils.py` +using the official AMD SMI Python library (`pip install amdsmi`). On Windows +the library searches for `libamd_smi.so` (a Linux shared object) at +`D:\opt\rocm\lib\`, which does not exist on the tested configuration. The +class logs a single info line and gracefully skips if `amdsmi` is not +installed or fails to load. + +### GPU Frequency / Temperature / Power: `providers/amd.py` + +**Problem:** The upstream used `rocm_smi.getSingleClockSpeed(0)`, +`rocm_smi.getTempVdev(0)`, `rocm_smi.getPowerVdev(0)`, and +`rocm_smi.getPowerCapVdev(0)` for clock speed, temperature, and power draw. +The AMD Windows WDDM driver on the RX 9070 XT does not register these +performance counters through any standard Python-accessible interface. + +**Resolution:** All three return sentinel values matching the `GPUSnapshot` +contract defaults — `0.0` for frequency, `-1.0` for temperature, +`(-1.0, 0.0, False)` for power. The frontend displays these as unavailable +(`--` / greyed out), identical behaviour to when the Intel provider cannot +open Level Zero handles or the NVIDIA provider cannot reach pynvml. + +### Shared Utilities: `providers/_utils.py` (new file) + +**Problem:** The upstream AMD provider imported system-level CPU and RAM +utility functions (`_get_cpu_info`, `_read_cpu_ram_stats`, +`_read_commit_charge`) from `providers/intel.py`. This created a spurious +dependency on the Intel Level Zero provider code for non-Intel users. + +**Fix:** Extracted these three functions plus `_is_admin()` into a new shared +module `providers/_utils.py`. Also relocated `_PdhQuery`, `_TypeperfGpuQuery`, +and `_AmdSmiGpuQuery` into the same module. Both `amd.py` and `nvidia.py` +now import from `_utils.py` instead of `intel.py`. The `intel.py` module is +no longer needed unless the Intel provider is loaded (auto-detection +fallback path). + +### NVIDIA Provider: `providers/nvidia.py` + +**Change:** Updated import path from `from .intel import ...` to +`from ._utils import ...`. No functional change — identical utility +functions. + +### Frontend: `web/xpu_monitor.js` + +**Problem:** The `__init__.py` declares `WEB_DIRECTORY = "./web"` which +tells ComfyUI to serve the JavaScript toolbar extension from a `web/` +subdirectory. This directory was not included in the initial workspace, +causing the toolbar capsules to not render. + +**Fix:** Added `web/xpu_monitor.js` (56 KB, identical to upstream v1.0.3). +The file is the full JavaScript frontend that renders the seven-capsule +status bar, handles WebSocket updates from the backend, and provides the +VRAM predictor UI. No modifications were made. + +### Dependencies: `requirements.txt` + +**Change:** `rocm_smi_lib` commented out with an explanatory note. No +replacement dependency added — VRAM reads use `torch.cuda` (bundled with +the ROCm PyTorch installation), GPU load reads use `typeperf` (Windows +built-in), and CPU/RAM reads use `psutil` (already required by the upstream). + +### Project Metadata: `pyproject.toml` + +**Changes:** +- Repository URL updated to `https://github.com/forkless/XPUSYS-Monitor-NG` +- Display name set to `XPUSYS-Monitor-NG` +- Description updated to reflect the POC nature +- Publisher ID set to `forkless` + +### Documentation + +- `README.md` — rewritten for the fork with POC context, status table, + relationship to upstream, tested hardware, support disclaimer, MIT license +- `AMD.md` — detailed technical summary of every change, intent, and end + result table +- `LICENSE.md` — MIT license (matches upstream) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3351c16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 forkless + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/providers/_utils.py b/providers/_utils.py index 613166e..833d343 100644 --- a/providers/_utils.py +++ b/providers/_utils.py @@ -277,18 +277,34 @@ class _PdhQuery: # --------------------------------------------------------------------------- # typeperf-based GPU utilisation fallback # -# Uses Windows built-in typeperf.exe (available since Vista) to query -# the \GPU Engine(*)\Utilization Percentage performance counter. +# ADDED for Windows-native AMD ROCm support (no rocm_smi_lib). # -# typeperf avoids the quoting/escaping headaches of PowerShell -Command -# and is available on every Windows system with WDDM drivers. +# Two approaches were attempted before settling on typeperf: # -# Output format (CSV): -# "(PDH-CSV 4.0) (...)", "\\COMPUTER\GPU Engine(*)\Utilization Percentage" -# "date time", "val1,val2,val3,..." +# 1. PDH ctypes — _PdhQuery (above). Uses PdhAddEnglishCounterW with +# wildcard path "\GPU Engine(*)\Utilization Percentage". The wildcard +# expands to hundreds of per-process engine instances, but +# PdhGetFormattedCounterValue on a wildcard handle returns only the +# first matching instance, not an aggregate. This approach is disabled +# for AMD in favour of typeperf. # -# We parse the second line, split the comma-separated values, and -# average them to get total GPU utilisation. +# 2. typeperf — This class (below). Windows built-in CLI tool that +# accepts the same counter path and returns CSV with one column per +# engine instance. Every column after the timestamp is a separate +# engine value. We parse all columns and take max()—not average—because +# with hundreds of engines (video decode, copy, timer, security, etc.) +# all reporting 0 at idle, averaging dilutes the real signal from the +# few active 3D/Compute engines during a workflow. +# +# 3. amdsmi — _AmdSmiGpuQuery (further below). Official AMD SMI +# library. Gracefully skipped on Windows because the PyPI package +# searches for libamd_smi.so (Linux-only). +# +# Output format (typeperf CSV): +# Line 1: "(PDH-CSV 4.0)","\\PC\GPU Engine(pid_..._engtype_3D)\...", ... +# Line 2: "date time","0.000000","1.299634","0.000000", ... +# +# REPLACES: upstream rocm_smi.getGpuBusyVdev(0) # --------------------------------------------------------------------------- import csv as _csv @@ -381,12 +397,26 @@ class _TypeperfGpuQuery: # --------------------------------------------------------------------------- # amdsmi-based GPU utilisation (official AMD SMI library) # -# Uses the AMD SMI Python package which ships with ROCm. Talks directly -# to the AMD driver — not through WDDM. Reports real GPU engine utilisation -# (GFX, MM, MEM) as percentages 0–100%. +# ADDED for Windows-native AMD ROCm support. +# +# The official AMD SMI Python package (pip install amdsmi) provides direct +# driver-level GPU metrics — engine utilisation (GFX, MM, MEM), temperature, +# power, clock speed — without going through WDDM performance counters. +# +# On Windows, the PyPI package's ctypes wrapper searches for the native +# library at a hardcoded Linux path (libamd_smi.so via ctypes.CDLL). +# Windows DLLs use different filenames and search paths, so the import +# fails with KeyError: 'libamd_smi.so' on a standard Windows ROCm install. +# +# This class uses try/except ImportError to gracefully skip when the +# package is not installed or the native library cannot be loaded. No +# crash, no stack trace — just a single info-line in the log. +# +# If AMD releases an official Windows-compatible amdsmi wheel in the +# future, this class will activate automatically without code changes. # # Install: pip install amdsmi -# Requires: ROCm 6+ (user has ROCm 7.2) +# Requires: ROCm 6+ (ROCm 7.2 on the tested configuration) # --------------------------------------------------------------------------- class _AmdSmiGpuQuery: diff --git a/providers/amd.py b/providers/amd.py index de91a8f..06755e3 100644 --- a/providers/amd.py +++ b/providers/amd.py @@ -122,8 +122,20 @@ class AMDProvider(BaseGPUProvider): """ Return (free_gb, total_gb, driver_used_gb) via torch.cuda.mem_get_info. + REPLACES: upstream rocm_smi.getMemFreeVdev(0), + rocm_smi.getMemSizeVdev(0), + rocm_smi.getMemUsedVdev(0) + + torch.cuda.mem_get_info() returns (free_bytes, total_bytes) from + the AMD HIP driver on ROCm 6+ for Windows. This is the same function + used by NVIDIA CUDA — AMD ROCm's HIP runtime implements the same + CUDA API surface, so it works without any AMD-specific library. + Falls back to get_device_properties if mem_get_info is unavailable. - Forces CUDA context init to ensure device queries succeed. + Forces CUDA context init (torch.cuda.synchronize) before reading; + some ROCm builds defer HIP context creation until the first GPU + operation, and mem_get_info() returns (0, 0) without an active + context. """ if not self._torch_ok: return 0.0, 0.0, 0.0 @@ -187,8 +199,20 @@ class AMDProvider(BaseGPUProvider): """ Return GPU utilisation %. - Tries amdsmi (official AMD SMI, bypasses WDDM). - Falls back to typeperf (WDDM counters, best-effort). + REPLACES: upstream rocm_smi.getGpuBusyVdev(0) + + Two-layer fallback chain: + 1. amdsmi — official AMD SMI library (pip install amdsmi). + Bypasses WDDM, talks directly to the AMD driver. + Gracefully skipped on Windows because the PyPI + package searches for libamd_smi.so (Linux-only). + 2. typeperf — Windows built-in (available since Vista). + Reads \\GPU Engine(*)\\Utilization Percentage + via WDDM performance counters. Returns CSV with + one column per engine instance (3D, Compute, + Copy, Video, Timer, etc.). We use max() across + all engines — averaging dilutes the signal + across hundreds of idle engine types. """ if self._as_gpu_ok: return self._as_gpu.read_gpu_utilization() @@ -200,7 +224,12 @@ class AMDProvider(BaseGPUProvider): """ GPU core frequency in MHz. - Unavailable on Windows without vendor driver API. + REPLACES: upstream rocm_smi.getSingleClockSpeed(0) + + The AMD WDDM driver on Windows does not expose GPU core clock + through any standard Python-accessible interface (no PDH counter, + no WMI class, no torch.cuda equivalent). Returns 0 (unavailable + sentinel matching the GPUSnapshot contract default). """ return 0.0 @@ -208,7 +237,14 @@ class AMDProvider(BaseGPUProvider): """ GPU core temperature in C. - Unavailable on Windows without vendor driver API. + REPLACES: upstream rocm_smi.getTempVdev(0) + + The AMD WDDM driver on the tested configuration (RX 9070 XT, + ROCm 7.2, Windows) does not register a GPU temperature performance + counter. Tested: typeperf -q "GPU Adapter" returned "object not + found". Returns -1 (unavailable sentinel). Some AMD cards on newer + driver versions or different Windows builds may expose this through + WMI or PDH — this is hardware/driver-dependent. """ return -1.0 @@ -216,7 +252,13 @@ class AMDProvider(BaseGPUProvider): """ Return (power_w, tgp_w, power_available). - Unavailable on Windows without vendor driver API. + REPLACES: upstream rocm_smi.getPowerVdev(0) and + rocm_smi.getPowerCapVdev(0) + + GPU power monitoring is not exposed through Windows standard APIs + on the tested AMD driver. Returns (-1.0, 0.0, False) — the + GPUSnapshot power_available=False tells the frontend to grey out + the PWR capsule. """ return -1.0, 0.0, False