Skip to content

[review-only] feat(rocm): inter-stream synchronization deps on roctracer backend - #4

Open
ajassani wants to merge 8 commits into
mainfrom
pr3-standalone/roctracer-sync-deps
Open

[review-only] feat(rocm): inter-stream synchronization deps on roctracer backend#4
ajassani wants to merge 8 commits into
mainfrom
pr3-standalone/roctracer-sync-deps

Conversation

@ajassani

@ajassani ajassani commented May 23, 2026

Copy link
Copy Markdown
Owner

What you see today vs. with this PR

ROCm Kineto traces emit hipStreamWaitEvent with no information about what the wait is on. You see an opaque CPU-side timing row, and there's no way to reconstruct the producer stream / event from trace data alone.

 Today (upstream/main, roctracer backend):
 {
   "name": "hipStreamWaitEvent",
   "args": { "cid": 213, "correlation": 234 }
 }

+After this PR:
+{
+  "name": "hipStreamWaitEvent",
+  "args": {
+    "cid": 213, "correlation": 234,
+    "hip_sync_kind": "stream_wait_event",
+    "hip_event": "0x7fb5a8000",
+    "hip_stream": "0x7fb5a8400",
+    "wait_on_stream": "0x7fb5a8800",
+    "wait_on_hip_event_id": "0x7fb5a8000",
+    "wait_on_hip_event_record_corr_id": 187
+  }
+}

CUPTI emits the equivalent metadata for cudaStreamWaitEvent via CUPTI_ACTIVITY_KIND_SYNCHRONIZATION. This PR brings ROCm traces to functional parity.

What this PR does

Hooks five HIP sync-family APIs (hipEventRecord, hipStreamWaitEvent, hipEventSynchronize, hipStreamSynchronize, hipDeviceSynchronize) on the roctracer backend. Captures a per-process hipEvent_t -> sorted vector<{stream, correlationId}> map populated by hipEventRecord callbacks, and resolves producer-stream attribution at JSON-emission time. Roctracer-only in this PR; the equivalent on the rocprofiler-sdk backend will follow in a separate PR.

Why roctracer first

PyTorch's third_party/kineto submodule currently pins a pre-March-2026 SHA, so every PyTorch ROCm wheel today uses the roctracer backend (USE_ROCPROFILER_SDK=OFF by default). Landing this on roctracer first puts the feature in users' hands the moment PyTorch bumps its kineto pin to a SHA that includes this PR -- no other changes needed.

The shared activity-row types in RocLogger.h are designed to be backend-agnostic; the rocprofiler-sdk backend will reuse them when its implementation lands in a follow-up.

Field-by-field comparison with CUPTI

Trace consumers (Perfetto, kineto post-processors, Chakra/HolisticTraceAnalysis) need a parallel field set across CUDA and ROCm. The mapping:

Concept CUPTI key (CUDA) This PR (ROCm) Notes
Sync-kind label cuda_sync_kind hip_sync_kind enum: stream_wait_event | event_synchronize | stream_synchronize | device_synchronize
Source stream of a wait wait_on_stream wait_on_stream exact match
Source corr-id of a wait wait_on_cuda_event_record_corr_id wait_on_hip_event_record_corr_id s/cuda/hip
Source event handle wait_on_cuda_event_id wait_on_hip_event_id s/cuda/hip
Event handle (producer) event_id hip_event see "value type" note below
Stream the activity ran on stream hip_stream see "value type" note below

The wait_on_* triplet is the primary consumer-facing parity surface -- those three fields are what trace analyzers follow to reconstruct inter-stream arrows. Field names match CUPTI exactly modulo the cuda -> hip substitution.

Note on field value types and naming

There is one deliberate divergence from CUPTI that's worth flagging:

  • CUPTI assigns small monotonic integer IDs to CUDA event and stream handles internally and emits those in the trace ("event_id": 7, "stream": 2). The raw cudaEvent_t pointer never leaves CUPTI.
  • roctracer delivers HIP API arguments verbatim. hipEvent_t and hipStream_t are opaque pointers in the HIP runtime ABI, and roctracer does not provide an equivalent ID-translation layer.

