Skip to content

DeepEP v2:Add experimental direct recv_x dispatch path.#690

Open
kzlxd wants to merge 1 commit into
deepseek-ai:mainfrom
kzlxd:Dispacth_Direct_recv_x
Open

DeepEP v2:Add experimental direct recv_x dispatch path.#690
kzlxd wants to merge 1 commit into
deepseek-ai:mainfrom
kzlxd:Dispacth_Direct_recv_x

Conversation

@kzlxd

@kzlxd kzlxd commented Jul 14, 2026

Copy link
Copy Markdown

Add Direct Recv X dispatch path

Summary

This PR adds an experimental Direct Recv X path for compact EP dispatch.

The optimization removes the extra local hidden-state copy in the dispatch copy epilogue. In the original path, hidden states are first written into the NCCL symmetric staging buffer, then copied again into the final recv_x tensor by the epilogue. With Direct Recv X enabled, dispatch writes hidden states directly into the final compact recv_x window, while metadata and top-k tensors continue to use the existing staging + epilogue path.

Direct Recv X is enabled by:

EP_V2_EXPERIMENTAL_DIRECT_RECV_X=1

This PR also includes a small SM90 / H800 JIT compilation fix for an SM100-only shared-memory bulk-store instruction.

Background

The original dispatch path has two buffer layers:

                 dispatch kernel                      copy epilogue
src x/topk  ------------------------------> staging -----------------> recv_x/topk
            write hidden + metadata/top-k              read staging, write final tensors

staging is NCCL symmetric / window memory and can be accessed by the communication path directly.

recv_x is a normal PyTorch tensor. It is materialized after dispatch by dispatch_copy_epilogue.

This means hidden states always pay one extra local copy:

staging.hidden -> recv_x.hidden

The copy cost grows linearly with token count and becomes more visible with wider hidden sizes.

Direct Recv X design

The dispatch kernel already knows:

  • which destination rank a token is sent to;
  • which slot index the token occupies on the destination rank.

The receiver notify warp also knows how many tokens each source rank contributes. From those counts, it can compute the exclusive prefix base for each source rank in the compact recv_x layout.

Direct Recv X publishes this receiver-side prefix information back to the source rank:

receiver:
  rank_count[source_rank] -> exclusive prefix
  publish prefix[source_rank] back to source

source dispatch warp:
  compact_idx = prefix[dst/source group] + dst_slot_idx
  write hidden directly to recv_x[compact_idx]

The hidden-state data path becomes:

src rank x
  -> dispatch kernel
  -> target rank recv_x

The metadata / top-k path stays unchanged:

metadata/top-k
  -> staging
  -> dispatch_copy_epilogue
  -> recv_topk_idx / recv_topk_weights / recv_src_metadata / channel_linked_list

So the epilogue no longer copies hidden states, but it still materializes the metadata and top-k outputs.

Implementation

  • Reserve a Direct Recv X output window at the tail of the DeepEP symmetric communication buffer when EP_V2_EXPERIMENTAL_DIRECT_RECV_X=1.

    • Size:
      num_ranks * num_max_tokens_per_rank * hidden * elem_size
      
  • Return recv_x as a view into this symmetric-window tail in Direct Recv X mode.

  • Extend dispatch runtime arguments and kernels so dispatch can write hidden states directly into the receiver-side recv_x window.

  • Let dispatch_copy_epilogue skip the hidden-state copy when Direct Recv X is enabled.

    • The epilogue still writes:
      • recv_topk_idx
      • recv_topk_weights
      • recv_src_metadata
      • channel_linked_list
      • FP8 scale-factor tensors
  • Support both compact dispatch variants:

    • single-node / scaleup dispatch;
    • hybrid scaleout + scaleup dispatch.
  • Keep the feature behind one experimental env flag:

    • EP_V2_EXPERIMENTAL_DIRECT_RECV_X=1
  • Add guardrails for unsupported modes:

    • non-cached dispatch only;
    • non-expanded dispatch only;
    • deterministic dispatch is not supported;
    • previous_event_before_epilogue is rejected because recv_x is produced during dispatch instead of during the epilogue.

H100 benchmark results

Configuration:

  • GPU: H100
  • Path: compact EP dispatch
  • Direct mode: EP_V2_EXPERIMENTAL_DIRECT_RECV_X=1
  • Metric: dispatch + copy epilogue latency

Parameter:

  • direct-recv-x
  • num-sms 12
  • num-topk 8
  • prefer-overlap-with-compute 1

EP8

Tokens Dtype Dispatch non-direct / direct Copy non-direct / direct Total non-direct / direct Total delta
4096 FP8 545.6 / 536.6 us 161.7 / 51.9 us 707.3 / 588.5 us -118.9 us, 16.8% faster
8192 FP8 1066.0 / 1042.4 us 325.7 / 92.4 us 1391.7 / 1134.8 us -256.8 us, 18.5% faster
4096 BF16 1038.3 / 1076.7 us 304.3 / 96.1 us 1342.7 / 1172.8 us -169.9 us, 12.7% faster
8192 BF16 2048.8 / 2124.8 us 606.3 / 187.5 us 2655.1 / 2312.4 us -342.8 us, 12.9% faster

EP16

Tokens Dtype Dispatch non-direct / direct Copy non-direct / direct Total non-direct / direct Total delta
4096 FP8 753.9 / 768.0 us 166.5 / 34.6 us 920.4 / 802.6 us -117.8 us, 12.8% faster
8192 FP8 1416.9 / 1374.6 us 339.3 / 35.4 us 1756.3 / 1410.1 us -346.2 us, 19.7% faster
4096 BF16 1341.8 / 1437.8 us 289.6 / 103.7 us 1631.4 / 1541.5 us -89.9 us, 5.5% faster
8192 BF16 2600.5 / 2737.4 us 588.0 / 195.4 us 3188.5 / 2932.8 us -255.7 us, 8.0% faster

Result summary

Direct Recv X substantially reduces the copy epilogue cost by removing the hidden-state copy from staging to recv_x.

End-to-end dispatch + copy improvement:

  • EP8: 12.7% - 18.5% faster
  • EP16: 5.5% - 19.7% faster

In some BF16 cases, the dispatch kernel itself becomes slightly slower because it now performs direct remote recv_x writes. The removed epilogue hidden copy still more than offsets that overhead, so total latency improves.

Comment on lines +503 to +508
} else if (stored_dst_slot_idx >= 0 and direct_recv_x_idx < 0) {
// Fallback: timeout on prefix sum, write SF/metadata only via RDMA
gin.put<team_t>(
recv_buffer.get_token_buffer(stored_dst_slot_idx).get_sf_ptr(),
send_buffer_token.get_sf_ptr(),
kNumSidecarBytes, stored_dst_rank_idx,

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.

🔴 critical: Prefix-timeout/slot-invalid fallback silently drops the token's hidden payload. When the receiver-published base is missing/late/on a wrong peer, the kernel prints a timeout and then writes the sidecar only (or nothing), so the token's hidden content is lost — there is no abort and no fallback to the old staging-copy path. The experimental flag's correctness rests on this never happening, which is fragile in multi-scaleup/scaleout/hybrid routing. Same pattern in hybrid_dispatch.cuh:638-660 and 343.

🤖 v4

@kzlxd kzlxd Jul 15, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

already add except error

Comment on lines +492 to +499
if (stored_dst_slot_idx >= 0 and dst_x_ptr == nullptr and direct_recv_x_idx >= 0) {
gin.put<team_t>(
math::advance_ptr(direct_recv_x, static_cast<int64_t>(direct_recv_x_idx) * kNumHiddenBytes),
send_buffer_token.get_hidden_ptr(),
kNumHiddenBytes, stored_dst_rank_idx,
ncclGinOptFlagsAggregateRequests);
gin.put<team_t>(
recv_buffer.get_token_buffer(stored_dst_slot_idx).get_sf_ptr(),

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.

🟡 warning: Even in the normal RDMA scaleout path, the hidden payload is written via a separate RDMA put into recv_x while the sidecar goes into staging. This is fine only if the published base is always valid here; see the timeout/drop concern above.

🤖 v4

// Cached expand mode: read pre-computed dst_tensor_idx from metadata
dst_tensor_idx = recv_src_metadata[i * kMetadataStride + 2 + lane_idx];
} else if (kDoExpand and not kCachedMode and dst_expert_idx >= 0) {
} else if (kDoExpand and dst_expert_idx >= 0) {

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.

🔴 critical: Regression in the shared (non-experimental) epilogue: the expanded atomic-add branch lost its not kCachedMode guard (else if (kDoExpand and dst_expert_idx >= 0)). This epilogue is launched for cached/expanded dispatch even when Direct Recv X is off, so it can corrupt cached-expand dispatch and/or its per-expert counters on the baseline path.

🤖 v4

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines +343 to +348
workspace_layout.get_scaleup_rank_count_ptr<true>() + scaleup_rank_idx,
math::encode_decode_positive(static_cast<int64_t>(prefix)), i);
prefix += rank_count[i];
}
__syncwarp();
}

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.

🟡 warning: The receiver-side prefix-publish loop uses a per-scaleup slot that is later polled with a timeout and a separate channel-signaled-tail mechanism (cf. dispatch.cuh:272/397/562). The two variants of the same protocol differ in which slot/peer they address; the design needs a single, race-free publication/decoder contract before the experimental flag can be trusted across scaleout+scaleup topologies.

🤖 v4

Comment thread csrc/elastic/buffer.hpp
Comment on lines +995 to +1014
const bool use_direct_recv_x = get_env<int>("EP_V2_EXPERIMENTAL_DIRECT_RECV_X", 0) != 0;
if (use_direct_recv_x) {
EP_HOST_ASSERT(not cached_mode and
"EP_V2_EXPERIMENTAL_DIRECT_RECV_X currently supports non-cached dispatch only");
EP_HOST_ASSERT(not do_expand and
"EP_V2_EXPERIMENTAL_DIRECT_RECV_X currently supports non-expanded dispatch only");
EP_HOST_ASSERT(not previous_event_before_epilogue.has_value() and
"EP_V2_EXPERIMENTAL_DIRECT_RECV_X writes recv_x in dispatch and cannot honor previous_event_before_epilogue");
}
const auto direct_recv_x_buffer_offset = use_direct_recv_x ? get_comm_buffer_size(
num_max_tokens_per_rank, hidden, num_sf_packs, num_topk, x.element_size(),
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks,
nccl_context->is_scaleup_nvlink, allow_multiple_reduction) : 0;
void* direct_recv_x_ptr = use_direct_recv_x ? math::advance_ptr(buffer, direct_recv_x_buffer_offset) : nullptr;
if (use_direct_recv_x) {
const auto direct_recv_x_bytes = get_direct_recv_x_buffer_size(
num_max_tokens_per_rank, hidden, x.element_size(),
nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks);
EP_HOST_ASSERT(direct_recv_x_buffer_offset + direct_recv_x_bytes <= num_gpu_buffer_bytes and
"EP_V2_EXPERIMENTAL_DIRECT_RECV_X requires extra GPU buffer bytes; "

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.

🟡 warning: Guardrails are asserted on the C++ side for cached/expanded/previous_event and the window-fit/size, but the deterministic-dispatch restriction exists only in the Python runtime/elastic.py:938 and is only skipped (not asserted) in the test. If the C++ kernel is templated with deterministic + Direct Recv X it has no guard. Recommend asserting determinism alongside the others in buffer.hpp too, or documenting why it is safe.

🤖 v4

Comment thread csrc/elastic/buffer.hpp
Comment on lines +666 to 680
return math::align(std::max(num_dispatch_bytes, num_combine_bytes), symmetric::kNumAlignmentBytes);
}

static int64_t get_direct_recv_x_buffer_size(const int& num_max_tokens_per_rank,
const int& hidden, const int& elem_size,
const int& num_scaleout_ranks, const int& num_scaleup_ranks) {
const auto num_ranks = num_scaleup_ranks * num_scaleout_ranks;
return math::align<int64_t>(
static_cast<int64_t>(num_ranks) * num_max_tokens_per_rank * hidden * elem_size,
symmetric::kNumAlignmentBytes);
}

static int64_t calculate_buffer_size(const int64_t& nccl_comm,
const int& num_max_tokens_per_rank, const int& hidden,
int num_topk, const bool& use_fp8_dispatch,

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.

🟡 warning: The reserved Direct Recv X window is appended to the max(dispatch, combine) size only when the env flag is on. But the recv_x view (buffer.hpp:1130) is taken at a fixed offset equal to the whole comm buffer size, while the epilogue/combine layout still reads the staging area within the comm buffer. If num_bytes is sized manually/by hint without the extra window, the host assert catches it — good — but the buffer-size hint path (get_buffer_size_hint) and the construction path must stay consistent. Worth a unit-level size-accounting check before merging.

🤖 v4

@ds-review-bot

Copy link
Copy Markdown
Collaborator

🤖 ds-review-bot Code Review

v6

⚠️ 未完成评审(upstream_error:模型上游服务不可用)
{"error":{"message":"Budget has been exceeded! Current cost: 2834.028324850001, Max budget: 2833.297387599996","type":"budget_exceeded","param":null,"code":"400"}}

v4

The Direct Recv X PR is already fully implemented in the current working tree (committed at HEAD 0226cd2 on top of the real DeepEP base dd758ca). The seven changed files wire up and test the entire feature: a Direct Recv X window reserved at the tail of the symmetric NCCL comm buffer; recv_x returned as a view into that window; the dispatch kernel writing hidden states directly into recv_x using a receiver-published per-source prefix (both single-node/scaleup and hybrid/scaleout variants); the copy epilogue skipping the hidden copy while still producing metadata/top-k/SF; the experimental env flag and its guardrails (non-cached, non-expanded, no previous_event_before_epilogue). Since the feature is already written, the value of a fresh review is to surface the parts that are still fragile/unfinished before the experimental flag is promoted/merged — and the one part promised in the PR summary that is missing from the diff.

v3

⚠️ 未完成评审(upstream_error:模型上游服务不可用)
400 {"error":{"message":"Budget has been exceeded! Current cost: 2834.028324850001, Max budget: 2833.297387599996","type":"budget_exceeded","param":null,"code":"400"}}

Files reviewed: 7
Issues found: 🔴 2 critical | 🟡 4 warning
Inline comments posted: 6

⚠️ Parse warning: [v6] upstream_error:模型上游服务不可用; [v3] upstream_error:模型上游服务不可用

Reserve a symmetric receive window and let dispatch write recv_x payloads directly so the epilogue can skip the hidden-state copy in supported compact dispatch cases.
@kzlxd
kzlxd force-pushed the Dispacth_Direct_recv_x branch from 0226cd2 to 6757592 Compare July 14, 2026 08:46
@kzlxd kzlxd changed the title Add experimental direct recv_x dispatch path. DeepEP v2:Add experimental direct recv_x dispatch path. Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants