Skip to content

Hoist heap-base loads out of the Triton all-gather store loops - #626

Open
dsidler wants to merge 5 commits into
mainfrom
dev/dasidler/hoist-all-gather-heap-bases
Open

dsidler wants to merge 5 commits into
mainfrom
dev/dasidler/hoist-all-gather-heap-bases

Conversation

@dsidler

@dsidler dsidler commented Nov 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

ctx.all_gather called ctx.store once per destination, and every call did two tl.load(heap_bases + rank) (source and destination base). A load issued after a store cannot be hoisted above it, and waiting for that load also waits for every earlier store, so the compiler put an s_waitcnt vmcnt(0) in front of each store. On gfx942 vmcnt counts stores too, so the remote stores were effectively serialized.

This loads the local and all peer heap bases once before the first store, keeps each peer as a scalar pointer delta (peer_base - local_base), and then issues plain stores. The local copy goes first and peers are written in a per-rank rotated order (rank + 1, rank + 2, ...), the same idea as the Gluon all-gather in #594. No API change.

Assembly (256x256 fp16 tile, _fused_matmul_all_gather_kernel, gfx942)

before after
s_waitcnt vmcnt(0) in the epilogue 128 1
stores preceded by a drain 128 of 128 0 of 128
global loads in the epilogue 14 (re-issued per destination) 0 (hoisted above the K loop)
epilogue instructions 1843 1251

Benchmark

benchmark/ops/bench_matmul_all_gather.py, the CU-based matmul_all_gather, 8 ranks, fp16, N=3584, K=8192, two runs each on current main:

M_local main this PR
1024 1.39 ms 1.01 ms
4096 4.76 ms 2.30 ms
16384 13.8 ms 7.64 ms

Triton CCL all-gather (iris/ccl/triton/all_gather.py)

The same pattern is applied to persistent_all_gather, which called iris.store per remote destination inside the tile loop. The output base is translated once per peer before the tile loop; the local copy is written first and peers in rotated order, like the Gluon version in #594. persistent_all_gather_partitioned is left as it was: hoisting its single destination base out of the tile loop made it 5-12% slower at 64 or fewer programs.

Assembly of persistent_all_gather (gfx942): global loads 13-14 -> 1, vmcnt(0) drains 9 -> 2-3, stores immediately after a drain 8/8 -> 5/8 (I did not look into the remaining 5).

benchmark/ccl/bench_all_gather.py, persistent, 8 ranks, fp16, N=4096, two runs each. With 64 or more programs the kernel is link-bound and the change is neutral. With a small CU budget, issue serialization limits it and the change helps:

M comm_sms main this PR
4096 8 2.13 ms 1.38 ms
4096 16 1.16 ms 0.89 ms
4096 32 0.89 ms 0.83 ms
4096 64 0.83 ms 0.82 ms
16384 8 8.39 ms 5.19 ms
16384 16 4.59 ms 3.35 ms
16384 32 3.35 ms 3.15 ms
16384 64 3.09 ms 3.04 ms

🤖 Generated with Claude Code

dsidler and others added 3 commits November 4, 2026 18:00
ctx.all_gather called ctx.store once per destination, and every call loaded
heap_bases twice (source and destination base). A load issued after a store
cannot be hoisted above it, and waiting for that load also waits for every
earlier store, so the compiler emitted s_waitcnt vmcnt(0) before each of the
128 stores of a 256x256 fp16 tile, serializing the remote stores.

Load the local and all peer heap bases once before the first store, keep each
peer as a scalar pointer delta, and write to peers in a per-rank rotated order
starting with the local copy. The assembly now has a single vmcnt(0) in the
epilogue and no loads between stores.

Co-Authored-By: Claude <noreply@anthropic.com>
persistent_all_gather called iris.store once per remote destination inside the
tile loop, so every store re-loaded heap_bases twice and was preceded by an
s_waitcnt vmcnt(0). persistent_all_gather_partitioned did the same translation
after every tile's store even though its destination never changes.

Translate the output base once per peer before the tile loop and store through
the pre-translated pointers. The persistent kernel now writes the local copy
first and peers in a per-rank rotated order, matching the Gluon all-gather.

Co-Authored-By: Claude <noreply@anthropic.com>
Translating the destination base once before the tile loop made
persistent_all_gather_partitioned 5-12% slower at 64 or fewer programs
(for example 4.2 ms -> 4.6 ms at M=16384, N=4096, 64 programs), so restore
the original per-tile iris.store there. Only the persistent kernel keeps
the hoisting.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added in-progress We are working on it iris Iris project issue labels Nov 5, 2026
Keep the original destination loop, its local-store branch and its ascending
order. Only translate the output base for every destination before the tile
loop and replace the iris.store call with a tl.store through the pre-translated
pointer.

Co-Authored-By: Claude <noreply@anthropic.com>
@dsidler
dsidler marked this pull request as ready for review November 5, 2026 08:01
Comment thread iris/ccl/triton/all_gather.py Outdated
@@ -104,8 +114,6 @@ def persistent_all_gather(
# Send local shard data to all destination ranks
# Each rank's input goes to output[group_rank * M : (group_rank + 1) * M, :] on all ranks
for i in tl.static_range(world_size):

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.

can we do the traffic shaping here too? this still loops 0..world_size-1 with local in the middle, so every rank writes to the same peer at the same time. context.py does local-first + rotated order like #594

Comment thread iris/mem/triton/context.py Outdated

# Start with the local copy, then write to peers in a per-rank rotated order so that each
# destination sees a different writer at any given moment.
tl.store(dst_ptr, tile.data, mask=combined_mask)

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.

local store lost its hint — the old self.store passed hint=(1, tile.block_n) for every dest including self. the remote stores below still get max_contiguous/multiple_of but this one doesnt

Write the local copy first and the peers in a per-rank rotated order in
persistent_all_gather, matching ctx.all_gather and the Gluon all-gather, so
each destination sees one writer at a time. Also apply the same
max_contiguous/multiple_of hint to the local store in ctx.all_gather that the
remote stores and the old self.store path had.

Co-Authored-By: Claude <noreply@anthropic.com>

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

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants