Skip to content

feat(clr): add RetiringSignalTracker for device-memory queue signals (part 2) - #13507

Merged
renjithravindrankannath merged 1 commit into
developfrom
users/saleelk/amdSignalV2-clr-step3
Oct 29, 2026
Merged

renjithravindrankannath merged 1 commit into
developfrom
users/saleelk/amdSignalV2-clr-step3

Conversation

@saleelk

@saleelk saleelk commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Part 2 of a series moving CLR completion signals to the ROCr batch API
and device memory. This part covers the queue completion signals.

DEBUG_CLR_DEV_SIGNALS (default off) places each queue's completion
signals in device memory when the device has a large BAR and no
profiler tool is attached, the same condition as the graph signals, in
the same pool (uncached on gfx94x/gfx95x, coarse-grained on gfx125x).
Every host access to those signals then crosses PCIe, so VirtualGPU
uses a new RetiringSignalTracker, which proves that a signal's last use
finished from host-side state instead of reading the signal, and
re-arms signals ahead of use in batches. Otherwise VirtualGPU keeps
LegacySignalTracker. Device-memory signals, graph and queue, are
HIP-only: OpenCL keeps host signals and LegacySignalTracker.

RetiringSignalTracker:

  • Retirement. Each use records its AQL slot, whether the packet had the
    barrier bit, and the HW queue generation. A use published with the
    barrier bit is a fence: its completion proves that every earlier
    packet on the HW queue completed. The packet processor may advance
    the read index before a packet completes (HSA System Architecture
    2.8.3), so the read index only nominates the newest fence it passed,
    and one load of that fence's signal confirms it. Reading the newest
    fence in a batch (ProbeFence) likewise retires the whole batch with
    one load. A HW queue change (NoteQueueChange) starts a new
    generation.
  • Arming. Reclaim walks the ring from the oldest use, re-arms retired
    slots with direct stores and one HDP flush per 32 slots, and stops at
    the first busy one. A slot whose signal is still shared (an event, a
    GPU wait or a pending handler) gets an armed spare instead; the old
    signal is parked and reused once the tracker holds the last
    reference. A ring about to reuse a busy slot grows, up to
    16 x ROC_SIGNAL_POOL_SIZE, before waiting on the oldest signal.
  • Timing. A use's timing is collected before its signal is re-armed,
    so no Timestamp lists an armed signal. A command whose signals wrap
    the ring, such as a graph launch under the built-in profiler, would
    otherwise have its async handler wait on a re-armed signal while
    holding the Timestamp the next use needs, and read the next use's
    timestamps.
  • Timing-only uses. A dispatch that takes a signal only because the
    built-in profiler times it gets an armed host-memory signal from a
    separate pool (PlaceForUse), so the handler reads its value and
    timestamps without crossing PCIe. A use that a GPU waiter or a sync
    may read swaps a device-memory signal back into the slot. A slot
    keeps its signal when the replacement's allocation fell back to host
    memory.
  • GPU waits. Signals returned by WaitingSignal for a GPU waiter are
    pinned with a reference until the consuming packets have passed and
    every earlier use has retired, so re-arming cannot hide the old use
    from the waiter. A drained queue (NoteQueueDrained) releases them.
  • Store ordering. Host stores to device-memory signals are
    write-combined. FlushStores fences them, so a peer GPU that waits on a
    signal over XGMI reads the re-arm, and flushes HDP only when the AQL
    rings are in system memory: a device-memory ring receives the packets
    that use the signals through the same BAR after the stores, in order.
    A signal handed to a ROCr copy is flushed when its use begins, since
    ROCr's SDMA and blit rings are in system memory.
  • The async handler holds a reference to the signal until it has
    processed it, and drops it before the VirtualGPU reference and inside
    the async-handler scope, so neither the signal nor the HW queue is
    destroyed on the handler thread outside the deferral path.

