Skip to content

Commit fb73fe9

Browse files
wangwei02claude
authored andcommitted
修复 DeepEPv2 反向 MoE 场景的路由权重梯度传播问题
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> (cherry picked from commit 7a9715dffbb47a33a7aebbb337d507b17d3408ec)
1 parent af9a040 commit fb73fe9

8 files changed

Lines changed: 242 additions & 45 deletions

File tree

csrc/elastic/buffer.hpp

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -666,11 +666,13 @@ class ElasticBuffer {
666666
std::vector<int>,
667667
torch::Tensor, torch::Tensor, torch::Tensor, torch::Tensor,
668668
std::optional<torch::Tensor>, std::optional<torch::Tensor>,
669+
std::optional<torch::Tensor>, // recv_aux_weights: per-row router-weight gradient scalar in NvS layout
669670
std::optional<EventHandle>>
670671
dispatch(const torch::Tensor& x,
671672
const std::optional<torch::Tensor>& sf,
672673
const torch::Tensor& topk_idx,
673674
const std::optional<torch::Tensor>& topk_weights,
675+
const std::optional<torch::Tensor>& aux_weights, // second per-(t,k) scalar carried alongside topk_weights
674676
const std::optional<torch::Tensor>& cumulative_local_expert_recv_stats,
675677
const std::optional<int>& cached_num_recv_tokens,
676678
const std::optional<std::vector<int>>& cached_num_recv_tokens_per_expert_list,
@@ -679,6 +681,7 @@ class ElasticBuffer {
679681
const std::optional<torch::Tensor>& cached_dst_buffer_slot_idx,
680682
const std::optional<torch::Tensor>& cached_token_metadata_at_forward,
681683
const std::optional<torch::Tensor>& cached_channel_linked_list,
684+
const std::optional<torch::Tensor>& cached_recv_src_metadata, // cached+expand replay reuses the recorded expanded-row map
682685
const int& num_max_tokens_per_rank,
683686
const int& num_experts, const int& expert_alignment,
684687
const int& num_sms, const int& num_qps,
@@ -687,7 +690,9 @@ class ElasticBuffer {
687690
const bool& async_with_compute_stream,
688691
const bool& allocate_on_comm_stream,
689692
const bool& do_handle_copy, const bool& do_cpu_sync, const bool& do_expand,
690-
const bool& use_tma_aligned_col_major_sf) const {
693+
const bool& use_tma_aligned_col_major_sf,
694+
const std::optional<torch::Tensor>& scatter_to_nvs_out,
695+
const std::optional<torch::Tensor>& scatter_src_metadata) const {
691696
// Check SM count
692697
EP_HOST_ASSERT(num_sms > 0);
693698

@@ -746,6 +751,18 @@ class ElasticBuffer {
746751
topk_weights_ptr = topk_weights->data_ptr<float>();
747752
}
748753

754+
// Optional second per-(t,k) scalar (router-weight gradient), carried alongside topk_weights so a
755+
// single dispatch yields both routing (recv_topk_weights) and this scalar (recv_aux_weights) in the
756+
// same layout, letting combine output d_hidden and d_route_weights in one pass.
757+
float* aux_weights_ptr = nullptr;
758+
if (aux_weights.has_value()) {
759+
const auto [num_tokens_a, num_topk_a] = get_shape<2>(aux_weights.value());
760+
EP_HOST_ASSERT(num_tokens == num_tokens_a and num_topk == num_topk_a);
761+
EP_HOST_ASSERT(aux_weights->is_cuda() and aux_weights->is_contiguous());
762+
EP_HOST_ASSERT(aux_weights->scalar_type() == torch::kFloat);
763+
aux_weights_ptr = aux_weights->data_ptr<float>();
764+
}
765+
749766
// Expert receiving counter
750767
int* cumulative_local_expert_recv_stats_ptr = nullptr;
751768
if (cumulative_local_expert_recv_stats.has_value()) {
@@ -947,6 +964,7 @@ class ElasticBuffer {
947964
EP_HOST_ASSERT(num_sms <= jit::device_runtime->get_num_sms());
948965
launch_dispatch(x.data_ptr(), sf_ptr,
949966
topk_idx.data_ptr<topk_idx_t>(), topk_weights_ptr,
967+
aux_weights_ptr,
950968
copied_topk_idx_ptr,
951969
cumulative_local_expert_recv_stats_ptr,
952970
psum_num_recv_tokens_per_scaleup_rank.data_ptr<int>(),
@@ -977,12 +995,27 @@ class ElasticBuffer {
977995
// Assign these values according to modes
978996
if (cached_mode) {
979997
// Cached mode
980-
// TODO: support to expand for MoE training backward with cached handles from non-expanding forward,
981-
// which requires maintaining the same expanding order between forward and backward
982-
EP_HOST_ASSERT(not do_expand and "Cannot do expand with cached mode");
998+
// Support expand replay for MoE training backward. The fresh expand dispatch that built this
999+
// handle recorded the per-(token,k) expanded-row assignment into recv_src_metadata[:, 2+k]; the
1000+
// cached epilogue reuses that map (passed in via `cached_recv_src_metadata`) instead of
1001+
// re-running the race-dependent atomicAdd, so the replay's expanded layout is row-for-row
1002+
// identical to the forward (see dispatch_copy_epilogue.cuh).
1003+
if (do_expand) {
1004+
EP_HOST_ASSERT(cached_recv_src_metadata.has_value() and
1005+
"cached+expand replay requires cached recv_src_metadata");
1006+
EP_HOST_ASSERT(expert_alignment == 1 and
1007+
"cached+expand replay is implemented for expert_alignment == 1");
1008+
}
9831009
EP_HOST_ASSERT(not do_cpu_sync and "Cannot do CPU sync with cached mode");
9841010
num_recv_tokens = cached_num_recv_tokens.value();
9851011
num_recv_tokens_per_expert_list = cached_num_recv_tokens_per_expert_list.value();
1012+
if (do_expand) {
1013+
// Total expanded rows = sum of per-expert received counts (expert_alignment == 1,
1014+
// so there is no per-expert alignment padding). Matches the fresh do_cpu_sync path.
1015+
num_expanded_tokens = 0;
1016+
for (const auto c : num_recv_tokens_per_expert_list)
1017+
num_expanded_tokens += c;
1018+
}
9861019
} else if (do_cpu_sync) {
9871020
// Non-cached mode with sync
9881021
const auto start_cpu_time = std::chrono::high_resolution_clock::now();
@@ -1046,14 +1079,20 @@ class ElasticBuffer {
10461079
auto recv_sf = std::optional<torch::Tensor>();
10471080
auto recv_topk_idx = std::optional<torch::Tensor>();
10481081
auto recv_topk_weights = std::optional<torch::Tensor>();
1049-
auto recv_src_metadata = torch::empty(
1050-
{num_recv_tokens, num_topk + 2},
1051-
torch::TensorOptions(torch::kCUDA).dtype(torch::kInt));
1082+
auto recv_aux_weights = std::optional<torch::Tensor>(); // per-row router-weight gradient scalar output
1083+
// In cached+expand replay, reuse the handle's recorded metadata (its [:, 2+k] columns hold the
1084+
// forward's expanded-row map, which the epilogue reads back). Otherwise allocate fresh.
1085+
auto recv_src_metadata = (cached_mode and do_expand)
1086+
? cached_recv_src_metadata.value()
1087+
: torch::empty(
1088+
{num_recv_tokens, num_topk + 2},
1089+
torch::TensorOptions(torch::kCUDA).dtype(torch::kInt));
10521090

10531091
// Optional tensors
10541092
void* recv_sf_ptr = nullptr;
10551093
topk_idx_t* recv_topk_idx_ptr = nullptr;
10561094
float* recv_topk_weights_ptr = nullptr;
1095+
float* recv_aux_weights_ptr = nullptr; // per-row router-weight gradient scalar
10571096
int recv_sf_token_stride = 0, recv_sf_hidden_stride = 0;
10581097
if (sf.has_value()) {
10591098
if (not use_tma_aligned_col_major_sf) {
@@ -1077,25 +1116,46 @@ class ElasticBuffer {
10771116
torch::empty({num_allocated_tokens, num_topk}, topk_weights->options());
10781117
recv_topk_weights_ptr = recv_topk_weights->data_ptr<float>();
10791118
}
1119+
// recv_aux_weights only in expand mode (needs the per-row [NvS] scalar)
1120+
if (aux_weights.has_value() and do_expand) {
1121+
recv_aux_weights = torch::empty({num_allocated_tokens}, aux_weights->options());
1122+
recv_aux_weights_ptr = recv_aux_weights->data_ptr<float>();
1123+
}
10801124

10811125
// Process prefix sum, in expanding mode, it is also atomic counters
10821126
if (do_expand) {
1083-
// Slice and exclusive part and do atomic additions into inclusive
1084-
EP_HOST_ASSERT(not cached_mode);
1085-
psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert.slice(0, 0, num_local_experts);
1127+
// For cached+expand the epilogue reuses the recorded expanded-row map (no atomic counters),
1128+
// and the cached psum is already shaped [num_local_experts]. Only the fresh expand path needs
1129+
// the exclusive prefix-sum slice used as atomic counters.
1130+
if (not cached_mode) {
1131+
// Slice the exclusive part and do atomic additions into inclusive
1132+
psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert.slice(0, 0, num_local_experts);
1133+
}
10861134
} else if (not cached_mode) {
10871135
// Slice the inclusive part (and will not be used in the epilogue)
10881136
psum_num_recv_tokens_per_expert = psum_num_recv_tokens_per_expert.slice(0, 1, num_local_experts + 1);
10891137
}
10901138
EP_HOST_ASSERT(psum_num_recv_tokens_per_expert.size(0) == num_local_experts);
10911139

1140+
// Fused combine-backward scatter. When `scatter_to_nvs_out` is given (only meaningful for the
1141+
// non-expand cached backward), the copy epilogue additionally scatters each received row to
1142+
// out_nvs[scatter_src_metadata[i, 2+k]] -- the NvS destinations recorded by the forward expand
1143+
// dispatch -- fusing the Python-side gather/index_copy into this dispatch kernel. `scatter_src_metadata`
1144+
// must be the forward handle's recv_src_metadata ([num_recv, num_topk+2]).
1145+
const bool do_scatter_to_nvs = scatter_to_nvs_out.has_value() and not do_expand;
1146+
void* out_nvs_ptr = scatter_to_nvs_out.has_value() ? scatter_to_nvs_out->data_ptr() : nullptr;
1147+
const int* scatter_src_metadata_ptr =
1148+
scatter_src_metadata.has_value() ? scatter_src_metadata->data_ptr<int>() : nullptr;
1149+
EP_HOST_ASSERT(not do_scatter_to_nvs or scatter_src_metadata_ptr != nullptr);
1150+
10921151
// Launch copy kernels with full SMs
10931152
stream_control_before_epilogue(previous_event_before_epilogue);
10941153
launch_dispatch_copy_epilogue(buffer, workspace,
10951154
psum_num_recv_tokens_per_scaleup_rank.data_ptr<int>(),
10961155
psum_num_recv_tokens_per_expert.data_ptr<int>(),
10971156
recv_x.data_ptr(), recv_sf_ptr,
10981157
recv_topk_idx_ptr, recv_topk_weights_ptr,
1158+
recv_aux_weights_ptr,
10991159
recv_src_metadata.data_ptr<int>(),
11001160
channel_linked_list_ptr,
11011161
num_recv_tokens, num_max_tokens_per_rank,
@@ -1108,6 +1168,7 @@ class ElasticBuffer {
11081168
jit::device_runtime->get_num_smem_bytes(),
11091169
num_channels,
11101170
do_expand, cached_mode,
1171+
do_scatter_to_nvs, scatter_src_metadata_ptr, out_nvs_ptr,
11111172
comm_stream);
11121173

11131174
// Stream control
@@ -1136,6 +1197,7 @@ class ElasticBuffer {
11361197
dst_buffer_slot_idx,
11371198
token_metadata_at_forward,
11381199
channel_linked_list,
1200+
recv_aux_weights, // per-row router-weight gradient scalar in NvS
11391201
event};
11401202
}
11411203

@@ -1186,8 +1248,22 @@ class ElasticBuffer {
11861248

11871249
// Check optional tensors
11881250
if (use_expanded_layout) {
1189-
// Reduction should be done with SwiGLU
1190-
EP_HOST_ASSERT(not topk_weights.has_value());
1251+
// Optionally carry the per-expanded-row router weight gradient (droute_weights_nvs) so combine
1252+
// can reduce it back to droute_weights_sk [S,K] in one pass. The weight is a 1D [NvS] float
1253+
// tensor aligned row-for-row with `x`. Each k-slot's scalar must land in its own output column,
1254+
// so we require expanded-send (allow_multiple_reduction == false) — local reduction would merge
1255+
// distinct k-slots' weights — and a single NVLink domain (the only path wired to deliver
1256+
// per-row weights to the source rank).
1257+
if (topk_weights.has_value()) {
1258+
const auto [num_w] = get_shape<1>(topk_weights.value());
1259+
EP_HOST_ASSERT(num_w == num_tokens);
1260+
EP_HOST_ASSERT(topk_weights->is_cuda() and topk_weights->is_contiguous());
1261+
EP_HOST_ASSERT(topk_weights->scalar_type() == torch::kFloat);
1262+
EP_HOST_ASSERT(not allow_multiple_reduction and
1263+
"expand-combine topk_weights requires allow_multiple_reduction=False");
1264+
EP_HOST_ASSERT(nccl_context->num_scaleout_ranks == 1 and
1265+
"expand-combine topk_weights is implemented for a single NVLink domain only");
1266+
}
11911267
} else if (topk_weights.has_value()) {
11921268
const auto [num_tokens__, num_topk__] = get_shape<2>(topk_weights.value());
11931269
EP_HOST_ASSERT(num_tokens == num_tokens__ and num_topk == num_topk__);

0 commit comments

Comments
 (0)