fix(perf): poll the right adapter under --device gpu/--monitor (#445)#467
Merged
Conversation
added 2 commits
May 7, 2026 17:17
xieofxie
commented
May 8, 2026
timenick
reviewed
May 11, 2026
xieofxie
commented
May 11, 2026
added 2 commits
May 11, 2026 15:32
timenick
approved these changes
May 11, 2026
ssss141414
pushed a commit
that referenced
this pull request
May 15, 2026
…#467) Fixes #445 — `winml perf --monitor --device gpu` now shows GPU utilization instead of silently falling back to NPU. ## Summary - **Adapter routing.** `PdhPoller` / `HWMonitor` now accept `device=` (`npu` | `gpu` | `cpu` | `auto`) and `ep_name=`. Per-device queries go through `build_npu_query` (Compute engine) or `build_gpu_query` (3D engine). - **ORT-first LUID resolution.** Added `resolve_adapter_luid(device_kind, ep_name=None)` in `sysinfo/pdh_adapters.py`. It walks `ort.get_ep_devices()`, filters by EP name + device type, reads `OrtHardwareDevice.metadata["LUID"]`, and converts the decimal LUID to the PDH `0xHHHHHHHH_0xHHHHHHHH` form via `(v >> 32, v & 0xFFFFFFFF)`. ORT-resolved LUIDs are sanity-checked against `enumerate_adapters()` — if PDH doesn't enumerate the LUID (some drivers register the adapter only for autoEP), we skip it and fall through to the PDH-fingerprint helpers (`discover_npu_luid`, `discover_gpu_luid`) instead of raising later in `build_adapter_query`. Same fallback applies when ORT isn't installed, when `get_ep_devices()` raises, when no matching ep_device exists, when LUID metadata is missing, or when an individual ep_device entry is malformed (each is wrapped in `try/except` so one bad entry can't break resolution). - **Graceful build-query degradation.** `PdhPoller.start()` wraps `build_npu_query` / `build_gpu_query` in a local `try/except ValueError`. If a resolved LUID still proves unusable, it clears `device_kind` / `adapter_luid` and degrades to CPU/RAM-only — CPU/RAM counters still register and the benchmark continues. - **`perf.py` plumbing.** `_run_benchmark_monitored` (HF), `_run_onnx_benchmark` (ONNX direct), and `_perf_modules` (per-module) all pass the resolved device + normalized EP name into `HWMonitor`. `--monitor` help text updated to *"Show live hardware utilization chart for the benchmarked device (NPU, GPU, or CPU)"*. - **`HWMonitor.to_dict()` schema.** Always emits an `npu` block (zeros when monitoring GPU — back-compat with existing consumers/tests). When `device_kind == "gpu"` a parallel `gpu` block is added. New top-level keys `device_kind` and `adapter_luid`. - **Live chart labels follow what's actually polled.** Centralised `adapter_label(device_kind)` helper in `hw_monitor.py`. `LiveMonitorDisplay` accepts `device_kind=` (threaded through from `_run_monitored_loop` as `hw.device_kind`); `HWLiveDisplay` derives the label dynamically from `self._hw.device_kind`. So `auto` resolving to GPU now labels "GPU", and CPU-only mode no longer falsely says "NPU". - **CPU-only display drops the adapter row entirely.** When no adapter is polled (`--device cpu` or `auto` resolving to nothing), `LiveMonitorDisplay` / `HWLiveDisplay` skip the green plot, drop the swatch from the chart legend, and collapse the status row to `CPU: ... | Sys Mem: ...` (no adapter cell, no Device Mem cell). The post-benchmark `display_console_report` in `perf.py` likewise prints CPU/RAM only — no zeroed `NPU: 0.0% avg, 0.0% peak`. ## Verification - Targeted suite: `tests/unit/session/test_ep_monitor.py` (84 passed, +15 new) plus `test_perf_cli.py` and `test_perf_module.py` (26 passed). - Full unit suite green: 4665 passed, 73 skipped, 2 xfailed. - `ruff check` clean on every modified file. - Smoke-checked on a Snapdragon X laptop after `winml.register_execution_providers(ort=True)`: ``` GPU (no ep_name) -> 0x00000000_0x000121CF # Adreno X1-85 via DML and QNN GPU (ep=DML) -> 0x00000000_0x000121CF GPU (ep=QNN) -> 0x00000000_0x000121CF NPU (ep=QNN) -> 0x00000000_0x00012C89 # authoritative, not just fingerprinted NPU (no ep_name) -> 0x00000000_0x00012C89 ``` --------- Co-authored-by: hualxie <hualxie@microsoft.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #445 —
winml perf --monitor --device gpunow shows GPU utilization instead of silently falling back to NPU.Summary
PdhPoller/HWMonitornow acceptdevice=(npu|gpu|cpu|auto) andep_name=. Per-device queries go throughbuild_npu_query(Compute engine) orbuild_gpu_query(3D engine).resolve_adapter_luid(device_kind, ep_name=None)insysinfo/pdh_adapters.py. It walksort.get_ep_devices(), filters by EP name + device type, readsOrtHardwareDevice.metadata["LUID"], and converts the decimal LUID to the PDH0xHHHHHHHH_0xHHHHHHHHform via(v >> 32, v & 0xFFFFFFFF). ORT-resolved LUIDs are sanity-checked againstenumerate_adapters()— if PDH doesn't enumerate the LUID (some drivers register the adapter only for autoEP), we skip it and fall through to the PDH-fingerprint helpers (discover_npu_luid,discover_gpu_luid) instead of raising later inbuild_adapter_query. Same fallback applies when ORT isn't installed, whenget_ep_devices()raises, when no matching ep_device exists, when LUID metadata is missing, or when an individual ep_device entry is malformed (each is wrapped intry/exceptso one bad entry can't break resolution).PdhPoller.start()wrapsbuild_npu_query/build_gpu_queryin a localtry/except ValueError. If a resolved LUID still proves unusable, it clearsdevice_kind/adapter_luidand degrades to CPU/RAM-only — CPU/RAM counters still register and the benchmark continues.perf.pyplumbing._run_benchmark_monitored(HF),_run_onnx_benchmark(ONNX direct), and_perf_modules(per-module) all pass the resolved device + normalized EP name intoHWMonitor.--monitorhelp text updated to "Show live hardware utilization chart for the benchmarked device (NPU, GPU, or CPU)".HWMonitor.to_dict()schema. Always emits annpublock (zeros when monitoring GPU — back-compat with existing consumers/tests). Whendevice_kind == "gpu"a parallelgpublock is added. New top-level keysdevice_kindandadapter_luid.adapter_label(device_kind)helper inhw_monitor.py.LiveMonitorDisplayacceptsdevice_kind=(threaded through from_run_monitored_loopashw.device_kind);HWLiveDisplayderives the label dynamically fromself._hw.device_kind. Soautoresolving to GPU now labels "GPU", and CPU-only mode no longer falsely says "NPU".--device cpuorautoresolving to nothing),LiveMonitorDisplay/HWLiveDisplayskip the green plot, drop the swatch from the chart legend, and collapse the status row toCPU: ... | Sys Mem: ...(no adapter cell, no Device Mem cell). The post-benchmarkdisplay_console_reportinperf.pylikewise prints CPU/RAM only — no zeroedNPU: 0.0% avg, 0.0% peak.Verification
tests/unit/session/test_ep_monitor.py(84 passed, +15 new) plustest_perf_cli.pyandtest_perf_module.py(26 passed).ruff checkclean on every modified file.winml.register_execution_providers(ort=True):