This PR emits the raw handles as hex strings ("hip_event": "0x7f...") rather than rolling our own integer-ID translation table. The reasons:

  1. Cross-trace identity is preserved by correlation, not by the handle. Every HIP runtime call gets a unique monotonic correlation ID. The wait_on_hip_event_record_corr_id field links a wait directly to its producing hipEventRecord via these correlation IDs, which never collide. Consumers that need to follow producer/consumer arrows should key on correlation.
  2. The g_eventMap resolution logic is collision-safe by construction. The map is keyed by handle but rewritten on every hipEventRecord, which matches HIP's own semantics (a wait sees the most recent record of the event). Even if a handle is destroyed and the pointer is later reused by a new event, the next hipEventRecord overwrites the stale entry before any wait can resolve against it.
  3. The handle in the JSON is a debugging aid, useful when manually inspecting a trace; not intended as a stable cross-trace identifier.

The naming convention (hip_* prefix) was chosen to mirror CUPTI's cuda_sync_kind rather than CUPTI's unprefixed event_id / stream -- because the value type differs, an explicit backend prefix avoids surprising consumers who'd expect an integer.

How to review

Commits are ordered by reviewability, not feature flow:

Commit Size Start here if you want to look at...
feat: capture inter-stream synchronization deps on the roctracer backend ~270 LoC The actual feature. g_eventMap design, recordEvent / resolveWait / clearEventMap, JSON specs in RoctracerActivity_inl.h, dispatch wiring in RocmActivityProfiler.cpp.
feat: add shared activity row types for inter-stream sync metadata ~54 LoC Mechanical type definitions in RocLogger.h. The rocprofiler-sdk backend will reuse these in a follow-up PR.
test: cover inter-stream dependency emission on the roctracer backend ~440 LoC 5 new TEST_F blocks gated with #ifdef ROCTRACER_FALLBACK. Each mirrors a CUPTI test where one exists.
refactor: rename sync_type metadata field to hip_sync_kind ~8 LoC Field-name cleanup to mirror CUPTI's cuda_sync_kind.
build: small fixes to let the roctracer backend build on ROCm 7+ ~16 LoC Build hygiene. Justified in the section below; safe to skip otherwise.

Build infrastructure: why one CMake + one init.cpp change

These two changes are coupled prerequisites and could draw "why are these in a roctracer feature PR?" without context. Short answer: without them, this PR cannot be CI-tested on modern ROCm.

  • libkineto/CMakeLists.txt currently unconditionally forces USE_ROCPROFILER_SDK=ON on ROCm 6.4+. There's no caller opt-out -- passing -DUSE_ROCPROFILER_SDK=OFF is silently overridden, so the roctracer code path is unreachable on any modern ROCm install. The build commit changes this to if(NOT DEFINED USE_ROCPROFILER_SDK) so the caller can opt out explicitly.
  • libkineto/src/init.cpp unconditionally calls RocprofLogger::ensureRegistered(), a rocprofsdk-only symbol. Once the CMake toggle is honored, the link fails with "undefined reference" on the roctracer build. The build commit guards the call with #ifndef ROCTRACER_FALLBACK. Roctracer's registration model differs (subscription via roctracer_open_pool at trace-start) and doesn't need an equivalent force-configure step.

The two are a pair: CMake change exposes a previously-unreachable code path, init.cpp change makes the resulting build link. Together they restore the ability to build the roctracer backend directly from upstream kineto on a modern ROCm system -- the surface upstream CI uses to validate this PR.

End-user behavior is unchanged:

  • Default on ROCm 6.4+: still rocprofiler-sdk.
  • Default on ROCm < 6.4: still roctracer.
  • New: -DUSE_ROCPROFILER_SDK=OFF is now respected on ROCm 6.4+ instead of silently overridden.

Tests

5 new TEST_F blocks in libkineto/test/RocmActivityProfilerTest.cpp, gated with #ifdef ROCTRACER_FALLBACK so the rocprofsdk build is unaffected:

