Repository navigation
[Fix] Keep residual rows and sums consistent across appends and pipeline handoffs - #43454
Merged
Merged
Conversation
ch-wan
requested review from
BBuf,
Edwardf0t1,
Fridge003,
HaiShaw,
JustinTong0323,
Ying1123,
fzyzcjy,
ispobock,
merrymercy,
sogalin,
wisclmy0611 and
zijiexia
as code owners
October 10, 2026 00:47
This was referenced Oct 10, 2026
ch-wan
force-pushed
the
cheng/hot-fix/stage-boundary-rows-and-pp-handoff
branch
from
October 10, 2026 03:23
d2909d4 to
56f5dac
Compare
ch-wan
force-pushed
the
cheng/refactor/stage-boundary-bound-paths
branch
from
October 10, 2026 19:40
1b1a779 to
08e64b9
Compare
Base automatically changed from
cheng/refactor/stage-boundary-bound-paths
to
main
October 10, 2026 19:40
Contributor
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
When an append ends on an attention that always leaves its sum, the next append's first stage rebuilt the attention's incoming boundary from a declaration whose history had been dropped, so it took the attention as the start of the stack and its residual as on full rows. After an FFN that returns the residual on this rank's attention-TP slice (an a2a MoE, or a dense FFN fully data-parallel), that consumer sliced the residual a second time: a size mismatch, or, on a small batch, wrong values without an error. ZAYA1's one-stage appends hit this with an a2a MoE backend and TP > 1. The stack now keeps the boundary each pending stage is bound with and hands it to the stage after it. An attention that follows such an attention now keeps the residual on the slice, as it does on an input-scattered batch. The step for that reduce-scatters over the full TP group, which under attention DP spans ranks holding other sequences, so binding rejects it there instead of summing across them.
…ention An attention that always leaves its sum has no exit, so when a stack ends on such attentions, the next pipeline rank or the final read takes the residual on the rows they ran on: where the FFN before them returned it. An FFN on its own rows (an a2a MoE, or a dense FFN fully data-parallel) returns it on this rank's attention-TP slice, while the next rank rebuilds it on the attention's rows and the final norm pairs it with the attention's full output. That FFN now returns the residual on the attention's rows, as an FFN does when it ends the stack or hands off itself.
On an input-scattered batch, the stage boundary hands a later pipeline rank the attention's partial sum as an owed handle, which the first stage completes. The model read the batch size and device off that handle before the first stage ran, so every later rank failed with AttributeError. It now reads both from positions.
The FFN before a pipeline handoff must exit on the attention's rows. For such an FFN, its incoming boundary rewrote the rows the residual is on while it runs, while its exit was bound from the FFN's own resolution of them, and the two disagreed: - on an input-scattered batch, the entry kept the residual on this rank's attention-TP slice while the exit started from every row, so the handoff sent the slice where the next rank expects every row; - under attention DP with a dense FFN fully data-parallel, when the rank's last FFN followed such a dense layer, binding failed with NotImplementedError. The rewrite is gone: the entry uses the FFN's own resolution, as its exit does, and as the same FFN does when it is not the last before a handoff.
to_pp() sent a statically declared partial sum unreduced, for the next rank's first stage to complete. But the pipeline sends each tensor as one slice per attention-TP rank and all-gathers the slices on the receiving rank, which reassembles only a tensor every attention-TP rank holds alike: a partial sum arrived as rank 0's slice of its partial next to rank 1's slice of its own, and the receiver summed that mixture again. NemotronH split between a Mamba or attention layer and the MLP or MoE after it, and DeepSeek with input-scattered attention and a reduce-scatter boundary reduction, produced wrong outputs under pipeline parallelism with attention TP > 1. With an all-reduce boundary reduction, input-scattered attention summed the received output a second time: the sender had completed it at its exit, and the receiver still declared it. to_pp() now completes every sum before transport, and from_pp() declares none: the receiving stage's entry takes the path for a sum already completed, which it binds next to the one for a sum its producer on the same rank leaves. The declared partial no longer crosses ranks.
Cut a layer stack between two pipeline ranks and check what each rank binds and what the ranks compute. Binding: stacks of two to four stages (an attention that always leaves its sum, a mixer whose exit may leave its sum to an FFN, a dense FFN and a sparse one) are cut at every stage boundary under attention TP2, TP2 with an a2a MoE, DP2 x TP2 with the dense FFN fully data-parallel and TP4 with input-scattered attention, with all-reduce and reduce-scatter boundary reductions. Each rank binds its stages with the other rank's stage next to them as their neighbour. Within each rank, each side of a boundary takes the residual on the rows the other side leaves it on; the sender hands the residual on, and the receiver takes it, on the attention's rows; the stages before the sender's last FFN and after the receiver's first bind as they do uncut; and a cut rejects only what the stack rejects uncut. Under input-scattered attention a rank's stages end on an FFN, as only models whose layers end with one run it. Values: simulated attention-TP ranks run the sender's stages, to_pp(), a transport that sends each tensor as one slice per attention-TP rank and gathers the slices back, from_pp() and the receiver's stages: for a mixer whose sum is left to the FFN after it, an input-scattered FFN with a reduce-scatter or an all-reduce boundary reduction, and an a2a MoE that writes its output into the residual at the handoff. Every rank's output and residual match the uncut run and a closed-form reference, the sender completes what its output owes once, and the receiver sums nothing it receives. A partial sum sent unsummed reaches the receiver as a mixture of the ranks' partials, and a complete one summed again is multiplied by the group size; both change the values.
ch-wan
force-pushed
the
cheng/hot-fix/stage-boundary-rows-and-pp-handoff
branch
from
October 10, 2026 19:40
56f5dac to
c726421
Compare
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.
This PR is part of a stack (oldest at bottom):
Motivation
Five defects in how a stage boundary places the residual and completes sums, each reachable on main:
AttributeErroron every later rank.NotImplementedErrorwhen a rank's last FFN followed such a dense layer.to_pp()sent a declared partial sum unreduced for the next rank to complete. The pipeline sends one slice per attention-TP rank and gathers the slices on the receiver, so the receiver reassembled a mixture of different ranks' partials. Nemotron-H cut between a mixer and the FFN after it, and DeepSeek with input-scattered attention and a reduce-scatter boundary reduction, produced wrong outputs under PP with attention TP > 1; with an all-reduce boundary reduction, input-scattered attention summed the received output a second time.Modifications
to_pp()completes every sum before transport andfrom_pp()declares none; the receiving stage takes the path it already binds for an input whose sum is complete.to_pp(), a transport that sends one slice per attention-TP rank and gathers them back,from_pp()and the receiver on simulated attention-TP ranks, and checks the values against the uncut run, that every owed sum completes once and that the receiver sums nothing.Accuracy Tests
How the stack is checked:
test_aiter_allreduce_fusion_amdcases that fail on NVIDIA on main too; the test files that changed most recently pass on every commit. The fulltest/registered/unitsuite (926 files, one process per file) shows no failure on the stack's head that main does not have.516fd1c1b5unless marked as on the merge candidate; rebasing onto current main had no conflicts, and the CPU checks were run again after it.961bcf481d, H200, asymmetric dummy weights unless noted): DeepSeek-V2-Lite input-scattered PP2 is bit-identical to PP1 (main crashes on it); Nemotron-H cut between a mixer and its FFN differs from PP1 only at the rounding level of the FFN-to-mixer cuts (main: 0.93 and 2.21); Llama, Qwen3-MoE, GPT-OSS and DeepSeek-V2-Lite are bit-identical to main at PP1 and PP2; GSM8K (real DeepSeek-V2-Lite weights, 250 questions, TP4 with attention DP2 and the dense FFN fully data-parallel) gives 0.360 at PP2, 0.364 at PP1 and 0.368 on main at PP1, medians of three runs whose run-to-run spread is 0.012–0.016. The last revision of the stack changed only comments, docstrings, an equivalent condition and explicitly set attributes after these runs; its bound plans are identical.For this PR:
rswith 2+2 and 1+3 layer splits andarwith 2+2: PP2 is bit-identical to PP1 (before: crash at the receiver, then wrong outputs, up to 2.4 in prompt logprob).Speed Tests and Profiling
The sum a PP rank used to leave to the next rank now completes before the handoff (one all-reduce or reduce-scatter over attention TP on the sending rank instead of one on the receiving rank). No other path changes.
Measured on the merge candidate against main (H200, dummy weights, decode with CUDA graph, three runs per tree, a difference counted only if it exceeds the same-tree spread): Qwen3-8B at TP1 and TP2, DeepSeek-V2-Lite at TP2, and DeepSeek-V2-Lite at TP4 with attention DP2, the dense FFN fully data-parallel and PP2 (1+26 layers) show no difference in prefill throughput (4k tokens, batch 1 and 8), decode throughput (batch 1, 16, 64), peak allocated / reserved memory per rank, or steady-state memory. DeepSeek-V2-Lite with input-scattered attention at TP2 PP2 runs only on this stack (main crashes), so it has no baseline.
Checklist
CI States
Latest PR Test (Base): 🚫 Run #38080746857
Latest PR Test (Extra): 🚫 Run #38080746610
Latest PR Test (AMD ROCm 10): ❌ Run #38080746826