Replies: 1 comment
|
Thank you for the detailed design. Regarding the open questions:
|
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
[RFC] ROCm profiler annotation support: ROCTX markers in XProf / TensorBoard
Date: 2026-08-05
Author: cj401-amd <chunyjin@amd.com>
Status: Proposed
Target: openxla/xla
mainSummary
This RFC proposes upstreaming ROCTX marker support into OpenXLA's profiler
stack, giving ROCm the same annotation capability that CUDA has had via NVTX
since TF 2.x. The change is structured as three layers:
roctx_utils.ccimplements the existingnvtx_utils.hinterface for ROCm, so every call site that today emits NVTX on CUDA
automatically emits ROCTX on ROCm with no new API.
rocm_tracer.ccregistersROCPROFILER_CALLBACK_TRACING_MARKER_CORE_APIto capture roctx ranges and marks as
Generictrace events./host:ROCTXXPlane is collected and mergedinto
/host:CPUinPostProcessSingleHostXSpacefollowing the identicalpattern used for NVTX, so TensorBoard's "ROCTX Threads" rows appear in the
same process lane as CUDA's "NVTX Threads" rows.
The change is ROCm-conditional at build time (Bazel
if_rocm), so it has zerocost on CPU/CUDA/TPU builds. It does not alter any public API, proto schema,
or XPlane format for non-ROCm targets.
Motivation
The problem
jax.profiler.TraceAnnotationandtf.profiler.experimental.Traceallowusers to annotate regions of their Python program. On CUDA, the annotation is
emitted both as an
AnnotationStackpush (attaches the label to kernel eventsas a
kTfOpstat) and as an NVTX range (captured by CUPTI and shown inTensorBoard as "NVTX Threads" rows).
On ROCm today, only the
AnnotationStackpath is active. Users seekTfOplabels on kernel events but get no visible timeline rows in TensorBoard — the
equivalent of watching a CUDA trace without the green annotation bars.
Third-party ROCm profiling tools (rocprof, Omnitrace, rocprofiler-sdk itself)
use the ROCTX library as the native annotation protocol, so the XLA stack is
the only consumer that does not participate.
Why it belongs in upstream
nvtx_utils.his an abstract interface that lives intsl/profiler/lib/.The CUDA implementation is
nvtx_utils.cc. Addingroctx_utils.ccasthe ROCm implementation of the same interface is the natural completion of
the abstraction the XLA team already designed.
CuptiTracer) and the corresponding XPlane mergein
post_process_single_host_xplane.ccestablished the pattern this changefollows exactly. A ROCm-only fork would diverge from that pattern
indefinitely.
TraceMeWrapper(the Python binding forjax.profiler.TraceAnnotation) wasextended to call
PushAnnotation/PopAnnotation. That change touchesxla/python/profiler.cc, which is upstream-only code.Design
Emitter:
roctx_utils.cctsl/profiler/lib/nvtx_utils.hdeclares a vendor-neutral annotationinterface:
The existing
nvtx_utils.ccimplements this interface for CUDA usingnvtx3/nvToolsExt.h. The newroctx_utils.ccimplements the same interfacefor ROCm using
rocprofiler-sdk-roctx/roctx.h.The key design decision is a dual-push in
RangePush/RangePop:This preserves both downstream consumers: the
kTfOpstat on kernel events(which requires
AnnotationStack) and the visible timeline row in TensorBoard(which requires a captured range event). Separating them would require callers
to manage two distinct push/pop sequences, which is error-prone and breaks the
single-call contract expected by
scoped_annotation.h.DefaultProfilerDomain()returns a non-null sentinel. This is requiredbecause
scoped_annotation.husesdomain != nullptrto gate theRangePush/RangePoppath vs. theAnnotationStack-only stub path. ROCmhas no domain concept; the sentinel value is never dereferenced or passed to
any roctx API.
Listener:
rocm_tracer.ccRocmTracer::InitProfiling()registersROCPROFILER_CALLBACK_TRACING_MARKER_CORE_APIalongside the existing HIP APIand kernel dispatch callbacks. The new
MarkerCallbackhandles threeoperations:
roctxRangePushARoctxFrame{label, start_timestamp}onto a per-thread stackroctxRangePopGenericRocmTracerEventwith start/end timestamps androctx_rangefield setroctxMarkAGenericevent (start == end)Per-thread state is protected by
roctx_stack_mutex_. Label strings areinterned in
roctx_strings_(anabsl::node_hash_set) for pointer stabilityand deduplication across multiple pushes of the same label.
Genericevents bypassApiActivityInfoExchange()and are routed directlyto
PerDeviceCollectorviastandalone_events_. This is correct becauseROCTX ranges are host-side events with no GPU activity counterpart; they
carry no correlation ID and require no merge with activity records.
Lock ordering (documented in
rocm_tracer.hto prevent future deadlock):Enable()is the sole exception: it holdscollector_mutex_before acquiringthe roctx mutexes, which is safe because no callback can call
collector()while
Enable()holdscollector_mutex_.XPlane routing
A new XPlane constant is added:
PerDeviceCollector::Export()routesGenericevents to the/host:ROCTXplane. In
PostProcessSingleHostXSpace, the ROCTX plane is merged into/host:CPUfollowing the identical pattern used for NVTX:TensorBoard displays the resulting merged lines as "ROCTX Threads" rows within
the process, visually equivalent to CUDA's "NVTX Threads" rows.
TraceMeWrapperchanges (xla/python/profiler.cc)TraceMeWrapperis the Python binding forjax.profiler.TraceAnnotation.It previously created only a
TraceMeCPU event. Two additions:tsl::profiler::PushAnnotation(annotation_name_),which routes through
roctx_utils.cc::RangePushon ROCm (ornvtx_utils.cc::RangePushon CUDA).~TraceMeWrapper()destructor callsStop()to ensurePopAnnotationfires even when the object is used outside a Python context manager
(e.g.,
t = jax.profiler.TraceAnnotation("name"); ...withoutwith).A
stopped_guard makesStop()idempotent so the context manager's__exit__and the GC-driven destructor cannot double-pop.Build system
The
roctx_utils.cctarget is linked viaif_rocm_is_configured:On non-ROCm builds,
roctx_utils.ccis never compiled and therocprofiler_sdk_roctxtarget is never pulled in.Alternatives considered
A. Separate
roctx_utils.hinterface instead of implementingnvtx_utils.hRejected.
nvtx_utils.his deliberately abstract and already has exactlythe operations ROCTX needs. A separate interface would duplicate call sites
in
scoped_annotation.handTraceMeWrapper, create a new abstraction thatcallers must track, and prevent the automatic "CUDA NVTX → ROCm ROCTX"
equivalence at every existing annotation point.
B. Implement via
rocprofsystem-level tracing only, no XLA listenerRejected. System-level profilers (rocprof v3, Omnitrace) can capture ROCTX
ranges independently, but XProf/TensorBoard's profiler plugin reads XSpace
protos produced by XLA's own collector. Without the
MarkerCallbacklistener,ROCTX ranges never enter the XSpace and TensorBoard never shows them —
defeating the goal of parity with the CUDA/TensorBoard experience.
C. Reuse
AnnotationStacktext as the ROCTX label withoutroctxRangePushARejected.
AnnotationStackis read in the HIP API callback and attached tokernel events; it does not produce timeline events. Without an actual roctx
push,
MarkerCallbacknever fires,/host:ROCTXremains empty, and theTensorBoard annotation bars do not appear.
Impact assessment
if_rocm_is_configured.kRoctxPlaneNameis a string constant; the plane appears only in ROCm profiles./host:CPUplanes; ROCTX lines appear as additional rows under the existing process lane.MarkerCallbackpath is additive; existingHIP_APIand kernel dispatch paths are unchanged.Enable()is the only site that acquirescollector_mutex_before the roctx mutexes; all callback paths hold only one mutex at a time.MarkerCallbacksilently drops an unmatchedroctxRangePop(empty stack) with aVLOG(2)trace, matching typical defensive handling in annotation stacks.roctxMarkAmessage is treated as an empty string; the event is dropped. Verified by testMarkerCallbackNullMessageSafelyIgnored.Testing
All tests run on a CPU-only host (no GPU required for the unit suite).
New unit tests
rocm_tracer_test.cc:MarkerCallbackPushPopEmitsRoctxRange— end-to-end: push + pop produces oneGenericevent with correct label, start, and duration.MarkerCallbackMarkEmitsInstantaneousEvent— mark produces event withstart == end.MarkerCallbackUnmatchedPopIsIgnored— pop with empty stack does not crash or produce a spurious event.MarkerCallbackNullMessageSafelyIgnored— null label inroctxMarkAdoes not crash.MarkerEventAppearsInExportedXSpace— exportedXSpacecontains/host:ROCTXplane with the expected event.RealRoctxCallsProduceNvtxRangeInXSpace— integration test against the reallibrocprofiler-sdk-roctx.so; assertskNVTXRangestat on exported events.roctx_utils_test.cc(13 tests):AnnotationStackand roctx paths exercised).GetCurrentRoctxLabelthread-safety and copy-safety.rocm_tracer_test.cc(existing, extended):AnnotationMapStoresRoctxRange—roctx_rangefield stored alongside annotation text.AnnotationMapRoctxRangeEmptyWhenNotProvided— no regression when only annotation text is present.AnnotationMapStoresRoctxRangeWhenAnnotationEmpty— standalone ROCTX annotation (noAnnotationStacktext) still produceskNVTXRange.On-GPU validation (ROCm hardware required)
RealRoctxCallsProduceNvtxRangeInXSpaceinrocm_tracer_test.cclinks againstlibrocprofiler-sdk-roctx.soand verifies that real roctx calls flow end-to-end through the listener.jax.profiler.TraceAnnotation("my_region")produces visible "ROCTX Threads" rows in TensorBoard on an MI300X system.Implementation plan
The implementation is complete and available at
ROCm/xla:cj/rocm-profiler-roctx-markers-with-emitter-pr0(rebased ontoopenxla/xla:mainas of 2026-08-05).The three commits can be submitted as a single PR or as a stack:
tsl/profiler/lib/roctx_utils.cc,BUILD.tplnvtx_utils.himplementation for ROCmrocm_tracer.cc/h,rocm_collector.cc/h,rocm_tracer_utils.cc/h,xplane_schema.cc/h,post_process_single_host_xplane.ccxla/python/profiler.cc,xla/python/BUILDTraceMeWrapper: dual-push forjax.profiler.TraceAnnotationOpen questions
nvtx_utils_implselect vs. separate target: The current implementationuses a
select()in the existingnvtx_utils_impltarget to swap inroctx_utils.ccon ROCm. An alternative is a separateroctx_utils_impltarget composed at the link layer. Either works;preference from the XLA build-system owners is welcome.
kNVTXRangestat name on ROCm events: ROCTX range labels are currentlystored under the
kNVTXRangeXStat key (matching CUPTI) so TensorBoard'sexisting parser reads them without changes. A new
kROCTXRangekey wouldbe more accurate but would require a TensorBoard plugin update. The current
choice favors zero-change adoption; feedback welcome.
Domain support: ROCTX has no domain concept (unlike NVTX which has
nvtxDomainCreate). TheProfilerDomainHandleparameter inroctx_utils.ccis ignored. If upstream adds domain-specific annotationpaths that distinguish user vs. framework ranges, ROCm will need a
different mechanism (e.g., label prefixing).
NameStreammapping: The current implementation mapsNameStream(stream, name)toroctxNameHipStream, which requires ahipStream_t. TheStreamHandleopaque type passed throughnvtx_utils.his cast directly. This works today but couples the abstraction to the
HIP type; a more principled mapping may be worth discussing.
All reactions