Skip to content

rocprofv3: surface inter-stream HIP event-sync dependencies on wait/sync records - #1

Draft
ajassani wants to merge 3 commits into
developfrom
feat/rocprofv3-interstream-sync-metadata
Draft

rocprofv3: surface inter-stream HIP event-sync dependencies on wait/sync records#1
ajassani wants to merge 3 commits into
developfrom
feat/rocprofv3-interstream-sync-metadata

Conversation

@ajassani

@ajassani ajassani commented May 24, 2026

Copy link
Copy Markdown
Owner

Motivation

rocprofv3 already shows the per-record stream ID on hipStreamWaitEvent and hipEventSynchronize tool records, but it does not yet surface which stream and which hipEventRecord call the waiter is actually depending on. As a result, tools that want to reconstruct a cross-stream / critical-path dependency graph from a captured trace have to re-run the workload under a separate analysis pass or guess at the topology from timestamps.

This PR closes that gap by adding two new fields on the HIP API tool record:

  • wait_on_stream — the stream that recorded the awaited hipEvent_t
  • wait_on_correlation_id — the rocprofv3 correlation id of that hipEventRecord call

Both default to 0 (the "no resolved dependency" sentinel) when the awaited event was never observed being recorded earlier in the same trace, so the change is non-breaking from a record-shape standpoint.

Technical Details

Event-lifecycle tracking utility

A new utility in source/lib/rocprofiler-sdk-tool/event_producer_map.{hpp,cpp} maintains two process-global, shared/unique-locked maps using the existing common::Synchronized + common::static_object building blocks:

  • producer maphipEvent_t -> { stream_id, hipEventRecord corr_id }. Written on PHASE_EXIT of hipEventRecord (and _spt / WithFlags variants); erased on hipEventDestroy.
  • resolved-wait mapwaiter corr_id -> { wait_on_stream, wait_on_corr_id }. Written on PHASE_EXIT of hipStreamWaitEvent (and _spt) and hipEventSynchronize after a producer lookup; drained at buffer-flush time when the corresponding tool record is materialized.

Callback wiring

A dedicated callback context is registered on ROCPROFILER_CALLBACK_TRACING_HIP_RUNTIME_API filtered to just the seven operation IDs involved in event lifecycle and synchronization. The context is gated on hip_runtime_api_trace so there is zero overhead when HIP tracing is off. The callback runs on PHASE_EXIT only (after the underlying HIP call has completed) so the producer state is consistent with what other threads will observe via lookup, and the body is wrapped in a top-level try/catch (the underlying std::unordered_map ops can throw std::bad_alloc under memory pressure and this code runs synchronously inside the SDK callback dispatch path).

Buffer-flush integration

At the existing buffered_tracing_callback flush site, the materialization of tool_buffer_tracing_hip_api_ext_record_t consumes any stashed resolved-wait entry for the record's correlation id and routes through an extended constructor that fills the new fields.

The lookup is gated on the record's operation kind — only hipStreamWaitEvent, hipStreamWaitEvent_spt, and hipEventSynchronize can ever carry a resolved dependency in the side-table, so the (locking) map call is skipped for the other ~99.95% of HIP API records on a representative DDP workload.

Reattach hygiene

Both maps live in common::static_object and persist across tool_inittool_detachtool_init cycles. hipEvent_t pointers can be reused across sessions, so we clear both maps at tool_init right before the dependency context is created. A single ROCP_INFO is also emitted at the same site so users see the service being enabled in the tool init log.

Output coverage

  • CSV (out_hip_api_trace.csv) widens from 7 to 10 columns: the existing 7 + Stream_Id + Wait_On_Stream_Id + Wait_On_Correlation_Id. A dedicated hip_api_csv_encoder is used only for HIP records so other CSV outputs are unchanged.
  • JSON (out_results.json) picks up wait_on_stream (as {"handle": N}) and wait_on_correlation_id on hip_api buffer records via cereal serialization. No schema breakage; the fields default to 0.
  • Perfetto pftrace emits two new args (wait_on_stream_ID, wait_on_corr_id) on the relevant slices and, when wait_on_correlation_id != 0, adds a second Flow::ProcessScoped terminating at the resolved producer so the cross-stream dependency shows up as an arrow in the Perfetto UI.

Deferred to follow-up PRs

  • OTF2 emitter changes need schema-level attribute IDs that deserve discussion with downstream consumers (Vampir, Score-P, etc.) on its own PR.
  • Rocpd column / schema migration for the new fields is deferred to its own PR for the SQL-migration review.

Both formatters continue to emit valid records today; they just don't yet carry the new fields.

⚠️ Breaking changes

The HIP API CSV (<out>_hip_api_trace.csv) now has 10 columns instead of 7. Consumers that parse this CSV positionally or hard-code the column count will need to be updated.

The change is contained: only HIP records use the new hip_api_csv_encoder<10>; other domains (kernels, markers, memory, etc.) keep their existing 7-column shape via the original api_csv_encoder<7>.

The added columns are:

Column Meaning
Stream_Id the call's own stream (previously dropped from the CSV even though it was always available on the record)
Wait_On_Stream_Id resolved producer stream — 0 if no resolved dependency
Wait_On_Correlation_Id resolved producer hipEventRecord correlation id — 0 if no resolved dependency

If a downstream gating flag for the new columns is preferred, happy to add an opt-in option (--hip-trace-deps or similar) so the default shape stays at 7 columns until consumers have migrated.

The JSON and Perfetto outputs are additive: new fields appear on existing records but no existing fields are renamed, reshaped, or removed.

JIRA ID

N/A — external contributor (no SWDEV access). Happy to have an internal champion attach a ticket if that helps with the merge gate.

Test Plan

Unit tests

source/lib/tests/tool/event_producer_map.cpp — 13 cases against the new tool-tests binary (a focused gtest unit-test executable for the tool-side state machine):

Case What it covers
lookup_unknown_event_returns_nullopt base read-miss
lookup_null_event_returns_nullopt null sentinel
record_then_lookup_round_trips basic write/read
record_overwrites_prior_entry re-recording an event picks up the latest producer
record_null_event_is_silent_noop null event write is rejected silently
forget_event_removes_entry hipEventDestroy cleanup
forget_unknown_event_is_silent_noop destroying an unrecorded event is safe
distinct_events_are_independent no cross-talk between event keys
stash_then_consume_resolved_wait_drains side-table is single-shot per waiter
consume_unknown_corr_id_returns_nullopt no-resolve case
stash_zero_corr_id_is_silent_noop 0 is reserved as the no-dep sentinel
clear_all_resets_both_maps tool-init / test-isolation hook
concurrent_record_and_forget_does_not_corrupt 4-writer × 1000-op stress

Integration tests