Test Validates
InterStreamDependencyTest Happy path: hipEventRecord -> hipStreamWaitEvent on same event resolves to producer correlationId.
StreamWaitEventFutureCorrelation Vector + upper_bound on event-handle reuse: wait at corr=101 must resolve to record corr=100, not a future corr=200.
EventMapClearedOnReset clearEventMap() runs in onResetTraceData -- stale records from prior session don't pollute the next.
EventSynchronizeResolvesProducer hipEventSynchronize gets producer attribution AND always emits wait_on_hip_event_id. CUPTI parity for the event-sync side.
UnresolvedWaitStillEmitsEventId wait_on_hip_event_id is emitted even when no producer match (event recorded before profiling started).

Validation status

The earlier (pre-restructure, 9-commit stacked) PR3 was validated on Shark26 (4x MI210, ROCm 7.2.3):

  • L1: [ PASSED ] 5 tests against roctracer backend; [ PASSED ] 5 tests against rocprofiler-sdk (after PR2's L1 build fix).
  • L2 (cross-backend trace diff): roctracer-PR3 vs rocprofsdk-patched produce identical sync metadata field set; 8/8 hipStreamWaitEvent resolved on both backends.
  • L3 (DDP): train_ddp_crosscp.py world_size=4 -- 32/32 hipStreamWaitEvent resolved, 0 dangling arrows.

This standalone branch is being re-validated now. The diff vs. the prior version is small and mechanical (drop PR1+PR2 stacking, restructure into 5 commits including the hip_sync_kind rename, gate tests with #ifdef ROCTRACER_FALLBACK); a fresh L1 + L2 run on Shark26 is in progress and will be posted as a comment on this PR.

Out of scope / follow-up

  • Inter-stream sync metadata on the rocprofiler-sdk backend -- reuses the shared types added in this PR; separate PR.
  • HIP graph APIs (hipGraphLaunch, etc.) -- separate concern.
  • Non-blocking query APIs (hipEventQuery, hipStreamQuery) -- not captured because they don't create sync dependencies.

Fork PR for self-review. Will be retargeted at pytorch/kineto:main when validation completes.

ajassani and others added 5 commits May 23, 2026 13:22
Four narrowly-scoped fixes that together make USE_ROCPROFILER_SDK=OFF
build cleanly on a recent ROCm install. Each is independent of the new
inter-stream dependency feature added by later commits; they only exist
because today's defaults assume rocprofiler-sdk on ROCm 6.4+.

* CMakeLists: respect a caller-supplied USE_ROCPROFILER_SDK instead of
  unconditionally overriding it. Without this, a user on ROCm 7.x cannot
  opt into the legacy roctracer backend even by passing
  -DUSE_ROCPROFILER_SDK=OFF -- the cmake block above silently flips it
  back on.

* init.cpp: RocprofLogger::ensureRegistered() is rocprofiler-sdk-specific
  (it calls rocprofiler_force_configure). When ROCTRACER_FALLBACK is
  defined, RocprofLogger.cpp isn't compiled, so the link fails on this
  symbol. Guard the call with an ifndef so the roctracer build links
  cleanly. Roctracer registers via its own callback API and doesn't need
  a force-configure step.

* RoctracerActivityApi.{h,cpp}: silence pre-existing
  -Wunused-private-field on registered_ and -Wunused-parameter on
  setMaxBufferSize via [[maybe_unused]]. Both warnings predate this work
  but only become errors under -Werror builds (which our test harness
  uses); harmless to suppress with the standard attribute.

Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce two new activity row types that the backend loggers will emit
for HIP synchronization APIs, plus a sync-kind enum and two new entries
in the existing rocprof_activity_types enum.

The types live in RocLogger.h alongside the other ' rocprof' row types
because they're shared across both ROCm backends (roctracer and
rocprofiler-sdk) -- only the emission code differs. This commit
introduces no emission code itself; that comes in the next commit for
the roctracer backend. The rocprofiler-sdk backend will gain its own
emission code in a follow-up PR.

* rocprofEventRecordRow captures a hipEventRecord call, including which
  hipEvent_t was recorded and which hipStream_t it was recorded on.

* rocprofSyncRow captures a sync-family API (hipStreamWaitEvent,
  hipEventSynchronize, hipStreamSynchronize, hipDeviceSynchronize)
  together with the resolved producer stream + correlation id when the
  consumer is waiting on a previously-recorded event. Mirrors CUPTI's
  CUPTI_ACTIVITY_KIND_SYNCHRONIZATION semantics.

Co-authored-by: Cursor <cursoragent@cursor.com>
Hook hipEventRecord, hipStreamWaitEvent, hipEventSynchronize,
hipStreamSynchronize, and hipDeviceSynchronize so each emits a
rocprofEventRecordRow or rocprofSyncRow with the metadata needed to
reconstruct cross-stream dependencies in Chrome / Perfetto traces. The
emitted JSON field names mirror CUPTI's CUPTI_ACTIVITY_KIND_SYNCHRONIZATION
schema (hip_event, hip_stream, sync_type, wait_on_stream,
wait_on_hip_event_record_corr_id, wait_on_hip_event_id) so trace
consumers can use a single code path across CUDA and ROCm.

Implementation details:

* RoctracerLogger.{cpp,h}: introduce a per-process g_eventMap of
  hipEvent_t -> sorted vector<{stream, correlationId}> alongside the
  existing roctracer subscription. The vector form handles hipEvent_t
  handle reuse correctly: hipEventDestroy + hipEventCreate can hand back
  the same hipEvent_t pointer, so the producer-correlation lookup uses
  upper_bound on the queryCorrId to always return the most recent
  record that strictly preceded the wait. clearEventMap() drops the map
  between profiling sessions so a previous session's records cannot
  pollute the next.

* RoctracerActivity_inl.h: add RuntimeActivity<rocprofEventRecordRow>
  and RuntimeActivity<rocprofSyncRow> JSON specializations that emit
  the resolved sync metadata. wait_on_* keys are only emitted when the
  producer lookup succeeded; when it doesn't (event recorded before
  profiling started, edge case), the raw hip_event/hip_stream fields
  are still emitted so post-processors can attempt their own
  reconstruction. wait_on_hip_event_id is always emitted for
  stream_wait_event and event_synchronize even without a producer match
  -- it just reports the hipEvent_t the wait was issued against.

