mirror of
https://github.com/deepseek-ai/DeepEP.git
synced 2026-11-05 00:47:32 +00:00
[PR #753] DeepEP v2: Kernel-side deterministic dispatch for the non-expand layout #757
Labels
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
deepseek-ai/DeepEP#757
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/deepseek-ai/DeepEP/pull/753
Author: @EricWang008
Created: 9/17/2026
Status: 🔄 Open
Base:
main← Head:deterministic-hybrid-dispatch📝 Commits (4)
3586de8feat(elastic): add kernel-side deterministic dispatch path2d69687feat(elastic): wire the deterministic prologue and row-form epilogue launchers35f4c8dfeat(elastic): orchestrate the deterministic dispatch on the host side4cd6431feat(elastic): keep the sorting epilogue only for the expand layout📊 Changes
7 files changed (+472 additions, -45 deletions)
View changed files
📝
csrc/elastic/buffer.hpp(+75 -8)📝
csrc/kernels/elastic/dispatch.hpp(+98 -6)📝
deep_ep/buffers/elastic.py(+14 -4)📝
deep_ep/include/deep_ep/common/layout.cuh(+10 -0)📝
deep_ep/include/deep_ep/impls/dispatch_copy_epilogue.cuh(+27 -6)➕
deep_ep/include/deep_ep/impls/dispatch_deterministic_prologue.cuh(+151 -0)📝
deep_ep/include/deep_ep/impls/hybrid_dispatch.cuh(+97 -21)📄 Description
Motivation
Deterministic dispatch is currently implemented as a post-hoc sort in
EPHandle.deterministic_sort: after the dispatch kernels finish, the received tokens are sorted bysrc_token_global_idxand every order-dependent tensor (recv_x,recv_sf,recv_topk_weights,recv_topk_idx,recv_src_metadata,channel_linked_list) is permuted in place.This is correct but costly. The sort plus the permutations add a full extra round trip over the received activations, and the cost grows with
num_recv_tokens * hidden.The root cause of the non-determinism is that a sender picks its destination slot with an
atomicAddon a shared counter, so the placement depends on the inter-warp race:This PR removes that race for the non-expand layout instead of repairing its consequences afterwards.
Approach
A lightweight prologue kernel (
dispatch_deterministic_prologue_impl) pre-computes the destination slot of every(token, top-k)entry, making the placement a pure function oftopk_idx. The dispatch kernels then replay those slots rather than racing for them, so the output is bitwise reproducible without any sorting.The emitted value is
row_base_idx * num_max_tokens_per_rank + row-local slot, where the row-local slot is the sender's running count of deduplicated entries to that destination. A row is private to a single source rank, which can never send more thannum_max_tokens_per_rankdeduplicated entries to one destination, so rows cannot overflow.The prologue is a cooperative kernel doing a two-pass scan: the first pass accumulates per-warp / per-SM destination counts, a grid-wide sync exchanges them, and the second pass turns the prefix sums into per-entry slots.
Direct vs hybrid dispatch
The two paths need different row bases:
row_base_idxnum_scaleout_ranks == 1)scaleup_rank_idxnum_scaleout_ranks > 1)scaleout_rank_idxThe direct path needs no layout change: its receive buffer is
[num_ranks][num_max_tokens_per_rank], i.e. it is already row-form by construction.The hybrid path is different. Its scale-up buffer has only
num_scaleup_rankssegments while there arenum_rankspossible sources, so locating a token requires per-source-rank counts. This PR therefore adds a row-form interpretation of the hybrid scale-up receive buffer, enabled by the newkRowFormtemplate argument ofdispatch_copy_epilogue_impl: every global source rank owns a private (partially filled) row, and a token sits atsegment = src_scaleup_rank_idx,in-segment offset = src_scaleout_rank_idx * num_max_tokens_per_rank + row-local slot.To feed that traversal, the hybrid notify phase now also publishes per-source-rank received-token counts, and
psum_num_recv_tokens_per_scaleup_rankis extended from[num_scaleup_ranks]to[num_scaleup_ranks | num_ranks]in this mode.Compatibility
No existing behaviour is removed. The kernel path is an addition, not a replacement:
deterministic_sortThe expanded layout assigns its output rows with
atomicAddonpsum_num_recv_tokens_per_expert, which the kernel path cannot make reproducible, so those cases must keep the sorting epilogue. The two are strictly complementary: C++ gates ondeterministic and not do_expand, Python gates onself.deterministic and do_expand.kDoZeroPadding,kExpertAlignment, the cached-expand path anddeterministic_sortitself are all preserved.Incidental fix
combineassertednum_scaleup_ranks == nccl_context->num_scaleup_rankson the psum tensor. Since the row-form dispatch appendsnum_ranksextra prefix sums, that check is relaxed to accept either length.New API
dispatch()gains an optionalnum_prologue_sms(0 = automatic, capped at 64 SMs). It only affects the deterministic prologue.Testing
Verified on H800 2×8 with
tests/elastic/test_ep.py --deterministic, covering both the direct and hybrid dispatch paths as well as the expand layout (which still falls back to the sorting epilogue).Performance
Configuration:
do_expand=false, BF16,expert_alignment=128,num_bias=2The 635 us sorting epilogue is replaced by a 6 us prologue, a ~18% reduction in end-to-end dispatch time (1.22× speedup).
dispatchandcopy_epiloguethemselves are unchanged, confirming the row-form traversal and the slot replay add no measurable overhead.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.