With DEBUG_CLR_DEV_SIGNALS only. With the flag off, every queue and
ManagedBuffer signal is created with hsa_signal_create and a
direct-dispatch queue batches DEBUG_CLR_MAX_BATCH_SIZE commands, as
before:

  • HwQueueTracker::Create builds the completion-signal ring (64) and the
    interrupt pool (5) as two device-memory batches.
  • The kernarg and staging ManagedBuffer chunk signals are a
    device-memory batch too. A chunk signal there is re-armed with a
    direct store, flushed only when the AQL ring is in system memory,
    since a store through ROCr reads it back across PCIe.
  • A direct-dispatch queue completes its submission batch after 64
    commands instead of DEBUG_CLR_MAX_BATCH_SIZE (1000), unless that
    flag is set (Device::SubmissionBatchLimit). A hipEventRecord marker
    doesn't own a batch, so in a record/sync loop every command keeps its
    signal referenced until the batch completes. The tracker parks those
    signals, and refills its pools with device allocations inside the
    timed intervals.
  • WaitForSignal skips its check before waiting for a device-memory
    signal, since the wait loads the value first and each load is a PCIe
    read. Host-memory signals keep the check.

Shared changes. These apply with DEBUG_CLR_DEV_SIGNALS off too:

  • ActiveSignal takes the AQL slot and barrier bit of the packet the
    signal completes; dispatchBarrierValuePacket reserves its slot before
    taking the signal.
  • Timing of a use known to be complete (after a CPU wait, or proven
    retired) skips the signal value check in CacheTimingData.
  • A ROCr submission that fails after issuing some of the sub-operations
    sharing one signal (DMA copyBufferRect line by line, SVM prefetch
    batch) waits for the issued ones (WaitForIssuedOps) before resetting
    the slot, with either tracker: their late decrements would otherwise
    complete the slot's next use early.
  • A blocking WaitForSignal returns as soon as its 100 us active phase
    sees completion, instead of also running the blocked wait.
  • The small-batch sync path (fix(clr): wait for a small-batch sync marker after releasing lock #13532) reads the marker's signal once:
    CompleteCpuWait checks the GPU-error state directly instead of
    reloading the signal, and HostQueue::finish skips the HW-event wait
    for a marker that enqueue already completed.

Known limitations with DEBUG_CLR_DEV_SIGNALS=1:

  • The event APIs read the event's signal and timestamps across PCIe,
    about 1.1 us per read (EventPerformance table below). The gain is in
    waits: completion lands in device memory, and GPU waiters poll local
    memory.
  • hipEventBlockingSync events spin, as they do with host signals: ROCr's
    busy-wait signals ignore the wait hint. With the flag on, each poll is
    a PCIe read.
  • Validated on MI300X only (PCIe to the host, XGMI between GPUs). IPC
    events are unaffected: they complete through a separate ROCr IPC
    signal.

Test (MI300X, container_saleel_1, image
rocm/ufb-private:vllm-0.26.0-ubuntu24.04-py3.14-nightlies-device-all-cdna-rocm10.1.0a20260826-568afb3a13,
ROCm SDK 10.1.0a20260826, ROCr built from this PR's develop base).
Host time per call in us, median of 2000:

API develop DEBUG_CLR_DEV_SIGNALS=0 DEBUG_CLR_DEV_SIGNALS=1
hipEventSynchronize (empty kernel, record, sync) 5.3 5.3 4.5-4.7
hipEventSynchronize, blocking-sync event 5.4 5.3 4.6-4.7
hipEventQuery (completed event) 0.08 0.08 1.2
hipStreamSynchronize (empty kernel, sync) 6.5 6.5 6.3-6.4
hipStreamSynchronize (completed work) 0.14 0.14 0.14
hipDeviceSynchronize (empty kernel, sync) 6.5 6.5 6.3-6.5

hip-tests EventPerformance (HIP_TEST_LEVEL=level_1), average in us. Each
timed call includes the harness's hipStreamSynchronize:

Benchmark develop DEBUG_CLR_DEV_SIGNALS=0 DEBUG_CLR_DEV_SIGNALS=1
Performance_hipEventRecord 6.6 6.6 8.2-8.5
Performance_hipEventSynchronize, default 6.1-6.4 6.3 8.5-8.9
Performance_hipEventSynchronize, blocking 6.2 6.1 8.5-8.8
Performance_hipEventElapsedTime 1.4 1.4 16.6-17.2
Performance_hipEventQuery 0.78 0.78 5.0-5.2
  • hip_launch_latency -k -g --verify passes with the flag off and on,
    with and without GPU_CLR_PROFILE_OUTPUT.
  • hipStreamWaitEvent on GPU1 for a GPU0 event recorded behind a spinning
    kernel never passed early (0 of 300 rounds), with the flag off and on.
  • hip-tests EventTest, StreamTest, GraphsTest1, GraphsTest2: failure
    sets identical to develop with the flag off and on.
  • rocvirtual.cpp, rocdevice.cpp and rocblit.cpp compile with
    ROCR_DYN_DLL defined.

JIRA ID : AIRUNTIME-2943

@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-steps branch from 42e5b1d to 157f83b Compare October 15, 2026 23:41
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from bbb9365 to ebbfd62 Compare October 15, 2026 23:41
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-steps branch from 6db9921 to e43445a Compare October 18, 2026 20:00
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from ebbfd62 to 1cea10e Compare October 19, 2026 06:28
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-steps branch from e43445a to ec68634 Compare October 19, 2026 23:35
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from 1cea10e to 6a9700d Compare October 19, 2026 23:35
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-steps branch from ec68634 to c3699a5 Compare October 19, 2026 23:42
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from 6a9700d to 70e190c Compare October 19, 2026 23:42
Base automatically changed from users/saleelk/amdSignalV2-clr-steps to develop October 21, 2026 05:25
@therock-pr-bot

therock-pr-bot Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

✅ All Policy Checks Passed

Check Status Details
📝 PR Description ✅ Pass —
⛔ Forbidden Files ✅ Pass —
🧪 Unit Test ⚠️ Warning Error: Source/code files changed without an accompanying unit test.
Expected: add at least one test file named like test_<name>.py / test_<name>.cpp (or <name>_test.*).
Current: code file(s) changed: projects/clr/rocclr/device/device.hpp, projects/clr/rocclr/device/rocm/rocblit.cpp, projects/clr/rocclr/device/rocm/rocdevice.cpp, projects/clr/rocclr/device/rocm/rocdevice.hpp, projects/clr/rocclr/device/rocm/rocvirtual.cpp (+4 more); no test file found
🚫 Draft PR 🔜 To Be Enabled —
🚩 Feature Flag 🔜 To Be Enabled —
📊 Code Coverage 🔜 To Be Enabled —

🎉 All policy checks passed!

📖 Need help? See the Policy FAQ for details on every check and how to fix failures.

🙋 Wish to Override Policy?

@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch 5 times, most recently from aff9968 to f1dd231 Compare October 27, 2026 05:39
@therock-pr-bot

Copy link
Copy Markdown

🚫 Please fix the failed policies before requesting reviews.

The following policy checks failed:

  • ❌ PR Description

The Not ready to Review label has been added to this PR.
Once all policies pass, the label will be removed automatically.

@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch 2 times, most recently from d4fce9a to 73c0dc0 Compare October 27, 2026 15:22
@saleelk
saleelk marked this pull request as ready for review October 27, 2026 15:23
@saleelk
saleelk requested a review from a team as a code owner October 27, 2026 15:23

@mangupta mangupta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-assisted pre-review (for @mangupta)

[SUGGESTION] Turning DEBUG_CLR_DEV_SIGNALS off does not restore the previous signal behavior

DEBUG_CLR_DEV_SIGNALS defaults to off, but several behavior changes apply to every queue regardless of the flag, including OpenCL queues:

  • HwQueueTracker::Create and ManagedBuffer::Create now go through batched Device::CreateSignals (batched=true) instead of per-signal hsa_signal_create.
  • ManagedBuffer chunk signals no longer pass the consumer agent list (previously 1, &agent).
  • hipEventSynchronize completes the submission batch and can run command callbacks on the syncing thread.
  • WaitForSignal's pre-check is removed.
  • CompleteCpuWait's completion check is changed.
  • dispatchBarrierValuePacket reserves its AQL slot before ActiveSignal.

So a regression in these paths cannot be ruled out by setting DEBUG_CLR_DEV_SIGNALS=0, and reverting the device-signal feature alone would not revert them.

Suggested fix: Either gate the batched allocation (and the dropped consumer agent) on QueueDeviceSignals(), or split the flag-independent changes into a separate PR so they can be reviewed and reverted on their own. Also document in the PR description that DEBUG_CLR_DEV_SIGNALS=0 does not restore the previous behavior.

Comment thread projects/clr/hipamd/src/hip_event.cpp Resolved
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from 73c0dc0 to f6162cc Compare October 27, 2026 16:40
@saleelk

saleelk commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Re: "Turning DEBUG_CLR_DEV_SIGNALS off does not restore the previous signal behavior". Thanks, addressed:

  • Batched allocation. Now gated on QueueDeviceSignals(). With the flag off, the queue ring, the interrupt pool and the ManagedBuffer chunk signals are created one at a time with hsa_signal_create as before. I checked with an LD_PRELOAD counter: no hsa_amd_signal_batch_create calls with the flag off.
  • ManagedBuffer consumer agent. Not a behavior change. The signals pass HSA_AMD_SIGNAL_AMD_GPU_ONLY, and with that attribute ROCr's SelectInterruptSignal never reads the consumer list: it always creates a DefaultSignal.
  • The rest are flag-independent on purpose: the removed WaitForSignal pre-check, the CompleteCpuWait check (fix(clr): wait for a small-batch sync marker after releasing lock #13532), the slot reservation in dispatchBarrierValuePacket, WaitForIssuedOps, and the hipEventSynchronize batch completion. Each fixes or speeds up both trackers. The PR description now has a section saying these apply with the flag off, so DEBUG_CLR_DEV_SIGNALS=0 doesn't restore the previous behavior in those paths. I'm happy to split them into a separate PR if you'd like them reviewed and reverted on their own.

@mangupta

Copy link
Copy Markdown
Contributor

🤖 AI-assisted pre-review (for @mangupta)

Re-reviewed the updated head (f6162cc7db16) — no new findings at or above the posting threshold.

@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from f6162cc to 8d2162f Compare October 28, 2026 01:52
@mangupta

Copy link
Copy Markdown
Contributor

🤖 AI-assisted pre-review (for @mangupta)

Re-reviewed the updated head (8d2162ffa154) — no new findings at or above the posting threshold.

@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch 2 times, most recently from 850c380 to 7366955 Compare October 29, 2026 00:37

@gandryey gandryey left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read-index retirement with the barrier-bit proof and batched re-arm look like the right direction, and the flag-gated rollout makes sense. Before approving I'd like to settle a few things, mostly about what changes when the flag is off.

  1. Flag-off path

These changes apply regardless of DEBUG_CLR_DEV_SIGNALS:

  • the ActiveSignal / slot changes in dispatchBarrierValuePacket;
  • skipping the value check in CacheTimingData;
  • removing the WaitForSignal pre-check;
  • hipEventSynchronize completing the submission batch.

Please add base vs PR numbers with DEBUG_CLR_DEV_SIGNALS=0 for the hip-tests EventPerformance benchmarks (HIP_TEST_LEVEL=level_1): Performance_hipEventQuery, Performance_hipEventSynchronize (default and blocking), Performance_hipEventElapsedTime and
Performance_hipEventRecord, plus an empty-kernel launch + hipStreamSynchronize round trip. Please also run the full hip-tests suites with the flag off, not only EventTest/StreamTest/GraphsTest1/2. If these shared changes aren't needed for the flag-off path, consider
moving them to a separate PR.

  1. Event API regressions

This is the table I'm looking at:

┌───────────────────────────────┬──────────┬─────────────────┐
│ Benchmark │ Base │ Current patch │
├───────────────────────────────┼──────────┼─────────────────┤
│ hipEventQuery │ 0.545 µs │ 1.469 µs (2.7×) │
├───────────────────────────────┼──────────┼─────────────────┤
│ hipEventSynchronize, default │ 6.73 µs │ 9.69 µs (1.44×) │
├───────────────────────────────┼──────────┼─────────────────┤
│ hipEventSynchronize, blocking │ 6.72 µs │ 9.69 µs (1.44×) │
├───────────────────────────────┼──────────┼─────────────────┤
│ hipEventElapsedTime │ 2.22 µs │ 8.05 µs (3.6×) │
└───────────────────────────────┴──────────┴─────────────────┘

Was this measured with the flag on or off? If off, these are regressions on the default path. If on, they're expected from CPU reads of device memory (~1 µs per PCIe read), and I'd suggest keeping event signals and timestamps in host memory, as the PR already does for
timing-only uses. Events are what the CPU queries and syncs on; the device-memory win is for GPU-side dependency waits.

  1. Blocking-sync events

The description reports hipEventSynchronize on a blocking-sync event dropping from 10.9 to 4.7 µs, the same as the default event. A blocking-sync wait sleeps on an interrupt, so it shouldn't match active polling. With the flag on, does a hipEventBlockingSync event
still sleep, or does it now spin? If it spins, that changes what the flag promises applications.

  1. Multi-GPU and IPC with the flag on

With completion and event signals in VRAM:

  • A cross-device hipStreamWaitEvent has the peer GPU read the owner's VRAM. Over XGMI that bypasses the owner's HDP, so a CPU re-arm isn't guaranteed visible to the peer. PCIe P2P goes through the owner's BAR/HDP and is probably fine.
  • IPC events are read by other processes.

The gating mentions large BAR and no profiler only. Please either gate the flag to a single GPU / non-IPC, or describe how cross-device and IPC waits are handled.

  1. Hardware coverage and CPU-wait cost

All data is MI300X. Please note which hardware the flag is validated on (Radeon/PCIe large-BAR parts behave differently with device-memory rings). Since CPU waits poll device memory, please add flag-on numbers for an empty-kernel round trip and for hipStreamSynchronize
on already-completed work. Each poll is a PCIe read, so these are where it costs.

  1. The parked-signal claim

For reference: on a ROCm 10.1 nightly (2026-08-26) on MI300X, the record → kernel → record → hipEventSynchronize loop does create ~3700 extra signals over 2000 iterations. But it costs only ~0.2 µs per iteration more than the same loop with hipStreamSynchronize (12.6
vs 12.4 µs average), with no visible outliers. Could you share the build and command that show the 75–575 µs outliers, so the improvement can be reproduced?

Summary: approval is fine if the flag-off numbers match base. Points 2–4 should be resolved, or written down as known limitations, before the flag is enabled anywhere.

@mangupta

Copy link
Copy Markdown
Contributor

🤖 AI-assisted pre-review (for @mangupta)

Re-reviewed the updated head (7366955da157) — no new findings at or above the posting threshold.

…(part 2)

Part 2 of a series moving CLR completion signals to the ROCr batch API
and device memory. This part covers the queue completion signals.

DEBUG_CLR_DEV_SIGNALS (default off) places each queue's completion
signals in device memory when the device has a large BAR and no
profiler tool is attached, the same condition as the graph signals, in
the same pool (uncached on gfx94x/gfx95x, coarse-grained on gfx125x).
Every host access to those signals then crosses PCIe, so VirtualGPU
uses a new RetiringSignalTracker, which proves that a signal's last use
finished from host-side state instead of reading the signal, and
re-arms signals ahead of use in batches. Otherwise VirtualGPU keeps
LegacySignalTracker. Device-memory signals, graph and queue, are
HIP-only: OpenCL keeps host signals and LegacySignalTracker.

RetiringSignalTracker:
- Retirement. Each use records its AQL slot, whether the packet had the
  barrier bit, and the HW queue generation. A use published with the
  barrier bit is a fence: its completion proves that every earlier
  packet on the HW queue completed. The packet processor may advance
  the read index before a packet completes (HSA System Architecture
  2.8.3), so the read index only nominates the newest fence it passed,
  and one load of that fence's signal confirms it. Reading the newest
  fence in a batch (ProbeFence) likewise retires the whole batch with
  one load. A HW queue change (NoteQueueChange) starts a new
  generation.
- Arming. Reclaim walks the ring from the oldest use, re-arms retired
  slots with direct stores and one HDP flush per 32 slots, and stops at
  the first busy one. A slot whose signal is still shared (an event, a
  GPU wait or a pending handler) gets an armed spare instead; the old
  signal is parked and reused once the tracker holds the last
  reference. A ring about to reuse a busy slot grows, up to
  16 x ROC_SIGNAL_POOL_SIZE, before waiting on the oldest signal.
- Timing. A use's timing is collected before its signal is re-armed,
  so no Timestamp lists an armed signal. A command whose signals wrap
  the ring, such as a graph launch under the built-in profiler, would
  otherwise have its async handler wait on a re-armed signal while
  holding the Timestamp the next use needs, and read the next use's
  timestamps.
- Timing-only uses. A dispatch that takes a signal only because the
  built-in profiler times it gets an armed host-memory signal from a
  separate pool (PlaceForUse), so the handler reads its value and
  timestamps without crossing PCIe. A use that a GPU waiter or a sync
  may read swaps a device-memory signal back into the slot. A slot
  keeps its signal when the replacement's allocation fell back to host
  memory.
- GPU waits. Signals returned by WaitingSignal for a GPU waiter are
  pinned with a reference until the consuming packets have passed and
  every earlier use has retired, so re-arming cannot hide the old use
  from the waiter. A drained queue (NoteQueueDrained) releases them.
- Store ordering. Host stores to device-memory signals are
  write-combined. FlushStores fences them, so a peer GPU that waits on a
  signal over XGMI reads the re-arm, and flushes HDP only when the AQL
  rings are in system memory: a device-memory ring receives the packets
  that use the signals through the same BAR after the stores, in order.
  A signal handed to a ROCr copy is flushed when its use begins, since
  ROCr's SDMA and blit rings are in system memory.
- The async handler holds a reference to the signal until it has
  processed it, and drops it before the VirtualGPU reference and inside
  the async-handler scope, so neither the signal nor the HW queue is
  destroyed on the handler thread outside the deferral path.

With DEBUG_CLR_DEV_SIGNALS only. With the flag off, every queue and
ManagedBuffer signal is created with hsa_signal_create and a
direct-dispatch queue batches DEBUG_CLR_MAX_BATCH_SIZE commands, as
before:
- HwQueueTracker::Create builds the completion-signal ring (64) and the
  interrupt pool (5) as two device-memory batches.
- The kernarg and staging ManagedBuffer chunk signals are a
  device-memory batch too. A chunk signal there is re-armed with a
  direct store, flushed only when the AQL ring is in system memory,
  since a store through ROCr reads it back across PCIe.
- A direct-dispatch queue completes its submission batch after 64
  commands instead of DEBUG_CLR_MAX_BATCH_SIZE (1000), unless that
  flag is set (Device::SubmissionBatchLimit). A hipEventRecord marker
  doesn't own a batch, so in a record/sync loop every command keeps its
  signal referenced until the batch completes. The tracker parks those
  signals, and refills its pools with device allocations inside the
  timed intervals.
- WaitForSignal skips its check before waiting for a device-memory
  signal, since the wait loads the value first and each load is a PCIe
  read. Host-memory signals keep the check.

Shared changes. These apply with DEBUG_CLR_DEV_SIGNALS off too:
- ActiveSignal takes the AQL slot and barrier bit of the packet the
  signal completes; dispatchBarrierValuePacket reserves its slot before
  taking the signal.
- Timing of a use known to be complete (after a CPU wait, or proven
  retired) skips the signal value check in CacheTimingData.
- A ROCr submission that fails after issuing some of the sub-operations
  sharing one signal (DMA copyBufferRect line by line, SVM prefetch
  batch) waits for the issued ones (WaitForIssuedOps) before resetting
  the slot, with either tracker: their late decrements would otherwise
  complete the slot's next use early.
- A blocking WaitForSignal returns as soon as its 100 us active phase
  sees completion, instead of also running the blocked wait.
- The small-batch sync path (#13532) reads the marker's signal once:
  CompleteCpuWait checks the GPU-error state directly instead of
  reloading the signal, and HostQueue::finish skips the HW-event wait
  for a marker that enqueue already completed.

Known limitations with DEBUG_CLR_DEV_SIGNALS=1:
- The event APIs read the event's signal and timestamps across PCIe,
  about 1.1 us per read (EventPerformance table below). The gain is in
  waits: completion lands in device memory, and GPU waiters poll local
  memory.
- hipEventBlockingSync events spin, as they do with host signals: ROCr's
  busy-wait signals ignore the wait hint. With the flag on, each poll is
  a PCIe read.
- Validated on MI300X only (PCIe to the host, XGMI between GPUs). IPC
  events are unaffected: they complete through a separate ROCr IPC
  signal.

Test (MI300X, container_saleel_1, image
rocm/ufb-private:vllm-0.26.0-ubuntu24.04-py3.14-nightlies-device-all-cdna-rocm10.1.0a20260826-568afb3a13,
ROCm SDK 10.1.0a20260826, ROCr built from this PR's develop base).
Host time per call in us, median of 2000:

| API | develop | DEBUG_CLR_DEV_SIGNALS=0 | DEBUG_CLR_DEV_SIGNALS=1 |
|---|---|---|---|
| hipEventSynchronize (empty kernel, record, sync) | 5.3 | 5.3 | 4.5-4.7 |
| hipEventSynchronize, blocking-sync event | 5.4 | 5.3 | 4.6-4.7 |
| hipEventQuery (completed event) | 0.08 | 0.08 | 1.2 |
| hipStreamSynchronize (empty kernel, sync) | 6.5 | 6.5 | 6.3-6.4 |
| hipStreamSynchronize (completed work) | 0.14 | 0.14 | 0.14 |
| hipDeviceSynchronize (empty kernel, sync) | 6.5 | 6.5 | 6.3-6.5 |

hip-tests EventPerformance (HIP_TEST_LEVEL=level_1), average in us. Each
timed call includes the harness's hipStreamSynchronize:

| Benchmark | develop | DEBUG_CLR_DEV_SIGNALS=0 | DEBUG_CLR_DEV_SIGNALS=1 |
|---|---|---|---|
| Performance_hipEventRecord | 6.6 | 6.6 | 8.2-8.5 |
| Performance_hipEventSynchronize, default | 6.1-6.4 | 6.3 | 8.5-8.9 |
| Performance_hipEventSynchronize, blocking | 6.2 | 6.1 | 8.5-8.8 |
| Performance_hipEventElapsedTime | 1.4 | 1.4 | 16.6-17.2 |
| Performance_hipEventQuery | 0.78 | 0.78 | 5.0-5.2 |

- hip_launch_latency -k -g --verify passes with the flag off and on,
  with and without GPU_CLR_PROFILE_OUTPUT.
- hipStreamWaitEvent on GPU1 for a GPU0 event recorded behind a spinning
  kernel never passed early (0 of 300 rounds), with the flag off and on.
- hip-tests EventTest, StreamTest, GraphsTest1, GraphsTest2: failure
  sets identical to develop with the flag off and on.
- rocvirtual.cpp, rocdevice.cpp and rocblit.cpp compile with
  ROCR_DYN_DLL defined.

JIRA ID : AIRUNTIME-2943
@saleelk
saleelk force-pushed the users/saleelk/amdSignalV2-clr-step3 branch from 7366955 to 9a50284 Compare October 29, 2026 02:46
@saleelk

saleelk commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

@gandryey

Thanks German. Updated the PR; numbers are in the description (MI300X, ROCm 10.1 nightly 2026-08-26, ROCr built from this PR's develop base).

  1. Flag off. hipEventSynchronize no longer completes the submission batch. That change was reverted after it caused the Windows graph-memory failures: the memory pool calls Event::synchronize on events whose stream is gone. The WaitForSignal pre-check is back for host-memory signals: dropping it had turned a cheap load of a completed signal into a wait call, which was the 0.03-0.1 us you'd see. With the flag off, EventPerformance (level_1) now matches develop: Query 0.78/0.78, ElapsedTime 1.4/1.4, Synchronize 6.1-6.4/6.3 (blocking 6.2/6.1), Record 6.6/6.6 us. The empty-kernel + hipStreamSynchronize round trip is 6.5/6.5 us. EventTest, StreamTest and GraphsTest1/2 have the same failure sets as develop; I'll run the rest of the hip-tests suites with the flag off and post the result. The remaining shared changes (slot reservation in dispatchBarrierValuePacket, the CacheTimingData check for proven-complete uses, WaitForIssuedOps) are listed under "Shared changes".

  2. Event API numbers. Your table is with the flag on. EventPerformance's CpuTimer also runs hipStreamSynchronize inside the timed section, so each benchmark pays for the marker's state and timestamp reads too: about 4 PCIe reads for Query (5.0-5.2 us) and about 14 for ElapsedTime (16.6-17.2 us). For now events stay in device memory, and this is documented as a known limitation of the flag. Moving event signals and timestamps to host memory, as the timing-only uses already do, is the natural follow-up if the flag gets enabled. It would give up the wait-side gain (hipEventSynchronize after a kernel 5.3 -> 4.5-4.7 us).

  3. Blocking-sync events. They spin in develop too. Event markers are GPU_ONLY signals, which ROCr creates as BusyWaitSignal, and BusyWaitSignal::WaitRelaxed ignores the wait hint (mwaitx polling, no interrupt). The flag doesn't change that, but each poll becomes a PCIe read. The old "10.9 -> 4.7 us" line was stale and is gone; blocking-sync and default events measure the same in all three columns now.

  4. Multi-GPU and IPC. I tested the XGMI case directly. A CPU store to GPU0 memory followed by a GPU1 read over XGMI: without a store fence, about 90-99% of GPU1's reads were stale, even 100 us later. That's the CPU's write-combining buffer, since the BAR is mapped write-combined. With an sfence and no HDP flush, 0 of 20000 reads were stale at 0-100 us delays. AQL submission already fences before the header and the doorbell, and re-arming happens inside a submission, so peers were covered. FlushStores now fences explicitly anyway, so its contract no longer depends on the caller (about 105 ns per re-arm batch of 32 uses). A cross-device test, GPU1 hipStreamWaitEvent on a GPU0 event recorded behind a spinning kernel, never passed early in 300 rounds with the flag on or off. IPC events complete through a separate ROCr IPC signal, so they aren't affected. Happy to share both tests.

  5. Hardware coverage. Validated on MI300X only (PCIe to the host, XGMI between GPUs); it's now stated in the description. Flag on: empty-kernel round trip 6.3-6.4 us (6.5 with the flag off), and hipStreamSynchronize on completed work 0.14 us either way, since that path doesn't touch a signal.

  6. Parked signals. Your measurement matches the flag-off path: there, each replacement is a cheap hsa_signal_create (~0.2 us/iteration). The 75-575 us outliers are flag-on only, where replacing a held signal means a device-memory allocation (64 signals ~75 us, 64 host-memory timing signals ~575 us). With the flag on, a queue now completes its batch every 64 commands instead of 1000. To reproduce the old behavior on this PR, set the limit explicitly: DEBUG_CLR_DEV_SIGNALS=1 DEBUG_CLR_MAX_BATCH_SIZE=1000 on a record -> empty kernel -> record -> hipEventSynchronize -> hipEventElapsedTime loop (2000 iterations) shows 12-13 iterations above 50 us, against 1-2 with the default.

@mangupta

Copy link
Copy Markdown
Contributor

🤖 AI-assisted pre-review (for @mangupta)

Re-reviewed the updated head (9a502842a3d0) — no new findings at or above the posting threshold.

@renjithravindrankannath

Copy link
Copy Markdown
Contributor

Ignoring MSI failure and bypassing to merge.

@renjithravindrankannath
renjithravindrankannath merged commit 48e8f6d into develop Oct 29, 2026
786 of 812 checks passed
@renjithravindrankannath
renjithravindrankannath deleted the users/saleelk/amdSignalV2-clr-step3 branch October 29, 2026 20:55
@systems-assistant

Copy link
Copy Markdown
Contributor

TheRock Submodule Bump Activity

Newest first

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants