fix: prevent PP hang in MoE aux-loss metric reduction - #3419
Conversation
|
/ok to test ddda2fd |
|
@yfw can you help review it? |
yfw
left a comment
There was a problem hiding this comment.
Thanks for digging into this — the failure mode you describe is real. force_initialize=True is exactly the right precedent to reach for.
The main thing I'd like to work through with you: against the Megatron-Core this repo currently pins (0.19.0, via 3rdparty/Megatron-Bridge-workspace/Megatron-Bridge/3rdparty/Megatron-LM @ cf2f07d, per uv.lock), the pre-initialization doesn't reach Megatron's tracker — so the hang would still occur. Details in the first inline comment, including a short CPU-only script you can run to check it on your side.
This is a version boundary, not a misreading on your part — I checked the upstream history rather than assuming. Your patch is correct against megatron-core ≤0.17.x, where that accessor returned the live global dict and the entry shape (including reduce_group_has_dp) matched exactly what you wrote. The MoE-logging refactor landed in core_v0.18.0 and turned that accessor into a copy. So this needs a port to the newer API, not a rethink — the diagnosis itself holds up.
For calibration, the preconditions for the hang look like: an MoE model, and a non-zero aux-loss balancing type, and PP>1, and a layout where some PP stage owns no aux-loss-recording MoE layer. No config shipped in this repo combines those (the aux_loss exemplars are PP=1; the MoE+PP ones use "none"), so this is a user-config-reachable bug — consistent with how you found it.
Reviewed with a team of specialized agents; the mechanism finding was independently reproduced by execution three times.
Generated by Claude Code
@yfw Thanks for the thorough review — the mechanism finding is correct, and I've pushed On the megatron-core version questionThe repro ran on NeMo-RL Same minor as this PR's base (
Which means the original patch was inert on the very environment where I reproduced the hang. I had confirmed the root cause (py-spy on the stuck ranks showed the PP What changed in
|
| # | Comment | Fix |
|---|---|---|
| 1 | tracker shim | MoEMetricsTracker.ensure_initialized() — what report(force_initialize=True) calls. Also drops the hardcoded device="cuda" and the stale reduce_group_has_dp field. |
| 2 | third call site | _finish_train_step_body now passes the same kwargs; all three sites symmetric. |
| 3 | track_names |
New get_aux_loss_track_names(model_config) covering load_balancing_loss / seq_load_balancing_loss / global_load_balancing_loss / z_loss, incl. the list form of moe_router_load_balancing_type. |
| 4 | zero-metric regression | Gate is now num_layers is not None and track_names — empty under "none", so nothing is pre-initialized. |
| 5 | tests | Assert against the real get_moe_metrics_tracker().metrics, no stubbing of the deprecated accessor. Verified they fail on ddda2fde and pass on 2df90ac9. |
| 6 | docstring / shadowing | Args: completed per the get_mtp_metrics() convention; loop-local renamed num_tracked_layers. |
One deliberate deviation from the suggested snippet
I used exact matching plus a non-zero coefficient check rather than the substring test in training.py:2689:
"aux_loss" in "seq_aux_loss"isTrue, so a substring test would pre-initializeload_balancing_lossforseq_aux_lossusers — re-creating the zero-metric problem from comment 4.sft.yaml:142anddpo.yaml:157setmoe_router_load_balancing_type: "aux_loss"but never setmoe_aux_loss_coeff(mcore default0.0), so the router returns early and records nothing.
This mirrors MoETopKRouter.get_aux_loss_coeff / is_aux_loss_enabled. Happy to switch to the upstream form if you'd rather stay byte-identical with training.py.
22e8c0a to
52538e2
Compare
|
/ok to test 52538e2 |
|
@yfw @terrykong can you help review it again? |
|
/ok to test 52538e2 |
|
/ok to test e492ecc |
When moe_router_load_balancing_type=aux_loss is enabled with pipeline_model_parallel_size > 1, training could silently hang inside get_moe_metrics(). Root cause: reduce_aux_losses_tracker_across_ranks() runs torch.distributed.all_reduce over the pipeline-parallel group for each name present in the *local* MoE logging tracker. Megatron creates the tracker entry lazily (save_to_aux_losses_tracker only allocates torch.zeros(num_layers) the first time a rank saves a loss). If some PP rank did not save an aux loss this step (e.g. a stage with no MoE layer, or an MTP MoE layer that lives only on the last stage), that rank skips the all_reduce for the name while other PP ranks perform it, so the collective mismatches participants and hangs with no traceback. Fix: mirror Megatron's own track_moe_metrics(force_initialize=True) guard. Before the reduction, pre-initialize the tracker on every rank so each tracked name exists with an equally-sized zero tensor of length (num_layers + mtp_num_layers), matching the size the router uses in save_to_aux_losses_tracker. num_layers/mtp_num_layers are threaded from model_config in both the policy and value Megatron workers. When num_layers is None the behaviour is unchanged, so this is a no-op for all existing call sites. Signed-off-by: dafu-wu <wuchengyi2006@163.com>
Addresses review feedback on the aux-loss pre-initialization added to get_moe_metrics(). The previous approach did not take effect on the pinned megatron-core, and would have introduced a zero-valued metric once it did. Reach the live tracker. get_moe_layer_wise_logging_tracker() is a deprecated shim that rebuilds a dict copy on every call, so writing the pre-initialized entry through it was discarded and reduce_aux_losses_tracker_across_ranks() still skipped the all_reduce -- the hang was not actually prevented. Use MoEMetricsTracker.ensure_initialized() instead, which is what Megatron's own report(force_initialize=True) calls. It also picks the device itself, removing the hardcoded device="cuda", and drops the stale reduce_group_has_dp field (renamed to needs_dp_avg in the MoE logging refactor). Derive track_names from the model config. The router records a distinct name per balancing type (load_balancing_loss, seq_load_balancing_loss, global_load_balancing_loss, z_loss), each driving its own all_reduce, and moe_router_load_balancing_type may be a list. Hardcoding "load_balancing_loss" left seq_aux_loss and global_aux_loss users hanging. get_aux_loss_track_names() matches the balancing type exactly and requires a non-zero coefficient, which mirrors MoETopKRouter.get_aux_loss_coeff/is_aux_loss_enabled: a substring test would treat seq_aux_loss as aux_loss, and configs that name a balancing type without setting moe_aux_loss_coeff leave the router recording nothing. Gate pre-initialization on track_names being non-empty. Workers only check num_moe_experts > 1, not whether load balancing is on. With the default moe_router_load_balancing_type: "none" the tracker stays empty today and the metric is dropped by the falsy guard; an effective pre-init would have started reporting a permanently-zero load_balancing_loss for every MoE config in the repo. Deriving track_names yields an empty list under "none", so nothing is pre-initialized. Also pass the pre-init arguments at the third call site, _finish_train_step_body (single-controller / split-API path), which was missed and has no MoE coverage in the nightly suite; document the new parameters in the Args block; and rename the loop-local num_layers to num_tracked_layers so it no longer shadows the parameter. Tests assert against the real get_moe_metrics_tracker().metrics rather than stubbing the deprecated accessor -- a stubbed plain dict is live, so such a test would pass while production stayed a no-op. Signed-off-by: dafu-wu <wuchengyi2006@163.com>
e492ecc to
1857253
Compare
|
@yfw Thanks for approving the PR! Could you also approve and run the four pending workflows? I don’t have permission to approve them. |
|
/ok to test b43bbf5 |
What does this PR do ?
Fixes a silent, no-traceback hang in
get_moe_metrics()when training aMoE model with
moe_router_load_balancing_type=aux_lossandpipeline_model_parallel_size > 1.Symptom
With
aux_lossload balancing enabled and PP > 1, training progresses for anumber of steps and then hangs: no exception, no OOM, no exit — GPUs go idle
and the process never returns. The same recipe with
moe_router_load_balancing_type=noneruns indefinitely without issue.Root cause
get_moe_metrics()callsreduce_aux_losses_tracker_across_ranks(), which runstorch.distributed.all_reduce(values, group=pp_group)for each name presentin the local MoE logging tracker:
The tracker entry is created lazily —
save_to_aux_losses_trackeronlyallocates
torch.zeros(num_layers)the first time a rank actually saves a loss.So if some PP rank did not save an aux loss this step — e.g. a pipeline
stage that holds no MoE layer, or an MTP MoE layer that lives only on the last
stage — that rank has no key for
load_balancing_loss, skips theall_reducefor that name, while other PP ranks perform it. The collective then mismatches
its participants across the PP group and hangs.
This is exactly the situation Megatron's own
track_moe_metricsguards againstvia
force_initialize=True; NeMo-RL callsreduce_aux_losses_tracker_across_ranks()directly and bypassed that guard.
Fix
Mirror the
force_initialize=Truebehaviour insideget_moe_metrics():before the reduction, pre-initialize the tracker on every rank so each
tracked name exists with an equally-sized zero tensor of length
num_layers + mtp_num_layers— the same size the router uses insave_to_aux_losses_tracker.num_layers/mtp_num_layersare threaded frommodel_configin both the policy and value Megatron workers.When
num_layers is None(all existing/other call sites), the behaviour iscompletely unchanged — this is a no-op guarded by an opt-in argument.
Usage
No config change required. The guard activates automatically for MoE models
(
num_moe_experts > 1) in the Megatron policy/value workers, which now passnum_layers/mtp_num_layersfrommodel_config. After this change,moe_router_load_balancing_type=aux_losswith PP > 1 no longer hangs.Files
nemo_rl/models/megatron/common.py— pre-initialize tracker inget_moe_metrics()nemo_rl/models/policy/workers/megatron_policy_worker.py— passnum_layers/mtp_num_layersnemo_rl/models/value/workers/megatron_value_worker.py— passnum_layers/mtp_num_layersBefore your PR is "Ready for review"
Additional Information
Repro parallelism where the hang was observed:
PP=4, TP=8, EP=8on aNemotron-3-style 120B MoE SFT run; also reproducible with MTP enabled (the MTP
MoE layer lives only on the last PP stage, guaranteeing the tracker-key
asymmetry across the PP group).