Skip to content

Triggered plans: one counter per chunk, per-peer completion counters - #616

Merged
ariscript merged 7 commits into
mainfrom
arprakas/plan-gate-counters
Nov 3, 2026
Merged

ariscript merged 7 commits into
mainfrom
arprakas/plan-gate-counters

Conversation

@ariscript

@ariscript ariscript commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

First of three stacked follow-ups to #573, in the order agreed on that PR: gate layout (this PR),
then rounds (#617), then destinations and arrival groups (#618). They address @dsidler's review points
1 and 4.

What changes

  • Gates are one int32 symmetric tensor: a counter and an arrival counter per chunk, then a completion
    counter per peer.
  • The copy engine polls a chunk's counter against writers × round, so mark_ready is a single
    release atomic. There is no last-writer election and no separate ready word.
  • plan.counters is contiguous int32, so a kernel that signals per tile can increment it directly
    instead of calling TriggeredView.mark_ready, for example a tritonBLAS EpilogueConfig(counter_buffer=...)
    (Add SignalView and WaitView tritonBLAS#81). That works only when the kernel's counter numbering matches the schedule (only
    some map_type / schedule-builder pairs do) and its barrier waits for stores (Triton with
    [Backend] Add a new TritionGPU barrier triton-lang/triton#9025); the docs spell out both.
  • Every round, each peer queue's chains end with one copy-engine add on the sending rank's completion
    counter for that peer:
    • plan.wait() now reads only local memory; the progress kernel's remote reads are gone.
    • Kernels can wait inline with the new TriggeredView.sent() / wait_sent(), for example before
      overwriting their source.
    • The peers a rank sends to are one flag each in the handle tensor, so there is no rank limit.
    • Each peer's completion signal is queued separately, even after a failed arm, and the original error
      is the one raised. A peer whose signal cannot be queued is no longer waited for, and the plan's
      memory is kept (with a warning) rather than freed while its copies may still run.
  • wait() also waits until this rank's own chunks have all their marks, so it never checks the mark
    count before the producer has finished.
  • start() sets the round with a fill_ queued on the current stream. It used to assign an element of
    the handle tensor, which copies from the host and waited for all queued GPU work. Kernels that take
    the handle must run on that stream, or wait for it.
  • Limit: writers × round must stay at or below 2³¹ − 1 (over 7M rounds at 304 writers).

Testing (MI300X, torch allocator)

  • tests/unittests/test_plan.py at 2 and 4 ranks. New tests:
    • a kernel that increments plan.counters with a plain gpu-scope atomic_add after
      debug_barrier, as the tritonBLAS epilogue does: 8 writers on one 4 MiB chunk, 10 rounds,
      no stale data;
    • wait_sent before reusing the source, from a second kernel and from inside a persistent producer;
    • a completion signal that cannot be queued, alone or after a failed arm;
    • the round limit.
  • View tests (Triton and Gluon) on one GPU, including the wait kernel waiting for this rank's marks and
    sent() past 64 ranks.
  • Docs build.

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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

4 open issues
What changed in this PR

Updates triggered-plan gate layout and device/host synchronization to use contiguous int32 counters (per-chunk marks + arrivals, and per-peer completion), enabling single-atomic mark_ready and local-only wait() progress checks.

Changes:

  • Replace per-chunk “ready word + counter” gate rows with int32 counters/arrivals and per-peer completion counters (ABI v2).
  • Add device APIs TriggeredView.sent() / wait_sent() and host-side completion signaling per peer queue.
  • Update unit tests and docs to reflect the new gate tensor layout and semantics.
File Description
iris/​experimental/​plan/​triton.py Switches device ABI/layout to int32 gates and adds sent()/wait_sent() logic.
iris/​experimental/​plan/​gluon.py Mirrors Triton TriggeredView changes for Gluon device code.
iris/​experimental/​plan/​triggered.py Allocates/slices new gate tensor, adds round limit check, and switches wait logic to local-only gates.
iris/​experimental/​plan/​kernels.py Replaces progress kernel with local-only wait kernel; updates release semantics.
iris/​experimental/​plan/​armer.py Arms chains polling counter thresholds and signals int32 arrivals + per-peer completion.
tests/​unittests/​test_plan.py Adds coverage for external counter increments, wait_sent, and round limit; updates arrivals assertions.
tests/​unittests/​test_plan_view.py Updates view tests for new counter/arrival layout and adds sent()/wait_sent() coverage.
tests/​unittests/​test_plan_view_gluon.py Updates Gluon view tests similarly and adds sent()/wait_sent() coverage.
docs/​conceptual/​triggered-plans.md Updates conceptual model and memory-ordering text for counter-based gating and completion counters.
docs/​reference/​host/​plan.md Exposes new device methods sent/wait_sent in API docs.
iris/​experimental/​plan/​__init__.py Updates module docstring to mention new device APIs.

Review effort: Lite

Comment thread iris/experimental/plan/gluon.py Resolved
Comment thread iris/experimental/plan/triton.py Resolved
Comment thread tests/unittests/test_plan.py Resolved
Comment thread iris/experimental/plan/triggered.py Resolved
@ariscript
ariscript force-pushed the arprakas/plan-gate-counters branch from 53d8704 to 8accc21 Compare October 30, 2026 22:43
ariscript and others added 3 commits November 2, 2026 11:23
…iggered plans

Gates become one int32 symmetric tensor: a counter and an arrival counter per chunk, then a
completion counter per peer.

- Chains poll the chunk's counter against writers x round, so mark_ready is a single release
  atomic: no last-writer election and no ready word. plan.counters is contiguous int32, so a kernel
  that signals per tile (a tritonBLAS epilogue) can increment it directly.
- Every round, a rank's chains to each peer end with an ATOMIC ADD32 on the sending rank's
  completion counter for that peer. wait() then reads only local words; the progress kernel's
  remote reads and armed table are gone. TriggeredView gains sent() / wait_sent().
- Arrival signals are 32-bit (ADD32, 0x0f). release_kernel raises the counter to writers x round
  with atomic_max, so close() opens partially marked gates.
- Round limit: writers x round <= 2^31 - 1.

Tests pass at 2 and 4 ranks on MI300X (torch allocator).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The device view keeps the peers a rank sends to as bits of one int64 word; at 64 ranks building it
failed with an int64 overflow from torch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nment

Assigning a Python int to an element of the CUDA handle tensor copies from the host and blocks
until the stream drains, so start() (and close()'s release) waited for all queued GPU work.
fill_ on a one-element view is queued like any kernel.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ariscript
ariscript force-pushed the arprakas/plan-gate-counters branch from 8accc21 to 8457221 Compare November 2, 2026 17:37

@nirvedhmeshram nirvedhmeshram left a comment

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.

Thanks, left some comments, also if some of them will be addressed in #617 or #618, please feel free to ignore them here.

Comment thread docs/conceptual/triggered-plans.md Resolved
Comment thread docs/conceptual/triggered-plans.md Resolved
Comment thread tests/unittests/test_plan.py Resolved
Comment thread docs/conceptual/triggered-plans.md Resolved
Comment thread docs/conceptual/triggered-plans.md Resolved
Comment thread iris/experimental/plan/triggered.py Resolved
Comment thread iris/experimental/plan/triton.py Resolved
Comment thread iris/experimental/plan/kernels.py Resolved
Comment thread iris/experimental/plan/triggered.py Resolved
Comment thread tests/ccl/test_all_gather_sdma.py Resolved
ariscript and others added 4 commits November 3, 2026 11:15
…eue separately

The view now ends with one word per rank, 1 if this rank's chains go to it, read by sent() and
the wait kernel; the int64 bitmask and its 63-rank limit are gone.

Every round queues each peer's completion signal separately, even after a failed arm, and the
original arm error is the one raised. A peer whose signal cannot be queued is no longer waited
for, and the plan's memory is kept for the life of the process (with a warning) rather than
freed while its copies may still run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wait kernel treated this rank's own chunks as done and relied on the completion counters,
which only exist when the rank has peers. With none, or with the producer on another stream,
wait() could reach the mark-count check before the producer finished and report a wrong count.
It now polls each own chunk's counter for writers x round (a local read).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d when the round is set

- Driving the counters: a gpu-scope release is enough on gfx942/gfx950 (it writes back L2 too);
  the barrier must wait for stores, which tl.debug_barrier only does on Triton with
  triton-lang/triton#9025; and which tritonBLAS map_type pairs with which schedule builder, since
  a mismatched chunk numbering ships chunks before their tiles are written, undetected.
- wait_sent: only after all of this rank's chunks are marked, so inside the producer every writer
  CTA must be resident; new test calls it from a persistent producer.
- start() sets the round with work queued on the current stream: kernels taking the handle must be
  on that stream or wait for it.
- Why counters stop at 2^31 - 1; the external-counter test says it mirrors tritonBLAS and relies on
  a store-waiting barrier.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ariscript
ariscript force-pushed the arprakas/plan-gate-counters branch from 8457221 to 4f1b311 Compare November 3, 2026 17:18

@nirvedhmeshram nirvedhmeshram left a comment

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.

LGTM

@ariscript
ariscript merged commit 2e77375 into main Nov 3, 2026
25 checks passed
@ariscript
ariscript deleted the arprakas/plan-gate-counters branch November 3, 2026 22:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in-progress We are working on it iris Iris project issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants