Record controlled K timings and add read-only AMD GPU telemetry
This commit is contained in:
1 parent
973eb5ecd2
commit
9dece89599
6 files changed
+364
-4
No files matched your search
@@ -249,8 +249,8 @@ completion. Every image stays in VRAM. The ZLUDA implementation adds
|
|||||||
`cuMemcpy2DAsync_v2` using `hipMemcpyParam2DAsync`; format-80 array emulation
|
`cuMemcpy2DAsync_v2` using `hipMemcpyParam2DAsync`; format-80 array emulation
|
||||||
is explicitly rejected because its synchronous implementation uses temporary
|
is explicitly rejected because its synchronous implementation uses temporary
|
||||||
host storage. The Windows path supplies canonical FP16/FP32 arrays instead.
|
host storage. The Windows path supplies canonical FP16/FP32 arrays instead.
|
||||||
This experiment is disabled by default until the correctness and game timing
|
This experiment remains disabled by default. Correctness checks pass, but the
|
||||||
gates below pass; there is no claimed FPS improvement yet.
|
controlled game comparison below shows only a small frame-time difference.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\scripts\windows\test-deferred-kernel-profile.ps1
|
.\scripts\windows\test-deferred-kernel-profile.ps1
|
||||||
@@ -277,6 +277,74 @@ four-way K/4K gate passes again with exact candidate RGB and finite RGBA
|
|||||||
(`test-results/k-batch-committed-source-4k`). This warms the committed runtime's
|
(`test-results/k-batch-committed-source-4k`). This warms the committed runtime's
|
||||||
JIT cache before the game comparison.
|
JIT cache before the game comparison.
|
||||||
|
|
||||||
|
The following user-confirmed 30-second foreground-scene capture contains 1925
|
||||||
|
presents at **64.265 FPS**, versus the previous unbatched run's 63.574 FPS.
|
||||||
|
Both use 2259x1271 -> 3840x2160, Independent Flip and SyncInterval 0, without
|
||||||
|
kernel-event profiling, output scans or attached debuggers. Mean present time
|
||||||
|
is 15.561 versus 15.730 ms, a 0.169 ms difference. This single pair does not
|
||||||
|
establish repeatability or a large performance improvement. The containing
|
||||||
|
ten-minute candidate run completes 35673 K frames with zero backend failures,
|
||||||
|
CPU image copies, previous-frame outputs or recorded game crashes. Whole-run
|
||||||
|
median input preparation drops from 0.482 to 0.332 ms; median external span
|
||||||
|
is 5.438 versus 5.399 ms. Whole-run stages are not the selected PresentMon
|
||||||
|
window. No GPU utilization conclusion is drawn from ETW estimates. Results:
|
||||||
|
`test-results/silent-hill2-k-batch-inputs-4k`. The K performance gate stays open.
|
||||||
|
|
||||||
|
### AMD driver telemetry
|
||||||
|
|
||||||
|
A separate read-only diagnostic polls the installed driver's ADLX base C
|
||||||
|
interfaces for device-wide usage, core/VRAM clocks, board power and temperatures.
|
||||||
|
It adds no work to game queues and changes no tuning setting. Build it locally
|
||||||
|
using the official SDK pinned at `32b5a740d42295c5dfe9026b9f52683da0f3af91`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\windows\build-gpu-telemetry.ps1
|
||||||
|
.\scripts\windows\capture-presentmon.ps1 -Seconds 30 -GpuTelemetry
|
||||||
|
```
|
||||||
|
|
||||||
|
The SDK retains its own AMD license; SDK headers, implementations, driver DLLs
|
||||||
|
and the SDK-dependent executable are excluded from public game packages.
|
||||||
|
The probe is independently written and uses the documented C ABI, including
|
||||||
|
with LLVM-MinGW. It loads `amdadlx64.dll` only from Windows System32 and
|
||||||
|
releases acquired interfaces before ADLX termination/unload. GPU selection
|
||||||
|
must match exactly one device (`-GpuName` selects a different name substring).
|
||||||
|
The process exits at its bound or target process exit. The capture retains
|
||||||
|
CSV, raw stderr, exit/timeout, driver DLL version/hash and SDK build identity.
|
||||||
|
No optional telemetry failure is silently converted to a valid measurement.
|
||||||
|
|
||||||
|
CSV records independent host UTC/QPC timestamps and the driver's raw sample
|
||||||
|
timestamp. On this installed ADLX 1.5.0.124, the latter is uptime despite the
|
||||||
|
SDK description of epoch time; do not interpret it as UTC. Unsupported metrics
|
||||||
|
are blank, zero utilization/clock remains valid, and the report uses each
|
||||||
|
driver timestamp once. Device-wide usage is distinct from PresentMon's
|
||||||
|
per-process frame attribution and is not measured WMMA occupancy. ADLX API:
|
||||||
|
[AMD GPU metrics sample](https://gpuopen.com/manuals/adlx/adlx-c__perf_g_p_u_metrics/).
|
||||||
|
|
||||||
|
The initial idle-device smoke test captures six valid polls, selects the real
|
||||||
|
RX 9070 XT and terminates ADLX successfully. Board power/core clock/usage
|
||||||
|
and temperatures are available; `GPUPower` is unsupported and stays absent
|
||||||
|
from the report. The first poll costs roughly 10 ms for initialization;
|
||||||
|
subsequent polls are about 0.08 ms at 500 ms intervals. This idle smoke test
|
||||||
|
does not establish game clocks, utilization or an overhead-free benchmark.
|
||||||
|
Results: `test-results/gpu-telemetry-smoke`.
|
||||||
|
|
||||||
|
The subsequent combined-capture smoke test collects 246 presents and ten
|
||||||
|
ADLX polls and exits cleanly (`test-results/gpu-telemetry-capture-smoke`).
|
||||||
|
It includes startup/scene changes and is excluded from FPS comparisons.
|
||||||
|
After the user confirms the foreground comparison scene, the 30-second run
|
||||||
|
with detailed d4r stages/boundary timestamps disabled records **64.661 FPS**
|
||||||
|
over 1935 presents. ADLX's sixty polls report mean usage **93.183%**, core
|
||||||
|
clock **3124.667 MHz**, board power **316.717 W**, temperature 57 C and hotspot
|
||||||
|
82.9 C. Mean PresentMon GPU busy/wait is 14.551/0.800 ms. All presents retain
|
||||||
|
Independent Flip / SyncInterval 0. This records high device-wide driver usage
|
||||||
|
in this run; the previous user-reported 63% has not been captured from the same
|
||||||
|
sensor/window and its discrepancy is unresolved. Do not infer a physical CPU
|
||||||
|
limit, isolated shader occupancy or a FSR4 speed comparison. Query overhead
|
||||||
|
is median 0.094 ms per 500 ms poll, with a roughly 12 ms first query. Results:
|
||||||
|
`test-results/silent-hill2-k-telemetry-unprofiled-4k`. The game stability
|
||||||
|
summary is still pending its existing ten-minute bound. Remaining K work now
|
||||||
|
prioritizes transformer/output-kernel cost; frame scheduling remains measured.
|
||||||
|
|
||||||
Further game coverage (HIP kernel profiling disabled, GPU output checks enabled):
|
Further game coverage (HIP kernel profiling disabled, GPU output checks enabled):
|
||||||
M at 1920x1080 completes 4833 frames, all finite. K at 3840x2160 completes
|
M at 1920x1080 completes 4833 frames, all finite. K at 3840x2160 completes
|
||||||
4685 frames, all finite. Neither has a backend failure, CPU image copy or an
|
4685 frames, all finite. Neither has a backend failure, CPU image copy or an
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ host synchronization; postpone further M/FP8 optimization. These user readings
|
|||||||
are not a controlled same-scene benchmark. Do not infer a hardware CPU limit
|
are not a controlled same-scene benchmark. Do not infer a hardware CPU limit
|
||||||
from utilization alone.
|
from utilization alone.
|
||||||
|
|
||||||
|
A subsequent unprofiled K/4K capture records **64.661 FPS** with direct AMD
|
||||||
|
ADLX mean usage **93.183%**, core clock **3124.667 MHz** and board power
|
||||||
|
**316.717 W**. The previous 63% reading is from a different sensor/window;
|
||||||
|
its discrepancy remains unresolved. This new capture places GPU kernel cost
|
||||||
|
at the center of the next K investigation, while retaining queue measurements.
|
||||||
|
|
||||||
The next diagnostics add `-ProfileCudaApi` (ZLUDA patch 0015): aggregate host
|
The next diagnostics add `-ProfileCudaApi` (ZLUDA patch 0015): aggregate host
|
||||||
CUDA API durations without HIP-event waits. Boundary stages now separate
|
CUDA API durations without HIP-event waits. Boundary stages now separate
|
||||||
prepare, copy-list setup, input/output submission and output-copy drain.
|
prepare, copy-list setup, input/output submission and output-copy drain.
|
||||||
@@ -103,7 +109,7 @@ stream, which needs explicit opt-in for timestamp sampling. The depth/stencil
|
|||||||
and packed-format burst/recreation gates pass 24 full K/4K frames: all twelve
|
and packed-format burst/recreation gates pass 24 full K/4K frames: all twelve
|
||||||
candidate RGB images match their fresh unbatched synchronous controls exactly,
|
candidate RGB images match their fresh unbatched synchronous controls exactly,
|
||||||
with finite RGBA. Results: `test-results/k-batch-deferred-4k` and
|
with finite RGBA. Results: `test-results/k-batch-deferred-4k` and
|
||||||
`test-results/k-batch-packed-4k`. The game performance gate is pending. The candidate remains
|
`test-results/k-batch-packed-4k`. The candidate remains
|
||||||
disabled by default; see `windows-performance.md` for semantics and commands.
|
disabled by default; see `windows-performance.md` for semantics and commands.
|
||||||
|
|
||||||
Current candidate source is ZLUDA `67127dde599879ada022d6439383d682e09e0b36`
|
Current candidate source is ZLUDA `67127dde599879ada022d6439383d682e09e0b36`
|
||||||
@@ -118,6 +124,23 @@ well as TheRock; PTX output/guards remain correct. It also affects a native K
|
|||||||
dec4 timing sample, so valid-looking events alone are not a timing-accuracy
|
dec4 timing sample, so valid-looking events alone are not a timing-accuracy
|
||||||
gate. No hardware utilization or kernel speedup is inferred from these samples.
|
gate. No hardware utilization or kernel speedup is inferred from these samples.
|
||||||
|
|
||||||
|
The committed batched-input game run completes **35673 K/4K frames** without
|
||||||
|
backend errors, CPU image copies, previous-frame output or recorded crashes.
|
||||||
|
The user-confirmed 30-second PresentMon window records **64.265 FPS** (1925
|
||||||
|
presents), compared with the prior unbatched 63.574 FPS. Both keep Independent
|
||||||
|
Flip / SyncInterval 0 and 2259x1271 -> 3840x2160. This small single-pair
|
||||||
|
difference is not a repeatable speedup gate. Whole-run median input preparation
|
||||||
|
falls from 0.482 to 0.332 ms, while median external span is 5.438 versus
|
||||||
|
5.399 ms. Results: `test-results/silent-hill2-k-batch-inputs-4k`.
|
||||||
|
K performance remains open and batching remains opt-in. The next diagnostic
|
||||||
|
adds a separate read-only AMD ADLX usage/clock/power capture alongside
|
||||||
|
PresentMon. Its local RX 9070 XT smoke test passes, retaining unsupported
|
||||||
|
metrics as absent and preserving the driver's raw timestamp. See
|
||||||
|
`windows-performance.md` for build/capture commands and SDK packaging limits.
|
||||||
|
The combined capture smoke test passes, followed by the user-confirmed
|
||||||
|
1935-present/60-poll measurement recorded at the top of this document. Logs
|
||||||
|
and runtime/DLL hashes: `test-results/silent-hill2-k-telemetry-unprofiled-4k`.
|
||||||
|
|
||||||
The public prerelease `windows-rdna4-dev-20261002-a346d76` is published to
|
The public prerelease `windows-rdna4-dev-20261002-a346d76` is published to
|
||||||
`xdfnx-dev/d4r`, containing source commit `a346d76` and ZLUDA `a1c506f`.
|
`xdfnx-dev/d4r`, containing source commit `a346d76` and ZLUDA `a1c506f`.
|
||||||
Archive: `d4r-windows-rdna4-20261002.zip`, 88893567 bytes, SHA256
|
Archive: `d4r-windows-rdna4-20261002.zip`, 88893567 bytes, SHA256
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param([string]$AdlxRoot, [string]$BuildDirectory, [string]$Compiler)
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
$repo=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../..'))
|
||||||
|
$commit='32b5a740d42295c5dfe9026b9f52683da0f3af91'
|
||||||
|
if (!$AdlxRoot) { $AdlxRoot=Join-Path $repo 'external/ADLX' }
|
||||||
|
if (!$BuildDirectory) { $BuildDirectory=Join-Path $repo 'build/gpu-telemetry' }
|
||||||
|
if (!$Compiler) { $Compiler=Join-Path $repo '.tools/llvm-mingw-20260922-ucrt-x86_64/bin/x86_64-w64-mingw32-clang.exe' }
|
||||||
|
if (!(Test-Path -LiteralPath $AdlxRoot)) {
|
||||||
|
& git clone --no-checkout https://github.com/GPUOpen-LibrariesAndSDKs/ADLX.git $AdlxRoot
|
||||||
|
if ($LASTEXITCODE) { throw 'ADLX SDK download failed.' }
|
||||||
|
& git -C $AdlxRoot checkout --detach $commit
|
||||||
|
if ($LASTEXITCODE) { throw 'ADLX SDK checkout failed.' }
|
||||||
|
}
|
||||||
|
if ((& git -C $AdlxRoot rev-parse HEAD).Trim() -ne $commit -or (& git -C $AdlxRoot status --porcelain)) {
|
||||||
|
throw "Expected clean ADLX SDK $commit; use a separate AdlxRoot."
|
||||||
|
}
|
||||||
|
if (!(Test-Path -LiteralPath $Compiler)) { throw 'Set Compiler to an x64 LLVM-MinGW clang.exe.' }
|
||||||
|
New-Item -ItemType Directory -Force $BuildDirectory | Out-Null
|
||||||
|
$exe=Join-Path $BuildDirectory 'd4r_gpu_telemetry.exe'
|
||||||
|
& $Compiler -std=c11 -O2 -Wall -Wextra -Wno-unknown-pragmas -DWIN32_LEAN_AND_MEAN -D_M_AMD64 `
|
||||||
|
-isystem "$AdlxRoot/SDK/Include" (Join-Path $repo 'tools/windows/gpu_telemetry.c') -o $exe
|
||||||
|
if ($LASTEXITCODE) { throw "GPU telemetry compile failed: $LASTEXITCODE" }
|
||||||
|
@{sdkCommit=$commit; sdkLicense=(Join-Path $AdlxRoot 'ADLX SDK License Agreement.pdf');
|
||||||
|
executable=$exe; compiler=$Compiler; sha256=(Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash} |
|
||||||
|
ConvertTo-Json | Set-Content -LiteralPath (Join-Path $BuildDirectory 'build-info.json') -Encoding UTF8
|
||||||
|
Write-Host "Built local read-only telemetry: $exe"
|
||||||
|
# The SDK, driver DLL and SDK-dependent binary are not added to public packages.
|
||||||
@@ -5,6 +5,9 @@ param(
|
|||||||
[ValidateRange(5,300)][int]$Seconds = 30,
|
[ValidateRange(5,300)][int]$Seconds = 30,
|
||||||
[ValidateRange(0,300)][int]$DelaySeconds = 0,
|
[ValidateRange(0,300)][int]$DelaySeconds = 0,
|
||||||
[string]$PresentMonPath,
|
[string]$PresentMonPath,
|
||||||
|
[switch]$GpuTelemetry,
|
||||||
|
[string]$TelemetryPath,
|
||||||
|
[string]$GpuName = 'RX 9070 XT',
|
||||||
[string]$OutputDirectory
|
[string]$OutputDirectory
|
||||||
)
|
)
|
||||||
$ErrorActionPreference='Stop'
|
$ErrorActionPreference='Stop'
|
||||||
@@ -32,16 +35,67 @@ $OutputDirectory=[IO.Path]::GetFullPath($OutputDirectory)
|
|||||||
New-Item -ItemType Directory -Force $OutputDirectory | Out-Null
|
New-Item -ItemType Directory -Force $OutputDirectory | Out-Null
|
||||||
$csv=Join-Path $OutputDirectory 'presentmon.csv'
|
$csv=Join-Path $OutputDirectory 'presentmon.csv'
|
||||||
if (Test-Path -LiteralPath $csv) { throw 'Preserve the existing capture; choose another OutputDirectory.' }
|
if (Test-Path -LiteralPath $csv) { throw 'Preserve the existing capture; choose another OutputDirectory.' }
|
||||||
|
if ($TelemetryPath) { $GpuTelemetry=$true }
|
||||||
|
if ($GpuTelemetry) {
|
||||||
|
if (!$TelemetryPath) { $TelemetryPath=Join-Path $repo 'build/gpu-telemetry/d4r_gpu_telemetry.exe' }
|
||||||
|
$TelemetryPath=(Get-Item -LiteralPath $TelemetryPath -ErrorAction Stop).FullName
|
||||||
|
if ($GpuName.Contains('"')) { throw 'GpuName cannot contain a double quote.' }
|
||||||
|
foreach ($file in @('gpu-telemetry.csv','gpu-telemetry.stderr.log','gpu-telemetry-environment.json')) {
|
||||||
|
if (Test-Path -LiteralPath (Join-Path $OutputDirectory $file)) { throw "Preserve existing $file; choose another OutputDirectory." }
|
||||||
|
}
|
||||||
|
}
|
||||||
@{version='2.6.0'; executable=$PresentMonPath; sha256=(Get-FileHash -LiteralPath $PresentMonPath -Algorithm SHA256).Hash;
|
@{version='2.6.0'; executable=$PresentMonPath; sha256=(Get-FileHash -LiteralPath $PresentMonPath -Algorithm SHA256).Hash;
|
||||||
targetProcessId=$target.Id; processName=$target.ProcessName; seconds=$Seconds; delaySeconds=$DelaySeconds;
|
targetProcessId=$target.Id; processName=$target.ProcessName; seconds=$Seconds; delaySeconds=$DelaySeconds;
|
||||||
|
gpuTelemetryRequested=[bool]$GpuTelemetry;
|
||||||
hwsRegistry=$(Get-ItemPropertyValue -LiteralPath 'HKLM:/SYSTEM/CurrentControlSet/Control/GraphicsDrivers' -Name HwSchMode -ErrorAction SilentlyContinue);
|
hwsRegistry=$(Get-ItemPropertyValue -LiteralPath 'HKLM:/SYSTEM/CurrentControlSet/Control/GraphicsDrivers' -Name HwSchMode -ErrorAction SilentlyContinue);
|
||||||
driver=@(Get-CimInstance Win32_VideoController | Select-Object Name,DriverVersion)} |
|
driver=@(Get-CimInstance Win32_VideoController | Select-Object Name,DriverVersion)} |
|
||||||
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $OutputDirectory 'presentmon-environment.json') -Encoding UTF8
|
ConvertTo-Json -Depth 4 | Set-Content -LiteralPath (Join-Path $OutputDirectory 'presentmon-environment.json') -Encoding UTF8
|
||||||
# Unique ETW session: never stop or replace an existing user's trace. The CLI
|
# Unique ETW session: never stop or replace an existing user's trace. The CLI
|
||||||
# terminates itself at the bound/process exit, without a service installation.
|
# terminates itself at the bound/process exit, without a service installation.
|
||||||
$session='D4R_' + [Guid]::NewGuid().ToString('N')
|
$session='D4R_' + [Guid]::NewGuid().ToString('N')
|
||||||
|
$telemetry=$null; $telemetryStarted=$false; $outStream=$null; $errStream=$null; $outTask=$null; $errTask=$null
|
||||||
|
try {
|
||||||
|
if ($GpuTelemetry) {
|
||||||
|
$info=[Diagnostics.ProcessStartInfo]::new()
|
||||||
|
$info.FileName=$TelemetryPath; $info.UseShellExecute=$false; $info.CreateNoWindow=$true
|
||||||
|
$info.RedirectStandardOutput=$true; $info.RedirectStandardError=$true
|
||||||
|
$info.Arguments="--seconds $Seconds --delay-ms $($DelaySeconds*1000) --interval-ms 500 --target-pid $($target.Id) --gpu-name `"$GpuName`""
|
||||||
|
$outStream=[IO.File]::Open((Join-Path $OutputDirectory 'gpu-telemetry.csv'),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
||||||
|
$errStream=[IO.File]::Open((Join-Path $OutputDirectory 'gpu-telemetry.stderr.log'),[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::Read)
|
||||||
|
$telemetry=[Diagnostics.Process]::new(); $telemetry.StartInfo=$info
|
||||||
|
if (!$telemetry.Start()) { throw 'GPU telemetry process failed to start.' }
|
||||||
|
$telemetryStarted=$true
|
||||||
|
$outTask=$telemetry.StandardOutput.BaseStream.CopyToAsync($outStream)
|
||||||
|
$errTask=$telemetry.StandardError.BaseStream.CopyToAsync($errStream)
|
||||||
|
}
|
||||||
& $PresentMonPath --process_id $target.Id --timed $Seconds --delay $DelaySeconds --terminate_after_timed --terminate_on_proc_exit --session_name $session --no_console_stats --no_track_input --qpc_time_ms --output_file $csv 2>&1 |
|
& $PresentMonPath --process_id $target.Id --timed $Seconds --delay $DelaySeconds --terminate_after_timed --terminate_on_proc_exit --session_name $session --no_console_stats --no_track_input --qpc_time_ms --output_file $csv 2>&1 |
|
||||||
Tee-Object -FilePath (Join-Path $OutputDirectory 'presentmon-console.log')
|
Tee-Object -FilePath (Join-Path $OutputDirectory 'presentmon-console.log')
|
||||||
if ($LASTEXITCODE) { throw "PresentMon failed with exit code $LASTEXITCODE; see presentmon-console.log." }
|
if ($LASTEXITCODE) { throw "PresentMon failed with exit code $LASTEXITCODE; see presentmon-console.log." }
|
||||||
|
} finally {
|
||||||
|
try { if ($telemetryStarted) {
|
||||||
|
$timedOut=$false
|
||||||
|
if (!$telemetry.HasExited -and !$telemetry.WaitForExit(30000)) { $timedOut=$true; $telemetry.Kill(); $telemetry.WaitForExit() }
|
||||||
|
if ($outTask) { [void]$outTask.GetAwaiter().GetResult() }
|
||||||
|
if ($errTask) { [void]$errTask.GetAwaiter().GetResult() }
|
||||||
|
$driverDll=Join-Path $env:WINDIR 'System32/amdadlx64.dll'
|
||||||
|
$driverInfo=$null
|
||||||
|
if (Test-Path -LiteralPath $driverDll) {
|
||||||
|
$driverInfo=@{path=$driverDll; version=(Get-Item -LiteralPath $driverDll).VersionInfo.FileVersion;
|
||||||
|
sha256=(Get-FileHash -LiteralPath $driverDll -Algorithm SHA256).Hash}
|
||||||
|
}
|
||||||
|
@{executable=$TelemetryPath; sha256=(Get-FileHash -LiteralPath $TelemetryPath -Algorithm SHA256).Hash;
|
||||||
|
exitCode=$telemetry.ExitCode; timeout=$timedOut; targetProcessId=$target.Id; gpuName=$GpuName;
|
||||||
|
intervalMs=500; driverDll=$driverInfo; readOnly=$true;
|
||||||
|
buildInfo=$(if (Test-Path -LiteralPath (Join-Path (Split-Path $TelemetryPath) 'build-info.json')) {
|
||||||
|
Get-Content -LiteralPath (Join-Path (Split-Path $TelemetryPath) 'build-info.json') -Raw | ConvertFrom-Json
|
||||||
|
})} |
|
||||||
|
ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $OutputDirectory 'gpu-telemetry-environment.json') -Encoding UTF8
|
||||||
|
if ($telemetry.ExitCode -ne 0) { Write-Warning 'GPU telemetry failed; retain PresentMon capture and see gpu-telemetry.stderr.log.' }
|
||||||
|
} } finally {
|
||||||
|
if ($telemetry) { $telemetry.Dispose() }
|
||||||
|
if ($outStream) { $outStream.Dispose() }
|
||||||
|
if ($errStream) { $errStream.Dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!(Test-Path -LiteralPath $csv) -or (Get-Item -LiteralPath $csv).Length -eq 0) { throw 'PresentMon captured no frames.' }
|
if (!(Test-Path -LiteralPath $csv) -or (Get-Item -LiteralPath $csv).Length -eq 0) { throw 'PresentMon captured no frames.' }
|
||||||
Write-Host "PresentMon capture: $csv"
|
Write-Host "PresentMon capture: $csv"
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
// Read-only AMD driver telemetry, separate from the game and HIP runtime.
|
||||||
|
// Uses the ADLX SDK's C interface; no SDK/sample implementation is vendored.
|
||||||
|
#include <windows.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <ADLX.h>
|
||||||
|
#include <IPerformanceMonitoring.h>
|
||||||
|
|
||||||
|
static int number(const char* text, unsigned* value) {
|
||||||
|
char* end = NULL;
|
||||||
|
errno = 0;
|
||||||
|
unsigned long parsed = strtoul(text, &end, 10);
|
||||||
|
if (!text[0] || *end || text[0] == '-' || errno == ERANGE) return 0;
|
||||||
|
*value = (unsigned)parsed;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void real_value(ADLX_RESULT status, double value) {
|
||||||
|
if (ADLX_SUCCEEDED(status) && isfinite(value) && value >= 0) printf("%.6f", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void int_value(ADLX_RESULT status, int value) {
|
||||||
|
if (ADLX_SUCCEEDED(status) && value >= 0) printf("%d", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
unsigned seconds = 30, interval = 500, target_pid = 0, delay = 0;
|
||||||
|
const char* gpu_name = "RX 9070 XT";
|
||||||
|
for (int i = 1; i < argc; ++i) {
|
||||||
|
if (i + 1 == argc) return 2;
|
||||||
|
const char* key = argv[i];
|
||||||
|
const char* value = argv[++i];
|
||||||
|
unsigned* dest = !strcmp(key, "--seconds") ? &seconds :
|
||||||
|
!strcmp(key, "--interval-ms") ? &interval :
|
||||||
|
!strcmp(key, "--target-pid") ? &target_pid :
|
||||||
|
!strcmp(key, "--delay-ms") ? &delay : NULL;
|
||||||
|
if (dest) { if (!number(value, dest)) return 2; }
|
||||||
|
else if (!strcmp(key, "--gpu-name")) gpu_name = value;
|
||||||
|
else return 2;
|
||||||
|
}
|
||||||
|
if (seconds < 1 || seconds > 300 || interval < 100 || interval > 5000 || delay > 300000) return 2;
|
||||||
|
int exit_code = 1, initialized = 0;
|
||||||
|
HMODULE dll = NULL;
|
||||||
|
HANDLE target = NULL;
|
||||||
|
IADLXSystem* system = NULL; // Owned by ADLXInitialize/ADLXTerminate.
|
||||||
|
IADLXGPUList* gpus = NULL;
|
||||||
|
IADLXGPU* selected = NULL;
|
||||||
|
IADLXPerformanceMonitoringServices* service = NULL;
|
||||||
|
ADLXTerminate_Fn terminate = NULL;
|
||||||
|
if (target_pid) {
|
||||||
|
target = OpenProcess(SYNCHRONIZE, FALSE, target_pid);
|
||||||
|
if (!target) { fprintf(stderr, "OpenProcess error=%lu\n", GetLastError()); goto done; }
|
||||||
|
}
|
||||||
|
// Load the installed driver library, never a DLL from the game/current dir.
|
||||||
|
dll = LoadLibraryExW(L"amdadlx64.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||||
|
if (!dll) { fprintf(stderr, "ADLX driver DLL unavailable error=%lu\n", GetLastError()); goto done; }
|
||||||
|
ADLXQueryFullVersion_Fn query_version = (ADLXQueryFullVersion_Fn)GetProcAddress(dll, ADLX_QUERY_FULL_VERSION_FUNCTION_NAME);
|
||||||
|
ADLXInitialize_Fn initialize = (ADLXInitialize_Fn)GetProcAddress(dll, ADLX_INIT_FUNCTION_NAME);
|
||||||
|
terminate = (ADLXTerminate_Fn)GetProcAddress(dll, ADLX_TERMINATE_FUNCTION_NAME);
|
||||||
|
adlx_uint64 runtime_version = 0;
|
||||||
|
if (!query_version || !initialize || !terminate || ADLX_FAILED(query_version(&runtime_version))) goto done;
|
||||||
|
// Only use the base interfaces. Request no version newer than either the
|
||||||
|
// installed runtime or these headers (e.g. SDK 2.0 with driver ADLX 1.5).
|
||||||
|
adlx_uint64 requested = runtime_version < ADLX_FULL_VERSION ? runtime_version : ADLX_FULL_VERSION;
|
||||||
|
ADLX_RESULT result = initialize(requested, &system);
|
||||||
|
fprintf(stderr, "ADLX runtime=%llu requested=%llu initialize=%d interval_ms=%u target_pid=%u\n",
|
||||||
|
(unsigned long long)runtime_version, (unsigned long long)requested, result, interval, target_pid);
|
||||||
|
if (ADLX_FAILED(result) || !system) goto done;
|
||||||
|
initialized = 1;
|
||||||
|
result = system->pVtbl->GetGPUs(system, &gpus);
|
||||||
|
if (ADLX_FAILED(result) || !gpus) goto done;
|
||||||
|
unsigned matches = 0;
|
||||||
|
for (adlx_uint i = gpus->pVtbl->Begin(gpus); i < gpus->pVtbl->End(gpus); ++i) {
|
||||||
|
IADLXGPU* gpu = NULL;
|
||||||
|
if (ADLX_FAILED(gpus->pVtbl->At_GPUList(gpus, i, &gpu)) || !gpu) goto done;
|
||||||
|
const char* name = NULL; const char* pnp = NULL; adlx_int id = -1;
|
||||||
|
gpu->pVtbl->Name(gpu, &name);
|
||||||
|
gpu->pVtbl->PNPString(gpu, &pnp);
|
||||||
|
gpu->pVtbl->UniqueId(gpu, &id);
|
||||||
|
fprintf(stderr, "ADLX GPU index=%u id=%d name=%s pnp=%s\n", i, id, name ? name : "unknown", pnp ? pnp : "unknown");
|
||||||
|
if (name && strstr(name, gpu_name)) {
|
||||||
|
++matches;
|
||||||
|
if (!selected) { selected = gpu; gpu = NULL; }
|
||||||
|
}
|
||||||
|
if (gpu) gpu->pVtbl->Release(gpu);
|
||||||
|
}
|
||||||
|
if (matches != 1) { fprintf(stderr, "Expected one GPU matching '%s', found %u\n", gpu_name, matches); goto done; }
|
||||||
|
result = system->pVtbl->GetPerformanceMonitoringServices(system, &service);
|
||||||
|
if (ADLX_FAILED(result) || !service) goto done;
|
||||||
|
LARGE_INTEGER frequency, qpc;
|
||||||
|
QueryPerformanceFrequency(&frequency);
|
||||||
|
// Preserve the driver's timestamp verbatim: the installed ADLX 1.5 reports
|
||||||
|
// uptime here despite SDK documentation describing epoch time. Use our
|
||||||
|
// independent QPC/UTC stamps for correlation with PresentMon.
|
||||||
|
printf("qpc_ms,host_unix_ms,sensor_timestamp_ms,gpu_usage_pct,gpu_clock_mhz,vram_clock_mhz,gpu_power_w,board_power_w,temperature_c,hotspot_c,poll_ms\n");
|
||||||
|
fflush(stdout);
|
||||||
|
if (delay) Sleep(delay);
|
||||||
|
ULONGLONG start = GetTickCount64();
|
||||||
|
unsigned samples = 0;
|
||||||
|
while (GetTickCount64() - start < (ULONGLONG)seconds * 1000) {
|
||||||
|
if (target && WaitForSingleObject(target, 0) == WAIT_OBJECT_0) break;
|
||||||
|
LARGE_INTEGER before, after;
|
||||||
|
QueryPerformanceCounter(&before);
|
||||||
|
IADLXGPUMetrics* metrics = NULL;
|
||||||
|
result = service->pVtbl->GetCurrentGPUMetrics(service, selected, &metrics);
|
||||||
|
QueryPerformanceCounter(&after);
|
||||||
|
if (ADLX_FAILED(result) || !metrics) { fprintf(stderr, "GetCurrentGPUMetrics result=%d\n", result); goto done; }
|
||||||
|
FILETIME time; ULARGE_INTEGER unix_time;
|
||||||
|
GetSystemTimePreciseAsFileTime(&time);
|
||||||
|
unix_time.LowPart = time.dwLowDateTime; unix_time.HighPart = time.dwHighDateTime;
|
||||||
|
QueryPerformanceCounter(&qpc);
|
||||||
|
adlx_int64 sensor_time = 0;
|
||||||
|
printf("%.6f,%llu,", qpc.QuadPart * 1000. / frequency.QuadPart,
|
||||||
|
(unsigned long long)((unix_time.QuadPart - 116444736000000000ULL) / 10000));
|
||||||
|
if (ADLX_SUCCEEDED(metrics->pVtbl->TimeStamp(metrics, &sensor_time))) printf("%lld", (long long)sensor_time);
|
||||||
|
double real = -1; int integer = -1;
|
||||||
|
printf(","); result = metrics->pVtbl->GPUUsage(metrics, &real); real_value(result, real);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUClockSpeed(metrics, &integer); int_value(result, integer);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUVRAMClockSpeed(metrics, &integer); int_value(result, integer);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUPower(metrics, &real); real_value(result, real);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUTotalBoardPower(metrics, &real); real_value(result, real);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUTemperature(metrics, &real); real_value(result, real);
|
||||||
|
printf(","); result = metrics->pVtbl->GPUHotspotTemperature(metrics, &real); real_value(result, real);
|
||||||
|
printf(",%.6f\n", (after.QuadPart - before.QuadPart) * 1000. / frequency.QuadPart);
|
||||||
|
metrics->pVtbl->Release(metrics);
|
||||||
|
++samples; fflush(stdout);
|
||||||
|
ULONGLONG elapsed = GetTickCount64() - start;
|
||||||
|
ULONGLONG next = (ULONGLONG)samples * interval;
|
||||||
|
if (next > elapsed) Sleep((DWORD)(next - elapsed));
|
||||||
|
}
|
||||||
|
fprintf(stderr, "ADLX collected=%u read_only=1\n", samples);
|
||||||
|
exit_code = samples ? 0 : 1;
|
||||||
|
done:
|
||||||
|
// All acquired interfaces must be released before unloading ADLX.
|
||||||
|
if (service) service->pVtbl->Release(service);
|
||||||
|
if (selected) selected->pVtbl->Release(selected);
|
||||||
|
if (gpus) gpus->pVtbl->Release(gpus);
|
||||||
|
if (initialized) {
|
||||||
|
ADLX_RESULT status = terminate();
|
||||||
|
fprintf(stderr, "ADLX terminate=%d\n", status);
|
||||||
|
if (ADLX_FAILED(status)) exit_code = 1;
|
||||||
|
}
|
||||||
|
if (dll) FreeLibrary(dll);
|
||||||
|
if (target) CloseHandle(target);
|
||||||
|
return exit_code;
|
||||||
|
}
|
||||||
@@ -76,6 +76,38 @@ def presentmon(directory):
|
|||||||
return captures
|
return captures
|
||||||
|
|
||||||
|
|
||||||
|
def gpu_telemetry(directory):
|
||||||
|
captures = []
|
||||||
|
for path in sorted(directory.glob('gpu-telemetry*.csv')):
|
||||||
|
unique = {}
|
||||||
|
with path.open(encoding='utf-8-sig', newline='') as stream:
|
||||||
|
rows = list(csv.DictReader(stream))
|
||||||
|
# Driver telemetry may repeat a cached measurement. Count polls but
|
||||||
|
# use each sensor timestamp once; missing/unsupported values stay absent.
|
||||||
|
for row in rows:
|
||||||
|
try:
|
||||||
|
timestamp = int(row.get('sensor_timestamp_ms', ''))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if timestamp > 0:
|
||||||
|
unique.setdefault(timestamp, row)
|
||||||
|
metrics = {}
|
||||||
|
for name in ('gpu_usage_pct', 'gpu_clock_mhz', 'vram_clock_mhz', 'gpu_power_w',
|
||||||
|
'board_power_w', 'temperature_c', 'hotspot_c', 'poll_ms'):
|
||||||
|
values = []
|
||||||
|
for row in unique.values():
|
||||||
|
try:
|
||||||
|
value = float(row.get(name, ''))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
if math.isfinite(value) and value >= 0 and (name != 'gpu_usage_pct' or value <= 100):
|
||||||
|
values.append(value)
|
||||||
|
if values:
|
||||||
|
metrics[name] = stats(values)
|
||||||
|
captures.append(dict(file=path.name, polls=len(rows), uniqueSamples=len(unique), metrics=metrics))
|
||||||
|
return captures
|
||||||
|
|
||||||
|
|
||||||
def report(directory, metadata_directory=None):
|
def report(directory, metadata_directory=None):
|
||||||
kernel_metadata = metadata(metadata_directory)
|
kernel_metadata = metadata(metadata_directory)
|
||||||
stages = collections.defaultdict(list)
|
stages = collections.defaultdict(list)
|
||||||
@@ -138,6 +170,7 @@ def report(directory, metadata_directory=None):
|
|||||||
invalidKernelSamples=invalid_samples,
|
invalidKernelSamples=invalid_samples,
|
||||||
gpuBoundaryStages={},
|
gpuBoundaryStages={},
|
||||||
presentMon=presentmon(directory),
|
presentMon=presentmon(directory),
|
||||||
|
gpuTelemetry=gpu_telemetry(directory),
|
||||||
replayPairs=[],
|
replayPairs=[],
|
||||||
kernels=[], notes=[
|
kernels=[], notes=[
|
||||||
'CPU stage times include waits and host work; they are not isolated GPU timings.',
|
'CPU stage times include waits and host work; they are not isolated GPU timings.',
|
||||||
@@ -145,6 +178,7 @@ def report(directory, metadata_directory=None):
|
|||||||
'D3D12 boundary timestamps span copies and HIP/scheduling across the external fence; they do not isolate kernel execution or measure Present.',
|
'D3D12 boundary timestamps span copies and HIP/scheduling across the external fence; they do not isolate kernel execution or measure Present.',
|
||||||
'Boundary timestamps are read after existing completion fences; only 32 timing bytes are read, with no extra completion wait. GPU clock idle behavior can affect intervals.',
|
'Boundary timestamps are read after existing completion fences; only 32 timing bytes are read, with no extra completion wait. GPU clock idle behavior can affect intervals.',
|
||||||
'PresentMon ETW GPU busy/wait are per-process frame estimates, not hardware sensor utilization. HWS and cross-API context attribution can affect them.',
|
'PresentMon ETW GPU busy/wait are per-process frame estimates, not hardware sensor utilization. HWS and cross-API context attribution can affect them.',
|
||||||
|
'ADLX telemetry is driver-reported device-wide utilization/clocks/power, sampled in a separate read-only process. It does not isolate DLSS or WMMA occupancy.',
|
||||||
'Command record intervals are between NGX recording calls on one thread, not Present/FPS.',
|
'Command record intervals are between NGX recording calls on one thread, not Present/FPS.',
|
||||||
'Hook estimates use random one-in-64 samples; driver timing covers generated forwarding methods only.',
|
'Hook estimates use random one-in-64 samples; driver timing covers generated forwarding methods only.',
|
||||||
'Hook access includes lock waits; summing threads does not measure serial frame latency or CPU execution time.',
|
'Hook access includes lock waits; summing threads does not measure serial frame latency or CPU execution time.',
|
||||||
@@ -218,7 +252,7 @@ def main():
|
|||||||
parser.add_argument('--metadata-directory', type=pathlib.Path, help='llvm-readobj --notes output for native code objects')
|
parser.add_argument('--metadata-directory', type=pathlib.Path, help='llvm-readobj --notes output for native code objects')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
result = report(args.directory, args.metadata_directory)
|
result = report(args.directory, args.metadata_directory)
|
||||||
if not any(result[key] for key in ('cpuStages', 'kernels', 'commandStages', 'commandHookSamples', 'replayPairs', 'cudaApi', 'gpuBoundaryStages', 'presentMon')):
|
if not any(result[key] for key in ('cpuStages', 'kernels', 'commandStages', 'commandHookSamples', 'replayPairs', 'cudaApi', 'gpuBoundaryStages', 'presentMon', 'gpuTelemetry')):
|
||||||
parser.error('no complete stage or HIP-event records found')
|
parser.error('no complete stage or HIP-event records found')
|
||||||
if args.output:
|
if args.output:
|
||||||
args.output.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
|
args.output.write_text(json.dumps(result, indent=2) + '\n', encoding='utf-8')
|
||||||
@@ -232,6 +266,10 @@ def main():
|
|||||||
print(f"PRESENT {capture['file']} process={capture['processId']} swapchain={capture['swapchain']} frames={capture['frames']} fps={capture.get('presentFps', float('nan')):.3f}")
|
print(f"PRESENT {capture['file']} process={capture['processId']} swapchain={capture['swapchain']} frames={capture['frames']} fps={capture.get('presentFps', float('nan')):.3f}")
|
||||||
for name, row in capture['metrics'].items():
|
for name, row in capture['metrics'].items():
|
||||||
print(f" {name}: n={row['samples']} mean={row['mean']:.3f} ms median={row['median']:.3f} ms p95={row['p95']:.3f} ms")
|
print(f" {name}: n={row['samples']} mean={row['mean']:.3f} ms median={row['median']:.3f} ms p95={row['p95']:.3f} ms")
|
||||||
|
for capture in result['gpuTelemetry']:
|
||||||
|
print(f"ADLX {capture['file']}: polls={capture['polls']} unique_samples={capture['uniqueSamples']}")
|
||||||
|
for name, row in capture['metrics'].items():
|
||||||
|
print(f" {name}: n={row['samples']} mean={row['mean']:.3f} median={row['median']:.3f} p95={row['p95']:.3f}")
|
||||||
for row in result['cudaApi'][:20]:
|
for row in result['cudaApi'][:20]:
|
||||||
print(f"CUDA API {row['api']} {row['thread']}: n={row['calls']} mean={row['meanMs']:.6f} ms total={row['totalMs']:.3f} ms max={row['maxMs']:.3f} ms")
|
print(f"CUDA API {row['api']} {row['thread']}: n={row['calls']} mean={row['meanMs']:.6f} ms total={row['totalMs']:.3f} ms max={row['maxMs']:.3f} ms")
|
||||||
for row in result['commandHookSamples']:
|
for row in result['commandHookSamples']:
|
||||||
|
|||||||
Reference in new issue
Block a user