Repository navigation
[zero] compute the grad norm over all gradient stores at once - #6469
Merged
Merged
Conversation
With expert parallelism, LowLevelZero keeps dense and expert params in two gradient stores. step() computed a norm per store and added them after the square root (D + E instead of sqrt(D^2 + E^2)), and the expert norm was only reduced over the moe_dp group, so experts held by other EP ranks were never counted. Each EP rank got a different, usually much too small norm, and clipping used it. Follow Megatron-Core: _compute_grad_norm now takes the grads of all stores, sums their p-th powers locally, all-reduces the sum once over _grad_norm_process_groups() and takes the root once. The groups default to a new grad_norm_process_group argument, which is the store's own group when there is only one store. MoeHybridParallelZeroOptimizer passes its dp group, which spans the moe_dp and ep axes. HybridParallelZeroOptimizer adds the tp and pp groups, and its existing duplicate handling (1/tp_size for params not split by TP, 1/num_stages for shared params) moves unchanged into a _grad_norm_weight hook. This also drops its inf branch, which called the parent with shifted arguments, and every rank now joins the collective even without grads. Add test_moe_ep_zero_grad_norm.py (ep=2/4, MoE-only and full training): the norm must be equal on all ranks and match an fp32 single-GPU reference. Update test_zero_optimizer.py, which called the private method with the old signature. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yuxuan-lou
approved these changes
Oct 19, 2026
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.
Related issue
Fixes #6465
Related to #6464 / #6467 (expert grad scaling) and #6466 (expert grad sync with SP + EP). Those are separate bugs and are not touched here.
Background
With
MoeHybridParallelPlugin+ ZeRO and EP > 1, dense and expert parameters live in two ZeRO gradient stores: dense on the dp group, experts on the moe_dp group. The grad norm used for clipping (and returned byget_grad_norm()) was wrong in two ways:LowLevelZeroOptimizer.step()added the norms of the two stores after each was square-rooted, givingD + Einstead ofsqrt(D² + E²).D + E_local, and the value differed across ranks.In a real MoE model the experts carry most of the gradient, so the norm is under-estimated by roughly
1/sqrt(ep)and clipping fails to trigger. Because each rank clips its own ZeRO shard with its own norm, shards of the same parameter are also scaled differently. Reproduction and numbers are in #6465.Changes
The norm is now computed the way Megatron-Core does it (
get_grad_norm_fp32): each rank sums the p-th powers of the gradient shards it owns across all stores, the sum is all-reduced once, and the root is taken once.colossalai/zero/low_level/low_level_optim.py_compute_grad_norm(gradients, norm_type)takes the grads of all stores. The local sum (or max forinf) is all-reduced over_grad_norm_process_groups(), and the root is taken once. Every rank participates even without gradients: the old earlyreturn 0.0skipped the collective._grad_norm_process_groups(): the groups to reduce over. Defaults to the newgrad_norm_process_groupargument._grad_norm_weight(grad): how much this rank's copy of a gradient counts. Defaults to 1.grad_norm_process_group. It defaults to the store's group when there is a single store, so plugins with one store behave as before. It is required whenpg_to_param_listhas several groups.step()calls_compute_grad_normonce per param group with the grads of all stores, instead of adding per-store norms.colossalai/booster/plugin/hybrid_parallel_plugin.py(HybridParallelZeroOptimizer)_compute_grad_normoverride is replaced by the two hooks: tp and pp groups are added to the reduction groups._grad_norm_weight: a param not split by TP counts1/tp_sizeper TP rank, and a param shared by pipeline stages counts1/num_stagesper stage.infbranch that calledsuper()._compute_grad_norm(gradients, norm_type)with the arguments shifted.colossalai/booster/plugin/moe_hybrid_parallel_plugin.py:MoeHybridParallelZeroOptimizerpasses its dp group asgrad_norm_process_group. That group spans the moe_dp and ep axes, so it covers every dense shard and every expert exactly once; no separate ep reduction is needed.tests/test_moe/test_moe_ep_zero_grad_norm.py(4 GPUs, ZeRO-1, bf16, random routing). For ep=2 and ep=4, and for training only the MoE blocks or all parameters, it checks two things:tests/test_shardformer/test_hybrid_parallel_grad_clip_norm/test_zero_optimizer.pycalled the private_compute_grad_norm(dp_group, gradients=...)and is updated to the new signature.Not in this PR:
1/tp_sizeweights to Megatron's "count on TP rank 0 only". The copies are not guaranteed to be bit-identical, so that would change TP training numerics.zero_stage=0path ofHybridParallelPlugin/MoeHybridParallelPlugin, which has its own norm code.User and compatibility impact
max_norm > 0, clipping now uses the correct norm, identical on all ranks, so clipped runs change; this is the fix.get_grad_norm()changes the same way. Without EP (one gradient store) the result is unchanged up to floating-point summation order.LowLevelZeroOptimizer._compute_grad_normdrops itsdp_pgargument.LowLevelZeroOptimizerandHybridParallelZeroOptimizerget an optionalgrad_norm_process_groupargument. A custom subclass that passes several groups inpg_to_param_listmust now also passgrad_norm_process_group; the only one in the repository,MoeHybridParallelZeroOptimizer, is updated.Test environment
nvidia-smiCUDA Version: 13.2)torch.version.cuda: 12.8, NCCL 2.28.9)nvcc --version): 12.8 (V12.8.61)250c903cCI uses torch 2.13 / CUDA 13.0; CI results will be added below.
Test commands and results
All runs:
FAST_TESTunset, 4 GPUs reserved viagpu-run.tests/test_moetest_hybrid_parallel_grad_clip_norm, 4-GPU cases (AMP, naive, ZeRO optimizers with TP/PP)tests/test_zero/test_low_leveltest_grad_acc, fails identically on main, see Known issues)test_low_level_zero_plugin.py,test_3d_plugin.pyNew test, per config (4 GPUs), ZeRO norm / reference norm:
250c903ctest_moe_ep_zero_grad_norm.py: 1 passed on this branch, 1 failed on main. Its tolerance (5e-3) is 4× the largest error with the fix and smaller than the smallest error on main, so the value check and the all-ranks-equal check each catch the bug on their own.The first run of the grad-clip suite also failed
test_zero_optimizer.py::test_grad_clip_norm, because the test called the private_compute_grad_norm(dp_group, gradients=...). After updating the call it passes (counted above). The 6tests/test_moeskips are pre-existing markers.Logs and JUnit XML are kept on the team cluster in
test-results/grad_norm_fix_20261016/.CI (torch 2.13.0+cu130 image
sha256:a7c63f5a…, head4abd20b4):test_low_level_zero_plugin.py)8 9,fast_mode=false(run 38734809131)Layer 3 details from the JUnit reports:
test_moe_ep_zero_grad_norm[4],test_moe_checkpoint::test_mixtral_moe_layer[4],test_low_level_zero_checkpointIOandtest_grad_acc::test_grad_accumulation; the MoE skips are the pre-existing markers.test_grad_clip_normof the AMP, naive and ZeRO optimizers andtest_shard_mixtral::test_mixtral[4].The Draft-only
container smoke (batch 4a)check shows as failed because it was cancelled when the PR was marked ready; batch 4a ran and passed in Layer 2.Known issues
tests/test_zero/test_low_level/test_grad_acc.pyfails locally on torch 2.11, on this branch and on main alike. The reference model is built withDDP(..., bucket_cap_mb=0), which raisesAssertionError: bucket_cap_mb should be set by nowbefore any ZeRO code runs. Not introduced by this PR. It passes in CI on torch 2.13 (batch 8 above).test_grad_clip_norm_3dof the AMP, naive and ZeRO optimizers (3 passed),test_mixtral_3d[8]andtest_deepseek_3d[8](2 passed).skip,xfail,--ignoreor loosened tolerance is added. One existing test is changed only to follow the new private signature.Risk and rollback
_compute_grad_norm(dp_pg, ...)signature would break; none exists in the repository.Documentation and release impact
None: no README, installation, API docs, Docker,
version.txt, PyPI or DockerHub changes. A changelog line: "Fix the ZeRO gradient norm and clipping with expert parallelism; the norm was wrong and differed across ranks (#6465)."