Skip to content

Commit f2e8ef0

Browse files
committed
solving PP=1 + EP>1 issue, ignoring moe_freq and using only pattern for moe layers count
1 parent ef50ab0 commit f2e8ef0

3 files changed

Lines changed: 112 additions & 2 deletions

File tree

src/megatron/bridge/models/conversion/model_bridge.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,11 @@ def _megatron_local_name_to_global(
266266
vp_stage: Optional[int] = None,
267267
) -> str:
268268
"""Adjust layer number and expert number from local to global numbering."""
269+
# ``layer_module`` is resolved lazily and shared between the PP and EP
270+
# branches. It stays ``None`` on the EP-only path (PP=1, EP>1) until the EP
271+
# branch resolves it, so it must never be read before being assigned.
272+
layer_module = None
273+
269274
# PP
270275
pp_group = _get_pp_group(models)
271276
if "layers." in param_name and get_pg_size(pp_group) > 1:
@@ -290,7 +295,25 @@ def _megatron_local_name_to_global(
290295
is_expert_param = (is_grouped_expert_param or is_local_expert_param) and ".adapter." not in param_name
291296
ep_group = _get_ep_group(models) if is_expert_param else None
292297
if is_expert_param and ep_group is not None and get_pg_size(ep_group) > 1:
293-
num_experts_per_rank = layer_module.mlp.num_local_experts # per-layer, heterogeneous-safe
298+
# Resolve the layer module independently of the PP branch above: on the
299+
# EP-only path (PP=1, EP>1) that branch never runs, so ``layer_module``
300+
# is still ``None`` here. Prefer the per-layer module count
301+
# (heterogeneous-safe); fall back to the config-derived count when the
302+
# model isn't available (e.g. name-only conversions in tests).
303+
if layer_module is None:
304+
layer_match = re.match(r"^(.+?\.layers\.\d+)", param_name)
305+
if models is not None and layer_match is not None:
306+
try:
307+
_, layer_module = get_module_and_param_from_name(
308+
models=models, param_name=layer_match.group(1), vp_stage=vp_stage
309+
)
310+
except (ValueError, AttributeError):
311+
layer_module = None
312+
313+
if isinstance(layer_module, MegatronModule):
314+
num_experts_per_rank = layer_module.mlp.num_local_experts # per-layer, heterogeneous-safe
315+
else:
316+
num_experts_per_rank = config.num_moe_experts // get_pg_size(ep_group)
294317

295318
def _update_grouped_expert_number(param_name: str, param_type: str) -> str:
296319
"""Update expert number from local to global for weight or bias parameters."""

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,15 @@ def training_log(
10441044
num_layers = getattr(config.model, "num_layers", None)
10451045
moe_layer_freq = getattr(config.model, "moe_layer_freq", None)
10461046
pattern = getattr(config.model, "hybrid_layer_pattern", "") or ""
1047-
if moe_layer_freq is None and pattern: # per-layer averaging denominator
1047+
# For hybrid models the MoE layers are defined by the hybrid pattern
1048+
# ("E" == MoE layer), not by ``moe_layer_freq`` — which defaults to 1 on
1049+
# the inherited TransformerConfig/MambaModelProvider path and would
1050+
# otherwise make ``_count_moe_layers`` average aux/z-loss over *every*
1051+
# layer instead of only the MoE ones. The pattern is authoritative
1052+
# whenever present, so always derive the per-layer averaging denominator
1053+
# from it. ``num_layers`` stays at full depth for the tracker storage
1054+
# size (the NCCL-deadlock fix).
1055+
if pattern:
10481056
moe_layer_freq = [1 if c == "E" else 0 for c in pattern]
10491057

10501058
# Wrap the TB writer so MoE/MTP metrics also reach MLFlow / Comet (issue #2989).

tests/unit_tests/training/utils/test_train_utils.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,6 +1134,85 @@ def test_moe_logging_without_z_loss(
11341134
assert "z_loss" not in track_names
11351135
assert len(track_names) == 1
11361136

1137+
@mock.patch("megatron.bridge.training.utils.train_utils.get_num_microbatches")
1138+
@mock.patch("megatron.bridge.training.utils.train_utils.reduce_max_stat_across_model_parallel_group")
1139+
@mock.patch("megatron.bridge.training.utils.train_utils.get_world_size_safe")
1140+
@mock.patch("megatron.bridge.training.utils.train_utils.print_rank_last")
1141+
@mock.patch("megatron.bridge.training.utils.train_utils.track_moe_metrics")
1142+
@mock.patch("megatron.bridge.training.utils.train_utils.report_runtime")
1143+
@mock.patch("megatron.bridge.training.utils.train_utils.report_throughput")
1144+
@mock.patch("megatron.bridge.training.utils.train_utils.report_l2_norm_grad")
1145+
def test_moe_logging_hybrid_pattern_default_freq(
1146+
self,
1147+
mock_report_l2_norm_grad,
1148+
mock_report_throughput,
1149+
mock_report_runtime,
1150+
mock_track_moe,
1151+
mock_print_rank_last,
1152+
mock_get_world_size,
1153+
mock_reduce_lr,
1154+
mock_get_microbatches,
1155+
mock_config,
1156+
mock_global_state,
1157+
loss_dict,
1158+
):
1159+
"""Hybrid-MoE models must derive the averaging denominator from the pattern.
1160+
1161+
Regression: recipes such as ``nemotron_3_nano`` set a ``hybrid_layer_pattern``
1162+
with a handful of ``E`` (MoE) layers but leave ``moe_layer_freq`` at the
1163+
inherited default (``1``). ``num_layers`` must stay at full depth for the
1164+
tracker storage size (the NCCL-deadlock fix), while ``moe_layer_freq`` is
1165+
passed as the per-layer binary MoE mask derived from the pattern so aux/z-loss
1166+
are averaged over only the MoE layers.
1167+
"""
1168+
total_loss_dict = self.get_fresh_total_loss_dict()
1169+
1170+
mock_report_l2_norm_grad.return_value = {}
1171+
mock_report_throughput.return_value = {}
1172+
mock_report_runtime.return_value = {}
1173+
mock_get_microbatches.return_value = 8
1174+
mock_reduce_lr.return_value = 1e-4
1175+
mock_get_world_size.return_value = 32
1176+
1177+
# Hybrid Mamba+MoE config: pattern places MoE ("E") on 2 of 8 layers,
1178+
# while moe_layer_freq is the inherited default (1, not None).
1179+
pattern = "M-M*E-M*E-"
1180+
mock_config.model.num_moe_experts = 8
1181+
mock_config.model.moe_router_load_balancing_type = "aux_loss"
1182+
mock_config.model.moe_z_loss_coeff = None
1183+
mock_config.model.moe_per_layer_logging = True
1184+
mock_config.model.num_layers = len(pattern)
1185+
mock_config.model.moe_layer_freq = 1
1186+
mock_config.model.mtp_num_layers = None
1187+
mock_config.model.is_hybrid_model = True
1188+
mock_config.model.hybrid_layer_pattern = pattern
1189+
1190+
training_log(
1191+
loss_dict=loss_dict,
1192+
total_loss_dict=total_loss_dict,
1193+
learning_rate=1e-4,
1194+
decoupled_learning_rate=None,
1195+
loss_scale=1024.0,
1196+
report_memory_flag=False,
1197+
skipped_iter=0,
1198+
grad_norm=2.5,
1199+
params_norm=15.2,
1200+
num_zeros_in_grad=0,
1201+
config=mock_config,
1202+
global_state=mock_global_state,
1203+
history_wct=None,
1204+
model=None,
1205+
)
1206+
1207+
mock_track_moe.assert_called_once()
1208+
call_args = mock_track_moe.call_args
1209+
# Storage size stays at full depth so PP tensor sizes match (deadlock fix).
1210+
assert call_args.kwargs["num_layers"] == len(pattern)
1211+
# Averaging denominator is the per-layer MoE mask, summing to only the "E"s.
1212+
moe_layer_freq = call_args.kwargs["moe_layer_freq"]
1213+
assert moe_layer_freq == [1 if c == "E" else 0 for c in pattern]
1214+
assert sum(moe_layer_freq) == pattern.count("E") == 2
1215+
11371216
@mock.patch("megatron.bridge.training.utils.train_utils.get_num_microbatches")
11381217
@mock.patch("megatron.bridge.training.utils.train_utils.reduce_max_stat_across_model_parallel_group")
11391218
@mock.patch("megatron.bridge.training.utils.train_utils.get_world_size_safe")

0 commit comments

Comments
 (0)