Decouple expert parallelism from context parallelism with MoE parallel folding - #93
Conversation
…l folding setup_device_mesh now builds two device meshes over the same ranks, attn_mesh as (pp, dp, cp) and expt_mesh as (pp, dp, ep), so cp and ep each need only divide the stage size rather than each other and EP may span CP. dp_rank alone decides which data a rank loads, while ep_rank names only the experts a rank hosts. FSDP shards the attn parameters over the flattened dp x cp and the expert parameters over the dp axis of the expert view, both on a plain-sum reduction so that the single divide by the global token count in the training step is the only normalization, which also corrects expert gradients that were ep_size times too large under an average. sharding_strategy gives way to hsdp_replica, splitting both replica groups by the same count.
|
@claude review |
Compactness ReviewNo compactness issues. This is a disciplined refactor: the |
Performance ReviewNo performance regression identified. This is a mesh-relabeling refactor (single One gap, not a blocker: |
Correctness ReviewI traced the gradient and data flow of the folding change against the four validated meshes and found the core correct. Both parameter classes now reduce with a plain sum ( One regression, outside this PR's diff so no inline thread: |
Consistency ReviewThe diff itself is internally consistent: the files this PR rewrote ( However, the rename Removed field
Old DP formula and the "
Old batch-split / num_chunks formula (dropped the
Minor: |
Folding renamed `DistributedCfg.sharding_strategy` to `hsdp_replica` and changed how DP is derived, and several files outside that diff still describe the old contract. The h100-8n8g qwen3-30b-a3b benchmark set `distributed.sharding_strategy = "hsdp"`, which raises `AttributeError` at launch now that `DistributedCfg` is slotted without that field. It becomes `hsdp_replica = 2`, which reproduces the old behavior on that mesh: at 64 ranks with pp=4, cp=1, ep=8, the attention group is dp x cp = 16 and splits into 2 replicas of 8, and the expert group is expt_dp = 2 and splits into 2 replicas of 1, matching what `"hsdp"` built from `_concatenate`. The user guide, the estimate-memory skill, and the capture-nsys-profile skill carried the old `dp = total_gpus / (pp * cp * ep)` formula, the rule that `pp * cp * ep` must divide the world size, and the `num_chunks` divisor that included `ep_size`. Attention now derives `dp = (total_gpus / pp) / cp` and the experts derive `expt_dp = (total_gpus / pp) / ep`, only `pp` has to divide the world size with `cp` and `ep` each dividing the stage, and EP shards experts rather than data so it no longer divides the batch. The worked examples in the user guide are recomputed, and the troubleshooting entry now quotes the error strings the code actually raises. The add-new-model testing reference sized its label tensor by `ep_size * num_chunks`, which `test_dualpipev` now sizes by `dp_size * num_chunks`, and the capture script computed a `dp_size` that no longer matched the framework definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Why
A single
(pp, dp, cp, ep)mesh makesworld_size = pp * dp * cp * ep, so CP and EP multiply and compete for the same ranks. Data parallelism is whatever survives that product, which collapses exactly when both long context and wide expert sharding are wanted at once. On 32 ranks withpp=1, asking forcp=4andep=8leavesdp = 32 / (1 * 4 * 8) = 1, so a mesh that looks reasonable on paper trains with no data parallelism at all.The coupling also reaches the data map. Reading a distinct chunk per rank was keyed to
dp * ep, which makes expert sharding decide which tokens a rank owns. Those are separate concerns: EP names where experts live, and the tokens routed to an expert arrive there through the all-to-all rather than by being read there.Folding factors the same rank space twice instead of once. The
cp=4,ep=8case above becomesdp=8withexpt_dp=4, keeping full data parallelism while the attention and expert sides each get the degree they need.What changes
Attention and the experts now get independent parallelism over the same ranks, following MoE parallel folding section 3.2.
setup_device_meshbuilds two device meshes instead of one.attn_meshis(pp, dp, cp)andexpt_meshis(pp, dp, ep), two factorizations of the sameworld_size // ppranks of one pipeline stage, sodp * cp == ep * expt_dp == world_size // pp. PP is the one axis both views share and it stays outermost in each, which is what makes the two factorizations describe the same rank space.Three properties follow.
cpandepeach need only divide the stage size rather than each other, so EP may span CP or sit inside it. Both the CP group and the EP group remain contiguous rank blocks, so each stays on NVLink independently. Anddp_rankalone decides which data a rank loads, whileep_ranknames only the experts a rank hosts.FSDP shards the routed expert weights over
expt_mesh["dp"]and everything else over the flattenedattn_mesh["dp", "cp"], with a plain-sum gradient reduction on both, so the single divide by the global token count in the training step is the only normalization.DistributedCfg.hsdp_replicaabove 1 splits both groups into that many replicas, sharding within one and all-reducing across them.Correctness validation
Four meshes across two models, every one a PASS. The method is the
validate-correctnessskill: run the base branch twice and the feature branch once, then read the base-vs-feature difference as a ratio against the base-vs-base run-to-run floor. Thresholds are 3x warn and 5x fail. Every run is 32 steps with real routing,benchmark = False, warmup from 1e-6 to 1e-5.pp4-dp8-cp1-ep1-seq2048-bf16pp1-dp8-cp4-ep8-seq2048-bf16pp2-dp8-cp2-ep8-seq4096-bf16pp2-dp4-cp4-ep2-seq4096-bf16What each mesh covers
pp4-cp1-ep1is the arm where the branch is mathematically identical to the base, so its PASS isolates the mesh relabeling from every behavioral change. Its step-1 forward delta of exactly 0.0000 also establishes that the forward is bit-reproducible while the backward is not, which is the premise the whole envelope method rests on.pp1-cp4-ep8is the first folded mesh, with EP spanning CP. The gradient-scale correction changes expert gradients here by a factor ofep_sizeand still does not move the loss, because the fixture optimizes with AdamW, whose updateg / (sqrt(v) + eps)is invariant to a uniform per-parameter rescaling of the gradient. Where the correction does bite is the global gradient norm feedingclip_grad_norm_.pp2-cp2-ep8is the first mesh with PP, CP and EP all greater than one at the same time, and the first to exercise MLA-aware zigzag ring attention, which no GQA run reaches. The two branches also factor it differently: the base getsdp=1and reads data overdp * ep, the feature getsdp=8and reads overdpalone, so equal loss curves across that gap exercise the data-map redesign rather than bypass it.pp2-cp4-ep2inverts the nesting the other three share. Withcp=4andep=2the EP group sits inside a single CP group and the expert gradient reduction spans across CP boundaries. It also carriesexpt_dp=8, four times the largest value anywhere else, so a residual error in the plain-sum reduction would scale up here rather than cancel. The signal lands below the envelope and the feature arm ends 0.0005 from base0 while the two base arms sit 0.0020 apart.Provenance
Every arm below is listed with the git commit that wandb recorded for the process that produced it.
91018ecis the base branch and3203f94is this branch. The check matters because the arms are switched withgit checkout, so a run whose srun is queued when a checkout lands executes the other branch while still writing the log name it was launched under. Each run was also gated on a log holding steps 1 through 32 exactly once, in order, before it was accepted.Project PithTrain/pr-93.
Group
correctness/qwen3-30b-a3b:pp4-dp8-cp1-ep1-seq2048-bf1691018ec91018ec3203f94pp1-dp8-cp4-ep8-seq2048-bf1691018ec91018ec3203f94Group
correctness/deepseek-v2-lite:pp2-dp8-cp2-ep8-seq4096-bf1691018ec91018ec3203f94pp2-dp4-cp4-ep2-seq4096-bf1691018ec91018ec3203f94Mesh selection
Both branches have to be able to express a mesh for it to be testable at all. The base branch derives
dp = world / (pp * cp * ep), sopp * cp * epmust divide the world size. This branch needs onlypp * cpto divide the world size withepdividing the stage. On 32 ranks that intersection rules outpp2-cp4-ep8, which is why the deepseek meshes arepp2-cp2-ep8andpp2-cp4-ep2.One property has no base arm to compare against:
hsdp_replicaabove 1 does not exist on the base branch. Validating it needs a different framing,hsdp_replica=1against2on this branch, which tests self-consistency rather than equivalence to base.Performance
No measurable throughput difference. Peak memory is identical on every mesh.
Method:
validate-performance, force-balanced routing, 8 steps, medians over steps 2 to 8, project PithTrain/pr-93 groupperformance/<model>. Each mesh has four runs.baseandfeatcompare the branches.aa0andaa1are both the base branch, run back to back, and measure what the protocol reports when the true difference is zero.pp4-dp8-cp1-ep1-seq2048-bf16pp1-dp8-cp4-ep8-seq2048-bf16pp2-dp8-cp2-ep8-seq4096-bf16pp2-dp4-cp4-ep2-seq4096-bf16The four feat deltas span -1.83% to -2.35% and are not a speedup.
pp4-dp8-cp1-ep1setscp=1andep=1. Both branches then build identical rank layouts and identical collectives, so the true step-time difference on that mesh is zero. It reports -2.65%, the largest delta in the table. Rerunning that mesh with the arms reversed, feat first and base second, gives +1.15%. Two measurements with a known true value of zero therefore read -2.65% and -1.14%, and all four feat deltas fall inside that bracket.The A/A columns measure a different pairing and are not the bracket for a feat delta. All four A/A pairs are positive, +0.76% to +1.90%, and all five branch comparisons are negative, -1.14% to -2.65%. The A/A arms keep one branch checked out for both runs; a branch comparison runs
git checkoutbetween them. What the A/A arms establish is the size of the noise: a same-code pair moves 1 to 2 percent on its own.This protocol resolves to roughly plus or minus 2.5% at 8 steps. Every number in the table is inside that.
Peak GPU memory is identical to the reported precision on all four meshes, so folding costs nothing in memory. Memory does not fluctuate between runs, so this row is a direct result rather than a measurement inside a noise floor.
Do not read step time off the correctness runs above. Those use real routing, where expert imbalance dominates the variance: within-run scatter reaches a coefficient of variation of 9.5% with step-to-step spreads up to 38%. The same two deepseek meshes measured that way gave -6.2% and +3.2%, against -1.83% and -2.35% here.
Other testing
test_dualpipevagainst a single-device reference at pp=2 ep=2 for cp=1 and cp=2, packed, ragged, and packed with ragged, worst case 3.5e-3 on an expert weight against a 1e-2 threshold. The noise-floor guard now exempts routed expert weights, without which the expert gradients on deepseek-v2-lite all sit below the cutoff and the one test meant to catch an expert gradient-scale error cannot see one.test_muon_fsdpat ep=1, ep=2, and ep=2 with cp=2, exact to 0.00e+00.The operator suite and
tools/tests, 223 pass with 2 failures that reproduce on a clean base branch.