bugfix: fix qwen3.5 dp empty shard crash across dense and moe. - #2079
Open
Enguikong wants to merge 1 commit into
Open
bugfix: fix qwen3.5 dp empty shard crash across dense and moe.#2079Enguikong wants to merge 1 commit into
Enguikong wants to merge 1 commit into
Conversation
Under dp>1, an empty dp_rank is padded with a single fake token by
worker_impl so all ranks stay in the collective. Qwen3.5 hybrid GDN
layers did not know about this fake shard, so they read undefined
GDN state tensors and crashed at gt_Scalar / batch_input_builder.
Fix in four places, +49/-8, no behavior change on dp=1:
- worker_impl.cpp: early-return prepare_input_params_for_linear_attention
when q_max_seq_len == 0 or num_sequences == 0.
- npu_torch/qwen3_gated_delta_net_base.cpp: early-return
Qwen3GatedDeltaNetBaseImpl::forward on attn_metadata.is_dummy with
zeros_like output, placed before FlashComm1 sequence gather.
- llm/qwen3_next_hybrid_base.h: pass device_ into
AttentionMetadataBuilder::build so the is_dummy branch can construct
fake slot_mapping / q_seq_lens / kv_seq_lens tensors.
- npu_torch/fused_moe.cpp: add anonymous-namespace helper
make_moe_dp_token_nums that normalizes zero entries in
dp_global_token_nums to one; use it in FusedMoEImpl::forward so
parallel_state::gather does not fail CHECK_EQ on padded ranks.
Verified on Ascend 910C for Qwen3.5 4B / 9B / 27B (dense) and
Qwen3.5-35B-A3B (moe, ep=1) at dp=2, dp=4, world in {2, 4}; 128/128
concurrent HTTP 200 with zero crash on all configurations.
Style verified manually with clang-format 20.1.6 (matches
.pre-commit-config.yaml pinned version); commit made with --no-verify
because pre-commit hook mirror could not build its virtualenv on this
dev machine.
Enguikong
requested review from
Clement-Wang26,
DongheJin,
DragonFive,
JimHsiung,
Kang-Meng,
RobbieLeung,
XuZhang99,
liujinguang0125,
liutongxuan,
walsonyang,
xiao-yu-chen,
yingxudeng,
yq33victor and
zhang-minchao
as code owners
July 30, 2026 03:27
There was a problem hiding this comment.
Pull request overview
This PR fixes crashes when running Qwen3.5 dense + MoE under Data Parallel (dp_size > 1) where scheduling can produce an empty shard on one DP replica. The fix ensures dummy/padded shards don’t read undefined GDN state and don’t violate collective invariants during forward.
Changes:
- Pass
device_intoAttentionMetadataBuilder::build()so itsis_dummybranch can materialize placeholder tensors on the correct device. - Add early-return guards for dummy/empty-shard inputs in linear-attention preprocessing and in Qwen3.5 GDN forward.
- Normalize
dp_global_token_nums(0→1) for MoE DP gather to keep gather preconditions consistent with the fake-token padding behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| xllm/models/llm/qwen3_next_hybrid_base.h | Passes device_ into AttentionMetadataBuilder::build() so dummy attention metadata is created on the right device. |
| xllm/core/runtime/worker_impl.cpp | Skips linear-attention preprocessing for dummy/empty shards to avoid reading undefined GDN tensors. |
| xllm/core/layers/npu_torch/qwen3_gated_delta_net_base.cpp | Early-returns zeros_like(hidden_states) when attn_metadata.is_dummy to avoid undefined-state reads and skip FlashComm1 gather. |
| xllm/core/layers/npu_torch/fused_moe.cpp | Normalizes MoE DP token counts (0→1) before parallel_state::gather() to prevent empty-shard collective failures. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
1171
to
+1174
| const int64_t dp_rank = parallel_args_.dp_local_process_group_->rank(); | ||
| auto start = | ||
| std::accumulate(dp_tokens.begin(), dp_tokens.begin() + dp_rank, 0); | ||
| auto end = start + dp_tokens[dp_rank]; | ||
| auto start = std::accumulate( | ||
| moe_dp_token_nums.begin(), moe_dp_token_nums.begin() + dp_rank, 0); | ||
| auto end = start + moe_dp_token_nums[dp_rank]; |
Comment on lines
+54
to
+55
| // apply the same 0->1 normalization so the token list stays consistent across | ||
| // the DP group; the fake row is later dropped by the per-rank slice back. |
Comment on lines
+56
to
+65
| std::vector<int32_t> make_moe_dp_token_nums( | ||
| const std::vector<int32_t>& dp_global_token_nums) { | ||
| std::vector<int32_t> normalized = dp_global_token_nums; | ||
| for (int32_t& token_num : normalized) { | ||
| if (token_num == 0) { | ||
| token_num = 1; | ||
| } | ||
| } | ||
| return normalized; | ||
| } |
Enguikong
marked this pull request as draft
July 30, 2026 05:52
Enguikong
marked this pull request as ready for review
July 31, 2026 08:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Under Data Parallel (
dp_size > 1), request scheduling across DP replicas can leave a replica with zero sequences in a forward step. To keep HCCL collectives in lockstep,worker_implpads such an empty shard with a single fake token. This works for plain-attention models, but Qwen3.5's hybrid Gated Delta Net (GDN) path does not account for the fake shard — its GDN state tensors (kv_cache_tokens_nums,linear_state_ids, …) are left undefined and then read during forward, crashing the worker.Crash signature (the rank that got the empty shard dies first; rank 0 cascades):
Full crash stack (rank with empty shard)
The MoE path fails separately:
FusedMoEImpl::forwardfeedsdp_global_token_nums(still0for the padded rank) intoparallel_state::gather, which failsCHECK_EQ(local_rows, token_num_list[rank]).The balanced-DP path (all ranks busy) never triggers this, so single-request smoke tests pass and the bug only surfaces under concurrency.
Fix — four places, +49/-8, no behavior change on
dp=1(every guard is gated on the empty-shard /is_dummycondition, which never fires whendp_size == 1):worker_impl.cpp: early-returnprepare_input_params_for_linear_attentionwhenq_max_seq_len == 0 || num_sequences == 0, before reading undefined GDN tensors. MirrorsAttentionMetadataBuilder::build'sis_dummybranch.npu_torch/qwen3_gated_delta_net_base.cpp: early-returnQwen3GatedDeltaNetBaseImpl::forwardonattn_metadata.is_dummy, returningtorch::zeros_like(hidden_states). Placed before the FlashComm1 sequence gather so dummy shards never enter the collective. Mirrors the existingis_dummyearly-return innpu_torch/attention.cpp.llm/qwen3_next_hybrid_base.h: passdevice_intoAttentionMetadataBuilder::buildso theis_dummybranch can construct fakeslot_mapping/q_seq_lens/kv_seq_lenstensors on the right device. Uses the builder's existingdeviceparameter (defaultstd::nullopt); other callers unaffected.npu_torch/fused_moe.cpp: add file-local helpermake_moe_dp_token_numsthat normalizes zero entries indp_global_token_numsto one. All ranks apply the same0→1normalization so the token list is consistent across the DP group; the fake row is dropped by the existing per-rank slice-back. OnlyFusedMoEImpl::forwardis touched (theep>1paths are out of scope).Relationship to #2024: #2024 (
preserve raw dp token counts for empty shards) addresses the same empty-shard root cause but on a different path — it addsraw_dp_global_token_numsforDpEpPadding(EP padding / lm-head output compaction / reduce-scatter). This PR is complementary: it fixes the GDN forward, the linear-attention preprocessing, and theFusedMoEImpl::forwardDP gather, none of which #2024 touches. On currentmain,FusedMoEImpl::forwardstill gathers with the un-normalizeddp_global_token_nums, so this fix is still required.Verification (Ascend 910C):
dp=24-concurrent = 4/4 empty-reply +c10::Error; with fix → 4/4 HTTP 200, both ranks alive.max_tokens=64, warmup: 128/128 HTTP 200, zero crash on 4B/9B/27B (dense) and 35B-A3B (MoE,ep=1), atdp=2anddp=4,world ∈ {2,4}.dp=21.39x /dp=41.76x; 9B 1.33x / 1.65x; 27B(tp=2)dp=21.41x.dp=43.53x (88% efficiency); zero regression across 9 runs.xllm-service): instances link, 4/4 HTTP 200, zero crash.Scope / limitations:
--backend=llm). Qwen3.5 VL does not route through the DP encoder path —dp=1recommended for VL until addressed separately.FusedMoEImpl::forwardis patched; expert-parallel (ep>1) paths unchanged.dp=1/tp=4vsdp=2/tp=2(single-card OOM prevents a tp-aligned baseline), so its wall-clock is "correct under DP+MoE", not a clean DP-only speedup.Related Issues
N/A — no existing tracking issue. Complementary to #2024 (see Description).
Change Type
Pull Request Checklist
PR Title and Commit Messages
<type>: <subject>.Pre-commit Checks
pre-commitby runningpip install pre-commitor an equivalent command.pre-commit install.pre-commit run --all-filesand fixed any reported issues.Self Review
.agents/skills/code-review/references/custom-code-style.md, especially code written or assisted by AI.mainbranch.Build and Test Coverage
python setup.py build testhas passed on a CUDA machine.python setup.py build testhas passed on an NPU machine.python setup.py build testhas passed on an MLU machine.Reviewer Notes
dp=1production path is unaffected: every guard is gated on the empty-shard /is_dummycondition, which never fires whendp_size == 1.is_dummyhandling innpu_torch/attention.cpprather than introducing a new pattern.worker_impl.cpp:~1623, a different function).make_moe_dp_token_nums+ the two early-return guards; (2) VL DP is out of scope, documented asdp=1-recommended.