* RocmActivityProfiler.cpp: route the new ROCTRACER_ACTIVITY_EVENT_RECORD
  and ROCTRACER_ACTIVITY_SYNC records through handleRuntimeActivity on
  the roctracer backend, and call RoctracerLogger::clearEventMap() from
  onResetTraceData. The rocprofiler-sdk backend is untouched -- the
  same feature on that side will be added in a follow-up PR that
  reuses the shared types from RocLogger.h.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add 5 unit tests against RocmActivityProfilerTest verifying the
roctracer backend's new sync-metadata emission and the
g_eventMap lifecycle. The new tests are gated with
ROCTRACER_FALLBACK so the rocprofiler-sdk build is unaffected;
when the follow-up PR adds the same feature to the rocprofiler-sdk
backend, these tests will be re-enabled against it.

* InterStreamDependencyTest - happy path: a hipEventRecord followed
  by hipStreamWaitEvent on the same hipEvent_t produces a sync record
  with resolved wait_on_stream and wait_on_hip_event_record_corr_id.

* StreamWaitEventFutureCorrelation - vector-backed g_eventMap +
  upper_bound semantics: two hipEventRecord callbacks land on the same
  event handle (corr=100 then corr=200), and a hipStreamWaitEvent with
  corr=101 must resolve to corr=100, not corr=200. Mirrors CUPTI's
  SyncEventCorrIdOutOfOrder test.

* EventMapClearedOnReset - records from a prior profiling session must
  not leak into the next session's wait resolution. Mirrors CUPTI's
  WaitEventMapClearedOnReset test.

* EventSynchronizeResolvesProducer - hipEventSynchronize gets the same
  producer attribution as hipStreamWaitEvent, with the always-emitted
  wait_on_hip_event_id field present. CUPTI parity for the eventSync
  side of CUPTI_ACTIVITY_KIND_SYNCHRONIZATION.

