Skip to content

Triggered SDMA Plans - #573

Open
ariscript wants to merge 6 commits into
ROCm:mainfrom
ariscript:arprakas/triggered-sdma
Open

ariscript wants to merge 6 commits into
ROCm:mainfrom
ariscript:arprakas/triggered-sdma

Conversation

@ariscript

Copy link
Copy Markdown
Member

Adds a way for a running kernel to hand data to the copy engines (SDMA) without spending compute
units on the transfer. The host pre-arms copy-engine chains that wait on gates; a kernel opens a
gate once its data is written, and the copy engine copies that data to every peer and signals its
arrival there.

kernel writes a batch ──► publish(batch) opens the gate
          copy engine:  POLL_REGMEM(ready >= epoch) ─► COPY × segments ─► ATOMIC(peer arrival += 1)
peer kernel:  wait(batch) ◄── arrival

This is the plan layer only implemented with a Triton API so far. Gluon support and triggered collectives
can be added on top of this functionality.

API

ctx = iris.iris(heap_size, allocator_type="vmem_chunked")
schedules = [chunks(out[r * M : (r + 1) * M], num_batches, base=out) for r in range(world_size)]
plan = TriggeredPlan(ctx, out, schedules)      # collective: allocates gates, arms epoch 1

view = plan.begin_device_epoch()
producer[grid](..., view)                      # TriggeredView.initialize(view).publish(batch)
plan.end_device_epoch()                        # every batch delivered everywhere
plan.next_epoch()                              # barrier, re-arm, barrier
  • Schedules list each rank's batches (ordered groups of 1D or 2D segments behind one gate) in
    the order its producer completes them. Builders: whole, chunks, waves (one batch per wave of
    a persistent tiled kernel). A batch can have several contributors; the last publish opens it.
  • Device side (TriggeredView): publish, arrived, wait, reusable.

Design notes

  • Arming uses rocm-xio's host-initiated queue to each peer: POLL_REGMEM (func 5, >=), one
    COPY_LINEAR or sub-window copy per segment, then ATOMIC ADD_RTN_64 on the peer's arrival gate.
  • Ordering. Chains on one queue run strictly in arming order, so batches should be listed in
    the order they complete (a persistent kernel's waves complete in order by construction). We keep
    one queue per peer, matching rocm-xio's design. Plans sharing queues must begin and abort in
    arming order, and while chains are armed on a peer's queue, Iris.put / put_tile / put_tiles /
    quiet to that peer raise instead of queueing behind them.
  • Memory ordering. publish is s_waitcnt vmcnt(0) + CTA barrier + a release atomic at system
    scope; consumers use an acquire at system scope. Measured on MI300X and MI350X; see
    docs/conceptual/triggered-sdma.md.
  • Completion. end_device_epoch() checks completion with a small kernel that reads peers'
    arrival gates, rather than quiet(), which waits for everything on the queue (including other
    plans' parked chains).
  • Gates are 64-byte rows (ready, arrival, completion, error, contributor counter),
    polled with >= on the low 32 bits and never reset; a plan is limited to 2³² − 1 epochs.
  • Plans require allocator_type="vmem_chunked" (or "vmem"). The torch allocator re-imports
    peer heaps at new addresses on every symmetric allocation (establish_peer_access destroys the old
    imports), so a chain armed before an allocation would hold stale peer addresses. VMem keeps peer
    addresses fixed, including as the chunked heap grows. The torch allocator is rejected.

Testing

On 8× MI300X (gfx942):

  • New triggered tests at 2, 4 and 8 ranks: 1D batches with several segments each, 2D waves with
    clipped edge tiles, 8 contributors releasing one 4 MiB gate over 10 epochs (stale-data check),
    head-of-line ordering, misuse cases, heap growth while a plan is armed, a buffer spanning a heap
    chunk boundary, and the host put(wait_flag=, signal_flag=) path.
  • GPU-only tests of the device view, and byte-level tests of the copy, poll and NOP emitters.
  • Full unit suite at 2 ranks: 3119 passed, 46 skipped.

ariscript and others added 4 commits October 7, 2026 15:04
Only the atomic emitters had byte-level tests. Check COPY_LINEAR,
POLL_REGMEM and NOP against the layouts in the Linux amdgpu SDMA 4.4
driver, writing into an ordinary buffer rather than a queue.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The host pre-arms copy-engine chains that wait on gates; a kernel opens
a gate once its data is written, and the copy engine copies it to every
peer and signals its arrival there, without spending compute units.

- TriggeredPlan arms epoch 1 when created, then runs
  begin_device_epoch / end_device_epoch / next_epoch per epoch, plus
  run, abort and destroy. Gates are 64-byte rows polled with >=
  on their low 32 bits and never reset.
- Schedules list each rank's batches (ordered groups of 1D or 2D
  segments behind one gate) in the order its producer completes them.
  Builders: whole, chunks, waves.
- Chains are armed through rocm-xio's host-initiated queue to each
  peer: POLL_REGMEM, one copy per segment, ATOMIC ADD_RTN_64 on the
  peer's arrival gate. All of these packets have run on gfx942/gfx950.
- TriggeredView is the device side: publish (the last of a batch's
  contributors opens its gate), arrived, wait, reusable.
- end_device_epoch checks completion with a kernel reading peers'
  arrival gates, since quiet() waits for everything on the queue.
- Chains on one queue run in arming order, so plans sharing queues
  must begin and abort in arming order, and Iris.put/put_tile/
  put_tiles/quiet raise while chains are armed on that peer's queue.
- Plans need stable peer mappings: the torch allocator re-maps peer
  heaps on every allocation, so it is rejected; use vmem_chunked or
  vmem.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A conceptual page on the model, the kernel author's ordering contract
and the measured GPU-to-SDMA ordering, and an API reference page.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Bandit flags SHA1 as a weak hash (B324, high severity), which fails the
security scan. The digest only checks that every rank passed the same
schedules, so any hash works.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@ariscript
ariscript marked this pull request as ready for review October 8, 2026 15:15
Copilot AI balanced review requested due to automatic review settings October 8, 2026 15:15

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.


## Model

- A **segment** is one copy: a byte range, or a 2D sub-window (rows of a strided tensor). It is

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We need to think a bit more about the naming/new concepts introduced here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, this was mostly intended as a prototype to check if this is going in the right direction and pivot if necessary.

Each is a scalar atomic: it runs on one lane, and the compiler hands the
result to the other waves through LDS behind a CTA barrier. A polling
loop that exits on a per-wave condition, such as a clock read, makes the
waves leave after different numbers of iterations. The barriers then no
longer pair up, so waves read the result before it is written (and read
data before it arrives) or deadlock. Seen on MI300X with a consumer
bounded by s_memrealtime; bounding the loop by a poll count fixes it.

Stated in the kernel author's contract and in TriggeredView.arrived.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The vmem_chunked heap frees an allocation when its tensor is garbage
collected, independently on each rank. Frees processed in a different
order (its free lists are LIFO), or a reference cycle collected on one
rank before another, leave the ranks with different free lists, and
the same allocation then lands at different offsets. Every peer address
a plan arms is then wrong, silently: copies land elsewhere in the peer's
heap while arrivals still look correct if the gates happen to match.

Check both offsets across ranks when the plan is created. Marked FIXME:
it works around the allocator and goes once allocation is symmetric.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@ariscript
ariscript requested a review from efric as a code owner October 9, 2026 18:53

This branch has not been deployed

No deployments
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.

3 participants