Skip to content

Commit 8eea7a9

Browse files
fix(training): avoid double-counting MTP MoE metric layers (#5795)
Signed-off-by: Oded Ovadia <oded7272@gmail.com> Co-authored-by: gautham-kollu <gkollu@nvidia.com>
1 parent d69c7c4 commit 8eea7a9

2 files changed

Lines changed: 118 additions & 9 deletions

File tree

‎src/megatron/bridge/training/utils/train_utils.py‎

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import torch
2626
import torch.nn as nn
27+
from megatron.core.models.hybrid.hybrid_layer_allocation import Symbols, parse_hybrid_pattern
2728
from megatron.core.num_microbatches_calculator import get_num_microbatches
2829
from megatron.core.tensor_parallel import param_is_not_tensor_parallel_duplicate
2930
from megatron.core.transformer.module import MegatronModule
@@ -594,6 +595,42 @@ def _build_moe_metric_writer(
594595
return _MoeMetricFanoutWriter(tb_writer, comet_logger, mlflow_logger)
595596

596597

598+
def _get_num_moe_layers(model_config: Any) -> int:
599+
"""Count MoE aux-loss contributors for MCore metric averaging."""
600+
num_layers = model_config.num_layers
601+
mtp_num_layers = getattr(model_config, "mtp_num_layers", None) or 0
602+
repeated_mtp = getattr(model_config, "mtp_use_repeated_layer", False)
603+
604+
if getattr(model_config, "is_hybrid_model", False):
605+
pattern = parse_hybrid_pattern(getattr(model_config, "hybrid_layer_pattern", None))
606+
main_moe_layers = (pattern.main_pattern or "").count(Symbols.MOE)
607+
mtp_moe_layers = (pattern.mtp_pattern or "").count(Symbols.MOE)
608+
mtp_depth = pattern.mtp_num_depths
609+
else:
610+
moe_layer_freq = getattr(model_config, "moe_layer_freq", None)
611+
if moe_layer_freq is None:
612+
main_moe_layers = num_layers
613+
last_layer_is_moe = True
614+
elif isinstance(moe_layer_freq, int):
615+
main_moe_layers = sum(i % moe_layer_freq == 0 for i in range(num_layers))
616+
last_layer_is_moe = (num_layers - 1) % moe_layer_freq == 0
617+
elif isinstance(moe_layer_freq, list):
618+
main_moe_layers = sum(moe_layer_freq)
619+
last_layer_is_moe = bool(moe_layer_freq[-1])
620+
else:
621+
raise ValueError(f"Invalid moe_layer_freq: {moe_layer_freq}")
622+
623+
# Non-hybrid MTP copies the final main layer's dense/MoE layout.
624+
mtp_moe_layers = int(last_layer_is_moe and mtp_num_layers > 0)
625+
mtp_depth = mtp_num_layers
626+
627+
# A reused MTP block contributes once; distinct blocks contribute at each depth.
628+
if not repeated_mtp:
629+
mtp_moe_layers *= mtp_depth
630+
631+
return main_moe_layers + mtp_moe_layers
632+
633+
597634
def training_log(
598635
loss_dict: dict[str, torch.Tensor],
599636
total_loss_dict: dict[str, Any],
@@ -986,11 +1023,6 @@ def training_log(
9861023
if getattr(config.model, "moe_z_loss_coeff", None) is not None:
9871024
track_names.append("z_loss")
9881025

989-
if getattr(config.model, "is_hybrid_model", False):
990-
layers = getattr(config.model, "hybrid_layer_pattern", "").count("E")
991-
else:
992-
layers = getattr(config.model, "num_layers", None)
993-
9941026
# Wrap the TB writer so MoE/MTP metrics also reach MLFlow / Comet (issue #2989).
9951027
# No-op when neither logger is configured: the original writer is returned as-is.
9961028
moe_metric_writer = _build_moe_metric_writer(writer, comet_logger, mlflow_logger)
@@ -1003,7 +1035,8 @@ def training_log(
10031035
per_layer_logging=getattr(config.model, "moe_per_layer_logging", False),
10041036
force_initialize=True,
10051037
track_names=track_names,
1006-
num_layers=layers,
1038+
num_layers=config.model.num_layers,
1039+
num_moe_layers=_get_num_moe_layers(config.model),
10071040
moe_layer_freq=getattr(config.model, "moe_layer_freq", None),
10081041
mtp_num_layers=getattr(config.model, "mtp_num_layers", None),
10091042
pg_collection=pg_collection,

‎tests/unit_tests/training/utils/test_train_utils.py‎

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from megatron.bridge.training.state import GlobalState
2929
from megatron.bridge.training.utils.train_utils import (
3030
LinearForLastLayer,
31+
_get_num_moe_layers,
3132
calc_params_l2_norm,
3233
create_value_head_hook,
3334
freeze_moe_router,
@@ -83,6 +84,8 @@ def make_default_model_config():
8384
num_layers=24,
8485
moe_layer_freq=1,
8586
mtp_num_layers=None,
87+
mtp_use_repeated_layer=False,
88+
hybrid_layer_pattern=None,
8689
kv_channels=128,
8790
num_attention_heads=32,
8891
hidden_size=4096,
@@ -111,6 +114,62 @@ def make_default_model_config():
111114
)
112115

113116

117+
@pytest.mark.parametrize(
118+
("num_layers", "mtp_num_layers", "hybrid_pattern", "moe_layer_freq", "repeated_mtp", "expected"),
119+
[
120+
pytest.param(
121+
52,
122+
2,
123+
"MEMEM*EMEMEM*EMEMEM*EMEMEM*EMEMEM*EMEMEMEM*EMEMEMEME/*E/*E",
124+
None,
125+
True,
126+
24,
127+
id="nemotron_3_5",
128+
),
129+
pytest.param(
130+
88,
131+
2,
132+
"MEMEMEM*EMEMEMEM*EMEMEMEM*EMEMEMEMEM*EMEMEMEMEM*EMEMEMEMEM*EMEMEMEMEM*EMEMEMEM*EMEMEMEME/*E/*E",
133+
None,
134+
True,
135+
41,
136+
id="nemotron_3_super",
137+
),
138+
pytest.param(
139+
108,
140+
2,
141+
(
142+
"MEMEMEM*EMEMEM*EMEMEMEM*EMEMEMEM*EMEMEM*EMEMEMEM*EMEMEMEM*"
143+
"EMEMEM*EMEMEMEM*EMEMEMEM*EMEMEM*EMEMEMEM*EMEMEMEME/*E/*E"
144+
),
145+
None,
146+
True,
147+
49,
148+
id="nemotron_3_ultra",
149+
),
150+
pytest.param(61, 1, None, [0] * 3 + [1] * 58, False, 59, id="deepseek_v3"),
151+
pytest.param(45, 0, None, [0] * 3 + [1] * 42, False, 42, id="step_3_7"),
152+
pytest.param(4, 2, "MEME/*E/*E", None, False, 4, id="hybrid_distinct_mtp"),
153+
pytest.param(4, 2, "MEME/**/**", None, False, 2, id="hybrid_dense_mtp"),
154+
pytest.param(4, 2, None, [1, 0, 1, 1], True, 4, id="non_hybrid_repeated_mtp"),
155+
pytest.param(4, 2, None, [1, 0, 1, 1], False, 5, id="non_hybrid_distinct_mtp"),
156+
pytest.param(4, 2, None, [1, 0, 1, 0], False, 2, id="non_hybrid_dense_mtp"),
157+
],
158+
)
159+
def test_get_num_moe_layers(num_layers, mtp_num_layers, hybrid_pattern, moe_layer_freq, repeated_mtp, expected):
160+
"""Test representative architecture and edge-case contributor counts."""
161+
model_config = SimpleNamespace(
162+
num_layers=num_layers,
163+
moe_layer_freq=moe_layer_freq,
164+
mtp_num_layers=mtp_num_layers,
165+
mtp_use_repeated_layer=repeated_mtp,
166+
is_hybrid_model=hybrid_pattern is not None,
167+
hybrid_layer_pattern=hybrid_pattern,
168+
)
169+
170+
assert _get_num_moe_layers(model_config) == expected
171+
172+
114173
class TestTrainingLog:
115174
"""Test suite for the training_log function."""
116175

@@ -991,6 +1050,13 @@ def test_memory_reporting_checkpoint_resume(
9911050
assert result is True
9921051
mock_report_memory.assert_called_once()
9931052

1053+
@pytest.mark.parametrize(
1054+
("model_num_layers", "mtp_num_layers", "hybrid_pattern", "moe_layer_freq", "expected_num_moe_layers"),
1055+
[
1056+
pytest.param(12, None, None, 2, 6, id="non_hybrid"),
1057+
pytest.param(4, 2, "MMME/*E/*E", None, 3, id="hybrid"),
1058+
],
1059+
)
9941060
@mock.patch("megatron.bridge.training.utils.train_utils.get_num_microbatches")
9951061
@mock.patch("megatron.bridge.training.utils.train_utils.reduce_max_stat_across_model_parallel_group")
9961062
@mock.patch("megatron.bridge.training.utils.train_utils.get_world_size_safe")
@@ -1012,6 +1078,11 @@ def test_moe_logging(
10121078
mock_config,
10131079
mock_global_state,
10141080
loss_dict,
1081+
model_num_layers,
1082+
mtp_num_layers,
1083+
hybrid_pattern,
1084+
moe_layer_freq,
1085+
expected_num_moe_layers,
10151086
):
10161087
"""Test MoE (Mixture of Experts) logging when enabled."""
10171088
# Get fresh total_loss_dict for this test
@@ -1030,9 +1101,11 @@ def test_moe_logging(
10301101
mock_config.model.moe_router_load_balancing_type = "aux_loss"
10311102
mock_config.model.moe_z_loss_coeff = 0.1
10321103
mock_config.model.moe_per_layer_logging = True
1033-
mock_config.model.num_layers = 12
1034-
mock_config.model.moe_layer_freq = 2
1035-
mock_config.model.mtp_num_layers = None
1104+
mock_config.model.num_layers = model_num_layers
1105+
mock_config.model.moe_layer_freq = moe_layer_freq
1106+
mock_config.model.mtp_num_layers = mtp_num_layers
1107+
mock_config.model.is_hybrid_model = hybrid_pattern is not None
1108+
mock_config.model.hybrid_layer_pattern = hybrid_pattern
10361109

10371110
training_log(
10381111
loss_dict=loss_dict,
@@ -1056,6 +1129,9 @@ def test_moe_logging(
10561129
call_args = mock_track_moe.call_args
10571130
assert "load_balancing_loss" in call_args.kwargs["track_names"]
10581131
assert "z_loss" in call_args.kwargs["track_names"]
1132+
assert call_args.kwargs["num_layers"] == model_num_layers
1133+
assert call_args.kwargs["num_moe_layers"] == expected_num_moe_layers
1134+
assert call_args.kwargs["mtp_num_layers"] == mtp_num_layers
10591135

10601136
@mock.patch("megatron.bridge.training.utils.train_utils.get_num_microbatches")
10611137
@mock.patch("megatron.bridge.training.utils.train_utils.reduce_max_stat_across_model_parallel_group")

0 commit comments

Comments
 (0)