perf(ep): optimize IntraNode dispatch kernel for MI350X - #586
Conversation
fd2eb58 to
161f4f9
Compare
|
Great work, thanks @kudomcho !
|
|
@kudomcho Hi, have you tested the performance of intranode_ll dispatch on MI350? On MI355, its peak bandwidth is around ~380 GB/s. Also, do these optimizations bring any performance benefit to intranode_ll dispatch? Could we try porting them to intranode_ll dispatch? |
|
@TianDi101 All four items addressed:
|
|
@kawhil-amd Tested IntraNodeLL dispatch on MI350X (BF16 EP8, AUTO tuning configs from PR #464):
MI350X IntraNodeLL dispatch peaks at ~368 GB/s (vs MI355X ~380 GB/s — expected from 256 vs 304 CUs). Regarding porting the optimizations:
The 4-6% gap at large tokens (2048-4096) is similar to IntraNode V1's gap and comes from the same root cause: dispatch's routing + remote atomic slot reservation overhead that combine doesn't have. |
Motivation
IntraNode EP8 dispatch on MI350X (gfx950) shows a 12% bandwidth gap against combine at large token counts on the DeepSeek V3 configuration (BF16 same-type, 7168 hidden, top-8 experts, zero-copy combine). This gap was reported as a ~10% performance deficiency in dispatch vs combine for high-token intra-node workloads.
The portable dispatch body (
EpDispatchIntraNodeKernel_bodyinintranode.hpp) has three sources of overhead that do not exist in the combine kernel path:tokenIndices, re-computesdestPe, and re-checks per-PE deduplication — all of which Phase 1 already computed.cbar + cslotinstead ofmax(cbar, cslot).WarpCopy<T, 8>at BF16 hidden=7168 produces a degenerate unrolled loop (elemsPerWarp = 4096 > hiddenDim), falling back to Unroll=1 for the entire copy.Technical Details
Kernel changes (
src/ops/dispatch_combine/intranode.hpp)1. Cached routing (eliminate Phase 3 redundancy)
Phase 1 now caches each kept token's
destPeindispDestTokIdMapwith a sentinel bit (0x40000000). Phase 3 reads the cached value with a single global load +__shflinstead of re-readingtokenIndices(up totopkglobal loads per pair), re-dividing bynumExpertPerRank, and re-checking dedup via__any(). This eliminates ~32768 redundant global memory reads per kernel launch at 4096 tokens.The sentinel bit (
0x40000000 = 2^30) is safe becauseFlatTokenIndexvalues are bounded byworldSize * MaxNumTokensToSend(), which at EP8 with 4096 tokens is well below2^30.2. Overlapped completion waits
Ported from the gfx1250 dispatch body (
intranode_1250x.hpplines 811-826): the slot-drain wait (ShmemInt32WaitUntilEqualson peer memory) is issued before the grid barrier (ShmemUint32WaitUntilEqualson local memory). The slot read targets uncached peer memory and pays a full xGMI fabric round-trip even when the slot has long been drained — issuing it while the grid barrier is still spinning hides this latency. The two waits are independent (the slot address depends only ondestPe, not on the barrier state).3. WarpCopy Unroll 8 → 2
WarpCopy<T, 8>with BF16 (2 bytes) and wave64 computeselemsPerWarp = 8 × 64 × 8 = 4096. AthiddenDim = 7168, the main unrolled loop body runs only once (covering 4096 of 7168 elements), with the remaining 3072 elements handled by the Unroll=1 fallback — producing suboptimal instruction scheduling for the remote store pipeline.WarpCopy<T, 2>giveselemsPerWarp = 1024, producing 7 well-pipelined unrolled iterations that better utilize the memory controller's store queue for cross-GPU xGMI writes.4. Vectorized metadata copies
Weights and indices copies (8 floats + 8 ints per token) are converted from lane-parallel scalar stores (only 8 of 64 lanes active) to
WarpCopycalls that use all 64 lanes. This is a code quality improvement; the metadata is too small (64 bytes) relative to the 14 KB payload to produce a measurable BW change.Benchmark script (
tools/bench_dispatch_gap.sh)New benchmark script for rigorous dispatch-vs-combine comparison:
bench_results/--dispatch-geo/--combine-geoAdditional finding: missing bf16 dispatch tuning configs
PR #464's
gfx950_mi350x_IntraNode_ep8_dispatch.jsonhas only one bf16 entry (64 tokens at 128×4). WithMORI_EP_LAUNCH_CONFIG_MODE=AUTO, large-token bf16 dispatch falls back to this geometry, producing 284 GB/s instead of 363 GB/s with the correct 2048×16 geometry. Adding bf16 dispatch tuning entries for 128–524288 tokens is recommended as a follow-up.Performance Results
All numbers measured on MI350X (gfx950, 256 CUs), BF16 same-type EP8 IntraNode, 7168 hidden, top-8 experts, zero-copy combine. Dispatch geometry: 2048×16, combine geometry: 56×15. Each row is mean ± stddev GB/s over 5 iterations (3 warmup, 10 graph replays per iter) on verified-idle GPUs.
Kernel optimization (same geometry, 2048×16)
Kernel-level improvement: +3.1% dispatch BW, consistent across token counts.
Geometry optimization (256×16 → 2048×16)
More blocks generate more concurrent xGMI write traffic, better saturating the 7-link fabric. The dispatch kernel uses minimal shared memory per block (3 × 8 × 4B = 96B for
s_N/s_base/s_run), so high block counts do not pressure LDS.Combined improvement
Small/mid token results (dispatch faster than combine)
At small token counts, the optimized dispatch is faster than combine — the kernel overhead (Phase 1/2/completion) is amortized and dispatch's push model has lower fixed latency than combine's barrier + accumulation.
Dispatch geometry: 128×16 (256/512t), 256×16 (1024t), 2048×16 (2048t). Combine geometry: per MI350X tuning configs.
Comparison with MI355X reference (docs/EP8.en.md)
MI350X optimized dispatch now exceeds MI355X reference dispatch BW, and the gap vs combine (4.9–6.2%) is better than MI355X's own gap (13.8–16.5%).
Remaining gap (~5%) — profiled root cause
Local-write profiling (redirecting payload to local HBM instead of remote xGMI) measured 213 us of routing overhead out of the total 860 us dispatch time at 4096 tokens:
The xGMI writes alone (647 us) are 18% faster than combine (791 us), confirming the fabric transfer itself is efficient. The 213 us routing overhead is the gap — combine has no equivalent phase (it receives pre-routed data and goes straight to P2P reads + FMA accumulation).
Approaches tested to reduce the overhead:
Further reduction of the 213 us overhead requires either restructuring Phase 1 to process at the token level (needs wave64-compatible dedup), or host-side changes to pre-compute routing on a separate kernel launch.
Test Plan
Hardware required: 8× AMD Instinct MI350X or MI355X (gfx950)
Test Result
Stress test: 200 rounds, 0 failures (MI350X, BF16 EP8, 4096 tokens, 2048×16).
Performance (MI350X, clean GPUs):
Submission Checklist