[review-only] feat(rocm): inter-stream synchronization deps on roctracer backend - #4
[review-only] feat(rocm): inter-stream synchronization deps on roctracer backend#4ajassani wants to merge 8 commits into
Conversation
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>
Validation: standalone branch on Shark26 (4x MI210, ROCm 7.2.3)Branch HEAD: L1: libkineto unit tests (USE_ROCPROFILER_SDK=OFF)Build:
All 5 new tests parse the actual emitted trace JSON and assert on field presence + correlation linkage ( L2: real PyTorch cross-stream workloadPyTorch 2.10.0a0 (HIP 7.2.53211) on 4x MI210. Workload: 5 iterations of Note: this PyTorch was built against the prior PR3 kineto (pre-rename, pre-restructure), so the trace shows Trace counts:
Sample Sample Cross-checks:
Summary
The build-infra changes (CMake + init.cpp guard) work as intended -- the roctracer backend builds cleanly from upstream on ROCm 7+ via PR is ready for upstream review. |
`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>
Review feedback addressedThanks 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
Detail per finding1. Event-handle reuse can resolve to stale producers. The earlier reassurance was incomplete -- the reviewer is right that Hook Side effects:
New test 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 Two overloads of No change to the emitted JSON fields. Trace size and structure are identical; only the resolution timing moves. New test 3. Missing ValidationRe-ran L1 on Shark26 (4x MI210, ROCm 7.2.3, Breakdown:
Worth noting that ALL existing tests now route through |
What you see today vs. with this PR
ROCm Kineto traces emit
hipStreamWaitEventwith 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
cudaStreamWaitEventviaCUPTI_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-processhipEvent_t -> sorted vector<{stream, correlationId}>map populated byhipEventRecordcallbacks, 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/kinetosubmodule currently pins a pre-March-2026 SHA, so every PyTorch ROCm wheel today uses the roctracer backend (USE_ROCPROFILER_SDK=OFFby 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.hare 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:
cuda_sync_kindhip_sync_kindstream_wait_event|event_synchronize|stream_synchronize|device_synchronizewait_on_streamwait_on_streamwait_on_cuda_event_record_corr_idwait_on_hip_event_record_corr_idwait_on_cuda_event_idwait_on_hip_event_idevent_idhip_eventstreamhip_streamThe
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 thecuda->hipsubstitution.Note on field value types and naming
There is one deliberate divergence from CUPTI that's worth flagging:
"event_id": 7,"stream": 2). The rawcudaEvent_tpointer never leaves CUPTI.hipEvent_tandhipStream_tare 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:correlation, not by the handle. Every HIP runtime call gets a unique monotonic correlation ID. Thewait_on_hip_event_record_corr_idfield links a wait directly to its producinghipEventRecordvia these correlation IDs, which never collide. Consumers that need to follow producer/consumer arrows should key oncorrelation.g_eventMapresolution logic is collision-safe by construction. The map is keyed by handle but rewritten on everyhipEventRecord, 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 nexthipEventRecordoverwrites the stale entry before any wait can resolve against it.The naming convention (
hip_*prefix) was chosen to mirror CUPTI'scuda_sync_kindrather than CUPTI's unprefixedevent_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:
feat: capture inter-stream synchronization deps on the roctracer backendg_eventMapdesign,recordEvent/resolveWait/clearEventMap, JSON specs inRoctracerActivity_inl.h, dispatch wiring inRocmActivityProfiler.cpp.feat: add shared activity row types for inter-stream sync metadataRocLogger.h. The rocprofiler-sdk backend will reuse these in a follow-up PR.test: cover inter-stream dependency emission on the roctracer backendTEST_Fblocks gated with#ifdef ROCTRACER_FALLBACK. Each mirrors a CUPTI test where one exists.refactor: rename sync_type metadata field to hip_sync_kindcuda_sync_kind.build: small fixes to let the roctracer backend build on ROCm 7+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.txtcurrently unconditionally forcesUSE_ROCPROFILER_SDK=ONon ROCm 6.4+. There's no caller opt-out -- passing-DUSE_ROCPROFILER_SDK=OFFis silently overridden, so the roctracer code path is unreachable on any modern ROCm install. The build commit changes this toif(NOT DEFINED USE_ROCPROFILER_SDK)so the caller can opt out explicitly.libkineto/src/init.cppunconditionally callsRocprofLogger::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 viaroctracer_open_poolat 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:
-DUSE_ROCPROFILER_SDK=OFFis now respected on ROCm 6.4+ instead of silently overridden.Tests
5 new
TEST_Fblocks inlibkineto/test/RocmActivityProfilerTest.cpp, gated with#ifdef ROCTRACER_FALLBACKso the rocprofsdk build is unaffected:InterStreamDependencyTesthipEventRecord->hipStreamWaitEventon same event resolves to producer correlationId.StreamWaitEventFutureCorrelationupper_boundon event-handle reuse: wait at corr=101 must resolve to record corr=100, not a future corr=200.EventMapClearedOnResetclearEventMap()runs inonResetTraceData-- stale records from prior session don't pollute the next.EventSynchronizeResolvesProducerhipEventSynchronizegets producer attribution AND always emitswait_on_hip_event_id. CUPTI parity for the event-sync side.UnresolvedWaitStillEmitsEventIdwait_on_hip_event_idis 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):
[ PASSED ] 5 testsagainst roctracer backend;[ PASSED ] 5 testsagainst rocprofiler-sdk (after PR2's L1 build fix).hipStreamWaitEventresolved on both backends.train_ddp_crosscp.pyworld_size=4 -- 32/32hipStreamWaitEventresolved, 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_kindrename, 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
hipGraphLaunch, etc.) -- separate concern.hipEventQuery,hipStreamQuery) -- not captured because they don't create sync dependencies.Fork PR for self-review. Will be retargeted at
pytorch/kineto:mainwhen validation completes.