tests/rocprofv3/hip-interstream-sync/ — 1 execute + 5 validate cases. A small HIP workload exercises a cross-stream hipStreamWaitEvent plus a host hipEventSynchronize, then the validator asserts:

  • the new CSV columns are present;
  • every hipStreamWaitEvent and hipEventSynchronize row carries a non-zero Wait_On_Correlation_Id whose value matches some prior hipEventRecord row's Correlation_Id (no dangling references);
  • waiter rows report a Wait_On_Stream_Id distinct from their own Stream_Id (cross-stream is the entire point);
  • non-wait/sync rows still carry the 0 sentinel (no leakage onto records that shouldn't carry a dependency);
  • the same checks pass against the JSON output;
  • resolved-dependency counts agree between CSV and JSON for the same trace.

Local validation

  • tool-tests gtest binary: 13/13 pass on an MI300X / MI250 debug build.
  • ctest -R hip-interstream-sync: 6/6 pass (1 execute + 5 validate).
  • Manual rocprofv3 --hip-trace --kernel-trace --output-format json csv pftrace on a 2-rank PyTorch DDP workload: JSON, CSV, and pftrace all show non-zero wait_on_correlation_id for the cross-stream waits; reconstructed dependency DAG matches the expected backward → reduce topology (78 of 149,872 records carry resolved metadata, ≈0.05%).
  • Output-size impact measured on the same DDP trace: CSV +5.6%, JSON +3.2%, pftrace +0.006% (pftrace only emits the extra args + Flow when there's a resolved dependency, so its overhead scales with the number of sync events rather than the total HIP call count).

Test Result

$ /path/to/build/bin/tool-tests
[==========] Running 13 tests from 1 test suite.
[----------] 13 tests from EventProducerMap
... 13/13 OK ...
[==========] 13 tests from 1 test suite ran. (1 ms total)
[  PASSED  ] 13 tests.

$ ctest --test-dir build -R hip-interstream-sync --output-on-failure
        Start 718: tests.integration.execute.rocprofv3-test-hip-interstream-sync
1/6 Test #718: ............................................................   Passed    0.31 sec
        Start 713: ...validate.rocprofv3-test-hip-interstream-sync.test_hip_csv_has_inter_stream_columns
2/6 Test #713: ............................................................   Passed    0.19 sec
... 6/6 OK ...
100% tests passed, 0 tests failed out of 6

Submission Checklist

  • Looked over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
  • Unit tests added.
  • Integration test added (gated on integration-tests label, runs under existing GPU-required test infrastructure).
  • Reviewed for breaking changes (CSV column-count widening — flagged above).
  • OTF2 emitter follow-up PR (deferred).
  • Rocpd schema follow-up PR (deferred).

Draft status

This PR is open on the fork (ajassani/rocm-systems) for review and CI iteration before being promoted to an upstream PR against ROCm/rocm-systems. The OTF2 and Rocpd follow-ups will gate the upstream submission.

ajassani added 3 commits May 24, 2026 03:11
…wait/sync records

rocprofv3 already shows per-record stream IDs on hipStreamWaitEvent and
hipEventSynchronize tool records, but it does not yet say *which* stream and
*which* hipEventRecord the waiter is actually depending on. This makes it
hard for users to reconstruct critical-path / cross-stream dependency graphs
from a captured trace without re-running the workload under a separate
analysis pass.

This change adds an event-lifecycle tracking layer inside the tool library
and surfaces the resolved producer on the waiter's tool record in three of
the existing output formats (Perfetto, JSON, CSV). OTF2 and rocpd are
intentionally left for a follow-up PR because both involve schema-level
changes (pre-declared OTF2 attribute IDs, SQL migration) that deserve
separate review.

Mechanism
=========

A new utility in source/lib/rocprofiler-sdk-tool/event_producer_map.{hpp,cpp}
maintains two process-global, shared/unique-locked maps using the existing
common::Synchronized + common::static_object building blocks:

  * producer map:       hipEvent_t -> { stream_id, hipEventRecord corr_id }
                        written on PHASE_EXIT of hipEventRecord (+ _spt +
                        hipEventRecordWithFlags), erased on hipEventDestroy
  * resolved-wait map:  waiter corr_id -> { wait_on_stream, wait_on_corr_id }
                        written on PHASE_EXIT of hipStreamWaitEvent (+ _spt)
                        and hipEventSynchronize after a producer lookup,
                        drained at buffer-flush time

A dedicated callback context is registered on
ROCPROFILER_CALLBACK_TRACING_HIP_RUNTIME_API filtered to just the seven
operation IDs involved, gated on hip_runtime_api_trace so there is no
overhead when HIP tracing is off.

The resolved-wait map is necessary because the waiter's tool record is
materialized at buffer-flush time -- a different code path from where the
typed wait/sync args are accessible. Bridging the two via the waiter's
correlation_id keeps the producer lookup at callback time (when the args
are live) and the record population at flush time (when the tool record
struct is being built).

Tool record extension
=====================

tool_buffer_tracing_hip_api_ext_record_t (lib/output/stream_info.hpp) gains
two fields with backward-compatible defaults:

  rocprofiler_stream_id_t wait_on_stream         = {};   // 0 = none
  uint64_t                wait_on_correlation_id = 0;    // 0 = none

A 4-arg constructor variant accepts the resolved info; the existing 2-arg
constructor is unchanged so non-wait HIP API records continue to work
without modification. The cereal save() helper picks up the new fields so
JSON output gets them for free.

Output formats
==============

  * Perfetto: hipStreamWaitEvent / hipEventSynchronize slices gain
    `wait_on_stream_ID` and `wait_on_corr_id` arg fields and, when
    wait_on_correlation_id is non-zero, a second
    Flow::ProcessScoped(wait_on_correlation_id) so the dependency renders
    as an arrow from the producing hipEventRecord call to the waiter.

  * JSON: every hip_api buffer record now carries wait_on_stream and
    wait_on_correlation_id alongside the existing stream_id. Zero values
    indicate "no resolved dependency".

  * CSV: a HIP-specific 10-column encoder (hip_api_csv_encoder) is added
    so the HIP CSV gains Stream_Id (previously not surfaced),
    Wait_On_Stream_Id, and Wait_On_Correlation_Id columns appended after
    the existing 7. Other domains keep the 7-column api_csv_encoder
    unchanged.

Validation
==========

A minimal HIP workload (2 streams, hipEventRecord/hipStreamWaitEvent cross-
stream edge, plus a separate host-side hipEventSynchronize) was run under
rocprofv3 with --hip-trace --output-format json csv. The JSON output has
the wait_on_correlation_id field on every hip_api record (default 0) and
non-zero on exactly the 2 sync records. The CSV rows for those records
show the waiter's stream and the producer's stream + corr_id correctly
linked back to the originating hipEventRecord calls.
Adds unit and integration coverage for the wait_on_stream /
wait_on_correlation_id fields introduced in the previous commit.

Unit tests (source/lib/tests/tool/event_producer_map.cpp, 13 cases):

- record / lookup round-trip, including overwrite semantics
- null/zero sentinel handling (no-op, never inserted)
- forget removes producer entries; lookup of unknown returns nullopt
- stash + consume drains the resolved-wait side-table (single-shot)
- clear_for_testing wipes both maps
- 4-writer x 1000-op stress to surface any locking regression

The test binary compiles event_producer_map.cpp directly into the
gtest executable (no link to librocprofiler-sdk-tool.so) so each
test owns its own translation-unit-local static maps and the suite
stays hermetic. Wired into source/lib/tests/CMakeLists.txt via
add_subdirectory(tool).

Integration test (tests/rocprofv3/hip-interstream-sync, 5 pytest
cases driven by rocprofv3 + a HIP test app):

The workload (tests/bin/hip-interstream-sync/main.cpp) issues a
cross-stream dependency

  streamA: kernel -> hipEventRecord(eAB) -> kernel
                       |
                       v hipStreamWaitEvent(streamB, eAB)
  streamB:                 wait    -> kernel

and a separate hipEventRecord/hipEventSynchronize pair, then
destroys all events and streams.

The validator runs rocprofv3 --hip-trace --kernel-trace with json,
csv, and pftrace outputs against that binary, then asserts:

  - the HIP CSV exposes the new Stream_Id, Wait_On_Stream_Id, and
    Wait_On_Correlation_Id columns
  - every hipStreamWaitEvent and hipEventSynchronize row carries a
    non-zero Wait_On_Correlation_Id whose value matches some prior
    hipEventRecord row's Correlation_Id (no dangling references)
  - waiter rows report a Wait_On_Stream_Id distinct from their own
    Stream_Id (cross-stream is the entire point)
  - non-wait/sync rows still carry the 0 sentinel (no leakage)
  - the JSON hip_api records carry wait_on_stream and
    wait_on_correlation_id with the same cross-checks
  - resolved-dependency counts agree between the CSV and JSON
    output formats for the same trace

Both test apps and the validator are wired into
tests/bin/CMakeLists.txt and tests/rocprofv3/CMakeLists.txt.
Pre-review polish on the inter-stream event-sync metadata feature so the
upstream review focuses on the design rather than mechanical concerns.

Hot-path guard on the buffer-flush side-table lookup
----------------------------------------------------

buffered_tracing_callback was unconditionally calling
event_producer::consume_resolved_wait() for every HIP API buffer record.
The side-table is only ever populated for hipStreamWaitEvent /
hipStreamWaitEvent_spt / hipEventSynchronize records, so the lookup was
a guaranteed miss (with a write-lock acquisition) on ~99.95% of records
on a representative DDP workload. Gate the lookup on the record's
operation kind so the cost on every other HIP API call is zero.

Exception safety in the callback
--------------------------------

The dependency callback runs synchronously inside the SDK dispatch path
and mutates std::unordered_map state, which can throw std::bad_alloc
under memory pressure. An uncaught exception would propagate into the
user's HIP call site. Wrap the callback body in a try/catch and log via
ROCP_WARNING on the unhappy path.

Reattach hygiene + log visibility
---------------------------------

The producer / resolved-wait maps live in common::static_object and
persist across tool_init -> tool_detach -> tool_init cycles. hipEvent_t
pointers can be reused across sessions, which could cause false-positive
dependency resolutions on reattach. Clear both maps at tool_init right
before the dependency context is created. Also emit a one-shot
ROCP_INFO so users can see the service was enabled in the tool init log.

Header tidy
-----------

Rename event_producer::clear_for_testing() to clear_all() and document
that it is also used at tool_init for reattach state. The old name
was misleading now that the function has a production caller.

Validation
----------

  * tool-tests (gtest): 13/13 pass, including the renamed
    EventProducerMap.clear_all_resets_both_maps case.
  * rocprofv3-test-hip-interstream-sync (integration): 6/6 pass
    (1 execute + 5 validate).
  * Re-ran the 2-rank DDP workload under rocprofv3 with --hip-trace
    --kernel-trace --output-format json csv -- same resolved-dependency
    counts as before the guard (149,872 HIP API records, 246 with a
    non-zero Stream_Id, 78 with a non-zero Wait_On_Correlation_Id);
    metadata fidelity unchanged, just less locking on the hot path.
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