* UnresolvedWaitStillEmitsEventId - waits on events that were never
  observed (recorded before profiling started, edge case) still emit
  wait_on_hip_event_id so post-processors can attempt their own
  reconstruction. wait_on_stream and wait_on_hip_event_record_corr_id
  are absent in that case.

The three MockRocLogger helpers (addEventRecordActivity,
addSyncActivity, addSyncActivityResolvingFromMap) drive the same
production path as the real api_callback by calling
RoctracerLogger::recordEvent / resolveWait directly, so the tests
exercise the actual g_eventMap data structures and lookup logic
rather than a separate mock implementation.

Co-authored-by: Cursor <cursoragent@cursor.com>
CUPTI emits `cuda_sync_kind` for its synchronization activity rows
(see `CudaSyncActivity::metadataJson` in CuptiActivity.h). The roctracer
backend was emitting an unprefixed `sync_type` for the same concept.

Rename it to `hip_sync_kind` so trace consumers see a parallel
`cuda_sync_kind` / `hip_sync_kind` pair across the two backends. Other
HIP-specific fields (`hip_event`, `hip_stream`) already follow this
convention. Tests updated accordingly.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ajassani

Copy link
Copy Markdown
Owner Author

Validation: standalone branch on Shark26 (4x MI210, ROCm 7.2.3)

Branch HEAD: b481aca (5 commits ahead of upstream/main).

L1: libkineto unit tests (USE_ROCPROFILER_SDK=OFF)

Build: cmake -DKINETO_BACKEND=rocm -DUSE_ROCPROFILER_SDK=OFF -DLIBKINETO_BUILD_TESTS=ON. Configures and builds cleanly with HIP 7.2.53211 + cmake 4.3.2 on the modern ROCm install -- the build-infra commit (CMake opt-out + init.cpp guard) is doing its job.

RocmActivityProfilerTest: [ PASSED ] 13 tests, 0 failed (96 ms total).

Pre-existing (8) New in this PR (5)
SyncTrace InterStreamDependencyTest
HtoDMemcpyUsesRuntimeStreamWhenAsyncQueueIsZero StreamWaitEventFutureCorrelation
HtoDMemcpyKeepsNonzeroAsyncQueue EventMapClearedOnReset
HtoDMemcpyStaysOnZeroWhenRuntimeStreamMapsToMultipleQueues EventSynchronizeResolvesProducer
GpuNCCLCollectiveTest UnresolvedWaitStillEmitsEventId
GpuUserAnnotationTest
SubActivityProfilers
JsonGPUIDSortTest

All 5 new tests parse the actual emitted trace JSON and assert on field presence + correlation linkage (hip_sync_kind, wait_on_stream, wait_on_hip_event_record_corr_id, etc.), so this also validates the sync_type -> hip_sync_kind rename.

L2: real PyTorch cross-stream workload

PyTorch 2.10.0a0 (HIP 7.2.53211) on 4x MI210. Workload: 5 iterations of matmul on stream0 -> event.record -> stream1.wait_event(ev) -> matmul on stream1 -> sync. Profiler enabled, trace exported via prof.export_chrome_trace.

Note: this PyTorch was built against the prior PR3 kineto (pre-rename, pre-restructure), so the trace shows sync_type -- the field name as it existed in PR3. This L2 confirms the feature works end-to-end in a real workload; the field rename to hip_sync_kind is L1-validated.

Trace counts:

  • hipEventRecord: 5
  • hipStreamWaitEvent: 5

Sample hipEventRecord args:

cid: 81
correlation: 18
hip_event:  0x45fe8230
hip_stream: 0x460d78e0

Sample hipStreamWaitEvent args:

cid: 348
correlation: 20
sync_type: stream_wait_event           <-- this becomes hip_sync_kind post-rename
hip_stream: 0x460ff9a0
hip_event:  0x45fe8230                 <-- same handle as the record
wait_on_hip_event_id: 0x45fe8230       <-- same handle
wait_on_stream: 0x460d78e0             <-- producer stream from the record
wait_on_hip_event_record_corr_id: 18   <-- matches the record's correlation=18

Cross-checks:

  • wait_on_hip_event_record_corr_id=18 matches the producing hipEventRecord's correlation=18 -- the trace consumer can follow the link.
  • wait_on_stream=0x460d78e0 matches the record's hip_stream=0x460d78e0 -- producer stream attribution is correct.
  • hip_event is consistent across record (0x45fe8230) and wait (0x45fe8230).

Summary

Layer Result
L1 -- libkineto unit tests on standalone branch 13/13 PASSED including the 5 new inter-stream tests and the rename
L2 -- real PyTorch cross-stream workload All new fields emit correctly, correlation linkage verified, producer-stream attribution verified

The build-infra changes (CMake + init.cpp guard) work as intended -- the roctracer backend builds cleanly from upstream on ROCm 7+ via -DUSE_ROCPROFILER_SDK=OFF.

PR is ready for upstream review.

ajassani and others added 3 commits May 23, 2026 17:17
`recordEvent` and `resolveWait` use `std::lower_bound` / `std::upper_bound`,
which live in <algorithm>. Today the symbols are pulled in transitively
through other STL headers, but that is brittle under stricter toolchains
or different libstdc++ versions. Add the explicit include.

Raised in PR review.

Co-authored-by: Cursor <cursoragent@cursor.com>
Today g_eventMap is populated on hipEventRecord and cleared between
profiling sessions, but never updated when a hipEvent_t is destroyed.
If the HIP allocator later reuses the destroyed event's raw pointer
for a freshly created event, a wait on the new event can resolve to
the OLD record because the map is keyed by handle.

resolveWait does match HIP's "most recent record wins" semantics --
but only when the new event has been recorded at least once before
the wait, because each record overwrites the map entry. The pathological
case is:
  - record(evA, corr=10)
  - destroy(evA)
  - create(evB) -- allocator returns the same pointer as evA
  - wait_on(evB, corr=20)            <-- never recorded; HIP wait is a no-op
                                         but our trace claims a producer link
                                         pointing at the dead evA record

The trace would carry misleading wait_on_stream / wait_on_*_corr_id
fields and trace consumers cannot tell that the link is stale.

Hook HIP_API_ID_hipEventDestroy and evict the event from g_eventMap
via the new unrecordEvent helper. Net effect:
  - bounded map growth (entries die with their events)
  - no stale-resolution after handle reuse
  - hipEventDestroy is also emitted as a runtime row in the trace,
    matching CUPTI's cudaEventDestroy emission

Adds the EventMapEvictsOnDestroy test which exercises record -> wait
(resolves) -> destroy -> wait (must not resolve) -> re-record ->
wait (resolves to new producer).

Raised in PR review.

Co-authored-by: Cursor <cursoragent@cursor.com>
The original implementation resolved producer attribution
(wait_on_stream / wait_on_hip_event_record_corr_id) inside the
hipStreamWaitEvent and hipEventSynchronize api_callbacks. roctracer
dispatches those callbacks from the calling thread synchronously at
HIP_API_PHASE_EXIT. For applications that issue hipEventRecord on one
thread and the matching wait on another (PyTorch's autograd backward
running on its own thread is the canonical case), the wait callback
can land before the producing hipEventRecord callback. The wait was
then permanently emitted with no producer link, even though by trace
finalization the producer record was in g_eventMap.

Move the resolveWait() call out of the api_callback and into a new
RoctracerLogger::resolvePendingSyncs() pass that runs from
RocmActivityProfiler::processGpuActivities right before rows are
emitted. By that point all hipEventRecord callbacks for the trace
have been delivered, so the pass sees the final g_eventMap state and
back-fills the producer attribution into every unresolved
STREAM_WAIT_EVENT / EVENT_SYNCHRONIZE row.

Two overloads of resolvePendingSyncs are exposed: a no-arg form that
operates on the singleton's rows_ vector (production), and an
explicit-buffer form (used by unit tests via MockRocActivities, which
keeps its own activities_ buffer disjoint from the singleton). The
mock's processActivities now calls the explicit form before iterating,
mirroring the production path.

Resolution is no-op for rows that already have srcStream / srcCorrId
populated, so test helpers that pre-resolve at row construction
(addSyncActivity, addSyncActivityResolvingFromMap) keep working
unchanged.

