mirror of
https://github.com/deepseek-ai/DeepEP.git
synced 2026-11-05 00:47:32 +00:00
[PR #744] [MERGED] [HybridEP] Support unequal per-rank token counts (ragged dispatch) #749
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#749
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/744
Author: @abcdabcd987
Created: 9/1/2026
Status: ✅ Merged
Merged: 9/7/2026
Merged by: @jershi425
Base:
hybrid-ep← Head:ragged-tokens📝 Commits (5)
efa9196[HybridEP] Support unequal per-rank token counts (ragged dispatch)0aa8753Address review: inline handle pack/unpack, buffer-generation guard2364852Review: make buffer generation counter uint64_t268ca93Review: keep the ragged matrix end-padded only; dedicated dropped-token test7c646d6Review: remove dropped-token simulation from the ragged tests📊 Changes
11 files changed (+845 additions, -52 deletions)
View changed files
📝
csrc/hybrid_ep/backend/hybrid_ep_backend.cuh(+45 -5)📝
csrc/hybrid_ep/config.cuh(+10 -0)📝
csrc/hybrid_ep/executor/executor.cu(+12 -2)📝
csrc/hybrid_ep/executor/executor.cuh(+11 -0)📝
csrc/hybrid_ep/hybrid_ep.cu(+34 -9)📝
csrc/hybrid_ep/hybrid_ep.cuh(+13 -6)📝
csrc/hybrid_ep/pybind_hybrid_ep.cu(+4 -0)📝
deep_ep/hybrid_ep_buffer.py(+131 -9)📝
docs/Hybrid-EP_Implementation.md(+23 -20)📝
tests/test_hybrid_ep.py(+1 -1)➕
tests/test_ragged_hybrid_ep.py(+561 -0)📄 Description
Summary
HybridEP required every EP rank to dispatch the same number of tokens, forcing frameworks with variable-length (no-pad) batches to pad tokens up to the group max — pad rows that ride the dispatch payload and the expert GEMMs. A line-level audit showed the requirement is not fundamental: every payload path is already per-token routing-map-gated; only the routing-map allgather size, the metadata map strides, and the chunk-grid/flag arithmetic need a shared layout constant, not equal real counts.
This PR lifts the restriction:
num_of_tokens_per_rankkwarg ondispatch/dispatch_with_permute: a group-uniform token-slot countS(identical on every rank, e.g. the group max of the local counts; rounded up to a multiple of 16 internally, which satisfies the TMA remainder-chunk constraint for everyranks_per_node). Each rank'shidden/probs/ routing inputs keep their realN_i <= Srows. Omitted ⇒ defaults tohidden.size(0), i.e. the previous uniform behavior.Srows, with routed-nowhere rows (all-Falsebool rows in sparse mode, all--1sentinel rows in dense int16 top-k mode). Pad slots produce no NVLink/RDMA traffic, no permuted rows, and no expert GEMM rows — the only cost is ~(S-N_i) × K × 2bytes of extra routing-map allgather payload (≈0.03% of dispatch traffic at typical shapes) and zero-length chunk flags.num_of_valid_tokensfield (appended, so existing indices — includinghandle[10]overflow_flag — are unchanged; unpackers accept the old arity). Combine outputs are allocated atS(the combine kernel unconditionally writes every slot; un-routed rows come out as exact zeros) and returned narrowed toN_irows.Bug fixes included
ceil(N/chunk) != ceil(max/chunk)on 3+ nodes (pre-existing; now MAX-based, matching DOCA).rdma_syncsame-QP ordering).num_of_tokens_per_rankrows of the user probs tensor — OOB when slots exceed real rows; now bounded by the tensor's real row count.dispatch()now asserts routing rows == hidden rows on the fresh path (mirroringdispatch_with_permute), so a mismatch can't be masked by the internal padding as silent token drops.Note: the NIXL changes are code-reviewed but compile-untested here (no NIXL build environment); DOCA and single-domain MNNVL paths are unaffected by them.
API compatibility
N % 16 == 0(all existing tests / typical production configs) take a bit-identical path.N % 16 != 0now get internal slot alignment + combine narrowing instead of a potential TMA failure; output shapes are unchanged (Nrows).7→8and11→12by appendingnum_of_valid_tokens. Positional indices of existing fields are unchanged; fixed-arity structural unpacks of the returned handle need updating (one site intests/test_hybrid_ep.pywas).Validation
tests/test_hybrid_ep.py(BF16+FP8, all six correctness modes) andtests/test_graphed_hybrid_ep.py(non-blocking + CUDA graphs) pass unchanged; newtests/test_ragged_hybrid_ep.pypasses — bitwise vs a padded-referenceTorchRef: sparse/dense routing × fused/non-fused permute × with/without probs × BF16/FP8, cached-handle replay bitwise (tokens, probs, scaling factors), dropped (zero-expert) tokens, zero-token ranks (incl. dense mode and FP8), and theN % 16 != 0default path.USE_MNNVL=1).🤖 Generated with Claude Code
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.