Repository navigation
fix(muon): split the fully-gathered QKV tensor before orthogonalizing under GTP - #7117
Merged
fanshiqing merged 7 commits intoSep 14, 2026
Merged
Conversation
svcnvidia-nemo-ci
marked this pull request as draft
September 8, 2026 03:14
Contributor
|
This PR has been automatically converted to draft because all PRs must start as drafts. When you are ready for review, click Ready for Review to begin the review process. This will:
See the contribution guide for more details. |
fanshiqing
marked this pull request as ready for review
September 8, 2026 03:15
Member
Author
|
/claude review |
Member
Author
|
/ok to test b142e6a |
Closed
…shard
Whether split-QKV ran depended on the parallel layout, so one weight could get a
different Muon update at TP1 and under GTP. The two rules differ by 26% (95% on k
and v), and Newton-Schulz amplifies a 1e-7 input difference ~10^4x, so it does not
wash out: on a 30B MoE run GTP64 trailed its TP1 baseline by a constant 0.09-0.12B
token lag, 22x the measured run-to-run floor. This removes 82% of it.
Cause: `is_qkv` tested `param.shape[0] % sum(qkv_split_shapes)` -- this rank's
shard, not the weight. GTP row-shards dim 0, so the test fails once the GTP degree
stops dividing the query-group count, and the split is dropped on every GTP rank
while TP1, holding all the rows, keeps it. Silent and configuration-dependent,
with only a DEBUG log that never reached a production logger.
Fix:
- decide from the row count across GTP shards (local x gtp_size - pad)
- split after the all-gather; a row shard cuts q/k/v mid-boundary
- take the GTP degree from the optimizer's own group, gated on
is_gtp_weight_remat -- the same signal the split path uses
blockwise and distributed keep the rows sharded, so the split cannot run there.
They fall back to whole-matrix Newton-Schulz -- what they already did before this
fix -- and warn once, so existing jobs on those modes are unchanged. duplicated,
the default, gets the fix.
Tests: tests/unit_tests/generalized_tensor_parallel/test_gtp_muon_qkv.py
Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
fanshiqing
force-pushed
the
shiqingf/fix_qkv_muon_update_with_gtp
branch
from
September 8, 2026 04:42
b142e6a to
886b565
Compare
Member
Author
|
/ok to test 886b565 |
Member
Author
|
/claude review |
Signed-off-by: Shiqing Fan <shiqingf@nvidia.com>
Member
Author
|
/ok to test a6eb95e |
…_muon_update_with_gtp
Member
Author
|
/ok to test 1c72d57 |
FDecaYed
approved these changes
Sep 14, 2026
|
🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA/Megatron-LM/actions/runs/34833388075 |
xuwchen
pushed a commit
to xuwchen/Megatron-LM
that referenced
this pull request
Sep 22, 2026
… under GTP (NVIDIA#7117) Signed-off-by: Shiqing Fan <shiqingf@nvidia.com> (cherry picked from commit 312f867)
xuwchen
pushed a commit
to xuwchen/Megatron-LM
that referenced
this pull request
Sep 22, 2026
… under GTP (NVIDIA#7117) Signed-off-by: Shiqing Fan <shiqingf@nvidia.com> (cherry picked from commit 312f867)
nemo-autobot-origin Bot
pushed a commit
to svcnemo-autobot/Megatron-LM
that referenced
this pull request
Oct 9, 2026
Repairs for failures found by local validation of main slice ab20a60 merged into dev f80139c: - tests/unit_tests/inference: give the prefix-caching _StubEngine the cuda_graph_all_prefills attribute the merged DynamicInferenceEngine reads. - megatron/core/optimizer/layer_wise_optimizer.py: tolerate a pg_collection without a tp group (getattr(..., 'tp', None)). - tests/unit_tests/distributed/mfsdp_v2/profiler_utils.py: recover kernel correlation ids without FunctionEvent.linked_correlation_id, which the dev image's torch does not provide. - megatron/core/parallel_state.py: size dynamic data/context-parallel groups against the replicate DP-CP world (with_gtp_remat=False). - gpt3_mcore_te_tp2_pp2_ep4_mhc_overlap_attn_cudagraph{,_nosplit}: refresh dev H100 golden values after main cd4afff (NVIDIA#5916); lm loss and num-zeros match the values main recorded for this change. - megatron/core/inference/contexts/dynamic_context.py: compute Mamba intermediate-state offsets for every prefill chunk, as main does, so the last block boundary of a multi-chunk prompt is cached (required by main dfe9d04 NVIDIA#5952). - tests/unit_tests/test_training.py: give the dev-only training_log fixtures gtp_weight_remat_size=1, the argument main c5ff22b (NVIDIA#4967) made training_log read when computing the global batch size. - megatron/training/training.py: read args.freeze_all_layers with getattr(..., False) in the config-container model-builder path that main ff6b92a (NVIDIA#5926) added, as the same change already does in _forward_backward_grad_context; dev-only tests pass minimal args. - megatron/core/parallel_state.py: with weight rematerialization off (gtp_remat_size == expert_gtp_remat_size == 1), reuse the existing groups of identical membership for the expert GTP weight-remat group and the EGTP-merged expert tp-ep-pp group instead of creating duplicate NCCL process groups. main c5ff22b (NVIDIA#4967) creates them unconditionally; every NCCL group keeps a watchdog thread until process exit, and the extra groups exhausted the thread limit in the transformer unit-test bucket. - tests/functional_tests golden values: drop non-finite iteration-time samples from the 10 golden-value files this sync changes and set start_step/end_step/step_interval to the remaining steps, the layout main uses (cd4afff NVIDIA#5916, 7e6a045 NVIDIA#5997). tools/check_golden_values.py from main bb5647a (NVIDIA#5989) rejects NaN/inf in changed golden values. The H100 files lose only step 1, which lies outside the iteration-time steady window; the two gpt3_7b memory_speed GB200 files keep their even steps 2-24 at step_interval 2. - tests/test_utils/recipes/gb200/gpt-dynamic-inference.yaml: append the read-only GitLab credential to /root/.netrc with `>>` instead of `| tee -a`, so the token is not echoed to stdout and CI logs. The recipe arrives in dev with main 354b5d2 (NVIDIA#6104); this keeps the file otherwise identical to main. - tests/test_utils/python_scripts/generate_jet_trigger_job.py and .gitlab/stages/0{2,3,4}*.yml: restore main's opt-in error extraction (build_test_script, --enable-error-extraction and the four stage flags from main 06bb399, NVIDIA#5831). The earlier dev sync NVIDIA#7055 dropped them without a recorded reason, so this merge kept dev's copies while importing main's test_ci_triage.py, linear_ci.py and 06.triage.yml (e443bd3 NVIDIA#5881, b842f59 NVIDIA#5957), which depend on them. All four files now match main; 01.build.yml keeps dev's base-image pin. - moe/gpt3_mcore_te_tp2_pp1_te_8experts2parallel_{overlap_grad_reduce_param_gather_groupedGEMM,ddp_average_in_collective}_1node and moe/gpt3_moe_mcore_te_tp4_ep2_etp2_pp2_resume_torch_dist_dist_optimizer_1node: refresh the dev GB200 golden values after main cd4afff (NVIDIA#5916). This merge imports the grad-norm/num-zeros fix but kept dev's pre-fix GB200 goldens, so lm loss (exact) and num-zeros failed on dgx_gb200. The values come from the dgx_gb200 golden-value artifacts of CICD run 37688059691 on the previous head 29cf2c4. lm loss, num-zeros and both memory metrics are bit-identical to the goldens the reverted dev sync 5ccdc55 (NVIDIA#7094) regenerated. iteration-time drops its step-1 NaN (start_step 2), as above. - megatron/core/models/gpt/fine_grained_callables.py: forward config.moe_ncclep_zero_copy from the GPT schedule-plan TransformerLayerNode to ScheduleNode, the contract main NVIDIA#5735 added, so free_input never empties NCCL-EP symmetric-memory inputs. megatron/core/transformer/transformer_config.py: reject moe_ncclep_zero_copy with dev's ep_overlap_use_scheduled_tensor_release (NVIDIA#7062), whose scheduled release would own and empty those persistent shared buffers. Addresses review 5470751474 (finding 1). - megatron/core/optimizer/__init__.py and emerging_optimizers.py: with GTP_remat (main NVIDIA#4967), tag fused QKV weights from their TP-local rows before GTP sharding (param._unsharded_shape) instead of the local shard, which silently disabled dev's split-QKV Muon (NVIDIA#7085); rebuild the GTP_remat row shards and strip their alignment padding before the projection/per-head/global QKV split, then restore this rank's shard. Mirrors main's later fix (NVIDIA#7117), which is outside this slice. Addresses review 5470751474 (finding 2). Signed-off-by: svcnemo-autobot <svcnemo-autobot@nvidia.com>
xuwchen
pushed a commit
to xuwchen/Megatron-LM
that referenced
this pull request
Oct 10, 2026
… under GTP (NVIDIA#7117) Signed-off-by: Shiqing Fan <shiqingf@nvidia.com> (cherry picked from commit 312f867)
This branch was successfully 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?
Muon runs a different update rule on
linear_qkvdepending on whether GTP isenabled, silently. This makes the decision layout-invariant.
Problem
The split-QKV decision tests the local shard row count. Under GTP the weight is
row-sharded, so the same weight takes different paths:
param.shape[0]% 6528Three facts make it matter:
2.9 % of the rows, so joint orthogonalization lets
qdominate them).between layouts by ~10⁴x, and that floor is flat across condition number 1e1–1e6.
logging.DEBUGrecord that production never sees.Measured at 256 GPUs (identical init on both arms), GTP64 vs matched TP1:
training loss +0.173 % — a near-constant 0.09–0.12 B token lag, 4.3x outside
the run-to-run noise floor. GTP64 + Adam on the identical topology shows none, as
expected: Adam is elementwise, amplification ~1x.
Fix
qkv_global_rows():param.shape[0] * gtp_size - pad_length.fall mid-shard, so no rank can split correctly before the gather.
is_gtp_weight_remat-- the same signal the split path usesResult
(4 h from scratch, matched window 4.0–16.8 B, n=31. The residual is negative — GTP
slightly ahead — i.e. an overshoot past zero, not a leftover deficit.)
split partitions the same NS FLOPs rather than adding any, on 8 attention layers.
--muon-no-split-qkvon both arms also lands inside the floor(+0.008 %), confirming the asymmetry was the mechanism. Not the right fix — turning
the split off costs +0.216 % lm loss at TP1, so GTP should gain it, not TP1 lose it.
Supporting measurements
Split vs whole-matrix NS, production settings (16 steps,
polar_express,spectral,extra_scale_factor0.2), same 6528×2688 gradient:NS amplification of a 1e-7 relative input perturbation:
Also eliminated with data before landing here: expert routing, grad-norm inflation,
data assignment, loss/MTP reduction scope, router BF16 quantization, vocab-sized
weights, initialization.
Pre-checks
tools/autoformat.shon my PRtests/unit_tests/generalized_tensor_parallel/test_gtp_muon_qkv.py— 14 cases, allpassing at world 2 and 4. Nothing existing could have caught this:
test_muon_optimizer_qkv_splitsetsparam.is_qkvby hand on one rank, andtest_gtp_muon.py's parity cases carry no qkv weight.test_local_shard_would_have_disabled— the regression test proper; pins the shapethat used to fail,
102 × 64 = 6528.test_split_after_gather_matches_tp1— GTP result == TP1 result, per shard.test_gtp_split_differs_from_whole_matrix— negative control, so a regression tothe old behaviour cannot pass trivially.
test_row_sharded_modes_raise,test_padding_is_excluded,test_decision_is_layout_invariant,test_non_qkv_shape_is_rejected.test_gtp_muon.pypasses identically before and after (6 passed / 8 skipped bothsides; the 8 need an 8-GPU host).
Reviewer notes
blockwise/distributedwithmuon_split_qkv=Truenowraises. Those modes keep the weight row-sharded so no rank holds a q/k/v boundary —
they were silently getting a different update rule.
--muon-no-split-qkvis theopt-out.
qkv_global_rowsreadsis_qkv/qkv_split_shapes(tensor-parallel attrs) andgroup/pad_length(GTP attrs) off the same param. Both are copied at the samesites today; a path copying only one set would compute
global_rowswrongly.mixer.in_proj[12352, 2688] is also fused(
[z,x,B,C,dt]) and GTP-sharded, escaping only viause_muon=False.TODO(deyuf)).