Repository navigation
[rocr-runtime] Make the DXG topology snapshot safe for multiple consumers - #10034
Conversation
✅ All Policy Checks Passed
📖 Need help? See the Policy FAQ for details on every check and how to fix failures. |
… engine NumArrays is the number of shader arrays in one shader engine, so the whole GPU has NumShaderBanks * NumArrays of them. Dividing the compute unit count by NumArrays alone over-reports NumCUPerArray by the shader engine count: a 128 CU part with 4 shader engines and 2 arrays each was reported as 64 CU per array instead of 16. Divide by the total number of arrays instead, which is the relation KFD reports on bare metal. Fold the existing zero check into the same guard so a zero shader engine count cannot divide by zero either, and warn when the CU count is not a multiple of the array count, since the result is truncated there.
KFDGpuID was filled in from the PCI device id. The rest of topology.cpp uses it both as the "this node is a GPU" predicate and as the key that gpuid_to_nodeid() and get_device_id_by_gpu_id() invert, and a PCI device id satisfies neither: two identical GPUs in one machine share one, so both nodes claim the same id and every reverse lookup resolves to the first of them. Use the node ordinal instead. It is unique by construction and stable for the life of the snapshot, which is the only scope these lookups span, and it is never handed to the KMD so it does not have to match any driver-side id. Bias it by one so a GPU can never land on the reserved zero.
ROCr and the DXG thunk copy HsaNodeProperties across their interface without a size parameter, so a disagreement between the two builds corrupts the heap instead of failing the load, and nothing detects it. Add DxgAbiCheck, which takes the sizes the caller was built with and answers HSAKMT_STATUS_DRIVER_MISMATCH when one differs. ROCr resolves it with dlsym(), so a thunk built before the handshake existed reads as an older thunk rather than as a failure. Install hsakmt.h and hsakmttypes.h alongside the library. This is the only configuration that builds librocdxg and it is mutually exclusive with the libhsakmt branch that installs include/hsakmt wholesale, so without a rule here the dev package ships a pkg-config file naming an empty include directory. Add tests that compile both headers out of a staged copy of exactly that list, and that read the built library for the export ROCr will look up, then run them in the WSL job.
22c3bdd to
7609f04
Compare
cfreeamd
left a comment
There was a problem hiding this comment.
🤖 AI-assisted pre-review (for @cfreeamd)
Reviewed projects/rocr-runtime/ only. The lifecycle core is solid: SnapshotRefcount is balanced/ABA-free, KfdLifecycle teardown is correctly ordered and idempotent, and the Runtime::Load/AMD::Load scope guards leave no leak or double-free window — all well covered by the new standalone unit tests. Two correctness concerns and a wiring question are inline; the license-header and typo notes are nits.
7609f04 to
a993bb5
Compare
hsaKmtReleaseSystemProperties() tore the snapshot down unconditionally, so with two consumers in one process the first release freed the HsaNodeProperties the second was still reading. ROCr and rocprofiler-sdk are exactly that pair. Count the outstanding hsaKmtAcquireSystemProperties() references and drop the snapshot only when the last one goes. Reject an unmatched release rather than tearing down a snapshot somebody still owns, and unwind a snapshot that failed to build so a retry starts from the state the failed call found rather than on top of live device objects. Drop whatever is still standing at the last hsaKmtCloseKFD(): the snapshot names WDDMDevice objects belonging to the dxcore session that close is about to end, and a later reopen would hand those pointers back out. A fork child disowns its inherited references instead, because destroying them would issue D3DKMT calls against the parent's handles.
librocdxg.pc derived its prefix by walking three directories up from
${pcfiledir}, which is one too many: the file is installed to
<prefix>/lib/pkgconfig, so three levels up lands above the prefix
entirely. Every path the file emits was wrong by one level, and the
comment explaining the choice described borrowing libhsakmt's headers,
which this configuration does not build.
Walk up two instead, so includedir resolves to the directory the header
install rule writes hsakmt/ into, and say in the comment what actually
populates it. Nothing else does: libhsakmt is not built in this
configuration, so this package ships those headers or nobody does.
LoadThunkApiTable() returned void. Binding stops at the first entry point the shared thunk does not export, so everything after it in the table keeps whatever its member happened to hold and Load() carries on regardless. The first call through one of those slots is a fault the runtime has no way to attribute, and an installed thunk older than the runtime loading it is an ordinary configuration rather than a corrupt one. Report whether the table was fully bound and abandon the load when it was not. Give every pointer in the table a nullptr initializer as well, so a slot binding never reached holds a null that faults naming the function called rather than an indeterminate value that jumps somewhere arbitrary.
DestroyThunkInstance() looked DtifDestroy up and called it whenever it resolved, with no regard for whether an instance had ever been created. The rollback for a failed load runs it too, so a load that failed before CreateThunkInstance() destroyed an instance it never took. The loader also accepted a thunk exporting DtifCreate but not DtifDestroy, handing over an instance nothing in the process can give back, and only found that out during its own unwind with the library about to be closed. Resolve both halves before calling either and refuse a thunk that does not export both, the rule ImportExternalSemaphore() already applies to the other optional pair in the table. Record the destroy function as the instance is taken and release through that, clearing it before the call so a failed destroy cannot be retried into a second release. Owning nothing is then plain success, which is what the rollback path needs.
Acquire() keeps the singleton after a failed Load() and only drops the reference count, so Unload() never runs and everything Load() took before the failure stays. The next hsa_init() finds the drivers still registered and the agents still published, and because both are published by appending, it doubles them and re-runs Init() on a driver this process has already torn down. agents_by_gpuid_ likewise held pointers to agents DestroyAgents() had freed. Unwind on the failure path with scope guards that give back the topology, the loader, the extensions and the thunk loader in the reverse of the order Load() acquires them, sharing that teardown with Unload() so the two cannot drift. Every step tolerates the half-built state a failed load leaves behind and is safe to run more than once. Put the KFD open and the topology snapshot behind KfdLifecycle, which owns them as a pair and gives them back together, and hold the thunk loader by unique_ptr so a second attempt cannot overwrite the first and leak the shared thunk it keeps open.
a993bb5 to
ed34ecd
Compare
cfreeamd
left a comment
There was a problem hiding this comment.
🤖 AI-assisted pre-review (for @cfreeamd) — follow-up after 2026-08-15 author updates
Thank you for the extensive rework. Several prior findings are now resolved:
- [Important] #2 (abi.cpp compile) — resolved: new CI workflow + build-system-provided include paths confirm compilation succeeds.
- [Question] #3 (DxgAbiCheck not wired) — resolved:
DxgAbiCheckis now declared inhsakmt.h, exported inlibrocdxg.ver, and per the test comments ininstalled_header_smoke.cpp('Filled the way ThunkLoader::CheckThunkAbi() fills it'),CheckThunkAbi()now calls it. - [Nit] #5 ("Snashot" typo) — resolved: comment replaced entirely.
[Important] #1 (device-handle refcount) — clarification needed. See inline comment on openclose.cpp.
This is already handled at the current tip. The KERNEL_ALREADY_OPENED branch increments dxg_open_count at openclose.cpp:729 before returning, so the two-consumer sequence is 0→1→2, then ROCr shutdown drops it to 1 without running last-close teardown. The second consumer increment predates this PR and appears as an unchanged context line, which may be why it was missed in the diff. The substantive change in this PR was moving reset_suballocator() into the 0→1 branch. |
cfreeamd
left a comment
There was a problem hiding this comment.
🤖 AI-assisted pre-review (for @cfreeamd) — follow-up after author reply
Thanks for the detailed reply. You're right, and I've withdrawn the prior [Important] dxg_open_count finding. I fetched the full openclose.cpp at the current tip and confirmed the KERNEL_ALREADY_OPENED else branch does increment the count:
} else {
dxg_runtime->dxg_open_count++; // openclose.cpp:729
result = HSAKMT_STATUS_KERNEL_ALREADY_OPENED;
}That line lives in the gap between the two diff hunks (hunk 1 ends at new line ~717, hunk 2 starts at new line 730), so it never appeared in the changeset context — which is why it read as one-sided across the earlier passes. With both open paths incrementing and hsaKmtCloseKFD() gating last-close teardown on --dxg_open_count == 0, the refcount is balanced: in the two-consumer WSL case (rocprofiler-sdk 0->1, ROCr 1->2 ALREADY_OPENED, ROCr shutdown 2->1) the DXCore session correctly stays alive under the live consumer. No remaining blockers from my side — prior findings #2/#3 are moot after the DxgAbiCheck removal, and the ABI concern is handled cleanly via the forward declarations + installed_header_smoke tests.
Flora Cui noted on review that the hsakmt public headers are already installed by rocr-runtime, and asked whether a second copy shipped from the WSL build could put rocprofiler-sdk and rocr-runtime on mismatched headers now that both live in one repo. Both points land on the same defect, and develop already said so: the comment this rule displaced explains that no separate header installation belongs here and that duplicating it breaks TheRock's artifact uniqueness check (ROCm/TheRock#7005). include/hsakmt is installed by the else() branch of if (WIN_SDK) in libhsakmt/CMakeLists.txt, which the native package runs, and in a full ROCm install both copies land in the same prefix. Restore develop's comment verbatim so the rule is not re-added, and drop the installed_header_smoke targets with it. They only checked that the installed header set compiles standalone, so with no install rule they have no subject. dxg-snapshot-refcount and dxg-dynamic-exports still build and pass. Keep the hsakmt.h forward declarations. They are not test scaffolding: src/extsem.c includes only <hsakmt/hsakmt.h>, and without them GCC scopes struct kfd_runtime_info and struct kfd_dbg_device_info_entry to the prototypes that name them. linux/kfd_ioctl.h defines both and is excluded from the dev package, so these declarations are what make those prototypes well formed for any C consumer.
Thanks, that diagnosis was helpful. The missing increment was in the unchanged context between the two hunks, so I should have made that clearer in the description. |
Teardown previously stopped at the first failing step, so a failed topology- snapshot release could leave the DXCore session open. Complete every owned cleanup step while retaining the first error. Name the missing topology acquire when the debug-support query cannot inspect devices. Narrow the atfork comment because the pre-existing handler also writes a bitfield and reconstructs a mutex, so it does not follow the plain-store-only discipline previously claimed.
cfreeamd
left a comment
There was a problem hiding this comment.
🤖 AI-assisted pre-review (for @cfreeamd) — follow-up after 2026-08-27 commit
Reviewed the new commit fix(rocr-runtime): finish DXG cleanup after teardown errors (2026-08-27T22:45:01Z). This commit completes the teardown correctness work:
topology_drop_snapshot_at_last_close()ensures the snapshot cannot outlive the DXCore session that backs it; called beforeHeapFini()inhsaKmtCloseKFD()so WDDMDevice teardown still has a live session.- Lock in
hsaKmtOpenKFD()moved afterclear_after_fork(): correct becauseclear_after_fork()replaces*dxg_runtime, so a lock_guard on the old object would unlock freed memory. Fork path is single-threaded so the unlocked check is safe. reset_suballocator()moved inside the 0→1 branch only: second consumer's ALREADY_OPENED open no longer resets the first consumer's live allocations.AcquireTopologySnapshot()inKfdDriveris idempotent (topology_snapshot_acquired_guard), so repeatedGetSystemProperties()calls don't accumulate unbalanced thunk snapshot references.ShutDown()runs disable→release→close regardless of intermediate failures, reporting the first error instead of short-circuiting on it.Init()uses named scope guards (snapshot_guard,runtime_guard) for correct unwind on early failure; dismissed in order on success.- Fork child cleanup:
child_fork_handler()now zeroesdxg_open_countand clears snapshot refs before any entry point can act on them;clear_after_fork()callstopology_abandon_after_fork()to discard parent WDDMDevice references without deleting them.
All paths verified against the diff. No new concerns. Verdict remains approve.
The class wrapped a plain uint32_t behind five one-line methods and a three-way enum, which is more type than the job needs. Replace it with a std::atomic<uint32_t> member on _topology_props and inline the two things the class actually carried: the assignment rather than an increment when a snapshot is first published, and the zero check that stops an unmatched release from wrapping the count to UINT32_MAX and stranding the snapshot for the life of the process. Note that fetch_sub returns the value from before the subtraction, so the last-reference test reads == 1. Every access uses memory_order_relaxed, because hsakmt_mutex is what actually orders them: the teardown a final release triggers frees g_system, clears g_props and deletes the WDDMDevice objects, so that path holds the lock however the counter is typed. The member comment records this, since the guard and the decrement are now two operations rather than one call and the atomic on its own no longer makes the field safe to touch unlocked. Removing the class removes its unit test, and with it the last consumer of tests/unit_test_harness.h, so both go as well. The harness belongs with the lifecycle tests that are the first to actually need it. The fork-child handler comment in openclose.cpp is corrected to match: the snapshot count is now a relaxed store on a lock-free atomic rather than a plain store, which is still free of allocation and locking. topology.hpp is written back as uniformly CRLF. The commit that added snapshot_refcount.hpp had given its new lines bare LF endings, the only such lines in an otherwise CRLF file.
ce517b6 to
186e0b7
Compare
cfreeamd
left a comment
There was a problem hiding this comment.
🤖 AI-assisted pre-review (for @cfreeamd) — follow-up after 2026-08-31 refactor
refactor(libhsakmt): replace SnapshotRefcount with std::atomic (2026-08-31) assessed.
The std::atomic<uint32_t> replacement is correct. All prior SnapshotRefcount invariants are preserved:
- Unbalanced-release check (
count == 0→pr_err+ early return beforefetch_sub): preserved. - Last-reference teardown (
fetch_sub(1, relaxed) == 1 → topology_drop_snapshot()): correct. - Fork-child zeroing (
topology_clear_snapshot_refs()→ relaxed store): async-signal-safe.
Relaxed ordering at all call sites is appropriate — hsakmt_mutex provides the required acquire/release semantics for the surrounding data structures; the atomic prevents UB on the counter variable itself.
The lock_guard move (constructed after is_forked_child() check instead of before) is a genuine correctness fix: clear_after_fork() replaces *dxg_runtime, so a pre-fork lock_guard bound to the old object would call unlock() on freed memory when it went out of scope.
Prior cfreeamd APPROVED (2026-08-28) is confirmed valid for the current head.
jbonnell-amd
left a comment
There was a problem hiding this comment.
Approved for the .github/workflows changes
|
Will merge given code owner approval.
|
TheRock Submodule Bump ActivityNewest first
|
projects/rocr-runtime/tests/unit_test_harness.h has no consumer left. 565fcfa re-added it on this branch - #10034 had deleted it once the refcount test stopped using it - on the stated grounds that tests/dxg/kfd_lifecycle_test.cpp still needed it. 59f85ba deleted that test when it folded KfdLifecycle into KfdDriver, which left the header as the only thing this branch still adds under tests/. A repo-wide grep for unit_test_harness now matches nothing at all, not even an include: the header uses #pragma once, so its own text does not contain the name. No CMakeLists mentions it and no add_subdirectory() pulls in projects/rocr-runtime/tests, so nothing built or installed it. Deleting it here rather than leaving it to #10034 cannot cause a merge conflict, because #10034 already merged as 80763b5 and its removal of this header is an ancestor of this branch. origin/develop has no projects/rocr-runtime/tests directory, so this restores develop's state instead of duplicating another PR's deletion. Leaving the file would mean this branch resurrects a header develop had already retired. KfdDriver::ShutDown()'s record() lambda is deliberately left alone. Its stage parameter is genuinely unused once NDEBUG reduces debug_print to do {} while (false), but hsa-runtime64 is compiled with -Wall and without -Wextra, -Wunused-parameter or -Werror (core/util/utils.h, cmake_modules/hsa_common.cmake, runtime/hsa-runtime/CMakeLists.txt), and -Wunused-parameter is not implied by -Wall, so no build configuration defined in this tree diagnoses it. Adding a suppression would be dead weight. Nothing here was compiled or run - this machine has no compiler or CMake, and a deleted header and an unchanged lambda are not testable without one. Every claim above comes from reading the CMake flag chain and grepping the tree.
…7016) ## Motivation Enable `rocprofv3` counter collection and profiling on WSL2 for gfx11-class GPUs. On WSL, the complete KMT topology is read up front through `librocdxg`, using the same thunk interface as the HSA runtime. An agent is published only after all required topology information has been collected, so the published record remains stable for its lifetime. As a result, `rocprofiler_query_available_agents()` returns consistent agent information regardless of when it is called, including before the HSA runtime has initialized. This is required by `rocprofv3-avail` and counter metric lookup. Native Linux continues to use the existing `gnulinux` path. `select_platform()` (`agent.cpp:355`) checks `gnulinux::is_available()` first, which succeeds whenever the KFD sysfs nodes directory is present, so the WSL path is not selected on a normal Linux system. There are two ungated changes that also affect native Linux. Both are fixes and are called out under **Native-Linux impact** below. This work builds on [[#6399](#6399)], which introduced the platform dispatch structure used here. `platform_dispatch.cpp` and its six tests came from that PR (`d2f0b27ad5`) and are therefore listed as pre-existing under **Tests**. This PR adds WSL as another implementation behind the existing `select_platform()` interface. ## Dependency on [#10034](#10034) [#10034](#10034) contains the libhsakmt and ROCr prerequisites for this work. There is no CI gate, submodule pin, or build-time dependency that enforces merge order, so the two PRs can technically merge in either order. Their changes to `core/runtime/amd_topology.cpp` also do not overlap. However, two parts of this PR depend on #10034 before they are fully functional. ### 1. WSL enumeration is disabled in a stock build until the required hsakmt headers are installed WSL enumeration is controlled by the CMake probe `rocprofiler_sdk_dxg_topology_supported()` (`cmake/Modules/rocprofiler-sdk-utilities.cmake:120`). The probe requires the hsakmt headers used by the build to provide: * `HsaStructureSizes` * `HsaNodeProperties::LuidLowPart` * `HsaNodeProperties::LuidHighPart` * `OverrideEngineId` Released ROCm headers, including 7.2.4, do not provide all of these. When the probe fails, `agent_unsupported.cpp` is built instead of the WSL enumerator, `platform::wsl::is_available()` returns false, and `enumerate()` returns an empty result. The copies of `hsakmttypes.h` in this repository already contain the required definitions (lines 359-360 and 1578), but they are not currently installed. #10034 adds the WSL header installation rules in `libhsakmt/CMakeLists_wsl.txt` through `ROCDXG_PUBLIC_HEADERS`. Until that lands, a stock build of this PR will not enable WSL enumeration. ### 2. The DXG topology snapshot is not reference-counted on current `develop` Current `develop` unconditionally calls `topology_drop_snapshot()` from `hsaKmtReleaseSystemProperties()` (`libhsakmt/src/dxg/topology.cpp:417`). This frees the global topology snapshot and destroys its `WDDMDevice` objects even if another consumer still has the topology acquired. #10034 adds `src/dxg/snapshot_refcount.hpp` and makes snapshot destruction conditional on the reference count. The normal `rocprofv3` path is not affected because the SDK topology read happens from `ROCPROFILER_LIBRARY_CTOR` and completes before `main()`, and therefore before the application's `hsa_init()`. `rocprof-attach` is different because it injects into an already-running process. In that case there is no ordering guarantee between the SDK and the existing HSA runtime consumer, so late attach can fail until the reference-counting change from #10034 is present. ## Technical Details ### Agent enumeration (WSL) * `platform/wsl/dxg_topology.cpp` and `dxg_thunk.hpp` read KMT topology through `librocdxg` using: * `hsaKmtOpenKFD()` * `hsaKmtAcquireSystemProperties()` * `hsaKmtGetNodeProperties()` * `hsaKmtReleaseSystemProperties()` * `hsaKmtCloseKFD()` The symbols are resolved with `dlsym`. `DxgThunk::complete()` verifies that all five are available before any are called. * `HsaNodeProperties` has no size parameter, so the destination buffer includes `kNodePropsSlack = 128` bytes of additional space. * `platform/wsl/agent.cpp` matches DXG adapters to HSA nodes by LUID, with device ID as a fallback. Enumeration is allowed to be partial: an unmatched adapter is skipped with a diagnostic while successfully matched GPUs are still published. * Memory banks, caches, and IO links are not currently published by the WSL path. * If the thunk reports no GPU node, no GPU agent is published. A warning is emitted and the application continues. * `agent_mapping.{hpp,cpp}` adds a WSL-specific mapping policy. Native Linux keeps the existing strict behavior (`agent.cpp:755`), while WSL logs mismatches, keeps successfully paired GPUs, and continues. ### Counters * `counters/controller.cpp` emits a one-time warning that WSL counters are collected through the DXG vendor-packet path rather than the KFD profiler ioctl. * `WSLKMT_VENDOR_PACKET=1` is now set from `wsl::enumerate()` instead of `is_available()`. This matters when using `ROCPROFILER_FORCE_PLATFORM=wsl`, where the old placement was bypassed. Without the environment variable, the DXG path drops aqlprofile's vendor-specific PM4 IB and counter values are returned as zero. * `basic_counters.xml` and `derived_counters.xml` each gain an empty `<gfx1150 base="gfx11">` section to keep the installed metric files consistent. These XML files are not used by the runtime metric loader. Runtime metrics come from `config.yaml` (`counters/metrics.cpp:348`), which already supports gfx1150 and is unchanged by this PR. ### KFD * `kfd::init()` now probes for a usable KFD device. If `/dev/kfd` is unavailable or cannot be opened, initialization continues with a warning instead of aborting in `kfd_device_fd()`. ## Native-Linux impact Two changes are not WSL-gated and also apply to native Linux: * `aql/helpers.cpp:51`: `get_query_info()` returns an empty ID query for metrics that do not have a hardware block. Constant and topology metrics such as `simd_count` have an empty block name. Passing that empty name to `aqlprofile_get_pmc_info()` can dereference a null pointer and abort. * `kfd/kfd.cpp:1586`: the KFD availability check is unconditional. A system without a usable `/dev/kfd`, for example because the driver is not loaded or access is denied, now receives a warning and continues instead of aborting. Other rocprofiler services are unaffected. All other WSL-specific behavior remains isolated from native Linux. The DXG implementation requires `/dev/dxg` and `libdxcore.so`, and the PM4 command-buffer bounds change is limited to libhsakmt's DXG/WDDM submission path. ## Tests * **44 new tests** * 14 `wsl_dxg_topology` tests * 16 `wsl_dxg_thunk` tests * 10 `agent_mapping` tests * 4 `parse_gfx_target_version` tests The first two groups are in new executables. The mapping and target-version tests are added to the existing `rocprofiler-sdk-lib-tests`. None of these tests requires a GPU, HSA runtime, or real DXG thunk. The DXG tests use a fake thunk and fake dynamic loader. The 30 DXG-specific tests use the same CMake capability probe as the WSL enumerator, so they are only built when the required hsakmt interface is available. In a stock configuration, 14 of the 44 new tests are built. * `platform-dispatch-tests` and its six tests are pre-existing from #6399. * `rocprofv3-avail` * Three tests are moved out of the PC-sampling gate and are now enabled on Navi. * The L2 hit counter is selected by GPU family instead of being hardcoded. These tests have not previously run in Navi CI, so they may expose existing Navi-specific issues. * `hsa_barrier` * Skip when no GPU agent is available instead of dereferencing an empty agent map. * `agent` * `agent_visibility_multigpu` performs its `substr(1)` calls after the no-GPU skip, avoiding `std::out_of_range` when no agents are present. * `grid_max_size` tolerates the difference between rocprofiler's hardcoded `INT32_MAX` value and the value reported by the installed ROCr runtime. ## JIRA ID JIRA ID: ROCM-23912 ## Test Plan ## Test Result ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. --------- Co-authored-by: Mika Laitio <mika.laitio@amd.com>
…7016) ## Motivation Enable `rocprofv3` counter collection and profiling on WSL2 for gfx11-class GPUs. On WSL, the complete KMT topology is read up front through `librocdxg`, using the same thunk interface as the HSA runtime. An agent is published only after all required topology information has been collected, so the published record remains stable for its lifetime. As a result, `rocprofiler_query_available_agents()` returns consistent agent information regardless of when it is called, including before the HSA runtime has initialized. This is required by `rocprofv3-avail` and counter metric lookup. Native Linux continues to use the existing `gnulinux` path. `select_platform()` (`agent.cpp:355`) checks `gnulinux::is_available()` first, which succeeds whenever the KFD sysfs nodes directory is present, so the WSL path is not selected on a normal Linux system. There are two ungated changes that also affect native Linux. Both are fixes and are called out under **Native-Linux impact** below. This work builds on [[#6399](#6399)], which introduced the platform dispatch structure used here. `platform_dispatch.cpp` and its six tests came from that PR (`d2f0b27ad5`) and are therefore listed as pre-existing under **Tests**. This PR adds WSL as another implementation behind the existing `select_platform()` interface. ## Dependency on [#10034](#10034) [#10034](#10034) contains the libhsakmt and ROCr prerequisites for this work. There is no CI gate, submodule pin, or build-time dependency that enforces merge order, so the two PRs can technically merge in either order. Their changes to `core/runtime/amd_topology.cpp` also do not overlap. However, two parts of this PR depend on #10034 before they are fully functional. ### 1. WSL enumeration is disabled in a stock build until the required hsakmt headers are installed WSL enumeration is controlled by the CMake probe `rocprofiler_sdk_dxg_topology_supported()` (`cmake/Modules/rocprofiler-sdk-utilities.cmake:120`). The probe requires the hsakmt headers used by the build to provide: * `HsaStructureSizes` * `HsaNodeProperties::LuidLowPart` * `HsaNodeProperties::LuidHighPart` * `OverrideEngineId` Released ROCm headers, including 7.2.4, do not provide all of these. When the probe fails, `agent_unsupported.cpp` is built instead of the WSL enumerator, `platform::wsl::is_available()` returns false, and `enumerate()` returns an empty result. The copies of `hsakmttypes.h` in this repository already contain the required definitions (lines 359-360 and 1578), but they are not currently installed. #10034 adds the WSL header installation rules in `libhsakmt/CMakeLists_wsl.txt` through `ROCDXG_PUBLIC_HEADERS`. Until that lands, a stock build of this PR will not enable WSL enumeration. ### 2. The DXG topology snapshot is not reference-counted on current `develop` Current `develop` unconditionally calls `topology_drop_snapshot()` from `hsaKmtReleaseSystemProperties()` (`libhsakmt/src/dxg/topology.cpp:417`). This frees the global topology snapshot and destroys its `WDDMDevice` objects even if another consumer still has the topology acquired. #10034 adds `src/dxg/snapshot_refcount.hpp` and makes snapshot destruction conditional on the reference count. The normal `rocprofv3` path is not affected because the SDK topology read happens from `ROCPROFILER_LIBRARY_CTOR` and completes before `main()`, and therefore before the application's `hsa_init()`. `rocprof-attach` is different because it injects into an already-running process. In that case there is no ordering guarantee between the SDK and the existing HSA runtime consumer, so late attach can fail until the reference-counting change from #10034 is present. ## Technical Details ### Agent enumeration (WSL) * `platform/wsl/dxg_topology.cpp` and `dxg_thunk.hpp` read KMT topology through `librocdxg` using: * `hsaKmtOpenKFD()` * `hsaKmtAcquireSystemProperties()` * `hsaKmtGetNodeProperties()` * `hsaKmtReleaseSystemProperties()` * `hsaKmtCloseKFD()` The symbols are resolved with `dlsym`. `DxgThunk::complete()` verifies that all five are available before any are called. * `HsaNodeProperties` has no size parameter, so the destination buffer includes `kNodePropsSlack = 128` bytes of additional space. * `platform/wsl/agent.cpp` matches DXG adapters to HSA nodes by LUID, with device ID as a fallback. Enumeration is allowed to be partial: an unmatched adapter is skipped with a diagnostic while successfully matched GPUs are still published. * Memory banks, caches, and IO links are not currently published by the WSL path. * If the thunk reports no GPU node, no GPU agent is published. A warning is emitted and the application continues. * `agent_mapping.{hpp,cpp}` adds a WSL-specific mapping policy. Native Linux keeps the existing strict behavior (`agent.cpp:755`), while WSL logs mismatches, keeps successfully paired GPUs, and continues. ### Counters * `counters/controller.cpp` emits a one-time warning that WSL counters are collected through the DXG vendor-packet path rather than the KFD profiler ioctl. * `WSLKMT_VENDOR_PACKET=1` is now set from `wsl::enumerate()` instead of `is_available()`. This matters when using `ROCPROFILER_FORCE_PLATFORM=wsl`, where the old placement was bypassed. Without the environment variable, the DXG path drops aqlprofile's vendor-specific PM4 IB and counter values are returned as zero. * `basic_counters.xml` and `derived_counters.xml` each gain an empty `<gfx1150 base="gfx11">` section to keep the installed metric files consistent. These XML files are not used by the runtime metric loader. Runtime metrics come from `config.yaml` (`counters/metrics.cpp:348`), which already supports gfx1150 and is unchanged by this PR. ### KFD * `kfd::init()` now probes for a usable KFD device. If `/dev/kfd` is unavailable or cannot be opened, initialization continues with a warning instead of aborting in `kfd_device_fd()`. ## Native-Linux impact Two changes are not WSL-gated and also apply to native Linux: * `aql/helpers.cpp:51`: `get_query_info()` returns an empty ID query for metrics that do not have a hardware block. Constant and topology metrics such as `simd_count` have an empty block name. Passing that empty name to `aqlprofile_get_pmc_info()` can dereference a null pointer and abort. * `kfd/kfd.cpp:1586`: the KFD availability check is unconditional. A system without a usable `/dev/kfd`, for example because the driver is not loaded or access is denied, now receives a warning and continues instead of aborting. Other rocprofiler services are unaffected. All other WSL-specific behavior remains isolated from native Linux. The DXG implementation requires `/dev/dxg` and `libdxcore.so`, and the PM4 command-buffer bounds change is limited to libhsakmt's DXG/WDDM submission path. ## Tests * **44 new tests** * 14 `wsl_dxg_topology` tests * 16 `wsl_dxg_thunk` tests * 10 `agent_mapping` tests * 4 `parse_gfx_target_version` tests The first two groups are in new executables. The mapping and target-version tests are added to the existing `rocprofiler-sdk-lib-tests`. None of these tests requires a GPU, HSA runtime, or real DXG thunk. The DXG tests use a fake thunk and fake dynamic loader. The 30 DXG-specific tests use the same CMake capability probe as the WSL enumerator, so they are only built when the required hsakmt interface is available. In a stock configuration, 14 of the 44 new tests are built. * `platform-dispatch-tests` and its six tests are pre-existing from #6399. * `rocprofv3-avail` * Three tests are moved out of the PC-sampling gate and are now enabled on Navi. * The L2 hit counter is selected by GPU family instead of being hardcoded. These tests have not previously run in Navi CI, so they may expose existing Navi-specific issues. * `hsa_barrier` * Skip when no GPU agent is available instead of dereferencing an empty agent map. * `agent` * `agent_visibility_multigpu` performs its `substr(1)` calls after the no-GPU skip, avoiding `std::out_of_range` when no agents are present. * `grid_max_size` tolerates the difference between rocprofiler's hardcoded `INT32_MAX` value and the value reported by the installed ROCr runtime. ## JIRA ID JIRA ID: ROCM-23912 ## Test Plan ## Test Result ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. --------- Co-authored-by: Mika Laitio <mika.laitio@amd.com>
…7016) ## Motivation Enable `rocprofv3` counter collection and profiling on WSL2 for gfx11-class GPUs. On WSL, the complete KMT topology is read up front through `librocdxg`, using the same thunk interface as the HSA runtime. An agent is published only after all required topology information has been collected, so the published record remains stable for its lifetime. As a result, `rocprofiler_query_available_agents()` returns consistent agent information regardless of when it is called, including before the HSA runtime has initialized. This is required by `rocprofv3-avail` and counter metric lookup. Native Linux continues to use the existing `gnulinux` path. `select_platform()` (`agent.cpp:355`) checks `gnulinux::is_available()` first, which succeeds whenever the KFD sysfs nodes directory is present, so the WSL path is not selected on a normal Linux system. There are two ungated changes that also affect native Linux. Both are fixes and are called out under **Native-Linux impact** below. This work builds on [[#6399](#6399)], which introduced the platform dispatch structure used here. `platform_dispatch.cpp` and its six tests came from that PR (`d2f0b27ad5`) and are therefore listed as pre-existing under **Tests**. This PR adds WSL as another implementation behind the existing `select_platform()` interface. ## Dependency on [#10034](#10034) [#10034](#10034) contains the libhsakmt and ROCr prerequisites for this work. There is no CI gate, submodule pin, or build-time dependency that enforces merge order, so the two PRs can technically merge in either order. Their changes to `core/runtime/amd_topology.cpp` also do not overlap. However, two parts of this PR depend on #10034 before they are fully functional. ### 1. WSL enumeration is disabled in a stock build until the required hsakmt headers are installed WSL enumeration is controlled by the CMake probe `rocprofiler_sdk_dxg_topology_supported()` (`cmake/Modules/rocprofiler-sdk-utilities.cmake:120`). The probe requires the hsakmt headers used by the build to provide: * `HsaStructureSizes` * `HsaNodeProperties::LuidLowPart` * `HsaNodeProperties::LuidHighPart` * `OverrideEngineId` Released ROCm headers, including 7.2.4, do not provide all of these. When the probe fails, `agent_unsupported.cpp` is built instead of the WSL enumerator, `platform::wsl::is_available()` returns false, and `enumerate()` returns an empty result. The copies of `hsakmttypes.h` in this repository already contain the required definitions (lines 359-360 and 1578), but they are not currently installed. #10034 adds the WSL header installation rules in `libhsakmt/CMakeLists_wsl.txt` through `ROCDXG_PUBLIC_HEADERS`. Until that lands, a stock build of this PR will not enable WSL enumeration. ### 2. The DXG topology snapshot is not reference-counted on current `develop` Current `develop` unconditionally calls `topology_drop_snapshot()` from `hsaKmtReleaseSystemProperties()` (`libhsakmt/src/dxg/topology.cpp:417`). This frees the global topology snapshot and destroys its `WDDMDevice` objects even if another consumer still has the topology acquired. #10034 adds `src/dxg/snapshot_refcount.hpp` and makes snapshot destruction conditional on the reference count. The normal `rocprofv3` path is not affected because the SDK topology read happens from `ROCPROFILER_LIBRARY_CTOR` and completes before `main()`, and therefore before the application's `hsa_init()`. `rocprof-attach` is different because it injects into an already-running process. In that case there is no ordering guarantee between the SDK and the existing HSA runtime consumer, so late attach can fail until the reference-counting change from #10034 is present. ## Technical Details ### Agent enumeration (WSL) * `platform/wsl/dxg_topology.cpp` and `dxg_thunk.hpp` read KMT topology through `librocdxg` using: * `hsaKmtOpenKFD()` * `hsaKmtAcquireSystemProperties()` * `hsaKmtGetNodeProperties()` * `hsaKmtReleaseSystemProperties()` * `hsaKmtCloseKFD()` The symbols are resolved with `dlsym`. `DxgThunk::complete()` verifies that all five are available before any are called. * `HsaNodeProperties` has no size parameter, so the destination buffer includes `kNodePropsSlack = 128` bytes of additional space. * `platform/wsl/agent.cpp` matches DXG adapters to HSA nodes by LUID, with device ID as a fallback. Enumeration is allowed to be partial: an unmatched adapter is skipped with a diagnostic while successfully matched GPUs are still published. * Memory banks, caches, and IO links are not currently published by the WSL path. * If the thunk reports no GPU node, no GPU agent is published. A warning is emitted and the application continues. * `agent_mapping.{hpp,cpp}` adds a WSL-specific mapping policy. Native Linux keeps the existing strict behavior (`agent.cpp:755`), while WSL logs mismatches, keeps successfully paired GPUs, and continues. ### Counters * `counters/controller.cpp` emits a one-time warning that WSL counters are collected through the DXG vendor-packet path rather than the KFD profiler ioctl. * `WSLKMT_VENDOR_PACKET=1` is now set from `wsl::enumerate()` instead of `is_available()`. This matters when using `ROCPROFILER_FORCE_PLATFORM=wsl`, where the old placement was bypassed. Without the environment variable, the DXG path drops aqlprofile's vendor-specific PM4 IB and counter values are returned as zero. * `basic_counters.xml` and `derived_counters.xml` each gain an empty `<gfx1150 base="gfx11">` section to keep the installed metric files consistent. These XML files are not used by the runtime metric loader. Runtime metrics come from `config.yaml` (`counters/metrics.cpp:348`), which already supports gfx1150 and is unchanged by this PR. ### KFD * `kfd::init()` now probes for a usable KFD device. If `/dev/kfd` is unavailable or cannot be opened, initialization continues with a warning instead of aborting in `kfd_device_fd()`. ## Native-Linux impact Two changes are not WSL-gated and also apply to native Linux: * `aql/helpers.cpp:51`: `get_query_info()` returns an empty ID query for metrics that do not have a hardware block. Constant and topology metrics such as `simd_count` have an empty block name. Passing that empty name to `aqlprofile_get_pmc_info()` can dereference a null pointer and abort. * `kfd/kfd.cpp:1586`: the KFD availability check is unconditional. A system without a usable `/dev/kfd`, for example because the driver is not loaded or access is denied, now receives a warning and continues instead of aborting. Other rocprofiler services are unaffected. All other WSL-specific behavior remains isolated from native Linux. The DXG implementation requires `/dev/dxg` and `libdxcore.so`, and the PM4 command-buffer bounds change is limited to libhsakmt's DXG/WDDM submission path. ## Tests * **44 new tests** * 14 `wsl_dxg_topology` tests * 16 `wsl_dxg_thunk` tests * 10 `agent_mapping` tests * 4 `parse_gfx_target_version` tests The first two groups are in new executables. The mapping and target-version tests are added to the existing `rocprofiler-sdk-lib-tests`. None of these tests requires a GPU, HSA runtime, or real DXG thunk. The DXG tests use a fake thunk and fake dynamic loader. The 30 DXG-specific tests use the same CMake capability probe as the WSL enumerator, so they are only built when the required hsakmt interface is available. In a stock configuration, 14 of the 44 new tests are built. * `platform-dispatch-tests` and its six tests are pre-existing from #6399. * `rocprofv3-avail` * Three tests are moved out of the PC-sampling gate and are now enabled on Navi. * The L2 hit counter is selected by GPU family instead of being hardcoded. These tests have not previously run in Navi CI, so they may expose existing Navi-specific issues. * `hsa_barrier` * Skip when no GPU agent is available instead of dereferencing an empty agent map. * `agent` * `agent_visibility_multigpu` performs its `substr(1)` calls after the no-GPU skip, avoiding `std::out_of_range` when no agents are present. * `grid_max_size` tolerates the difference between rocprofiler's hardcoded `INT32_MAX` value and the value reported by the installed ROCr runtime. ## JIRA ID JIRA ID: ROCM-23912 ## Test Plan ## Test Result ## Submission Checklist - [ ] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests. --------- Co-authored-by: Mika Laitio <mika.laitio@amd.com>
Motivation
librocdxgalready exposes the WSL GPU topology through the standard KMT entry points:hsaKmtOpenKFD()hsaKmtAcquireSystemProperties()hsaKmtGetNodeProperties()hsaKmtReleaseSystemProperties()hsaKmtCloseKFD()This PR does not introduce a new topology interface. It fixes the lifetime and
ownership issues that arise when multiple components in the same process use
the existing DXG topology path.
ROCr is already one consumer, AMD SMI added a WSL GPU backend in #9725, and
#7016 adds rocprofiler-sdk as another. AMD SMI, for example, acquires the
topology in
WSLGPUBackend::TryPopulate()and holds it untilShutdown().The issues addressed here are:
consumer could release topology still in use by another;
hsaKmtOpenKFD()reset the fragment allocator on every open, including whenallocations from another consumer were still live;
hsaKmtCheckRuntimeDebugSupport()acquired a topology snapshot withoutreleasing it.
This PR was originally broader. At the reviewers' request, changes not required
for topology access have been split into two separate PRs so this one can be
reviewed as the topology prerequisite alone:
NumCUPerArray,KFDGpuID, failed-open descriptor cleanup, and thelibrocdxg.pcprefix;KfdLifecycle,Runtime::Load()rollback, thunk API validation, and DTIFownership.
Technical Details
Topology snapshot reference counting
A new
src/dxg/snapshot_refcount.hppisolates the reference-count transitionsso they can be tested without a GPU, DXCore adapter, or open thunk.
hsaKmtAcquireSystemProperties()now increments the reference count whenreusing an existing snapshot and initializes the count when publishing a newly
built snapshot.
hsaKmtReleaseSystemProperties()destroys the snapshot only when the finalreference is released. An unmatched release is rejected rather than tearing
down topology still owned by another consumer.
Failed topology construction is fully unwound so a later acquire cannot reuse
partially initialized state.
The final
hsaKmtCloseKFD()also releases any remaining snapshot before DXCoreis destroyed, because the snapshot contains
WDDMDeviceobjects owned by thatsession.
hsaKmtOpenKFD()open count and allocator lifetimeBoth the first-open and already-open paths maintain
dxg_open_count, so ittracks the number of live consumers rather than saturating at one.
The already-open increment is pre-existing code and appears only as unchanged
context between two diff hunks, which can make the change look one-sided when
reviewing the patch.
The functional change is the placement of
reset_suballocator(). It now runsonly on the first open. Previously, a second consumer could reset allocator
bookkeeping while allocations from the first consumer were still live.
Fork handling
child_fork_handler()clears the inherited open count and snapshot referencesusing plain stores only, keeping the atfork child handler minimal.
clear_after_fork()later abandons the parent'sWDDMDevicepointers withoutrunning their destructors, since doing so would issue D3DKMT calls using handles
created by the parent process. The last-close path follows the same ownership
rule in a forked child.
hsaKmtReleaseSystemProperties()now verifies that DXG is open before releasinga reference, preventing a child from releasing topology it never acquired.
hsaKmtOpenKFD()also callsclear_after_fork()before taking the runtime lock.Because
clear_after_fork()may replace the runtime object, taking the lockfirst could leave the
lock_guardreferring to freed storage.Debug support query
hsaKmtCheckRuntimeDebugSupport()previously calledhsaKmtAcquireSystemProperties()without releasing the acquired reference.It now takes no snapshot reference of its own and returns
NOT_SUPPORTEDwhenthe device list is empty, leaving topology ownership to the caller.
This change must land together with the explicit acquisition in
KfdDriver::Init(): the debug probe was where ROCr previously acquired itssnapshot reference as a side effect. Removing that behavior without adding the
explicit acquisition would leave ROCr holding no reference at all.
ROCr: accepting an already-open DXG thunk
KfdDriver::Open()now treatsHSAKMT_STATUS_KERNEL_ALREADY_OPENEDas successwhen
thunkLoader()->IsDXG().This is valid for DXG because
librocdxgreference-counts opens, so thealready-open path has acquired this consumer's handle reference. The native
thunk retains the existing behavior.
KfdDriver::Init()now explicitly acquires the driver's long-lived topologyreference before runtime enablement instead of relying on the former side
effect of the debug-support query. Failure paths release exactly what they
acquired.
Relationship to #7016
#7016 is the rocprofiler-sdk side of the WSL profiling work. It obtains topology
through the same five existing KMT entry points and requires no new topology
API.
The dependency on this PR is the snapshot lifetime mechanism.
On
develop,hsaKmtReleaseSystemProperties()destroys the single globalsnapshot unconditionally. With multiple consumers, that means one consumer can
release topology still held by ROCr or AMD SMI.
The normal
rocprofv3path in #7016 is safe through initialization ordering:its open/acquire/release/close sequence completes from the library constructor
before
main(), and therefore before the application'shsa_init()oramdsmi_init().Late attach has no equivalent ordering guarantee. For that reason, #7016
temporarily disables WSL enumeration when loaded through
rocprof-attach.That restriction can be removed once this PR is available.
There is no CI, submodule, or build-time mechanism enforcing merge order
between #7016 and this PR. They may merge in either order.
The only file currently modified by both is
libhsakmt/CMakeLists_wsl.txt: this PR adds theBUILD_ROCDXG_TESTSwiring,while #7016 adds PM4 frame-ring test wiring. The changes merge cleanly today,
as verified with
git merge-tree.JIRA ID
JIRA ID: ROCM-23912
Test Plan
Test Result
Submission Checklist