Repository navigation
fix(peft)!: make expert LoRA exact under suppressed tensor-parallel communication - #5635
Open
tigist-far wants to merge 3 commits into
Open
tigist-far wants to merge 3 commits into
tigist-far wants to merge 3 commits into
Conversation
Expert LoRA computes wrong results whenever Megatron suppresses a linear's own tensor-parallel collectives: at expert_tensor_parallel_size > 1 the default adapter's fc2 delta is built from unsummed partials and over-counted by the dispatcher's expert-TP reduce, its fc1 dL/dA silently loses every cross-rank term under an exact forward, the grouped per-expert adapter's bare all_gather severs autograd entirely, and shared-expert fc2 deltas under moe_shared_expert_overlap (plain TP > 1) are counted once per rank — same defect in MultiLoRALinear. Three test layers, each defect demonstrated as xfail(strict=True) so this commit lands green while recording the failures: torch-only closed-form algebra with negative controls; a CPU suite driving the real adapters through real MCore explicit_expert_comm suppression (one-rank gloo init plus mock group sizes; only the collectives are faked, and each fake asserts it received exactly the tensor the real collective would see); and a two-rank TP=2 x ETP=2 real-NCCL suite with its L0 launch script. Verified on 2x H100: every demonstration fails on its intended assertion against this tree (5 passed / 8 xfailed CPU; 4 passed / 7 xfailed distributed). The algebra files and the defect analysis originate from Mohammad Taufeeque's earlier investigation; the files are ported verbatim apart from docstrings. Co-authored-by: Taufeeque <9taufeeque9@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Tigist Diriba <tigist@far.ai>
…ommunication Complete the adapter arithmetic that explicit_expert_comm suppression leaves undone, using autograd-visible collectives on the groups each adapter already carries: reduce_from(copy_to(x)) over expert-TP for the row-parallel-input case (all-reduce on both passes), copy_to(x) for the column-parallel case (backward-only all-reduce restoring the suppressed dL/dz sum), and a differentiable last-dim all-gather (backward reduce-scatter-sum) replacing the grouped adapter's autograd-invisible bare all_gather in all three backends. On the output side, keep linear_out unsharded-gather-free and zero-embed the local shard at its rank offset, so the downstream sum (the dispatcher over expert-TP; SharedExpertMLP.post_forward_comm over dense TP) counts the delta exactly once with no scaling factor; MultiLoRALinear gets the same treatment on suppressed-comm row-parallel bases. Where no correct completion exists without new machinery, refuse loudly instead of training wrong: DoRA on expert or suppressed-comm TP-sharded linears, SharedOuterGroupedExpertAdapter at ETP>1, expert-adapter dropout at ETP>1 (per-rank RNG on dispatcher-replicated tokens), and LoRALinear.weight on grouped adapter classes. BREAKING CHANGE: the four configurations above now raise NotImplementedError where they previously ran with silently wrong numerics (or an accidental AttributeError). Checkpoint format, weight layout, and export paths are unchanged. The strict-xfail markers from the previous commit are deleted here; every demonstration now asserts for real. Verified on 2x H100 (--runxfail, TF32 off): CPU suite 13/13, distributed 11/11 on both ranks, refusal and wiring tests 26 passed, full peft regression 419 passed. At ETP=1 every new branch short-circuits at group size 1; peak memory is unchanged by construction (the embed buffer replaces the removed gather buffer). The fix design originates from Mohammad Taufeeque's earlier implementation of the same defect class. Co-authored-by: Taufeeque <9taufeeque9@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Tigist Diriba <tigist@far.ai>
Author
|
Restructured the description for readability, top-down: what breaks and for whom, then the mechanism, then a new Why this design section covering the constraints and the alternatives considered, then the breaking changes. Verification, performance, and the per-file changelog moved into collapsed sections; known limits stay visible. The per-path defect enumeration now lives only in #5634 rather than being duplicated here. No code, test, or technical-claim changes — description only. |
cuichenx
self-requested a review
September 18, 2026 22:02
This branch has not been deployed
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.
What does this PR do ?
LoRA adapters on MoE expert layers silently compute the wrong numbers whenever
expert_tensor_parallel_size > 1, and one variant is wrong at plainTP > 1whenmoe_shared_expert_overlapis enabled. Nothing errors and nothing warns: training runs to completion and the adapter is wrong. In one path the adapter trains nothing at all, because its output is detached from autograd. Every shipped recipe pinsexpert_tensor_parallel_size=1, so stock configurations are unaffected; custom MoE topologies are not.This PR makes all four affected paths exact, and makes the four configurations for which no correct implementation exists yet fail loudly instead of silently.
How it breaks
A LoRA adapter is a branch added to a base linear's output, so it must deliver its result in the same parallel state as the base: replicated, a shard, or a partial awaiting a sum. At
expert_tensor_parallel_size > 1Megatron-Core setsexplicit_expert_command deletes the expert linear's own collectives, because the MoE token dispatcher performs that sum later incombine. The base's output state therefore changes from replicated to partial. The adapters were never told: they kept gathering a full-width delta that the dispatcher then counted once per rank, and internally they consumed their own un-summed partial as though it were complete.Four defect families follow from this, spanning
ParallelLinearAdapter(both fc1 and fc2 cases),GroupedExpertLinearAdapter(all three backends), and the shared-expert/MultiLoRALinearpath undermoe_shared_expert_overlap. Per-path detail, measured failure signatures, and a two-GPU repro: #5634.What the fix does
Two small, autograd-visible completions, applied uniformly:
linear_in: for the fc2 case,reduce_from(copy_to(x))over the expert-TP group — an all-reduce in both directions, the exact adjoint pair for a sum whose result every rank consumes differently. For the fc1 case,copy_to(x)— forward identity whose backward all-reduce restores the suppresseddL/dzsum. The grouped class's fc1 gather becomesall_gather_last_dim_from_tensor_parallel_region(backward = reduce-scatter-sum, the true adjoint).linear_outkeepsgather_output=Falseand the local shard is placed at this rank's offset in a full-width buffer of zeros. Each output element then has exactly one non-zero contributor, so the downstream sum (dispatcher over ETP;post_forward_commover TP) reconstructsB @ zexactly once.All groups are passed explicitly (
group=) — the MCore default resolves the dense TP group, which is silently wrong for expert adapters.Why this design
Constraints. A candidate fix is a choice of shard axis for the adapter's
AandB, plus whatever collectives make it correct. Three constraints bind:(alpha/r)·B@A, because serving merges it into the base weight. "Close" is not a weaker form of correct — it is a delta no merged weight can represent.AandBmust keep the shard axis the stored format already records for it. That axis is checkpoint metadata (ShardedTensor.axis_fragmentations) and is what lets a TP=4 checkpoint load at TP=2; the export path pairs each tensor with the matchingRowParallelMapping/ColumnParallelMappingso its per-rank pieces reassemble into the right full tensor. This is a function of the base flavor, not a single axis per matrix:Ais sharded onrunder a column-parallel base and ond_inunder a row-parallel one, whileBis always sharded ond_out. The constraint forbids changing the axis for a given flavor, not the flavor-dependence itself, which is already part of the format. Moving an axis would break existing adapter checkpoints, mis-assemble exportedlora_A/lora_B, and silently change the merge.Alternatives considered, for the row-parallel case where the base's collectives are suppressed:
[tokens, rank]; output side is a local pad, no traffic[tokens, d_out]Ptimes. Identical code, opposite correctness, which is why it went unnoticed1/P(alpha/r)·B@Ano longer merges unless export applies the same factor, and the constant is silently wrong if the summing group ever changesBon the rank axisB's shard axis fromd_outtor, violating constraint 3. Worth revisiting only alongside a checkpoint-format changed_in / rtimes more traffic (roughly 100-700x)Recommendation. Zero-embedding is the only option that is exact, state-matched, and checkpoint-compatible. It also needs no compensating factor anywhere: each output element has exactly one non-zero contributor, so the sum the dispatcher already performs reconstructs
B @ zonce, and the embed's adjoint is a plain slice, so gradients need no compensation either. That is what keeps stored weights and the merge path untouched. The one alternative that beats it on cost (reduce-scatter) does so by roughly one small collective per adapted expert linear and requires a checkpoint migration to adopt; atr=32that is not a trade worth making, but it is the right thing to reconsider if the adapter checkpoint format is ever revised.Where a correct completion does not exist without new machinery, the PR refuses loudly instead of computing silently wrong results.
What this breaks (the reason for the
!)Four configurations that previously ran while silently training wrong now raise
NotImplementedError, each naming the offending config key and its workaround. This is the intended blast radius of the!in the title.Configs that previously ran while silently training wrong now raise
NotImplementedErrorwith messages naming the offending config:moe_shared_expert_overlap).experts_shared_outer_loras=Trueat ETP>1 (previously crashed with an unrelated shape error at first forward; export path has independent ETP>1 defects).dropout > 0at ETP>1.LoRALinear.weighton grouped adapter classes (previouslyAttributeError).Each refusal has a practical workaround today (ETP=1, the default
share_expert_adapters=True,dropout=0.0, the export-side merge path); lifting them properly is tracked in #5636.No checkpoint format, weight layout, or export-path change: shard axes and
sharded_state_dictfactories are untouched, and merges still compute exactlyα/r·B@Awith no parallelism factor — the zero-embed design is what keeps stored weights compensation-free.Changelog
Per-file changes
peft/utils.py—ParallelLinearAdapter: add_reduce_expert_low_rank_activationand_embed_row_parallel_shard; constructor predicates (_expert_row_parallel,_shared_expert_row_parallel) forcelin_out_gather_output=Falseon the two row-parallel-base cases.peft/utils.py—GroupedExpertLinearAdapter: replace the autograd-invisible_gather_along_last_dimwith_complete_low_rank_activation/_embed_expert_row_parallel_shardacross all three backends (per-expert,grouped_mm, TE-fp8);.contiguous()normalization before the activation-offloading tag; error text for the missing-group case kept byte-identical.peft/utils.py— construction-timeNotImplementedErrorforSharedOuterGroupedExpertAdapterat ETP>1, and for expert-adapterdropout > 0at ETP>1 on both adapter classes (dropout draws per-rank RNG on tokens the dispatcher replicates across ETP — not expressible by any merged weight).peft/dora.py—DoRA.transformrefuses MoE expert linears and suppressed-comm TP-sharded bases (its dense-group adapter and per-rank magnitude norm do not compose); replicated bases still transform.peft/lora_layers.py—LoRALinear.weightrefuses grouped adapter classes (previously an accidentalAttributeError; the merge path for grouped adapters is the export-side materializer).peft/multi_lora_layers.py—MultiLoRALinear: same zero-embed fix for the suppressed-comm row-parallel case (its mid-GEMM reduce was already correct).test_expert_row_parallel_adapter_algebra.pyandtest_expert_column_parallel_adapter_backward_algebra.py(torch-only closed-form ground truth with negative controls);test_expert_lora_etp.py(CPU suite driving the real adapters through real MCoreexplicit_expert_commsuppression via a one-rank gloo init + mock group sizes, only the collectives faked — each fake asserts it received exactly the tensor the real collective would see);test_expert_lora_etp_distributed.py(two-rank TP=2×ETP=2 real-NCCL suite, TF32 disabled for exact tolerances) +L0_Launch_peft_expert_etp_distributed.sh; DoRA refusal tests;LoRALinear.weightrefusal tests; MultiLoRA behavior-pinning tests; one existing test updated to pass apg_collection(the completion needs a resolvable group).GitHub Actions CI
See the CI section in the Contributing doc for how to trigger the CI. A Nvidia developer will need to approve and trigger the CI for external contributors.
Before your PR is "Ready for review"
Pre checks:
megatron.core.tensor_parallel.mappings(a hard dependency); the existing TEsafe_import_fromguards inpeft/utils.pyare untouchedIf you haven't finished some of the above items you can still open "Draft" PR.
Additional Information
Verification (A/B on 2×H100, real NCCL)
Verification (A/B on 2×H100, real NCCL)
Commit 1 checked out alone reproduces every defect: each demonstration is
xfail(strict=True)and was verified to fail on its intended assertion (not a harness error) against the pre-fix tree; commit 2 deletes the markers, so a strict XPASS gate mechanically proves each demonstrated defect is fixed.tests/unit_tests/peft/regressionMeasured defect signatures pre-fix (matching the closed-form predictions): fc2 combined delta = 2× a concatenation of unsummed partials; fc1
dL/dAmissing all cross-rank terms under an exact forward; grouped fc2 outputrequires_grad == False; shared overlap and MultiLoRA deltas exactly 2× at TP=2 (MultiLoRA greatest rel. diff 1.27).Performance and memory — no added communication at ETP=1; two output-side paths got faster
Performance and memory
Measured on the same 2x H100 setup, adapter in isolation per the A/B protocol: 50 CUDA-event-timed iterations after 10 warmup, even token counts so the pad path never fires, bf16 (the per-expert-backend row is fp32, that backend's trigger), 4096 tokens, hidden 2048, ffn 2688, rank 32, 4 local experts, TP=2 x ETP=2:
Reading the table — with the essential caveat that the broken arm is not same-work (it computes wrong values, partial gradients, or no gradients at all, which is the bug):
post_forward_comm/the dispatcher already performs — zero added communication.[tokens, rank]tensors and are latency-bound, and the whole adapter remains a sub-millisecond fraction of an MoE layer.dL/dA).At ETP=1 — every shipped recipe — all new branches short-circuit at group size 1, leaving one predicate check per adapter forward; dense LoRA paths are otherwise untouched. Not measured: end-to-end step-time at ETP>1 (the adapter is a small fraction of an MoE layer, but no full-model A/B was run), and a bitwise pre/post A/B at ETP=1 (asserted structurally; backed by the 419-test regression run rather than a bit comparison).
Known limits / residual risk
alltoalldispatcher (combine_preprocessreduce-scatter overexpt_tp). For the flex/DeepEP dispatcher the combine's sum semantics are inferred from the DeepEP contract, not verified — worth a maintainer's confirmation before relying on ETP>1 withflex.expert_tensor_parallel_size=1, so these defects are latent for stock configs; they fire on custom ETP>1 topologies (where we found them) and — for the shared-expert/MultiLoRA cases — at plain TP>1 withmoe_shared_expert_overlap.Fixes #5634. Related to #5636.