perf(expt_data): skip redundant TokenStart/TileStart stores in stage1#4361
perf(expt_data): skip redundant TokenStart/TileStart stores in stage1#4361amd-hhashemi wants to merge 2 commits into
Conversation
Each non-zero block now only stores its own BLOCK-sized chunk of the prefix-sum arrays, rather than all chunks. pid==0 still writes the full arrays for the sentinel. Reduces store traffic by ~(n_expts/BLOCK - 1)x, which benefits architectures where store pressure stalls downstream loads.
🏷️ CI GuideRuns automatically on every PR:
Extended tests (opt-in via labels):
|
There was a problem hiding this comment.
Pull request overview
Optimizes the MoE routing “expert data” stage1 Triton helper by gating global TokenStart/TileStart stores (and early-exiting non-zero programs) to reduce redundant memory traffic and improve _combined_routing performance.
Changes:
- Add a
doneflag to allow non-zero programs to stop stage1 once their relevant chunk is written. - Gate
TokenStart/TileStartstores so non-zero programs only write the chunk that contains their expert. - Apply masked stores for the tail chunk in the non-
EQUAL_BLOCKpath.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
aiter/ops/triton/_triton_kernels/moe/moe_routing/expt_data.py:53
- In
_combined_routing[(blocks1a + blocks1b,)]/_combined_routing_fused, stage1 is executed by routing program instances too (pids>= blocks1a).blocks1ais set ton_expts_totin_compute_expt_data_internal(aiter/ops/triton/moe/moe_routing/routing.py:260-262). Those routing pids then immediately run_routing_compute_indx*, which loadsTokensStart + expertfor arbitrary experts (aiter/ops/triton/_triton_kernels/moe/moe_routing/routing.py:83-85 / 99-101). With the newpid < i + BLOCK+donegating,pid >= n_expts_totblocks only write the last chunk (or nothing) and can readTokenStartentries they never initialized; there is no global barrier in_combined_routingto ensure pid0 ran first.
To preserve correctness without cross-program synchronization, routing pids should keep the old behavior (store all chunks / no early-exit) and only expert pids (pid < n_expts_tot) should use the per-chunk store + early-exit optimization.
if pid == 0 or pid < i + BLOCK:
tl.store(TokenStart + offs_n, token_starts, mask=mask_n)
tl.store(TileStart + offs_n, tile_starts, mask=mask_n)
if pid != 0 and pid < i + BLOCK:
done = True
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
aiter/ops/triton/_triton_kernels/moe/moe_routing/expt_data.py:40
- The comment above
store_pidsays “All other blocks can stop once they've written their own chunk”, but in_combined_routingthis helper runs forpid >= n_expts_totrouting blocks too. Those blocks are intentionally treated likepid==0(store_pidbecomes 0) and therefore do not early-exit. Updating the comment to mention the routing-block case will prevent future confusion about whypid >= n_expts_totwrites all chunks.
# pid==0 must run the full loop: it reads TileStart[n_expts_tot-1]
# below for the sentinel, so it must have written it first.
# All other blocks can stop once they've written their own chunk.
store_pid = pid if pid < n_expts_tot else 0
done = False
In
_expt_data_compute_stage1, every block previously computed the fullprefix-sum over all experts and wrote the entire
TokenStartandTileStartarrays to global memory. This meant
n_expts_totblocks were all storing thesame values to the same addresses — pure redundant traffic.
This change gates the stores so each non-zero block only writes the
BLOCK-sized chunk that contains its own expert.
pid==0still runs the fullloop and writes all chunks, as it must — it reads
TileStart[n_expts_tot-1]afterwards to compute the sentinel value.
The reduction in store traffic unblocks the loads in stage2 on architectures
where the memory subsystem serializes stores and loads to overlapping cache
lines. On MI450 this gives a 2x end-to-end speedup to
_combined_routing.Changes
_expt_data_compute_stage1: adddoneflag and per-chunk store gate(
pid == 0 or pid < i + BLOCK) so non-zero blocks exit after writingtheir own chunk
_expt_data_compute_stage2, kernel signatures, or call sitescounts, topk, seeds) covering the EQUAL_BLOCK path and all three remainder
cases of the unrolled loop