Adds the StreamWaitEventCallbackArrivesBeforeRecord test which inserts
the wait into the buffer before its producing record (i.e. callback
arrival order reversed) and verifies the resolved producer attribution
appears in the final trace JSON.

Raised in PR review.

Co-authored-by: Cursor <cursoragent@cursor.com>
@ajassani

Copy link
Copy Markdown
Owner Author

Review feedback addressed

Thanks for the careful read. All three findings are real -- pushed three commits that fix them and added covering tests for the two correctness issues.

Commits

SHA Fix Lines
d038be8 <algorithm> include +1
b7a4737 hipEventDestroy hook -> unrecordEvent evicts from g_eventMap +89 / -0 (incl. new test)
09fb8ee Defer producer resolution to buffer-flush time +159 / -20 (incl. new test)

Detail per finding

1. Event-handle reuse can resolve to stale producers. The earlier reassurance was incomplete -- the reviewer is right that resolveWait is collision-safe ONLY when the reused handle gets re-recorded before any wait. Apps that destroy + recreate + wait-without-record (badly-written, but observable) would have seen stale producer links in the trace.

Hook HIP_API_ID_hipEventDestroy in RoctracerLogger::api_callback and call the new unrecordEvent(void*) which erases the entry from g_eventMap. Also emits a hipEventDestroy runtime row in the trace, matching CUPTI's cudaEventDestroy emission.

Side effects:

  • Bounded map memory under destroy-heavy workloads (positive).
  • +1 runtime row per destroy: in PyTorch's event-pool model this is dozens per process; sub-0.1% of typical trace size.

New test EventMapEvictsOnDestroy covers the exact scenario from the review: record at corr=10, destroy, wait at corr=30 (must not resolve), then re-record at corr=40 and verify the new wait at corr=50 attributes to the new producer.

2. Producer lookup done too early for out-of-order callback delivery. Real bug in multi-threaded HIP callers (PyTorch with autograd backward on a separate thread is the canonical case).

Moved the resolveWait() call out of the hipStreamWaitEvent and hipEventSynchronize cases of api_callback and into a new RoctracerLogger::resolvePendingSyncs() pass that runs from RocmActivityProfiler::processGpuActivities right before rows are emitted. By that point all hipEventRecord callbacks for the trace have been delivered, so the resolution sees the final g_eventMap state.

Two overloads of resolvePendingSyncs are exposed -- a no-arg form that operates on the singleton's rows_ (production), and an explicit-buffer form (used by unit tests via MockRocActivities, whose activities_ buffer is disjoint from the singleton). The mock's processActivities now calls the explicit form before iterating, so all existing tests exercise the new code path.

No change to the emitted JSON fields. Trace size and structure are identical; only the resolution timing moves.

New test StreamWaitEventCallbackArrivesBeforeRecord reverses the buffer order: the wait row is pushed BEFORE its producing record arrives, and the test asserts the resolved producer attribution appears in the final trace JSON (i.e. deferred resolution did its job).

3. Missing <algorithm> include. Added.

Validation

Re-ran L1 on Shark26 (4x MI210, ROCm 7.2.3, USE_ROCPROFILER_SDK=OFF). Build cleanly; all tests pass:

[==========] 15 tests from 1 test suite ran. (97 ms total)
[  PASSED  ] 15 tests.

Breakdown:

  • 8 pre-existing tests (SyncTrace, HtoDMemcpy* x3, GpuNCCLCollectiveTest, GpuUserAnnotationTest, SubActivityProfilers, JsonGPUIDSortTest)
  • 5 inter-stream-dep tests from earlier (InterStreamDependencyTest, StreamWaitEventFutureCorrelation, EventMapClearedOnReset, EventSynchronizeResolvesProducer, UnresolvedWaitStillEmitsEventId)
  • 2 new tests from this round (StreamWaitEventCallbackArrivesBeforeRecord, EventMapEvictsOnDestroy)

Worth noting that ALL existing tests now route through resolvePendingSyncs on their way through the mock (the mock's processActivities calls it before iterating), so the deferred-resolution path is exercised broadly, not just by the new test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant